[OpenSpec 与项目骨架]:完成实施型变更与 Python 基础骨架,包含 OpenSpec 工件、核心模型、配置边界和基础测试

This commit is contained in:
mkbk
2026-06-17 18:12:58 +08:00
commit 86ad0c86f2
21 changed files with 2410 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from .models import ErrorCode, ProviderError
@dataclass(frozen=True, slots=True)
class AppConfig:
wake_word: str = "小杰小杰"
sample_rate: int = 16000
channels: int = 1
llm_base_url: str = "https://api.openai.com"
llm_api_key: str | None = None
llm_model: str = "gpt-4o-mini"
llm_api_style: str = "chat_completions"
audio_input_device: str | None = None
audio_output_device: str | None = None
asset_dir: Path = Path("assets/pet")
log_dir: Path = Path("logs")
context_max_messages: int = 12
context_max_chars: int = 12000
@classmethod
def from_env(cls, prefix: str = "OWNER_") -> "AppConfig":
def get(name: str, default: str | None = None) -> str | None:
value = os.environ.get(f"{prefix}{name}")
return default if value is None or value == "" else value
return cls(
wake_word=get("WAKE_WORD", "小杰小杰") or "小杰小杰",
sample_rate=int(get("SAMPLE_RATE", "16000") or "16000"),
channels=int(get("CHANNELS", "1") or "1"),
llm_base_url=(get("LLM_BASE_URL", "https://api.openai.com") or "").rstrip("/"),
llm_api_key=get("LLM_API_KEY"),
llm_model=get("LLM_MODEL", "gpt-4o-mini") or "gpt-4o-mini",
llm_api_style=get("LLM_API_STYLE", "chat_completions") or "chat_completions",
audio_input_device=get("AUDIO_INPUT_DEVICE"),
audio_output_device=get("AUDIO_OUTPUT_DEVICE"),
asset_dir=Path(get("ASSET_DIR", "assets/pet") or "assets/pet"),
log_dir=Path(get("LOG_DIR", "logs") or "logs"),
context_max_messages=int(get("CONTEXT_MAX_MESSAGES", "12") or "12"),
context_max_chars=int(get("CONTEXT_MAX_CHARS", "12000") or "12000"),
)
def require_llm_credentials(self) -> None:
if not self.llm_api_key:
raise ProviderError(
code=ErrorCode.LLM_API_KEY_MISSING,
message="OWNER_LLM_API_KEY is required for cloud LLM calls",
retryable=False,
provider="openai-compatible",
stage="llm",
)
def validate_basic(self) -> list[ProviderError]:
errors: list[ProviderError] = []
if self.sample_rate <= 0:
errors.append(
ProviderError(
ErrorCode.CONFIG_MISSING_VALUE,
"sample_rate must be positive",
False,
"config",
"startup",
)
)
if self.channels <= 0:
errors.append(
ProviderError(
ErrorCode.CONFIG_MISSING_VALUE,
"channels must be positive",
False,
"config",
"startup",
)
)
if self.llm_api_style not in {"chat_completions", "responses"}:
errors.append(
ProviderError(
ErrorCode.CONFIG_MISSING_VALUE,
"OWNER_LLM_API_STYLE must be chat_completions or responses",
False,
"config",
"startup",
)
)
return errors