@xmanrui/dsh-im 4.37.0 → 4.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PROACTIVE_DELIVERY.en.md +18 -2
- package/PROACTIVE_DELIVERY.md +18 -2
- package/README.en.md +2 -0
- package/README.md +2 -0
- package/assets/logo-plugin-message-link-gradient.webp +0 -0
- package/lib/client.js +158 -62
- package/lib/index.js +322 -317
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +1 -1
- package/plugin-src/client/bot-general-settings.js +79 -42
- package/plugin-src/client/channels/feishu/api.js +3 -1
- package/plugin-src/client/delivery-settings.js +2 -2
- package/plugin-src/client/i18n.js +8 -7
- package/plugin-src/client/styles.js +19 -0
- package/plugin-src/host/channels/feishu/production.mjs +1 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +18 -0
- package/plugin-src/host/delivery-adapter.mjs +9 -1
- package/plugin-src/host/delivery-service.mjs +4 -3
- package/plugin-src/host/session-sync-coordinator.mjs +9 -5
- package/scripts/verify-package.mjs +11 -6
- package/src/channels/dingtalk/dingtalk-api.mjs +41 -24
- package/src/channels/dingtalk/dingtalk-runtime.mjs +3 -2
- package/src/channels/discord/discord-runtime.mjs +13 -2
- package/src/channels/feishu/bridge.mjs +30 -11
- package/src/channels/feishu/feishu-cards.mjs +8 -7
- 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/step-push-mode.mjs +21 -0
- package/src/channels/matrix/matrix-crypto.mjs +19 -5
- package/src/channels/matrix/matrix-runtime.mjs +16 -6
- package/src/channels/qq/markdown-reply.mjs +7 -340
- package/src/channels/qq/qq-runtime.mjs +11 -3
- package/src/channels/shared/harness-approval.mjs +102 -2
- package/src/channels/shared/harness-client.mjs +15 -3
- package/src/channels/shared/i18n-en/shared-c.mjs +4 -0
- package/src/channels/shared/i18n-en/wecom.mjs +3 -0
- package/src/channels/shared/markdown-chunks.mjs +338 -0
- package/src/channels/shared/message-failure.mjs +1 -1
- package/src/channels/shared/text-harness-bridge.mjs +4 -1
- package/src/channels/slack/slack-api.mjs +2 -1
- package/src/channels/slack/slack-runtime.mjs +30 -0
- package/src/channels/telegram/telegram-runtime.mjs +1 -0
- package/src/channels/wecom/wecom-bridge.mjs +45 -10
- package/src/channels/wecom/wecom-runtime.mjs +11 -5
- package/src/channels/weixin/weixin-runtime.mjs +17 -10
- package/src/channels/whatsapp/whatsapp-markdown.mjs +59 -0
- package/src/channels/whatsapp/whatsapp-runtime.mjs +26 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BusyMessageDispatcher, canAutomaticallySteer } from '../shared/busy-message-dispatcher.mjs';
|
|
2
2
|
import { isPermissionCommand, runPermissionCommand } from '../shared/permission-command.mjs';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { chunkMarkdownText } from '../shared/markdown-chunks.mjs';
|
|
4
5
|
import QRCode from 'qrcode';
|
|
5
6
|
import { waitForFeishuOperation } from './feishu-channel.mjs';
|
|
6
7
|
import {
|
|
@@ -122,6 +123,7 @@ import {
|
|
|
122
123
|
import {
|
|
123
124
|
FEISHU_STEP_PUSH_MODES,
|
|
124
125
|
normalizeFeishuStepPushMode,
|
|
126
|
+
normalizeFeishuStepCardPanels,
|
|
125
127
|
} from './step-push-mode.mjs';
|
|
126
128
|
import { FeishuLiveCot } from './live-cot.mjs';
|
|
127
129
|
|
|
@@ -680,6 +682,7 @@ export class FeishuHarnessBridge {
|
|
|
680
682
|
#stepPush = false;
|
|
681
683
|
/** Step push presentation: discrete posts, a CardKit card, or native live CoT. */
|
|
682
684
|
#stepPushMode = FEISHU_STEP_PUSH_MODES.POST;
|
|
685
|
+
#stepCardPanels;
|
|
683
686
|
/** Per-conversation step push state: key → { lastSentAt, count, breakerLogged }. */
|
|
684
687
|
#stepPushSendState = new Map();
|
|
685
688
|
/** Live streaming step cards: key → per-turn card state (streaming_card mode). */
|
|
@@ -745,6 +748,7 @@ export class FeishuHarnessBridge {
|
|
|
745
748
|
mentionTopicReply = true,
|
|
746
749
|
stepPush = false,
|
|
747
750
|
stepPushMode = FEISHU_STEP_PUSH_MODES.POST,
|
|
751
|
+
stepCardPanels,
|
|
748
752
|
stepPushClock = null,
|
|
749
753
|
repair,
|
|
750
754
|
repairPollIntervalMs = REPAIR_POLL_INTERVAL_MS,
|
|
@@ -805,6 +809,7 @@ export class FeishuHarnessBridge {
|
|
|
805
809
|
this.#mentionTopicReply = mentionTopicReply !== false;
|
|
806
810
|
this.#stepPush = stepPush === true;
|
|
807
811
|
this.#stepPushMode = normalizeFeishuStepPushMode(stepPushMode);
|
|
812
|
+
this.#stepCardPanels = normalizeFeishuStepCardPanels(stepCardPanels);
|
|
808
813
|
this.#stepPushClock = stepPushClock ?? DEFAULT_STEP_PUSH_CLOCK;
|
|
809
814
|
this.#voice = voice ?? null;
|
|
810
815
|
this.#repair = repair ?? null;
|
|
@@ -901,6 +906,10 @@ export class FeishuHarnessBridge {
|
|
|
901
906
|
this.#stepPushMode = normalizeFeishuStepPushMode(value);
|
|
902
907
|
}
|
|
903
908
|
|
|
909
|
+
setStepCardPanels(value) {
|
|
910
|
+
this.#stepCardPanels = normalizeFeishuStepCardPanels(value);
|
|
911
|
+
}
|
|
912
|
+
|
|
904
913
|
setVoice(voice) {
|
|
905
914
|
this.#voice = voice ?? null;
|
|
906
915
|
}
|
|
@@ -4782,7 +4791,13 @@ export class FeishuHarnessBridge {
|
|
|
4782
4791
|
|
|
4783
4792
|
/** Replace the draft interval with `text` chunked into splitter-safe blocks. */
|
|
4784
4793
|
#writeStepCardAnswer(card, text) {
|
|
4785
|
-
|
|
4794
|
+
// JSON can expand one UTF-16 unit to six bytes (for example a control
|
|
4795
|
+
// character). Leave room for the element wrapper, and let the existing
|
|
4796
|
+
// Markdown splitter reopen fences in each independently rendered block.
|
|
4797
|
+
const chunks = Buffer.byteLength(JSON.stringify(text), 'utf8') + 128 <= STEP_STREAM_ANSWER_CHUNK_MAX_BYTES
|
|
4798
|
+
? [text]
|
|
4799
|
+
: chunkMarkdownText(text, Math.floor((STEP_STREAM_ANSWER_CHUNK_MAX_BYTES - 128) / 6));
|
|
4800
|
+
const blocks = chunks
|
|
4786
4801
|
.map((chunk) => ({ kind: 'message', text: chunk }));
|
|
4787
4802
|
if (card.answerStart !== null) {
|
|
4788
4803
|
card.blocks.splice(card.answerStart, card.answerEnd - card.answerStart + 1, ...blocks);
|
|
@@ -4845,7 +4860,7 @@ export class FeishuHarnessBridge {
|
|
|
4845
4860
|
* their lifecycle is owned by the ask path, not the mirror recovery.
|
|
4846
4861
|
* lastContent keeps stepStreamCard's raw JSON string (single-encoded).
|
|
4847
4862
|
*/
|
|
4848
|
-
async #persistMirrorState(card, liveBlocks, status) {
|
|
4863
|
+
async #persistMirrorState(card, liveBlocks, status, stepCardPanels) {
|
|
4849
4864
|
const sessionId = card.sessionSyncSessionId;
|
|
4850
4865
|
if (!sessionId || typeof this.#state.setMirror !== 'function') return;
|
|
4851
4866
|
await this.#state.setMirror(card.sessionSyncKey, {
|
|
@@ -4853,7 +4868,7 @@ export class FeishuHarnessBridge {
|
|
|
4853
4868
|
chatId: card.chatId,
|
|
4854
4869
|
cardIds: [...card.cardIds],
|
|
4855
4870
|
claimedAt: Date.now(),
|
|
4856
|
-
lastContent: stepStreamCard(liveBlocks, { status }),
|
|
4871
|
+
lastContent: stepStreamCard(liveBlocks, { status, stepCardPanels }),
|
|
4857
4872
|
blocks: structuredClone(card.blocks), answerStart: card.answerStart, answerEnd: card.answerEnd,
|
|
4858
4873
|
lastSeq: this.#sessionSyncTurns.get(card.sessionSyncKey)?.lastSeq ?? -1,
|
|
4859
4874
|
pendingStep: this.#sessionSyncTurns.get(card.sessionSyncKey)?.pendingStep ?? null,
|
|
@@ -4861,7 +4876,7 @@ export class FeishuHarnessBridge {
|
|
|
4861
4876
|
}
|
|
4862
4877
|
|
|
4863
4878
|
/** Sync only this interaction's cards; earlier interactions remain sealed. */
|
|
4864
|
-
async #syncStepCardChunks(card, chunks, status) {
|
|
4879
|
+
async #syncStepCardChunks(card, chunks, status, stepCardPanels) {
|
|
4865
4880
|
const answerVersion = card.answerVersion ?? 0;
|
|
4866
4881
|
const activeIds = card.cardIds.slice(card.activeCardStart);
|
|
4867
4882
|
const groups = chunks.length ? [...chunks] : [[]];
|
|
@@ -4874,6 +4889,7 @@ export class FeishuHarnessBridge {
|
|
|
4874
4889
|
// payload halfway through this render or be marked as already delivered.
|
|
4875
4890
|
const contents = groups.map((blocks, index) => stepStreamCard(blocks, {
|
|
4876
4891
|
status: index === groups.length - 1 ? status : 'sealed',
|
|
4892
|
+
stepCardPanels,
|
|
4877
4893
|
}));
|
|
4878
4894
|
for (let index = 0; index < contents.length; index += 1) {
|
|
4879
4895
|
let id = activeIds[index];
|
|
@@ -4898,12 +4914,13 @@ export class FeishuHarnessBridge {
|
|
|
4898
4914
|
|
|
4899
4915
|
async #renderStepCardNow(chatId, card) {
|
|
4900
4916
|
if (card.broken) return;
|
|
4901
|
-
const
|
|
4917
|
+
const stepCardPanels = this.#stepCardPanels;
|
|
4918
|
+
const chunks = splitStepStreamCardBlocks(card.blocks, undefined, undefined, stepCardPanels);
|
|
4902
4919
|
if (!chunks.length && card.messageId === null) return;
|
|
4903
4920
|
const live = chunks[chunks.length - 1] ?? [];
|
|
4904
4921
|
try {
|
|
4905
|
-
await this.#syncStepCardChunks(card, chunks, 'running');
|
|
4906
|
-
await this.#persistMirrorState(card, live, 'running');
|
|
4922
|
+
await this.#syncStepCardChunks(card, chunks, 'running', stepCardPanels);
|
|
4923
|
+
await this.#persistMirrorState(card, live, 'running', stepCardPanels);
|
|
4907
4924
|
card.lastRenderAt = this.#stepPushClock.now();
|
|
4908
4925
|
} catch (error) {
|
|
4909
4926
|
card.broken = true;
|
|
@@ -4943,9 +4960,10 @@ export class FeishuHarnessBridge {
|
|
|
4943
4960
|
if (body.trim()) {
|
|
4944
4961
|
this.#writeStepCardAnswer(card, body);
|
|
4945
4962
|
}
|
|
4946
|
-
const
|
|
4963
|
+
const stepCardPanels = this.#stepCardPanels;
|
|
4964
|
+
const chunks = splitStepStreamCardBlocks(card.blocks, undefined, undefined, stepCardPanels);
|
|
4947
4965
|
try {
|
|
4948
|
-
await this.#syncStepCardChunks(card, chunks, status);
|
|
4966
|
+
await this.#syncStepCardChunks(card, chunks, status, stepCardPanels);
|
|
4949
4967
|
return { ok: true, cardIds: card.cardIds };
|
|
4950
4968
|
} catch (error) {
|
|
4951
4969
|
// Only trust the streamed draft when the LAST SUCCESSFUL render actually
|
|
@@ -4996,8 +5014,9 @@ export class FeishuHarnessBridge {
|
|
|
4996
5014
|
if (card.broken) return;
|
|
4997
5015
|
if (card.messageId === null) return;
|
|
4998
5016
|
try {
|
|
4999
|
-
const
|
|
5000
|
-
|
|
5017
|
+
const stepCardPanels = this.#stepCardPanels;
|
|
5018
|
+
const chunks = splitStepStreamCardBlocks(card.blocks, undefined, undefined, stepCardPanels);
|
|
5019
|
+
await this.#syncStepCardChunks(card, chunks, 'sealed', stepCardPanels);
|
|
5001
5020
|
} catch (error) {
|
|
5002
5021
|
this.#logger.warn?.(
|
|
5003
5022
|
'[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
|
|
|
@@ -1000,15 +1001,16 @@ export const STEP_STREAM_CARD_MAX_BYTES = 24_000;
|
|
|
1000
1001
|
* Build one streaming step card from the accumulated process blocks:
|
|
1001
1002
|
* { kind: 'message', text } — interim note / warning / context line
|
|
1002
1003
|
* { kind: 'tools', lines: string[] } — tool-call summary panel
|
|
1003
|
-
* `status`: 'running'
|
|
1004
|
+
* `status`: 'running' uses the configured panel expansion and ends with an italic status
|
|
1004
1005
|
* line; 'completed' / 'stopped' collapse the panels and swap the status text;
|
|
1005
1006
|
* 'sealed' is an overflow spill chunk with no status line at all. Finished
|
|
1006
1007
|
* turns (and sealed spill chunks) merge every tool/thinking panel into one
|
|
1007
1008
|
* collapsed "process details" panel so the sealed card stays compact.
|
|
1008
1009
|
*/
|
|
1009
1010
|
|
|
1010
|
-
export function stepStreamCard(rawBlocks, { status = 'running' } = {}) {
|
|
1011
|
+
export function stepStreamCard(rawBlocks, { status = 'running', stepCardPanels } = {}) {
|
|
1011
1012
|
const running = status === 'running';
|
|
1013
|
+
const panels = normalizeFeishuStepCardPanels(stepCardPanels);
|
|
1012
1014
|
const elements = [];
|
|
1013
1015
|
const panelElements = [];
|
|
1014
1016
|
for (const block of Array.isArray(rawBlocks) ? rawBlocks : []) {
|
|
@@ -1021,10 +1023,8 @@ export function stepStreamCard(rawBlocks, { status = 'running' } = {}) {
|
|
|
1021
1023
|
title: block.kind === 'tools'
|
|
1022
1024
|
? t('🛠️ 工具摘要({count})', { count })
|
|
1023
1025
|
: t('💭 思考过程({count})', { count }),
|
|
1024
|
-
//
|
|
1025
|
-
|
|
1026
|
-
// tuck them inside one collapsed "process details" wrapper.
|
|
1027
|
-
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),
|
|
1028
1028
|
});
|
|
1029
1029
|
if (running) elements.push(panel);
|
|
1030
1030
|
else panelElements.push(panel);
|
|
@@ -1265,6 +1265,7 @@ export function splitStepStreamCardBlocks(
|
|
|
1265
1265
|
blocks,
|
|
1266
1266
|
limit = STEP_STREAM_CARD_MAX_BYTES,
|
|
1267
1267
|
maxTables = STEP_STREAM_CARD_MAX_TABLES,
|
|
1268
|
+
stepCardPanels,
|
|
1268
1269
|
) {
|
|
1269
1270
|
// A single block can carry more tables than a card accepts — one short answer
|
|
1270
1271
|
// with six small tables — and splitting only between blocks cannot help. Each
|
|
@@ -1277,7 +1278,7 @@ export function splitStepStreamCardBlocks(
|
|
|
1277
1278
|
const chunks = [];
|
|
1278
1279
|
let current = [];
|
|
1279
1280
|
for (const block of list) {
|
|
1280
|
-
const tooLarge = Buffer.byteLength(stepStreamCard([...current, block]), 'utf8') > limit;
|
|
1281
|
+
const tooLarge = Buffer.byteLength(stepStreamCard([...current, block], { stepCardPanels }), 'utf8') > limit;
|
|
1281
1282
|
const tooManyTables = stepStreamCardTableCount([...current, block]) > maxTables;
|
|
1282
1283
|
if (current.length > 0 && (tooLarge || tooManyTables)) {
|
|
1283
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,
|
|
@@ -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 ---------------------------------------------------
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
extractOutboundMentions,
|
|
37
37
|
hasRoomMention,
|
|
38
38
|
} from './matrix-rich-text.mjs';
|
|
39
|
+
import { chunkMarkdownParts } from '../shared/markdown-chunks.mjs';
|
|
39
40
|
|
|
40
41
|
const RECONNECT_DELAYS_MS = Object.freeze([1_000, 3_000, 5_000, 10_000, 30_000]);
|
|
41
42
|
const SYNC_LONG_POLL_MS = 30_000;
|
|
@@ -917,14 +918,23 @@ export class MatrixRuntime {
|
|
|
917
918
|
? target.threadId : null;
|
|
918
919
|
const replyToEventId = typeof target?.replyToEventId === 'string' && isMatrixEventId(target.replyToEventId)
|
|
919
920
|
? target.replyToEventId : null;
|
|
920
|
-
const chunks =
|
|
921
|
+
const chunks = options.format === 'markdown'
|
|
922
|
+
? chunkMarkdownParts(text, this.#config.maxMessageLength)
|
|
923
|
+
: splitMessageText(text, this.#config.maxMessageLength).map(plain => ({ markdown: plain, plain }));
|
|
921
924
|
const providerMessageIds = [];
|
|
922
925
|
for (const chunk of chunks) {
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
926
|
+
let content;
|
|
927
|
+
try {
|
|
928
|
+
content = buildMatrixTextContent({
|
|
929
|
+
text: chunk.markdown,
|
|
930
|
+
mentionUserIds: extractOutboundMentions(chunk.markdown),
|
|
931
|
+
roomMention: this.#config.allowRoomMentions && hasRoomMention(chunk.markdown),
|
|
932
|
+
});
|
|
933
|
+
} catch {
|
|
934
|
+
this.#logger.warn?.('[dsh-im:matrix] Rich text could not be prepared; sending plain text');
|
|
935
|
+
if (!chunk.plain) continue;
|
|
936
|
+
content = { msgtype: 'm.text', body: chunk.plain };
|
|
937
|
+
}
|
|
928
938
|
applyMatrixRelations(content, { threadId, replyToEventId });
|
|
929
939
|
const result = await this.#sendRoomEvent(roomId, 'm.room.message', content, {
|
|
930
940
|
...(options.signal ? { signal: options.signal } : {}),
|