[配置]:完成 .env 文件读取配置,包含自动加载、CLI 参数、测试覆盖和中文说明

This commit is contained in:
mkbk
2026-06-17 18:59:11 +08:00
parent 9729969915
commit 61abf0c674
5 changed files with 78 additions and 20 deletions
+5 -4
View File
@@ -21,6 +21,7 @@ from .wakeword import KeywordWakeWordProvider
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="owner-voice-pet")
parser.add_argument("--env-file", default=".env", help="Path to .env config file")
parser.add_argument("--show-config", action="store_true", help="Print non-secret config summary")
subparsers = parser.add_subparsers(dest="command")
subparsers.add_parser("acceptance", help="Run deterministic end-to-end pipeline acceptance")
@@ -32,7 +33,7 @@ def main(argv: list[str] | None = None) -> int:
args = parser.parse_args(argv)
if args.show_config:
config = AppConfig.from_env()
config = AppConfig.from_dotenv(args.env_file)
print(
json.dumps(
{
@@ -53,7 +54,7 @@ def main(argv: list[str] | None = None) -> int:
return 0
if args.command == "validate-assets":
infos = validate_pet_assets(AppConfig.from_env().asset_dir)
infos = validate_pet_assets(AppConfig.from_dotenv(args.env_file).asset_dir)
print(json.dumps({"assets": len(infos), "valid": True}, ensure_ascii=False, sort_keys=True))
return 0
@@ -68,7 +69,7 @@ def main(argv: list[str] | None = None) -> int:
return 0 if result["success"] else 1
if args.command == "llm-smoke":
config = AppConfig.from_env()
config = AppConfig.from_dotenv(args.env_file)
if args.no_stream:
config = AppConfig(
wake_word=config.wake_word,
@@ -102,7 +103,7 @@ def main(argv: list[str] | None = None) -> int:
def run_acceptance() -> dict[str, object]:
validate_pet_assets(AppConfig.from_env().asset_dir)
validate_pet_assets(AppConfig.from_dotenv().asset_dir)
frames = [
AudioFrame(b"\x80\x80", 16000, 1, 0, 0, {"duration_ms": 20, "wake_word": "小杰小杰", "wake_confidence": 0.99}),
AudioFrame(b"\xff\xff", 16000, 1, 20, 1, {"duration_ms": 20, "speech": True, "transcript": "你是谁"}),
+37 -3
View File
@@ -1,6 +1,5 @@
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
@@ -25,9 +24,11 @@ class AppConfig:
context_max_chars: int = 12000
@classmethod
def from_env(cls, prefix: str = "OWNER_") -> "AppConfig":
def from_dotenv(cls, path: str | Path = ".env", prefix: str = "OWNER_") -> "AppConfig":
values = parse_dotenv(Path(path))
def get(name: str, default: str | None = None) -> str | None:
value = os.environ.get(f"{prefix}{name}")
value = values.get(f"{prefix}{name}")
return default if value is None or value == "" else value
return cls(
@@ -47,6 +48,10 @@ class AppConfig:
context_max_chars=int(get("CONTEXT_MAX_CHARS", "12000") or "12000"),
)
@classmethod
def from_env(cls, prefix: str = "OWNER_") -> "AppConfig":
return cls.from_dotenv(".env", prefix=prefix)
def require_llm_credentials(self) -> None:
if not self.llm_api_key:
raise ProviderError(
@@ -100,3 +105,32 @@ class AppConfig:
)
)
return errors
def parse_dotenv(path: Path) -> dict[str, str]:
if not path.exists():
return {}
values: dict[str, str] = {}
for line_no, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line.startswith("export "):
line = line.removeprefix("export ").strip()
if "=" not in line:
raise ValueError(f"invalid .env line {line_no}: missing '='")
key, value = line.split("=", 1)
key = key.strip()
value = _strip_dotenv_value(value.strip())
if not key:
raise ValueError(f"invalid .env line {line_no}: empty key")
values[key] = value
return values
def _strip_dotenv_value(value: str) -> str:
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
return value[1:-1]
if " #" in value:
return value.split(" #", 1)[0].rstrip()
return value