mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30:17 +08:00
refactor(database): migrate proxy_config to per-app three-row structure
Replace singleton proxy_config table with app_type primary key structure, allowing independent proxy settings for Claude, Codex, and Gemini. Add GlobalProxyConfig queries and per-app config management in DAO layer.
This commit is contained in:
@@ -8,69 +8,251 @@ use crate::proxy::types::*;
|
||||
use super::super::{lock_conn, Database};
|
||||
|
||||
impl Database {
|
||||
// ==================== Proxy Config ====================
|
||||
// ==================== Global Proxy Config ====================
|
||||
|
||||
/// 获取代理配置
|
||||
/// 获取全局代理配置(统一字段)
|
||||
///
|
||||
/// 从 claude 行读取(三行镜像一致)
|
||||
pub async fn get_global_proxy_config(&self) -> Result<GlobalProxyConfig, AppError> {
|
||||
// 使用 block 限制 conn 的作用域,避免跨 await 持有锁
|
||||
let result = {
|
||||
let conn = lock_conn!(self.conn);
|
||||
conn.query_row(
|
||||
"SELECT proxy_enabled, listen_address, listen_port, enable_logging
|
||||
FROM proxy_config WHERE app_type = 'claude'",
|
||||
[],
|
||||
|row| {
|
||||
Ok(GlobalProxyConfig {
|
||||
proxy_enabled: row.get::<_, i32>(0)? != 0,
|
||||
listen_address: row.get(1)?,
|
||||
listen_port: row.get::<_, i32>(2)? as u16,
|
||||
enable_logging: row.get::<_, i32>(3)? != 0,
|
||||
})
|
||||
},
|
||||
)
|
||||
};
|
||||
// conn 已在 block 结束时释放
|
||||
|
||||
match result {
|
||||
Ok(config) => Ok(config),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => {
|
||||
// 如果不存在,创建默认配置
|
||||
self.init_proxy_config_rows().await?;
|
||||
Ok(GlobalProxyConfig {
|
||||
proxy_enabled: false,
|
||||
listen_address: "127.0.0.1".to_string(),
|
||||
listen_port: 5000,
|
||||
enable_logging: true,
|
||||
})
|
||||
}
|
||||
Err(e) => Err(AppError::Database(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新全局代理配置(镜像写三行)
|
||||
pub async fn update_global_proxy_config(
|
||||
&self,
|
||||
config: GlobalProxyConfig,
|
||||
) -> Result<(), AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
|
||||
conn.execute(
|
||||
"UPDATE proxy_config SET
|
||||
proxy_enabled = ?1,
|
||||
listen_address = ?2,
|
||||
listen_port = ?3,
|
||||
enable_logging = ?4,
|
||||
updated_at = datetime('now')",
|
||||
rusqlite::params![
|
||||
if config.proxy_enabled { 1 } else { 0 },
|
||||
config.listen_address,
|
||||
config.listen_port as i32,
|
||||
if config.enable_logging { 1 } else { 0 },
|
||||
],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取应用级代理配置
|
||||
pub async fn get_proxy_config_for_app(
|
||||
&self,
|
||||
app_type: &str,
|
||||
) -> Result<AppProxyConfig, AppError> {
|
||||
// 使用 block 限制 conn 的作用域,避免跨 await 持有锁
|
||||
let app_type_owned = app_type.to_string();
|
||||
let result = {
|
||||
let conn = lock_conn!(self.conn);
|
||||
conn.query_row(
|
||||
"SELECT app_type, 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
|
||||
FROM proxy_config WHERE app_type = ?1",
|
||||
[app_type],
|
||||
|row| {
|
||||
Ok(AppProxyConfig {
|
||||
app_type: row.get(0)?,
|
||||
enabled: row.get::<_, i32>(1)? != 0,
|
||||
auto_failover_enabled: row.get::<_, i32>(2)? != 0,
|
||||
max_retries: row.get::<_, i32>(3)? as u32,
|
||||
streaming_first_byte_timeout: row.get::<_, i32>(4)? as u32,
|
||||
streaming_idle_timeout: row.get::<_, i32>(5)? as u32,
|
||||
non_streaming_timeout: row.get::<_, i32>(6)? as u32,
|
||||
circuit_failure_threshold: row.get::<_, i32>(7)? as u32,
|
||||
circuit_success_threshold: row.get::<_, i32>(8)? as u32,
|
||||
circuit_timeout_seconds: row.get::<_, i32>(9)? as u32,
|
||||
circuit_error_rate_threshold: row.get(10)?,
|
||||
circuit_min_requests: row.get::<_, i32>(11)? as u32,
|
||||
})
|
||||
},
|
||||
)
|
||||
};
|
||||
// conn 已在 block 结束时释放
|
||||
|
||||
match result {
|
||||
Ok(config) => Ok(config),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => {
|
||||
// 如果不存在,创建默认配置
|
||||
self.init_proxy_config_rows().await?;
|
||||
Ok(AppProxyConfig {
|
||||
app_type: app_type_owned,
|
||||
enabled: false,
|
||||
auto_failover_enabled: false,
|
||||
max_retries: 3,
|
||||
streaming_first_byte_timeout: 30,
|
||||
streaming_idle_timeout: 60,
|
||||
non_streaming_timeout: 300,
|
||||
circuit_failure_threshold: 5,
|
||||
circuit_success_threshold: 2,
|
||||
circuit_timeout_seconds: 60,
|
||||
circuit_error_rate_threshold: 0.5,
|
||||
circuit_min_requests: 10,
|
||||
})
|
||||
}
|
||||
Err(e) => Err(AppError::Database(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新应用级代理配置
|
||||
pub async fn update_proxy_config_for_app(
|
||||
&self,
|
||||
config: AppProxyConfig,
|
||||
) -> Result<(), AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
|
||||
conn.execute(
|
||||
"UPDATE proxy_config SET
|
||||
enabled = ?2,
|
||||
auto_failover_enabled = ?3,
|
||||
max_retries = ?4,
|
||||
streaming_first_byte_timeout = ?5,
|
||||
streaming_idle_timeout = ?6,
|
||||
non_streaming_timeout = ?7,
|
||||
circuit_failure_threshold = ?8,
|
||||
circuit_success_threshold = ?9,
|
||||
circuit_timeout_seconds = ?10,
|
||||
circuit_error_rate_threshold = ?11,
|
||||
circuit_min_requests = ?12,
|
||||
updated_at = datetime('now')
|
||||
WHERE app_type = ?1",
|
||||
rusqlite::params![
|
||||
config.app_type,
|
||||
if config.enabled { 1 } else { 0 },
|
||||
if config.auto_failover_enabled { 1 } else { 0 },
|
||||
config.max_retries as i32,
|
||||
config.streaming_first_byte_timeout as i32,
|
||||
config.streaming_idle_timeout as i32,
|
||||
config.non_streaming_timeout as i32,
|
||||
config.circuit_failure_threshold as i32,
|
||||
config.circuit_success_threshold as i32,
|
||||
config.circuit_timeout_seconds as i32,
|
||||
config.circuit_error_rate_threshold,
|
||||
config.circuit_min_requests as i32,
|
||||
],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 初始化 proxy_config 表的三行数据
|
||||
async fn init_proxy_config_rows(&self) -> Result<(), AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
|
||||
for app_type in &["claude", "codex", "gemini"] {
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO proxy_config (app_type) VALUES (?1)",
|
||||
[app_type],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ==================== Legacy Proxy Config (兼容旧代码) ====================
|
||||
|
||||
/// 获取代理配置(兼容旧接口,返回 claude 行的配置)
|
||||
pub async fn get_proxy_config(&self) -> Result<ProxyConfig, AppError> {
|
||||
// 在一个作用域内获取锁并查询,确保锁在await之前释放
|
||||
// 使用 block 限制 conn 的作用域,避免跨 await 持有锁
|
||||
let result = {
|
||||
let conn = lock_conn!(self.conn);
|
||||
conn.query_row(
|
||||
"SELECT listen_address, listen_port, max_retries,
|
||||
request_timeout, enable_logging, live_takeover_active,
|
||||
enable_logging,
|
||||
streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout
|
||||
FROM proxy_config WHERE id = 1",
|
||||
FROM proxy_config WHERE app_type = 'claude'",
|
||||
[],
|
||||
|row| {
|
||||
Ok(ProxyConfig {
|
||||
listen_address: row.get(0)?,
|
||||
listen_port: row.get::<_, i32>(1)? as u16,
|
||||
max_retries: row.get::<_, i32>(2)? as u8,
|
||||
request_timeout: row.get::<_, i32>(3)? as u64,
|
||||
enable_logging: row.get::<_, i32>(4)? != 0,
|
||||
live_takeover_active: row.get::<_, i32>(5).unwrap_or(0) != 0,
|
||||
streaming_first_byte_timeout: row.get::<_, i32>(6).unwrap_or(30) as u64,
|
||||
streaming_idle_timeout: row.get::<_, i32>(7).unwrap_or(60) as u64,
|
||||
non_streaming_timeout: row.get::<_, i32>(8).unwrap_or(300) as u64,
|
||||
request_timeout: 300, // 废弃字段,返回默认值
|
||||
enable_logging: row.get::<_, i32>(3)? != 0,
|
||||
live_takeover_active: false, // 废弃字段
|
||||
streaming_first_byte_timeout: row.get::<_, i32>(4).unwrap_or(30) as u64,
|
||||
streaming_idle_timeout: row.get::<_, i32>(5).unwrap_or(60) as u64,
|
||||
non_streaming_timeout: row.get::<_, i32>(6).unwrap_or(300) as u64,
|
||||
})
|
||||
},
|
||||
)
|
||||
}; // conn锁在这里释放
|
||||
};
|
||||
// conn 已在 block 结束时释放
|
||||
|
||||
match result {
|
||||
Ok(config) => Ok(config),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => {
|
||||
// 如果不存在,插入默认配置
|
||||
let default_config = ProxyConfig::default();
|
||||
self.update_proxy_config(default_config.clone()).await?;
|
||||
Ok(default_config)
|
||||
// 如果不存在,初始化默认配置
|
||||
self.init_proxy_config_rows().await?;
|
||||
Ok(ProxyConfig::default())
|
||||
}
|
||||
Err(e) => Err(AppError::Database(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新代理配置
|
||||
/// 更新代理配置(兼容旧接口,更新所有三行的公共字段)
|
||||
pub async fn update_proxy_config(&self, config: ProxyConfig) -> Result<(), AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
|
||||
// 更新所有三行的公共字段
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO proxy_config
|
||||
(id, enabled, listen_address, listen_port, max_retries, request_timeout,
|
||||
enable_logging, live_takeover_active, target_app,
|
||||
streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout,
|
||||
created_at, updated_at)
|
||||
VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11,
|
||||
COALESCE((SELECT created_at FROM proxy_config WHERE id = 1), datetime('now')),
|
||||
datetime('now'))",
|
||||
"UPDATE proxy_config SET
|
||||
listen_address = ?1,
|
||||
listen_port = ?2,
|
||||
max_retries = ?3,
|
||||
enable_logging = ?4,
|
||||
streaming_first_byte_timeout = ?5,
|
||||
streaming_idle_timeout = ?6,
|
||||
non_streaming_timeout = ?7,
|
||||
updated_at = datetime('now')",
|
||||
rusqlite::params![
|
||||
0, // 已移除自动启用逻辑,保留列但固定为 0
|
||||
config.listen_address,
|
||||
config.listen_port as i32,
|
||||
config.max_retries as i32,
|
||||
config.request_timeout as i32,
|
||||
if config.enable_logging { 1 } else { 0 },
|
||||
if config.live_takeover_active { 1 } else { 0 },
|
||||
"claude", // 兼容旧字段,写入默认值
|
||||
config.streaming_first_byte_timeout as i32,
|
||||
config.streaming_idle_timeout as i32,
|
||||
config.non_streaming_timeout as i32,
|
||||
@@ -81,27 +263,26 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 设置 Live 接管状态(仅更新 proxy_config 表,兼容旧逻辑)
|
||||
///
|
||||
/// 注意:此方法不会清除 settings 表中的 proxy_takeover_* 状态。
|
||||
/// settings 表的状态由 set_proxy_takeover_enabled 单独管理,用于跨重启保持状态。
|
||||
pub async fn set_live_takeover_active(&self, active: bool) -> Result<(), AppError> {
|
||||
// 仅更新 proxy_config 表(兼容旧版本)
|
||||
let conn = lock_conn!(self.conn);
|
||||
conn.execute(
|
||||
"UPDATE proxy_config SET live_takeover_active = ?1, updated_at = datetime('now') WHERE id = 1",
|
||||
rusqlite::params![if active { 1 } else { 0 }],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
/// 设置 Live 接管状态(兼容旧版本,更新 enabled 字段)
|
||||
pub async fn set_live_takeover_active(&self, _active: bool) -> Result<(), AppError> {
|
||||
// 不再使用此字段,由 enabled 字段替代
|
||||
// 保留空实现以兼容旧代码
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查是否处于 Live 接管模式
|
||||
///
|
||||
/// v3.8.0+:以 settings 表中的 `proxy_takeover_{app_type}` 为真实来源
|
||||
/// 检查是否有任一 app 的 enabled = true
|
||||
pub async fn is_live_takeover_active(&self) -> Result<bool, AppError> {
|
||||
self.has_any_proxy_takeover()
|
||||
let conn = lock_conn!(self.conn);
|
||||
let count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM proxy_config WHERE enabled = 1",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
// ==================== Provider Health ====================
|
||||
@@ -280,19 +461,22 @@ impl Database {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ==================== Circuit Breaker Config ====================
|
||||
// ==================== Circuit Breaker Config (Legacy Compatibility) ====================
|
||||
|
||||
/// 获取熔断器配置
|
||||
/// 获取熔断器配置(兼容旧接口,从 claude 行读取)
|
||||
///
|
||||
/// 熔断器配置已合并到 proxy_config 表,每 app 独立
|
||||
/// 此方法保留用于兼容旧代码,建议使用 get_proxy_config_for_app
|
||||
pub async fn get_circuit_breaker_config(
|
||||
&self,
|
||||
) -> Result<crate::proxy::circuit_breaker::CircuitBreakerConfig, AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
|
||||
let config = conn
|
||||
.query_row(
|
||||
"SELECT failure_threshold, success_threshold, timeout_seconds,
|
||||
error_rate_threshold, min_requests
|
||||
FROM circuit_breaker_config WHERE id = 1",
|
||||
// 使用 block 限制 conn 的作用域,避免跨 await 持有锁
|
||||
let result = {
|
||||
let conn = lock_conn!(self.conn);
|
||||
conn.query_row(
|
||||
"SELECT circuit_failure_threshold, circuit_success_threshold, circuit_timeout_seconds,
|
||||
circuit_error_rate_threshold, circuit_min_requests
|
||||
FROM proxy_config WHERE app_type = 'claude'",
|
||||
[],
|
||||
|row| {
|
||||
Ok(crate::proxy::circuit_breaker::CircuitBreakerConfig {
|
||||
@@ -304,27 +488,39 @@ impl Database {
|
||||
})
|
||||
},
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
};
|
||||
// conn 已在 block 结束时释放
|
||||
|
||||
Ok(config)
|
||||
match result {
|
||||
Ok(config) => Ok(config),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => {
|
||||
// 如果不存在,初始化默认配置
|
||||
self.init_proxy_config_rows().await?;
|
||||
Ok(crate::proxy::circuit_breaker::CircuitBreakerConfig::default())
|
||||
}
|
||||
Err(e) => Err(AppError::Database(e.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新熔断器配置
|
||||
/// 更新熔断器配置(兼容旧接口,更新所有三行)
|
||||
///
|
||||
/// 熔断器配置已合并到 proxy_config 表
|
||||
/// 此方法保留用于兼容旧代码,建议使用 update_proxy_config_for_app
|
||||
pub async fn update_circuit_breaker_config(
|
||||
&self,
|
||||
config: &crate::proxy::circuit_breaker::CircuitBreakerConfig,
|
||||
) -> Result<(), AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
|
||||
// 更新所有三行的熔断器配置
|
||||
conn.execute(
|
||||
"UPDATE circuit_breaker_config
|
||||
SET failure_threshold = ?1,
|
||||
success_threshold = ?2,
|
||||
timeout_seconds = ?3,
|
||||
error_rate_threshold = ?4,
|
||||
min_requests = ?5,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = 1",
|
||||
"UPDATE proxy_config SET
|
||||
circuit_failure_threshold = ?1,
|
||||
circuit_success_threshold = ?2,
|
||||
circuit_timeout_seconds = ?3,
|
||||
circuit_error_rate_threshold = ?4,
|
||||
circuit_min_requests = ?5,
|
||||
updated_at = datetime('now')",
|
||||
rusqlite::params![
|
||||
config.failure_threshold as i32,
|
||||
config.success_threshold as i32,
|
||||
|
||||
@@ -63,11 +63,13 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
// --- 代理接管状态管理 ---
|
||||
// --- 代理接管状态管理(已废弃,使用 proxy_config.enabled 替代)---
|
||||
|
||||
/// 获取指定应用的代理接管状态
|
||||
///
|
||||
/// 使用 settings 表存储各应用的接管状态,key 格式: `proxy_takeover_{app_type}`
|
||||
/// **已废弃**: 请使用 `proxy_config.enabled` 字段替代
|
||||
/// 此方法仅用于数据库迁移时读取旧数据
|
||||
#[deprecated(since = "3.9.0", note = "使用 get_proxy_config_for_app().enabled 替代")]
|
||||
pub fn get_proxy_takeover_enabled(&self, app_type: &str) -> Result<bool, AppError> {
|
||||
let key = format!("proxy_takeover_{app_type}");
|
||||
match self.get_setting(&key)? {
|
||||
@@ -78,8 +80,11 @@ impl Database {
|
||||
|
||||
/// 设置指定应用的代理接管状态
|
||||
///
|
||||
/// - `true` = 开启代理接管
|
||||
/// - `false` = 关闭代理接管
|
||||
/// **已废弃**: 请使用 `proxy_config.enabled` 字段替代
|
||||
#[deprecated(
|
||||
since = "3.9.0",
|
||||
note = "使用 update_proxy_config_for_app() 修改 enabled 字段"
|
||||
)]
|
||||
pub fn set_proxy_takeover_enabled(
|
||||
&self,
|
||||
app_type: &str,
|
||||
@@ -91,6 +96,9 @@ impl Database {
|
||||
}
|
||||
|
||||
/// 检查是否有任一应用开启了代理接管
|
||||
///
|
||||
/// **已废弃**: 请使用 `is_live_takeover_active()` 替代
|
||||
#[deprecated(since = "3.9.0", note = "使用 is_live_takeover_active() 替代")]
|
||||
pub fn has_any_proxy_takeover(&self) -> Result<bool, AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
let count: i64 = conn
|
||||
@@ -104,6 +112,12 @@ impl Database {
|
||||
}
|
||||
|
||||
/// 清除所有代理接管状态(将所有 proxy_takeover_* 设置为 false)
|
||||
///
|
||||
/// **已废弃**: settings 表不再用于存储代理状态
|
||||
#[deprecated(
|
||||
since = "3.9.0",
|
||||
note = "使用 update_proxy_config_for_app() 清除各应用的 enabled 字段"
|
||||
)]
|
||||
pub fn clear_all_proxy_takeover(&self) -> Result<(), AppError> {
|
||||
let conn = lock_conn!(self.conn);
|
||||
conn.execute(
|
||||
|
||||
+250
-220
@@ -55,47 +55,28 @@ impl Database {
|
||||
// 3. MCP Servers 表
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS mcp_servers (
|
||||
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
|
||||
)",
|
||||
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
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 4. Prompts 表
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS prompts (
|
||||
id TEXT NOT NULL,
|
||||
app_type TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
description TEXT,
|
||||
enabled BOOLEAN NOT NULL DEFAULT 1,
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER,
|
||||
PRIMARY KEY (id, app_type)
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
conn.execute("CREATE TABLE IF NOT EXISTS prompts (
|
||||
id TEXT NOT NULL, app_type TEXT NOT NULL, name TEXT NOT NULL, content TEXT NOT NULL,
|
||||
description TEXT, enabled BOOLEAN NOT NULL DEFAULT 1, created_at INTEGER, updated_at INTEGER,
|
||||
PRIMARY KEY (id, app_type)
|
||||
)", []).map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 5. Skills 表
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS skills (
|
||||
directory TEXT NOT NULL,
|
||||
app_type TEXT NOT NULL,
|
||||
installed BOOLEAN NOT NULL DEFAULT 0,
|
||||
installed_at INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (directory, app_type)
|
||||
)",
|
||||
directory TEXT NOT NULL, app_type TEXT NOT NULL, installed BOOLEAN NOT NULL DEFAULT 0,
|
||||
installed_at INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (directory, app_type)
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
@@ -103,172 +84,124 @@ impl Database {
|
||||
// 6. Skill Repos 表
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS skill_repos (
|
||||
owner TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
branch TEXT NOT NULL DEFAULT 'main',
|
||||
enabled BOOLEAN NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY (owner, name)
|
||||
)",
|
||||
owner TEXT NOT NULL, name TEXT NOT NULL, branch TEXT NOT NULL DEFAULT 'main',
|
||||
enabled BOOLEAN NOT NULL DEFAULT 1, PRIMARY KEY (owner, name)
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 7. Settings 表 (通用配置)
|
||||
// 7. Settings 表
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
)",
|
||||
"CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 8. Proxy Config 表 (代理服务器配置)
|
||||
// 代理配置表(单例)
|
||||
// 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')),
|
||||
proxy_enabled INTEGER NOT NULL DEFAULT 0, listen_address TEXT NOT NULL DEFAULT '127.0.0.1',
|
||||
listen_port INTEGER NOT NULL DEFAULT 5000, 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 30,
|
||||
streaming_idle_timeout INTEGER NOT NULL DEFAULT 60, non_streaming_timeout INTEGER NOT NULL DEFAULT 300,
|
||||
circuit_failure_threshold INTEGER NOT NULL DEFAULT 5, 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.5,
|
||||
circuit_min_requests INTEGER NOT NULL DEFAULT 10,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)", []).map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 初始化三行数据(每应用不同默认值)
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS proxy_config (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
enabled INTEGER NOT NULL DEFAULT 0,
|
||||
listen_address TEXT NOT NULL DEFAULT '127.0.0.1',
|
||||
listen_port INTEGER NOT NULL DEFAULT 5000,
|
||||
max_retries INTEGER NOT NULL DEFAULT 3,
|
||||
request_timeout INTEGER NOT NULL DEFAULT 300,
|
||||
enable_logging INTEGER NOT NULL DEFAULT 1,
|
||||
target_app TEXT NOT NULL DEFAULT 'claude',
|
||||
streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 30,
|
||||
streaming_idle_timeout INTEGER NOT NULL DEFAULT 60,
|
||||
non_streaming_timeout INTEGER NOT NULL DEFAULT 300,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)",
|
||||
"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 ('claude', 6, 45, 90, 300, 8, 3, 90, 0.6, 15)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 尝试添加 target_app 列(如果表已存在但缺少该列)
|
||||
// 忽略 "duplicate column name" 错误
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE proxy_config ADD COLUMN target_app TEXT NOT NULL DEFAULT 'claude'",
|
||||
[],
|
||||
);
|
||||
|
||||
// 9. Provider Health 表 (Provider健康状态)
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS provider_health (
|
||||
provider_id TEXT NOT NULL,
|
||||
app_type TEXT NOT NULL,
|
||||
is_healthy INTEGER NOT NULL DEFAULT 1,
|
||||
consecutive_failures INTEGER NOT NULL DEFAULT 0,
|
||||
last_success_at TEXT,
|
||||
last_failure_at TEXT,
|
||||
last_error TEXT,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (provider_id, app_type),
|
||||
FOREIGN KEY (provider_id, app_type) REFERENCES providers(id, app_type) ON DELETE CASCADE
|
||||
)",
|
||||
"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 ('codex', 3, 30, 60, 300, 5, 2, 60, 0.5, 10)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 10. Proxy Request Logs 表 (详细请求日志)
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS proxy_request_logs (
|
||||
request_id TEXT PRIMARY KEY,
|
||||
provider_id TEXT NOT NULL,
|
||||
app_type TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
input_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
output_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
input_cost_usd TEXT NOT NULL DEFAULT '0',
|
||||
output_cost_usd TEXT NOT NULL DEFAULT '0',
|
||||
cache_read_cost_usd TEXT NOT NULL DEFAULT '0',
|
||||
cache_creation_cost_usd TEXT NOT NULL DEFAULT '0',
|
||||
total_cost_usd TEXT NOT NULL DEFAULT '0',
|
||||
latency_ms INTEGER NOT NULL,
|
||||
first_token_ms INTEGER,
|
||||
duration_ms INTEGER,
|
||||
status_code INTEGER NOT NULL,
|
||||
error_message TEXT,
|
||||
session_id TEXT,
|
||||
provider_type TEXT,
|
||||
is_streaming INTEGER NOT NULL DEFAULT 0,
|
||||
cost_multiplier TEXT NOT NULL DEFAULT '1.0',
|
||||
created_at INTEGER NOT NULL
|
||||
)",
|
||||
"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 ('gemini', 5, 30, 60, 300, 5, 2, 60, 0.5, 10)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 9. Provider Health 表
|
||||
conn.execute("CREATE TABLE IF NOT EXISTS provider_health (
|
||||
provider_id TEXT NOT NULL, app_type TEXT NOT NULL, is_healthy INTEGER NOT NULL DEFAULT 1,
|
||||
consecutive_failures INTEGER NOT NULL DEFAULT 0, last_success_at TEXT, last_failure_at TEXT,
|
||||
last_error TEXT, updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (provider_id, app_type),
|
||||
FOREIGN KEY (provider_id, app_type) REFERENCES providers(id, app_type) ON DELETE CASCADE
|
||||
)", []).map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 10. Proxy Request Logs 表
|
||||
conn.execute("CREATE TABLE IF NOT EXISTS proxy_request_logs (
|
||||
request_id TEXT PRIMARY KEY, provider_id TEXT NOT NULL, app_type TEXT NOT NULL, model TEXT NOT NULL,
|
||||
input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
cache_read_tokens INTEGER NOT NULL DEFAULT 0, cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
input_cost_usd TEXT NOT NULL DEFAULT '0', output_cost_usd TEXT NOT NULL DEFAULT '0',
|
||||
cache_read_cost_usd TEXT NOT NULL DEFAULT '0', cache_creation_cost_usd TEXT NOT NULL DEFAULT '0',
|
||||
total_cost_usd TEXT NOT NULL DEFAULT '0', latency_ms INTEGER NOT NULL, first_token_ms INTEGER,
|
||||
duration_ms INTEGER, status_code INTEGER NOT NULL, error_message TEXT, session_id TEXT,
|
||||
provider_type TEXT, is_streaming INTEGER NOT NULL DEFAULT 0,
|
||||
cost_multiplier TEXT NOT NULL DEFAULT '1.0', created_at INTEGER NOT NULL
|
||||
)", []).map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_request_logs_provider ON proxy_request_logs(provider_id, app_type)", [])
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_request_logs_created_at ON proxy_request_logs(created_at)", [])
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_request_logs_provider
|
||||
ON proxy_request_logs(provider_id, app_type)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_request_logs_model ON proxy_request_logs(model)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_request_logs_created_at
|
||||
ON proxy_request_logs(created_at)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_request_logs_session ON proxy_request_logs(session_id)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_request_logs_model
|
||||
ON proxy_request_logs(model)",
|
||||
"CREATE INDEX IF NOT EXISTS idx_request_logs_status ON proxy_request_logs(status_code)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_request_logs_session
|
||||
ON proxy_request_logs(session_id)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_request_logs_status
|
||||
ON proxy_request_logs(status_code)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 11. Model Pricing 表 (模型定价)
|
||||
// 11. Model Pricing 表
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS model_pricing (
|
||||
model_id TEXT PRIMARY KEY,
|
||||
display_name TEXT NOT NULL,
|
||||
input_cost_per_million TEXT NOT NULL,
|
||||
output_cost_per_million TEXT NOT NULL,
|
||||
cache_read_cost_per_million TEXT NOT NULL DEFAULT '0',
|
||||
cache_creation_cost_per_million TEXT NOT NULL DEFAULT '0'
|
||||
)",
|
||||
model_id TEXT PRIMARY KEY, display_name TEXT NOT NULL,
|
||||
input_cost_per_million TEXT NOT NULL, output_cost_per_million TEXT NOT NULL,
|
||||
cache_read_cost_per_million TEXT NOT NULL DEFAULT '0',
|
||||
cache_creation_cost_per_million TEXT NOT NULL DEFAULT '0'
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 12. Stream Check Logs 表 (流式健康检查日志)
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS stream_check_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
provider_id TEXT NOT NULL,
|
||||
provider_name TEXT NOT NULL,
|
||||
app_type TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
success INTEGER NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
response_time_ms INTEGER,
|
||||
http_status INTEGER,
|
||||
model_used TEXT,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
tested_at INTEGER NOT NULL
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
// 12. Stream Check Logs 表
|
||||
conn.execute("CREATE TABLE IF NOT EXISTS stream_check_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT, provider_id TEXT NOT NULL, provider_name TEXT NOT NULL,
|
||||
app_type TEXT NOT NULL, status TEXT NOT NULL, success INTEGER NOT NULL, message TEXT NOT NULL,
|
||||
response_time_ms INTEGER, http_status INTEGER, model_used TEXT,
|
||||
retry_count INTEGER DEFAULT 0, tested_at INTEGER NOT NULL
|
||||
)", []).map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_stream_check_logs_provider
|
||||
@@ -277,35 +210,13 @@ impl Database {
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 13. Circuit Breaker Config 表 (熔断器配置)
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS circuit_breaker_config (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
failure_threshold INTEGER NOT NULL DEFAULT 5,
|
||||
success_threshold INTEGER NOT NULL DEFAULT 2,
|
||||
timeout_seconds INTEGER NOT NULL DEFAULT 60,
|
||||
error_rate_threshold REAL NOT NULL DEFAULT 0.5,
|
||||
min_requests INTEGER NOT NULL DEFAULT 10,
|
||||
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
// 插入默认熔断器配置
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO circuit_breaker_config (id) VALUES (1)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
// 注意:circuit_breaker_config 已合并到 proxy_config 表中
|
||||
|
||||
// 16. Proxy Live Backup 表 (Live 配置备份)
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS proxy_live_backup (
|
||||
app_type TEXT PRIMARY KEY,
|
||||
original_config TEXT NOT NULL,
|
||||
backed_up_at TEXT NOT NULL
|
||||
)",
|
||||
app_type TEXT PRIMARY KEY, original_config TEXT NOT NULL, backed_up_at TEXT NOT NULL
|
||||
)",
|
||||
[],
|
||||
)
|
||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||
@@ -316,7 +227,7 @@ impl Database {
|
||||
[],
|
||||
);
|
||||
|
||||
// 尝试添加超时配置列到 proxy_config 表(v3 新增)
|
||||
// 尝试添加超时配置列到 proxy_config 表
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE proxy_config ADD COLUMN streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 30",
|
||||
[],
|
||||
@@ -492,7 +403,7 @@ impl Database {
|
||||
"BOOLEAN NOT NULL DEFAULT 0",
|
||||
)?;
|
||||
|
||||
// 添加代理超时配置字段(在 v2 迁移中直接添加)
|
||||
// 添加代理超时配置字段
|
||||
if Self::table_exists(conn, "proxy_config")? {
|
||||
Self::add_column_if_missing(
|
||||
conn,
|
||||
@@ -528,35 +439,18 @@ impl Database {
|
||||
)
|
||||
.map_err(|e| AppError::Database(format!("创建 failover 索引失败: {e}")))?;
|
||||
|
||||
// proxy_request_logs 表(包含所有字段)
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS proxy_request_logs (
|
||||
request_id TEXT PRIMARY KEY,
|
||||
provider_id TEXT NOT NULL,
|
||||
app_type TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
input_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
output_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
input_cost_usd TEXT NOT NULL DEFAULT '0',
|
||||
output_cost_usd TEXT NOT NULL DEFAULT '0',
|
||||
cache_read_cost_usd TEXT NOT NULL DEFAULT '0',
|
||||
cache_creation_cost_usd TEXT NOT NULL DEFAULT '0',
|
||||
total_cost_usd TEXT NOT NULL DEFAULT '0',
|
||||
latency_ms INTEGER NOT NULL,
|
||||
first_token_ms INTEGER,
|
||||
duration_ms INTEGER,
|
||||
status_code INTEGER NOT NULL,
|
||||
error_message TEXT,
|
||||
session_id TEXT,
|
||||
provider_type TEXT,
|
||||
is_streaming INTEGER NOT NULL DEFAULT 0,
|
||||
cost_multiplier TEXT NOT NULL DEFAULT '1.0',
|
||||
created_at INTEGER NOT NULL
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
// proxy_request_logs 表
|
||||
conn.execute("CREATE TABLE IF NOT EXISTS proxy_request_logs (
|
||||
request_id TEXT PRIMARY KEY, provider_id TEXT NOT NULL, app_type TEXT NOT NULL, model TEXT NOT NULL,
|
||||
input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
cache_read_tokens INTEGER NOT NULL DEFAULT 0, cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
|
||||
input_cost_usd TEXT NOT NULL DEFAULT '0', output_cost_usd TEXT NOT NULL DEFAULT '0',
|
||||
cache_read_cost_usd TEXT NOT NULL DEFAULT '0', cache_creation_cost_usd TEXT NOT NULL DEFAULT '0',
|
||||
total_cost_usd TEXT NOT NULL DEFAULT '0', latency_ms INTEGER NOT NULL, first_token_ms INTEGER,
|
||||
duration_ms INTEGER, status_code INTEGER NOT NULL, error_message TEXT, session_id TEXT,
|
||||
provider_type TEXT, is_streaming INTEGER NOT NULL DEFAULT 0,
|
||||
cost_multiplier TEXT NOT NULL DEFAULT '1.0', created_at INTEGER NOT NULL
|
||||
)", [])?;
|
||||
|
||||
// 为已存在的表添加新字段
|
||||
Self::add_column_if_missing(conn, "proxy_request_logs", "provider_type", "TEXT")?;
|
||||
@@ -578,13 +472,11 @@ impl Database {
|
||||
// model_pricing 表
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS model_pricing (
|
||||
model_id TEXT PRIMARY KEY,
|
||||
display_name TEXT NOT NULL,
|
||||
input_cost_per_million TEXT NOT NULL,
|
||||
output_cost_per_million TEXT NOT NULL,
|
||||
cache_read_cost_per_million TEXT NOT NULL DEFAULT '0',
|
||||
cache_creation_cost_per_million TEXT NOT NULL DEFAULT '0'
|
||||
)",
|
||||
model_id TEXT PRIMARY KEY, display_name TEXT NOT NULL,
|
||||
input_cost_per_million TEXT NOT NULL, output_cost_per_million TEXT NOT NULL,
|
||||
cache_read_cost_per_million TEXT NOT NULL DEFAULT '0',
|
||||
cache_creation_cost_per_million TEXT NOT NULL DEFAULT '0'
|
||||
)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
@@ -596,6 +488,144 @@ impl Database {
|
||||
// 重构 skills 表(添加 app_type 字段)
|
||||
Self::migrate_skills_table(conn)?;
|
||||
|
||||
// 重构 proxy_config 为三行结构(每应用独立配置)
|
||||
Self::migrate_proxy_config_to_per_app(conn)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将 proxy_config 迁移为三行结构(每应用独立配置)
|
||||
fn migrate_proxy_config_to_per_app(conn: &Connection) -> Result<(), AppError> {
|
||||
// 检查是否已经是新表结构(幂等性)
|
||||
if !Self::table_exists(conn, "proxy_config")? {
|
||||
// 表不存在,跳过迁移(新安装)
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if Self::has_column(conn, "proxy_config", "app_type")? {
|
||||
// 已经是三行结构,跳过迁移
|
||||
log::info!("proxy_config 已经是三行结构,跳过迁移");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 读取旧配置
|
||||
let old_config = conn
|
||||
.query_row(
|
||||
"SELECT listen_address, listen_port, max_retries, enable_logging,
|
||||
streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout
|
||||
FROM proxy_config WHERE id = 1",
|
||||
[],
|
||||
|row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, i32>(1)?,
|
||||
row.get::<_, i32>(2)?,
|
||||
row.get::<_, i32>(3)?,
|
||||
row.get::<_, i32>(4).unwrap_or(30),
|
||||
row.get::<_, i32>(5).unwrap_or(60),
|
||||
row.get::<_, i32>(6).unwrap_or(300),
|
||||
))
|
||||
},
|
||||
)
|
||||
.unwrap_or_else(|_| ("127.0.0.1".to_string(), 5000, 3, 1, 30, 60, 300));
|
||||
|
||||
let old_cb = conn.query_row(
|
||||
"SELECT failure_threshold, success_threshold, timeout_seconds, error_rate_threshold, min_requests
|
||||
FROM circuit_breaker_config WHERE id = 1", [],
|
||||
|row| Ok((row.get::<_, i32>(0)?, row.get::<_, i32>(1)?, row.get::<_, i64>(2)?,
|
||||
row.get::<_, f64>(3)?, row.get::<_, i32>(4)?))
|
||||
).unwrap_or((5, 2, 60, 0.5, 10));
|
||||
|
||||
let get_bool = |key: &str| -> bool {
|
||||
conn.query_row("SELECT value FROM settings WHERE key = ?", [key], |r| {
|
||||
r.get::<_, String>(0)
|
||||
})
|
||||
.map(|v| v == "true" || v == "1")
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
let apps = [
|
||||
(
|
||||
"claude",
|
||||
get_bool("proxy_takeover_claude"),
|
||||
get_bool("auto_failover_enabled_claude"),
|
||||
6,
|
||||
45,
|
||||
90,
|
||||
8,
|
||||
3,
|
||||
90,
|
||||
0.6,
|
||||
15,
|
||||
),
|
||||
(
|
||||
"codex",
|
||||
get_bool("proxy_takeover_codex"),
|
||||
get_bool("auto_failover_enabled_codex"),
|
||||
3,
|
||||
old_config.4,
|
||||
old_config.5,
|
||||
old_cb.0,
|
||||
old_cb.1,
|
||||
old_cb.2,
|
||||
old_cb.3,
|
||||
old_cb.4,
|
||||
),
|
||||
(
|
||||
"gemini",
|
||||
get_bool("proxy_takeover_gemini"),
|
||||
get_bool("auto_failover_enabled_gemini"),
|
||||
5,
|
||||
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')),
|
||||
proxy_enabled INTEGER NOT NULL DEFAULT 0, listen_address TEXT NOT NULL DEFAULT '127.0.0.1',
|
||||
listen_port INTEGER NOT NULL DEFAULT 5000, 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 30,
|
||||
streaming_idle_timeout INTEGER NOT NULL DEFAULT 60, non_streaming_timeout INTEGER NOT NULL DEFAULT 300,
|
||||
circuit_failure_threshold INTEGER NOT NULL DEFAULT 5, 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.5,
|
||||
circuit_min_requests INTEGER NOT NULL DEFAULT 10,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)", [])?;
|
||||
|
||||
// 插入三行配置
|
||||
for (app, takeover, failover, retries, fb, idle, cb_f, cb_s, cb_t, cb_r, cb_m) in apps {
|
||||
conn.execute(
|
||||
"INSERT INTO proxy_config_new (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)
|
||||
VALUES (?1, 0, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)",
|
||||
rusqlite::params![app, old_config.0, old_config.1, old_config.3,
|
||||
if takeover { 1 } else { 0 }, if failover { 1 } else { 0 },
|
||||
retries, fb, idle, old_config.6, cb_f, cb_s, cb_t, cb_r, cb_m]
|
||||
).map_err(|e| AppError::Database(format!("插入 {app} 配置失败: {e}")))?;
|
||||
}
|
||||
|
||||
// 替换表并清理
|
||||
conn.execute("DROP TABLE IF EXISTS proxy_config", [])?;
|
||||
conn.execute("ALTER TABLE proxy_config_new RENAME TO proxy_config", [])?;
|
||||
conn.execute("DROP TABLE IF EXISTS circuit_breaker_config", [])?;
|
||||
conn.execute("DELETE FROM settings WHERE key LIKE 'proxy_takeover_%'", [])?;
|
||||
conn.execute(
|
||||
"DELETE FROM settings WHERE key LIKE 'auto_failover_enabled_%'",
|
||||
[],
|
||||
)?;
|
||||
|
||||
log::info!("proxy_config 已迁移为三行结构");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import type {
|
||||
ProxyConfig,
|
||||
ProxyStatus,
|
||||
ProxyServerInfo,
|
||||
ProxyTakeoverStatus,
|
||||
GlobalProxyConfig,
|
||||
AppProxyConfig,
|
||||
} from "@/types/proxy";
|
||||
|
||||
export const proxyApi = {
|
||||
// ========== 代理服务器控制 API ==========
|
||||
|
||||
// 启动代理服务器
|
||||
async startProxyServer(): Promise<ProxyServerInfo> {
|
||||
return invoke("start_proxy_server");
|
||||
},
|
||||
|
||||
// 停止代理服务器并恢复配置
|
||||
async stopProxyWithRestore(): Promise<void> {
|
||||
return invoke("stop_proxy_with_restore");
|
||||
},
|
||||
|
||||
// 获取代理服务器状态
|
||||
async getProxyStatus(): Promise<ProxyStatus> {
|
||||
return invoke("get_proxy_status");
|
||||
},
|
||||
|
||||
// 检查代理服务器是否正在运行
|
||||
async isProxyRunning(): Promise<boolean> {
|
||||
return invoke("is_proxy_running");
|
||||
},
|
||||
|
||||
// 检查是否处于接管模式
|
||||
async isLiveTakeoverActive(): Promise<boolean> {
|
||||
return invoke("is_live_takeover_active");
|
||||
},
|
||||
|
||||
// 代理模式下切换供应商
|
||||
async switchProxyProvider(appType: string, providerId: string): Promise<void> {
|
||||
return invoke("switch_proxy_provider", { appType, providerId });
|
||||
},
|
||||
|
||||
// ========== 接管状态 API ==========
|
||||
|
||||
// 获取各应用接管状态
|
||||
async getProxyTakeoverStatus(): Promise<ProxyTakeoverStatus> {
|
||||
return invoke("get_proxy_takeover_status");
|
||||
},
|
||||
|
||||
// 为指定应用开启/关闭接管
|
||||
async setProxyTakeoverForApp(appType: string, enabled: boolean): Promise<void> {
|
||||
return invoke("set_proxy_takeover_for_app", { appType, enabled });
|
||||
},
|
||||
|
||||
// ========== Legacy 代理配置 API (兼容) ==========
|
||||
|
||||
// 获取代理配置(旧版 v2 兼容接口)
|
||||
async getProxyConfig(): Promise<ProxyConfig> {
|
||||
return invoke("get_proxy_config");
|
||||
},
|
||||
|
||||
// 更新代理配置(旧版 v2 兼容接口)
|
||||
async updateProxyConfig(config: ProxyConfig): Promise<void> {
|
||||
return invoke("update_proxy_config", { config });
|
||||
},
|
||||
|
||||
// ========== v3+ 全局/应用级配置 API ==========
|
||||
|
||||
// 获取全局代理配置
|
||||
async getGlobalProxyConfig(): Promise<GlobalProxyConfig> {
|
||||
return invoke("get_global_proxy_config");
|
||||
},
|
||||
|
||||
// 更新全局代理配置
|
||||
async updateGlobalProxyConfig(config: GlobalProxyConfig): Promise<void> {
|
||||
return invoke("update_global_proxy_config", { config });
|
||||
},
|
||||
|
||||
// 获取指定应用的代理配置
|
||||
async getProxyConfigForApp(appType: string): Promise<AppProxyConfig> {
|
||||
return invoke("get_proxy_config_for_app", { appType });
|
||||
},
|
||||
|
||||
// 更新指定应用的代理配置
|
||||
async updateProxyConfigForApp(config: AppProxyConfig): Promise<void> {
|
||||
return invoke("update_proxy_config_for_app", { config });
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,222 @@
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { proxyApi } from "@/lib/api/proxy";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { GlobalProxyConfig, AppProxyConfig } from "@/types/proxy";
|
||||
|
||||
// ========== 代理服务器状态 Hooks ==========
|
||||
|
||||
/**
|
||||
* 获取代理服务器状态
|
||||
*/
|
||||
export function useProxyStatus() {
|
||||
return useQuery({
|
||||
queryKey: ["proxyStatus"],
|
||||
queryFn: () => proxyApi.getProxyStatus(),
|
||||
refetchInterval: 5000, // 每 5 秒刷新一次
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查代理服务器是否运行
|
||||
*/
|
||||
export function useIsProxyRunning() {
|
||||
return useQuery({
|
||||
queryKey: ["proxyRunning"],
|
||||
queryFn: () => proxyApi.isProxyRunning(),
|
||||
refetchInterval: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否处于接管模式
|
||||
*/
|
||||
export function useIsLiveTakeoverActive() {
|
||||
return useQuery({
|
||||
queryKey: ["liveTakeoverActive"],
|
||||
queryFn: () => proxyApi.isLiveTakeoverActive(),
|
||||
refetchInterval: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取各应用接管状态
|
||||
*/
|
||||
export function useProxyTakeoverStatus() {
|
||||
return useQuery({
|
||||
queryKey: ["proxyTakeoverStatus"],
|
||||
queryFn: () => proxyApi.getProxyTakeoverStatus(),
|
||||
refetchInterval: 2000,
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 代理服务器控制 Hooks ==========
|
||||
|
||||
/**
|
||||
* 启动代理服务器
|
||||
*/
|
||||
export function useStartProxyServer() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => proxyApi.startProxyServer(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyRunning"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["liveTakeoverActive"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止代理服务器
|
||||
*/
|
||||
export function useStopProxyServer() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => proxyApi.stopProxyWithRestore(),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyRunning"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["liveTakeoverActive"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置应用接管状态
|
||||
*/
|
||||
export function useSetProxyTakeoverForApp() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ appType, enabled }: { appType: string; enabled: boolean }) =>
|
||||
proxyApi.setProxyTakeoverForApp(appType, enabled),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["liveTakeoverActive"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 代理模式下切换供应商
|
||||
*/
|
||||
export function useSwitchProxyProvider() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({ appType, providerId }: { appType: string; providerId: string }) =>
|
||||
proxyApi.switchProxyProvider(appType, providerId),
|
||||
onSuccess: (_, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["providers", variables.appType] });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(t("proxy.switchFailed", { error: error.message }));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Legacy 代理配置 Hooks (兼容) ==========
|
||||
|
||||
/**
|
||||
* 获取代理配置(旧版)
|
||||
*/
|
||||
export function useProxyConfig() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: config, isLoading } = useQuery({
|
||||
queryKey: ["proxyConfig"],
|
||||
queryFn: () => proxyApi.getProxyConfig(),
|
||||
});
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: proxyApi.updateProxyConfig,
|
||||
onSuccess: () => {
|
||||
toast.success(t("proxy.settings.toast.saved"), { closeButton: true });
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyConfig"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(t("proxy.settings.toast.saveFailed", { error: error.message }));
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
config,
|
||||
isLoading,
|
||||
updateConfig: updateMutation.mutateAsync,
|
||||
isUpdating: updateMutation.isPending,
|
||||
};
|
||||
}
|
||||
|
||||
// ========== v3+ 全局/应用级配置 Hooks ==========
|
||||
|
||||
/**
|
||||
* 获取全局代理配置
|
||||
*/
|
||||
export function useGlobalProxyConfig() {
|
||||
return useQuery({
|
||||
queryKey: ["globalProxyConfig"],
|
||||
queryFn: () => proxyApi.getGlobalProxyConfig(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新全局代理配置
|
||||
*/
|
||||
export function useUpdateGlobalProxyConfig() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (config: GlobalProxyConfig) => proxyApi.updateGlobalProxyConfig(config),
|
||||
onSuccess: () => {
|
||||
toast.success(t("proxy.settings.toast.saved"), { closeButton: true });
|
||||
queryClient.invalidateQueries({ queryKey: ["globalProxyConfig"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyConfig"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyStatus"] });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(t("proxy.settings.toast.saveFailed", { error: error.message }));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定应用的代理配置
|
||||
*/
|
||||
export function useAppProxyConfig(appType: string) {
|
||||
return useQuery({
|
||||
queryKey: ["appProxyConfig", appType],
|
||||
queryFn: () => proxyApi.getProxyConfigForApp(appType),
|
||||
enabled: !!appType,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定应用的代理配置
|
||||
*/
|
||||
export function useUpdateAppProxyConfig() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (config: AppProxyConfig) => proxyApi.updateProxyConfigForApp(config),
|
||||
onSuccess: (_, variables) => {
|
||||
toast.success(t("proxy.settings.toast.saved"), { closeButton: true });
|
||||
queryClient.invalidateQueries({ queryKey: ["appProxyConfig", variables.appType] });
|
||||
queryClient.invalidateQueries({ queryKey: ["proxyConfig"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["circuitBreakerConfig"] });
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(t("proxy.settings.toast.saveFailed", { error: error.message }));
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user