diff --git a/package.json b/package.json index 4908778ac..42144b204 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.14.8", + "version": "0.14.10", "type": "module", "repository": { "type": "git", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f23c4b1af..e2c3e4c2a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3367,7 +3367,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.14.8" +version = "0.14.10" dependencies = [ "anyhow", "async-stream", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 05629bc3e..89c18974f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.14.8" +version = "0.14.10" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/commands/provider_pool_cmd.rs b/src-tauri/src/commands/provider_pool_cmd.rs index 10c73bffc..e28e6a1c4 100644 --- a/src-tauri/src/commands/provider_pool_cmd.rs +++ b/src-tauri/src/commands/provider_pool_cmd.rs @@ -1249,3 +1249,83 @@ pub async fn start_antigravity_oauth_login( Ok(credential) } + +/// 获取 Kiro 凭证的 Machine ID 指纹信息 +/// +/// 返回凭证的唯一设备指纹,用于在 UI 中展示 +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct KiroFingerprintInfo { + /// Machine ID(SHA256 哈希,64 字符) + pub machine_id: String, + /// Machine ID 的短格式(前 16 字符) + pub machine_id_short: String, + /// 指纹来源(profileArn / clientId / system) + pub source: String, + /// 认证方式 + pub auth_method: String, +} + +#[tauri::command] +pub async fn get_kiro_credential_fingerprint( + db: State<'_, DbConnection>, + uuid: String, +) -> Result { + use crate::database::dao::provider_pool::ProviderPoolDao; + use crate::providers::kiro::{generate_machine_id_from_credentials, KiroProvider}; + + // 获取凭证文件路径(在锁释放前完成) + let creds_file_path = { + let conn = db.lock().map_err(|e| e.to_string())?; + let credential = ProviderPoolDao::get_by_uuid(&conn, &uuid) + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("凭证不存在: {}", uuid))?; + + // 检查是否为 Kiro 凭证 + match &credential.credential { + CredentialData::KiroOAuth { creds_file_path } => creds_file_path.clone(), + _ => return Err("只有 Kiro 凭证支持获取指纹信息".to_string()), + } + }; // conn 在这里释放 + + // 加载凭证文件(异步操作,锁已释放) + let mut provider = KiroProvider::new(); + provider + .load_credentials_from_path(&creds_file_path) + .await + .map_err(|e| format!("加载凭证失败: {}", e))?; + + // 确定指纹来源 + let (source, profile_arn, client_id) = if provider.credentials.profile_arn.is_some() { + ( + "profileArn".to_string(), + provider.credentials.profile_arn.as_deref(), + None, + ) + } else if provider.credentials.client_id.is_some() { + ( + "clientId".to_string(), + None, + provider.credentials.client_id.as_deref(), + ) + } else { + ("system".to_string(), None, None) + }; + + // 生成 Machine ID + let machine_id = generate_machine_id_from_credentials(profile_arn, client_id); + let machine_id_short = machine_id[..16].to_string(); + + // 获取认证方式 + let auth_method = provider + .credentials + .auth_method + .clone() + .unwrap_or_else(|| "social".to_string()); + + Ok(KiroFingerprintInfo { + machine_id, + machine_id_short, + source, + auth_method, + }) +} diff --git a/src-tauri/src/converter/openai_to_cw.rs b/src-tauri/src/converter/openai_to_cw.rs index dac31c77d..9dcfab5ce 100644 --- a/src-tauri/src/converter/openai_to_cw.rs +++ b/src-tauri/src/converter/openai_to_cw.rs @@ -5,18 +5,31 @@ use std::collections::HashMap; use uuid::Uuid; /// 模型映射表 +/// +/// 参考 AIClient-2-API 的 provider-models.js 和 claude-kiro.js +/// 支持的模型列表: +/// - claude-opus-4-5, claude-opus-4-5-20251101 +/// - claude-haiku-4-5, claude-haiku-4-5-20251001 +/// - claude-sonnet-4-5, claude-sonnet-4-5-20250929 +/// - claude-sonnet-4-20250514 +/// - claude-3-7-sonnet-20250219, claude-3-5-sonnet-20241022, claude-3-5-sonnet-latest pub fn get_model_map() -> HashMap<&'static str, &'static str> { let mut map = HashMap::new(); + // Opus 4.5 系列 map.insert("claude-opus-4-5", "claude-opus-4.5"); map.insert("claude-opus-4-5-20251101", "claude-opus-4.5"); + // Haiku 4.5 系列 map.insert("claude-haiku-4-5", "claude-haiku-4.5"); map.insert("claude-haiku-4-5-20251001", "claude-haiku-4.5"); + // Sonnet 4.5 系列 map.insert("claude-sonnet-4-5", "CLAUDE_SONNET_4_5_20250929_V1_0"); map.insert( "claude-sonnet-4-5-20250929", "CLAUDE_SONNET_4_5_20250929_V1_0", ); + // Sonnet 4 系列 map.insert("claude-sonnet-4-20250514", "CLAUDE_SONNET_4_20250514_V1_0"); + // Sonnet 3.7/3.5 系列(兼容旧版本) map.insert( "claude-3-7-sonnet-20250219", "CLAUDE_3_7_SONNET_20250219_V1_0", @@ -32,6 +45,22 @@ pub fn get_model_map() -> HashMap<&'static str, &'static str> { map } +/// 获取 Kiro 支持的模型列表 +/// +/// 返回所有支持的模型 ID,用于前端展示和健康检查 +pub fn get_supported_models() -> Vec<&'static str> { + vec![ + "claude-opus-4-5", + "claude-opus-4-5-20251101", + "claude-haiku-4-5", + "claude-haiku-4-5-20251001", + "claude-sonnet-4-5", + "claude-sonnet-4-5-20250929", + "claude-sonnet-4-20250514", + "claude-3-7-sonnet-20250219", + ] +} + pub const DEFAULT_MODEL: &str = "CLAUDE_SONNET_4_5_20250929_V1_0"; /// 预处理消息:合并连续的 tool 消息到前一个 assistant 消息后的 user 消息 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a4494cd41..115ee6cbe 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1683,6 +1683,7 @@ pub fn run() { commands::provider_pool_cmd::migrate_private_config_to_pool, commands::provider_pool_cmd::start_antigravity_oauth_login, commands::provider_pool_cmd::get_antigravity_auth_url_and_wait, + commands::provider_pool_cmd::get_kiro_credential_fingerprint, // Route commands commands::route_cmd::get_available_routes, commands::route_cmd::get_route_curl_examples, diff --git a/src-tauri/src/providers/antigravity.rs b/src-tauri/src/providers/antigravity.rs index 842886e60..c81120c5e 100644 --- a/src-tauri/src/providers/antigravity.rs +++ b/src-tauri/src/providers/antigravity.rs @@ -38,9 +38,12 @@ const REFRESH_SKEW: i64 = 3000; pub const ANTIGRAVITY_MODELS: &[&str] = &[ "gemini-3-pro-preview", "gemini-3-pro-image-preview", + "gemini-3-flash-preview", + "gemini-2.5-flash", "gemini-2.5-computer-use-preview-10-2025", "gemini-claude-sonnet-4-5", "gemini-claude-sonnet-4-5-thinking", + "gemini-claude-opus-4-5-thinking", ]; /// 模型别名映射(用户友好名称 -> 内部名称) @@ -49,8 +52,11 @@ fn alias_to_model_name(model: &str) -> &str { "gemini-2.5-computer-use-preview-10-2025" => "rev19-uic3-1p", "gemini-3-pro-image-preview" => "gemini-3-pro-image", "gemini-3-pro-preview" => "gemini-3-pro-high", + "gemini-3-flash-preview" => "gemini-3-flash", + "gemini-2.5-flash" => "gemini-2.5-flash", "gemini-claude-sonnet-4-5" => "claude-sonnet-4-5", "gemini-claude-sonnet-4-5-thinking" => "claude-sonnet-4-5-thinking", + "gemini-claude-opus-4-5-thinking" => "claude-opus-4-5-thinking", _ => model, } } @@ -62,8 +68,11 @@ fn model_name_to_alias(model: &str) -> &str { "rev19-uic3-1p" => "gemini-2.5-computer-use-preview-10-2025", "gemini-3-pro-image" => "gemini-3-pro-image-preview", "gemini-3-pro-high" => "gemini-3-pro-preview", + "gemini-3-flash" => "gemini-3-flash-preview", + "gemini-2.5-flash" => "gemini-2.5-flash", "claude-sonnet-4-5" => "gemini-claude-sonnet-4-5", "claude-sonnet-4-5-thinking" => "gemini-claude-sonnet-4-5-thinking", + "claude-opus-4-5-thinking" => "gemini-claude-opus-4-5-thinking", _ => model, } } diff --git a/src-tauri/src/providers/kiro.rs b/src-tauri/src/providers/kiro.rs index 638f751ad..5d0ff7765 100644 --- a/src-tauri/src/providers/kiro.rs +++ b/src-tauri/src/providers/kiro.rs @@ -6,7 +6,73 @@ use serde::{Deserialize, Serialize}; use std::error::Error; use std::path::PathBuf; -/// 生成设备指纹 (Machine ID 的 SHA256) +/// 根据凭证信息生成唯一的 Machine ID(参考 AIClient-2-API 实现) +/// +/// 关键改进:每个凭证生成独立的 Machine ID,避免多账号共用同一指纹被检测 +/// 优先级:profileArn > clientId > 系统硬件 ID +/// +/// 这样每个 OAuth 凭证都有自己独立的指纹,模拟不同设备登录 +pub fn generate_machine_id_from_credentials( + profile_arn: Option<&str>, + client_id: Option<&str>, +) -> String { + use sha2::{Digest, Sha256}; + + // 优先使用凭证相关的唯一标识,确保每个账号有独立的 Machine ID + let unique_key = profile_arn + .filter(|s| !s.is_empty()) + .or(client_id.filter(|s| !s.is_empty())) + .map(|s| s.to_string()) + .unwrap_or_else(|| { + // 回退到系统硬件 ID + get_raw_machine_id().unwrap_or_else(|| "KIRO_DEFAULT_MACHINE".to_string()) + }); + + let mut hasher = Sha256::new(); + hasher.update(unique_key.as_bytes()); + let result = hasher.finalize(); + format!("{:x}", result) +} + +/// 获取系统运行时信息 +/// +/// 返回真实的操作系统名称和版本,用于构建更真实的 User-Agent +fn get_system_runtime_info() -> (String, String) { + let os_name = if cfg!(target_os = "macos") { + // macOS: 获取真实版本号 + let version = std::process::Command::new("sw_vers") + .arg("-productVersion") + .output() + .ok() + .and_then(|o| String::from_utf8(o.stdout).ok()) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|| "14.0".to_string()); + format!("macos#{}", version) + } else if cfg!(target_os = "linux") { + // Linux: 获取内核版本 + let version = std::process::Command::new("uname") + .arg("-r") + .output() + .ok() + .and_then(|o| String::from_utf8(o.stdout).ok()) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|| "5.15.0".to_string()); + format!("linux#{}", version) + } else if cfg!(target_os = "windows") { + // Windows: 使用固定版本(实际应该获取真实版本) + "windows#10.0".to_string() + } else { + "other#1.0".to_string() + }; + + // Node.js 版本模拟(Kiro IDE 使用 Electron,内置 Node.js) + // 使用常见的 LTS 版本 + let node_version = "20.18.0".to_string(); + + (os_name, node_version) +} + +/// 生成设备指纹 (Machine ID 的 SHA256) - 保留用于兼容 /// /// 与 Kiro IDE 保持一致的指纹生成方式(参考 Kir-Manager): /// - macOS: 使用 IOPlatformUUID(硬件级别唯一标识) @@ -657,7 +723,11 @@ impl KiroProvider { ); // 获取设备指纹和版本号(用于 Social 认证的 User-Agent) - let device_fp = get_device_fingerprint(); + // 使用基于凭证的 Machine ID,确保每个账号有独立的指纹 + let machine_id = generate_machine_id_from_credentials( + self.credentials.profile_arn.as_deref(), + self.credentials.client_id.as_deref(), + ); let kiro_version = get_kiro_version(); let resp = if auth_method == "idc" { @@ -690,11 +760,11 @@ impl KiroProvider { .header("Host", "oidc.us-east-1.amazonaws.com") .header( "x-amz-user-agent", - "aws-sdk-js/3.738.0 ua/2.1 os/other lang/js api/sso-oidc#3.738.0 m/E KiroIDE", + format!("aws-sdk-js/3.738.0 ua/2.1 os/other lang/js api/sso-oidc#3.738.0 m/E KiroIDE-{}-{}", kiro_version, machine_id), ) .header("User-Agent", "node") .header("Accept", "*/*") - .header("Connection", "keep-alive") + .header("Connection", "close") .json(&body) .send() .await? @@ -707,13 +777,14 @@ impl KiroProvider { .post(&refresh_url) .header( "User-Agent", - format!("KiroIDE-{}-{}", kiro_version, device_fp), + format!("KiroIDE-{}-{}", kiro_version, machine_id), ) .header("Accept", "application/json, text/plain, */*") .header("Accept-Encoding", "br, gzip, deflate") .header("Content-Type", "application/json") .header("Accept-Language", "*") .header("Sec-Fetch-Mode", "cors") + .header("Connection", "close") .json(&body) .send() .await? @@ -881,7 +952,7 @@ impl KiroProvider { None }; - let cw_request = convert_openai_to_codewhisperer(request, profile_arn); + let cw_request = convert_openai_to_codewhisperer(request, profile_arn.clone()); let url = self.get_base_url(); // Debug: 记录转换后的请求 @@ -923,9 +994,20 @@ impl KiroProvider { ); } - // 生成设备指纹用于伪装 Kiro IDE - let device_fp = get_device_fingerprint(); + // 生成基于凭证的唯一 Machine ID(关键改进:每个账号独立指纹) + let machine_id = generate_machine_id_from_credentials( + profile_arn.as_deref(), + self.credentials.client_id.as_deref(), + ); let kiro_version = get_kiro_version(); + let (os_name, node_version) = get_system_runtime_info(); + + tracing::debug!( + "[KIRO_FINGERPRINT] machine_id={} (based on profile_arn={}, client_id={})", + &machine_id[..16], + profile_arn.is_some(), + self.credentials.client_id.is_some() + ); let resp = self .client @@ -935,17 +1017,20 @@ impl KiroProvider { .header("Accept", "application/json") .header("amz-sdk-invocation-id", uuid::Uuid::new_v4().to_string()) .header("amz-sdk-request", "attempt=1; max=1") + .header("x-amzn-kiro-agent-mode", "vibe") + // 关键指纹头:使用基于凭证的唯一 Machine ID .header( "x-amz-user-agent", - format!("aws-sdk-js/1.0.7 KiroIDE-{kiro_version}-{device_fp}"), + format!("aws-sdk-js/1.0.0 KiroIDE-{kiro_version}-{machine_id}"), ) .header( "user-agent", format!( - "aws-sdk-js/1.0.7 ua/2.1 os/macos#14.0 lang/js md/nodejs#20.16.0 api/codewhispererstreaming#1.0.7 m/E KiroIDE-{kiro_version}-{device_fp}" + "aws-sdk-js/1.0.0 ua/2.1 os/{os_name} lang/js md/nodejs#{node_version} api/codewhispererruntime#1.0.0 m/E KiroIDE-{kiro_version}-{machine_id}" ), ) - .header("x-amzn-kiro-agent-mode", "vibe") + // 添加 Connection: close 避免连接复用被检测 + .header("Connection", "close") .json(&cw_request) .send() .await?; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7fb7d6dde..4267457c2 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ProxyCast", - "version": "0.14.8", + "version": "0.14.10", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/provider-pool/CredentialCard.tsx b/src/components/provider-pool/CredentialCard.tsx index edcd5b052..dc22b6521 100644 --- a/src/components/provider-pool/CredentialCard.tsx +++ b/src/components/provider-pool/CredentialCard.tsx @@ -17,11 +17,18 @@ import { Globe, BarChart3, ChevronUp, + Fingerprint, + Copy, + Check, } from "lucide-react"; import type { CredentialDisplay, CredentialSource, } from "@/lib/api/providerPool"; +import { + getKiroCredentialFingerprint, + type KiroFingerprintInfo, +} from "@/lib/api/providerPool"; import { usageApi, type UsageInfo } from "@/lib/api/usage"; import { UsageDisplay } from "./UsageDisplay"; @@ -59,6 +66,46 @@ export function CredentialCard({ const [usageInfo, setUsageInfo] = useState(null); const [usageError, setUsageError] = useState(null); + // 指纹信息状态(仅 Kiro 凭证) + const [fingerprintInfo, setFingerprintInfo] = + useState(null); + const [fingerprintLoading, setFingerprintLoading] = useState(false); + const [fingerprintExpanded, setFingerprintExpanded] = useState(false); + const [fingerprintCopied, setFingerprintCopied] = useState(false); + + // 查询指纹信息 + const handleCheckFingerprint = async () => { + if (fingerprintExpanded && fingerprintInfo) { + // 已展开且有数据,直接折叠 + setFingerprintExpanded(false); + return; + } + + setFingerprintExpanded(true); + setFingerprintLoading(true); + + try { + const info = await getKiroCredentialFingerprint(credential.uuid); + setFingerprintInfo(info); + } catch (e) { + console.error("获取指纹信息失败:", e); + } finally { + setFingerprintLoading(false); + } + }; + + // 复制 Machine ID + const handleCopyMachineId = async () => { + if (!fingerprintInfo) return; + try { + await navigator.clipboard.writeText(fingerprintInfo.machine_id); + setFingerprintCopied(true); + setTimeout(() => setFingerprintCopied(false), 2000); + } catch (e) { + console.error("复制失败:", e); + } + }; + // 查询用量 const handleCheckUsage = async () => { if (usageExpanded && usageInfo) { @@ -295,6 +342,24 @@ export function CredentialCard({ )} + {/* 指纹信息按钮 - 仅 Kiro 凭证显示 */} + {isKiroCredential && ( + + )} + {/* 用量查询按钮 - 仅 Kiro 凭证显示 */} {isKiroCredential && ( + + + {fingerprintLoading ? ( +
+
+ 加载中... +
+ ) : fingerprintInfo ? ( +
+
+ + Machine ID: + + + {fingerprintInfo.machine_id_short}... + + +
+
+ + 来源: + + {fingerprintInfo.source} + + + + 认证: + + {fingerprintInfo.auth_method} + + +
+
+ ) : ( +
+ 无法获取指纹信息 +
+ )} +
+ )} + {/* 用量信息展示区域 - 仅 Kiro 凭证 */} {isKiroCredential && usageExpanded && (
diff --git a/src/lib/api/providerPool.ts b/src/lib/api/providerPool.ts index 862288d8e..312347949 100644 --- a/src/lib/api/providerPool.ts +++ b/src/lib/api/providerPool.ts @@ -404,3 +404,22 @@ export interface MigrationResult { skipped_count: number; errors: string[]; } + +// Kiro 凭证指纹信息 +export interface KiroFingerprintInfo { + /** Machine ID(SHA256 哈希,64 字符) */ + machine_id: string; + /** Machine ID 的短格式(前 16 字符) */ + machine_id_short: string; + /** 指纹来源(profileArn / clientId / system) */ + source: string; + /** 认证方式 */ + auth_method: string; +} + +// 获取 Kiro 凭证的指纹信息 +export async function getKiroCredentialFingerprint( + uuid: string, +): Promise { + return invoke("get_kiro_credential_fingerprint", { uuid }); +}