@xmanrui/dsh-im 4.37.0 → 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/assets/logo-plugin-message-link-gradient.webp +0 -0
- package/lib/client.js +158 -62
- package/lib/index.js +305 -305
- 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/scripts/verify-package.mjs +11 -6
- package/src/channels/feishu/bridge.mjs +22 -10
- 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/shared/harness-approval.mjs +102 -2
- package/src/channels/shared/i18n-en/shared-c.mjs +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.38.0",
|
|
4
4
|
"icon": "./assets/logo-plugin-message-link-gradient.webp",
|
|
5
5
|
"description": "把微信、飞书、企微、钉钉、QQ、Telegram、WhatsApp、Slack、Discord、Matrix接入DSH。 Connect Telegram, WhatsApp, Slack, Discord, Matrix, WeChat, Feishu, WeCom, DingTalk, and QQ to DSH.",
|
|
6
6
|
"keywords": [
|
|
@@ -1,32 +1,45 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { h } from './i18n.js';
|
|
3
3
|
import { SET_MESSAGE_MODE_ENDPOINT, normalizeBusyMessageMode } from '../../src/channels/shared/message-mode.mjs';
|
|
4
|
+
import { normalizeFeishuStepCardPanels } from '../../src/channels/feishu/step-push-mode.mjs';
|
|
5
|
+
import { FEISHU_ENDPOINTS } from './channels/feishu/api.js';
|
|
4
6
|
|
|
5
|
-
function
|
|
6
|
-
if (result?.ok !== true) throw new Error(result?.error?.message || '
|
|
7
|
+
function settingsFromResult(result, botId) {
|
|
8
|
+
if (result?.ok !== true) throw new Error(result?.error?.message || '无法读取通用设置,请稍后重试。');
|
|
7
9
|
const bot = result.value?.bots?.find((entry) => entry.botId === botId);
|
|
8
10
|
if (!bot) throw new Error('找不到要修改的机器人。');
|
|
9
|
-
return
|
|
11
|
+
return {
|
|
12
|
+
busyMessageMode: normalizeBusyMessageMode(bot.busyMessageMode),
|
|
13
|
+
stepCardPanels: normalizeFeishuStepCardPanels(bot.stepCardPanels),
|
|
14
|
+
};
|
|
10
15
|
}
|
|
11
16
|
|
|
12
|
-
export function BotGeneralSettingsPage({ account, rpcCall }) {
|
|
13
|
-
const
|
|
14
|
-
const [
|
|
17
|
+
export function BotGeneralSettingsPage({ channel, account, rpcCall }) {
|
|
18
|
+
const modeId = React.useId();
|
|
19
|
+
const [settings, setSettings] = React.useState(() => ({
|
|
20
|
+
busyMessageMode: 'queue', stepCardPanels: normalizeFeishuStepCardPanels(),
|
|
21
|
+
}));
|
|
22
|
+
const [savedSettings, setSavedSettings] = React.useState(null);
|
|
15
23
|
const [busy, setBusy] = React.useState(false);
|
|
16
24
|
const [feedback, setFeedback] = React.useState(null);
|
|
17
25
|
const mounted = React.useRef(false);
|
|
26
|
+
const inFlight = React.useRef(false);
|
|
27
|
+
const mode = settings.busyMessageMode;
|
|
18
28
|
|
|
19
29
|
const load = React.useCallback(async () => {
|
|
30
|
+
if (inFlight.current) return;
|
|
31
|
+
inFlight.current = true;
|
|
20
32
|
setBusy(true);
|
|
21
33
|
setFeedback(null);
|
|
22
34
|
try {
|
|
23
|
-
const value =
|
|
35
|
+
const value = settingsFromResult(await rpcCall('connection.status', {}), account.botId);
|
|
24
36
|
if (!mounted.current) return;
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
setSettings(value);
|
|
38
|
+
setSavedSettings(value);
|
|
27
39
|
} catch (error) {
|
|
28
|
-
if (mounted.current) setFeedback(
|
|
40
|
+
if (mounted.current) setFeedback(error.message);
|
|
29
41
|
} finally {
|
|
42
|
+
inFlight.current = false;
|
|
30
43
|
if (mounted.current) setBusy(false);
|
|
31
44
|
}
|
|
32
45
|
}, [account.botId, rpcCall]);
|
|
@@ -37,47 +50,71 @@ export function BotGeneralSettingsPage({ account, rpcCall }) {
|
|
|
37
50
|
return () => { mounted.current = false; };
|
|
38
51
|
}, [load]);
|
|
39
52
|
|
|
40
|
-
const save = async () => {
|
|
53
|
+
const save = async (field, value) => {
|
|
54
|
+
if (inFlight.current || savedSettings === null || value === savedSettings[field]) return;
|
|
55
|
+
inFlight.current = true;
|
|
56
|
+
setSettings({ ...settings, [field]: value });
|
|
41
57
|
setBusy(true);
|
|
42
58
|
setFeedback(null);
|
|
43
59
|
try {
|
|
44
|
-
const
|
|
45
|
-
|
|
60
|
+
const endpoint = field === 'stepCardPanels' ? FEISHU_ENDPOINTS.setStepCardPanels : SET_MESSAGE_MODE_ENDPOINT;
|
|
61
|
+
const saved = settingsFromResult(await rpcCall(endpoint, {
|
|
62
|
+
botId: account.botId, [field]: value,
|
|
46
63
|
}), account.botId);
|
|
47
64
|
if (!mounted.current) return;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
setFeedback({ text: '已保存,对新收到的消息生效。' });
|
|
65
|
+
setSettings(saved);
|
|
66
|
+
setSavedSettings(saved);
|
|
51
67
|
} catch (error) {
|
|
52
|
-
if (mounted.current)
|
|
68
|
+
if (mounted.current) {
|
|
69
|
+
setSettings(savedSettings);
|
|
70
|
+
setFeedback(error.message);
|
|
71
|
+
}
|
|
53
72
|
} finally {
|
|
73
|
+
inFlight.current = false;
|
|
54
74
|
if (mounted.current) setBusy(false);
|
|
55
75
|
}
|
|
56
76
|
};
|
|
57
77
|
|
|
58
|
-
return h('section', { className: 'dim-
|
|
59
|
-
h('
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
return h('section', { className: 'dim-botGeneralSettings', 'aria-label': '机器人通用设置', 'aria-busy': busy },
|
|
79
|
+
h('div', { className: 'dim-botGeneralList' },
|
|
80
|
+
h('div', { className: 'dim-botGeneralRow' },
|
|
81
|
+
h('div', { className: 'dim-botGeneralText' },
|
|
82
|
+
h('label', { className: 'dim-botGeneralTitle', htmlFor: modeId }, '消息处理方式'),
|
|
83
|
+
h('p', { className: 'dim-botGeneralHelp', id: `${modeId}-description` }, mode === 'queue'
|
|
84
|
+
? '当前任务结束后处理新消息'
|
|
85
|
+
: '将新消息补充到当前任务')),
|
|
86
|
+
h('div', { className: 'dim-botGeneralControl' },
|
|
87
|
+
h('select', {
|
|
88
|
+
id: modeId, value: mode, disabled: busy || savedSettings === null,
|
|
89
|
+
'aria-label': '消息处理方式',
|
|
90
|
+
'aria-describedby': `${modeId}-description`,
|
|
91
|
+
onChange: (event) => void save('busyMessageMode', event.target.value),
|
|
92
|
+
},
|
|
93
|
+
h('option', { value: 'queue' }, '排队'),
|
|
94
|
+
h('option', { value: 'steer' }, '插话')),
|
|
95
|
+
h('svg', { width: 14, height: 14, viewBox: '0 0 16 16', fill: 'none', 'aria-hidden': true },
|
|
96
|
+
h('path', { d: 'm4 6 4 4 4-4', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' })))),
|
|
97
|
+
channel === 'feishu' ? [
|
|
98
|
+
['thinkingExpanded', '展开思考过程'],
|
|
99
|
+
['toolsExpanded', '展开工具摘要'],
|
|
100
|
+
].map(([field, label]) => h('div', { key: field, className: 'dim-botGeneralRow dim-botGeneralPanelRow' },
|
|
101
|
+
h('label', { className: 'dim-botGeneralTitle', htmlFor: `${modeId}-${field}` }, label),
|
|
102
|
+
h('div', { className: 'dim-botGeneralControl' },
|
|
103
|
+
h('input', {
|
|
104
|
+
id: `${modeId}-${field}`, type: 'checkbox', role: 'switch', className: 'dim-contextSwitch',
|
|
105
|
+
checked: settings.stepCardPanels[field], disabled: busy || savedSettings === null,
|
|
106
|
+
'aria-label': label, 'aria-describedby': `${modeId}-panels-description`,
|
|
107
|
+
onChange: (event) => void save('stepCardPanels', {
|
|
108
|
+
...settings.stepCardPanels, [field]: event.target.checked,
|
|
109
|
+
}),
|
|
110
|
+
})))) : null),
|
|
111
|
+
channel === 'feishu' ? h('p', { className: 'dim-botGeneralHelp dim-botGeneralPanelHelp', id: `${modeId}-panels-description` },
|
|
112
|
+
'仅对实时过程卡生效,任务结束后自动收起。') : null,
|
|
113
|
+
feedback ? h('p', {
|
|
114
|
+
className: 'dim-accessFeedback', 'data-tone': 'error', role: 'alert',
|
|
115
|
+
}, feedback) : null,
|
|
116
|
+
savedSettings === null && !busy ? h('div', { className: 'dim-botGeneralActions' },
|
|
117
|
+
h('button', {
|
|
118
|
+
type: 'button', className: 'dim-deliveryButton', onClick: () => void load(),
|
|
119
|
+
}, '重新读取')) : null);
|
|
83
120
|
}
|
|
@@ -14,7 +14,7 @@ import { normalizeModelCatalog, normalizeModelSelection, SET_MODEL_ENDPOINT } fr
|
|
|
14
14
|
import { normalizeLastMessageError } from "../../last-message-error.js";
|
|
15
15
|
import { normalizeAccessPolicy } from "../../../../src/channels/shared/access-policy.mjs";
|
|
16
16
|
import { normalizeContextEnhancementConfig } from "../../../../src/channels/shared/context-enhancement.mjs";
|
|
17
|
-
import { normalizeFeishuStepPushMode } from "../../../../src/channels/feishu/step-push-mode.mjs";
|
|
17
|
+
import { normalizeFeishuStepPushMode, normalizeFeishuStepCardPanels } from "../../../../src/channels/feishu/step-push-mode.mjs";
|
|
18
18
|
import { normalizeFeishuVoiceConfig } from "../../../../src/channels/feishu/voice-config.mjs";
|
|
19
19
|
import { normalizeSlashPanelConfig } from "../../../../src/channels/feishu/slash-command-panel.mjs";
|
|
20
20
|
|
|
@@ -42,6 +42,7 @@ export const FEISHU_ENDPOINTS = Object.freeze({
|
|
|
42
42
|
setMentionTopicReply: "bot.mention-topic-reply.set",
|
|
43
43
|
setStepPush: "bot.step-push.set",
|
|
44
44
|
setStepPushMode: "bot.step-push-mode.set",
|
|
45
|
+
setStepCardPanels: "bot.step-card-panels.set",
|
|
45
46
|
setVoice: "bot.voice.set",
|
|
46
47
|
setSlashPanel: "bot.slash-panel.set",
|
|
47
48
|
// Kept for rolling upgrades. The multi-bot UI never calls these endpoints.
|
|
@@ -233,6 +234,7 @@ export function normalizeBotConnection(value, fallbackBotId) {
|
|
|
233
234
|
mentionTopicReply: value.mentionTopicReply !== false,
|
|
234
235
|
stepPush: value.stepPush === true,
|
|
235
236
|
stepPushMode: normalizeFeishuStepPushMode(value.stepPushMode),
|
|
237
|
+
stepCardPanels: normalizeFeishuStepCardPanels(value.stepCardPanels),
|
|
236
238
|
voice: normalizeFeishuVoiceConfig(value.voice),
|
|
237
239
|
slashPanel: normalizeSlashPanelConfig(value.slashPanel),
|
|
238
240
|
groupMessagePermissionGranted: value.groupMessagePermissionGranted === true,
|
|
@@ -26,9 +26,9 @@ export const DELIVERY_ENDPOINTS = Object.freeze({
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
export const BOT_SETTINGS_TABS = Object.freeze([
|
|
29
|
+
Object.freeze({ id: 'general', label: '通用' }),
|
|
29
30
|
Object.freeze({ id: 'delivery', label: '投递设置' }),
|
|
30
31
|
Object.freeze({ id: 'access', label: '访问设置' }),
|
|
31
|
-
Object.freeze({ id: 'general', label: '通用' }),
|
|
32
32
|
]);
|
|
33
33
|
|
|
34
34
|
export const FEISHU_BOT_SETTINGS_TABS = Object.freeze([
|
|
@@ -781,7 +781,7 @@ export function DeliveryTargetSettingsPage({
|
|
|
781
781
|
'aria-labelledby': activeTabDomId,
|
|
782
782
|
},
|
|
783
783
|
activeTab.id === 'general'
|
|
784
|
-
? h(BotGeneralSettingsPage, { key: account.botId, account, rpcCall: accessRpcCall })
|
|
784
|
+
? h(BotGeneralSettingsPage, { key: `${channel}:${account.botId}`, channel, account, rpcCall: accessRpcCall })
|
|
785
785
|
: activeTab.id === 'access'
|
|
786
786
|
? h(AccessPolicySettingsPage, {
|
|
787
787
|
channel,
|
|
@@ -1028,14 +1028,15 @@ const EN = Object.freeze({
|
|
|
1028
1028
|
"通用": "General",
|
|
1029
1029
|
"处理中…": "Processing…",
|
|
1030
1030
|
"机器人通用设置": "General bot settings",
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1031
|
+
'展开思考过程': 'Expand thinking',
|
|
1032
|
+
'展开工具摘要': 'Expand tool summaries',
|
|
1033
|
+
'仅对实时过程卡生效,任务结束后自动收起。': 'Applies to streaming process cards only. Panels collapse when the task ends.',
|
|
1034
|
+
'请选择实时过程卡面板的展开状态。': 'Choose whether to expand the streaming process card panels.',
|
|
1035
|
+
"消息处理方式": "Message handling",
|
|
1036
|
+
"排队": "Queue",
|
|
1034
1037
|
"插话": "Steer",
|
|
1035
|
-
"
|
|
1036
|
-
"
|
|
1037
|
-
"没有正在运行的任务时,正常开始新任务。图片、文件、语音和带引用的消息按原有方式处理。": "When idle, start a new task normally. Images, files, voice and quoted messages keep their existing handling.",
|
|
1038
|
-
"已保存,对新收到的消息生效。": "Saved. Applies to newly received messages.",
|
|
1038
|
+
"当前任务结束后处理新消息": "Process new messages after the current task",
|
|
1039
|
+
"将新消息补充到当前任务": "Add new messages to the current task",
|
|
1039
1040
|
"无法读取消息处理设置,请稍后重试。": "Could not read message handling settings. Try again later.",
|
|
1040
1041
|
"请提交有效的消息处理方式。": "Choose a valid message handling mode.",
|
|
1041
1042
|
'通用设置': 'General settings',
|
|
@@ -633,6 +633,25 @@ const CSS = String.raw`
|
|
|
633
633
|
.dim-accessFeedback[data-tone="success"] { color: var(--dsw-alias-state-success-primary, #20a162); }
|
|
634
634
|
.dim-accessFeedback[data-tone="error"] { color: var(--dsw-alias-state-error-primary, #d54941); }
|
|
635
635
|
.dim-accessActions { display: flex; justify-content: flex-end; }
|
|
636
|
+
.dim-botGeneralSettings { min-width: 0; }
|
|
637
|
+
.dim-botGeneralList { min-width: 0; display: grid; }
|
|
638
|
+
.dim-botGeneralRow { min-width: 0; display: grid; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; gap: 24px; padding: 16px 0; border-bottom: 1px solid var(--dsw-alias-border-l2, #dfe1e5); }
|
|
639
|
+
.dim-botGeneralRow:last-child { border-bottom: 0; }
|
|
640
|
+
.dim-botGeneralPanelRow { border-bottom: 0; }
|
|
641
|
+
.dim-botGeneralText { min-width: 0; display: grid; gap: 4px; }
|
|
642
|
+
.dim-botGeneralTitle { color: var(--dsw-alias-label-primary, #1f2329); font-size: 14px; line-height: 22px; font-weight: 400; }
|
|
643
|
+
.dim-botGeneralHelp { margin: 0; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
|
|
644
|
+
.dim-botGeneralPanelHelp { padding-bottom: 16px; border-bottom: 1px solid var(--dsw-alias-border-l2, #dfe1e5); }
|
|
645
|
+
.dim-botGeneralControl { position: relative; min-width: 0; max-width: 100%; justify-self: end; color: var(--dsw-alias-label-primary, #1f2329); }
|
|
646
|
+
.dim-botGeneralControl select { display: block; min-width: 104px; max-width: 100%; height: 36px; padding: 0 36px 0 14px; border: 0; border-radius: 10px; color: inherit; background: var(--dsw-alias-bg-module-platform, #f5f6f7); font: inherit; font-size: 14px; line-height: 22px; appearance: none; cursor: pointer; }
|
|
647
|
+
.dim-botGeneralControl select:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, #eef0f3); }
|
|
648
|
+
.dim-botGeneralControl select:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 2px; }
|
|
649
|
+
.dim-botGeneralControl select:disabled { opacity: .5; cursor: not-allowed; }
|
|
650
|
+
.dim-botGeneralControl svg { position: absolute; top: 50%; right: 12px; transform: translateY(-50%); pointer-events: none; }
|
|
651
|
+
.dim-botGeneralControl .dim-contextSwitch { display: block; }
|
|
652
|
+
.dim-botGeneralControl .dim-contextSwitch:disabled { opacity: .5; cursor: not-allowed; }
|
|
653
|
+
.dim-botGeneralControl .dim-contextSwitch:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary, #3370ff); outline-offset: 3px; }
|
|
654
|
+
.dim-botGeneralActions { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 8px; padding-top: 16px; }
|
|
636
655
|
.dim-generalSettingsPage { min-width: 0; display: grid; }
|
|
637
656
|
.dim-generalSettingsHeader { min-width: 0; margin: 0 0 8px; }
|
|
638
657
|
.dim-generalSettingsHeader h2 { margin: 0; color: var(--dsw-alias-label-primary, #1f2329); font-size: 18px; line-height: 25px; font-weight: 680; }
|
|
@@ -260,6 +260,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
260
260
|
mentionTopicReply: botConfig.mentionTopicReply,
|
|
261
261
|
stepPush: botConfig.stepPush,
|
|
262
262
|
stepPushMode: botConfig.stepPushMode,
|
|
263
|
+
stepCardPanels: botConfig.stepCardPanels,
|
|
263
264
|
voice: voiceSource,
|
|
264
265
|
sessionSyncTargetsFor: sessionSyncTargetsFor,
|
|
265
266
|
ownerOpenIds: botConfig.ownerOpenIds ?? [botConfig.ownerOpenId],
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
import {
|
|
31
31
|
isFeishuStepPushMode,
|
|
32
32
|
normalizeFeishuStepPushMode,
|
|
33
|
+
isFeishuStepCardPanels,
|
|
34
|
+
normalizeFeishuStepCardPanels,
|
|
33
35
|
} from '../../../../src/channels/feishu/step-push-mode.mjs';
|
|
34
36
|
import { normalizeFeishuVoiceConfig } from '../../../../src/channels/feishu/voice-config.mjs';
|
|
35
37
|
import {
|
|
@@ -315,6 +317,7 @@ function publicBotEntry(entry) {
|
|
|
315
317
|
mentionTopicReply: source.mentionTopicReply !== false,
|
|
316
318
|
stepPush: source.stepPush === true,
|
|
317
319
|
stepPushMode: normalizeFeishuStepPushMode(source.stepPushMode),
|
|
320
|
+
stepCardPanels: normalizeFeishuStepCardPanels(source.stepCardPanels),
|
|
318
321
|
voice: normalizeFeishuVoiceConfig(source.voice),
|
|
319
322
|
slashPanel: normalizeSlashPanelConfig(source.slashPanel),
|
|
320
323
|
groupMessagePermissionGranted: source.groupMessagePermissionGranted === true,
|
|
@@ -504,6 +507,13 @@ function validPayload(endpoint, payload) {
|
|
|
504
507
|
? null
|
|
505
508
|
: '请选择分步直推的呈现方式。';
|
|
506
509
|
}
|
|
510
|
+
if (endpoint === FEISHU_ENDPOINTS.setStepCardPanels) {
|
|
511
|
+
return hasOnlyKeys(payload, new Set(['botId', 'stepCardPanels']))
|
|
512
|
+
&& safeOpaqueId(payload.botId)
|
|
513
|
+
&& isFeishuStepCardPanels(payload.stepCardPanels)
|
|
514
|
+
? null
|
|
515
|
+
: '请选择实时过程卡面板的展开状态。';
|
|
516
|
+
}
|
|
507
517
|
if (endpoint === FEISHU_ENDPOINTS.setVoice) {
|
|
508
518
|
const voice = payload?.voice;
|
|
509
519
|
const validVoice = voice === null
|
|
@@ -835,6 +845,14 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
|
|
|
835
845
|
await controller.updateStepPushMode(payload.botId, payload.stepPushMode),
|
|
836
846
|
{ encodeQr: cachedEncodeQr },
|
|
837
847
|
);
|
|
848
|
+
} else if (endpoint === FEISHU_ENDPOINTS.setStepCardPanels) {
|
|
849
|
+
if (typeof controller.updateStepCardPanels !== 'function') {
|
|
850
|
+
throw new Error('Step card panel update is unavailable');
|
|
851
|
+
}
|
|
852
|
+
value = await toPublicFeishuStatus(
|
|
853
|
+
await controller.updateStepCardPanels(payload.botId, payload.stepCardPanels),
|
|
854
|
+
{ encodeQr: cachedEncodeQr },
|
|
855
|
+
);
|
|
838
856
|
} else if (endpoint === FEISHU_ENDPOINTS.setVoice) {
|
|
839
857
|
if (typeof controller.updateVoice !== 'function') {
|
|
840
858
|
throw new Error('Voice update is unavailable');
|
|
@@ -148,27 +148,32 @@ if (client.includes('settings.plugins.tab') || clientSources.includes('settings.
|
|
|
148
148
|
}
|
|
149
149
|
// Connections still have no channel-enable toggle. Checkable inputs are owned
|
|
150
150
|
// only by the shared context editor, the saved-target Session sync row, the
|
|
151
|
-
// email settings,
|
|
151
|
+
// email settings, the Telegram thinking-traces block, and Feishu panel settings.
|
|
152
152
|
// The context editor contains one switch template and one mapped field-input
|
|
153
153
|
// template; the delivery target adds one ordinary checkbox template; the
|
|
154
|
-
// Telegram thinking-traces block adds one ordinary checkbox template.
|
|
154
|
+
// Telegram thinking-traces block adds one ordinary checkbox template. General
|
|
155
|
+
// settings adds one mapped switch template for the two Feishu panel preferences.
|
|
155
156
|
const contextEditorSource = await readFile(resolve(root, 'plugin-src/client/context-enhancement.js'), 'utf8');
|
|
156
157
|
const deliverySettingsSource = await readFile(resolve(root, 'plugin-src/client/delivery-settings.js'), 'utf8');
|
|
157
158
|
const emailSettingsSource = await readFile(resolve(root, 'plugin-src/client/channels/email/index.js'), 'utf8');
|
|
158
159
|
const thinkingTracesSource = await readFile(resolve(root, 'plugin-src/client/channels/telegram/thinking-traces.js'), 'utf8');
|
|
160
|
+
const botGeneralSettingsSource = await readFile(resolve(root, 'plugin-src/client/bot-general-settings.js'), 'utf8');
|
|
159
161
|
const otherClientSources = clientSources
|
|
160
162
|
.replace(contextEditorSource, '')
|
|
161
163
|
.replace(deliverySettingsSource, '')
|
|
162
164
|
.replace(emailSettingsSource, '')
|
|
163
|
-
.replace(thinkingTracesSource, '')
|
|
165
|
+
.replace(thinkingTracesSource, '')
|
|
166
|
+
.replace(botGeneralSettingsSource, '');
|
|
164
167
|
if (/role:\s*["']switch|type:\s*["']checkbox/.test(otherClientSources)
|
|
165
168
|
|| (deliverySettingsSource.match(/type:\s*["']checkbox["']/g) ?? []).length !== 1
|
|
166
169
|
|| /role:\s*["']switch["']/u.test(deliverySettingsSource)
|
|
167
170
|
|| (thinkingTracesSource.match(/type:\s*["']checkbox["']/g) ?? []).length !== 1
|
|
168
171
|
|| /role:\s*["']switch["']/u.test(thinkingTracesSource)
|
|
169
|
-
|| (
|
|
170
|
-
|| (
|
|
171
|
-
|
|
172
|
+
|| (botGeneralSettingsSource.match(/type:\s*["']checkbox["']/g) ?? []).length !== 1
|
|
173
|
+
|| (botGeneralSettingsSource.match(/role:\s*["']switch["']/g) ?? []).length !== 1
|
|
174
|
+
|| (client.match(/role:\s*["']switch["']/g) ?? []).length !== 2
|
|
175
|
+
|| (client.match(/type:\s*["']checkbox["']/g) ?? []).length !== 5) {
|
|
176
|
+
throw new Error('checkable inputs must be limited to context enhancement, Session sync, email settings, Telegram thinking traces, and Feishu panel settings');
|
|
172
177
|
}
|
|
173
178
|
for (const marker of ['bot.context-enhancement.set', '<dsh_im_source>', '<dsh_im_source_guidance>']) {
|
|
174
179
|
if (!host.includes(marker) || !client.includes(marker)) {
|
|
@@ -122,6 +122,7 @@ import {
|
|
|
122
122
|
import {
|
|
123
123
|
FEISHU_STEP_PUSH_MODES,
|
|
124
124
|
normalizeFeishuStepPushMode,
|
|
125
|
+
normalizeFeishuStepCardPanels,
|
|
125
126
|
} from './step-push-mode.mjs';
|
|
126
127
|
import { FeishuLiveCot } from './live-cot.mjs';
|
|
127
128
|
|
|
@@ -680,6 +681,7 @@ export class FeishuHarnessBridge {
|
|
|
680
681
|
#stepPush = false;
|
|
681
682
|
/** Step push presentation: discrete posts, a CardKit card, or native live CoT. */
|
|
682
683
|
#stepPushMode = FEISHU_STEP_PUSH_MODES.POST;
|
|
684
|
+
#stepCardPanels;
|
|
683
685
|
/** Per-conversation step push state: key → { lastSentAt, count, breakerLogged }. */
|
|
684
686
|
#stepPushSendState = new Map();
|
|
685
687
|
/** Live streaming step cards: key → per-turn card state (streaming_card mode). */
|
|
@@ -745,6 +747,7 @@ export class FeishuHarnessBridge {
|
|
|
745
747
|
mentionTopicReply = true,
|
|
746
748
|
stepPush = false,
|
|
747
749
|
stepPushMode = FEISHU_STEP_PUSH_MODES.POST,
|
|
750
|
+
stepCardPanels,
|
|
748
751
|
stepPushClock = null,
|
|
749
752
|
repair,
|
|
750
753
|
repairPollIntervalMs = REPAIR_POLL_INTERVAL_MS,
|
|
@@ -805,6 +808,7 @@ export class FeishuHarnessBridge {
|
|
|
805
808
|
this.#mentionTopicReply = mentionTopicReply !== false;
|
|
806
809
|
this.#stepPush = stepPush === true;
|
|
807
810
|
this.#stepPushMode = normalizeFeishuStepPushMode(stepPushMode);
|
|
811
|
+
this.#stepCardPanels = normalizeFeishuStepCardPanels(stepCardPanels);
|
|
808
812
|
this.#stepPushClock = stepPushClock ?? DEFAULT_STEP_PUSH_CLOCK;
|
|
809
813
|
this.#voice = voice ?? null;
|
|
810
814
|
this.#repair = repair ?? null;
|
|
@@ -901,6 +905,10 @@ export class FeishuHarnessBridge {
|
|
|
901
905
|
this.#stepPushMode = normalizeFeishuStepPushMode(value);
|
|
902
906
|
}
|
|
903
907
|
|
|
908
|
+
setStepCardPanels(value) {
|
|
909
|
+
this.#stepCardPanels = normalizeFeishuStepCardPanels(value);
|
|
910
|
+
}
|
|
911
|
+
|
|
904
912
|
setVoice(voice) {
|
|
905
913
|
this.#voice = voice ?? null;
|
|
906
914
|
}
|
|
@@ -4845,7 +4853,7 @@ export class FeishuHarnessBridge {
|
|
|
4845
4853
|
* their lifecycle is owned by the ask path, not the mirror recovery.
|
|
4846
4854
|
* lastContent keeps stepStreamCard's raw JSON string (single-encoded).
|
|
4847
4855
|
*/
|
|
4848
|
-
async #persistMirrorState(card, liveBlocks, status) {
|
|
4856
|
+
async #persistMirrorState(card, liveBlocks, status, stepCardPanels) {
|
|
4849
4857
|
const sessionId = card.sessionSyncSessionId;
|
|
4850
4858
|
if (!sessionId || typeof this.#state.setMirror !== 'function') return;
|
|
4851
4859
|
await this.#state.setMirror(card.sessionSyncKey, {
|
|
@@ -4853,7 +4861,7 @@ export class FeishuHarnessBridge {
|
|
|
4853
4861
|
chatId: card.chatId,
|
|
4854
4862
|
cardIds: [...card.cardIds],
|
|
4855
4863
|
claimedAt: Date.now(),
|
|
4856
|
-
lastContent: stepStreamCard(liveBlocks, { status }),
|
|
4864
|
+
lastContent: stepStreamCard(liveBlocks, { status, stepCardPanels }),
|
|
4857
4865
|
blocks: structuredClone(card.blocks), answerStart: card.answerStart, answerEnd: card.answerEnd,
|
|
4858
4866
|
lastSeq: this.#sessionSyncTurns.get(card.sessionSyncKey)?.lastSeq ?? -1,
|
|
4859
4867
|
pendingStep: this.#sessionSyncTurns.get(card.sessionSyncKey)?.pendingStep ?? null,
|
|
@@ -4861,7 +4869,7 @@ export class FeishuHarnessBridge {
|
|
|
4861
4869
|
}
|
|
4862
4870
|
|
|
4863
4871
|
/** Sync only this interaction's cards; earlier interactions remain sealed. */
|
|
4864
|
-
async #syncStepCardChunks(card, chunks, status) {
|
|
4872
|
+
async #syncStepCardChunks(card, chunks, status, stepCardPanels) {
|
|
4865
4873
|
const answerVersion = card.answerVersion ?? 0;
|
|
4866
4874
|
const activeIds = card.cardIds.slice(card.activeCardStart);
|
|
4867
4875
|
const groups = chunks.length ? [...chunks] : [[]];
|
|
@@ -4874,6 +4882,7 @@ export class FeishuHarnessBridge {
|
|
|
4874
4882
|
// payload halfway through this render or be marked as already delivered.
|
|
4875
4883
|
const contents = groups.map((blocks, index) => stepStreamCard(blocks, {
|
|
4876
4884
|
status: index === groups.length - 1 ? status : 'sealed',
|
|
4885
|
+
stepCardPanels,
|
|
4877
4886
|
}));
|
|
4878
4887
|
for (let index = 0; index < contents.length; index += 1) {
|
|
4879
4888
|
let id = activeIds[index];
|
|
@@ -4898,12 +4907,13 @@ export class FeishuHarnessBridge {
|
|
|
4898
4907
|
|
|
4899
4908
|
async #renderStepCardNow(chatId, card) {
|
|
4900
4909
|
if (card.broken) return;
|
|
4901
|
-
const
|
|
4910
|
+
const stepCardPanels = this.#stepCardPanels;
|
|
4911
|
+
const chunks = splitStepStreamCardBlocks(card.blocks, undefined, undefined, stepCardPanels);
|
|
4902
4912
|
if (!chunks.length && card.messageId === null) return;
|
|
4903
4913
|
const live = chunks[chunks.length - 1] ?? [];
|
|
4904
4914
|
try {
|
|
4905
|
-
await this.#syncStepCardChunks(card, chunks, 'running');
|
|
4906
|
-
await this.#persistMirrorState(card, live, 'running');
|
|
4915
|
+
await this.#syncStepCardChunks(card, chunks, 'running', stepCardPanels);
|
|
4916
|
+
await this.#persistMirrorState(card, live, 'running', stepCardPanels);
|
|
4907
4917
|
card.lastRenderAt = this.#stepPushClock.now();
|
|
4908
4918
|
} catch (error) {
|
|
4909
4919
|
card.broken = true;
|
|
@@ -4943,9 +4953,10 @@ export class FeishuHarnessBridge {
|
|
|
4943
4953
|
if (body.trim()) {
|
|
4944
4954
|
this.#writeStepCardAnswer(card, body);
|
|
4945
4955
|
}
|
|
4946
|
-
const
|
|
4956
|
+
const stepCardPanels = this.#stepCardPanels;
|
|
4957
|
+
const chunks = splitStepStreamCardBlocks(card.blocks, undefined, undefined, stepCardPanels);
|
|
4947
4958
|
try {
|
|
4948
|
-
await this.#syncStepCardChunks(card, chunks, status);
|
|
4959
|
+
await this.#syncStepCardChunks(card, chunks, status, stepCardPanels);
|
|
4949
4960
|
return { ok: true, cardIds: card.cardIds };
|
|
4950
4961
|
} catch (error) {
|
|
4951
4962
|
// Only trust the streamed draft when the LAST SUCCESSFUL render actually
|
|
@@ -4996,8 +5007,9 @@ export class FeishuHarnessBridge {
|
|
|
4996
5007
|
if (card.broken) return;
|
|
4997
5008
|
if (card.messageId === null) return;
|
|
4998
5009
|
try {
|
|
4999
|
-
const
|
|
5000
|
-
|
|
5010
|
+
const stepCardPanels = this.#stepCardPanels;
|
|
5011
|
+
const chunks = splitStepStreamCardBlocks(card.blocks, undefined, undefined, stepCardPanels);
|
|
5012
|
+
await this.#syncStepCardChunks(card, chunks, 'sealed', stepCardPanels);
|
|
5001
5013
|
} catch (error) {
|
|
5002
5014
|
this.#logger.warn?.(
|
|
5003
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
|
|
|
@@ -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,
|