Refactor backend configuration management and update environment settings

- Replace the `config.py` module with a new `settings.py` to streamline environment variable management, focusing on database, CORS, and TURN settings.
- Update references throughout the backend codebase to use the new `settings` module instead of the deprecated `config`.
- Modify the `.env.example` file to reflect the new configuration approach, indicating that model provider credentials should be maintained separately.
- Enhance the `AssistantConfig` model to clarify the source of runtime connection information, ensuring it is injected from model resources rather than relying on defaults from the environment.
- Introduce new user scripts for audio and video management in the Tampermonkey environment, enhancing WebRTC capabilities.
This commit is contained in:
Xin Wang
2026-07-09 21:40:29 +08:00
parent 195579c5b7
commit 35d24acf40
19 changed files with 106 additions and 139 deletions

View File

@@ -1,10 +1,9 @@
"""assistant_id → 运行时配置(把真 key 在服务端组装好)。
浏览器只传 assistant_id;真 key 在这里从 model_resources 取出注入。
助手按 capability binding 引用资源;取不到则回退该能力默认资源,再回退 .env
助手按 capability binding 引用资源;取不到则回退该能力默认资源。
"""
import config
from db.models import Assistant, AssistantModelBinding, ModelResource
from models import AssistantConfig
from sqlalchemy import select
@@ -63,14 +62,14 @@ async def _agent_resource_for(
).scalar_one_or_none()
def _value(resource: ModelResource | None, key: str, default):
def _value(resource: ModelResource | None, key: str, default=""):
if not resource:
return default
value = (resource.values or {}).get(key, default)
return default if value is None else value
def _secret(resource: ModelResource | None, key: str, default: str) -> str:
def _secret(resource: ModelResource | None, key: str, default: str = "") -> str:
if not resource:
return default
return str((resource.secrets or {}).get(key) or default)
@@ -148,19 +147,15 @@ async def resolve_runtime_config(
agent_interface_type=(agent_resource.interface_type if agent_resource else ""),
agent_values=(agent_resource.values or {}) if agent_resource else {},
agent_secrets=(agent_resource.secrets or {}) if agent_resource else {},
# 运行时连接信息(真 key + url):模型资源优先,否则 .env 兜底
llm_api_key=_secret(llm_resource, "apiKey", config.LLM_API_KEY),
llm_base_url=str(_value(llm_resource, "apiUrl", config.LLM_BASE_URL)),
vision_llm_api_key=_secret(
vision_resource, "apiKey", config.LLM_API_KEY
),
vision_llm_base_url=str(
_value(vision_resource, "apiUrl", config.LLM_BASE_URL)
),
stt_api_key=_secret(stt_resource, "apiKey", config.STT_API_KEY),
stt_base_url=str(_value(stt_resource, "apiUrl", config.STT_BASE_URL)),
tts_api_key=_secret(tts_resource, "apiKey", config.TTS_API_KEY),
tts_base_url=str(_value(tts_resource, "apiUrl", config.TTS_BASE_URL)),
# 运行时连接信息(真 key + url):来自模型资源,不再从 .env 兜底
llm_api_key=_secret(llm_resource, "apiKey"),
llm_base_url=str(_value(llm_resource, "apiUrl")),
vision_llm_api_key=_secret(vision_resource, "apiKey"),
vision_llm_base_url=str(_value(vision_resource, "apiUrl")),
stt_api_key=_secret(stt_resource, "apiKey"),
stt_base_url=str(_value(stt_resource, "apiUrl")),
tts_api_key=_secret(tts_resource, "apiKey"),
tts_base_url=str(_value(tts_resource, "apiUrl")),
realtime_api_key=_secret(realtime_resource, "apiKey", ""),
realtime_base_url=str(_value(realtime_resource, "apiUrl", "")),
)

View File

@@ -12,10 +12,10 @@ from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
import httpx
from websockets.asyncio.client import connect as websocket_connect
import config
from schemas import ModelResourceTestResult
TEST_TIMEOUT_SECONDS = 10.0
DEFAULT_TEST_TTS_VOICE = "alloy"
def _endpoint(base_url: str, path: str) -> str:
@@ -116,7 +116,7 @@ async def test_model_resource(
json={
"model": model_id,
"input": "测试",
"voice": str(values.get("voice") or config.TTS_VOICE),
"voice": str(values.get("voice") or DEFAULT_TEST_TTS_VOICE),
"response_format": "pcm",
"speed": float(values.get("speed") or 1),
},

View File

@@ -11,7 +11,6 @@ import base64
from io import BytesIO
from uuid import uuid4
import config
from loguru import logger
from models import AssistantConfig
from openai import AsyncOpenAI
@@ -77,6 +76,12 @@ VISION_ANALYSIS_SYSTEM_PROMPT = (
)
def _require(value: str, label: str) -> str:
if value:
return value
raise ValueError(f"缺少模型资源配置: {label}")
def _vision_uses_main_llm(cfg: AssistantConfig) -> bool:
"""模型自己支持图片时,沿用 Pipecat 的同上下文视觉工具路径。"""
return not cfg.vision_model_resource_id and cfg.llm_support_image_input
@@ -107,12 +112,12 @@ async def _analyze_image_with_vision_model(
extra_body = cfg.vision_llm_values.get("extraBody")
extra = {"extra_body": extra_body} if isinstance(extra_body, dict) else {}
client = AsyncOpenAI(
api_key=cfg.vision_llm_api_key or config.LLM_API_KEY,
base_url=cfg.vision_llm_base_url or config.LLM_BASE_URL,
api_key=_require(cfg.vision_llm_api_key, "Vision LLM apiKey"),
base_url=_require(cfg.vision_llm_base_url, "Vision LLM apiUrl"),
)
try:
response = await client.chat.completions.create(
model=cfg.vision_model or config.LLM_MODEL,
model=_require(cfg.vision_model, "Vision LLM modelId"),
messages=[
{"role": "system", "content": VISION_ANALYSIS_SYSTEM_PROMPT},
{
@@ -665,9 +670,9 @@ async def run_pipeline(
target = await engine.route(
wf_state["current"],
history,
api_key=cfg.llm_api_key or config.LLM_API_KEY,
base_url=cfg.llm_base_url or config.LLM_BASE_URL,
model=cfg.model or config.LLM_MODEL,
api_key=_require(cfg.llm_api_key, "LLM apiKey"),
base_url=_require(cfg.llm_base_url, "LLM apiUrl"),
model=_require(cfg.model, "LLM modelId"),
)
if target and target != wf_state["current"]:
logger.info(f"文本兜底触发转移 → {engine.name(target)}")

View File

@@ -4,7 +4,6 @@
按 interface_type 扩展时在这里加分支即可——这是未来接更多模型的唯一入口。
"""
import config
from loguru import logger
from models import AssistantConfig
@@ -27,6 +26,12 @@ from services.pipecat.xfyun_tts import DEFAULT_XFYUN_TTS_URL, XfyunTTSService
TTS_STOP_FRAME_TIMEOUT_S = 1.0
def _require(value: str, label: str) -> str:
if value:
return value
raise ValueError(f"缺少模型资源配置: {label}")
def _language(value: str) -> Language | None:
if not value:
return None
@@ -40,7 +45,7 @@ def _language(value: str) -> Language | None:
def create_stt(cfg: AssistantConfig):
"""SenseVoice / FunASR 等,走 OpenAI 兼容的 /v1/audio/transcriptions。
连接信息优先用 cfg(由 config_resolver 从 DB 注入),为空回退 .env 默认
连接信息来自 cfg(由 config_resolver 从 DB 注入,或调试时 inline_config 传入)
"""
if cfg.stt_interface_type == "xfyun-asr":
return XfyunASRService(
@@ -59,10 +64,10 @@ def create_stt(cfg: AssistantConfig):
raise ValueError(f"不支持的 ASR 接口类型: {cfg.stt_interface_type}")
return OpenAISTTService(
api_key=cfg.stt_api_key or config.STT_API_KEY,
base_url=cfg.stt_base_url or config.STT_BASE_URL,
api_key=_require(cfg.stt_api_key, "ASR apiKey"),
base_url=_require(cfg.stt_base_url, "ASR apiUrl"),
settings=OpenAISTTService.Settings(
model=cfg.asr or config.STT_MODEL,
model=_require(cfg.asr, "ASR modelId"),
language=_language(cfg.stt_language),
),
)
@@ -75,10 +80,10 @@ def create_llm(cfg: AssistantConfig):
extra_body = cfg.llm_values.get("extraBody")
extra = {"extra_body": extra_body} if isinstance(extra_body, dict) else {}
return OpenAILLMService(
api_key=cfg.llm_api_key or config.LLM_API_KEY,
base_url=cfg.llm_base_url or config.LLM_BASE_URL,
api_key=_require(cfg.llm_api_key, "LLM apiKey"),
base_url=_require(cfg.llm_base_url, "LLM apiUrl"),
settings=OpenAILLMService.Settings(
model=cfg.model or config.LLM_MODEL,
model=_require(cfg.model, "LLM modelId"),
extra=extra,
),
)
@@ -86,7 +91,7 @@ def create_llm(cfg: AssistantConfig):
def create_tts(cfg: AssistantConfig):
"""CosyVoice 等,走 OpenAI 兼容的 /v1/audio/speech。"""
voice = cfg.voice or config.TTS_VOICE
voice = _require(cfg.voice, "TTS voice")
if cfg.tts_interface_type == "xfyun-super-tts":
return XfyunSuperTTSService(
app_id=str(cfg.tts_secrets.get("appId") or ""),
@@ -126,11 +131,11 @@ def create_tts(cfg: AssistantConfig):
# 注册为原样映射后仍由 OpenAI SDK 按字符串透传给供应商。
VALID_VOICES.setdefault(voice, voice)
return OpenAITTSService(
api_key=cfg.tts_api_key or config.TTS_API_KEY,
base_url=cfg.tts_base_url or config.TTS_BASE_URL,
api_key=_require(cfg.tts_api_key, "TTS apiKey"),
base_url=_require(cfg.tts_base_url, "TTS apiUrl"),
stop_frame_timeout_s=TTS_STOP_FRAME_TIMEOUT_S,
settings=OpenAITTSService.Settings(
model=cfg.tts_model or config.TTS_MODEL,
model=_require(cfg.tts_model, "TTS modelId"),
voice=voice,
speed=cfg.tts_speed,
),
@@ -143,9 +148,9 @@ def create_realtime_service(cfg: AssistantConfig):
from services.pipecat.stepfun_realtime import StepFunRealtimeService
return StepFunRealtimeService(
api_key=cfg.realtime_api_key,
model=cfg.realtimeModel,
base_url=cfg.realtime_base_url,
api_key=_require(cfg.realtime_api_key, "Realtime apiKey"),
model=_require(cfg.realtimeModel, "Realtime modelId"),
base_url=_require(cfg.realtime_base_url, "Realtime apiUrl"),
instructions=cfg.prompt,
voice=str(cfg.realtime_values.get("voice") or "linjiajiejie"),
input_sample_rate=int(

View File

@@ -7,28 +7,28 @@ import hashlib
import hmac
import time
import config
import settings
STUN_URL = "stun:stun.l.google.com:19302"
def _turn_credentials() -> tuple[str, str] | None:
"""Return (username, credential) for TURN, or None when TURN is not configured."""
if not config.TURN_URLS:
if not settings.TURN_URLS:
return None
if config.TURN_SECRET:
expiry = int(time.time()) + config.TURN_CREDENTIAL_TTL
if settings.TURN_SECRET:
expiry = int(time.time()) + settings.TURN_CREDENTIAL_TTL
username = f"{expiry}:ai-video"
credential = base64.b64encode(
hmac.new(
config.TURN_SECRET.encode("utf-8"),
settings.TURN_SECRET.encode("utf-8"),
username.encode("utf-8"),
hashlib.sha1,
).digest()
).decode("utf-8")
return username, credential
if config.TURN_USERNAME and config.TURN_PASSWORD:
return config.TURN_USERNAME, config.TURN_PASSWORD
if settings.TURN_USERNAME and settings.TURN_PASSWORD:
return settings.TURN_USERNAME, settings.TURN_PASSWORD
return None
@@ -39,7 +39,7 @@ def client_ice_servers() -> list[dict]:
if not creds:
return servers
username, credential = creds
for url in config.TURN_URLS:
for url in settings.TURN_URLS:
servers.append({"urls": url, "username": username, "credential": credential})
return servers
@@ -53,7 +53,7 @@ def aiortc_ice_servers() -> list:
if not creds:
return servers
username, credential = creds
for url in config.TURN_URLS:
for url in settings.TURN_URLS:
servers.append(
RTCIceServer(urls=url, username=username, credential=credential)
)