mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-30 10:25:05 +08:00
feat(grokbuild): add first-class Grok Build support (#5453)
* feat(grokbuild): add backend integration * feat(grokbuild): add provider and management UI * test(grokbuild): cover configuration and integrations * fix(grokbuild): address backend review feedback * fix(grokbuild): complete UI review feedback * feat(grokbuild): add CLI lifecycle management * fix(grokbuild): align provider icon fallback * test(grokbuild): cover provider icon persistence
This commit is contained in:
@@ -65,7 +65,8 @@ 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_opencode BOOLEAN NOT NULL DEFAULT 0,
|
||||
enabled_gemini BOOLEAN NOT NULL DEFAULT 0, enabled_grokbuild BOOLEAN NOT NULL DEFAULT 0,
|
||||
enabled_opencode BOOLEAN NOT NULL DEFAULT 0,
|
||||
enabled_hermes BOOLEAN NOT NULL DEFAULT 0
|
||||
)",
|
||||
[],
|
||||
@@ -93,6 +94,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_grokbuild BOOLEAN NOT NULL DEFAULT 0,
|
||||
enabled_opencode BOOLEAN NOT NULL DEFAULT 0,
|
||||
enabled_hermes BOOLEAN NOT NULL DEFAULT 0,
|
||||
installed_at INTEGER NOT NULL DEFAULT 0,
|
||||
@@ -122,7 +124,7 @@ impl Database {
|
||||
|
||||
// 8. Proxy Config 表(三行结构,app_type 主键)
|
||||
conn.execute("CREATE TABLE IF NOT EXISTS proxy_config (
|
||||
app_type TEXT PRIMARY KEY CHECK (app_type IN ('claude','codex','gemini')),
|
||||
app_type TEXT PRIMARY KEY CHECK (app_type IN ('claude','codex','gemini','grokbuild')),
|
||||
proxy_enabled INTEGER NOT NULL DEFAULT 0, listen_address TEXT NOT NULL DEFAULT '127.0.0.1',
|
||||
listen_port INTEGER NOT NULL DEFAULT 15721, enable_logging INTEGER NOT NULL DEFAULT 1,
|
||||
enabled INTEGER NOT NULL DEFAULT 0, auto_failover_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
@@ -169,6 +171,15 @@ impl Database {
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO proxy_config (app_type, max_retries,
|
||||
streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout,
|
||||
circuit_failure_threshold, circuit_success_threshold, circuit_timeout_seconds,
|
||||
circuit_error_rate_threshold, circuit_min_requests)
|
||||
VALUES ('grokbuild', 3, 60, 120, 600, 4, 2, 60, 0.6, 10)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
}
|
||||
|
||||
// 9. Provider Health 表
|
||||
@@ -485,6 +496,16 @@ impl Database {
|
||||
Self::migrate_v12_to_v13(conn)?;
|
||||
Self::set_user_version(conn, 13)?;
|
||||
}
|
||||
13 => {
|
||||
log::info!("迁移数据库从 v13 到 v14(添加 Grok Build 代理配置)");
|
||||
Self::migrate_v13_to_v14(conn)?;
|
||||
Self::set_user_version(conn, 14)?;
|
||||
}
|
||||
14 => {
|
||||
log::info!("迁移数据库从 v14 到 v15(Skills/MCP 添加 Grok Build 支持)");
|
||||
Self::migrate_v14_to_v15(conn)?;
|
||||
Self::set_user_version(conn, 15)?;
|
||||
}
|
||||
_ => {
|
||||
return Err(AppError::Database(format!(
|
||||
"未知的数据库版本 {version},无法迁移到 {SCHEMA_VERSION}"
|
||||
@@ -798,12 +819,25 @@ impl Database {
|
||||
old_cb.3,
|
||||
old_cb.4,
|
||||
),
|
||||
(
|
||||
"grokbuild",
|
||||
false,
|
||||
false,
|
||||
3,
|
||||
old_config.4,
|
||||
old_config.5,
|
||||
old_cb.0,
|
||||
old_cb.1,
|
||||
old_cb.2,
|
||||
old_cb.3,
|
||||
old_cb.4,
|
||||
),
|
||||
];
|
||||
|
||||
// 创建新表
|
||||
conn.execute("DROP TABLE IF EXISTS proxy_config_new", [])?;
|
||||
conn.execute("CREATE TABLE proxy_config_new (
|
||||
app_type TEXT PRIMARY KEY CHECK (app_type IN ('claude','codex','gemini')),
|
||||
app_type TEXT PRIMARY KEY CHECK (app_type IN ('claude','codex','gemini','grokbuild')),
|
||||
proxy_enabled INTEGER NOT NULL DEFAULT 0, listen_address TEXT NOT NULL DEFAULT '127.0.0.1',
|
||||
listen_port INTEGER NOT NULL DEFAULT 15721, enable_logging INTEGER NOT NULL DEFAULT 1,
|
||||
enabled INTEGER NOT NULL DEFAULT 0, auto_failover_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
@@ -814,6 +848,7 @@ impl Database {
|
||||
circuit_min_requests INTEGER NOT NULL DEFAULT 10,
|
||||
default_cost_multiplier TEXT NOT NULL DEFAULT '1',
|
||||
pricing_model_source TEXT NOT NULL DEFAULT 'response',
|
||||
live_takeover_active INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)", [])?;
|
||||
|
||||
@@ -1354,6 +1389,127 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// v13 -> v14: allow Grok Build to own an independent proxy configuration row.
|
||||
fn migrate_v13_to_v14(conn: &Connection) -> Result<(), AppError> {
|
||||
if !Self::table_exists(conn, "proxy_config")? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
conn.execute("DROP TABLE IF EXISTS proxy_config_v14", [])
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
conn.execute(
|
||||
"CREATE TABLE proxy_config_v14 (
|
||||
app_type TEXT PRIMARY KEY CHECK (app_type IN ('claude','codex','gemini','grokbuild')),
|
||||
proxy_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
listen_address TEXT NOT NULL DEFAULT '127.0.0.1',
|
||||
listen_port INTEGER NOT NULL DEFAULT 15721,
|
||||
enable_logging INTEGER NOT NULL DEFAULT 1,
|
||||
enabled INTEGER NOT NULL DEFAULT 0,
|
||||
auto_failover_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
max_retries INTEGER NOT NULL DEFAULT 3,
|
||||
streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 60,
|
||||
streaming_idle_timeout INTEGER NOT NULL DEFAULT 120,
|
||||
non_streaming_timeout INTEGER NOT NULL DEFAULT 600,
|
||||
circuit_failure_threshold INTEGER NOT NULL DEFAULT 4,
|
||||
circuit_success_threshold INTEGER NOT NULL DEFAULT 2,
|
||||
circuit_timeout_seconds INTEGER NOT NULL DEFAULT 60,
|
||||
circuit_error_rate_threshold REAL NOT NULL DEFAULT 0.6,
|
||||
circuit_min_requests INTEGER NOT NULL DEFAULT 10,
|
||||
default_cost_multiplier TEXT NOT NULL DEFAULT '1',
|
||||
pricing_model_source TEXT NOT NULL DEFAULT 'response',
|
||||
live_takeover_active INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
let copied_columns = [
|
||||
("app_type", "'claude'"),
|
||||
("proxy_enabled", "0"),
|
||||
("listen_address", "'127.0.0.1'"),
|
||||
("listen_port", "15721"),
|
||||
("enable_logging", "1"),
|
||||
("enabled", "0"),
|
||||
("auto_failover_enabled", "0"),
|
||||
("max_retries", "3"),
|
||||
("streaming_first_byte_timeout", "60"),
|
||||
("streaming_idle_timeout", "120"),
|
||||
("non_streaming_timeout", "600"),
|
||||
("circuit_failure_threshold", "4"),
|
||||
("circuit_success_threshold", "2"),
|
||||
("circuit_timeout_seconds", "60"),
|
||||
("circuit_error_rate_threshold", "0.6"),
|
||||
("circuit_min_requests", "10"),
|
||||
("default_cost_multiplier", "'1'"),
|
||||
("pricing_model_source", "'response'"),
|
||||
("live_takeover_active", "0"),
|
||||
("created_at", "datetime('now')"),
|
||||
("updated_at", "datetime('now')"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(column, fallback)| {
|
||||
Self::has_column(conn, "proxy_config", column).map(|exists| {
|
||||
if exists {
|
||||
format!("\"{column}\"")
|
||||
} else {
|
||||
fallback.into()
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, AppError>>()?
|
||||
.join(", ");
|
||||
|
||||
let copy_sql = format!(
|
||||
"INSERT INTO proxy_config_v14 (
|
||||
app_type, proxy_enabled, listen_address, listen_port, enable_logging,
|
||||
enabled, auto_failover_enabled, max_retries,
|
||||
streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout,
|
||||
circuit_failure_threshold, circuit_success_threshold, circuit_timeout_seconds,
|
||||
circuit_error_rate_threshold, circuit_min_requests,
|
||||
default_cost_multiplier, pricing_model_source, live_takeover_active,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT {copied_columns} FROM proxy_config"
|
||||
);
|
||||
conn.execute(©_sql, [])
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
conn.execute("DROP TABLE proxy_config", [])
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
conn.execute("ALTER TABLE proxy_config_v14 RENAME TO proxy_config", [])
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO proxy_config (app_type) VALUES ('grokbuild')",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// v14 -> v15: persist Grok Build enablement for unified Skills and MCP.
|
||||
fn migrate_v14_to_v15(conn: &Connection) -> Result<(), AppError> {
|
||||
if Self::table_exists(conn, "mcp_servers")? {
|
||||
Self::add_column_if_missing(
|
||||
conn,
|
||||
"mcp_servers",
|
||||
"enabled_grokbuild",
|
||||
"BOOLEAN NOT NULL DEFAULT 0",
|
||||
)?;
|
||||
}
|
||||
if Self::table_exists(conn, "skills")? {
|
||||
Self::add_column_if_missing(
|
||||
conn,
|
||||
"skills",
|
||||
"enabled_grokbuild",
|
||||
"BOOLEAN NOT NULL DEFAULT 0",
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 插入默认模型定价数据
|
||||
/// 格式: (model_id, display_name, input, output, cache_read, cache_creation)
|
||||
/// 注意: model_id 使用短横线格式(如 claude-haiku-4-5),与 API 返回的模型名称标准化后一致
|
||||
@@ -2766,7 +2922,7 @@ mod tests {
|
||||
|
||||
Database::apply_schema_migrations_on_conn(&conn)?;
|
||||
|
||||
assert_eq!(Database::get_user_version(&conn)?, 13);
|
||||
assert_eq!(Database::get_user_version(&conn)?, SCHEMA_VERSION);
|
||||
assert!(Database::has_column(
|
||||
&conn,
|
||||
"proxy_request_logs",
|
||||
@@ -2787,4 +2943,82 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrate_v13_to_v14_adds_grokbuild_proxy_row_and_preserves_values() -> Result<(), AppError> {
|
||||
let conn = Connection::open_in_memory()?;
|
||||
Database::create_tables_on_conn(&conn)?;
|
||||
conn.execute("DELETE FROM proxy_config WHERE app_type = 'grokbuild'", [])?;
|
||||
conn.execute(
|
||||
"UPDATE proxy_config SET enabled = 1, max_retries = 9 WHERE app_type = 'codex'",
|
||||
[],
|
||||
)?;
|
||||
Database::set_user_version(&conn, 13)?;
|
||||
|
||||
Database::apply_schema_migrations_on_conn(&conn)?;
|
||||
|
||||
assert_eq!(Database::get_user_version(&conn)?, SCHEMA_VERSION);
|
||||
let grok_rows: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM proxy_config WHERE app_type = 'grokbuild'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
assert_eq!(grok_rows, 1);
|
||||
let codex_values: (i64, i64) = conn.query_row(
|
||||
"SELECT enabled, max_retries FROM proxy_config WHERE app_type = 'codex'",
|
||||
[],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)?;
|
||||
assert_eq!(codex_values, (1, 9));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migrate_v14_to_v15_adds_grokbuild_skill_and_mcp_flags() -> Result<(), AppError> {
|
||||
let conn = Connection::open_in_memory()?;
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE mcp_servers (
|
||||
id TEXT PRIMARY KEY,
|
||||
enabled_codex BOOLEAN NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE skills (
|
||||
id TEXT PRIMARY KEY,
|
||||
enabled_codex BOOLEAN NOT NULL DEFAULT 0
|
||||
);",
|
||||
)?;
|
||||
conn.execute(
|
||||
"INSERT INTO mcp_servers (id, enabled_codex) VALUES ('mcp-1', 1)",
|
||||
[],
|
||||
)?;
|
||||
conn.execute(
|
||||
"INSERT INTO skills (id, enabled_codex) VALUES ('skill-1', 1)",
|
||||
[],
|
||||
)?;
|
||||
Database::set_user_version(&conn, 14)?;
|
||||
|
||||
Database::apply_schema_migrations_on_conn(&conn)?;
|
||||
|
||||
assert_eq!(Database::get_user_version(&conn)?, SCHEMA_VERSION);
|
||||
assert!(Database::has_column(
|
||||
&conn,
|
||||
"mcp_servers",
|
||||
"enabled_grokbuild"
|
||||
)?);
|
||||
assert!(Database::has_column(&conn, "skills", "enabled_grokbuild")?);
|
||||
let mcp_values: (i64, i64) = conn.query_row(
|
||||
"SELECT enabled_codex, enabled_grokbuild FROM mcp_servers WHERE id = 'mcp-1'",
|
||||
[],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)?;
|
||||
let skill_values: (i64, i64) = conn.query_row(
|
||||
"SELECT enabled_codex, enabled_grokbuild FROM skills WHERE id = 'skill-1'",
|
||||
[],
|
||||
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||
)?;
|
||||
assert_eq!(mcp_values, (1, 0));
|
||||
assert_eq!(skill_values, (1, 0));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user