Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ last-updated: 2025-12-07
- 旧的 `transparent_proxy_*` 字段会在读取配置时自动迁移到新结构
- 新命令:`start_tool_proxy`、`stop_tool_proxy`、`get_all_proxy_status`
- 旧命令保持兼容,内部使用新架构实现
- **配置管理机制(2025-12-12)**:
- 代理启动时自动创建内置 Profile(`dc_proxy_*`),通过 `ProfileManager` 切换配置
- 内置 Profile 在 UI 中不可见(列表查询时过滤 `dc_proxy_` 前缀)
- `dc_proxy_` 为系统保留前缀,用户无法创建同名 Profile
- 代理关闭时自动还原到启动前激活的 Profile
- 运行时禁止修改 `ToolProxyConfig`,确保配置一致性
- `original_active_profile` 字段记录启动前的 Profile 用于还原
- Gemini CLI 的 model 字段为可选,允许不填(内置代理 Profile 不设置 model,保留用户原有配置)
- `ToolProxyConfig` 额外存储 `real_profile_name`、`auto_start`、工具级 `session_endpoint_config_enabled`,全局配置新增 `hide_transparent_proxy_tip` 控制设置页横幅显示
- `GlobalConfig.hide_session_config_hint` 持久化会话级端点提示的隐藏状态,`ProxyControlBar`/`ProxySettingsDialog`/`ClaudeContent` 通过 `open-proxy-settings` 与 `proxy-config-updated` 事件联动刷新视图
- 日志系统支持完整配置管理:`GlobalConfig.log_config` 存储级别/格式/输出目标;`log_commands.rs` 提供查询与更新命令,`LogSettingsTab` 可热重载级别、保存文件输出设置;`core/logger.rs` 通过 `update_log_level` reload 机制动态调整
Expand All @@ -169,7 +177,23 @@ last-updated: 2025-12-07
- `ToolStatusCache` 标记为已废弃,保留仅用于向后兼容
- 所有工具状态查询统一走 `ToolRegistry::get_local_tool_status()` 和 `refresh_and_get_local_status()`
- UI 相关的托盘/窗口操作集中在 `src-tauri/src/ui/*`,其它模块如需最小化到托盘请调用 `ui::hide_window_to_tray` 等封装方法。
- 新增 `TransparentProxyPage` 与会话数据库:`SESSION_MANAGER` 使用 SQLite 记录每个代理会话的 endpoint/API Key,前端可按工具启停代理、查看历史并启用「会话级 Endpoint 配置」开关。页面内的 `ProxyControlBar`、`ProxySettingsDialog`、`ProxyConfigDialog` 负责代理启停、配置切换、工具级设置并内建缺失配置提示。
- **会话管理系统(2025-12-12 重构)**:
- **架构迁移**:`SessionDatabase` 已删除,`SessionManager` 直接使用 `DataManager::sqlite()` 管理会话数据
- **核心模块**(位于 `services/session/`):
- `manager.rs`:`SESSION_MANAGER` 单例,内部持有 `Arc<DataManager>` 和 `db_path`
- `db_utils.rs`:私有工具模块,提供 `QueryRow ↔ ProxySession` 转换、SQL 常量定义
- `models.rs`:数据模型(`ProxySession`、`SessionEvent`、`SessionListResponse`)
- **查询缓存**:所有数据库查询自动利用 `SqliteManager` 的查询缓存(容量 100,TTL 5分钟)
- **转换工具**:
- `parse_proxy_session(row)` - 将 QueryRow 转换为 ProxySession(处理 13 个字段 + NULL 值)
- `parse_count(row)` - 提取计数查询结果
- `parse_session_config(row)` - 提取三元组配置 (config_name, url, api_key)
- **后台任务**:
- 批量写入任务:每 100ms 或缓冲区满 10 条时批量 upsert 会话
- 定期清理任务:每小时清理 30 天未活跃会话和超过 1000 条的旧会话
- **测试覆盖**:10 个单元测试(5 个 db_utils 转换测试 + 2 个 models 测试 + 3 个 manager 集成测试)
- **代码减少**:从 366 行(db.rs)减少到 ~320 行(manager.rs 250 行 + db_utils.rs 70 行工具函数)
- 前端透明代理页面:`TransparentProxyPage` 通过 `SESSION_MANAGER` 记录每个代理会话的 endpoint/API Key,支持按工具启停代理、查看历史并启用「会话级 Endpoint 配置」开关。页面内的 `ProxyControlBar`、`ProxySettingsDialog`、`ProxyConfigDialog` 负责代理启停、配置切换、工具级设置并内建缺失配置提示。
- **余额监控页面(BalancePage)**:
- 后端提供通用 `fetch_api` 命令(位于 `commands/api_commands.rs`),支持 GET/POST、自定义 headers、超时控制
- 前端使用 JavaScript `Function` 构造器执行用户自定义的 extractor 脚本(位于 `utils/extractor.ts`)
Expand Down
5 changes: 3 additions & 2 deletions src-tauri/src/commands/profile_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ pub enum ProfileInput {
Gemini {
api_key: String,
base_url: String,
model: String,
#[serde(default)]
model: Option<String>,
},
Codex {
api_key: String,
Expand Down Expand Up @@ -119,7 +120,7 @@ pub async fn pm_save_profile(
model,
} = input
{
manager.save_gemini_profile(&name, api_key, base_url, Some(model))
manager.save_gemini_profile(&name, api_key, base_url, model)
} else {
Err(anyhow::anyhow!("Gemini CLI 需要 Gemini Profile 数据"))
}
Expand Down
193 changes: 165 additions & 28 deletions src-tauri/src/commands/proxy_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,40 +349,87 @@ pub async fn start_tool_proxy(
tool_id: String,
manager_state: State<'_, ProxyManagerState>,
) -> Result<String, String> {
// 从 ProxyConfigManager 读取配置
use ::duckcoding::services::profile_manager::ProfileManager;

let profile_mgr = ProfileManager::new().map_err(|e| e.to_string())?;
let proxy_config_mgr = ProxyConfigManager::new().map_err(|e| e.to_string())?;
let tool_config = proxy_config_mgr

// 读取当前配置
let mut tool_config = proxy_config_mgr
.get_config(&tool_id)
.map_err(|e| e.to_string())?
.ok_or_else(|| format!("工具 {} 的代理配置不存在", tool_id))?;

// 检查是否启用
if !tool_config.enabled {
return Err(format!("{} 的透明代理未启用", tool_id));
// 检查是否已在运行
if manager_state.manager.is_running(&tool_id).await {
return Err(format!("{} 代理已在运行", tool_id));
}

// 检查必要字段
if !tool_config.enabled {
return Err(format!("{} 的透明代理未启用", tool_id));
}
if tool_config.local_api_key.is_none() {
return Err("透明代理保护密钥未设置".to_string());
}
if tool_config.real_api_key.is_none() {
return Err("真实 API Key 未设置".to_string());
if tool_config.real_api_key.is_none() || tool_config.real_base_url.is_none() {
return Err("真实 API Key 或 Base URL 未设置".to_string());
}
if tool_config.real_base_url.is_none() {
return Err("真实 Base URL 未设置".to_string());

// ========== Profile 切换逻辑 ==========

// 1. 读取当前激活的 Profile 名称
let original_profile = profile_mgr
.get_active_profile_name(&tool_id)
.map_err(|e| e.to_string())?;

// 2. 保存到 ToolProxyConfig
tool_config.original_active_profile = original_profile.clone();
proxy_config_mgr
.update_config(&tool_id, tool_config.clone())
.map_err(|e| e.to_string())?;

// 3. 验证内置 Profile 是否存在
let proxy_profile_name = format!("dc_proxy_{}", tool_id.replace("-", "_"));

let profile_exists = match tool_id.as_str() {
"claude-code" => profile_mgr.get_claude_profile(&proxy_profile_name).is_ok(),
"codex" => profile_mgr.get_codex_profile(&proxy_profile_name).is_ok(),
"gemini-cli" => profile_mgr.get_gemini_profile(&proxy_profile_name).is_ok(),
_ => false,
};

if !profile_exists {
return Err(format!(
"内置 Profile 不存在,请先保存代理配置: {}",
proxy_profile_name
));
}

// 4. 激活内置 Profile(这会自动同步到原生配置文件)
profile_mgr
.activate_profile(&tool_id, &proxy_profile_name)
.map_err(|e| format!("激活内置 Profile 失败: {}", e))?;

tracing::info!(
tool_id = %tool_id,
original_profile = ?original_profile,
proxy_profile = %proxy_profile_name,
"已切换到代理 Profile"
);

// ========== 启动代理 ==========

let proxy_port = tool_config.port;

// 启动代理
manager_state
.manager
.start_proxy(&tool_id, tool_config)
.await
.map_err(|e| format!("启动代理失败: {}", e))?;

Ok(format!(
"✅ {} 透明代理已启动\n监听端口: {}\n请求将自动转发",
"✅ {} 透明代理已启动\n监听端口: {}\n已切换到代理配置",
tool_id, proxy_port
))
}
Expand All @@ -393,30 +440,59 @@ pub async fn stop_tool_proxy(
tool_id: String,
manager_state: State<'_, ProxyManagerState>,
) -> Result<String, String> {
// 读取全局配置
let config = get_global_config()
.await
.map_err(|e| format!("读取配置失败: {e}"))?
.ok_or_else(|| "全局配置不存在".to_string())?;
use ::duckcoding::services::profile_manager::ProfileManager;

let profile_mgr = ProfileManager::new().map_err(|e| e.to_string())?;
let proxy_config_mgr = ProxyConfigManager::new().map_err(|e| e.to_string())?;

// 读取代理配置
let mut tool_config = proxy_config_mgr
.get_config(&tool_id)
.map_err(|e| e.to_string())?
.ok_or_else(|| format!("工具 {} 的代理配置不存在", tool_id))?;

// ========== 停止代理 ==========

// 停止代理
manager_state
.manager
.stop_proxy(&tool_id)
.await
.map_err(|e| format!("停止代理失败: {e}"))?;

// 恢复工具配置
if let Some(tool_config) = config.get_proxy_config(&tool_id) {
if tool_config.real_api_key.is_some() {
let tool = Tool::by_id(&tool_id).ok_or_else(|| format!("未知工具: {tool_id}"))?;
// ========== Profile 还原逻辑 ==========

TransparentProxyConfigService::disable_transparent_proxy(&tool, &config)
.map_err(|e| format!("恢复配置失败: {e}"))?;
}
}
let original_profile = tool_config.original_active_profile.take();

if let Some(profile_name) = original_profile {
// 有原始 Profile,切回去
profile_mgr
.activate_profile(&tool_id, &profile_name)
.map_err(|e| format!("还原 Profile 失败: {}", e))?;

Ok(format!("✅ {tool_id} 透明代理已停止\n配置已恢复"))
tracing::info!(
tool_id = %tool_id,
restored_profile = %profile_name,
"已还原到原始 Profile"
);

// 清空 original_active_profile 字段
proxy_config_mgr
.update_config(&tool_id, tool_config)
.map_err(|e| e.to_string())?;

Ok(format!(
"✅ {tool_id} 透明代理已停止\n已还原到 Profile: {profile_name}"
))
} else {
// 没有原始 Profile(启动代理前用户就没激活任何 Profile)
// 按需求:不做任何操作,保持当前状态
tracing::info!(
tool_id = %tool_id,
"启动代理前无激活 Profile,保持当前状态"
);

Ok(format!("✅ {tool_id} 透明代理已停止"))
}
}

/// 获取所有工具的透明代理状态
Expand Down Expand Up @@ -531,11 +607,72 @@ pub async fn get_proxy_config(
pub async fn update_proxy_config(
tool_id: String,
config: ::duckcoding::models::proxy_config::ToolProxyConfig,
manager_state: State<'_, ProxyManagerState>,
) -> Result<(), String> {
use ::duckcoding::services::profile_manager::ProfileManager;

// ========== 运行时保护检查 ==========
if manager_state.manager.is_running(&tool_id).await {
return Err(format!("{} 代理正在运行,请先停止代理再修改配置", tool_id));
}

// ========== 更新配置到全局配置文件 ==========
let proxy_mgr = ProxyConfigManager::new().map_err(|e| e.to_string())?;
proxy_mgr
.update_config(&tool_id, config)
.map_err(|e| e.to_string())
.update_config(&tool_id, config.clone())
.map_err(|e| e.to_string())?;

// ========== 同步创建/更新内置 Profile ==========

// 只有在配置完整时才创建内置 Profile
if config.enabled
&& config.local_api_key.is_some()
&& config.real_api_key.is_some()
&& config.real_base_url.is_some()
{
let profile_mgr = ProfileManager::new().map_err(|e| e.to_string())?;
let proxy_profile_name = format!("dc_proxy_{}", tool_id.replace("-", "_"));
let proxy_endpoint = format!("http://127.0.0.1:{}", config.port);
let proxy_key = config.local_api_key.unwrap();

match tool_id.as_str() {
"claude-code" => {
profile_mgr
.save_claude_profile_internal(&proxy_profile_name, proxy_key, proxy_endpoint)
.map_err(|e| format!("同步内置 Profile 失败: {}", e))?;
}
"codex" => {
profile_mgr
.save_codex_profile_internal(
&proxy_profile_name,
proxy_key,
proxy_endpoint,
Some("responses".to_string()),
)
.map_err(|e| format!("同步内置 Profile 失败: {}", e))?;
}
"gemini-cli" => {
profile_mgr
.save_gemini_profile_internal(
&proxy_profile_name,
proxy_key,
proxy_endpoint,
None, // 不设置 model,保留用户原有配置
)
.map_err(|e| format!("同步内置 Profile 失败: {}", e))?;
}
_ => return Err(format!("不支持的工具: {}", tool_id)),
}

tracing::info!(
tool_id = %tool_id,
proxy_profile = %proxy_profile_name,
port = config.port,
"已同步更新内置 Profile"
);
}

Ok(())
}

/// 获取所有工具的代理配置
Expand Down
66 changes: 66 additions & 0 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,67 @@ fn hide_window_to_tray<R: Runtime>(window: &WebviewWindow<R>) {
}
}

/// 初始化内置 Profile(用于透明代理配置切换)
fn initialize_proxy_profiles() -> Result<(), Box<dyn std::error::Error>> {
use duckcoding::services::profile_manager::ProfileManager;
use duckcoding::services::proxy_config_manager::ProxyConfigManager;

let proxy_mgr = ProxyConfigManager::new()?;
let profile_mgr = ProfileManager::new()?;

for tool_id in &["claude-code", "codex", "gemini-cli"] {
if let Ok(Some(config)) = proxy_mgr.get_config(tool_id) {
// 只有在配置完整时才创建内置 Profile
if config.enabled
&& config.local_api_key.is_some()
&& config.real_api_key.is_some()
&& config.real_base_url.is_some()
{
let proxy_profile_name = format!("dc_proxy_{}", tool_id.replace("-", "_"));
let proxy_endpoint = format!("http://127.0.0.1:{}", config.port);
let proxy_key = config.local_api_key.unwrap();

let result = match *tool_id {
"claude-code" => profile_mgr.save_claude_profile_internal(
&proxy_profile_name,
proxy_key,
proxy_endpoint,
),
"codex" => profile_mgr.save_codex_profile_internal(
&proxy_profile_name,
proxy_key,
proxy_endpoint,
Some("responses".to_string()),
),
"gemini-cli" => profile_mgr.save_gemini_profile_internal(
&proxy_profile_name,
proxy_key,
proxy_endpoint,
None, // 不设置 model,保留用户原有配置
),
_ => continue,
};

if let Err(e) = result {
tracing::warn!(
tool_id = tool_id,
error = ?e,
"初始化内置 Profile 失败"
);
} else {
tracing::debug!(
tool_id = tool_id,
profile = %proxy_profile_name,
"已初始化内置 Profile"
);
}
}
}
}

Ok(())
}

fn main() {
// 🆕 初始化日志系统(必须在最前面)
use duckcoding::core::init_logger;
Expand All @@ -156,6 +217,11 @@ fn main() {

tracing::info!("DuckCoding 应用启动");

// 🆕 初始化内置 Profile(用于透明代理)
if let Err(e) = initialize_proxy_profiles() {
tracing::warn!(error = ?e, "初始化内置 Profile 失败");
}

// 创建透明代理服务实例(旧架构,保持兼容)
let transparent_proxy_port = 8787; // 默认端口,实际会从配置读取
let transparent_proxy_service = TransparentProxyService::new(transparent_proxy_port);
Expand Down
Loading
Loading