refactor(provider): certify typed write ownership

Freeze prerequisite A as a component-level certification unit. Add the immutable v5 certification suite, split create/update row DTOs, preserve immutable creation time, map strict-create races to AppError::Conflict, make aggregate compensation insert-or-restore, and enforce reconcile preconditions through a single-lock transaction primitive.

Old save_provider callsite classification remains exhaustively recorded in 4f78451405575158ff6562c7021c7f31f2860780; this checkpoint does not add or reclassify an omitted legacy callsite. It tightens the remaining reconciliation classifications there: default live import is [create]; OpenCode/OpenClaw/Hermes existing branches are [update] and absent branches are [create]; universal Claude/Codex/Gemini branches are [create/update] selected from an observed fingerprint. The sealed compensation helper remains the only [restore] path. The old reconcile_provider_record symbol is deleted.

Remaining update_provider_settings_config callsites are classified as [update]: codex_history_migration updates an already-read Codex row; proxy token synchronization updates already-read Claude, Codex, Gemini, and GrokBuild rows. Each now uses ProviderKey plus ProviderRowUpdate, explicitly removes hydrated endpoint projections, preserves endpoint authority, and fails on a missing row instead of silently succeeding.
This commit is contained in:
SaladDay
2026-08-01 07:48:57 +00:00
parent 10f2dacbe4
commit 2bc92e0f79
15 changed files with 2763 additions and 109 deletions
+43 -24
View File
@@ -4,13 +4,14 @@
use crate::app_config::AppType;
use crate::config::{get_claude_settings_path, read_json_file, write_json_file};
use crate::database::Database;
use crate::database::{Database, ProviderKey, ProviderRowUpdate};
use crate::provider::Provider;
use crate::proxy::server::ProxyServer;
use crate::proxy::switch_lock::SwitchLockManager;
use crate::proxy::types::*;
use crate::services::provider::{
build_effective_settings_with_common_config, write_live_with_common_config,
build_effective_settings_with_common_config, provider_to_mutation_input,
write_live_with_common_config,
};
use serde_json::{json, Map, Value};
use std::str::FromStr;
@@ -1055,11 +1056,16 @@ impl ProxyService {
}
}
if let Err(e) = self.db.update_provider_settings_config(
"claude",
&provider_id,
&provider.settings_config,
) {
if let Some(meta) = provider.meta.as_mut() {
meta.custom_endpoints.clear();
}
let input = provider_to_mutation_input(provider);
let result =
ProviderKey::new("claude", &provider_id).and_then(|key| {
let row = ProviderRowUpdate::from_input(&input)?;
self.db.update_provider(&key, &row)
});
if let Err(e) = result {
log::warn!("同步 Claude Token 到数据库失败: {e}");
} else {
log::info!(
@@ -1116,11 +1122,15 @@ impl ProxyService {
}
}
if let Err(e) = self.db.update_provider_settings_config(
"codex",
&provider_id,
&provider.settings_config,
) {
if let Some(meta) = provider.meta.as_mut() {
meta.custom_endpoints.clear();
}
let input = provider_to_mutation_input(provider);
let result = ProviderKey::new("codex", &provider_id).and_then(|key| {
let row = ProviderRowUpdate::from_input(&input)?;
self.db.update_provider(&key, &row)
});
if let Err(e) = result {
log::warn!("同步 Codex Token 到数据库失败: {e}");
} else {
log::info!("已同步 Codex Token 到数据库 (provider: {provider_id})");
@@ -1168,11 +1178,15 @@ impl ProxyService {
}
}
if let Err(e) = self.db.update_provider_settings_config(
"gemini",
&provider_id,
&provider.settings_config,
) {
if let Some(meta) = provider.meta.as_mut() {
meta.custom_endpoints.clear();
}
let input = provider_to_mutation_input(provider);
let result = ProviderKey::new("gemini", &provider_id).and_then(|key| {
let row = ProviderRowUpdate::from_input(&input)?;
self.db.update_provider(&key, &row)
});
if let Err(e) = result {
log::warn!("同步 Gemini Token 到数据库失败: {e}");
} else {
log::info!(
@@ -1211,15 +1225,20 @@ impl ProxyService {
format!("更新 Grok Build API Key 失败: {e}")
})?;
provider.settings_config["config"] = json!(updated);
self.db
.update_provider_settings_config(
"grokbuild",
&provider_id,
&provider.settings_config,
)
.map_err(|e| {
if let Some(meta) = provider.meta.as_mut() {
meta.custom_endpoints.clear();
}
let input = provider_to_mutation_input(provider);
let key = ProviderKey::new("grokbuild", &provider_id).map_err(
|e| format!("同步 Grok Build Token 到数据库失败: {e}"),
)?;
let row =
ProviderRowUpdate::from_input(&input).map_err(|e| {
format!("同步 Grok Build Token 到数据库失败: {e}")
})?;
self.db.update_provider(&key, &row).map_err(|e| {
format!("同步 Grok Build Token 到数据库失败: {e}")
})?;
}
}
}