@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
package/locale/en.json
CHANGED
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.38.1",
|
|
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');
|
|
@@ -404,7 +404,15 @@ export function createDeliveryAdapter({ channel, workspaces, coreController, sta
|
|
|
404
404
|
if (typeof coreController.sendProactiveText !== 'function') {
|
|
405
405
|
throw new TypeError('delivery controller cannot send proactive text');
|
|
406
406
|
}
|
|
407
|
-
await coreController.sendProactiveText(botId, target, text, options);
|
|
407
|
+
const result = await coreController.sendProactiveText(botId, target, text, options);
|
|
408
|
+
// Rich senders may report failure without throwing. Never report these
|
|
409
|
+
// as success, or retry an uncertain send that may already be visible.
|
|
410
|
+
if (result === false || result?.sent === false
|
|
411
|
+
|| result?.deliveryOutcome === 'failed' || result?.deliveryOutcome === 'unknown') {
|
|
412
|
+
const error = new Error('Session sync delivery was not confirmed');
|
|
413
|
+
error.code = result?.deliveryOutcome === 'unknown' ? 'send-result-unknown' : 'delivery-failed';
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
408
416
|
return { sent: true };
|
|
409
417
|
},
|
|
410
418
|
});
|
|
@@ -341,11 +341,12 @@ export class DeliveryService {
|
|
|
341
341
|
return false;
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
async sendSessionSyncText(botId, targetId, sessionId, text, { signal } = {}) {
|
|
344
|
+
async sendSessionSyncText(botId, targetId, sessionId, text, { signal, format = 'plain' } = {}) {
|
|
345
345
|
const id = botIdOf(botId);
|
|
346
346
|
const targetKey = targetIdOf(targetId);
|
|
347
347
|
if (typeof sessionId !== 'string' || !sessionId
|
|
348
|
-
|| typeof text !== 'string' || !text.trim()
|
|
348
|
+
|| typeof text !== 'string' || !text.trim()
|
|
349
|
+
|| (format !== 'plain' && format !== 'markdown')) {
|
|
349
350
|
throw deliveryError('bad-request', 'Invalid session sync delivery');
|
|
350
351
|
}
|
|
351
352
|
cancellation(signal);
|
|
@@ -354,7 +355,7 @@ export class DeliveryService {
|
|
|
354
355
|
throw deliveryError('session-sync-unavailable', 'Session sync is unavailable');
|
|
355
356
|
}
|
|
356
357
|
try {
|
|
357
|
-
await adapter.sendSessionSyncText(id, targetKey, sessionId, text, { signal });
|
|
358
|
+
await adapter.sendSessionSyncText(id, targetKey, sessionId, text, { signal, format });
|
|
358
359
|
return { sent: true };
|
|
359
360
|
} catch (error) {
|
|
360
361
|
if (signal?.aborted || error?.name === 'AbortError' || error?.code === 'ABORT_ERR') {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import { deliverSessionSyncMirror } from '../../src/channels/shared/session-sync-registry.mjs';
|
|
7
7
|
|
|
8
8
|
const DSH_USER_PREFIX = '[来自 DSH]\n';
|
|
9
|
-
const DSH_ASSISTANT_PREFIX = '[DSH 助手]\n';
|
|
9
|
+
const DSH_ASSISTANT_PREFIX = '[DSH 助手]\n\n';
|
|
10
10
|
|
|
11
11
|
function completedTurn(reason) {
|
|
12
12
|
return (typeof reason === 'string' ? reason : reason?.kind) === 'completed';
|
|
@@ -55,7 +55,7 @@ export function createSessionSyncCoordinator({ deliveryService, logger = console
|
|
|
55
55
|
);
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
const deliver = async (sessionId, targets, text, phase) => {
|
|
58
|
+
const deliver = async (sessionId, targets, text, phase, format = 'plain') => {
|
|
59
59
|
const unique = new Map();
|
|
60
60
|
for (const target of targets) {
|
|
61
61
|
if (validRecipient(target)) unique.set(recipientKey(target), target);
|
|
@@ -67,6 +67,9 @@ export function createSessionSyncCoordinator({ deliveryService, logger = console
|
|
|
67
67
|
target.targetId,
|
|
68
68
|
sessionId,
|
|
69
69
|
text,
|
|
70
|
+
// Feishu has already tried its dedicated mirror card. Preserve its
|
|
71
|
+
// existing plain fallback instead of attempting another rich card.
|
|
72
|
+
{ format: target.channel === 'feishu' ? 'plain' : format },
|
|
70
73
|
)
|
|
71
74
|
)));
|
|
72
75
|
const successful = new Map();
|
|
@@ -165,13 +168,14 @@ export function createSessionSyncCoordinator({ deliveryService, logger = console
|
|
|
165
168
|
return false;
|
|
166
169
|
}
|
|
167
170
|
}));
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
171
|
+
const fallback = recipients.filter((_target, index) => !rendered[index]);
|
|
172
|
+
if (fallback.length === 0) return;
|
|
170
173
|
await deliver(
|
|
171
174
|
sessionId,
|
|
172
|
-
|
|
175
|
+
fallback,
|
|
173
176
|
`${DSH_ASSISTANT_PREFIX}${state.assistant.text}`,
|
|
174
177
|
'assistant delivery',
|
|
178
|
+
'markdown',
|
|
175
179
|
);
|
|
176
180
|
};
|
|
177
181
|
|
|
@@ -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)) {
|
|
@@ -3,6 +3,7 @@ import { extname } from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
import { fetchImageBuffer, ImagePromptError } from '../shared/image-prompt.mjs';
|
|
5
5
|
import { t } from '../shared/i18n.mjs';
|
|
6
|
+
import { chunkMarkdownParts } from '../shared/markdown-chunks.mjs';
|
|
6
7
|
import { DINGTALK_MENU_TEMPLATE_ID, dingtalkMenuCardData } from './dingtalk-menu.mjs';
|
|
7
8
|
|
|
8
9
|
export const DINGTALK_REGISTRATION_BASE_URL = 'https://oapi.dingtalk.com/';
|
|
@@ -402,6 +403,7 @@ function positiveNumber(value, fallback) {
|
|
|
402
403
|
}
|
|
403
404
|
|
|
404
405
|
export function createDingtalkApi({
|
|
406
|
+
logger = console,
|
|
405
407
|
fetchImpl = fetch,
|
|
406
408
|
registrationBaseUrl = process.env.DINGTALK_REGISTRATION_BASE_URL
|
|
407
409
|
|| DINGTALK_REGISTRATION_BASE_URL,
|
|
@@ -1075,37 +1077,52 @@ export function createDingtalkApi({
|
|
|
1075
1077
|
return true;
|
|
1076
1078
|
},
|
|
1077
1079
|
|
|
1078
|
-
async sendRobotText({ clientId, clientSecret, target, text, signal }) {
|
|
1080
|
+
async sendRobotText({ clientId, clientSecret, target, text, signal, format = 'plain' }) {
|
|
1079
1081
|
if (typeof text !== 'string' || !text.trim()) throw new TypeError('text is required');
|
|
1080
|
-
const content = text;
|
|
1081
1082
|
const normalizedTarget = normalizeFileTarget(target);
|
|
1082
1083
|
const token = await accessToken({ clientId, clientSecret, signal });
|
|
1083
|
-
const body = {
|
|
1084
|
-
robotCode: normalizedTarget.robotCode,
|
|
1085
|
-
msgKey: 'sampleText',
|
|
1086
|
-
msgParam: JSON.stringify({ content }),
|
|
1087
|
-
...(normalizedTarget.type === 'group'
|
|
1088
|
-
? { openConversationId: normalizedTarget.openConversationId }
|
|
1089
|
-
: { userIds: [normalizedTarget.userId] }),
|
|
1090
|
-
};
|
|
1091
1084
|
const pathname = normalizedTarget.type === 'group'
|
|
1092
1085
|
? 'v1.0/robot/groupMessages/send'
|
|
1093
1086
|
: 'v1.0/robot/oToMessages/batchSend';
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
'
|
|
1105
|
-
|
|
1106
|
-
|
|
1087
|
+
const send = async (content, markdown) => {
|
|
1088
|
+
const response = await requestJson(fetchImpl, endpoint(apiBase, pathname), {
|
|
1089
|
+
body: {
|
|
1090
|
+
robotCode: normalizedTarget.robotCode,
|
|
1091
|
+
msgKey: markdown ? 'sampleMarkdown' : 'sampleText',
|
|
1092
|
+
msgParam: JSON.stringify(markdown ? { title: 'DSH', text: content } : { content }),
|
|
1093
|
+
...(normalizedTarget.type === 'group'
|
|
1094
|
+
? { openConversationId: normalizedTarget.openConversationId }
|
|
1095
|
+
: { userIds: [normalizedTarget.userId] }),
|
|
1096
|
+
},
|
|
1097
|
+
headers: { 'x-acs-dingtalk-access-token': token },
|
|
1098
|
+
signal,
|
|
1099
|
+
action: '主动文字消息发送',
|
|
1100
|
+
});
|
|
1101
|
+
const rejection = rejectedProviderResponse(response);
|
|
1102
|
+
if (rejection) {
|
|
1103
|
+
throw new DingtalkApiError('send-rejected', '钉钉服务拒绝了主动文字消息。', {
|
|
1104
|
+
providerCode: rejection,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
return response;
|
|
1108
|
+
};
|
|
1109
|
+
if (format !== 'markdown') return send(text, false);
|
|
1110
|
+
// Conservative Unicode payload size; reuse QQ's fence/table-aware split.
|
|
1111
|
+
const chunks = chunkMarkdownParts(text, 1_000);
|
|
1112
|
+
let result;
|
|
1113
|
+
for (const chunk of chunks) {
|
|
1114
|
+
try {
|
|
1115
|
+
result = await send(chunk.markdown, true);
|
|
1116
|
+
} catch (error) {
|
|
1117
|
+
// A missing message model is a definite format rejection. Do not
|
|
1118
|
+
// retry auth/target failures, rate limits or ambiguous transport errors.
|
|
1119
|
+
if (signal?.aborted || error?.providerCode !== 'sendMessage.model.notMatch'
|
|
1120
|
+
|| !(error.code === 'send-rejected' || error.status === 400)) throw error;
|
|
1121
|
+
logger.warn?.('[dsh-im:dingtalk] Markdown rejected; sending this chunk as text');
|
|
1122
|
+
if (chunk.plain) result = await send(chunk.plain, false);
|
|
1123
|
+
}
|
|
1107
1124
|
}
|
|
1108
|
-
return
|
|
1125
|
+
return result;
|
|
1109
1126
|
},
|
|
1110
1127
|
|
|
1111
1128
|
async sendFile(request) {
|
|
@@ -163,7 +163,7 @@ export class DingtalkRuntime {
|
|
|
163
163
|
maxMessageChars = 4_000,
|
|
164
164
|
connectTimeoutMs = 15_000,
|
|
165
165
|
connectPollIntervalMs = 25,
|
|
166
|
-
api = createDingtalkApi(),
|
|
166
|
+
api = createDingtalkApi({ logger }),
|
|
167
167
|
streamFactory = defaultStreamFactory,
|
|
168
168
|
}) {
|
|
169
169
|
if (!config || !nonEmptyString(config.clientId) || !nonEmptyString(clientSecret)) {
|
|
@@ -443,7 +443,7 @@ export class DingtalkRuntime {
|
|
|
443
443
|
}, options);
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
-
async sendProactiveText(target, text, { signal } = {}) {
|
|
446
|
+
async sendProactiveText(target, text, { signal, format = 'plain' } = {}) {
|
|
447
447
|
const userId = typeof target?.route?.userId === 'string'
|
|
448
448
|
? target.route.userId.trim() : '';
|
|
449
449
|
const openConversationId = typeof target?.route?.openConversationId === 'string'
|
|
@@ -471,6 +471,7 @@ export class DingtalkRuntime {
|
|
|
471
471
|
...(target.kind === 'user' ? { userId } : { openConversationId }),
|
|
472
472
|
},
|
|
473
473
|
text,
|
|
474
|
+
format,
|
|
474
475
|
signal: signal ?? this.#abortController.signal,
|
|
475
476
|
});
|
|
476
477
|
} catch (cause) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { extractConnectionEvidence, createConnectionDiagnostics, atConnectionStage } from '../shared/connection-error.mjs';
|
|
2
2
|
import { createEditableMessageStream, splitMessageText } from '../shared/editable-message-stream.mjs';
|
|
3
|
+
import { chunkMarkdownText } from '../shared/markdown-chunks.mjs';
|
|
4
|
+
import { createTextDeliveryBlock } from '../shared/semantic/delivery.mjs';
|
|
3
5
|
import { fetchFileStream } from '../shared/file-download.mjs';
|
|
4
6
|
import { fetchImageBuffer } from '../shared/image-prompt.mjs';
|
|
5
7
|
import { t } from '../shared/i18n.mjs';
|
|
@@ -428,10 +430,19 @@ export class DiscordBotClient {
|
|
|
428
430
|
this.#signal = signal;
|
|
429
431
|
}
|
|
430
432
|
|
|
431
|
-
|
|
433
|
+
sendText(target, text) {
|
|
434
|
+
return this.#sendText(target, text, false);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
sendDelivery(target, value) {
|
|
438
|
+
const block = createTextDeliveryBlock(value);
|
|
439
|
+
return this.#sendText(target, block.text, block.format === 'markdown');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async #sendText(target, text, markdown) {
|
|
432
443
|
const notice = !this.#deliveredNotices.has(target) && target?.notice
|
|
433
444
|
? String(target.notice) : null;
|
|
434
|
-
const chunks = splitMessageText(notice ? `${notice}\n\n${text}` : text, 1_900);
|
|
445
|
+
const chunks = (markdown ? chunkMarkdownText : splitMessageText)(notice ? `${notice}\n\n${text}` : text, 1_900);
|
|
435
446
|
const providerMessageIds = [];
|
|
436
447
|
for (const [index, chunk] of chunks.entries()) {
|
|
437
448
|
const result = await this.#api.createMessage({
|