Skip to content

Commit d41939e

Browse files
committed
feat: add Gemini CLI OAuth provider
- Add GeminiProvider with OAuth token refresh support - Add Gemini credential management commands - Update Providers page with tabs for Kiro/Gemini - Auto-detect credential file changes for both providers - Add Gemini models to /v1/models endpoint - OAuth credentials must be set via GEMINI_OAUTH_CLIENT_ID/SECRET env vars
1 parent 6d53ccc commit d41939e

5 files changed

Lines changed: 518 additions & 1 deletion

File tree

src-tauri/src/lib.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,149 @@ struct CheckResult {
240240
reloaded: bool,
241241
}
242242

243+
// ============ Gemini Provider Commands ============
244+
245+
#[derive(serde::Serialize)]
246+
struct GeminiCredentialStatus {
247+
loaded: bool,
248+
has_access_token: bool,
249+
has_refresh_token: bool,
250+
expiry_date: Option<i64>,
251+
is_valid: bool,
252+
creds_path: String,
253+
}
254+
255+
#[tauri::command]
256+
async fn get_gemini_credentials(state: tauri::State<'_, AppState>) -> Result<GeminiCredentialStatus, String> {
257+
let s = state.read().await;
258+
let creds = &s.gemini_provider.credentials;
259+
let path = providers::gemini::GeminiProvider::default_creds_path();
260+
261+
Ok(GeminiCredentialStatus {
262+
loaded: creds.access_token.is_some() || creds.refresh_token.is_some(),
263+
has_access_token: creds.access_token.is_some(),
264+
has_refresh_token: creds.refresh_token.is_some(),
265+
expiry_date: creds.expiry_date,
266+
is_valid: s.gemini_provider.is_token_valid(),
267+
creds_path: path.to_string_lossy().to_string(),
268+
})
269+
}
270+
271+
#[tauri::command]
272+
async fn reload_gemini_credentials(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result<String, String> {
273+
let mut s = state.write().await;
274+
logs.write().await.add("info", "[Gemini] 正在加载凭证...");
275+
s.gemini_provider.load_credentials().map_err(|e| e.to_string())?;
276+
logs.write().await.add("info", "[Gemini] 凭证加载成功");
277+
Ok("Gemini credentials reloaded".to_string())
278+
}
279+
280+
#[tauri::command]
281+
async fn refresh_gemini_token(state: tauri::State<'_, AppState>, logs: tauri::State<'_, LogState>) -> Result<String, String> {
282+
let mut s = state.write().await;
283+
logs.write().await.add("info", "[Gemini] 正在刷新 Token...");
284+
let result = s.gemini_provider.refresh_token().await.map_err(|e| e.to_string());
285+
match &result {
286+
Ok(_) => logs.write().await.add("info", "[Gemini] Token 刷新成功"),
287+
Err(e) => logs.write().await.add("error", &format!("[Gemini] Token 刷新失败: {}", e)),
288+
}
289+
result
290+
}
291+
292+
#[tauri::command]
293+
async fn get_gemini_env_variables(state: tauri::State<'_, AppState>) -> Result<Vec<EnvVariable>, String> {
294+
let s = state.read().await;
295+
let creds = &s.gemini_provider.credentials;
296+
let mut vars = Vec::new();
297+
298+
if let Some(token) = &creds.access_token {
299+
vars.push(EnvVariable {
300+
key: "GEMINI_ACCESS_TOKEN".to_string(),
301+
value: token.clone(),
302+
masked: mask_token(token),
303+
});
304+
}
305+
if let Some(token) = &creds.refresh_token {
306+
vars.push(EnvVariable {
307+
key: "GEMINI_REFRESH_TOKEN".to_string(),
308+
value: token.clone(),
309+
masked: mask_token(token),
310+
});
311+
}
312+
if let Some(expiry) = creds.expiry_date {
313+
let expiry_str = expiry.to_string();
314+
vars.push(EnvVariable {
315+
key: "GEMINI_EXPIRY_DATE".to_string(),
316+
value: expiry_str.clone(),
317+
masked: expiry_str,
318+
});
319+
}
320+
321+
Ok(vars)
322+
}
323+
324+
#[tauri::command]
325+
async fn get_gemini_token_file_hash() -> Result<String, String> {
326+
let path = providers::gemini::GeminiProvider::default_creds_path();
327+
if !path.exists() {
328+
return Ok("".to_string());
329+
}
330+
331+
let content = std::fs::read(&path).map_err(|e| e.to_string())?;
332+
let hash = format!("{:x}", md5::compute(&content));
333+
Ok(hash)
334+
}
335+
336+
#[tauri::command]
337+
async fn check_and_reload_gemini_credentials(
338+
state: tauri::State<'_, AppState>,
339+
logs: tauri::State<'_, LogState>,
340+
last_hash: String,
341+
) -> Result<CheckResult, String> {
342+
let path = providers::gemini::GeminiProvider::default_creds_path();
343+
344+
if !path.exists() {
345+
return Ok(CheckResult {
346+
changed: false,
347+
new_hash: "".to_string(),
348+
reloaded: false,
349+
});
350+
}
351+
352+
let content = std::fs::read(&path).map_err(|e| e.to_string())?;
353+
let new_hash = format!("{:x}", md5::compute(&content));
354+
355+
if !last_hash.is_empty() && new_hash != last_hash {
356+
logs.write().await.add("info", "[Gemini][自动检测] 凭证文件已变化,正在重新加载...");
357+
358+
let mut s = state.write().await;
359+
match s.gemini_provider.load_credentials() {
360+
Ok(_) => {
361+
logs.write().await.add("info", "[Gemini][自动检测] 凭证重新加载成功");
362+
Ok(CheckResult {
363+
changed: true,
364+
new_hash,
365+
reloaded: true,
366+
})
367+
}
368+
Err(e) => {
369+
logs.write().await.add("error", &format!("[Gemini][自动检测] 凭证重新加载失败: {}", e));
370+
Ok(CheckResult {
371+
changed: true,
372+
new_hash,
373+
reloaded: false,
374+
})
375+
}
376+
}
377+
} else {
378+
Ok(CheckResult {
379+
changed: false,
380+
new_hash,
381+
reloaded: false,
382+
})
383+
}
384+
}
385+
243386
#[tauri::command]
244387
async fn get_logs(logs: tauri::State<'_, LogState>) -> Result<Vec<logger::LogEntry>, String> {
245388
Ok(logs.read().await.get_logs())
@@ -324,12 +467,21 @@ pub fn run() {
324467
get_server_status,
325468
get_config,
326469
save_config,
470+
// Kiro commands
327471
refresh_kiro_token,
328472
reload_credentials,
329473
get_kiro_credentials,
330474
get_env_variables,
331475
get_token_file_hash,
332476
check_and_reload_credentials,
477+
// Gemini commands
478+
get_gemini_credentials,
479+
reload_gemini_credentials,
480+
refresh_gemini_token,
481+
get_gemini_env_variables,
482+
get_gemini_token_file_hash,
483+
check_and_reload_gemini_credentials,
484+
// Common
333485
get_logs,
334486
clear_logs,
335487
test_api,

0 commit comments

Comments
 (0)