@xmanrui/dsh-im 4.16.0 โ 4.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +66 -18
- package/lib/index.js +259 -256
- package/package.json +5 -1
- package/plugin-src/client/channels/feishu/api.js +3 -0
- package/plugin-src/client/channels/feishu/index.js +45 -13
- package/plugin-src/client/i18n.js +13 -2
- package/plugin-src/host/channels/feishu/production.mjs +1 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +20 -0
- package/src/channels/dingtalk/dingtalk-api.mjs +41 -48
- package/src/channels/dingtalk/dingtalk-bridge.mjs +5 -1
- package/src/channels/feishu/bridge.mjs +475 -11
- package/src/channels/feishu/feishu-cards.mjs +130 -0
- package/src/channels/feishu/feishu-runtime.mjs +10 -0
- package/src/channels/feishu/multi-bot-controller.mjs +28 -0
- package/src/channels/feishu/plugin-config-store.mjs +2 -0
- package/src/channels/feishu/step-push-mode.mjs +19 -0
- package/src/channels/shared/i18n-en/feishu.mjs +8 -0
|
@@ -903,6 +903,136 @@ export function approvalCard({ toolName, operation, reason, approvalId }) {
|
|
|
903
903
|
return cardWith(t('๐ ๅทฅๅ
ทๅฎกๆน'), elements);
|
|
904
904
|
}
|
|
905
905
|
|
|
906
|
+
// โโ Streaming step card (ๆตๅผ่ฟ็จๅก็) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
907
|
+
|
|
908
|
+
/** One streaming step card carries at most this many JSON bytes after the
|
|
909
|
+
* card has been serialized (Feishu caps card content near 30KB; stay lower
|
|
910
|
+
* so headers and JSON escaping always fit). */
|
|
911
|
+
export const STEP_STREAM_CARD_MAX_BYTES = 24_000;
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Build one streaming step card from the accumulated process blocks:
|
|
915
|
+
* { kind: 'message', text } โ interim note / warning / context line
|
|
916
|
+
* { kind: 'tools', lines: string[] } โ tool-call summary panel
|
|
917
|
+
* `status`: 'running' keeps panels expanded and ends with an italic status
|
|
918
|
+
* line; 'completed' / 'stopped' collapse the panels and swap the status text;
|
|
919
|
+
* 'sealed' is an overflow spill chunk with no status line at all. Finished
|
|
920
|
+
* turns (and sealed spill chunks) merge every tool/thinking panel into one
|
|
921
|
+
* collapsed "process details" panel so the sealed card stays compact.
|
|
922
|
+
*/
|
|
923
|
+
|
|
924
|
+
export function stepStreamCard(rawBlocks, { status = 'running' } = {}) {
|
|
925
|
+
const running = status === 'running';
|
|
926
|
+
const elements = [];
|
|
927
|
+
const panelElements = [];
|
|
928
|
+
for (const block of Array.isArray(rawBlocks) ? rawBlocks : []) {
|
|
929
|
+
if (block?.kind === 'tools' || block?.kind === 'notes') {
|
|
930
|
+
const lines = (Array.isArray(block.lines) ? block.lines : [])
|
|
931
|
+
.filter((line) => typeof line === 'string' && line.trim());
|
|
932
|
+
if (lines.length === 0) continue;
|
|
933
|
+
const count = lines.length + (Number(block.omitted) || 0);
|
|
934
|
+
const panel = stepPanel(lines, {
|
|
935
|
+
title: block.kind === 'tools'
|
|
936
|
+
? t('๐ ๏ธ ๅทฅๅ
ทๆ่ฆ๏ผ{count}๏ผ', { count })
|
|
937
|
+
: t('๐ญ ๆ่่ฟ็จ๏ผ{count}๏ผ', { count }),
|
|
938
|
+
// Tool summaries stay visible while the turn runs; thinking notes
|
|
939
|
+
// remain folded at all times. Finished turns keep both panels but
|
|
940
|
+
// tuck them inside one collapsed "process details" wrapper.
|
|
941
|
+
expanded: block.kind === 'tools' && running,
|
|
942
|
+
});
|
|
943
|
+
if (running) elements.push(panel);
|
|
944
|
+
else panelElements.push(panel);
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
const text = typeof block?.text === 'string' ? block.text.trim() : '';
|
|
948
|
+
if (text) elements.push({ tag: 'markdown', content: text });
|
|
949
|
+
}
|
|
950
|
+
if (!running && panelElements.length > 0) {
|
|
951
|
+
// Finished turns: the tool/thinking panels nest inside one collapsed
|
|
952
|
+
// "process details" wrapper, so the sealed card shows a single line.
|
|
953
|
+
elements.push(processDetailsPanel(panelElements));
|
|
954
|
+
}
|
|
955
|
+
if (elements.length === 0) elements.push({ tag: 'markdown', content: ' ' });
|
|
956
|
+
if (status !== 'sealed') {
|
|
957
|
+
elements.push({ tag: 'markdown', content: `_${stepStatusText(status)}_` });
|
|
958
|
+
}
|
|
959
|
+
return JSON.stringify({
|
|
960
|
+
schema: '2.0',
|
|
961
|
+
header: { title: plainText(t('โ๏ธ ไปปๅก่ฟ็จ')), template: 'blue' },
|
|
962
|
+
body: { elements },
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/** The collapsed wrapper that holds the per-kind panels on finished turns. */
|
|
967
|
+
function processDetailsPanel(children) {
|
|
968
|
+
return {
|
|
969
|
+
tag: 'collapsible_panel',
|
|
970
|
+
expanded: false,
|
|
971
|
+
background_color: 'grey-50',
|
|
972
|
+
border: { color: 'grey', corner_radius: '8px' },
|
|
973
|
+
padding: '8px 8px 8px 8px',
|
|
974
|
+
header: {
|
|
975
|
+
title: { tag: 'plain_text', content: t('๐ ่ฟ็จ่ฏฆๆ
') },
|
|
976
|
+
vertical_align: 'center',
|
|
977
|
+
padding: '8px 8px 8px 8px',
|
|
978
|
+
icon: { tag: 'standard_icon', token: 'down-small-ccm_outlined', color: 'grey', size: '16px 16px' },
|
|
979
|
+
icon_position: 'right',
|
|
980
|
+
icon_expanded_angle: -180,
|
|
981
|
+
},
|
|
982
|
+
elements: children,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/** The collapsible grey panel used for tool summaries and thinking notes. */
|
|
987
|
+
function stepPanel(lines, { title, expanded }) {
|
|
988
|
+
return {
|
|
989
|
+
tag: 'collapsible_panel',
|
|
990
|
+
expanded: expanded === true,
|
|
991
|
+
background_color: 'grey-50',
|
|
992
|
+
border: { color: 'grey', corner_radius: '8px' },
|
|
993
|
+
padding: '8px 8px 8px 8px',
|
|
994
|
+
header: {
|
|
995
|
+
title: { tag: 'plain_text', content: title },
|
|
996
|
+
vertical_align: 'center',
|
|
997
|
+
padding: '8px 8px 8px 8px',
|
|
998
|
+
icon: { tag: 'standard_icon', token: 'down-small-ccm_outlined', color: 'grey', size: '16px 16px' },
|
|
999
|
+
icon_position: 'right',
|
|
1000
|
+
icon_expanded_angle: -180,
|
|
1001
|
+
},
|
|
1002
|
+
elements: [{ tag: 'markdown', content: lines.join('\n') }],
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function stepStatusText(status) {
|
|
1007
|
+
if (status === 'completed') return t('ๅทฒๅฎๆ');
|
|
1008
|
+
if (status === 'stopped') return t('ๅทฒๅๆญข');
|
|
1009
|
+
return t('่ฟ่กไธญ');
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Split accumulated blocks into card-sized chunks at block boundaries,
|
|
1014
|
+
* budgeted by the encoded running-status card (the largest render). Every
|
|
1015
|
+
* chunk keeps at least one block so progress is never dropped. The caller
|
|
1016
|
+
* renders all but the last chunk as `sealed` and the last one live.
|
|
1017
|
+
*/
|
|
1018
|
+
export function splitStepStreamCardBlocks(blocks, limit = STEP_STREAM_CARD_MAX_BYTES) {
|
|
1019
|
+
const list = (Array.isArray(blocks) ? blocks : []).filter(Boolean);
|
|
1020
|
+
if (list.length === 0) return [];
|
|
1021
|
+
const chunks = [];
|
|
1022
|
+
let current = [];
|
|
1023
|
+
for (const block of list) {
|
|
1024
|
+
if (current.length > 0
|
|
1025
|
+
&& Buffer.byteLength(stepStreamCard([...current, block]), 'utf8') > limit) {
|
|
1026
|
+
chunks.push(current);
|
|
1027
|
+
current = [block];
|
|
1028
|
+
} else {
|
|
1029
|
+
current.push(block);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
if (current.length > 0) chunks.push(current);
|
|
1033
|
+
return chunks;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
906
1036
|
/**
|
|
907
1037
|
* Interactive question card. When the question carries options, each option is
|
|
908
1038
|
* rendered as its own button; the selected option label is submitted via a
|
|
@@ -3,6 +3,7 @@ import { FeishuHarnessBridge } from './bridge.mjs';
|
|
|
3
3
|
import { cardActionProbeCard } from './feishu-cards.mjs';
|
|
4
4
|
import { VerifiedFeishuChannel } from './feishu-channel.mjs';
|
|
5
5
|
import { normalizeFeishuGroupResponseMode } from './group-response-mode.mjs';
|
|
6
|
+
import { normalizeFeishuStepPushMode } from './step-push-mode.mjs';
|
|
6
7
|
import {
|
|
7
8
|
registerSlashCommands,
|
|
8
9
|
SLASH_COMMAND_MANIFEST,
|
|
@@ -112,6 +113,7 @@ export class FeishuRuntime {
|
|
|
112
113
|
#groupResponseMode;
|
|
113
114
|
#groupTopicReply;
|
|
114
115
|
#stepPush;
|
|
116
|
+
#stepPushMode;
|
|
115
117
|
#ownerOpenIds;
|
|
116
118
|
#harness;
|
|
117
119
|
#state;
|
|
@@ -143,6 +145,7 @@ export class FeishuRuntime {
|
|
|
143
145
|
groupResponseMode,
|
|
144
146
|
groupTopicReply = false,
|
|
145
147
|
stepPush = false,
|
|
148
|
+
stepPushMode = 'post',
|
|
146
149
|
ownerOpenId,
|
|
147
150
|
ownerOpenIds,
|
|
148
151
|
harness,
|
|
@@ -180,6 +183,7 @@ export class FeishuRuntime {
|
|
|
180
183
|
this.#groupResponseMode = normalizeFeishuGroupResponseMode(groupResponseMode);
|
|
181
184
|
this.#groupTopicReply = groupTopicReply === true;
|
|
182
185
|
this.#stepPush = stepPush === true;
|
|
186
|
+
this.#stepPushMode = normalizeFeishuStepPushMode(stepPushMode);
|
|
183
187
|
this.#ownerOpenIds = normalizedOwners;
|
|
184
188
|
this.#harness = harness;
|
|
185
189
|
this.#state = state;
|
|
@@ -214,6 +218,11 @@ export class FeishuRuntime {
|
|
|
214
218
|
this.#bridge?.setStepPush(this.#stepPush);
|
|
215
219
|
}
|
|
216
220
|
|
|
221
|
+
setStepPushMode(value) {
|
|
222
|
+
this.#stepPushMode = normalizeFeishuStepPushMode(value);
|
|
223
|
+
this.#bridge?.setStepPushMode(this.#stepPushMode);
|
|
224
|
+
}
|
|
225
|
+
|
|
217
226
|
async start() {
|
|
218
227
|
while (true) {
|
|
219
228
|
while (this.#stopping) await this.#stopping;
|
|
@@ -304,6 +313,7 @@ export class FeishuRuntime {
|
|
|
304
313
|
groupResponseMode: this.#groupResponseMode,
|
|
305
314
|
groupTopicReply: this.#groupTopicReply,
|
|
306
315
|
stepPush: this.#stepPush,
|
|
316
|
+
stepPushMode: this.#stepPushMode,
|
|
307
317
|
repair: this.#repair,
|
|
308
318
|
replyTimeoutMs: this.#replyTimeoutMs,
|
|
309
319
|
// Interaction cards (approval/question buttons) are on by default.
|
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
isFeishuGroupResponseMode,
|
|
16
16
|
normalizeFeishuGroupResponseMode,
|
|
17
17
|
} from './group-response-mode.mjs';
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_FEISHU_STEP_PUSH_MODE,
|
|
20
|
+
isFeishuStepPushMode,
|
|
21
|
+
normalizeFeishuStepPushMode,
|
|
22
|
+
} from './step-push-mode.mjs';
|
|
18
23
|
|
|
19
24
|
const ACTIVE_REGISTRATION_STATES = new Set([
|
|
20
25
|
'starting', 'qr_ready', 'polling', 'slow_down', 'domain_switched',
|
|
@@ -92,6 +97,7 @@ function configuredBotFingerprint(config) {
|
|
|
92
97
|
groupResponseMode: normalizeFeishuGroupResponseMode(config.groupResponseMode),
|
|
93
98
|
groupTopicReply: config.groupTopicReply === true,
|
|
94
99
|
stepPush: config.stepPush === true,
|
|
100
|
+
stepPushMode: normalizeFeishuStepPushMode(config.stepPushMode),
|
|
95
101
|
groupMessagePermissionGranted: config.groupMessagePermissionGranted === true,
|
|
96
102
|
deletionPending: config.deletionPending === true,
|
|
97
103
|
connectedAt: config.connectedAt ?? null,
|
|
@@ -447,6 +453,8 @@ export class MultiBotDshFeishuController {
|
|
|
447
453
|
botName: bot.name,
|
|
448
454
|
botOpenId: bot.openId,
|
|
449
455
|
activated: bot.activated,
|
|
456
|
+
stepPush: existing?.stepPush ?? true,
|
|
457
|
+
stepPushMode: existing?.stepPushMode ?? DEFAULT_FEISHU_STEP_PUSH_MODE,
|
|
450
458
|
deletionPending: false,
|
|
451
459
|
connectedAt: new Date().toISOString(),
|
|
452
460
|
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
@@ -614,6 +622,20 @@ export class MultiBotDshFeishuController {
|
|
|
614
622
|
}));
|
|
615
623
|
}
|
|
616
624
|
|
|
625
|
+
async updateStepPushMode(botId, stepPushMode) {
|
|
626
|
+
this.#assertOpen();
|
|
627
|
+
if (!isFeishuStepPushMode(stepPushMode)) {
|
|
628
|
+
throw new TypeError('Invalid Feishu step push mode');
|
|
629
|
+
}
|
|
630
|
+
return this.#serializeConfig(() => this.#withBotTransition(botId, async () => {
|
|
631
|
+
const config = this.#requireBot(botId);
|
|
632
|
+
const saved = await this.#configStore.saveBot({ ...config, stepPushMode });
|
|
633
|
+
this.#runtimes.get(botId)?.setStepPushMode?.(saved.stepPushMode);
|
|
634
|
+
this.#touch();
|
|
635
|
+
return this.status(botId);
|
|
636
|
+
}));
|
|
637
|
+
}
|
|
638
|
+
|
|
617
639
|
async deleteBot(botId) {
|
|
618
640
|
this.#assertOpen();
|
|
619
641
|
return this.#serializeConfig(() => this.#withBotTransition(botId, async () => {
|
|
@@ -678,6 +700,7 @@ export class MultiBotDshFeishuController {
|
|
|
678
700
|
groupResponseMode: normalizeFeishuGroupResponseMode(config.groupResponseMode),
|
|
679
701
|
groupTopicReply: config.groupTopicReply === true,
|
|
680
702
|
stepPush: config.stepPush === true,
|
|
703
|
+
stepPushMode: normalizeFeishuStepPushMode(config.stepPushMode),
|
|
681
704
|
groupMessagePermissionGranted: config.groupMessagePermissionGranted === true,
|
|
682
705
|
bot: publicBot(config),
|
|
683
706
|
connection,
|
|
@@ -1111,6 +1134,11 @@ export class MultiBotDshFeishuController {
|
|
|
1111
1134
|
botName: bot.name,
|
|
1112
1135
|
botOpenId: bot.openId,
|
|
1113
1136
|
activated: bot.activated,
|
|
1137
|
+
// New connections start with the process-card presentation; existing
|
|
1138
|
+
// bots keep whatever they saved before (the spread above re-applies
|
|
1139
|
+
// their stored values, undefined falls through to the defaults here).
|
|
1140
|
+
stepPush: existing?.stepPush ?? true,
|
|
1141
|
+
stepPushMode: existing?.stepPushMode ?? DEFAULT_FEISHU_STEP_PUSH_MODE,
|
|
1114
1142
|
deletionPending: false,
|
|
1115
1143
|
connectedAt: new Date().toISOString(),
|
|
1116
1144
|
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
@@ -2,6 +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
6
|
|
|
6
7
|
export const LEGACY_FEISHU_SECRET_REF = 'DSH_FEISHU_APP_SECRET';
|
|
7
8
|
|
|
@@ -47,6 +48,7 @@ function normalizeBot(value, { legacy = false } = {}) {
|
|
|
47
48
|
groupResponseMode: normalizeFeishuGroupResponseMode(value.groupResponseMode),
|
|
48
49
|
groupTopicReply: value.groupTopicReply === true,
|
|
49
50
|
stepPush: value.stepPush === true,
|
|
51
|
+
stepPushMode: normalizeFeishuStepPushMode(value.stepPushMode),
|
|
50
52
|
groupMessagePermissionGranted: value.groupMessagePermissionGranted === true,
|
|
51
53
|
deletionPending: value.deletionPending === true,
|
|
52
54
|
connectedAt: cleanString(value.connectedAt),
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const FEISHU_STEP_PUSH_MODES = Object.freeze({
|
|
2
|
+
POST: 'post',
|
|
3
|
+
STREAMING_CARD: 'streaming_card',
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
/** New connections explicitly opt into the process-card presentation. */
|
|
7
|
+
export const DEFAULT_FEISHU_STEP_PUSH_MODE = FEISHU_STEP_PUSH_MODES.STREAMING_CARD;
|
|
8
|
+
|
|
9
|
+
export function normalizeFeishuStepPushMode(value) {
|
|
10
|
+
// Bots created before modes existed used posts when step push was enabled.
|
|
11
|
+
return value === FEISHU_STEP_PUSH_MODES.STREAMING_CARD
|
|
12
|
+
? FEISHU_STEP_PUSH_MODES.STREAMING_CARD
|
|
13
|
+
: FEISHU_STEP_PUSH_MODES.POST;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isFeishuStepPushMode(value) {
|
|
17
|
+
return value === FEISHU_STEP_PUSH_MODES.POST
|
|
18
|
+
|| value === FEISHU_STEP_PUSH_MODES.STREAMING_CARD;
|
|
19
|
+
}
|
|
@@ -323,6 +323,14 @@ export default {
|
|
|
323
323
|
'๐ ่ฟๅ่ๅ': '๐ Back to menu',
|
|
324
324
|
'ๅทฒๅฎๆ': 'Completed',
|
|
325
325
|
'ๅทฒๅๆญข': 'Stopped',
|
|
326
|
+
'่ฟ่กไธญ': 'Running',
|
|
327
|
+
'โ๏ธ ไปปๅก่ฟ็จ': 'โ๏ธ Task progress',
|
|
328
|
+
'๐ ่ฟ็จ่ฏฆๆ
': '๐ Process details',
|
|
329
|
+
'๐ ๏ธ ๅทฅๅ
ทๆ่ฆ๏ผ{count}๏ผ': '๐ ๏ธ Tool summary ({count})',
|
|
330
|
+
'๐ญ ๆ่่ฟ็จ๏ผ{count}๏ผ': '๐ญ Thinking ({count})',
|
|
331
|
+
'๐ ๏ธ ๅทฅๅ
ท': '๐ ๏ธ Tools',
|
|
332
|
+
'๐ญ ๆ่': '๐ญ Thinking',
|
|
333
|
+
'๐ ่ฟ็จ่ฏฆๆ
๏ผๅทฅๅ
ท {tools} ยท ๆ่ {notes}๏ผ': '๐ Process details (tools {tools} ยท thinking {notes})',
|
|
326
334
|
'ๅทฒไธญๆญข': 'Aborted',
|
|
327
335
|
'ๅทฒๅๆถ': 'Cancelled',
|
|
328
336
|
'ๅทฒ็ปๆ': 'Ended',
|