1use clap::Subcommand;
2
3use crate::app_config::AppType;
4use crate::cli::ui::{highlight, info, success};
5use crate::error::AppError;
6use crate::{AppState, ProxyConfig};
7
8#[derive(Subcommand, Debug, Clone)]
9pub enum ProxyCommand {
10 Show,
12
13 Enable,
15
16 Disable,
18
19 Serve {
21 #[arg(long)]
23 listen_address: Option<String>,
24
25 #[arg(long)]
27 listen_port: Option<u16>,
28
29 #[arg(long = "takeover", value_enum)]
31 takeovers: Vec<AppType>,
32 },
33}
34
35pub fn execute(cmd: ProxyCommand) -> Result<(), AppError> {
36 match cmd {
37 ProxyCommand::Show => show_proxy(),
38 ProxyCommand::Enable => set_proxy_enabled(true),
39 ProxyCommand::Disable => set_proxy_enabled(false),
40 ProxyCommand::Serve {
41 listen_address,
42 listen_port,
43 takeovers,
44 } => serve_proxy(listen_address, listen_port, takeovers),
45 }
46}
47
48fn get_state() -> Result<AppState, AppError> {
49 AppState::try_new()
50}
51
52fn create_runtime() -> Result<tokio::runtime::Runtime, AppError> {
53 tokio::runtime::Builder::new_current_thread()
54 .enable_all()
55 .build()
56 .map_err(|e| AppError::Message(format!("failed to create async runtime: {e}")))
57}
58
59fn show_proxy() -> Result<(), AppError> {
60 let state = get_state()?;
61 let runtime = create_runtime()?;
62 let global = runtime.block_on(state.proxy_service.get_global_config())?;
63 let config = runtime.block_on(state.proxy_service.get_config())?;
64 let status = runtime.block_on(state.proxy_service.get_status());
65 let takeovers = runtime
66 .block_on(state.proxy_service.get_takeover_status())
67 .map_err(AppError::Message)?;
68
69 println!("{}", highlight(crate::t!("Local Proxy", "本地代理")));
70 for line in build_proxy_overview_lines(&state, &global, &config, &status, &takeovers) {
71 println!("{line}");
72 }
73
74 Ok(())
75}
76
77fn set_proxy_enabled(enabled: bool) -> Result<(), AppError> {
78 let state = get_state()?;
79 let runtime = create_runtime()?;
80 let config = runtime.block_on(state.proxy_service.set_global_enabled(enabled))?;
81
82 println!(
83 "{}",
84 success(&format!(
85 "{}: {}",
86 crate::t!("Proxy switch", "代理开关"),
87 if config.proxy_enabled {
88 crate::t!("enabled", "开启")
89 } else {
90 crate::t!("disabled", "关闭")
91 }
92 ))
93 );
94
95 Ok(())
96}
97
98fn serve_proxy(
99 listen_address: Option<String>,
100 listen_port: Option<u16>,
101 takeovers: Vec<AppType>,
102) -> Result<(), AppError> {
103 let state = get_state()?;
104 let runtime = create_runtime()?;
105
106 runtime.block_on(async move {
107 let service = state.proxy_service.clone();
108 let base_config = service.get_config().await?;
109 let effective_config = apply_overrides(&base_config, listen_address, listen_port);
110
111 let result = async {
112 let server_info = service
113 .start_with_runtime_config(effective_config)
114 .await
115 .map_err(AppError::Message)?;
116
117 if let Err(err) = apply_takeovers(&service, &takeovers).await {
118 let _ = service.stop_with_restore().await;
119 return Err(AppError::Message(err));
120 }
121
122 if let Err(err) = service.publish_runtime_session_if_needed(&server_info) {
123 let _ = service.stop_with_restore().await;
124 return Err(AppError::Message(err));
125 }
126 crate::services::state_coordination::clear_restore_mutation_guard_bypass_env();
127
128 println!("{}", highlight(crate::t!("Local Proxy Running", "本地代理已启动")));
129 println!(
130 "{}",
131 success(&format!(
132 "{} http://{}:{}",
133 crate::t!("Listening on", "监听地址"),
134 server_info.address,
135 server_info.port
136 ))
137 );
138 println!(
139 "{}",
140 info(crate::t!(
141 "Claude: /v1/messages · Codex: /v1/chat/completions + /v1/responses · Gemini: /v1beta/*",
142 "Claude: /v1/messages · Codex: /v1/chat/completions + /v1/responses · Gemini: /v1beta/*"
143 ))
144 );
145 if !takeovers.is_empty() {
146 println!(
147 "{}",
148 success(&format!(
149 "{} {}",
150 crate::t!("Manual takeover enabled for:", "已为以下应用开启手动接管:"),
151 takeovers
152 .iter()
153 .map(AppType::as_str)
154 .collect::<Vec<_>>()
155 .join(", ")
156 ))
157 );
158 }
159 for line in build_auto_failover_status_lines(&state) {
160 println!("{}", info(&line));
161 }
162 println!(
163 "{}",
164 info(crate::t!(
165 "Press Ctrl-C to stop the proxy.",
166 "按 Ctrl-C 停止代理。"
167 ))
168 );
169
170 tokio::signal::ctrl_c()
171 .await
172 .map_err(|e| AppError::Message(format!("failed to listen for Ctrl-C: {e}")))?;
173
174 service
175 .stop_with_restore()
176 .await
177 .map_err(AppError::Message)?;
178 println!(
179 "{}",
180 success(crate::t!("✓ Proxy stopped.", "✓ 代理已停止。"))
181 );
182
183 Ok(())
184 }
185 .await;
186
187 result
188 })
189}
190
191async fn apply_takeovers(
192 service: &crate::ProxyService,
193 takeovers: &[AppType],
194) -> Result<(), String> {
195 for app in takeovers {
196 match app {
197 AppType::Claude | AppType::Codex | AppType::Gemini => {
198 service.set_takeover_for_app(app.as_str(), true).await?;
199 }
200 _ => {
201 return Err(format!(
202 "proxy takeover is not supported for {}",
203 app.as_str()
204 ));
205 }
206 }
207 }
208
209 Ok(())
210}
211
212fn apply_overrides(
213 original: &ProxyConfig,
214 listen_address: Option<String>,
215 listen_port: Option<u16>,
216) -> ProxyConfig {
217 let mut config = original.clone();
218 if let Some(address) = listen_address {
219 config.listen_address = address;
220 }
221 if let Some(port) = listen_port {
222 config.listen_port = port;
223 }
224 config
225}
226
227fn build_proxy_overview_lines(
228 state: &AppState,
229 global: &crate::proxy::types::GlobalProxyConfig,
230 config: &ProxyConfig,
231 status: &crate::ProxyStatus,
232 takeovers: &crate::proxy::types::ProxyTakeoverStatus,
233) -> Vec<String> {
234 let current_providers = AppType::all()
235 .map(|app| {
236 let current = state
237 .db
238 .get_current_provider(app.as_str())
239 .unwrap_or(None)
240 .filter(|id| !id.trim().is_empty())
241 .unwrap_or_else(|| crate::t!("(not set)", "(未设置)").to_string());
242 format!("- {}: {}", app.as_str(), current)
243 })
244 .collect::<Vec<_>>();
245
246 let listen_host = if status.running && !status.address.is_empty() {
247 status.address.as_str()
248 } else {
249 config.listen_address.as_str()
250 };
251 let listen_port = if status.running && status.port > 0 {
252 status.port
253 } else {
254 config.listen_port
255 };
256
257 let mut lines = vec![
258 format!(
259 "{}: {}",
260 crate::t!("Running", "运行中"),
261 if status.running {
262 crate::t!("yes", "是")
263 } else {
264 crate::t!("no", "否")
265 }
266 ),
267 format!(
268 "{}: {}",
269 crate::t!("Enabled", "启用状态"),
270 if global.proxy_enabled {
271 crate::t!("enabled", "开启")
272 } else {
273 crate::t!("disabled", "关闭")
274 }
275 ),
276 format!(
277 "{}: {}:{}",
278 crate::t!("Listen", "监听"),
279 listen_host,
280 listen_port
281 ),
282 crate::t!(
283 "Mode: local proxy (manual takeover and automatic failover follow app settings)",
284 "模式:本地代理(手动接管和自动故障转移遵循各应用配置)"
285 )
286 .to_string(),
287 format!(
288 "{}: {}",
289 crate::t!("Retries", "重试次数"),
290 config.max_retries
291 ),
292 format!(
293 "{}: {}",
294 crate::t!("Logging", "日志"),
295 if config.enable_logging {
296 crate::t!("enabled", "开启")
297 } else {
298 crate::t!("disabled", "关闭")
299 }
300 ),
301 format!(
302 "{}: {}s / {}s / {}s",
303 crate::t!(
304 "Timeouts (first-byte / idle / non-stream)",
305 "超时(首字 / 空闲 / 非流式)"
306 ),
307 config.streaming_first_byte_timeout,
308 config.streaming_idle_timeout,
309 config.non_streaming_timeout
310 ),
311 String::new(),
312 crate::t!("Takeovers:", "接管状态:").to_string(),
313 format!(
314 "- Claude: {}",
315 if takeovers.claude {
316 crate::t!("takeover on", "已接管")
317 } else {
318 crate::t!("takeover off", "未接管")
319 }
320 ),
321 format!(
322 "- Codex: {}",
323 if takeovers.codex {
324 crate::t!("takeover on", "已接管")
325 } else {
326 crate::t!("takeover off", "未接管")
327 }
328 ),
329 format!(
330 "- Gemini: {}",
331 if takeovers.gemini {
332 crate::t!("takeover on", "已接管")
333 } else {
334 crate::t!("takeover off", "未接管")
335 }
336 ),
337 String::new(),
338 crate::t!("Auto failover:", "自动故障转移:").to_string(),
339 ];
340 lines.extend(build_auto_failover_status_lines(state));
341 lines.extend([
342 String::new(),
343 crate::t!("Current providers:", "当前供应商:").to_string(),
344 ]);
345 lines.extend(current_providers);
346 lines.extend([
347 String::new(),
348 crate::t!("Routes:", "路由:").to_string(),
349 "- Claude: /v1/messages, /claude/v1/messages".to_string(),
350 "- Codex: /chat/completions, /v1/chat/completions, /responses, /v1/responses".to_string(),
351 "- Gemini: /v1beta/*, /gemini/v1beta/*".to_string(),
352 String::new(),
353 crate::t!(
354 "Issue #49 manual Claude setup:",
355 "Issue #49 的 Claude 手动接线:"
356 )
357 .to_string(),
358 format!(
359 "- ANTHROPIC_BASE_URL=http://{}:{}",
360 listen_host, listen_port
361 ),
362 "- ANTHROPIC_AUTH_TOKEN=proxy-placeholder".to_string(),
363 crate::t!(
364 "- Keep the real upstream base URL and API key in the selected Claude provider inside cc-switch.",
365 "- 真实上游 base URL 和 API key 仍保存在 cc-switch 里选中的 Claude provider 中。"
366 )
367 .to_string(),
368 String::new(),
369 crate::t!(
370 "Manual takeover is controlled with --takeover; automatic failover uses each app's proxy settings and failover queue.",
371 "手动接管通过 --takeover 控制;自动故障转移使用各应用的代理配置和故障转移队列。"
372 )
373 .to_string(),
374 String::new(),
375 format!(
376 "{}: cc-switch proxy serve --listen-address {} --listen-port {}",
377 crate::t!("Debug command", "调试命令"),
378 config.listen_address,
379 config.listen_port
380 ),
381 format!(
382 "{}: cc-switch proxy serve --takeover claude",
383 crate::t!("Takeover command", "接管命令")
384 ),
385 ]);
386
387 lines
388}
389
390fn build_auto_failover_status_lines(state: &AppState) -> Vec<String> {
391 [
392 (AppType::Claude, "Claude"),
393 (AppType::Codex, "Codex"),
394 (AppType::Gemini, "Gemini"),
395 ]
396 .into_iter()
397 .map(|(app, label)| {
398 let (_, auto_failover_enabled) = state.db.get_proxy_flags_sync(app.as_str());
399 format!(
400 "- {}: {}",
401 label,
402 if auto_failover_enabled {
403 crate::t!("auto failover on", "自动故障转移开启")
404 } else {
405 crate::t!("auto failover off", "自动故障转移关闭")
406 }
407 )
408 })
409 .collect()
410}
411
412#[cfg(test)]
413mod tests {
414 use std::sync::{Arc, RwLock};
415
416 use crate::{
417 proxy::types::{GlobalProxyConfig, ProxyStatus, ProxyTakeoverStatus},
418 Database, MultiAppConfig, ProxyService,
419 };
420
421 use super::build_proxy_overview_lines;
422
423 #[test]
424 fn proxy_overview_lines_include_runtime_status_and_takeover_state() {
425 let db = Arc::new(Database::memory().expect("create database"));
426 let state = crate::AppState {
427 db: db.clone(),
428 config: RwLock::new(MultiAppConfig::default()),
429 proxy_service: ProxyService::new(db),
430 };
431 let global = GlobalProxyConfig {
432 proxy_enabled: true,
433 listen_address: "127.0.0.1".to_string(),
434 listen_port: 15721,
435 enable_logging: true,
436 };
437 let config = crate::ProxyConfig::default();
438 let status = ProxyStatus {
439 running: true,
440 address: "127.0.0.1".to_string(),
441 port: 24567,
442 ..Default::default()
443 };
444 let takeover = ProxyTakeoverStatus {
445 claude: true,
446 codex: false,
447 gemini: true,
448 };
449
450 let lines = build_proxy_overview_lines(&state, &global, &config, &status, &takeover);
451 let output = lines.join("\n");
452
453 assert!(
454 output.contains("Running: yes") || output.contains("运行中: 是"),
455 "proxy show output should include foreground runtime status"
456 );
457 assert!(
458 output.contains("127.0.0.1:24567"),
459 "proxy show output should prefer the active runtime listen address when the proxy is running"
460 );
461 assert!(
462 output.contains("Claude: takeover on") || output.contains("Claude: 已接管"),
463 "proxy show output should include Claude manual takeover state"
464 );
465 assert!(
466 output.contains("Gemini: takeover on") || output.contains("Gemini: 已接管"),
467 "proxy show output should include Gemini manual takeover state"
468 );
469 }
470
471 #[test]
472 fn proxy_overview_lines_report_configured_auto_failover_state() {
473 let db = Arc::new(Database::memory().expect("create database"));
474 db.set_proxy_flags_sync("codex", false, true)
475 .expect("enable codex auto failover");
476 let state = crate::AppState {
477 db: db.clone(),
478 config: RwLock::new(MultiAppConfig::default()),
479 proxy_service: ProxyService::new(db),
480 };
481 let global = GlobalProxyConfig {
482 proxy_enabled: true,
483 listen_address: "127.0.0.1".to_string(),
484 listen_port: 15721,
485 enable_logging: true,
486 };
487 let config = crate::ProxyConfig::default();
488 let status = ProxyStatus::default();
489 let takeover = ProxyTakeoverStatus::default();
490
491 let lines = build_proxy_overview_lines(&state, &global, &config, &status, &takeover);
492 let output = lines.join("\n");
493
494 assert!(
495 output.contains("Codex: auto failover on")
496 || output.contains("Codex: 自动故障转移开启"),
497 "proxy show output should reflect app-specific auto failover settings"
498 );
499 assert!(
500 !output.contains("automatic failover disabled"),
501 "proxy show output should not hard-code automatic failover as disabled"
502 );
503 }
504}