@xmanrui/dsh-im 4.28.1 → 4.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +5 -3
- package/README.md +5 -3
- package/lib/client.js +370 -115
- package/lib/index.js +286 -286
- package/package.json +5 -1
- package/plugin-src/client/channels/feishu/api.js +3 -0
- package/plugin-src/client/channels/feishu/voice-editor.js +115 -0
- package/plugin-src/client/channels/feishu/voice-settings.js +71 -0
- package/plugin-src/client/delivery-settings.js +8 -0
- package/plugin-src/client/i18n.js +16 -0
- package/plugin-src/client/styles.js +11 -0
- package/plugin-src/host/channels/feishu/production.mjs +11 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +21 -0
- package/src/channels/feishu/bridge.mjs +158 -11
- package/src/channels/feishu/feishu-cards.mjs +4 -2
- package/src/channels/feishu/feishu-runtime.mjs +16 -0
- package/src/channels/feishu/multi-bot-controller.mjs +22 -0
- package/src/channels/feishu/plugin-config-store.mjs +2 -0
- package/src/channels/feishu/voice-config.mjs +34 -0
- package/src/channels/feishu/voice.mjs +134 -0
- package/src/channels/shared/harness-approval.mjs +24 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmanrui/dsh-im",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.29.0",
|
|
4
4
|
"description": "把十二种 IM 渠道和公网 AI Office 接入本机 DeepSeek Harness。 Connect twelve IM channels and a public AI Office to a local DeepSeek Harness.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -85,6 +85,10 @@
|
|
|
85
85
|
{
|
|
86
86
|
"name": "Zhe (Phil) Yang",
|
|
87
87
|
"url": "https://github.com/yangzhe1991"
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"name": "Biertolovisk",
|
|
91
|
+
"url": "https://github.com/rosalynthompson32-a11y"
|
|
88
92
|
}
|
|
89
93
|
],
|
|
90
94
|
"license": "MIT",
|
|
@@ -14,6 +14,7 @@ import { normalizeLastMessageError } from "../../last-message-error.js";
|
|
|
14
14
|
import { normalizeAccessPolicy } from "../../../../src/channels/shared/access-policy.mjs";
|
|
15
15
|
import { normalizeContextEnhancementConfig } from "../../../../src/channels/shared/context-enhancement.mjs";
|
|
16
16
|
import { normalizeFeishuStepPushMode } from "../../../../src/channels/feishu/step-push-mode.mjs";
|
|
17
|
+
import { normalizeFeishuVoiceConfig } from "../../../../src/channels/feishu/voice-config.mjs";
|
|
17
18
|
|
|
18
19
|
export const FEISHU_RPC_CHANNEL = "/feishu";
|
|
19
20
|
|
|
@@ -38,6 +39,7 @@ export const FEISHU_ENDPOINTS = Object.freeze({
|
|
|
38
39
|
setGroupTopicReply: "bot.group-topic-reply.set",
|
|
39
40
|
setStepPush: "bot.step-push.set",
|
|
40
41
|
setStepPushMode: "bot.step-push-mode.set",
|
|
42
|
+
setVoice: "bot.voice.set",
|
|
41
43
|
// Kept for rolling upgrades. The multi-bot UI never calls these endpoints.
|
|
42
44
|
testConnection: "connection.test",
|
|
43
45
|
disconnect: "connection.disconnect",
|
|
@@ -227,6 +229,7 @@ export function normalizeBotConnection(value, fallbackBotId) {
|
|
|
227
229
|
groupTopicReply: value.groupTopicReply === true,
|
|
228
230
|
stepPush: value.stepPush === true,
|
|
229
231
|
stepPushMode: normalizeFeishuStepPushMode(value.stepPushMode),
|
|
232
|
+
voice: normalizeFeishuVoiceConfig(value.voice),
|
|
230
233
|
groupMessagePermissionGranted: value.groupMessagePermissionGranted === true,
|
|
231
234
|
bot: normalizeBot(value.bot),
|
|
232
235
|
health: normalizeHealth(value.health, connected),
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { h } from '../../i18n.js';
|
|
4
|
+
|
|
5
|
+
// 语音交互编辑器:开关 + 凭据引用 + 模型/音色参数。
|
|
6
|
+
// 凭据值由插件凭据库管理,这里只保存 secretRef 名称——密钥本身不进入配置,
|
|
7
|
+
// 也不经本表单传输。开启后:收到语音消息时转写为文字走正常流水线,并用
|
|
8
|
+
// 语音回复同回合答案(需 DashScope qwen3 系列密钥与可用 ffmpeg)。
|
|
9
|
+
export function VoiceEditor({ value = null, disabled = false, onSave }) {
|
|
10
|
+
const [enabled, setEnabled] = React.useState(value != null);
|
|
11
|
+
const [secretRef, setSecretRef] = React.useState(value?.secretRef ?? 'DASHSCOPE_API_KEY');
|
|
12
|
+
const [asrModel, setAsrModel] = React.useState(value?.asrModel ?? 'qwen3-asr-flash');
|
|
13
|
+
const [ttsModel, setTtsModel] = React.useState(value?.ttsModel ?? 'qwen3-tts-flash');
|
|
14
|
+
const [ttsVoice, setTtsVoice] = React.useState(value?.ttsVoice ?? 'Momo');
|
|
15
|
+
const [saving, setSaving] = React.useState(false);
|
|
16
|
+
const [error, setError] = React.useState(null);
|
|
17
|
+
const titleId = React.useId();
|
|
18
|
+
const helpId = `${titleId}-help`;
|
|
19
|
+
|
|
20
|
+
React.useEffect(() => {
|
|
21
|
+
setEnabled(value != null);
|
|
22
|
+
if (value) {
|
|
23
|
+
setSecretRef(value.secretRef ?? 'DASHSCOPE_API_KEY');
|
|
24
|
+
setAsrModel(value.asrModel ?? 'qwen3-asr-flash');
|
|
25
|
+
setTtsModel(value.ttsModel ?? 'qwen3-tts-flash');
|
|
26
|
+
setTtsVoice(value.ttsVoice ?? 'Momo');
|
|
27
|
+
}
|
|
28
|
+
}, [value]);
|
|
29
|
+
|
|
30
|
+
const persist = async (payload) => {
|
|
31
|
+
if (saving || disabled) return;
|
|
32
|
+
setSaving(true);
|
|
33
|
+
setError(null);
|
|
34
|
+
try {
|
|
35
|
+
await onSave?.(payload);
|
|
36
|
+
} catch (cause) {
|
|
37
|
+
setError(cause?.message ?? '语音设置保存失败,请重试。');
|
|
38
|
+
} finally {
|
|
39
|
+
setSaving(false);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const save = () => persist(enabled ? { enabled: true, secretRef, asrModel, ttsModel, ttsVoice } : null);
|
|
44
|
+
|
|
45
|
+
// 切到“关闭”立即持久化 null:关闭态没有保存按钮,若不在这里落盘,
|
|
46
|
+
// 后台会一直保持原配置,用户以为语音已停而实际仍在处理语音。
|
|
47
|
+
const toggle = (next) => {
|
|
48
|
+
setEnabled(next);
|
|
49
|
+
if (!next) void persist(null);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const input = (label, state, setState, placeholder) => h('label', { className: 'dim-feishuVoiceField' },
|
|
53
|
+
h('span', { className: 'dim-feishuVoiceLabel' }, label),
|
|
54
|
+
h('input', {
|
|
55
|
+
type: 'text',
|
|
56
|
+
className: 'dim-feishuVoiceInput',
|
|
57
|
+
value: state,
|
|
58
|
+
placeholder,
|
|
59
|
+
disabled: disabled || saving,
|
|
60
|
+
onChange: (event) => setState(event.target.value),
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return h('section', {
|
|
65
|
+
className: 'dim-feishuGroupControl',
|
|
66
|
+
"aria-labelledby": titleId,
|
|
67
|
+
},
|
|
68
|
+
h('div', { className: 'dim-feishuGroupControlHeader' },
|
|
69
|
+
h('div', { className: 'dim-presetTitle' },
|
|
70
|
+
h('h3', { id: titleId }, '语音交互'),
|
|
71
|
+
h('span', { className: 'dim-presetHelp' },
|
|
72
|
+
h('button', {
|
|
73
|
+
type: 'button',
|
|
74
|
+
className: 'dim-presetHelpButton',
|
|
75
|
+
"aria-label": '查看语音交互说明',
|
|
76
|
+
"aria-describedby": helpId,
|
|
77
|
+
}, h('span', { "aria-hidden": 'true' }, '?')),
|
|
78
|
+
h('span', {
|
|
79
|
+
id: helpId,
|
|
80
|
+
className: 'dim-presetTooltip',
|
|
81
|
+
role: 'tooltip',
|
|
82
|
+
}, '收到语音消息时转写为文字并按文字流水线处理,答案同时以语音回复;转写失败时降级为仅支持文字/图片/文件的提示。凭据值在插件凭据库里管理,这里只引用环境变量名。'))),
|
|
83
|
+
saving
|
|
84
|
+
? h('span', { className: 'dim-feishuGroupControlStatus', role: 'status' }, '保存中…')
|
|
85
|
+
: null),
|
|
86
|
+
h('select', {
|
|
87
|
+
className: 'dim-feishuGroupSelect',
|
|
88
|
+
value: enabled ? 'on' : 'off',
|
|
89
|
+
disabled: disabled || saving,
|
|
90
|
+
"aria-label": '语音交互开关',
|
|
91
|
+
onChange: (event) => toggle(event.target.value === 'on'),
|
|
92
|
+
},
|
|
93
|
+
h('option', { value: 'off' }, '关闭'),
|
|
94
|
+
h('option', { value: 'on' }, '开启(语音提问与语音回复)')),
|
|
95
|
+
enabled
|
|
96
|
+
? h('div', { className: 'dim-feishuVoiceForm' },
|
|
97
|
+
input('凭据引用(环境变量名)', secretRef, setSecretRef, 'DASHSCOPE_API_KEY'),
|
|
98
|
+
input('转写模型(ASR)', asrModel, setAsrModel, 'qwen3-asr-flash'),
|
|
99
|
+
input('合成模型(TTS)', ttsModel, setTtsModel, 'qwen3-tts-flash'),
|
|
100
|
+
input('回复音色', ttsVoice, setTtsVoice, 'Momo'),
|
|
101
|
+
h('p', { className: 'dim-feishuGroupHelp' },
|
|
102
|
+
'需要 DashScope 密钥(凭据库中以"凭据引用"为名的环境变量)与可用 ffmpeg;音色支持 Cherry/Serena/Bella/Chelsie/Momo/Katerina 等 qwen3-tts-flash 官方音色。'),
|
|
103
|
+
h('div', { className: 'dim-feishuVoiceActions' },
|
|
104
|
+
h('button', {
|
|
105
|
+
type: 'button',
|
|
106
|
+
className: 'dim-feishuVoiceSave',
|
|
107
|
+
onClick: () => void save(),
|
|
108
|
+
disabled: disabled || saving || !secretRef.trim(),
|
|
109
|
+
}, '保存语音设置')))
|
|
110
|
+
: h('p', { className: 'dim-feishuGroupHelp' }, '默认关闭;开启后机器人可听懂语音消息并以语音回复。'),
|
|
111
|
+
error ? h('p', {
|
|
112
|
+
className: 'dim-feishuGroupError',
|
|
113
|
+
role: 'alert',
|
|
114
|
+
}, error) : null);
|
|
115
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { h } from '../../i18n.js';
|
|
4
|
+
import { FEISHU_ENDPOINTS, normalizeBotsSnapshot, unwrapRpcResult } from './api.js';
|
|
5
|
+
import { VoiceEditor } from './voice-editor.js';
|
|
6
|
+
|
|
7
|
+
export function FeishuVoiceSettingsPage({ account, rpcCall }) {
|
|
8
|
+
const [voice, setVoice] = React.useState(null);
|
|
9
|
+
const [phase, setPhase] = React.useState('loading');
|
|
10
|
+
const [error, setError] = React.useState(null);
|
|
11
|
+
const mounted = React.useRef(true);
|
|
12
|
+
|
|
13
|
+
const invoke = React.useCallback(async (endpoint, payload, signal) => {
|
|
14
|
+
if (typeof rpcCall !== 'function') throw new Error('飞书语音设置暂不可用。');
|
|
15
|
+
return unwrapRpcResult(await rpcCall(endpoint, payload, signal));
|
|
16
|
+
}, [rpcCall]);
|
|
17
|
+
|
|
18
|
+
const applySnapshot = React.useCallback((value) => {
|
|
19
|
+
const bot = normalizeBotsSnapshot(value).bots.find((entry) => entry.botId === account.botId);
|
|
20
|
+
if (!bot) throw new Error('未找到当前飞书机器人,请返回列表后重试。');
|
|
21
|
+
if (mounted.current) setVoice(bot.voice);
|
|
22
|
+
}, [account.botId]);
|
|
23
|
+
|
|
24
|
+
const loadSettings = React.useCallback(async (signal) => {
|
|
25
|
+
setPhase('loading');
|
|
26
|
+
setError(null);
|
|
27
|
+
try {
|
|
28
|
+
const value = await invoke(FEISHU_ENDPOINTS.status, {}, signal);
|
|
29
|
+
if (signal?.aborted || !mounted.current) return;
|
|
30
|
+
applySnapshot(value);
|
|
31
|
+
setPhase('ready');
|
|
32
|
+
} catch (cause) {
|
|
33
|
+
if (signal?.aborted || !mounted.current) return;
|
|
34
|
+
setError(cause?.message ?? '飞书语音设置暂不可用。');
|
|
35
|
+
setPhase('error');
|
|
36
|
+
}
|
|
37
|
+
}, [applySnapshot, invoke]);
|
|
38
|
+
|
|
39
|
+
React.useEffect(() => {
|
|
40
|
+
mounted.current = true;
|
|
41
|
+
const controller = new AbortController();
|
|
42
|
+
void loadSettings(controller.signal);
|
|
43
|
+
return () => {
|
|
44
|
+
mounted.current = false;
|
|
45
|
+
controller.abort();
|
|
46
|
+
};
|
|
47
|
+
}, [loadSettings]);
|
|
48
|
+
|
|
49
|
+
const saveVoice = async (settings) => {
|
|
50
|
+
const value = await invoke(FEISHU_ENDPOINTS.setVoice, {
|
|
51
|
+
botId: account.botId,
|
|
52
|
+
voice: settings === null ? null : { ...voice, ...settings },
|
|
53
|
+
});
|
|
54
|
+
if (mounted.current) applySnapshot(value);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (phase === 'loading') {
|
|
58
|
+
return h('div', { className: 'dim-deliveryState', role: 'status', 'aria-busy': 'true' },
|
|
59
|
+
'正在读取语音设置…');
|
|
60
|
+
}
|
|
61
|
+
if (phase === 'error') {
|
|
62
|
+
return h('div', { className: 'dim-deliveryState', role: 'alert' },
|
|
63
|
+
h('p', null, error),
|
|
64
|
+
h('button', {
|
|
65
|
+
type: 'button',
|
|
66
|
+
className: 'dim-deliveryButton',
|
|
67
|
+
onClick: () => void loadSettings(),
|
|
68
|
+
}, '重新读取'));
|
|
69
|
+
}
|
|
70
|
+
return h(VoiceEditor, { value: voice, onSave: saveVoice });
|
|
71
|
+
}
|
|
@@ -2,6 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { AccessPolicySettingsPage } from './access-policy-settings.js';
|
|
4
4
|
import { FeishuGroupSettingsPage } from './channels/feishu/group-settings.js';
|
|
5
|
+
import { FeishuVoiceSettingsPage } from './channels/feishu/voice-settings.js';
|
|
5
6
|
import { h, isEnglish, localizeText } from './i18n.js';
|
|
6
7
|
|
|
7
8
|
export const DELIVERY_RPC_CHANNEL = '/dsh-im-delivery';
|
|
@@ -29,6 +30,7 @@ export const BOT_SETTINGS_TABS = Object.freeze([
|
|
|
29
30
|
export const FEISHU_BOT_SETTINGS_TABS = Object.freeze([
|
|
30
31
|
...BOT_SETTINGS_TABS,
|
|
31
32
|
Object.freeze({ id: 'group', label: '群聊' }),
|
|
33
|
+
Object.freeze({ id: 'voice', label: '语音交互' }),
|
|
32
34
|
]);
|
|
33
35
|
|
|
34
36
|
export function botSettingsTabsForChannel(channel) {
|
|
@@ -792,6 +794,12 @@ export function DeliveryTargetSettingsPage({
|
|
|
792
794
|
account,
|
|
793
795
|
rpcCall: accessRpcCall,
|
|
794
796
|
})
|
|
797
|
+
: activeTab.id === 'voice' && channel === 'feishu'
|
|
798
|
+
? h(FeishuVoiceSettingsPage, {
|
|
799
|
+
key: account.botId,
|
|
800
|
+
account,
|
|
801
|
+
rpcCall: accessRpcCall,
|
|
802
|
+
})
|
|
795
803
|
: h(React.Fragment, null,
|
|
796
804
|
h('section', { className: 'dim-deliveryIdentity', 'aria-labelledby': 'dim-delivery-bot-title' },
|
|
797
805
|
h('div', { className: 'dim-deliveryIdentityHeading' },
|
|
@@ -282,6 +282,22 @@ const EN = Object.freeze({
|
|
|
282
282
|
'推荐长任务使用:过程与最终答案都在同一张卡片里实时更新,不刷屏': 'Recommended for long tasks: the process and the final answer update live in one card without flooding the chat',
|
|
283
283
|
'每一步都单独发一条消息(含工具调用和过程说明);注意长任务会连续发送较多消息': 'Every step is sent as its own message (including tool calls and notes); long tasks may send many messages in a row',
|
|
284
284
|
'分步直推设置保存失败,请重试。': 'Could not save the step push setting. Try again.',
|
|
285
|
+
'语音交互': 'Voice',
|
|
286
|
+
'正在读取语音设置…': 'Loading voice settings…',
|
|
287
|
+
'飞书语音设置暂不可用。': 'Feishu voice settings are unavailable.',
|
|
288
|
+
'未找到当前飞书机器人,请返回列表后重试。': 'This Feishu bot was not found. Return to the bot list and try again.',
|
|
289
|
+
'查看语音交互说明': 'View voice help',
|
|
290
|
+
'语音交互开关': 'Voice on/off',
|
|
291
|
+
'开启(语音提问与语音回复)': 'On (voice questions and voice replies)',
|
|
292
|
+
'收到语音消息时转写为文字并按文字流水线处理,答案同时以语音回复;转写失败时降级为仅支持文字/图片/文件的提示。凭据值在插件凭据库里管理,这里只引用环境变量名。': 'Voice messages are transcribed to text and processed by the normal text pipeline; the answer is also sent as a voice reply. If transcription fails, the bot falls back to the text/image/file-only notice. The credential value lives in the plugin credential store; only the environment variable name is referenced here.',
|
|
293
|
+
'凭据引用(环境变量名)': 'Credential reference (environment variable name)',
|
|
294
|
+
'转写模型(ASR)': 'Transcription model (ASR)',
|
|
295
|
+
'合成模型(TTS)': 'Synthesis model (TTS)',
|
|
296
|
+
'回复音色': 'Reply voice',
|
|
297
|
+
'需要 DashScope 密钥(凭据库中以"凭据引用"为名的环境变量)与可用 ffmpeg;音色支持 Cherry/Serena/Bella/Chelsie/Momo/Katerina 等 qwen3-tts-flash 官方音色。': 'Requires a DashScope API key (the credential-store environment variable named by the credential reference above) and a working ffmpeg. Supported voices include the official qwen3-tts-flash voices Cherry/Serena/Bella/Chelsie/Momo/Katerina.',
|
|
298
|
+
'保存语音设置': 'Save voice settings',
|
|
299
|
+
'默认关闭;开启后机器人可听懂语音消息并以语音回复。': 'Off by default. When enabled, the bot understands voice messages and replies by voice.',
|
|
300
|
+
'语音设置保存失败,请重试。': 'Could not save the voice settings. Try again.',
|
|
285
301
|
'zcode过程卡模式': 'ZCode process-card mode',
|
|
286
302
|
'过程说明、工具摘要与最终答案都在同一张卡片中原地刷新':
|
|
287
303
|
'Process notes, the tool summary, and the final answer refresh in place inside one card',
|
|
@@ -479,6 +479,17 @@ const CSS = String.raw`
|
|
|
479
479
|
.dim-feishuGroupSelect:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary, #3370ff); box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary, #3370ff) 15%, transparent); }
|
|
480
480
|
.dim-feishuGroupSelect:disabled { cursor: not-allowed; opacity: .55; }
|
|
481
481
|
.dim-feishuGroupHelp { margin: -2px 0 0; color: var(--dsw-alias-label-tertiary, #8f959e); font-size: 12px; line-height: 19px; }
|
|
482
|
+
.dim-feishuVoiceForm { min-width: 0; display: grid; gap: 10px; }
|
|
483
|
+
.dim-feishuVoiceField { min-width: 0; display: grid; gap: 5px; }
|
|
484
|
+
.dim-feishuVoiceLabel { color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; }
|
|
485
|
+
.dim-feishuVoiceInput { min-width: 0; width: 100%; height: 36px; padding: 0 11px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 13px; line-height: 36px; transition: border-color .15s ease, box-shadow .15s ease; }
|
|
486
|
+
.dim-feishuVoiceInput:hover:not(:disabled) { border-color: color-mix(in srgb, var(--dsw-alias-state-business-primary, #3370ff) 42%, var(--dsw-alias-border-l2, #dfe1e5)); }
|
|
487
|
+
.dim-feishuVoiceInput:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary, #3370ff); box-shadow: 0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary, #3370ff) 15%, transparent); }
|
|
488
|
+
.dim-feishuVoiceInput:disabled { cursor: not-allowed; opacity: .55; }
|
|
489
|
+
.dim-feishuVoiceActions { display: flex; justify-content: flex-start; }
|
|
490
|
+
.dim-feishuVoiceSave { height: 34px; padding: 0 15px; border: 1px solid var(--dsw-alias-state-business-primary, #3370ff); border-radius: 8px; color: var(--dsw-alias-state-business-primary, #3370ff); background: var(--dsw-alias-bg-layer-1, #fff); font: inherit; font-size: 13px; line-height: 32px; cursor: pointer; transition: border-color .15s ease, color .15s ease, background .15s ease; }
|
|
491
|
+
.dim-feishuVoiceSave:hover:not(:disabled) { color: #0f5fce; border-color: #0f5fce; background: color-mix(in srgb, #3370ff 6%, var(--dsw-alias-bg-layer-1, #fff)); }
|
|
492
|
+
.dim-feishuVoiceSave:disabled { cursor: not-allowed; opacity: .55; }
|
|
482
493
|
.dim-feishuGroupPermissionAction { display: flex; justify-content: flex-start; }
|
|
483
494
|
.dim-feishuGroupPermissionAction .dim-deliveryButton { color: var(--dsw-alias-state-business-primary, #3370ff); border-color: color-mix(in srgb, var(--dsw-alias-state-business-primary, #3370ff) 30%, var(--dsw-alias-border-l2, #dfe1e5)); }
|
|
484
495
|
.dim-feishuGroupError, .dim-feishuGroupRefreshError { margin: 0; padding: 9px 11px; border-radius: 8px; font-size: 12px; line-height: 18px; }
|
|
@@ -234,6 +234,16 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
234
234
|
const workspaceScope = createBotWorkspaceScope(harness, {
|
|
235
235
|
botId: id, workspaces, state, agentPresetCatalog,
|
|
236
236
|
});
|
|
237
|
+
// 语音是可选渠道能力:凭据读取失败时静默禁用,不阻断机器人连接。
|
|
238
|
+
let voiceSource = null;
|
|
239
|
+
if (botConfig.voice) {
|
|
240
|
+
try {
|
|
241
|
+
const resolved = await ctx.credentials.resolve(botConfig.voice.secretRef);
|
|
242
|
+
if (resolved?.value) voiceSource = { config: botConfig.voice, secret: resolved.value };
|
|
243
|
+
} catch (error) {
|
|
244
|
+
logger.warn?.('[dsh-feishu-voice] 语音凭据读取失败,语音已禁用:', error?.message ?? String(error));
|
|
245
|
+
}
|
|
246
|
+
}
|
|
237
247
|
return new Runtime({
|
|
238
248
|
lark,
|
|
239
249
|
botId: id,
|
|
@@ -246,6 +256,7 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
246
256
|
groupTopicReply: botConfig.groupTopicReply,
|
|
247
257
|
stepPush: botConfig.stepPush,
|
|
248
258
|
stepPushMode: botConfig.stepPushMode,
|
|
259
|
+
voice: voiceSource,
|
|
249
260
|
sessionSyncTargetsFor: sessionSyncTargetsFor,
|
|
250
261
|
ownerOpenIds: botConfig.ownerOpenIds ?? [botConfig.ownerOpenId],
|
|
251
262
|
harness: workspaceScope.harness,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
isFeishuStepPushMode,
|
|
31
31
|
normalizeFeishuStepPushMode,
|
|
32
32
|
} from '../../../../src/channels/feishu/step-push-mode.mjs';
|
|
33
|
+
import { normalizeFeishuVoiceConfig } from '../../../../src/channels/feishu/voice-config.mjs';
|
|
33
34
|
import {
|
|
34
35
|
FEISHU_ENDPOINTS as FEISHU_CLIENT_ENDPOINTS,
|
|
35
36
|
FEISHU_RPC_CHANNEL,
|
|
@@ -307,6 +308,7 @@ function publicBotEntry(entry) {
|
|
|
307
308
|
groupTopicReply: source.groupTopicReply === true,
|
|
308
309
|
stepPush: source.stepPush === true,
|
|
309
310
|
stepPushMode: normalizeFeishuStepPushMode(source.stepPushMode),
|
|
311
|
+
voice: normalizeFeishuVoiceConfig(source.voice),
|
|
310
312
|
groupMessagePermissionGranted: source.groupMessagePermissionGranted === true,
|
|
311
313
|
bot: publicBot(source.bot),
|
|
312
314
|
health: publicHealth(source, connected),
|
|
@@ -490,6 +492,17 @@ function validPayload(endpoint, payload) {
|
|
|
490
492
|
? null
|
|
491
493
|
: '请选择分步直推的呈现方式。';
|
|
492
494
|
}
|
|
495
|
+
if (endpoint === FEISHU_ENDPOINTS.setVoice) {
|
|
496
|
+
const voice = payload?.voice;
|
|
497
|
+
const validVoice = voice === null
|
|
498
|
+
|| (typeof voice === 'object' && !Array.isArray(voice)
|
|
499
|
+
&& (voice.enabled === false || normalizeFeishuVoiceConfig(voice) !== null));
|
|
500
|
+
return hasOnlyKeys(payload, new Set(['botId', 'voice']))
|
|
501
|
+
&& safeOpaqueId(payload.botId)
|
|
502
|
+
&& validVoice
|
|
503
|
+
? null
|
|
504
|
+
: '语音配置无效。';
|
|
505
|
+
}
|
|
493
506
|
return 'Unknown Feishu endpoint.';
|
|
494
507
|
}
|
|
495
508
|
|
|
@@ -797,6 +810,14 @@ export function createFeishuRpcHandler(controller, { encodeQr = qrCodeDataUrl }
|
|
|
797
810
|
await controller.updateStepPushMode(payload.botId, payload.stepPushMode),
|
|
798
811
|
{ encodeQr: cachedEncodeQr },
|
|
799
812
|
);
|
|
813
|
+
} else if (endpoint === FEISHU_ENDPOINTS.setVoice) {
|
|
814
|
+
if (typeof controller.updateVoice !== 'function') {
|
|
815
|
+
throw new Error('Voice update is unavailable');
|
|
816
|
+
}
|
|
817
|
+
value = await toPublicFeishuStatus(
|
|
818
|
+
await controller.updateVoice(payload.botId, payload.voice),
|
|
819
|
+
{ encodeQr: cachedEncodeQr },
|
|
820
|
+
);
|
|
800
821
|
} else {
|
|
801
822
|
if (typeof controller.deleteBot !== 'function') throw new Error('Multi-bot delete is unavailable');
|
|
802
823
|
value = await toPublicFeishuStatus(await controller.deleteBot(payload.botId), { encodeQr: cachedEncodeQr });
|