v0.4.3: fix IdC token refresh with form-urlencoded format, add debug logging

This commit is contained in:
coso
2025-12-14 14:32:29 +08:00
parent 3fac9f75ca
commit cf0bf919c2
19 changed files with 454 additions and 24 deletions
+2 -1
View File
@@ -7,7 +7,8 @@
"Bash(cargo test:*)",
"Bash(cargo build:*)",
"Bash(npm run check:*)",
"Bash(npm run:*)"
"Bash(npm run:*)",
"Bash(tree:*)"
],
"deny": [],
"ask": []
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "proxycast",
"private": true,
"version": "0.4.2",
"version": "0.4.3",
"type": "module",
"scripts": {
"dev": "vite",
+76 -1
View File
@@ -8,6 +8,18 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
@@ -810,6 +822,18 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fastrand"
version = "2.3.0"
@@ -1332,12 +1356,30 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "hashlink"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "heck"
version = "0.4.1"
@@ -1865,6 +1907,17 @@ dependencies = [
"libc",
]
[[package]]
name = "libsqlite3-sys"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@@ -2789,7 +2842,7 @@ dependencies = [
[[package]]
name = "proxycast"
version = "0.4.1"
version = "0.4.3"
dependencies = [
"async-stream",
"axum",
@@ -2799,6 +2852,7 @@ dependencies = [
"md5",
"regex",
"reqwest",
"rusqlite",
"serde",
"serde_json",
"tauri",
@@ -2807,6 +2861,7 @@ dependencies = [
"tokio",
"tracing",
"tracing-subscriber",
"urlencoding",
"uuid",
]
@@ -3058,6 +3113,20 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rusqlite"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
dependencies = [
"bitflags 2.10.0",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -4513,6 +4582,12 @@ dependencies = [
"serde",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "urlpattern"
version = "0.3.0"
+3 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "proxycast"
version = "0.4.2"
version = "0.4.3"
description = "AI API Proxy Desktop App"
authors = ["you"]
edition = "2021"
@@ -29,6 +29,8 @@ futures = "0.3"
async-stream = "0.3"
regex = "1"
md5 = "0.7"
urlencoding = "2"
rusqlite = { version = "0.31", features = ["bundled"] }
[features]
default = ["custom-protocol"]
+2
View File
@@ -0,0 +1,2 @@
// MCP Servers DAO - 数据访问对象
// TODO: 实现 mcp_servers 表的 CRUD 操作
+3
View File
@@ -0,0 +1,3 @@
pub mod mcp;
pub mod prompts;
pub mod providers;
+2
View File
@@ -0,0 +1,2 @@
// Prompts DAO - 数据访问对象
// TODO: 实现 prompts 表的 CRUD 操作
+2
View File
@@ -0,0 +1,2 @@
// Providers DAO - 数据访问对象
// TODO: 实现 providers 表的 CRUD 操作
+46
View File
@@ -0,0 +1,46 @@
use rusqlite::Connection;
use serde_json::Value;
use std::path::PathBuf;
/// 从旧的 JSON 配置迁移数据到 SQLite
pub fn migrate_from_json(
conn: &Connection,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// 检查是否已经迁移过
let migrated: bool = conn
.query_row(
"SELECT value FROM settings WHERE key = 'migrated_from_json'",
[],
|row| row.get::<_, String>(0),
)
.map(|v| v == "true")
.unwrap_or(false);
if migrated {
return Ok(());
}
// 读取旧配置文件
let home = dirs::home_dir().ok_or("Cannot find home directory")?;
let config_path = home.join(".proxycast").join("config.json");
if config_path.exists() {
let content = std::fs::read_to_string(&config_path)?;
let config: Value = serde_json::from_str(&content)?;
// TODO: 解析旧配置并插入到数据库
// 这里需要根据实际的旧配置格式来实现
// 备份旧配置
let backup_path = home.join(".proxycast").join("config.json.backup");
std::fs::copy(&config_path, &backup_path)?;
}
// 标记迁移完成
conn.execute(
"INSERT OR REPLACE INTO settings (key, value) VALUES ('migrated_from_json', 'true')",
[],
)?;
Ok(())
}
+28
View File
@@ -0,0 +1,28 @@
pub mod dao;
pub mod migration;
pub mod schema;
use rusqlite::Connection;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
pub type DbConnection = Arc<Mutex<Connection>>;
/// 获取数据库文件路径
pub fn get_db_path() -> PathBuf {
let home = dirs::home_dir().expect("Cannot find home directory");
let db_dir = home.join(".proxycast");
std::fs::create_dir_all(&db_dir).expect("Cannot create .proxycast directory");
db_dir.join("proxycast.db")
}
/// 初始化数据库连接
pub fn init_database() -> Result<DbConnection, rusqlite::Error> {
let db_path = get_db_path();
let conn = Connection::open(&db_path)?;
// 创建表结构
schema::create_tables(&conn)?;
Ok(Arc::new(Mutex::new(conn)))
}
+64
View File
@@ -0,0 +1,64 @@
use rusqlite::Connection;
pub fn create_tables(conn: &Connection) -> Result<(), rusqlite::Error> {
// Providers 表
conn.execute(
"CREATE TABLE IF NOT EXISTS providers (
id TEXT NOT NULL,
app_type TEXT NOT NULL,
name TEXT NOT NULL,
settings_config TEXT NOT NULL,
category TEXT,
icon TEXT,
icon_color TEXT,
notes TEXT,
created_at INTEGER,
sort_index INTEGER,
is_current INTEGER DEFAULT 0,
PRIMARY KEY (id, app_type)
)",
[],
)?;
// MCP 服务器表
conn.execute(
"CREATE TABLE IF NOT EXISTS mcp_servers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
server_config TEXT NOT NULL,
description TEXT,
enabled_proxycast INTEGER DEFAULT 0,
enabled_claude INTEGER DEFAULT 0,
enabled_codex INTEGER DEFAULT 0,
enabled_gemini INTEGER DEFAULT 0,
created_at INTEGER
)",
[],
)?;
// 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,
is_current INTEGER DEFAULT 0,
created_at INTEGER,
PRIMARY KEY (id, app_type)
)",
[],
)?;
// 设置表
conn.execute(
"CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)",
[],
)?;
Ok(())
}
+1
View File
@@ -1,5 +1,6 @@
mod config;
mod converter;
mod database;
mod logger;
mod models;
mod providers;
+41
View File
@@ -0,0 +1,41 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AppType {
ProxyCast,
Claude,
Codex,
Gemini,
}
impl AppType {
pub fn as_str(&self) -> &'static str {
match self {
AppType::ProxyCast => "proxycast",
AppType::Claude => "claude",
AppType::Codex => "codex",
AppType::Gemini => "gemini",
}
}
}
impl std::str::FromStr for AppType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"proxycast" => Ok(AppType::ProxyCast),
"claude" => Ok(AppType::Claude),
"codex" => Ok(AppType::Codex),
"gemini" => Ok(AppType::Gemini),
_ => Err(format!("Invalid app type: {}", s)),
}
}
}
impl std::fmt::Display for AppType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
+37
View File
@@ -0,0 +1,37 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServer {
pub id: String,
pub name: String,
pub server_config: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub enabled_proxycast: bool,
#[serde(default)]
pub enabled_claude: bool,
#[serde(default)]
pub enabled_codex: bool,
#[serde(default)]
pub enabled_gemini: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<i64>,
}
impl McpServer {
pub fn new(id: String, name: String, server_config: Value) -> Self {
Self {
id,
name,
server_config,
description: None,
enabled_proxycast: false,
enabled_claude: false,
enabled_codex: false,
enabled_gemini: false,
created_at: Some(chrono::Utc::now().timestamp()),
}
}
}
+8
View File
@@ -1,10 +1,18 @@
pub mod anthropic;
pub mod app_type;
pub mod codewhisperer;
pub mod mcp_model;
pub mod openai;
pub mod prompt_model;
pub mod provider_model;
#[allow(unused_imports)]
pub use anthropic::*;
pub use app_type::AppType;
#[allow(unused_imports)]
pub use codewhisperer::*;
pub use mcp_model::McpServer;
#[allow(unused_imports)]
pub use openai::*;
pub use prompt_model::Prompt;
pub use provider_model::Provider;
+29
View File
@@ -0,0 +1,29 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Prompt {
pub id: String,
pub app_type: String,
pub name: String,
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub is_current: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<i64>,
}
impl Prompt {
pub fn new(id: String, app_type: String, name: String, content: String) -> Self {
Self {
id,
app_type,
name,
content,
description: None,
is_current: false,
created_at: Some(chrono::Utc::now().timestamp()),
}
}
}
+42
View File
@@ -0,0 +1,42 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Provider {
pub id: String,
pub app_type: String,
pub name: String,
pub settings_config: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub icon_color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_index: Option<i32>,
#[serde(default)]
pub is_current: bool,
}
impl Provider {
pub fn new(id: String, app_type: String, name: String, settings_config: Value) -> Self {
Self {
id,
app_type,
name,
settings_config,
category: None,
icon: None,
icon_color: None,
notes: None,
created_at: Some(chrono::Utc::now().timestamp()),
sort_index: None,
is_current: false,
}
}
}
+66 -19
View File
@@ -102,6 +102,13 @@ impl KiroProvider {
if tokio::fs::try_exists(&path).await.unwrap_or(false) {
let content = tokio::fs::read_to_string(&path).await?;
let creds: KiroCredentials = serde_json::from_str(&content)?;
tracing::info!(
"[KIRO] Main file loaded: has_access={}, has_refresh={}, has_client_id={}, auth_method={:?}",
creds.access_token.is_some(),
creds.refresh_token.is_some(),
creds.client_id.is_some(),
creds.auth_method
);
merge_credentials(&mut merged, &creds);
}
@@ -114,6 +121,12 @@ impl KiroProvider {
{
if let Ok(content) = tokio::fs::read_to_string(&file_path).await {
if let Ok(creds) = serde_json::from_str::<KiroCredentials>(&content) {
tracing::info!(
"[KIRO] Extra file {:?}: has_client_id={}, has_client_secret={}",
file_path.file_name(),
creds.client_id.is_some(),
creds.client_secret.is_some()
);
merge_credentials(&mut merged, &creds);
}
}
@@ -121,6 +134,15 @@ impl KiroProvider {
}
}
tracing::info!(
"[KIRO] Final merged: has_access={}, has_refresh={}, has_client_id={}, has_client_secret={}, auth_method={:?}",
merged.access_token.is_some(),
merged.refresh_token.is_some(),
merged.client_id.is_some(),
merged.client_secret.is_some(),
merged.auth_method
);
self.credentials = merged;
Ok(())
}
@@ -161,25 +183,43 @@ impl KiroProvider {
.to_lowercase();
let refresh_url = self.get_refresh_url();
let body = if auth_method == "idc" {
serde_json::json!({
"refreshToken": refresh_token,
"clientId": self.credentials.client_id,
"clientSecret": self.credentials.client_secret,
"grantType": "refresh_token"
})
} else {
serde_json::json!({ "refreshToken": refresh_token })
};
let resp = if auth_method == "idc" {
// AWS OIDC endpoint requires form-urlencoded format
let client_id = self
.credentials
.client_id
.as_ref()
.ok_or("No client_id for IdC refresh")?;
let client_secret = self
.credentials
.client_secret
.as_ref()
.ok_or("No client_secret for IdC refresh")?;
let resp = self
.client
.post(&refresh_url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.json(&body)
.send()
.await?;
let form_body = format!(
"grant_type=refresh_token&client_id={}&client_secret={}&refresh_token={}",
urlencoding::encode(client_id),
urlencoding::encode(client_secret),
urlencoding::encode(refresh_token)
);
self.client
.post(&refresh_url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Accept", "application/json")
.body(form_body)
.send()
.await?
} else {
let body = serde_json::json!({ "refreshToken": refresh_token });
self.client
.post(&refresh_url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.json(&body)
.send()
.await?
};
if !resp.status().is_success() {
let status = resp.status();
@@ -188,13 +228,20 @@ impl KiroProvider {
}
let data: serde_json::Value = resp.json().await?;
// AWS OIDC returns snake_case, social endpoint returns camelCase
let new_token = data["accessToken"]
.as_str()
.or_else(|| data["access_token"].as_str())
.ok_or("No access token in response")?;
self.credentials.access_token = Some(new_token.to_string());
if let Some(rt) = data["refreshToken"].as_str() {
// Handle both camelCase and snake_case response formats
if let Some(rt) = data["refreshToken"]
.as_str()
.or_else(|| data["refresh_token"].as_str())
{
self.credentials.refresh_token = Some(rt.to_string());
}
if let Some(arn) = data["profileArn"].as_str() {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ProxyCast",
"version": "0.4.2",
"version": "0.4.3",
"identifier": "com.proxycast.app",
"build": {
"beforeDevCommand": "npm run dev",