feat(opencode): complete Phase 5 - provider service layer

Implement OpenCode-specific provider service logic with additive mode:
- add(): Always write to live config (no is_current check needed)
- update(): Always sync changes to live config
- delete(): Remove from both DB and live config (no is_current check)

New helper functions in live.rs:
- write_live_snapshot(): Write provider to opencode.json provider section
- remove_opencode_provider_from_live(): Remove provider from live config
- import_opencode_providers_from_live(): Import existing providers from
  ~/.config/opencode/opencode.json into CC Switch database

Key design: OpenCode uses additive mode where all providers coexist
in the config file, unlike Claude/Codex/Gemini which use replacement
mode with a single active provider.
This commit is contained in:
Jason
2026-01-15 16:15:01 +08:00
parent 7ea2c3452b
commit b8538b211d
2 changed files with 137 additions and 11 deletions
+104 -3
View File
@@ -121,9 +121,32 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re
write_gemini_live(provider)?;
}
AppType::OpenCode => {
// OpenCode uses additive mode - providers are written directly to config
// This will be fully implemented in Phase 3
log::debug!("OpenCode live snapshot not needed (additive mode)");
// OpenCode uses additive mode - write provider to config
use crate::opencode_config;
use crate::provider::OpenCodeProviderConfig;
// Convert settings_config to OpenCodeProviderConfig
let opencode_config_result =
serde_json::from_value::<OpenCodeProviderConfig>(provider.settings_config.clone());
match opencode_config_result {
Ok(config) => {
opencode_config::set_typed_provider(&provider.id, &config)?;
log::info!(
"OpenCode provider '{}' written to live config",
provider.id
);
}
Err(e) => {
log::warn!(
"Failed to parse OpenCode provider config for '{}': {}",
provider.id,
e
);
// Still try to write as raw JSON
opencode_config::set_provider(&provider.id, provider.settings_config.clone())?;
}
}
}
}
Ok(())
@@ -437,3 +460,81 @@ pub(crate) fn write_gemini_live(provider: &Provider) -> Result<(), AppError> {
Ok(())
}
/// Remove an OpenCode provider from the live configuration
///
/// This is specific to OpenCode's additive mode - removing a provider
/// from the opencode.json file.
pub(crate) fn remove_opencode_provider_from_live(provider_id: &str) -> Result<(), AppError> {
use crate::opencode_config;
// Check if OpenCode config directory exists
if !opencode_config::get_opencode_dir().exists() {
log::debug!(
"OpenCode config directory doesn't exist, skipping removal of '{}'",
provider_id
);
return Ok(());
}
opencode_config::remove_provider(provider_id)?;
log::info!(
"OpenCode provider '{}' removed from live config",
provider_id
);
Ok(())
}
/// Import all providers from OpenCode live config to database
///
/// This imports existing providers from ~/.config/opencode/opencode.json
/// into the CC Switch database. Each provider found will be added to the
/// database with is_current set to false.
pub fn import_opencode_providers_from_live(state: &AppState) -> Result<usize, AppError> {
use crate::opencode_config;
let providers = opencode_config::get_typed_providers()?;
if providers.is_empty() {
return Ok(0);
}
let mut imported = 0;
let existing = state.db.get_all_providers("opencode")?;
for (id, config) in providers {
// Skip if already exists in database
if existing.contains_key(&id) {
log::debug!("OpenCode provider '{}' already exists in database, skipping", id);
continue;
}
// Convert to Value for settings_config
let settings_config = match serde_json::to_value(&config) {
Ok(v) => v,
Err(e) => {
log::warn!("Failed to serialize OpenCode provider '{}': {}", id, e);
continue;
}
};
// Create provider
let provider = Provider::with_id(
id.clone(),
config.name.clone().unwrap_or_else(|| id.clone()),
settings_config,
None,
);
// Save to database
if let Err(e) = state.db.save_provider("opencode", &provider) {
log::warn!("Failed to import OpenCode provider '{}': {}", id, e);
continue;
}
imported += 1;
log::info!("Imported OpenCode provider '{}' from live config", id);
}
Ok(imported)
}
+33 -8
View File
@@ -20,13 +20,16 @@ use crate::settings::CustomEndpoint;
use crate::store::AppState;
// Re-export sub-module functions for external access
pub use live::{import_default_config, read_live_settings, sync_current_to_live};
pub use live::{
import_default_config, import_opencode_providers_from_live, read_live_settings,
sync_current_to_live,
};
// Internal re-exports (pub(crate))
pub(crate) use live::write_live_snapshot;
// Internal re-exports
use live::write_gemini_live;
use live::{remove_opencode_provider_from_live, write_gemini_live};
use usage::validate_usage_script;
/// Provider business logic service
@@ -152,7 +155,13 @@ impl ProviderService {
// Save to database
state.db.save_provider(app_type.as_str(), &provider)?;
// Check if sync is needed (if this is current provider, or no current provider)
// OpenCode uses additive mode - always write to live config
if matches!(app_type, AppType::OpenCode) {
write_live_snapshot(&app_type, &provider)?;
return Ok(true);
}
// For other apps: Check if sync is needed (if this is current provider, or no current provider)
let current = state.db.get_current_provider(app_type.as_str())?;
if current.is_none() {
// No current provider, set as current and sync
@@ -176,14 +185,20 @@ impl ProviderService {
Self::normalize_provider_if_claude(&app_type, &mut provider);
Self::validate_provider_settings(&app_type, &provider)?;
// Check if this is current provider (use effective current, not just DB)
// Save to database
state.db.save_provider(app_type.as_str(), &provider)?;
// OpenCode uses additive mode - always update in live config
if matches!(app_type, AppType::OpenCode) {
write_live_snapshot(&app_type, &provider)?;
return Ok(true);
}
// For other apps: Check if this is current provider (use effective current, not just DB)
let effective_current =
crate::settings::get_effective_current_provider(&state.db, &app_type)?;
let is_current = effective_current.as_deref() == Some(provider.id.as_str());
// Save to database
state.db.save_provider(app_type.as_str(), &provider)?;
if is_current {
// 如果代理接管模式处于激活状态,并且代理服务正在运行:
// - 不写 Live 配置(否则会破坏接管)
@@ -216,8 +231,18 @@ impl ProviderService {
/// Delete a provider
///
/// 同时检查本地 settings 和数据库的当前供应商,防止删除任一端正在使用的供应商。
/// 对于 OpenCode(累加模式),可以随时删除任意供应商,同时从 live 配置中移除。
pub fn delete(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> {
// Check both local settings and database
// OpenCode uses additive mode - no current provider concept
if matches!(app_type, AppType::OpenCode) {
// Remove from database
state.db.delete_provider(app_type.as_str(), id)?;
// Also remove from live config
remove_opencode_provider_from_live(id)?;
return Ok(());
}
// For other apps: Check both local settings and database
let local_current = crate::settings::get_current_provider(&app_type);
let db_current = state.db.get_current_provider(app_type.as_str())?;