fix(opencode): address issues found during OpenCode integration review

- Fix MCP server not removed from opencode.json when unchecked in edit modal
- Fix Windows atomic write failure when opencode.json already exists
- Fix i18n keys mismatch in OpenCodeFormFields (use opencode.* namespace)
- Fix unit test missing apps.opencode field assertion
This commit is contained in:
Jason
2026-01-15 19:07:49 +08:00
parent 36d6d48002
commit de3a22535d
12 changed files with 53 additions and 62 deletions
+6 -1
View File
@@ -587,7 +587,12 @@ impl MultiAppConfig {
log::info!("检测到已存在配置文件且 Prompt 列表为空,将尝试从现有提示词文件自动导入");
let mut imported = false;
for app in [AppType::Claude, AppType::Codex, AppType::Gemini, AppType::OpenCode] {
for app in [
AppType::Claude,
AppType::Codex,
AppType::Gemini,
AppType::OpenCode,
] {
// 复用已有的单应用导入逻辑
if Self::auto_import_prompt_if_exists(self, app)? {
imported = true;
+7 -13
View File
@@ -68,10 +68,7 @@ pub fn convert_to_opencode_format(spec: &Value) -> Result<Value, AppError> {
.as_object()
.ok_or_else(|| AppError::McpValidation("MCP spec must be a JSON object".into()))?;
let typ = obj
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("stdio");
let typ = obj.get("type").and_then(|v| v.as_str()).unwrap_or("stdio");
let mut result = serde_json::Map::new();
@@ -81,10 +78,7 @@ pub fn convert_to_opencode_format(spec: &Value) -> Result<Value, AppError> {
result.insert("type".into(), json!("local"));
// Merge command and args into a single array
let cmd = obj
.get("command")
.and_then(|v| v.as_str())
.unwrap_or("");
let cmd = obj.get("command").and_then(|v| v.as_str()).unwrap_or("");
let mut command_arr = vec![json!(cmd)];
if let Some(args) = obj.get("args").and_then(|v| v.as_array()) {
@@ -149,10 +143,7 @@ pub fn convert_from_opencode_format(spec: &Value) -> Result<Value, AppError> {
.as_object()
.ok_or_else(|| AppError::McpValidation("OpenCode MCP spec must be a JSON object".into()))?;
let typ = obj
.get("type")
.and_then(|v| v.as_str())
.unwrap_or("local");
let typ = obj.get("type").and_then(|v| v.as_str()).unwrap_or("local");
let mut result = serde_json::Map::new();
@@ -360,7 +351,10 @@ mod tests {
assert_eq!(result["type"], "local");
assert_eq!(result["command"][0], "npx");
assert_eq!(result["command"][1], "-y");
assert_eq!(result["command"][2], "@modelcontextprotocol/server-filesystem");
assert_eq!(
result["command"][2],
"@modelcontextprotocol/server-filesystem"
);
assert_eq!(result["environment"]["HOME"], "/Users/test");
assert_eq!(result["enabled"], true);
}
+11 -29
View File
@@ -21,6 +21,7 @@
//! }
//! ```
use crate::config::write_json_file;
use crate::error::AppError;
use crate::provider::{OpenCodeModel, OpenCodeProviderConfig, OpenCodeProviderOptions};
use crate::settings::get_opencode_override_dir;
@@ -100,21 +101,8 @@ pub fn read_opencode_config() -> Result<Value, AppError> {
/// 使用临时文件 + 重命名确保原子性
pub fn write_opencode_config(config: &Value) -> Result<(), AppError> {
let path = get_opencode_config_path();
// Ensure directory exists
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
}
// Write to temporary file first
let temp_path = path.with_extension("json.tmp");
let content =
serde_json::to_string_pretty(config).map_err(|e| AppError::JsonSerialize { source: e })?;
std::fs::write(&temp_path, &content).map_err(|e| AppError::io(&temp_path, e))?;
// Atomic rename
std::fs::rename(&temp_path, &path).map_err(|e| AppError::io(&path, e))?;
// 复用统一的原子写入逻辑(兼容 Windows 上目标文件已存在的情况)
write_json_file(&path, config)?;
log::debug!("OpenCode config written to {:?}", path);
Ok(())
@@ -147,7 +135,10 @@ pub fn set_provider(id: &str, config: Value) -> Result<(), AppError> {
full_config["provider"] = json!({});
}
if let Some(providers) = full_config.get_mut("provider").and_then(|v| v.as_object_mut()) {
if let Some(providers) = full_config
.get_mut("provider")
.and_then(|v| v.as_object_mut())
{
providers.insert(id.to_string(), config);
}
@@ -205,8 +196,7 @@ pub fn get_typed_provider(id: &str) -> Result<Option<OpenCodeProviderConfig>, Ap
/// 设置供应商配置(类型化)
pub fn set_typed_provider(id: &str, config: &OpenCodeProviderConfig) -> Result<(), AppError> {
let value =
serde_json::to_value(config).map_err(|e| AppError::JsonSerialize { source: e })?;
let value = serde_json::to_value(config).map_err(|e| AppError::JsonSerialize { source: e })?;
set_provider(id, value)
}
@@ -302,15 +292,7 @@ pub fn create_provider_config(
let model_map: HashMap<String, OpenCodeModel> = models
.into_iter()
.map(|(id, name)| {
(
id,
OpenCodeModel {
name,
limit: None,
},
)
})
.map(|(id, name)| (id, OpenCodeModel { name, limit: None }))
.collect();
OpenCodeProviderConfig {
@@ -350,11 +332,11 @@ pub fn validate_provider_config(config: &OpenCodeProviderConfig) -> Result<(), A
pub fn provider_to_opencode_config(
settings_config: &Value,
) -> Result<OpenCodeProviderConfig, AppError> {
serde_json::from_value(settings_config.clone()).map_err(|e| AppError::JsonSerialize { source: e })
serde_json::from_value(settings_config.clone())
.map_err(|e| AppError::JsonSerialize { source: e })
}
/// 将 OpenCode 配置转换为通用 Provider settings_config
pub fn opencode_config_to_provider(config: &OpenCodeProviderConfig) -> Result<Value, AppError> {
serde_json::to_value(config).map_err(|e| AppError::JsonSerialize { source: e })
}
-1
View File
@@ -571,4 +571,3 @@ impl OpenCodeProviderConfig {
.unwrap_or(false)
}
}
+8 -1
View File
@@ -37,6 +37,9 @@ impl McpService {
if prev_apps.gemini && !server.apps.gemini {
Self::remove_server_from_app(state, &server.id, &AppType::Gemini)?;
}
if prev_apps.opencode && !server.apps.opencode {
Self::remove_server_from_app(state, &server.id, &AppType::OpenCode)?;
}
// 同步到各个启用的应用
Self::sync_server_to_apps(state, &server)?;
@@ -114,7 +117,11 @@ impl McpService {
mcp::sync_single_server_to_gemini(&Default::default(), &server.id, &server.server)?;
}
AppType::OpenCode => {
mcp::sync_single_server_to_opencode(&Default::default(), &server.id, &server.server)?;
mcp::sync_single_server_to_opencode(
&Default::default(),
&server.id,
&server.server,
)?;
}
}
Ok(())
+5 -5
View File
@@ -132,10 +132,7 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re
match opencode_config_result {
Ok(config) => {
opencode_config::set_typed_provider(&provider.id, &config)?;
log::info!(
"OpenCode provider '{}' written to live config",
provider.id
);
log::info!("OpenCode provider '{}' written to live config", provider.id);
}
Err(e) => {
log::warn!(
@@ -505,7 +502,10 @@ pub fn import_opencode_providers_from_live(state: &AppState) -> Result<usize, Ap
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);
log::debug!(
"OpenCode provider '{}' already exists in database, skipping",
id
);
continue;
}
+1 -1
View File
@@ -190,7 +190,7 @@ impl StreamCheckService {
return Err(AppError::localized(
"opencode_no_stream_check",
"OpenCode 暂不支持健康检查",
"OpenCode does not support health check yet"
"OpenCode does not support health check yet",
));
}
};