mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-03-29 23:29:10 +08:00
feat(opencode): Phase 1 - Backend data structure expansion for OpenCode support
Add OpenCode as the 4th supported application with additive provider management: - Add OpenCode variant to AppType enum with all related match statements - Add enabled_opencode field to McpApps and SkillApps structures - Add opencode field to McpRoot and PromptRoot - Add database schema migration v3→v4 with enabled_opencode columns - Add settings.rs support for opencode_config_dir and current_provider_opencode - Create opencode_config.rs module for config file I/O operations - Update all services (proxy, mcp, skill, provider, stream_check) for OpenCode - Add OpenCode support to deeplink provider and MCP parsing - Update commands/config.rs for OpenCode config status and paths Key design decisions: - OpenCode uses additive mode (no is_current needed, no proxy support) - Config path: ~/.config/opencode/opencode.json - MCP format: stdio→local, sse/http→remote conversion planned - Stream check returns error (not yet implemented for OpenCode)
This commit is contained in:
@@ -13,6 +13,8 @@ pub struct McpApps {
|
||||
pub codex: bool,
|
||||
#[serde(default)]
|
||||
pub gemini: bool,
|
||||
#[serde(default)]
|
||||
pub opencode: bool,
|
||||
}
|
||||
|
||||
impl McpApps {
|
||||
@@ -22,6 +24,7 @@ impl McpApps {
|
||||
AppType::Claude => self.claude,
|
||||
AppType::Codex => self.codex,
|
||||
AppType::Gemini => self.gemini,
|
||||
AppType::OpenCode => self.opencode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +34,7 @@ impl McpApps {
|
||||
AppType::Claude => self.claude = enabled,
|
||||
AppType::Codex => self.codex = enabled,
|
||||
AppType::Gemini => self.gemini = enabled,
|
||||
AppType::OpenCode => self.opencode = enabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,12 +50,15 @@ impl McpApps {
|
||||
if self.gemini {
|
||||
apps.push(AppType::Gemini);
|
||||
}
|
||||
if self.opencode {
|
||||
apps.push(AppType::OpenCode);
|
||||
}
|
||||
apps
|
||||
}
|
||||
|
||||
/// 检查是否所有应用都未启用
|
||||
pub fn is_empty(&self) -> bool {
|
||||
!self.claude && !self.codex && !self.gemini
|
||||
!self.claude && !self.codex && !self.gemini && !self.opencode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +71,8 @@ pub struct SkillApps {
|
||||
pub codex: bool,
|
||||
#[serde(default)]
|
||||
pub gemini: bool,
|
||||
#[serde(default)]
|
||||
pub opencode: bool,
|
||||
}
|
||||
|
||||
impl SkillApps {
|
||||
@@ -73,6 +82,7 @@ impl SkillApps {
|
||||
AppType::Claude => self.claude,
|
||||
AppType::Codex => self.codex,
|
||||
AppType::Gemini => self.gemini,
|
||||
AppType::OpenCode => self.opencode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +92,7 @@ impl SkillApps {
|
||||
AppType::Claude => self.claude = enabled,
|
||||
AppType::Codex => self.codex = enabled,
|
||||
AppType::Gemini => self.gemini = enabled,
|
||||
AppType::OpenCode => self.opencode = enabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,12 +108,15 @@ impl SkillApps {
|
||||
if self.gemini {
|
||||
apps.push(AppType::Gemini);
|
||||
}
|
||||
if self.opencode {
|
||||
apps.push(AppType::OpenCode);
|
||||
}
|
||||
apps
|
||||
}
|
||||
|
||||
/// 检查是否所有应用都未启用
|
||||
pub fn is_empty(&self) -> bool {
|
||||
!self.claude && !self.codex && !self.gemini
|
||||
!self.claude && !self.codex && !self.gemini && !self.opencode
|
||||
}
|
||||
|
||||
/// 仅启用指定应用(其他应用设为禁用)
|
||||
@@ -205,6 +219,9 @@ pub struct McpRoot {
|
||||
pub codex: McpConfig,
|
||||
#[serde(default, skip_serializing_if = "McpConfig::is_empty")]
|
||||
pub gemini: McpConfig,
|
||||
/// OpenCode MCP 配置(v4.0.0+,实际使用 opencode.json)
|
||||
#[serde(default, skip_serializing_if = "McpConfig::is_empty")]
|
||||
pub opencode: McpConfig,
|
||||
}
|
||||
|
||||
impl Default for McpRoot {
|
||||
@@ -216,6 +233,7 @@ impl Default for McpRoot {
|
||||
claude: McpConfig::default(),
|
||||
codex: McpConfig::default(),
|
||||
gemini: McpConfig::default(),
|
||||
opencode: McpConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,6 +254,8 @@ pub struct PromptRoot {
|
||||
pub codex: PromptConfig,
|
||||
#[serde(default)]
|
||||
pub gemini: PromptConfig,
|
||||
#[serde(default)]
|
||||
pub opencode: PromptConfig,
|
||||
}
|
||||
|
||||
use crate::config::{copy_file, get_app_config_dir, get_app_config_path, write_json_file};
|
||||
@@ -249,7 +269,8 @@ use crate::provider::ProviderManager;
|
||||
pub enum AppType {
|
||||
Claude,
|
||||
Codex,
|
||||
Gemini, // 新增
|
||||
Gemini,
|
||||
OpenCode,
|
||||
}
|
||||
|
||||
impl AppType {
|
||||
@@ -257,7 +278,8 @@ impl AppType {
|
||||
match self {
|
||||
AppType::Claude => "claude",
|
||||
AppType::Codex => "codex",
|
||||
AppType::Gemini => "gemini", // 新增
|
||||
AppType::Gemini => "gemini",
|
||||
AppType::OpenCode => "opencode",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,11 +292,12 @@ impl FromStr for AppType {
|
||||
match normalized.as_str() {
|
||||
"claude" => Ok(AppType::Claude),
|
||||
"codex" => Ok(AppType::Codex),
|
||||
"gemini" => Ok(AppType::Gemini), // 新增
|
||||
"gemini" => Ok(AppType::Gemini),
|
||||
"opencode" => Ok(AppType::OpenCode),
|
||||
other => Err(AppError::localized(
|
||||
"unsupported_app",
|
||||
format!("不支持的应用标识: '{other}'。可选值: claude, codex, gemini。"),
|
||||
format!("Unsupported app id: '{other}'. Allowed: claude, codex, gemini."),
|
||||
format!("不支持的应用标识: '{other}'。可选值: claude, codex, gemini, opencode。"),
|
||||
format!("Unsupported app id: '{other}'. Allowed: claude, codex, gemini, opencode."),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -291,6 +314,9 @@ pub struct CommonConfigSnippets {
|
||||
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub gemini: Option<String>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opencode: Option<String>,
|
||||
}
|
||||
|
||||
impl CommonConfigSnippets {
|
||||
@@ -300,6 +326,7 @@ impl CommonConfigSnippets {
|
||||
AppType::Claude => self.claude.as_ref(),
|
||||
AppType::Codex => self.codex.as_ref(),
|
||||
AppType::Gemini => self.gemini.as_ref(),
|
||||
AppType::OpenCode => self.opencode.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +336,7 @@ impl CommonConfigSnippets {
|
||||
AppType::Claude => self.claude = snippet,
|
||||
AppType::Codex => self.codex = snippet,
|
||||
AppType::Gemini => self.gemini = snippet,
|
||||
AppType::OpenCode => self.opencode = snippet,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,7 +375,8 @@ impl Default for MultiAppConfig {
|
||||
let mut apps = HashMap::new();
|
||||
apps.insert("claude".to_string(), ProviderManager::default());
|
||||
apps.insert("codex".to_string(), ProviderManager::default());
|
||||
apps.insert("gemini".to_string(), ProviderManager::default()); // 新增
|
||||
apps.insert("gemini".to_string(), ProviderManager::default());
|
||||
apps.insert("opencode".to_string(), ProviderManager::default());
|
||||
|
||||
Self {
|
||||
version: 2,
|
||||
@@ -506,6 +535,7 @@ impl MultiAppConfig {
|
||||
AppType::Claude => &self.mcp.claude,
|
||||
AppType::Codex => &self.mcp.codex,
|
||||
AppType::Gemini => &self.mcp.gemini,
|
||||
AppType::OpenCode => &self.mcp.opencode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,6 +545,7 @@ impl MultiAppConfig {
|
||||
AppType::Claude => &mut self.mcp.claude,
|
||||
AppType::Codex => &mut self.mcp.codex,
|
||||
AppType::Gemini => &mut self.mcp.gemini,
|
||||
AppType::OpenCode => &mut self.mcp.opencode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,6 +559,7 @@ impl MultiAppConfig {
|
||||
Self::auto_import_prompt_if_exists(&mut config, AppType::Claude)?;
|
||||
Self::auto_import_prompt_if_exists(&mut config, AppType::Codex)?;
|
||||
Self::auto_import_prompt_if_exists(&mut config, AppType::Gemini)?;
|
||||
Self::auto_import_prompt_if_exists(&mut config, AppType::OpenCode)?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
@@ -547,6 +579,7 @@ impl MultiAppConfig {
|
||||
if !self.prompts.claude.prompts.is_empty()
|
||||
|| !self.prompts.codex.prompts.is_empty()
|
||||
|| !self.prompts.gemini.prompts.is_empty()
|
||||
|| !self.prompts.opencode.prompts.is_empty()
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -554,7 +587,7 @@ impl MultiAppConfig {
|
||||
log::info!("检测到已存在配置文件且 Prompt 列表为空,将尝试从现有提示词文件自动导入");
|
||||
|
||||
let mut imported = false;
|
||||
for app in [AppType::Claude, AppType::Codex, AppType::Gemini] {
|
||||
for app in [AppType::Claude, AppType::Codex, AppType::Gemini, AppType::OpenCode] {
|
||||
// 复用已有的单应用导入逻辑
|
||||
if Self::auto_import_prompt_if_exists(self, app)? {
|
||||
imported = true;
|
||||
@@ -623,6 +656,7 @@ impl MultiAppConfig {
|
||||
AppType::Claude => &mut config.prompts.claude.prompts,
|
||||
AppType::Codex => &mut config.prompts.codex.prompts,
|
||||
AppType::Gemini => &mut config.prompts.gemini.prompts,
|
||||
AppType::OpenCode => &mut config.prompts.opencode.prompts,
|
||||
};
|
||||
|
||||
prompts.insert(id, prompt);
|
||||
@@ -656,6 +690,7 @@ impl MultiAppConfig {
|
||||
AppType::Claude => &self.mcp.claude.servers,
|
||||
AppType::Codex => &self.mcp.codex.servers,
|
||||
AppType::Gemini => &self.mcp.gemini.servers,
|
||||
AppType::OpenCode => &self.mcp.opencode.servers,
|
||||
};
|
||||
|
||||
for (id, entry) in old_servers {
|
||||
|
||||
@@ -51,6 +51,15 @@ pub async fn get_config_status(app: String) -> Result<ConfigStatus, String> {
|
||||
|
||||
Ok(ConfigStatus { exists, path })
|
||||
}
|
||||
AppType::OpenCode => {
|
||||
let config_path = crate::opencode_config::get_opencode_config_path();
|
||||
let exists = config_path.exists();
|
||||
let path = crate::opencode_config::get_opencode_dir()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
Ok(ConfigStatus { exists, path })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +76,7 @@ pub async fn get_config_dir(app: String) -> Result<String, String> {
|
||||
AppType::Claude => config::get_claude_config_dir(),
|
||||
AppType::Codex => codex_config::get_codex_config_dir(),
|
||||
AppType::Gemini => crate::gemini_config::get_gemini_dir(),
|
||||
AppType::OpenCode => crate::opencode_config::get_opencode_dir(),
|
||||
};
|
||||
|
||||
Ok(dir.to_string_lossy().to_string())
|
||||
@@ -79,6 +89,7 @@ pub async fn open_config_folder(handle: AppHandle, app: String) -> Result<bool,
|
||||
AppType::Claude => config::get_claude_config_dir(),
|
||||
AppType::Codex => codex_config::get_codex_config_dir(),
|
||||
AppType::Gemini => crate::gemini_config::get_gemini_dir(),
|
||||
AppType::OpenCode => crate::opencode_config::get_opencode_dir(),
|
||||
};
|
||||
|
||||
if !config_dir.exists() {
|
||||
|
||||
@@ -13,7 +13,7 @@ impl Database {
|
||||
pub fn get_all_mcp_servers(&self) -> Result<IndexMap<String, McpServer>, AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, name, server_config, description, homepage, docs, tags, enabled_claude, enabled_codex, enabled_gemini
|
||||
"SELECT id, name, server_config, description, homepage, docs, tags, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode
|
||||
FROM mcp_servers
|
||||
ORDER BY name ASC, id ASC"
|
||||
).map_err(|e| AppError::Database(e.to_string()))?;
|
||||
@@ -30,6 +30,7 @@ impl Database {
|
||||
let enabled_claude: bool = row.get(7)?;
|
||||
let enabled_codex: bool = row.get(8)?;
|
||||
let enabled_gemini: bool = row.get(9)?;
|
||||
let enabled_opencode: bool = row.get(10)?;
|
||||
|
||||
let server = serde_json::from_str(&server_config_str).unwrap_or_default();
|
||||
let tags = serde_json::from_str(&tags_str).unwrap_or_default();
|
||||
@@ -44,6 +45,7 @@ impl Database {
|
||||
claude: enabled_claude,
|
||||
codex: enabled_codex,
|
||||
gemini: enabled_gemini,
|
||||
opencode: enabled_opencode,
|
||||
},
|
||||
description,
|
||||
homepage,
|
||||
@@ -68,8 +70,8 @@ impl Database {
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO mcp_servers (
|
||||
id, name, server_config, description, homepage, docs, tags,
|
||||
enabled_claude, enabled_codex, enabled_gemini
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
|
||||
enabled_claude, enabled_codex, enabled_gemini, enabled_opencode
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)",
|
||||
params![
|
||||
server.id,
|
||||
server.name,
|
||||
@@ -84,6 +86,7 @@ impl Database {
|
||||
server.apps.claude,
|
||||
server.apps.codex,
|
||||
server.apps.gemini,
|
||||
server.apps.opencode,
|
||||
],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! 提供 Skills 和 Skill Repos 的 CRUD 操作。
|
||||
//!
|
||||
//! v3.10.0+ 统一管理架构:
|
||||
//! - Skills 使用统一的 id 主键,支持三应用启用标志
|
||||
//! - Skills 使用统一的 id 主键,支持四应用启用标志
|
||||
//! - 实际文件存储在 ~/.cc-switch/skills/,同步到各应用目录
|
||||
|
||||
use crate::app_config::{InstalledSkill, SkillApps};
|
||||
@@ -22,7 +22,7 @@ impl Database {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT id, name, description, directory, repo_owner, repo_name, repo_branch,
|
||||
readme_url, enabled_claude, enabled_codex, enabled_gemini, installed_at
|
||||
readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, installed_at
|
||||
FROM skills ORDER BY name ASC",
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
@@ -42,8 +42,9 @@ impl Database {
|
||||
claude: row.get(8)?,
|
||||
codex: row.get(9)?,
|
||||
gemini: row.get(10)?,
|
||||
opencode: row.get(11)?,
|
||||
},
|
||||
installed_at: row.get(11)?,
|
||||
installed_at: row.get(12)?,
|
||||
})
|
||||
})
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
@@ -62,7 +63,7 @@ impl Database {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT id, name, description, directory, repo_owner, repo_name, repo_branch,
|
||||
readme_url, enabled_claude, enabled_codex, enabled_gemini, installed_at
|
||||
readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, installed_at
|
||||
FROM skills WHERE id = ?1",
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
@@ -81,8 +82,9 @@ impl Database {
|
||||
claude: row.get(8)?,
|
||||
codex: row.get(9)?,
|
||||
gemini: row.get(10)?,
|
||||
opencode: row.get(11)?,
|
||||
},
|
||||
installed_at: row.get(11)?,
|
||||
installed_at: row.get(12)?,
|
||||
})
|
||||
});
|
||||
|
||||
@@ -99,8 +101,8 @@ impl Database {
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO skills
|
||||
(id, name, description, directory, repo_owner, repo_name, repo_branch,
|
||||
readme_url, enabled_claude, enabled_codex, enabled_gemini, installed_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
|
||||
readme_url, enabled_claude, enabled_codex, enabled_gemini, enabled_opencode, installed_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
|
||||
params![
|
||||
skill.id,
|
||||
skill.name,
|
||||
@@ -113,6 +115,7 @@ impl Database {
|
||||
skill.apps.claude,
|
||||
skill.apps.codex,
|
||||
skill.apps.gemini,
|
||||
skill.apps.opencode,
|
||||
skill.installed_at,
|
||||
],
|
||||
)
|
||||
@@ -142,8 +145,8 @@ impl Database {
|
||||
let conn = lock_conn!(self.conn);
|
||||
let affected = conn
|
||||
.execute(
|
||||
"UPDATE skills SET enabled_claude = ?1, enabled_codex = ?2, enabled_gemini = ?3 WHERE id = ?4",
|
||||
params![apps.claude, apps.codex, apps.gemini, id],
|
||||
"UPDATE skills SET enabled_claude = ?1, enabled_codex = ?2, enabled_gemini = ?3, enabled_opencode = ?4 WHERE id = ?5",
|
||||
params![apps.claude, apps.codex, apps.gemini, apps.opencode, id],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
Ok(affected > 0)
|
||||
|
||||
@@ -47,7 +47,7 @@ const DB_BACKUP_RETAIN: usize = 10;
|
||||
|
||||
/// 当前 Schema 版本号
|
||||
/// 每次修改表结构时递增,并在 schema.rs 中添加相应的迁移逻辑
|
||||
pub(crate) const SCHEMA_VERSION: i32 = 3;
|
||||
pub(crate) const SCHEMA_VERSION: i32 = 4;
|
||||
|
||||
/// 安全地序列化 JSON,避免 unwrap panic
|
||||
pub(crate) fn to_json_string<T: Serialize>(value: &T) -> Result<String, AppError> {
|
||||
|
||||
@@ -58,7 +58,7 @@ impl Database {
|
||||
id TEXT PRIMARY KEY, name TEXT NOT NULL, server_config TEXT NOT NULL,
|
||||
description TEXT, homepage TEXT, docs TEXT, tags TEXT NOT NULL DEFAULT '[]',
|
||||
enabled_claude BOOLEAN NOT NULL DEFAULT 0, enabled_codex BOOLEAN NOT NULL DEFAULT 0,
|
||||
enabled_gemini BOOLEAN NOT NULL DEFAULT 0
|
||||
enabled_gemini BOOLEAN NOT NULL DEFAULT 0, enabled_opencode BOOLEAN NOT NULL DEFAULT 0
|
||||
)",
|
||||
[],
|
||||
)
|
||||
@@ -85,6 +85,7 @@ impl Database {
|
||||
enabled_claude BOOLEAN NOT NULL DEFAULT 0,
|
||||
enabled_codex BOOLEAN NOT NULL DEFAULT 0,
|
||||
enabled_gemini BOOLEAN NOT NULL DEFAULT 0,
|
||||
enabled_opencode BOOLEAN NOT NULL DEFAULT 0,
|
||||
installed_at INTEGER NOT NULL DEFAULT 0
|
||||
)",
|
||||
[],
|
||||
@@ -346,6 +347,11 @@ impl Database {
|
||||
Self::migrate_v2_to_v3(conn)?;
|
||||
Self::set_user_version(conn, 3)?;
|
||||
}
|
||||
3 => {
|
||||
log::info!("迁移数据库从 v3 到 v4(OpenCode 支持)");
|
||||
Self::migrate_v3_to_v4(conn)?;
|
||||
Self::set_user_version(conn, 4)?;
|
||||
}
|
||||
_ => {
|
||||
return Err(AppError::Database(format!(
|
||||
"未知的数据库版本 {version},无法迁移到 {SCHEMA_VERSION}"
|
||||
@@ -849,6 +855,30 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// v3 -> v4 迁移:添加 OpenCode 支持
|
||||
///
|
||||
/// 为 mcp_servers 和 skills 表添加 enabled_opencode 列。
|
||||
fn migrate_v3_to_v4(conn: &Connection) -> Result<(), AppError> {
|
||||
// 为 mcp_servers 表添加 enabled_opencode 列
|
||||
Self::add_column_if_missing(
|
||||
conn,
|
||||
"mcp_servers",
|
||||
"enabled_opencode",
|
||||
"BOOLEAN NOT NULL DEFAULT 0",
|
||||
)?;
|
||||
|
||||
// 为 skills 表添加 enabled_opencode 列
|
||||
Self::add_column_if_missing(
|
||||
conn,
|
||||
"skills",
|
||||
"enabled_opencode",
|
||||
"BOOLEAN NOT NULL DEFAULT 0",
|
||||
)?;
|
||||
|
||||
log::info!("v3 -> v4 迁移完成:已添加 OpenCode 支持");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 插入默认模型定价数据
|
||||
/// 格式: (model_id, display_name, input, output, cache_read, cache_creation)
|
||||
/// 注意: model_id 使用短横线格式(如 claude-haiku-4-5),与 API 返回的模型名称标准化后一致
|
||||
|
||||
@@ -166,6 +166,7 @@ pub(crate) fn parse_mcp_apps(apps_str: &str) -> Result<McpApps, AppError> {
|
||||
claude: false,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
opencode: false,
|
||||
};
|
||||
|
||||
for app in apps_str.split(',') {
|
||||
@@ -173,6 +174,7 @@ pub(crate) fn parse_mcp_apps(apps_str: &str) -> Result<McpApps, AppError> {
|
||||
"claude" => apps.claude = true,
|
||||
"codex" => apps.codex = true,
|
||||
"gemini" => apps.gemini = true,
|
||||
"opencode" => apps.opencode = true,
|
||||
other => {
|
||||
return Err(AppError::InvalidInput(format!(
|
||||
"Invalid app in 'apps': {other}"
|
||||
|
||||
@@ -145,6 +145,7 @@ pub(crate) fn build_provider_from_request(
|
||||
AppType::Claude => build_claude_settings(request),
|
||||
AppType::Codex => build_codex_settings(request),
|
||||
AppType::Gemini => build_gemini_settings(request),
|
||||
AppType::OpenCode => build_opencode_settings(request),
|
||||
};
|
||||
|
||||
// Build usage script configuration if provided
|
||||
@@ -363,6 +364,33 @@ fn build_gemini_settings(request: &DeepLinkImportRequest) -> serde_json::Value {
|
||||
json!({ "env": env })
|
||||
}
|
||||
|
||||
/// Build OpenCode settings configuration
|
||||
fn build_opencode_settings(request: &DeepLinkImportRequest) -> serde_json::Value {
|
||||
let endpoint = get_primary_endpoint(request);
|
||||
|
||||
// Build options object
|
||||
let mut options = serde_json::Map::new();
|
||||
if !endpoint.is_empty() {
|
||||
options.insert("baseURL".to_string(), json!(endpoint));
|
||||
}
|
||||
if let Some(api_key) = &request.api_key {
|
||||
options.insert("apiKey".to_string(), json!(api_key));
|
||||
}
|
||||
|
||||
// Build models object
|
||||
let mut models = serde_json::Map::new();
|
||||
if let Some(model) = &request.model {
|
||||
models.insert(model.clone(), json!({ "name": model }));
|
||||
}
|
||||
|
||||
// Default to openai-compatible npm package
|
||||
json!({
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"options": options,
|
||||
"models": models
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Config Merge Logic
|
||||
// =============================================================================
|
||||
|
||||
@@ -13,6 +13,7 @@ mod gemini_config;
|
||||
mod gemini_mcp;
|
||||
mod init_status;
|
||||
mod mcp;
|
||||
mod opencode_config;
|
||||
mod panic_hook;
|
||||
mod prompt;
|
||||
mod prompt_files;
|
||||
|
||||
@@ -91,6 +91,7 @@ pub fn import_from_claude(config: &mut MultiAppConfig) -> Result<usize, AppError
|
||||
claude: true,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
opencode: false,
|
||||
},
|
||||
description: None,
|
||||
homepage: None,
|
||||
|
||||
@@ -235,6 +235,7 @@ pub fn import_from_codex(config: &mut MultiAppConfig) -> Result<usize, AppError>
|
||||
claude: false,
|
||||
codex: true,
|
||||
gemini: false,
|
||||
opencode: false,
|
||||
},
|
||||
description: None,
|
||||
homepage: None,
|
||||
|
||||
@@ -87,6 +87,7 @@ pub fn import_from_gemini(config: &mut MultiAppConfig) -> Result<usize, AppError
|
||||
claude: false,
|
||||
codex: false,
|
||||
gemini: true,
|
||||
opencode: false,
|
||||
},
|
||||
description: None,
|
||||
homepage: None,
|
||||
|
||||
154
src-tauri/src/opencode_config.rs
Normal file
154
src-tauri/src/opencode_config.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
//! OpenCode 配置文件读写模块
|
||||
//!
|
||||
//! 处理 `~/.config/opencode/opencode.json` 配置文件的读写操作。
|
||||
//! OpenCode 使用累加式供应商管理,所有供应商配置共存于同一配置文件中。
|
||||
|
||||
use crate::error::AppError;
|
||||
use crate::settings::get_opencode_override_dir;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// 获取 OpenCode 配置目录
|
||||
///
|
||||
/// 默认路径: `~/.config/opencode/`
|
||||
/// 可通过 settings.opencode_config_dir 覆盖
|
||||
pub fn get_opencode_dir() -> PathBuf {
|
||||
if let Some(override_dir) = get_opencode_override_dir() {
|
||||
return override_dir;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Windows: %APPDATA%\opencode
|
||||
dirs::data_dir()
|
||||
.map(|d| d.join("opencode"))
|
||||
.unwrap_or_else(|| PathBuf::from(".config").join("opencode"))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
// Unix: ~/.config/opencode
|
||||
dirs::home_dir()
|
||||
.map(|h| h.join(".config").join("opencode"))
|
||||
.unwrap_or_else(|| PathBuf::from(".config").join("opencode"))
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取 OpenCode 配置文件路径
|
||||
///
|
||||
/// 返回 `~/.config/opencode/opencode.json`
|
||||
pub fn get_opencode_config_path() -> PathBuf {
|
||||
get_opencode_dir().join("opencode.json")
|
||||
}
|
||||
|
||||
/// 读取 OpenCode 配置文件
|
||||
///
|
||||
/// 返回完整的配置 JSON 对象
|
||||
pub fn read_opencode_config() -> Result<serde_json::Value, AppError> {
|
||||
let path = get_opencode_config_path();
|
||||
|
||||
if !path.exists() {
|
||||
// Return empty config with schema
|
||||
return Ok(serde_json::json!({
|
||||
"$schema": "https://opencode.ai/config.json"
|
||||
}));
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&path).map_err(|e| AppError::io(&path, e))?;
|
||||
serde_json::from_str(&content).map_err(|e| AppError::json(&path, e))
|
||||
}
|
||||
|
||||
/// 写入 OpenCode 配置文件(原子写入)
|
||||
///
|
||||
/// 使用临时文件 + 重命名确保原子性
|
||||
pub fn write_opencode_config(config: &serde_json::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))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取所有供应商配置
|
||||
pub fn get_providers() -> Result<serde_json::Map<String, serde_json::Value>, AppError> {
|
||||
let config = read_opencode_config()?;
|
||||
Ok(config
|
||||
.get("provider")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// 设置供应商配置
|
||||
pub fn set_provider(id: &str, config: serde_json::Value) -> Result<(), AppError> {
|
||||
let mut full_config = read_opencode_config()?;
|
||||
|
||||
if full_config.get("provider").is_none() {
|
||||
full_config["provider"] = serde_json::json!({});
|
||||
}
|
||||
|
||||
if let Some(providers) = full_config.get_mut("provider").and_then(|v| v.as_object_mut()) {
|
||||
providers.insert(id.to_string(), config);
|
||||
}
|
||||
|
||||
write_opencode_config(&full_config)
|
||||
}
|
||||
|
||||
/// 删除供应商配置
|
||||
pub fn remove_provider(id: &str) -> Result<(), AppError> {
|
||||
let mut config = read_opencode_config()?;
|
||||
|
||||
if let Some(providers) = config.get_mut("provider").and_then(|v| v.as_object_mut()) {
|
||||
providers.remove(id);
|
||||
}
|
||||
|
||||
write_opencode_config(&config)
|
||||
}
|
||||
|
||||
/// 获取所有 MCP 服务器配置
|
||||
pub fn get_mcp_servers() -> Result<serde_json::Map<String, serde_json::Value>, AppError> {
|
||||
let config = read_opencode_config()?;
|
||||
Ok(config
|
||||
.get("mcp")
|
||||
.and_then(|v| v.as_object())
|
||||
.cloned()
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// 设置 MCP 服务器配置
|
||||
pub fn set_mcp_server(id: &str, config: serde_json::Value) -> Result<(), AppError> {
|
||||
let mut full_config = read_opencode_config()?;
|
||||
|
||||
if full_config.get("mcp").is_none() {
|
||||
full_config["mcp"] = serde_json::json!({});
|
||||
}
|
||||
|
||||
if let Some(mcp) = full_config.get_mut("mcp").and_then(|v| v.as_object_mut()) {
|
||||
mcp.insert(id.to_string(), config);
|
||||
}
|
||||
|
||||
write_opencode_config(&full_config)
|
||||
}
|
||||
|
||||
/// 删除 MCP 服务器配置
|
||||
pub fn remove_mcp_server(id: &str) -> Result<(), AppError> {
|
||||
let mut config = read_opencode_config()?;
|
||||
|
||||
if let Some(mcp) = config.get_mut("mcp").and_then(|v| v.as_object_mut()) {
|
||||
mcp.remove(id);
|
||||
}
|
||||
|
||||
write_opencode_config(&config)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use crate::codex_config::get_codex_auth_path;
|
||||
use crate::config::get_claude_settings_path;
|
||||
use crate::error::AppError;
|
||||
use crate::gemini_config::get_gemini_dir;
|
||||
use crate::opencode_config::get_opencode_dir;
|
||||
|
||||
/// 返回指定应用所使用的提示词文件路径。
|
||||
pub fn prompt_file_path(app: &AppType) -> Result<PathBuf, AppError> {
|
||||
@@ -12,12 +13,14 @@ pub fn prompt_file_path(app: &AppType) -> Result<PathBuf, AppError> {
|
||||
AppType::Claude => get_base_dir_with_fallback(get_claude_settings_path(), ".claude")?,
|
||||
AppType::Codex => get_base_dir_with_fallback(get_codex_auth_path(), ".codex")?,
|
||||
AppType::Gemini => get_gemini_dir(),
|
||||
AppType::OpenCode => get_opencode_dir(),
|
||||
};
|
||||
|
||||
let filename = match app {
|
||||
AppType::Claude => "CLAUDE.md",
|
||||
AppType::Codex => "AGENTS.md",
|
||||
AppType::Gemini => "GEMINI.md",
|
||||
AppType::OpenCode => "OPENCODE.md",
|
||||
};
|
||||
|
||||
Ok(base_dir.join(filename))
|
||||
|
||||
@@ -132,6 +132,10 @@ impl ProviderType {
|
||||
}
|
||||
ProviderType::Gemini
|
||||
}
|
||||
AppType::OpenCode => {
|
||||
// OpenCode doesn't support proxy, but return a default type for completeness
|
||||
ProviderType::Codex // Fallback to Codex-like type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +180,10 @@ pub fn get_adapter(app_type: &AppType) -> Box<dyn ProviderAdapter> {
|
||||
AppType::Claude => Box::new(ClaudeAdapter::new()),
|
||||
AppType::Codex => Box::new(CodexAdapter::new()),
|
||||
AppType::Gemini => Box::new(GeminiAdapter::new()),
|
||||
AppType::OpenCode => {
|
||||
// OpenCode doesn't support proxy, fallback to Codex adapter
|
||||
Box::new(CodexAdapter::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,10 @@ impl ConfigService {
|
||||
AppType::Codex => Self::sync_codex_live(config, ¤t_id, &provider)?,
|
||||
AppType::Claude => Self::sync_claude_live(config, ¤t_id, &provider)?,
|
||||
AppType::Gemini => Self::sync_gemini_live(config, ¤t_id, &provider)?,
|
||||
AppType::OpenCode => {
|
||||
// OpenCode uses additive mode, no live sync needed
|
||||
// OpenCode providers are managed directly in the config file
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -113,6 +113,11 @@ impl McpService {
|
||||
AppType::Gemini => {
|
||||
mcp::sync_single_server_to_gemini(&Default::default(), &server.id, &server.server)?;
|
||||
}
|
||||
AppType::OpenCode => {
|
||||
// OpenCode MCP sync will be implemented in Phase 4
|
||||
// For now, skip silently
|
||||
log::debug!("OpenCode MCP sync not yet implemented, skipping");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -135,6 +140,10 @@ impl McpService {
|
||||
AppType::Claude => mcp::remove_server_from_claude(id)?,
|
||||
AppType::Codex => mcp::remove_server_from_codex(id)?,
|
||||
AppType::Gemini => mcp::remove_server_from_gemini(id)?,
|
||||
AppType::OpenCode => {
|
||||
// OpenCode MCP removal will be implemented in Phase 4
|
||||
log::debug!("OpenCode MCP removal not yet implemented, skipping");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -120,6 +120,11 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re
|
||||
// Delegate to write_gemini_live which handles env file writing correctly
|
||||
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)");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -220,6 +225,21 @@ pub fn read_live_settings(app_type: AppType) -> Result<Value, AppError> {
|
||||
"config": config_obj
|
||||
}))
|
||||
}
|
||||
AppType::OpenCode => {
|
||||
use crate::opencode_config::{get_opencode_config_path, read_opencode_config};
|
||||
|
||||
let config_path = get_opencode_config_path();
|
||||
if !config_path.exists() {
|
||||
return Err(AppError::localized(
|
||||
"opencode.config.missing",
|
||||
"OpenCode 配置文件不存在",
|
||||
"OpenCode configuration file not found",
|
||||
));
|
||||
}
|
||||
|
||||
let config = read_opencode_config()?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,6 +315,24 @@ pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<bool
|
||||
"config": config_obj
|
||||
})
|
||||
}
|
||||
AppType::OpenCode => {
|
||||
// OpenCode uses additive mode - import from live is not the same pattern
|
||||
// For now, return an empty config structure
|
||||
use crate::opencode_config::{get_opencode_config_path, read_opencode_config};
|
||||
|
||||
let config_path = get_opencode_config_path();
|
||||
if !config_path.exists() {
|
||||
return Err(AppError::localized(
|
||||
"opencode.live.missing",
|
||||
"OpenCode 配置文件不存在",
|
||||
"OpenCode configuration file is missing",
|
||||
));
|
||||
}
|
||||
|
||||
// For OpenCode, we return the full config - but note that OpenCode
|
||||
// uses additive mode, so importing defaults works differently
|
||||
read_opencode_config()?
|
||||
}
|
||||
};
|
||||
|
||||
let mut provider = Provider::with_id(
|
||||
|
||||
@@ -380,6 +380,7 @@ impl ProviderService {
|
||||
AppType::Claude => Self::extract_claude_common_config(&provider.settings_config),
|
||||
AppType::Codex => Self::extract_codex_common_config(&provider.settings_config),
|
||||
AppType::Gemini => Self::extract_gemini_common_config(&provider.settings_config),
|
||||
AppType::OpenCode => Self::extract_opencode_common_config(&provider.settings_config),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,6 +393,7 @@ impl ProviderService {
|
||||
AppType::Claude => Self::extract_claude_common_config(settings_config),
|
||||
AppType::Codex => Self::extract_codex_common_config(settings_config),
|
||||
AppType::Gemini => Self::extract_gemini_common_config(settings_config),
|
||||
AppType::OpenCode => Self::extract_opencode_common_config(settings_config),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,6 +527,29 @@ impl ProviderService {
|
||||
.map_err(|e| AppError::Message(format!("Serialization failed: {e}")))
|
||||
}
|
||||
|
||||
/// Extract common config for OpenCode (JSON format)
|
||||
fn extract_opencode_common_config(settings: &Value) -> Result<String, AppError> {
|
||||
// OpenCode uses a different config structure with npm, options, models
|
||||
// For common config, we exclude provider-specific fields like apiKey
|
||||
let mut config = settings.clone();
|
||||
|
||||
// Remove provider-specific fields
|
||||
if let Some(obj) = config.as_object_mut() {
|
||||
if let Some(options) = obj.get_mut("options").and_then(|v| v.as_object_mut()) {
|
||||
options.remove("apiKey");
|
||||
options.remove("baseURL");
|
||||
}
|
||||
// Keep npm and models as they might be common
|
||||
}
|
||||
|
||||
if config.is_null() || (config.is_object() && config.as_object().unwrap().is_empty()) {
|
||||
return Ok("{}".to_string());
|
||||
}
|
||||
|
||||
serde_json::to_string_pretty(&config)
|
||||
.map_err(|e| AppError::Message(format!("Serialization failed: {e}")))
|
||||
}
|
||||
|
||||
/// Import default configuration from live files (re-export)
|
||||
///
|
||||
/// Returns `Ok(true)` if imported, `Ok(false)` if skipped.
|
||||
@@ -691,6 +716,17 @@ impl ProviderService {
|
||||
use crate::gemini_config::validate_gemini_settings;
|
||||
validate_gemini_settings(&provider.settings_config)?
|
||||
}
|
||||
AppType::OpenCode => {
|
||||
// OpenCode uses a different config structure: { npm, options, models }
|
||||
// Basic validation - must be an object
|
||||
if !provider.settings_config.is_object() {
|
||||
return Err(AppError::localized(
|
||||
"provider.opencode.settings.not_object",
|
||||
"OpenCode 配置必须是 JSON 对象",
|
||||
"OpenCode configuration must be a JSON object",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate and clean UsageScript configuration (common for all app types)
|
||||
@@ -828,6 +864,40 @@ impl ProviderService {
|
||||
|
||||
Ok((api_key, base_url))
|
||||
}
|
||||
AppType::OpenCode => {
|
||||
// OpenCode uses options.apiKey and options.baseURL
|
||||
let options = provider
|
||||
.settings_config
|
||||
.get("options")
|
||||
.and_then(|v| v.as_object())
|
||||
.ok_or_else(|| {
|
||||
AppError::localized(
|
||||
"provider.opencode.options.missing",
|
||||
"配置格式错误: 缺少 options",
|
||||
"Invalid configuration: missing options section",
|
||||
)
|
||||
})?;
|
||||
|
||||
let api_key = options
|
||||
.get("apiKey")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| {
|
||||
AppError::localized(
|
||||
"provider.opencode.api_key.missing",
|
||||
"缺少 API Key",
|
||||
"API key is missing",
|
||||
)
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
let base_url = options
|
||||
.get("baseURL")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
Ok((api_key, base_url))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,6 +368,10 @@ impl ProxyService {
|
||||
AppType::Claude => self.read_claude_live()?,
|
||||
AppType::Codex => self.read_codex_live()?,
|
||||
AppType::Gemini => self.read_gemini_live()?,
|
||||
AppType::OpenCode => {
|
||||
// OpenCode doesn't support proxy features
|
||||
return Err("OpenCode 不支持代理功能".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
self.sync_live_config_to_provider(app_type, &live_config)
|
||||
@@ -581,6 +585,9 @@ impl ProxyService {
|
||||
}
|
||||
}
|
||||
}
|
||||
AppType::OpenCode => {
|
||||
// OpenCode doesn't support proxy features, skip silently
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -759,6 +766,10 @@ impl ProxyService {
|
||||
AppType::Claude => ("claude", self.read_claude_live()?),
|
||||
AppType::Codex => ("codex", self.read_codex_live()?),
|
||||
AppType::Gemini => ("gemini", self.read_gemini_live()?),
|
||||
AppType::OpenCode => {
|
||||
// OpenCode doesn't support proxy features
|
||||
return Err("OpenCode 不支持代理功能".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
let json_str = serde_json::to_string(&config)
|
||||
@@ -967,6 +978,10 @@ impl ProxyService {
|
||||
self.write_gemini_live(&live_config)?;
|
||||
log::info!("Gemini Live 配置已接管,代理地址: {proxy_url}");
|
||||
}
|
||||
AppType::OpenCode => {
|
||||
// OpenCode doesn't support proxy features
|
||||
return Err("OpenCode 不支持代理功能".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1050,6 +1065,9 @@ impl ProxyService {
|
||||
let _ = self.write_gemini_live(&live_config);
|
||||
}
|
||||
}
|
||||
AppType::OpenCode => {
|
||||
// OpenCode doesn't support proxy features, skip silently
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1082,6 +1100,9 @@ impl ProxyService {
|
||||
log::info!("Gemini Live 配置已恢复");
|
||||
}
|
||||
}
|
||||
AppType::OpenCode => {
|
||||
// OpenCode doesn't support proxy features, skip silently
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1161,6 +1182,10 @@ impl ProxyService {
|
||||
AppType::Claude => self.write_claude_live(config),
|
||||
AppType::Codex => self.write_codex_live(config),
|
||||
AppType::Gemini => self.write_gemini_live(config),
|
||||
AppType::OpenCode => {
|
||||
// OpenCode doesn't support proxy features
|
||||
Err("OpenCode 不支持代理功能".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1178,6 +1203,10 @@ impl ProxyService {
|
||||
Ok(config) => Self::is_gemini_live_taken_over(&config),
|
||||
Err(_) => false,
|
||||
},
|
||||
AppType::OpenCode => {
|
||||
// OpenCode doesn't support proxy takeover
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1217,6 +1246,10 @@ impl ProxyService {
|
||||
AppType::Claude => self.cleanup_claude_takeover_placeholders_in_live(),
|
||||
AppType::Codex => self.cleanup_codex_takeover_placeholders_in_live(),
|
||||
AppType::Gemini => self.cleanup_gemini_takeover_placeholders_in_live(),
|
||||
AppType::OpenCode => {
|
||||
// OpenCode doesn't support proxy features
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -183,6 +183,11 @@ impl SkillService {
|
||||
return Ok(custom.join("skills"));
|
||||
}
|
||||
}
|
||||
AppType::OpenCode => {
|
||||
if let Some(custom) = crate::settings::get_opencode_override_dir() {
|
||||
return Ok(custom.join("skills"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 默认路径:回退到用户主目录下的标准位置
|
||||
@@ -196,6 +201,7 @@ impl SkillService {
|
||||
AppType::Claude => home.join(".claude").join("skills"),
|
||||
AppType::Codex => home.join(".codex").join("skills"),
|
||||
AppType::Gemini => home.join(".gemini").join("skills"),
|
||||
AppType::OpenCode => home.join(".config").join("opencode").join("skills"),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -425,6 +431,7 @@ impl SkillService {
|
||||
AppType::Claude => "claude",
|
||||
AppType::Codex => "codex",
|
||||
AppType::Gemini => "gemini",
|
||||
AppType::OpenCode => "opencode",
|
||||
};
|
||||
|
||||
unmanaged
|
||||
@@ -468,6 +475,7 @@ impl SkillService {
|
||||
AppType::Claude => "claude",
|
||||
AppType::Codex => "codex",
|
||||
AppType::Gemini => "gemini",
|
||||
AppType::OpenCode => "opencode",
|
||||
};
|
||||
found_in.push(app_str.to_string());
|
||||
}
|
||||
|
||||
@@ -185,6 +185,14 @@ impl StreamCheckService {
|
||||
)
|
||||
.await
|
||||
}
|
||||
AppType::OpenCode => {
|
||||
// OpenCode doesn't support stream check yet
|
||||
return Err(AppError::localized(
|
||||
"opencode_no_stream_check",
|
||||
"OpenCode 暂不支持健康检查",
|
||||
"OpenCode does not support health check yet"
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let response_time = start.elapsed().as_millis() as u64;
|
||||
@@ -477,9 +485,24 @@ impl StreamCheckService {
|
||||
}
|
||||
AppType::Gemini => Self::extract_env_model(provider, "GEMINI_MODEL")
|
||||
.unwrap_or_else(|| config.gemini_model.clone()),
|
||||
AppType::OpenCode => {
|
||||
// OpenCode uses models map in settings_config
|
||||
// Try to extract first model from the models object
|
||||
Self::extract_opencode_model(provider).unwrap_or_else(|| "gpt-4o".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_opencode_model(provider: &Provider) -> Option<String> {
|
||||
let models = provider
|
||||
.settings_config
|
||||
.get("models")
|
||||
.and_then(|m| m.as_object())?;
|
||||
|
||||
// Return the first model ID from the models map
|
||||
models.keys().next().map(|s| s.to_string())
|
||||
}
|
||||
|
||||
fn extract_env_model(provider: &Provider, key: &str) -> Option<String> {
|
||||
provider
|
||||
.settings_config
|
||||
|
||||
@@ -47,6 +47,8 @@ pub struct AppSettings {
|
||||
pub codex_config_dir: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub gemini_config_dir: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub opencode_config_dir: Option<String>,
|
||||
|
||||
// ===== 当前供应商 ID(设备级)=====
|
||||
/// 当前 Claude 供应商 ID(本地存储,优先于数据库 is_current)
|
||||
@@ -58,6 +60,9 @@ pub struct AppSettings {
|
||||
/// 当前 Gemini 供应商 ID(本地存储,优先于数据库 is_current)
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub current_provider_gemini: Option<String>,
|
||||
/// 当前 OpenCode 供应商 ID(本地存储,对 OpenCode 可能无意义,但保持结构一致)
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub current_provider_opencode: Option<String>,
|
||||
}
|
||||
|
||||
fn default_show_in_tray() -> bool {
|
||||
@@ -84,9 +89,11 @@ impl Default for AppSettings {
|
||||
claude_config_dir: None,
|
||||
codex_config_dir: None,
|
||||
gemini_config_dir: None,
|
||||
opencode_config_dir: None,
|
||||
current_provider_claude: None,
|
||||
current_provider_codex: None,
|
||||
current_provider_gemini: None,
|
||||
current_provider_opencode: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,6 +126,13 @@ impl AppSettings {
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
self.opencode_config_dir = self
|
||||
.opencode_config_dir
|
||||
.as_ref()
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
self.language = self
|
||||
.language
|
||||
.as_ref()
|
||||
@@ -251,6 +265,14 @@ pub fn get_gemini_override_dir() -> Option<PathBuf> {
|
||||
.map(|p| resolve_override_path(p))
|
||||
}
|
||||
|
||||
pub fn get_opencode_override_dir() -> Option<PathBuf> {
|
||||
let settings = settings_store().read().ok()?;
|
||||
settings
|
||||
.opencode_config_dir
|
||||
.as_ref()
|
||||
.map(|p| resolve_override_path(p))
|
||||
}
|
||||
|
||||
// ===== 当前供应商管理函数 =====
|
||||
|
||||
/// 获取指定应用类型的当前供应商 ID(从本地 settings 读取)
|
||||
@@ -263,6 +285,7 @@ pub fn get_current_provider(app_type: &AppType) -> Option<String> {
|
||||
AppType::Claude => settings.current_provider_claude.clone(),
|
||||
AppType::Codex => settings.current_provider_codex.clone(),
|
||||
AppType::Gemini => settings.current_provider_gemini.clone(),
|
||||
AppType::OpenCode => settings.current_provider_opencode.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,6 +300,7 @@ pub fn set_current_provider(app_type: &AppType, id: Option<&str>) -> Result<(),
|
||||
AppType::Claude => settings.current_provider_claude = id.map(|s| s.to_string()),
|
||||
AppType::Codex => settings.current_provider_codex = id.map(|s| s.to_string()),
|
||||
AppType::Gemini => settings.current_provider_gemini = id.map(|s| s.to_string()),
|
||||
AppType::OpenCode => settings.current_provider_opencode = id.map(|s| s.to_string()),
|
||||
}
|
||||
|
||||
update_settings(settings)
|
||||
|
||||
Reference in New Issue
Block a user