feat: 添加 Hermite 同步服务与导航优化

This commit is contained in:
caoxiaozhu
2026-05-09 09:14:04 +00:00
parent 6d91528b7c
commit 694ee42781
9 changed files with 455 additions and 95 deletions

View File

@@ -17,6 +17,12 @@ from app.models.system_setting import SystemSetting
from app.models.system_setting_secret import SystemSettingSecret
from app.repositories.settings import SETTINGS_ROW_ID, SettingsRepository
from app.schemas.settings import SettingsRead, SettingsWrite
from app.services.hermes_sync import (
HermesModelRoute,
capture_hermes_config_snapshot,
restore_hermes_config_snapshot,
sync_hermes_model_settings,
)
@dataclass(frozen=True, slots=True)
@@ -32,23 +38,23 @@ class ModelSlotConfig:
priority: int
MODEL_SLOT_CONFIGS = {
"main": ModelSlotConfig(
provider_attr="main_provider",
model_attr="main_model",
endpoint_attr="main_endpoint",
legacy_secret_attr="main_api_key_encrypted",
default_provider="Codex",
default_model="codex-mini-latest",
default_endpoint="https://api.openai.com/v1",
capability="chat",
priority=10,
),
"backup": ModelSlotConfig(
provider_attr="backup_provider",
model_attr="backup_model",
endpoint_attr="backup_endpoint",
legacy_secret_attr="backup_api_key_encrypted",
MODEL_SLOT_CONFIGS = {
"main": ModelSlotConfig(
provider_attr="main_provider",
model_attr="main_model",
endpoint_attr="main_endpoint",
legacy_secret_attr="main_api_key_encrypted",
default_provider="Codex",
default_model="codex-mini-latest",
default_endpoint="https://api.openai.com/v1",
capability="chat",
priority=10,
),
"backup": ModelSlotConfig(
provider_attr="backup_provider",
model_attr="backup_model",
endpoint_attr="backup_endpoint",
legacy_secret_attr="backup_api_key_encrypted",
default_provider="GLM",
default_model="glm-5.1",
default_endpoint="https://open.bigmodel.cn/api/paas/v4/",
@@ -175,9 +181,9 @@ class SettingsService:
model_rows = self.ensure_model_settings_ready(settings_row, secrets_row)
return self._serialize(settings_row, secrets_row, model_rows)
def save_settings_snapshot(self, payload: SettingsWrite) -> SettingsRead:
settings_row, secrets_row = self.ensure_settings_ready()
model_rows = self.ensure_model_settings_ready(settings_row, secrets_row)
def save_settings_snapshot(self, payload: SettingsWrite) -> SettingsRead:
settings_row, secrets_row = self.ensure_settings_ready()
model_rows = self.ensure_model_settings_ready(settings_row, secrets_row)
if payload.adminForm.newPassword:
if len(payload.adminForm.newPassword) < 5:
@@ -200,18 +206,18 @@ class SettingsService:
settings_row.strong_password = payload.adminForm.strongPassword
settings_row.login_alert_enabled = payload.adminForm.loginAlertEnabled
self._apply_model_setting(
model_rows["main"],
payload.llmForm.mainProvider,
payload.llmForm.mainModel,
payload.llmForm.mainEndpoint,
payload.llmForm.mainApiKey,
)
self._apply_model_setting(
model_rows["backup"],
payload.llmForm.backupProvider,
payload.llmForm.backupModel,
payload.llmForm.backupEndpoint,
self._apply_model_setting(
model_rows["main"],
payload.llmForm.mainProvider,
payload.llmForm.mainModel,
payload.llmForm.mainEndpoint,
payload.llmForm.mainApiKey,
)
self._apply_model_setting(
model_rows["backup"],
payload.llmForm.backupProvider,
payload.llmForm.backupModel,
payload.llmForm.backupEndpoint,
payload.llmForm.backupApiKey,
)
self._apply_model_setting(
@@ -279,30 +285,60 @@ class SettingsService:
payload.renderForm.jwtSecret,
)
self._replace_secret_if_present(secrets_row, "smtp_password_encrypted", payload.mailForm.password)
hermes_snapshot = capture_hermes_config_snapshot()
try:
sync_hermes_model_settings(
primary_route=self._build_hermes_model_route(model_rows["main"]),
fallback_route=self._build_hermes_model_route(model_rows["backup"]),
)
self.db.add(settings_row)
self.db.add(secrets_row)
for model_row in model_rows.values():
self.db.add(model_row)
self.db.commit()
except Exception:
self.db.rollback()
restore_hermes_config_snapshot(hermes_snapshot)
raise
self.db.refresh(settings_row)
self.db.refresh(secrets_row)
for model_row in model_rows.values():
self.db.refresh(model_row)
return self._serialize(settings_row, secrets_row, model_rows)
self.db.add(settings_row)
self.db.add(secrets_row)
for model_row in model_rows.values():
self.db.add(model_row)
self.db.commit()
self.db.refresh(settings_row)
self.db.refresh(secrets_row)
for model_row in model_rows.values():
self.db.refresh(model_row)
return self._serialize(settings_row, secrets_row, model_rows)
def load_saved_model_api_key(self, slot: str | None) -> str:
if not slot or slot not in MODEL_SLOT_CONFIGS:
return ""
def load_saved_model_api_key(self, slot: str | None) -> str:
if not slot or slot not in MODEL_SLOT_CONFIGS:
return ""
settings_row, secrets_row = self.ensure_settings_ready()
model_rows = self.ensure_model_settings_ready(settings_row, secrets_row)
encrypted_value = model_rows[slot].api_key_encrypted
if not encrypted_value:
return ""
return decrypt_secret(encrypted_value)
return decrypt_secret(encrypted_value)
def get_runtime_model_config(self, slot: str) -> dict[str, str]:
if slot not in MODEL_SLOT_CONFIGS:
raise ValueError("未知模型槽位。")
settings_row, secrets_row = self.ensure_settings_ready()
model_rows = self.ensure_model_settings_ready(settings_row, secrets_row)
model_row = model_rows[slot]
return {
"slot": slot,
"provider": model_row.provider,
"model": model_row.model_name,
"endpoint": model_row.endpoint,
"apiKey": self.load_saved_model_api_key(slot),
"capability": model_row.capability,
}
def get_admin_credentials(self) -> AdminCredentialRecord | None:
settings_row, secrets_row = self.ensure_settings_ready()
@@ -448,10 +484,22 @@ class SettingsService:
model_row.model_name = model_name
model_row.endpoint = endpoint
normalized_api_key = api_key.strip()
normalized_api_key = api_key.strip()
if normalized_api_key:
model_row.api_key_encrypted = encrypt_secret(normalized_api_key)
def _build_hermes_model_route(self, model_row: SystemModelSetting) -> HermesModelRoute:
api_key = ""
if model_row.api_key_encrypted:
api_key = decrypt_secret(model_row.api_key_encrypted)
return HermesModelRoute(
provider_label=str(model_row.provider or "").strip(),
model=str(model_row.model_name or "").strip(),
endpoint=str(model_row.endpoint or "").strip(),
api_key=api_key,
)
def _ensure_settings_schema(self) -> None:
bind = self.db.get_bind()
inspector = inspect(bind)