@xmanrui/dsh-im 4.36.1 → 4.38.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 +14 -2
- package/README.md +13 -1
- package/assets/logo-plugin-message-link-gradient.webp +0 -0
- package/lib/client.js +417 -189
- package/lib/index.js +317 -316
- package/package.json +2 -2
- package/plugin-src/client/bot-general-settings.js +120 -0
- package/plugin-src/client/channels/dingtalk/api.js +3 -1
- package/plugin-src/client/channels/feishu/api.js +6 -2
- package/plugin-src/client/channels/qq/api.js +3 -1
- package/plugin-src/client/channels/shared/token-api.js +3 -1
- package/plugin-src/client/channels/wecom/api.js +3 -1
- package/plugin-src/client/channels/wecom-app/api.js +3 -1
- package/plugin-src/client/channels/weixin/api.js +3 -1
- package/plugin-src/client/channels/whatsapp/api.js +3 -1
- package/plugin-src/client/delivery-settings.js +5 -1
- package/plugin-src/client/i18n.js +14 -0
- package/plugin-src/client/styles.js +19 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +11 -0
- package/plugin-src/host/channels/email/rpc.mjs +1 -0
- package/plugin-src/host/channels/feishu/production.mjs +1 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +31 -0
- package/plugin-src/host/channels/imessage/rpc.mjs +1 -0
- package/plugin-src/host/channels/matrix/rpc.mjs +10 -0
- package/plugin-src/host/channels/qq/rpc.mjs +11 -0
- package/plugin-src/host/channels/shared/rpc.mjs +9 -0
- package/plugin-src/host/channels/slack/rpc.mjs +10 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +11 -0
- package/plugin-src/host/channels/wecom-app/rpc.mjs +10 -0
- package/plugin-src/host/channels/weixin/rpc.mjs +11 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +11 -0
- package/scripts/verify-package.mjs +11 -6
- package/src/channels/dingtalk/dingtalk-bridge.mjs +31 -2
- package/src/channels/dingtalk/dingtalk-menu.mjs +1 -1
- package/src/channels/dingtalk/dingtalk-runtime.mjs +2 -1
- package/src/channels/discord/discord-runtime.mjs +3 -1
- package/src/channels/feishu/bridge.mjs +91 -15
- package/src/channels/feishu/feishu-cards.mjs +16 -11
- package/src/channels/feishu/feishu-runtime.mjs +10 -1
- package/src/channels/feishu/multi-bot-controller.mjs +18 -0
- package/src/channels/feishu/plugin-config-store.mjs +2 -1
- package/src/channels/feishu/slash-command-panel.mjs +1 -0
- package/src/channels/feishu/step-push-mode.mjs +21 -0
- package/src/channels/matrix/matrix-crypto.mjs +19 -5
- package/src/channels/qq/qq-bridge.mjs +40 -2
- package/src/channels/qq/qq-menu.mjs +2 -2
- package/src/channels/shared/bot-workspace-store.mjs +69 -1
- package/src/channels/shared/busy-message-dispatcher.mjs +94 -0
- package/src/channels/shared/clear-command.mjs +85 -0
- package/src/channels/shared/command-catalog.mjs +3 -0
- package/src/channels/shared/command-permission.mjs +2 -0
- package/src/channels/shared/control-command.mjs +20 -11
- package/src/channels/shared/harness-approval.mjs +102 -2
- package/src/channels/shared/i18n-en/feishu.mjs +6 -4
- package/src/channels/shared/i18n-en/qq.mjs +2 -1
- package/src/channels/shared/i18n-en/shared-b.mjs +27 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +4 -0
- package/src/channels/shared/message-mode.mjs +19 -0
- package/src/channels/shared/text-harness-bridge.mjs +48 -5
- package/src/channels/wecom/wecom-bridge.mjs +26 -2
- package/src/channels/wecom/wecom-cards.mjs +1 -1
- package/src/channels/wecom-app/wecom-app-bridge.mjs +26 -2
- package/src/channels/weixin/weixin-bridge.mjs +22 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BusyMessageDispatcher, canAutomaticallySteer } from '../shared/busy-message-dispatcher.mjs';
|
|
1
2
|
import { isPermissionCommand, runPermissionCommand } from '../shared/permission-command.mjs';
|
|
2
3
|
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import QRCode from 'qrcode';
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
isBatchInputCommand,
|
|
44
45
|
} from '../shared/batch-input.mjs';
|
|
45
46
|
import { runCompactCommand } from '../shared/compact-command.mjs';
|
|
47
|
+
import { isClearCommand, runClearCommand } from '../shared/clear-command.mjs';
|
|
46
48
|
import { isHistoryCommand, runHistoryCommand } from '../shared/history-command.mjs';
|
|
47
49
|
import {
|
|
48
50
|
isControlCommand,
|
|
@@ -120,6 +122,7 @@ import {
|
|
|
120
122
|
import {
|
|
121
123
|
FEISHU_STEP_PUSH_MODES,
|
|
122
124
|
normalizeFeishuStepPushMode,
|
|
125
|
+
normalizeFeishuStepCardPanels,
|
|
123
126
|
} from './step-push-mode.mjs';
|
|
124
127
|
import { FeishuLiveCot } from './live-cot.mjs';
|
|
125
128
|
|
|
@@ -178,7 +181,7 @@ const REPAIR_URL_HOSTS = new Set([
|
|
|
178
181
|
|
|
179
182
|
const ARCHIVED_COMMAND = /^\/archived(?:\s+(on|off))?$/i;
|
|
180
183
|
/** Matches fast card commands that should not be queued behind a running task. */
|
|
181
|
-
const CARD_COMMAND = /^\/(?:m(?:enu)?|new|help|status|compact|(?:sessionlist|sessions)(?:\s|$)|workspacelist|workspaces|wsl|watchlist|archived(?:\s+(on|off))?)$/i;
|
|
184
|
+
const CARD_COMMAND = /^\/(?:m(?:enu)?|new|help|status|compact|clear|(?:sessionlist|sessions)(?:\s|$)|workspacelist|workspaces|wsl|watchlist|archived(?:\s+(on|off))?)$/i;
|
|
182
185
|
|
|
183
186
|
// ── Step push (分步直推) limits ──────────────────────────────────────────────
|
|
184
187
|
/** Feishu allows 5 msg/s per chat; step push stays well below that. */
|
|
@@ -617,6 +620,7 @@ export class FeishuHarnessBridge {
|
|
|
617
620
|
#contextEnhancement;
|
|
618
621
|
#accessPolicy;
|
|
619
622
|
#queues = new Map();
|
|
623
|
+
#busyMessages = new BusyMessageDispatcher();
|
|
620
624
|
#batchInputs = new BatchInputManager();
|
|
621
625
|
#pendingInteractions = new Map();
|
|
622
626
|
#interactionKeys = new Map();
|
|
@@ -677,6 +681,7 @@ export class FeishuHarnessBridge {
|
|
|
677
681
|
#stepPush = false;
|
|
678
682
|
/** Step push presentation: discrete posts, a CardKit card, or native live CoT. */
|
|
679
683
|
#stepPushMode = FEISHU_STEP_PUSH_MODES.POST;
|
|
684
|
+
#stepCardPanels;
|
|
680
685
|
/** Per-conversation step push state: key → { lastSentAt, count, breakerLogged }. */
|
|
681
686
|
#stepPushSendState = new Map();
|
|
682
687
|
/** Live streaming step cards: key → per-turn card state (streaming_card mode). */
|
|
@@ -742,6 +747,7 @@ export class FeishuHarnessBridge {
|
|
|
742
747
|
mentionTopicReply = true,
|
|
743
748
|
stepPush = false,
|
|
744
749
|
stepPushMode = FEISHU_STEP_PUSH_MODES.POST,
|
|
750
|
+
stepCardPanels,
|
|
745
751
|
stepPushClock = null,
|
|
746
752
|
repair,
|
|
747
753
|
repairPollIntervalMs = REPAIR_POLL_INTERVAL_MS,
|
|
@@ -802,6 +808,7 @@ export class FeishuHarnessBridge {
|
|
|
802
808
|
this.#mentionTopicReply = mentionTopicReply !== false;
|
|
803
809
|
this.#stepPush = stepPush === true;
|
|
804
810
|
this.#stepPushMode = normalizeFeishuStepPushMode(stepPushMode);
|
|
811
|
+
this.#stepCardPanels = normalizeFeishuStepCardPanels(stepCardPanels);
|
|
805
812
|
this.#stepPushClock = stepPushClock ?? DEFAULT_STEP_PUSH_CLOCK;
|
|
806
813
|
this.#voice = voice ?? null;
|
|
807
814
|
this.#repair = repair ?? null;
|
|
@@ -898,6 +905,10 @@ export class FeishuHarnessBridge {
|
|
|
898
905
|
this.#stepPushMode = normalizeFeishuStepPushMode(value);
|
|
899
906
|
}
|
|
900
907
|
|
|
908
|
+
setStepCardPanels(value) {
|
|
909
|
+
this.#stepCardPanels = normalizeFeishuStepCardPanels(value);
|
|
910
|
+
}
|
|
911
|
+
|
|
901
912
|
setVoice(voice) {
|
|
902
913
|
this.#voice = voice ?? null;
|
|
903
914
|
}
|
|
@@ -1388,7 +1399,27 @@ export class FeishuHarnessBridge {
|
|
|
1388
1399
|
this.#interactionTasks.add(current);
|
|
1389
1400
|
return current;
|
|
1390
1401
|
}
|
|
1391
|
-
return this.#
|
|
1402
|
+
return this.#busyMessages.dispatch({
|
|
1403
|
+
key, messageId, text: commandText,
|
|
1404
|
+
eligible: () => event.message.message_type === 'text' && !hasImages && !hasFiles
|
|
1405
|
+
&& !hasReplyReference(commandMessage) && !isFeishuLocalCommand(commandText)
|
|
1406
|
+
&& !(NUMBER_REPLY.test(commandText) && this.#menus.has(key))
|
|
1407
|
+
&& canAutomaticallySteer(this.#accessPolicy, conversationType, senderOpenId(event)),
|
|
1408
|
+
harness: this.#harness, state: this.#state, status: this.#status,
|
|
1409
|
+
signal: this.#signal, control: { owner: this, key }, logger: this.#logger,
|
|
1410
|
+
acceptedMessageIds: this.#acceptedMessageIds,
|
|
1411
|
+
pendingInteraction: () => this.#pendingInteractions.has(key) || this.#approvals.hasPending(key),
|
|
1412
|
+
enhancement: {
|
|
1413
|
+
snapshot: this.#acceptedMessageIds.get(messageId),
|
|
1414
|
+
source: () => ({ channel: 'feishu', senderId: senderOpenId(event),
|
|
1415
|
+
chatId: event.message.chat_id, threadId: event.message.thread_id }),
|
|
1416
|
+
},
|
|
1417
|
+
isQueued: () => this.#queues.has(key),
|
|
1418
|
+
enqueue: (options) => this.#enqueueMessage(event, messageId, key, processingReaction, options),
|
|
1419
|
+
send: (reply) => this.#send(event.message.chat_id, reply, { replyTo: messageId }),
|
|
1420
|
+
onSteered: () => this.#finishReaction(messageId, processingReaction, 'DONE'),
|
|
1421
|
+
onError: (error) => this.#handleMessageFailure(event, messageId, processingReaction, error),
|
|
1422
|
+
});
|
|
1392
1423
|
}
|
|
1393
1424
|
|
|
1394
1425
|
#finishBatchResult(event, messageId, processingReaction, result) {
|
|
@@ -1545,6 +1576,7 @@ export class FeishuHarnessBridge {
|
|
|
1545
1576
|
}
|
|
1546
1577
|
|
|
1547
1578
|
async waitForIdle() {
|
|
1579
|
+
await this.#busyMessages.whenIdle();
|
|
1548
1580
|
// Drain to a fixed point: awaited work can register compensation or
|
|
1549
1581
|
// another serialized tail before it settles.
|
|
1550
1582
|
for (;;) {
|
|
@@ -1600,6 +1632,7 @@ export class FeishuHarnessBridge {
|
|
|
1600
1632
|
deferredDelivery: this.#deferred,
|
|
1601
1633
|
hasFiles: hasInboundFiles(message),
|
|
1602
1634
|
pendingInteraction: this.#hasPendingInteraction(key),
|
|
1635
|
+
busy: this.#queues.has(key),
|
|
1603
1636
|
control: { owner: this, key },
|
|
1604
1637
|
enhancement: captureContextEnhancementSource(
|
|
1605
1638
|
this.#contextEnhancement,
|
|
@@ -1691,7 +1724,8 @@ export class FeishuHarnessBridge {
|
|
|
1691
1724
|
// 命令识别对 text 与纯文本 post 一视同仁:post 富文本若仅含单个
|
|
1692
1725
|
// 文本段落(如复制粘贴的 /new),同样按命令处理;带图片/文件不认。
|
|
1693
1726
|
// accept() 侧已用 nonEmptyString(content) 判定,两侧保持一致。
|
|
1694
|
-
const
|
|
1727
|
+
const rawCommandText = text ? text.trim() : '';
|
|
1728
|
+
const commandText = !hasImages && !hasFiles && text ? rawCommandText : null;
|
|
1695
1729
|
if (!text && !hasImages && !hasFiles && !hasReply) {
|
|
1696
1730
|
// An "@bot" with nothing else carries no instruction to parse, and the
|
|
1697
1731
|
// menu card is what the reader is reaching for — answer it the way `/m`
|
|
@@ -1751,6 +1785,19 @@ export class FeishuHarnessBridge {
|
|
|
1751
1785
|
}
|
|
1752
1786
|
return;
|
|
1753
1787
|
}
|
|
1788
|
+
if (isClearCommand(rawCommandText)) {
|
|
1789
|
+
const clearCommand = await runClearCommand(rawCommandText, this.#harness, this.#state, key, {
|
|
1790
|
+
signal: this.#signal,
|
|
1791
|
+
hasImages,
|
|
1792
|
+
hasFiles,
|
|
1793
|
+
pendingInteraction: this.#hasPendingInteraction(key),
|
|
1794
|
+
busy: this.#queues.has(key),
|
|
1795
|
+
});
|
|
1796
|
+
if (clearCommand) {
|
|
1797
|
+
await this.#send(event.message.chat_id, clearCommand.message, { replyTo: event.message.message_id });
|
|
1798
|
+
}
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1754
1801
|
if (SESSION_LIST_PREFIX.test(commandText)) {
|
|
1755
1802
|
const argument = commandText.replace(/^\/(?:sessionlist|sessions)/i, '').trim();
|
|
1756
1803
|
const request = parseSessionListArgument(argument);
|
|
@@ -2551,6 +2598,7 @@ export class FeishuHarnessBridge {
|
|
|
2551
2598
|
actor = null,
|
|
2552
2599
|
menuContext = null,
|
|
2553
2600
|
newSessionActionId = null,
|
|
2601
|
+
fromMessageQueue = false,
|
|
2554
2602
|
}) {
|
|
2555
2603
|
// Confirmations triggered by a card interaction stay anchored to the
|
|
2556
2604
|
// card's message so they land inside the same Feishu topic.
|
|
@@ -2704,7 +2752,9 @@ export class FeishuHarnessBridge {
|
|
|
2704
2752
|
await this.#handleNewSessionMenu({ key, chatId, messageId, menuContext, newSessionActionId });
|
|
2705
2753
|
return;
|
|
2706
2754
|
}
|
|
2707
|
-
|
|
2755
|
+
// A queued menu pick runs after earlier messages; its own unfinished
|
|
2756
|
+
// queue (including later messages) must not prevent the reset.
|
|
2757
|
+
if ((!fromMessageQueue && this.#queues.has(key)) || this.#hasPendingInteraction(key)) {
|
|
2708
2758
|
await reply(t('当前任务仍在运行,请先停止任务或等待任务完成后再开启新会话。'));
|
|
2709
2759
|
return;
|
|
2710
2760
|
}
|
|
@@ -2739,6 +2789,10 @@ export class FeishuHarnessBridge {
|
|
|
2739
2789
|
await this.#handleCompact(key, chatId, messageId);
|
|
2740
2790
|
return;
|
|
2741
2791
|
}
|
|
2792
|
+
if (action === 'clear') {
|
|
2793
|
+
await this.#handleClear(key, chatId, messageId);
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2742
2796
|
if (action === 'stop') {
|
|
2743
2797
|
await this.#handleStop(key, chatId, messageId);
|
|
2744
2798
|
return;
|
|
@@ -2927,7 +2981,9 @@ export class FeishuHarnessBridge {
|
|
|
2927
2981
|
await this.#handleRepairCommand(event, '/repair');
|
|
2928
2982
|
return;
|
|
2929
2983
|
}
|
|
2930
|
-
await this.#handleCardAction(action, {
|
|
2984
|
+
await this.#handleCardAction(action, {
|
|
2985
|
+
chatId, key, messageId: replyTo, fromMessageQueue: action === 'new',
|
|
2986
|
+
});
|
|
2931
2987
|
return;
|
|
2932
2988
|
}
|
|
2933
2989
|
if (menu.kind === 'sessions') {
|
|
@@ -3509,6 +3565,22 @@ export class FeishuHarnessBridge {
|
|
|
3509
3565
|
}
|
|
3510
3566
|
}
|
|
3511
3567
|
|
|
3568
|
+
/** Run /clear while retaining the current Session binding. */
|
|
3569
|
+
async #handleClear(key, chatId, replyTo = null) {
|
|
3570
|
+
try {
|
|
3571
|
+
const result = await runClearCommand(
|
|
3572
|
+
'/clear', this.#harness, this.#state, key, {
|
|
3573
|
+
signal: this.#signal,
|
|
3574
|
+
pendingInteraction: this.#hasPendingInteraction(key),
|
|
3575
|
+
busy: this.#queues.has(key),
|
|
3576
|
+
},
|
|
3577
|
+
);
|
|
3578
|
+
await this.#send(chatId, result?.message || t('清空当前会话上下文失败,当前会话未修改,请稍后重试。'), { replyTo });
|
|
3579
|
+
} catch (error) {
|
|
3580
|
+
await this.#sendFailure(chatId, error, { logLabel: 'clear', replyTo });
|
|
3581
|
+
}
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3512
3584
|
/**
|
|
3513
3585
|
* Stop the running task in the bound session (mirrors `/stop`).
|
|
3514
3586
|
*/
|
|
@@ -4781,7 +4853,7 @@ export class FeishuHarnessBridge {
|
|
|
4781
4853
|
* their lifecycle is owned by the ask path, not the mirror recovery.
|
|
4782
4854
|
* lastContent keeps stepStreamCard's raw JSON string (single-encoded).
|
|
4783
4855
|
*/
|
|
4784
|
-
async #persistMirrorState(card, liveBlocks, status) {
|
|
4856
|
+
async #persistMirrorState(card, liveBlocks, status, stepCardPanels) {
|
|
4785
4857
|
const sessionId = card.sessionSyncSessionId;
|
|
4786
4858
|
if (!sessionId || typeof this.#state.setMirror !== 'function') return;
|
|
4787
4859
|
await this.#state.setMirror(card.sessionSyncKey, {
|
|
@@ -4789,7 +4861,7 @@ export class FeishuHarnessBridge {
|
|
|
4789
4861
|
chatId: card.chatId,
|
|
4790
4862
|
cardIds: [...card.cardIds],
|
|
4791
4863
|
claimedAt: Date.now(),
|
|
4792
|
-
lastContent: stepStreamCard(liveBlocks, { status }),
|
|
4864
|
+
lastContent: stepStreamCard(liveBlocks, { status, stepCardPanels }),
|
|
4793
4865
|
blocks: structuredClone(card.blocks), answerStart: card.answerStart, answerEnd: card.answerEnd,
|
|
4794
4866
|
lastSeq: this.#sessionSyncTurns.get(card.sessionSyncKey)?.lastSeq ?? -1,
|
|
4795
4867
|
pendingStep: this.#sessionSyncTurns.get(card.sessionSyncKey)?.pendingStep ?? null,
|
|
@@ -4797,7 +4869,7 @@ export class FeishuHarnessBridge {
|
|
|
4797
4869
|
}
|
|
4798
4870
|
|
|
4799
4871
|
/** Sync only this interaction's cards; earlier interactions remain sealed. */
|
|
4800
|
-
async #syncStepCardChunks(card, chunks, status) {
|
|
4872
|
+
async #syncStepCardChunks(card, chunks, status, stepCardPanels) {
|
|
4801
4873
|
const answerVersion = card.answerVersion ?? 0;
|
|
4802
4874
|
const activeIds = card.cardIds.slice(card.activeCardStart);
|
|
4803
4875
|
const groups = chunks.length ? [...chunks] : [[]];
|
|
@@ -4810,6 +4882,7 @@ export class FeishuHarnessBridge {
|
|
|
4810
4882
|
// payload halfway through this render or be marked as already delivered.
|
|
4811
4883
|
const contents = groups.map((blocks, index) => stepStreamCard(blocks, {
|
|
4812
4884
|
status: index === groups.length - 1 ? status : 'sealed',
|
|
4885
|
+
stepCardPanels,
|
|
4813
4886
|
}));
|
|
4814
4887
|
for (let index = 0; index < contents.length; index += 1) {
|
|
4815
4888
|
let id = activeIds[index];
|
|
@@ -4834,12 +4907,13 @@ export class FeishuHarnessBridge {
|
|
|
4834
4907
|
|
|
4835
4908
|
async #renderStepCardNow(chatId, card) {
|
|
4836
4909
|
if (card.broken) return;
|
|
4837
|
-
const
|
|
4910
|
+
const stepCardPanels = this.#stepCardPanels;
|
|
4911
|
+
const chunks = splitStepStreamCardBlocks(card.blocks, undefined, undefined, stepCardPanels);
|
|
4838
4912
|
if (!chunks.length && card.messageId === null) return;
|
|
4839
4913
|
const live = chunks[chunks.length - 1] ?? [];
|
|
4840
4914
|
try {
|
|
4841
|
-
await this.#syncStepCardChunks(card, chunks, 'running');
|
|
4842
|
-
await this.#persistMirrorState(card, live, 'running');
|
|
4915
|
+
await this.#syncStepCardChunks(card, chunks, 'running', stepCardPanels);
|
|
4916
|
+
await this.#persistMirrorState(card, live, 'running', stepCardPanels);
|
|
4843
4917
|
card.lastRenderAt = this.#stepPushClock.now();
|
|
4844
4918
|
} catch (error) {
|
|
4845
4919
|
card.broken = true;
|
|
@@ -4879,9 +4953,10 @@ export class FeishuHarnessBridge {
|
|
|
4879
4953
|
if (body.trim()) {
|
|
4880
4954
|
this.#writeStepCardAnswer(card, body);
|
|
4881
4955
|
}
|
|
4882
|
-
const
|
|
4956
|
+
const stepCardPanels = this.#stepCardPanels;
|
|
4957
|
+
const chunks = splitStepStreamCardBlocks(card.blocks, undefined, undefined, stepCardPanels);
|
|
4883
4958
|
try {
|
|
4884
|
-
await this.#syncStepCardChunks(card, chunks, status);
|
|
4959
|
+
await this.#syncStepCardChunks(card, chunks, status, stepCardPanels);
|
|
4885
4960
|
return { ok: true, cardIds: card.cardIds };
|
|
4886
4961
|
} catch (error) {
|
|
4887
4962
|
// Only trust the streamed draft when the LAST SUCCESSFUL render actually
|
|
@@ -4932,8 +5007,9 @@ export class FeishuHarnessBridge {
|
|
|
4932
5007
|
if (card.broken) return;
|
|
4933
5008
|
if (card.messageId === null) return;
|
|
4934
5009
|
try {
|
|
4935
|
-
const
|
|
4936
|
-
|
|
5010
|
+
const stepCardPanels = this.#stepCardPanels;
|
|
5011
|
+
const chunks = splitStepStreamCardBlocks(card.blocks, undefined, undefined, stepCardPanels);
|
|
5012
|
+
await this.#syncStepCardChunks(card, chunks, 'sealed', stepCardPanels);
|
|
4937
5013
|
} catch (error) {
|
|
4938
5014
|
this.#logger.warn?.(
|
|
4939
5015
|
'[dsh-feishu] step streaming card rotate seal failed:',
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { posix, win32 } from 'node:path';
|
|
18
18
|
import { t } from '../shared/i18n.mjs';
|
|
19
|
+
import { normalizeFeishuStepCardPanels } from './step-push-mode.mjs';
|
|
19
20
|
|
|
20
21
|
export const MENU_PAGE_SIZE = 10;
|
|
21
22
|
|
|
@@ -353,6 +354,7 @@ export function menuCard(ctx) {
|
|
|
353
354
|
// ── 任务控制(对运行中任务的操作)────────────────────────
|
|
354
355
|
elements.push({ tag: 'div', text: markdown(t('**任务控制**')) });
|
|
355
356
|
elements.push(buttonPair(t('⏹ 停止'), 'stop', t('📐 压缩'), 'compact'));
|
|
357
|
+
elements.push(button(t('🧹 清空上下文'), 'clear'));
|
|
356
358
|
elements.push({ tag: 'hr' });
|
|
357
359
|
|
|
358
360
|
// ── 补充指令 + 归档切换(并列)────────────────────────────
|
|
@@ -573,6 +575,7 @@ export function menuHelpText() {
|
|
|
573
575
|
'/status 连接状态',
|
|
574
576
|
'/version 查看插件版本',
|
|
575
577
|
'/compact 压缩当前会话上下文',
|
|
578
|
+
'/clear 清空当前会话上下文(保留 Session 绑定和历史记录)',
|
|
576
579
|
'/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)',
|
|
577
580
|
'/archived on/off 会话列表显示/隐藏归档',
|
|
578
581
|
'',
|
|
@@ -620,10 +623,11 @@ const HELP_CARD_FEATURES = [
|
|
|
620
623
|
'6. 📋 会话/关注 — 查看/绑定会话,管理关注',
|
|
621
624
|
'7. ⏹ 停止 — 停止当前任务',
|
|
622
625
|
'8. 📐 压缩 — 压缩当前会话上下文',
|
|
623
|
-
'9.
|
|
624
|
-
'10.
|
|
625
|
-
'11.
|
|
626
|
-
'12.
|
|
626
|
+
'9. 🧹 清空上下文 — 清空上下文并保留绑定和历史记录',
|
|
627
|
+
'10. 补充指令 — 给 Agent 发送指令',
|
|
628
|
+
'11. 🗄 归档切换 — 显示/隐藏归档会话',
|
|
629
|
+
'12. 📊 状态 — 查看系统连接状态',
|
|
630
|
+
'13. 📖 帮助 — 查看本帮助',
|
|
627
631
|
].join('\n');
|
|
628
632
|
|
|
629
633
|
const HELP_TEXT_COMMANDS = [
|
|
@@ -638,6 +642,7 @@ const HELP_TEXT_COMMANDS = [
|
|
|
638
642
|
'`/workspacelist` — 列出工作区',
|
|
639
643
|
'`/status` — 查看连接状态',
|
|
640
644
|
'`/compact` — 压缩上下文',
|
|
645
|
+
'`/clear` — 清空上下文并保留 Session 绑定和历史记录',
|
|
641
646
|
'`/stop` — 停止当前任务',
|
|
642
647
|
'`/steer 指令` — 补充指令',
|
|
643
648
|
'`/watch ID` — 关注会话',
|
|
@@ -996,15 +1001,16 @@ export const STEP_STREAM_CARD_MAX_BYTES = 24_000;
|
|
|
996
1001
|
* Build one streaming step card from the accumulated process blocks:
|
|
997
1002
|
* { kind: 'message', text } — interim note / warning / context line
|
|
998
1003
|
* { kind: 'tools', lines: string[] } — tool-call summary panel
|
|
999
|
-
* `status`: 'running'
|
|
1004
|
+
* `status`: 'running' uses the configured panel expansion and ends with an italic status
|
|
1000
1005
|
* line; 'completed' / 'stopped' collapse the panels and swap the status text;
|
|
1001
1006
|
* 'sealed' is an overflow spill chunk with no status line at all. Finished
|
|
1002
1007
|
* turns (and sealed spill chunks) merge every tool/thinking panel into one
|
|
1003
1008
|
* collapsed "process details" panel so the sealed card stays compact.
|
|
1004
1009
|
*/
|
|
1005
1010
|
|
|
1006
|
-
export function stepStreamCard(rawBlocks, { status = 'running' } = {}) {
|
|
1011
|
+
export function stepStreamCard(rawBlocks, { status = 'running', stepCardPanels } = {}) {
|
|
1007
1012
|
const running = status === 'running';
|
|
1013
|
+
const panels = normalizeFeishuStepCardPanels(stepCardPanels);
|
|
1008
1014
|
const elements = [];
|
|
1009
1015
|
const panelElements = [];
|
|
1010
1016
|
for (const block of Array.isArray(rawBlocks) ? rawBlocks : []) {
|
|
@@ -1017,10 +1023,8 @@ export function stepStreamCard(rawBlocks, { status = 'running' } = {}) {
|
|
|
1017
1023
|
title: block.kind === 'tools'
|
|
1018
1024
|
? t('🛠️ 工具摘要({count})', { count })
|
|
1019
1025
|
: t('💭 思考过程({count})', { count }),
|
|
1020
|
-
//
|
|
1021
|
-
|
|
1022
|
-
// tuck them inside one collapsed "process details" wrapper.
|
|
1023
|
-
expanded: block.kind === 'tools' && running,
|
|
1026
|
+
// Finished turns keep both panels inside the collapsed details wrapper.
|
|
1027
|
+
expanded: running && (block.kind === 'tools' ? panels.toolsExpanded : panels.thinkingExpanded),
|
|
1024
1028
|
});
|
|
1025
1029
|
if (running) elements.push(panel);
|
|
1026
1030
|
else panelElements.push(panel);
|
|
@@ -1261,6 +1265,7 @@ export function splitStepStreamCardBlocks(
|
|
|
1261
1265
|
blocks,
|
|
1262
1266
|
limit = STEP_STREAM_CARD_MAX_BYTES,
|
|
1263
1267
|
maxTables = STEP_STREAM_CARD_MAX_TABLES,
|
|
1268
|
+
stepCardPanels,
|
|
1264
1269
|
) {
|
|
1265
1270
|
// A single block can carry more tables than a card accepts — one short answer
|
|
1266
1271
|
// with six small tables — and splitting only between blocks cannot help. Each
|
|
@@ -1273,7 +1278,7 @@ export function splitStepStreamCardBlocks(
|
|
|
1273
1278
|
const chunks = [];
|
|
1274
1279
|
let current = [];
|
|
1275
1280
|
for (const block of list) {
|
|
1276
|
-
const tooLarge = Buffer.byteLength(stepStreamCard([...current, block]), 'utf8') > limit;
|
|
1281
|
+
const tooLarge = Buffer.byteLength(stepStreamCard([...current, block], { stepCardPanels }), 'utf8') > limit;
|
|
1277
1282
|
const tooManyTables = stepStreamCardTableCount([...current, block]) > maxTables;
|
|
1278
1283
|
if (current.length > 0 && (tooLarge || tooManyTables)) {
|
|
1279
1284
|
chunks.push(current);
|
|
@@ -5,7 +5,7 @@ import { FeishuHarnessBridge } from './bridge.mjs';
|
|
|
5
5
|
import { cardActionProbeCard } from './feishu-cards.mjs';
|
|
6
6
|
import { VerifiedFeishuChannel, waitForFeishuOperation } from './feishu-channel.mjs';
|
|
7
7
|
import { normalizeFeishuGroupResponseMode } from './group-response-mode.mjs';
|
|
8
|
-
import { normalizeFeishuStepPushMode } from './step-push-mode.mjs';
|
|
8
|
+
import { normalizeFeishuStepPushMode, normalizeFeishuStepCardPanels } from './step-push-mode.mjs';
|
|
9
9
|
import { createVoice } from './voice.mjs';
|
|
10
10
|
import {
|
|
11
11
|
syncSlashCommands,
|
|
@@ -119,6 +119,7 @@ export class FeishuRuntime {
|
|
|
119
119
|
#mentionTopicReply;
|
|
120
120
|
#stepPush;
|
|
121
121
|
#stepPushMode;
|
|
122
|
+
#stepCardPanels;
|
|
122
123
|
#voice = null;
|
|
123
124
|
#sessionSyncTargetsFor;
|
|
124
125
|
#ownerOpenIds;
|
|
@@ -171,6 +172,7 @@ export class FeishuRuntime {
|
|
|
171
172
|
mentionTopicReply = true,
|
|
172
173
|
stepPush = false,
|
|
173
174
|
stepPushMode = 'post',
|
|
175
|
+
stepCardPanels,
|
|
174
176
|
voice = null,
|
|
175
177
|
sessionSyncTargetsFor = null,
|
|
176
178
|
ownerOpenId,
|
|
@@ -214,6 +216,7 @@ export class FeishuRuntime {
|
|
|
214
216
|
this.#mentionTopicReply = mentionTopicReply !== false;
|
|
215
217
|
this.#stepPush = stepPush === true;
|
|
216
218
|
this.#stepPushMode = normalizeFeishuStepPushMode(stepPushMode);
|
|
219
|
+
this.#stepCardPanels = normalizeFeishuStepCardPanels(stepCardPanels);
|
|
217
220
|
// voice 为 { config, secret } 来源;凭据缺失时 createVoice 返回禁用对象,
|
|
218
221
|
// 桥接层完全跳过语音路径,不影响连接。
|
|
219
222
|
this.#voice = voice == null
|
|
@@ -262,6 +265,11 @@ export class FeishuRuntime {
|
|
|
262
265
|
this.#bridge?.setStepPushMode(this.#stepPushMode);
|
|
263
266
|
}
|
|
264
267
|
|
|
268
|
+
setStepCardPanels(value) {
|
|
269
|
+
this.#stepCardPanels = normalizeFeishuStepCardPanels(value);
|
|
270
|
+
this.#bridge?.setStepCardPanels(this.#stepCardPanels);
|
|
271
|
+
}
|
|
272
|
+
|
|
265
273
|
setVoice(source) {
|
|
266
274
|
this.#voice = source == null
|
|
267
275
|
? null
|
|
@@ -371,6 +379,7 @@ export class FeishuRuntime {
|
|
|
371
379
|
mentionTopicReply: this.#mentionTopicReply,
|
|
372
380
|
stepPush: this.#stepPush,
|
|
373
381
|
stepPushMode: this.#stepPushMode,
|
|
382
|
+
stepCardPanels: this.#stepCardPanels,
|
|
374
383
|
voice: this.#voice,
|
|
375
384
|
sessionSyncTargetsFor: this.#sessionSyncTargetsFor,
|
|
376
385
|
repair: this.#repair,
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
DEFAULT_FEISHU_STEP_PUSH_MODE,
|
|
22
22
|
isFeishuStepPushMode,
|
|
23
23
|
normalizeFeishuStepPushMode,
|
|
24
|
+
isFeishuStepCardPanels,
|
|
25
|
+
normalizeFeishuStepCardPanels,
|
|
24
26
|
} from './step-push-mode.mjs';
|
|
25
27
|
import { normalizeFeishuVoiceConfig } from './voice-config.mjs';
|
|
26
28
|
import {
|
|
@@ -106,6 +108,7 @@ function configuredBotFingerprint(config) {
|
|
|
106
108
|
mentionTopicReply: config.mentionTopicReply !== false,
|
|
107
109
|
stepPush: config.stepPush === true,
|
|
108
110
|
stepPushMode: normalizeFeishuStepPushMode(config.stepPushMode),
|
|
111
|
+
stepCardPanels: normalizeFeishuStepCardPanels(config.stepCardPanels),
|
|
109
112
|
voice: config.voice,
|
|
110
113
|
slashPanel: normalizeSlashPanelConfig(config.slashPanel),
|
|
111
114
|
groupMessagePermissionGranted: config.groupMessagePermissionGranted === true,
|
|
@@ -816,6 +819,20 @@ export class MultiBotDshFeishuController {
|
|
|
816
819
|
}));
|
|
817
820
|
}
|
|
818
821
|
|
|
822
|
+
async updateStepCardPanels(botId, stepCardPanels) {
|
|
823
|
+
this.#assertOpen();
|
|
824
|
+
if (!isFeishuStepCardPanels(stepCardPanels)) {
|
|
825
|
+
throw new TypeError('Invalid Feishu step card panels');
|
|
826
|
+
}
|
|
827
|
+
return this.#serializeConfig(() => this.#withBotTransition(botId, async () => {
|
|
828
|
+
const config = this.#requireBot(botId);
|
|
829
|
+
const saved = await this.#configStore.saveBot({ ...config, stepCardPanels });
|
|
830
|
+
this.#runtimes.get(botId)?.setStepCardPanels?.(saved.stepCardPanels);
|
|
831
|
+
this.#touch();
|
|
832
|
+
return this.status(botId);
|
|
833
|
+
}));
|
|
834
|
+
}
|
|
835
|
+
|
|
819
836
|
async updateVoice(botId, voice) {
|
|
820
837
|
this.#assertOpen();
|
|
821
838
|
if (voice !== null && (typeof voice !== 'object' || Array.isArray(voice))) {
|
|
@@ -901,6 +918,7 @@ export class MultiBotDshFeishuController {
|
|
|
901
918
|
mentionTopicReply: config.mentionTopicReply !== false,
|
|
902
919
|
stepPush: config.stepPush === true,
|
|
903
920
|
stepPushMode: normalizeFeishuStepPushMode(config.stepPushMode),
|
|
921
|
+
stepCardPanels: normalizeFeishuStepCardPanels(config.stepCardPanels),
|
|
904
922
|
voice: normalizeFeishuVoiceConfig(config.voice),
|
|
905
923
|
slashPanel: normalizeSlashPanelConfig(config.slashPanel),
|
|
906
924
|
groupMessagePermissionGranted: config.groupMessagePermissionGranted === true,
|
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
import { normalizeFeishuGroupResponseMode } from './group-response-mode.mjs';
|
|
5
|
-
import { normalizeFeishuStepPushMode } from './step-push-mode.mjs';
|
|
5
|
+
import { normalizeFeishuStepPushMode, normalizeFeishuStepCardPanels } from './step-push-mode.mjs';
|
|
6
6
|
import { normalizeFeishuVoiceConfig } from './voice-config.mjs';
|
|
7
7
|
import { normalizeSlashPanelConfig } from './slash-command-panel.mjs';
|
|
8
8
|
|
|
@@ -52,6 +52,7 @@ function normalizeBot(value, { legacy = false } = {}) {
|
|
|
52
52
|
mentionTopicReply: value.mentionTopicReply !== false,
|
|
53
53
|
stepPush: value.stepPush === true,
|
|
54
54
|
stepPushMode: normalizeFeishuStepPushMode(value.stepPushMode),
|
|
55
|
+
stepCardPanels: normalizeFeishuStepCardPanels(value.stepCardPanels),
|
|
55
56
|
voice: normalizeFeishuVoiceConfig(value.voice),
|
|
56
57
|
slashPanel: normalizeSlashPanelConfig(value.slashPanel),
|
|
57
58
|
groupMessagePermissionGranted: value.groupMessagePermissionGranted === true,
|
|
@@ -30,6 +30,7 @@ export const SLASH_COMMAND_MANIFEST = Object.freeze([
|
|
|
30
30
|
{ command: 'help', icon: 'promptword_outlined', default: '查看帮助', en_us: 'Show help' },
|
|
31
31
|
{ command: 'status', icon: 'ai-functions_outlined', default: '查看机器人状态', en_us: 'Show bot status' },
|
|
32
32
|
{ command: 'compact', icon: 'ai-block_outlined', default: '压缩当前会话上下文', en_us: 'Compact the current session' },
|
|
33
|
+
{ command: 'clear', icon: 'clear_outlined', default: '清空当前会话上下文', en_us: 'Clear the current session context' },
|
|
33
34
|
{ command: 'sessionlist', icon: 'chat-ai_outlined', default: '列出会话', en_us: 'List sessions' },
|
|
34
35
|
{ command: 'workspacelist', icon: 'folder_outlined', default: '列出工作区', en_us: 'List workspaces' },
|
|
35
36
|
{ command: 'workspaces', icon: 'folder_outlined', default: '列出工作区', en_us: 'List workspaces' },
|
|
@@ -7,6 +7,27 @@ export const FEISHU_STEP_PUSH_MODES = Object.freeze({
|
|
|
7
7
|
/** New connections explicitly opt into the process-card presentation. */
|
|
8
8
|
export const DEFAULT_FEISHU_STEP_PUSH_MODE = FEISHU_STEP_PUSH_MODES.STREAMING_CARD;
|
|
9
9
|
|
|
10
|
+
export const DEFAULT_FEISHU_STEP_CARD_PANELS = Object.freeze({
|
|
11
|
+
thinkingExpanded: false,
|
|
12
|
+
toolsExpanded: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export function normalizeFeishuStepCardPanels(value) {
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
thinkingExpanded: typeof value?.thinkingExpanded === 'boolean'
|
|
18
|
+
? value.thinkingExpanded : DEFAULT_FEISHU_STEP_CARD_PANELS.thinkingExpanded,
|
|
19
|
+
toolsExpanded: typeof value?.toolsExpanded === 'boolean'
|
|
20
|
+
? value.toolsExpanded : DEFAULT_FEISHU_STEP_CARD_PANELS.toolsExpanded,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isFeishuStepCardPanels(value) {
|
|
25
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
26
|
+
&& Object.keys(value).every((key) => key === 'thinkingExpanded' || key === 'toolsExpanded')
|
|
27
|
+
&& typeof value.thinkingExpanded === 'boolean'
|
|
28
|
+
&& typeof value.toolsExpanded === 'boolean';
|
|
29
|
+
}
|
|
30
|
+
|
|
10
31
|
export function normalizeFeishuStepPushMode(value) {
|
|
11
32
|
// Bots created before modes existed used posts when step push was enabled.
|
|
12
33
|
if (value === FEISHU_STEP_PUSH_MODES.STREAMING_CARD) {
|
|
@@ -97,6 +97,7 @@ export class MatrixCryptoEngine {
|
|
|
97
97
|
#seenIndex = new Map();
|
|
98
98
|
#pendingEvents = new Map();
|
|
99
99
|
#requestState = new Map();
|
|
100
|
+
#pendingTasks = new Set();
|
|
100
101
|
#lastKeyMaintenanceAt = 0;
|
|
101
102
|
#stats = {
|
|
102
103
|
undecryptable: 0,
|
|
@@ -239,6 +240,12 @@ export class MatrixCryptoEngine {
|
|
|
239
240
|
}
|
|
240
241
|
|
|
241
242
|
async stop() {
|
|
243
|
+
this.#ready = false;
|
|
244
|
+
// A key request can enqueue persistence after shutdown starts. Drain both
|
|
245
|
+
// before releasing sessions or allowing the caller to remove the store.
|
|
246
|
+
while (this.#pendingTasks.size > 0) {
|
|
247
|
+
await Promise.allSettled([...this.#pendingTasks]);
|
|
248
|
+
}
|
|
242
249
|
for (const entry of this.#inbound.values()) entry.session.free?.();
|
|
243
250
|
for (const entry of this.#outbound.values()) entry.session.free?.();
|
|
244
251
|
this.#inbound.clear();
|
|
@@ -247,7 +254,12 @@ export class MatrixCryptoEngine {
|
|
|
247
254
|
this.#memberCache.clear();
|
|
248
255
|
this.#pendingEvents.clear();
|
|
249
256
|
this.#seenIndex.clear();
|
|
250
|
-
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#trackTask(task) {
|
|
260
|
+
const tracked = task.finally(() => this.#pendingTasks.delete(tracked));
|
|
261
|
+
this.#pendingTasks.add(tracked);
|
|
262
|
+
return tracked;
|
|
251
263
|
}
|
|
252
264
|
|
|
253
265
|
async maintain() {
|
|
@@ -717,7 +729,9 @@ export class MatrixCryptoEngine {
|
|
|
717
729
|
if (!entry) {
|
|
718
730
|
this.#stats.undecryptable += 1;
|
|
719
731
|
this.#pendingEvents.set(`${key}|${event?.event_id ?? 'x'}`, { roomId, event, at: this.#now() });
|
|
720
|
-
void this.#requestRoomKey(roomId, event)
|
|
732
|
+
void this.#trackTask(this.#requestRoomKey(roomId, event).catch((error) => {
|
|
733
|
+
this.#logger.warn?.('[dsh-im:matrix] a room key request failed:', error?.message ?? error);
|
|
734
|
+
}));
|
|
721
735
|
return null;
|
|
722
736
|
}
|
|
723
737
|
const seen = this.#seenIndexes(key);
|
|
@@ -908,7 +922,7 @@ export class MatrixCryptoEngine {
|
|
|
908
922
|
} catch {
|
|
909
923
|
return Promise.resolve();
|
|
910
924
|
}
|
|
911
|
-
return this.#store.apply({ groupOutbound }).catch(() => undefined);
|
|
925
|
+
return this.#trackTask(this.#store.apply({ groupOutbound }).catch(() => undefined));
|
|
912
926
|
}
|
|
913
927
|
|
|
914
928
|
#persistInbound(key) {
|
|
@@ -936,7 +950,7 @@ export class MatrixCryptoEngine {
|
|
|
936
950
|
lastUsedAt: entry.lastUsedAt,
|
|
937
951
|
},
|
|
938
952
|
].slice(-900);
|
|
939
|
-
return this.#store.apply({ groupInbound }).catch(() => undefined);
|
|
953
|
+
return this.#trackTask(this.#store.apply({ groupInbound }).catch(() => undefined));
|
|
940
954
|
}
|
|
941
955
|
|
|
942
956
|
#persistRequestState() {
|
|
@@ -944,7 +958,7 @@ export class MatrixCryptoEngine {
|
|
|
944
958
|
if (!snapshot) return Promise.resolve();
|
|
945
959
|
const requestState = {};
|
|
946
960
|
for (const [key, state] of [...this.#requestState].slice(-200)) requestState[key] = { ...state };
|
|
947
|
-
return this.#store.apply({ requestState }).catch(() => undefined);
|
|
961
|
+
return this.#trackTask(this.#store.apply({ requestState }).catch(() => undefined));
|
|
948
962
|
}
|
|
949
963
|
|
|
950
964
|
// ---- key registration ---------------------------------------------------
|