fix: separate voice credentials and refresh feature toggle
This commit is contained in:
@@ -48,4 +48,6 @@ FRP_LOCAL_PORT=80
|
|||||||
# 自定义域名,逗号分隔,如:qa.huiyushuyuan.cn
|
# 自定义域名,逗号分隔,如:qa.huiyushuyuan.cn
|
||||||
FRP_CUSTOM_DOMAINS=qa.huiyushuyuan.cn
|
FRP_CUSTOM_DOMAINS=qa.huiyushuyuan.cn
|
||||||
ALIYUN_NLS_APP_KEY=
|
ALIYUN_NLS_APP_KEY=
|
||||||
|
ALIYUN_NLS_ACCESS_KEY_ID=
|
||||||
|
ALIYUN_NLS_ACCESS_KEY_SECRET=
|
||||||
ALIYUN_NLS_ENDPOINT=https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/asr
|
ALIYUN_NLS_ENDPOINT=https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/asr
|
||||||
|
|||||||
@@ -83,8 +83,15 @@ const groupBlueprints: Record<string, SettingGroupBlueprint[]> = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "语音输入",
|
title: "语音输入",
|
||||||
description: "控制用户端麦克风入口和单次录音上限。",
|
description: "控制用户端麦克风入口、录音上限和独立的阿里云语音凭证。",
|
||||||
keys: ["voice_input_enabled", "voice_max_duration_seconds"],
|
keys: [
|
||||||
|
"voice_input_enabled",
|
||||||
|
"voice_max_duration_seconds",
|
||||||
|
"aliyun_nls_app_key",
|
||||||
|
"aliyun_nls_access_key_id",
|
||||||
|
"aliyun_nls_access_key_secret",
|
||||||
|
"aliyun_nls_endpoint",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
外部服务: [
|
外部服务: [
|
||||||
|
|||||||
@@ -231,6 +231,38 @@ export const systemSettingSections: SystemSettingSection[] = [
|
|||||||
max: 60,
|
max: 60,
|
||||||
description: "达到设定时长后自动停止并转成文字,阿里云一句话识别上限为 60 秒。",
|
description: "达到设定时长后自动停止并转成文字,阿里云一句话识别上限为 60 秒。",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "aliyun_nls_app_key",
|
||||||
|
label: "阿里云语音 AppKey",
|
||||||
|
type: "text",
|
||||||
|
defaultValue: "",
|
||||||
|
placeholder: "请输入智能语音交互项目 AppKey",
|
||||||
|
description: "填写阿里云智能语音交互控制台中对应应用的 AppKey。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "aliyun_nls_access_key_id",
|
||||||
|
label: "语音服务 AccessKey ID",
|
||||||
|
type: "password",
|
||||||
|
defaultValue: "",
|
||||||
|
placeholder: "请单独填写语音服务 AccessKey ID",
|
||||||
|
description: "语音服务独立配置;如与短信使用同一账号,也需要在这里手动填写。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "aliyun_nls_access_key_secret",
|
||||||
|
label: "语音服务 AccessKey Secret",
|
||||||
|
type: "password",
|
||||||
|
defaultValue: "",
|
||||||
|
placeholder: "请单独填写语音服务 AccessKey Secret",
|
||||||
|
description: "加密保存且不会明文回显,不再从短信配置中读取。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "aliyun_nls_endpoint",
|
||||||
|
label: "阿里云语音 Endpoint",
|
||||||
|
type: "text",
|
||||||
|
defaultValue: "https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/asr",
|
||||||
|
placeholder: "https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/asr",
|
||||||
|
description: "一句话识别服务地址,一般保持默认值即可。",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -59,4 +59,6 @@ BOOTSTRAP_ADMIN_USERNAME=admin
|
|||||||
BOOTSTRAP_ADMIN_PASSWORD=admin123456
|
BOOTSTRAP_ADMIN_PASSWORD=admin123456
|
||||||
BOOTSTRAP_ADMIN_NAME=系统管理员
|
BOOTSTRAP_ADMIN_NAME=系统管理员
|
||||||
ALIYUN_NLS_APP_KEY=
|
ALIYUN_NLS_APP_KEY=
|
||||||
|
ALIYUN_NLS_ACCESS_KEY_ID=
|
||||||
|
ALIYUN_NLS_ACCESS_KEY_SECRET=
|
||||||
ALIYUN_NLS_ENDPOINT=https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/asr
|
ALIYUN_NLS_ENDPOINT=https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/asr
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ class Settings(BaseSettings):
|
|||||||
aliyun_sms_template_param_key: str = "code"
|
aliyun_sms_template_param_key: str = "code"
|
||||||
aliyun_sms_endpoint: str = "dysmsapi.aliyuncs.com"
|
aliyun_sms_endpoint: str = "dysmsapi.aliyuncs.com"
|
||||||
aliyun_nls_app_key: str = ""
|
aliyun_nls_app_key: str = ""
|
||||||
|
aliyun_nls_access_key_id: str = ""
|
||||||
|
aliyun_nls_access_key_secret: str = ""
|
||||||
aliyun_nls_endpoint: str = "https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/asr"
|
aliyun_nls_endpoint: str = "https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/asr"
|
||||||
mock_rag_enabled: bool = False
|
mock_rag_enabled: bool = False
|
||||||
mock_model_enabled: bool = True
|
mock_model_enabled: bool = True
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ from app.core.config import get_settings
|
|||||||
|
|
||||||
ENCRYPTED_PREFIX = "enc:v1:"
|
ENCRYPTED_PREFIX = "enc:v1:"
|
||||||
MASKED_SECRET = "******"
|
MASKED_SECRET = "******"
|
||||||
SENSITIVE_CONFIG_KEYS = {"aliyun_sms_access_key_secret", "feishu_app_secret"}
|
SENSITIVE_CONFIG_KEYS = {
|
||||||
|
"aliyun_sms_access_key_secret",
|
||||||
|
"aliyun_nls_access_key_secret",
|
||||||
|
"feishu_app_secret",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class SecretService:
|
class SecretService:
|
||||||
|
|||||||
@@ -105,15 +105,15 @@ def load_voice_config(db: Session) -> VoiceInputConfig:
|
|||||||
values = {row.config_key: row.config_value for row in rows}
|
values = {row.config_key: row.config_value for row in rows}
|
||||||
enabled = _bool(values.get("voice_input_enabled"), settings.voice_input_enabled)
|
enabled = _bool(values.get("voice_input_enabled"), settings.voice_input_enabled)
|
||||||
duration = _int(values.get("voice_max_duration_seconds"), settings.voice_max_duration_seconds, 5, 60)
|
duration = _int(values.get("voice_max_duration_seconds"), settings.voice_max_duration_seconds, 5, 60)
|
||||||
access_key_id = str(values.get("aliyun_sms_access_key_id") or settings.aliyun_sms_access_key_id).strip()
|
access_key_id = str(values.get("aliyun_nls_access_key_id") or settings.aliyun_nls_access_key_id).strip()
|
||||||
encrypted_secret = str(values.get("aliyun_sms_access_key_secret") or settings.aliyun_sms_access_key_secret).strip()
|
encrypted_secret = str(values.get("aliyun_nls_access_key_secret") or settings.aliyun_nls_access_key_secret).strip()
|
||||||
return VoiceInputConfig(
|
return VoiceInputConfig(
|
||||||
enabled=enabled,
|
enabled=enabled,
|
||||||
max_duration_seconds=duration,
|
max_duration_seconds=duration,
|
||||||
app_key=settings.aliyun_nls_app_key.strip(),
|
app_key=str(values.get("aliyun_nls_app_key") or settings.aliyun_nls_app_key).strip(),
|
||||||
access_key_id=access_key_id,
|
access_key_id=access_key_id,
|
||||||
access_key_secret=SecretService.decrypt(encrypted_secret),
|
access_key_secret=SecretService.decrypt(encrypted_secret),
|
||||||
endpoint=settings.aliyun_nls_endpoint.strip(),
|
endpoint=str(values.get("aliyun_nls_endpoint") or settings.aliyun_nls_endpoint).strip(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ def settings(**overrides):
|
|||||||
"voice_input_enabled": False,
|
"voice_input_enabled": False,
|
||||||
"voice_max_duration_seconds": 60,
|
"voice_max_duration_seconds": 60,
|
||||||
"aliyun_nls_app_key": "app-key",
|
"aliyun_nls_app_key": "app-key",
|
||||||
|
"aliyun_nls_access_key_id": "voice-access-id",
|
||||||
|
"aliyun_nls_access_key_secret": "voice-access-secret",
|
||||||
"aliyun_nls_endpoint": "https://nls.example/asr",
|
"aliyun_nls_endpoint": "https://nls.example/asr",
|
||||||
"aliyun_sms_access_key_id": "access-id",
|
|
||||||
"aliyun_sms_access_key_secret": "access-secret",
|
|
||||||
}
|
}
|
||||||
values.update(overrides)
|
values.update(overrides)
|
||||||
return SimpleNamespace(**values)
|
return SimpleNamespace(**values)
|
||||||
@@ -50,6 +50,22 @@ def test_transcription_endpoint_is_rejected_when_switch_is_off(monkeypatch):
|
|||||||
assert exc.value.status_code == 404
|
assert exc.value.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_voice_credentials_are_loaded_independently_from_sms(monkeypatch):
|
||||||
|
monkeypatch.setattr("app.services.voice_input_service.get_settings", lambda: settings())
|
||||||
|
with database() as db:
|
||||||
|
db.add_all([
|
||||||
|
SystemConfig(config_key="aliyun_sms_access_key_id", config_value="sms-id"),
|
||||||
|
SystemConfig(config_key="aliyun_nls_app_key", config_value="db-app-key"),
|
||||||
|
SystemConfig(config_key="aliyun_nls_access_key_id", config_value="db-voice-id"),
|
||||||
|
SystemConfig(config_key="aliyun_nls_access_key_secret", config_value="db-voice-secret"),
|
||||||
|
])
|
||||||
|
db.commit()
|
||||||
|
config = load_voice_config(db)
|
||||||
|
assert config.app_key == "db-app-key"
|
||||||
|
assert config.access_key_id == "db-voice-id"
|
||||||
|
assert config.access_key_secret == "db-voice-secret"
|
||||||
|
|
||||||
|
|
||||||
def test_transcription_returns_provider_text(monkeypatch):
|
def test_transcription_returns_provider_text(monkeypatch):
|
||||||
monkeypatch.setattr("app.services.voice_input_service.get_settings", lambda: settings(voice_input_enabled=True))
|
monkeypatch.setattr("app.services.voice_input_service.get_settings", lambda: settings(voice_input_enabled=True))
|
||||||
monkeypatch.setattr("app.services.voice_input_service._normalize_audio", lambda *_: b"wav")
|
monkeypatch.setattr("app.services.voice_input_service._normalize_audio", lambda *_: b"wav")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Mic, Send, Square, X } from "@lucide/vue";
|
import { Mic, Send, Square, X } from "@lucide/vue";
|
||||||
import { nextTick, onMounted, ref } from "vue";
|
import { nextTick, onMounted, onUnmounted, ref } from "vue";
|
||||||
|
|
||||||
import { useVoiceRecorder } from "../composables/useVoiceRecorder";
|
import { useVoiceRecorder } from "../composables/useVoiceRecorder";
|
||||||
import { api, transcribeVoice } from "../services/api";
|
import { api, transcribeVoice } from "../services/api";
|
||||||
@@ -37,7 +37,7 @@ const { recording, elapsedSeconds, start: startRecording, stop: stopRecording, c
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
onMounted(async () => {
|
async function refreshVoiceConfig() {
|
||||||
try {
|
try {
|
||||||
const config = await api.voiceConfig();
|
const config = await api.voiceConfig();
|
||||||
voiceEnabled.value = config.enabled;
|
voiceEnabled.value = config.enabled;
|
||||||
@@ -45,6 +45,25 @@ onMounted(async () => {
|
|||||||
} catch {
|
} catch {
|
||||||
voiceEnabled.value = false;
|
voiceEnabled.value = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshVoiceConfigWhenVisible() {
|
||||||
|
if (document.visibilityState === "visible") void refreshVoiceConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
let voiceConfigTimer: number | undefined;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void refreshVoiceConfig();
|
||||||
|
window.addEventListener("focus", refreshVoiceConfigWhenVisible);
|
||||||
|
document.addEventListener("visibilitychange", refreshVoiceConfigWhenVisible);
|
||||||
|
voiceConfigTimer = window.setInterval(refreshVoiceConfigWhenVisible, 30_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener("focus", refreshVoiceConfigWhenVisible);
|
||||||
|
document.removeEventListener("visibilitychange", refreshVoiceConfigWhenVisible);
|
||||||
|
if (voiceConfigTimer !== undefined) window.clearInterval(voiceConfigTimer);
|
||||||
});
|
});
|
||||||
|
|
||||||
function resize() {
|
function resize() {
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ services:
|
|||||||
MOCK_SMS_ENABLED: "true"
|
MOCK_SMS_ENABLED: "true"
|
||||||
MOCK_SMS_CODE: "123456"
|
MOCK_SMS_CODE: "123456"
|
||||||
ALIYUN_NLS_APP_KEY: "${ALIYUN_NLS_APP_KEY:-FBFMOHMH4JFv7UNn}"
|
ALIYUN_NLS_APP_KEY: "${ALIYUN_NLS_APP_KEY:-FBFMOHMH4JFv7UNn}"
|
||||||
|
ALIYUN_NLS_ACCESS_KEY_ID: "${ALIYUN_NLS_ACCESS_KEY_ID:-}"
|
||||||
|
ALIYUN_NLS_ACCESS_KEY_SECRET: "${ALIYUN_NLS_ACCESS_KEY_SECRET:-}"
|
||||||
MOCK_RAG_ENABLED: "false"
|
MOCK_RAG_ENABLED: "false"
|
||||||
MOCK_MODEL_ENABLED: "true"
|
MOCK_MODEL_ENABLED: "true"
|
||||||
FEISHU_MOCK_ENABLED: "false"
|
FEISHU_MOCK_ENABLED: "false"
|
||||||
|
|||||||
Reference in New Issue
Block a user