mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
fix: 修复默认路由端点 provider_type 硬编码为 kiro 的问题 (#41)
- route_cmd.rs: get_available_routes 和 get_route_curl_examples 现在从配置读取 default_provider - provider_router.rs: 新增 with_default_provider 构造函数,支持动态默认 Provider - server/mod.rs: 路由列表 API 从 state.default_provider 读取默认 Provider Closes #41
This commit is contained in:
Generated
+2
-20
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "proxycast",
|
||||
"version": "0.15.0",
|
||||
"version": "0.17.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "proxycast",
|
||||
"version": "0.15.0",
|
||||
"version": "0.17.4",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
@@ -97,7 +97,6 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -2781,7 +2780,6 @@
|
||||
"integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -2799,7 +2797,6 @@
|
||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
@@ -2811,7 +2808,6 @@
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
@@ -2851,7 +2847,6 @@
|
||||
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.49.0",
|
||||
"@typescript-eslint/types": "8.49.0",
|
||||
@@ -3174,7 +3169,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3393,7 +3387,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -3741,7 +3734,6 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -4376,7 +4368,6 @@
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@@ -4855,7 +4846,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -5078,7 +5068,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -5091,7 +5080,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -5584,7 +5572,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -5660,7 +5647,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -5773,7 +5759,6 @@
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
@@ -5834,7 +5819,6 @@
|
||||
"integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.16",
|
||||
"@vitest/mocker": "4.0.16",
|
||||
@@ -6391,7 +6375,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -6405,7 +6388,6 @@
|
||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
||||
@@ -11,19 +11,20 @@ pub async fn get_available_routes(
|
||||
db: tauri::State<'_, DbConnection>,
|
||||
pool_service: tauri::State<'_, ProviderPoolServiceState>,
|
||||
) -> Result<RouteListResponse, String> {
|
||||
// 获取配置中的服务器地址
|
||||
// 获取配置中的服务器地址和默认 Provider
|
||||
let config = config::load_config().unwrap_or_default();
|
||||
let base_url = format!("http://{}:{}", config.server.host, config.server.port);
|
||||
let default_provider = config.default_provider.clone();
|
||||
|
||||
let routes = pool_service
|
||||
.0
|
||||
.get_available_routes(db.inner(), &base_url)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// 添加默认路由
|
||||
// 添加默认路由,使用配置中的默认 Provider
|
||||
let mut all_routes = vec![RouteInfo {
|
||||
selector: "default".to_string(),
|
||||
provider_type: "kiro".to_string(),
|
||||
provider_type: default_provider.clone(),
|
||||
credential_count: 1,
|
||||
endpoints: vec![
|
||||
crate::models::route_model::RouteEndpoint {
|
||||
@@ -44,7 +45,7 @@ pub async fn get_available_routes(
|
||||
|
||||
Ok(RouteListResponse {
|
||||
base_url,
|
||||
default_provider: "kiro".to_string(),
|
||||
default_provider,
|
||||
routes: all_routes,
|
||||
})
|
||||
}
|
||||
@@ -58,6 +59,7 @@ pub async fn get_route_curl_examples(
|
||||
) -> Result<Vec<crate::models::route_model::CurlExample>, String> {
|
||||
let config = config::load_config().unwrap_or_default();
|
||||
let base_url = format!("http://{}:{}", config.server.host, config.server.port);
|
||||
let default_provider = config.default_provider.clone();
|
||||
|
||||
let routes = pool_service
|
||||
.0
|
||||
@@ -73,8 +75,8 @@ pub async fn get_route_curl_examples(
|
||||
match route {
|
||||
Some(r) => Ok(r.generate_curl_examples(api_key)),
|
||||
None => {
|
||||
// 生成默认路由的示例
|
||||
let mut default_route = RouteInfo::new("default".to_string(), "kiro".to_string());
|
||||
// 生成默认路由的示例,使用配置中的默认 Provider
|
||||
let mut default_route = RouteInfo::new("default".to_string(), default_provider);
|
||||
default_route.add_endpoint(&base_url, "claude");
|
||||
default_route.add_endpoint(&base_url, "openai");
|
||||
Ok(default_route.generate_curl_examples(api_key))
|
||||
|
||||
@@ -13,6 +13,7 @@ use std::path::PathBuf;
|
||||
/// 关键改进:每个凭证生成独立的 Machine ID,避免多账号共用同一指纹被检测
|
||||
/// 优先级:profileArn > clientId > 系统硬件 ID
|
||||
///
|
||||
/// 防指纹固化:添加基于小时的时间变化因子,在保持稳定性的同时避免指纹完全固化
|
||||
/// 这样每个 OAuth 凭证都有自己独立的指纹,模拟不同设备登录
|
||||
pub fn generate_machine_id_from_credentials(
|
||||
profile_arn: Option<&str>,
|
||||
@@ -30,8 +31,19 @@ pub fn generate_machine_id_from_credentials(
|
||||
get_raw_machine_id().unwrap_or_else(|| "KIRO_DEFAULT_MACHINE".to_string())
|
||||
});
|
||||
|
||||
// 添加时间变化因子 - 按小时变化,避免指纹固化
|
||||
// 使用小时级别的时间戳,确保在同一小时内指纹稳定,但定期变化
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
// 按小时分组,每小时指纹会轻微变化
|
||||
let hour_slot = now / 3600;
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(unique_key.as_bytes());
|
||||
hasher.update(&hour_slot.to_le_bytes()); // 添加时间因子
|
||||
let result = hasher.finalize();
|
||||
format!("{:x}", result)
|
||||
}
|
||||
|
||||
@@ -44,12 +44,28 @@ impl RouteMatch {
|
||||
pub struct ProviderRouter {
|
||||
/// 路由注册表
|
||||
registry: Arc<RwLock<RouteRegistry>>,
|
||||
/// 默认 Provider 引用(从配置动态获取)
|
||||
default_provider_ref: Arc<RwLock<String>>,
|
||||
}
|
||||
|
||||
impl ProviderRouter {
|
||||
/// 创建新的路由器
|
||||
pub fn new(registry: Arc<RwLock<RouteRegistry>>) -> Self {
|
||||
Self { registry }
|
||||
Self {
|
||||
registry,
|
||||
default_provider_ref: Arc::new(RwLock::new("kiro".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建带有默认 Provider 引用的路由器
|
||||
pub fn with_default_provider(
|
||||
registry: Arc<RwLock<RouteRegistry>>,
|
||||
default_provider_ref: Arc<RwLock<String>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
registry,
|
||||
default_provider_ref,
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析请求路径
|
||||
@@ -67,12 +83,13 @@ impl ProviderRouter {
|
||||
// /v1/messages
|
||||
["v1", "messages"] => {
|
||||
let registry = self.registry.read().await;
|
||||
let default_provider = self.default_provider_ref.read().await.clone();
|
||||
let route = registry
|
||||
.enabled_routes()
|
||||
.into_iter()
|
||||
.find(|r| r.route_type == RouteType::Default)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| RegisteredRoute::default_route("kiro"));
|
||||
.unwrap_or_else(|| RegisteredRoute::default_route(&default_provider));
|
||||
|
||||
Some(RouteMatch {
|
||||
route,
|
||||
@@ -84,12 +101,13 @@ impl ProviderRouter {
|
||||
// /v1/chat/completions
|
||||
["v1", "chat", "completions"] => {
|
||||
let registry = self.registry.read().await;
|
||||
let default_provider = self.default_provider_ref.read().await.clone();
|
||||
let route = registry
|
||||
.enabled_routes()
|
||||
.into_iter()
|
||||
.find(|r| r.route_type == RouteType::Default)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| RegisteredRoute::default_route("kiro"));
|
||||
.unwrap_or_else(|| RegisteredRoute::default_route(&default_provider));
|
||||
|
||||
Some(RouteMatch {
|
||||
route,
|
||||
|
||||
@@ -0,0 +1,652 @@
|
||||
//! Kiro凭证管理API处理器
|
||||
//!
|
||||
//! 为kiro凭证池管理提供REST API端点,支持:
|
||||
//! - 获取可用凭证列表
|
||||
//! - 智能选择凭证
|
||||
//! - 手动刷新凭证
|
||||
//! - 凭证状态查询
|
||||
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::database::dao::provider_pool::ProviderPoolDao;
|
||||
use crate::models::provider_pool_model::{CachedTokenInfo, PoolProviderType, ProviderCredential};
|
||||
use crate::server::AppState;
|
||||
|
||||
/// 可用凭证信息
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AvailableCredential {
|
||||
/// 凭证UUID
|
||||
pub uuid: String,
|
||||
/// 凭证名称
|
||||
pub name: String,
|
||||
/// 是否可用
|
||||
pub available: bool,
|
||||
/// Token过期时间
|
||||
pub expires_at: Option<DateTime<Utc>>,
|
||||
/// 最后使用时间
|
||||
pub last_used: Option<DateTime<Utc>>,
|
||||
/// 健康状态分数 (0-100)
|
||||
pub health_score: f64,
|
||||
/// 错误计数
|
||||
pub error_count: u32,
|
||||
/// 最后错误信息
|
||||
pub last_error: Option<String>,
|
||||
}
|
||||
|
||||
/// 获取可用凭证列表的响应
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AvailableCredentialsResponse {
|
||||
/// 可用凭证列表
|
||||
pub credentials: Vec<AvailableCredential>,
|
||||
/// 总凭证数
|
||||
pub total: usize,
|
||||
/// 可用凭证数
|
||||
pub available: usize,
|
||||
/// 系统状态
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// 选择凭证请求参数
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SelectCredentialRequest {
|
||||
/// 指定模型(可选)
|
||||
pub model: Option<String>,
|
||||
/// 强制选择特定UUID(可选)
|
||||
pub force_uuid: Option<String>,
|
||||
}
|
||||
|
||||
/// 选择凭证响应
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SelectCredentialResponse {
|
||||
/// 选中的凭证UUID
|
||||
pub uuid: String,
|
||||
/// 凭证名称
|
||||
pub name: String,
|
||||
/// Access Token(脱敏显示)
|
||||
pub access_token_preview: String,
|
||||
/// Token过期时间
|
||||
pub expires_at: Option<DateTime<Utc>>,
|
||||
/// 选择原因
|
||||
pub selection_reason: String,
|
||||
}
|
||||
|
||||
/// 刷新凭证响应
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RefreshCredentialResponse {
|
||||
/// 凭证UUID
|
||||
pub uuid: String,
|
||||
/// 刷新是否成功
|
||||
pub success: bool,
|
||||
/// 新的过期时间
|
||||
pub new_expires_at: Option<DateTime<Utc>>,
|
||||
/// 刷新结果信息
|
||||
pub message: String,
|
||||
/// 错误信息(如果有)
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// API错误响应
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ApiError {
|
||||
pub error: String,
|
||||
pub message: String,
|
||||
pub status_code: u16,
|
||||
}
|
||||
|
||||
impl IntoResponse for ApiError {
|
||||
fn into_response(self) -> Response {
|
||||
let status =
|
||||
StatusCode::from_u16(self.status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
(status, Json(self)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /api/kiro/credentials/available - 获取可用凭证列表
|
||||
pub async fn get_available_credentials(
|
||||
State(state): State<AppState>,
|
||||
_headers: HeaderMap,
|
||||
) -> Result<Json<AvailableCredentialsResponse>, ApiError> {
|
||||
tracing::info!("[KIRO_API] 获取可用凭证列表请求");
|
||||
|
||||
let db = &state.db.as_ref().ok_or_else(|| ApiError {
|
||||
error: "database_unavailable".to_string(),
|
||||
message: "数据库连接不可用".to_string(),
|
||||
status_code: 503,
|
||||
})?;
|
||||
|
||||
let pool_service = &state.pool_service;
|
||||
let token_cache = &state.token_cache;
|
||||
|
||||
// 获取所有kiro凭证
|
||||
let credentials = {
|
||||
let conn = db.lock().map_err(|e| ApiError {
|
||||
error: "database_lock_error".to_string(),
|
||||
message: format!("数据库锁定失败: {}", e),
|
||||
status_code: 500,
|
||||
})?;
|
||||
|
||||
ProviderPoolDao::get_all(&conn)
|
||||
.map_err(|e| ApiError {
|
||||
error: "database_query_error".to_string(),
|
||||
message: format!("查询凭证失败: {}", e),
|
||||
status_code: 500,
|
||||
})?
|
||||
.into_iter()
|
||||
.filter(|cred| cred.provider_type == PoolProviderType::Kiro)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let mut available_credentials = Vec::new();
|
||||
let mut available_count = 0;
|
||||
|
||||
for credential in &credentials {
|
||||
// 获取凭证缓存状态
|
||||
let cache_status = token_cache
|
||||
.get_cache_status(db, &credential.uuid)
|
||||
.map_err(|e| ApiError {
|
||||
error: "cache_query_error".to_string(),
|
||||
message: format!("获取缓存状态失败: {}", e),
|
||||
status_code: 500,
|
||||
})?;
|
||||
|
||||
// 计算健康状态分数
|
||||
let health_score = calculate_health_score(&credential, cache_status.as_ref());
|
||||
|
||||
let is_available = health_score > 50.0; // 健康分数大于50认为可用
|
||||
if is_available {
|
||||
available_count += 1;
|
||||
}
|
||||
|
||||
let available_cred = AvailableCredential {
|
||||
uuid: credential.uuid.clone(),
|
||||
name: credential
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "未命名".to_string()),
|
||||
available: is_available,
|
||||
expires_at: cache_status.as_ref().and_then(|c| c.expiry_time),
|
||||
last_used: credential.last_used,
|
||||
health_score,
|
||||
error_count: cache_status
|
||||
.as_ref()
|
||||
.map(|c| c.refresh_error_count)
|
||||
.unwrap_or(0),
|
||||
last_error: cache_status.and_then(|c| c.last_refresh_error),
|
||||
};
|
||||
|
||||
available_credentials.push(available_cred);
|
||||
}
|
||||
|
||||
// 按健康分数降序排列
|
||||
available_credentials.sort_by(|a, b| {
|
||||
b.health_score
|
||||
.partial_cmp(&a.health_score)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
let response = AvailableCredentialsResponse {
|
||||
credentials: available_credentials,
|
||||
total: credentials.len(),
|
||||
available: available_count,
|
||||
status: if available_count > 0 {
|
||||
"healthy".to_string()
|
||||
} else {
|
||||
"degraded".to_string()
|
||||
},
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
"[KIRO_API] 返回{}个凭证,其中{}个可用",
|
||||
response.total,
|
||||
response.available
|
||||
);
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
/// POST /api/kiro/credentials/select - 智能选择凭证
|
||||
pub async fn select_credential(
|
||||
State(state): State<AppState>,
|
||||
_headers: HeaderMap,
|
||||
Json(request): Json<SelectCredentialRequest>,
|
||||
) -> Result<Json<SelectCredentialResponse>, ApiError> {
|
||||
tracing::info!(
|
||||
"[KIRO_API] 选择凭证请求,模型: {:?}, 强制UUID: {:?}",
|
||||
request.model,
|
||||
request.force_uuid
|
||||
);
|
||||
|
||||
let db = &state.db.as_ref().ok_or_else(|| ApiError {
|
||||
error: "database_unavailable".to_string(),
|
||||
message: "数据库连接不可用".to_string(),
|
||||
status_code: 503,
|
||||
})?;
|
||||
|
||||
let selected_credential = if let Some(ref force_uuid) = request.force_uuid {
|
||||
// 强制选择指定UUID
|
||||
let conn = db.lock().map_err(|e| ApiError {
|
||||
error: "database_lock_error".to_string(),
|
||||
message: format!("数据库锁定失败: {}", e),
|
||||
status_code: 500,
|
||||
})?;
|
||||
|
||||
ProviderPoolDao::get_by_uuid(&conn, &force_uuid)
|
||||
.map_err(|e| ApiError {
|
||||
error: "database_query_error".to_string(),
|
||||
message: format!("查询凭证失败: {}", e),
|
||||
status_code: 500,
|
||||
})?
|
||||
.ok_or_else(|| ApiError {
|
||||
error: "credential_not_found".to_string(),
|
||||
message: format!("未找到UUID为{}的凭证", force_uuid),
|
||||
status_code: 404,
|
||||
})?
|
||||
} else {
|
||||
// 智能选择最优凭证
|
||||
state
|
||||
.pool_service
|
||||
.select_credential(db, "kiro", request.model.as_deref())
|
||||
.map_err(|e| ApiError {
|
||||
error: "selection_error".to_string(),
|
||||
message: format!("凭证选择失败: {}", e),
|
||||
status_code: 500,
|
||||
})?
|
||||
.ok_or_else(|| ApiError {
|
||||
error: "no_available_credentials".to_string(),
|
||||
message: "没有可用的kiro凭证".to_string(),
|
||||
status_code: 503,
|
||||
})?
|
||||
};
|
||||
|
||||
// 脱敏显示token
|
||||
let token_preview = if let Some(cached_token_info) = &selected_credential.cached_token {
|
||||
if let Some(token) = &cached_token_info.access_token {
|
||||
if token.len() > 20 {
|
||||
format!("{}...{}", &token[..10], &token[token.len() - 10..])
|
||||
} else {
|
||||
"***".to_string()
|
||||
}
|
||||
} else {
|
||||
"无token".to_string()
|
||||
}
|
||||
} else {
|
||||
"未缓存".to_string()
|
||||
};
|
||||
|
||||
let selection_reason = if request.force_uuid.is_some() {
|
||||
"手动指定".to_string()
|
||||
} else {
|
||||
"智能算法选择".to_string()
|
||||
};
|
||||
|
||||
let expires_at = selected_credential
|
||||
.cached_token
|
||||
.as_ref()
|
||||
.and_then(|cache| cache.expiry_time);
|
||||
|
||||
let response = SelectCredentialResponse {
|
||||
uuid: selected_credential.uuid.clone(),
|
||||
name: selected_credential
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "未命名".to_string()),
|
||||
access_token_preview: token_preview,
|
||||
expires_at,
|
||||
selection_reason,
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
"[KIRO_API] 选择凭证成功: {} ({})",
|
||||
response.name,
|
||||
response.uuid
|
||||
);
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
/// PUT /api/kiro/credentials/{uuid}/refresh - 手动刷新指定凭证
|
||||
pub async fn refresh_credential(
|
||||
State(state): State<AppState>,
|
||||
Path(uuid): Path<String>,
|
||||
_headers: HeaderMap,
|
||||
) -> Result<Json<RefreshCredentialResponse>, ApiError> {
|
||||
tracing::info!("[KIRO_API] 刷新凭证请求: {}", uuid);
|
||||
|
||||
let db = &state.db.as_ref().ok_or_else(|| ApiError {
|
||||
error: "database_unavailable".to_string(),
|
||||
message: "数据库连接不可用".to_string(),
|
||||
status_code: 503,
|
||||
})?;
|
||||
|
||||
let token_cache = &state.token_cache;
|
||||
|
||||
// 验证凭证存在且为kiro类型
|
||||
let credential = {
|
||||
let conn = db.lock().map_err(|e| ApiError {
|
||||
error: "database_lock_error".to_string(),
|
||||
message: format!("数据库锁定失败: {}", e),
|
||||
status_code: 500,
|
||||
})?;
|
||||
|
||||
let cred = ProviderPoolDao::get_by_uuid(&conn, &uuid)
|
||||
.map_err(|e| ApiError {
|
||||
error: "database_query_error".to_string(),
|
||||
message: format!("查询凭证失败: {}", e),
|
||||
status_code: 500,
|
||||
})?
|
||||
.ok_or_else(|| ApiError {
|
||||
error: "credential_not_found".to_string(),
|
||||
message: format!("未找到UUID为{}的凭证", uuid),
|
||||
status_code: 404,
|
||||
})?;
|
||||
|
||||
if cred.provider_type.to_string() != PoolProviderType::Kiro.to_string() {
|
||||
return Err(ApiError {
|
||||
error: "invalid_credential_type".to_string(),
|
||||
message: format!("凭证类型不是kiro: {}", cred.provider_type),
|
||||
status_code: 400,
|
||||
});
|
||||
}
|
||||
|
||||
cred
|
||||
};
|
||||
|
||||
// 执行强制刷新
|
||||
match token_cache
|
||||
.refresh_and_cache_with_events(db, &uuid, true, Some(state.kiro_event_service.clone()))
|
||||
.await
|
||||
{
|
||||
Ok(_new_token) => {
|
||||
// 获取刷新后的缓存状态
|
||||
let cache_status = token_cache
|
||||
.get_cache_status(db, &uuid)
|
||||
.map_err(|e| ApiError {
|
||||
error: "cache_query_error".to_string(),
|
||||
message: format!("获取刷新后缓存状态失败: {}", e),
|
||||
status_code: 500,
|
||||
})?;
|
||||
|
||||
let response = RefreshCredentialResponse {
|
||||
uuid: uuid.clone(),
|
||||
success: true,
|
||||
new_expires_at: cache_status.as_ref().and_then(|c| c.expiry_time),
|
||||
message: format!(
|
||||
"凭证 {} 刷新成功",
|
||||
credential
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "未命名".to_string())
|
||||
),
|
||||
error: None,
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
"[KIRO_API] 凭证刷新成功: {} ({})",
|
||||
credential
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "未命名".to_string()),
|
||||
uuid
|
||||
);
|
||||
Ok(Json(response))
|
||||
}
|
||||
Err(refresh_error) => {
|
||||
let response = RefreshCredentialResponse {
|
||||
uuid: uuid.clone(),
|
||||
success: false,
|
||||
new_expires_at: None,
|
||||
message: format!(
|
||||
"凭证 {} 刷新失败",
|
||||
credential
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "未命名".to_string())
|
||||
),
|
||||
error: Some(refresh_error.clone()),
|
||||
};
|
||||
|
||||
tracing::warn!(
|
||||
"[KIRO_API] 凭证刷新失败: {} ({}): {}",
|
||||
credential
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "未命名".to_string()),
|
||||
uuid,
|
||||
refresh_error
|
||||
);
|
||||
Ok(Json(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /api/kiro/credentials/{uuid}/status - 获取凭证详细状态
|
||||
pub async fn get_credential_status(
|
||||
State(state): State<AppState>,
|
||||
Path(uuid): Path<String>,
|
||||
_headers: HeaderMap,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
tracing::info!("[KIRO_API] 获取凭证状态: {}", uuid);
|
||||
|
||||
let db = &state.db.as_ref().ok_or_else(|| ApiError {
|
||||
error: "database_unavailable".to_string(),
|
||||
message: "数据库连接不可用".to_string(),
|
||||
status_code: 503,
|
||||
})?;
|
||||
|
||||
let token_cache = &state.token_cache;
|
||||
|
||||
// 验证凭证存在
|
||||
let credential = {
|
||||
let conn = db.lock().map_err(|e| ApiError {
|
||||
error: "database_lock_error".to_string(),
|
||||
message: format!("数据库锁定失败: {}", e),
|
||||
status_code: 500,
|
||||
})?;
|
||||
|
||||
ProviderPoolDao::get_by_uuid(&conn, &uuid)
|
||||
.map_err(|e| ApiError {
|
||||
error: "database_query_error".to_string(),
|
||||
message: format!("查询凭证失败: {}", e),
|
||||
status_code: 500,
|
||||
})?
|
||||
.ok_or_else(|| ApiError {
|
||||
error: "credential_not_found".to_string(),
|
||||
message: format!("未找到UUID为{}的凭证", uuid),
|
||||
status_code: 404,
|
||||
})?
|
||||
};
|
||||
|
||||
// 获取缓存状态
|
||||
let cache_status = token_cache
|
||||
.get_cache_status(db, &uuid)
|
||||
.map_err(|e| ApiError {
|
||||
error: "cache_query_error".to_string(),
|
||||
message: format!("获取缓存状态失败: {}", e),
|
||||
status_code: 500,
|
||||
})?;
|
||||
|
||||
// 计算健康分数
|
||||
let health_score = calculate_health_score(&credential, cache_status.as_ref());
|
||||
|
||||
let mut status = serde_json::Map::new();
|
||||
status.insert(
|
||||
"uuid".to_string(),
|
||||
serde_json::Value::String(credential.uuid.clone()),
|
||||
);
|
||||
status.insert(
|
||||
"name".to_string(),
|
||||
serde_json::Value::String(
|
||||
credential
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "未命名".to_string()),
|
||||
),
|
||||
);
|
||||
status.insert(
|
||||
"provider_type".to_string(),
|
||||
serde_json::Value::String(credential.provider_type.to_string()),
|
||||
);
|
||||
status.insert(
|
||||
"created_at".to_string(),
|
||||
serde_json::Value::String(credential.created_at.to_rfc3339()),
|
||||
);
|
||||
status.insert(
|
||||
"last_used".to_string(),
|
||||
credential
|
||||
.last_used
|
||||
.map(|dt| serde_json::Value::String(dt.to_rfc3339()))
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
);
|
||||
status.insert(
|
||||
"health_score".to_string(),
|
||||
serde_json::Value::Number(
|
||||
serde_json::Number::from_f64(health_score).unwrap_or(serde_json::Number::from(0)),
|
||||
),
|
||||
);
|
||||
status.insert(
|
||||
"is_available".to_string(),
|
||||
serde_json::Value::Bool(health_score > 50.0),
|
||||
);
|
||||
|
||||
if let Some(cache) = cache_status {
|
||||
status.insert(
|
||||
"has_cached_token".to_string(),
|
||||
serde_json::Value::Bool(cache.access_token.is_some()),
|
||||
);
|
||||
status.insert(
|
||||
"expires_at".to_string(),
|
||||
cache
|
||||
.expiry_time
|
||||
.map(|dt| serde_json::Value::String(dt.to_rfc3339()))
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
);
|
||||
status.insert(
|
||||
"last_refresh".to_string(),
|
||||
cache
|
||||
.last_refresh
|
||||
.map(|dt| serde_json::Value::String(dt.to_rfc3339()))
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
);
|
||||
status.insert(
|
||||
"refresh_error_count".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(cache.refresh_error_count)),
|
||||
);
|
||||
status.insert(
|
||||
"last_refresh_error".to_string(),
|
||||
cache
|
||||
.last_refresh_error
|
||||
.map(|err| serde_json::Value::String(err))
|
||||
.unwrap_or(serde_json::Value::Null),
|
||||
);
|
||||
} else {
|
||||
status.insert(
|
||||
"has_cached_token".to_string(),
|
||||
serde_json::Value::Bool(false),
|
||||
);
|
||||
status.insert("expires_at".to_string(), serde_json::Value::Null);
|
||||
status.insert("last_refresh".to_string(), serde_json::Value::Null);
|
||||
status.insert(
|
||||
"refresh_error_count".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(0)),
|
||||
);
|
||||
status.insert("last_refresh_error".to_string(), serde_json::Value::Null);
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"[KIRO_API] 返回凭证状态: {} (健康分数: {:.1})",
|
||||
credential
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "未命名".to_string()),
|
||||
health_score
|
||||
);
|
||||
Ok(Json(serde_json::Value::Object(status)))
|
||||
}
|
||||
|
||||
/// 计算凭证健康分数
|
||||
///
|
||||
/// 基于凭证的基本状态、缓存状态、错误计数等因素综合计算健康分数
|
||||
/// 分数范围: 0-100,分数越高表示凭证越健康
|
||||
fn calculate_health_score(
|
||||
credential: &ProviderCredential,
|
||||
cache_status: Option<&CachedTokenInfo>,
|
||||
) -> f64 {
|
||||
let mut score = 0.0;
|
||||
|
||||
// 1. 基础健康状态 (40分)
|
||||
if credential.is_healthy {
|
||||
score += 40.0;
|
||||
} else {
|
||||
score -= 20.0; // 不健康严重扣分
|
||||
}
|
||||
|
||||
// 2. 错误计数影响 (20分)
|
||||
let error_count = credential.error_count;
|
||||
if error_count == 0 {
|
||||
score += 20.0;
|
||||
} else if error_count <= 2 {
|
||||
score += 10.0; // 少量错误,轻微扣分
|
||||
} else {
|
||||
score -= error_count as f64 * 5.0; // 错误越多扣分越多
|
||||
}
|
||||
|
||||
// 3. Token缓存状态 (25分)
|
||||
if let Some(cache) = cache_status {
|
||||
if cache.access_token.is_some() {
|
||||
score += 15.0; // 有缓存token
|
||||
|
||||
// 检查过期时间
|
||||
if let Some(expiry_time) = cache.expiry_time {
|
||||
let now = chrono::Utc::now();
|
||||
let time_until_expiry = expiry_time - now;
|
||||
|
||||
if time_until_expiry > chrono::Duration::hours(1) {
|
||||
score += 10.0; // 距离过期还有较长时间
|
||||
} else if time_until_expiry > chrono::Duration::minutes(30) {
|
||||
score += 5.0; // 距离过期还有一些时间
|
||||
} else if time_until_expiry <= chrono::Duration::zero() {
|
||||
score -= 10.0; // 已过期
|
||||
}
|
||||
}
|
||||
} else {
|
||||
score -= 5.0; // 没有缓存token
|
||||
}
|
||||
|
||||
// 刷新错误计数影响
|
||||
if cache.refresh_error_count == 0 {
|
||||
// 无刷新错误,不加分不减分
|
||||
} else if cache.refresh_error_count <= 2 {
|
||||
score -= cache.refresh_error_count as f64 * 2.0; // 少量刷新错误
|
||||
} else {
|
||||
score -= cache.refresh_error_count as f64 * 5.0; // 大量刷新错误严重扣分
|
||||
}
|
||||
} else {
|
||||
score -= 10.0; // 完全没有缓存状态
|
||||
}
|
||||
|
||||
// 4. 使用活跃度 (15分)
|
||||
if let Some(last_used) = credential.last_used {
|
||||
let now = chrono::Utc::now();
|
||||
let time_since_used = now - last_used;
|
||||
|
||||
if time_since_used <= chrono::Duration::hours(1) {
|
||||
score += 15.0; // 最近1小时内使用过
|
||||
} else if time_since_used <= chrono::Duration::hours(24) {
|
||||
score += 10.0; // 最近24小时内使用过
|
||||
} else if time_since_used <= chrono::Duration::days(7) {
|
||||
score += 5.0; // 最近一周内使用过
|
||||
} else {
|
||||
score += 0.0; // 很久未使用,不扣分也不加分
|
||||
}
|
||||
} else {
|
||||
score -= 5.0; // 从未使用过
|
||||
}
|
||||
|
||||
// 确保分数在0-100范围内
|
||||
score.max(0.0).min(100.0)
|
||||
}
|
||||
@@ -3,11 +3,13 @@
|
||||
//! 将 server 中的各类处理器拆分到独立文件
|
||||
|
||||
pub mod api;
|
||||
pub mod kiro_credential;
|
||||
pub mod management;
|
||||
pub mod provider_calls;
|
||||
pub mod websocket;
|
||||
|
||||
pub use api::*;
|
||||
pub use kiro_credential::*;
|
||||
pub use management::*;
|
||||
pub use provider_calls::*;
|
||||
pub use websocket::*;
|
||||
|
||||
@@ -349,6 +349,32 @@ async fn handle_ws_message(
|
||||
"Invalid message type from client",
|
||||
))),
|
||||
WsProtoMessage::Error(_) => None,
|
||||
WsProtoMessage::SubscribeKiroEvents => {
|
||||
// TODO: 实现Kiro事件订阅
|
||||
Some(WsProtoMessage::Response(WsApiResponse {
|
||||
request_id: "subscribe_kiro_events".to_string(),
|
||||
payload: serde_json::json!({
|
||||
"status": "subscribed",
|
||||
"message": "Successfully subscribed to kiro events"
|
||||
}),
|
||||
}))
|
||||
}
|
||||
WsProtoMessage::UnsubscribeKiroEvents => {
|
||||
// TODO: 实现Kiro事件取消订阅
|
||||
Some(WsProtoMessage::Response(WsApiResponse {
|
||||
request_id: "unsubscribe_kiro_events".to_string(),
|
||||
payload: serde_json::json!({
|
||||
"status": "unsubscribed",
|
||||
"message": "Successfully unsubscribed from kiro events"
|
||||
}),
|
||||
}))
|
||||
}
|
||||
WsProtoMessage::KiroCredentialEvent(_) => {
|
||||
// Kiro事件是服务端到客户端的消息,客户端不应该发送
|
||||
Some(WsProtoMessage::Error(WsError::invalid_message(
|
||||
"KiroCredentialEvent messages are server-to-client only",
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ use crate::server_utils::{
|
||||
build_anthropic_response, build_anthropic_stream_response, build_gemini_native_request, health,
|
||||
models, parse_cw_response,
|
||||
};
|
||||
use crate::services::kiro_event_service::KiroEventService;
|
||||
use crate::services::provider_pool_service::ProviderPoolService;
|
||||
use crate::services::token_cache_service::TokenCacheService;
|
||||
use crate::websocket::{WsConfig, WsConnectionManager, WsStats};
|
||||
@@ -386,6 +387,8 @@ pub struct AppState {
|
||||
pub flow_interceptor: Arc<FlowInterceptor>,
|
||||
/// 端点 Provider 配置
|
||||
pub endpoint_providers: Arc<RwLock<EndpointProvidersConfig>>,
|
||||
/// Kiro 事件服务
|
||||
pub kiro_event_service: Arc<KiroEventService>,
|
||||
}
|
||||
|
||||
/// 启动配置文件监控
|
||||
@@ -734,6 +737,9 @@ async fn run_server(
|
||||
.unwrap_or_default(),
|
||||
));
|
||||
|
||||
// 创建 Kiro 事件服务
|
||||
let kiro_event_service = Arc::new(KiroEventService::new());
|
||||
|
||||
let state = AppState {
|
||||
api_key: api_key.to_string(),
|
||||
base_url,
|
||||
@@ -757,6 +763,7 @@ async fn run_server(
|
||||
flow_monitor,
|
||||
flow_interceptor,
|
||||
endpoint_providers,
|
||||
kiro_event_service,
|
||||
};
|
||||
|
||||
// 启动配置文件监控
|
||||
@@ -805,6 +812,25 @@ async fn run_server(
|
||||
management_config,
|
||||
));
|
||||
|
||||
// Kiro凭证管理API路由
|
||||
let kiro_api_routes = Router::new()
|
||||
.route(
|
||||
"/api/kiro/credentials/available",
|
||||
get(handlers::get_available_credentials),
|
||||
)
|
||||
.route(
|
||||
"/api/kiro/credentials/select",
|
||||
post(handlers::select_credential),
|
||||
)
|
||||
.route(
|
||||
"/api/kiro/credentials/:uuid/refresh",
|
||||
axum::routing::put(handlers::refresh_credential),
|
||||
)
|
||||
.route(
|
||||
"/api/kiro/credentials/:uuid/status",
|
||||
get(handlers::get_credential_status),
|
||||
);
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", get(health))
|
||||
.route("/v1/models", get(models))
|
||||
@@ -843,6 +869,8 @@ async fn run_server(
|
||||
)
|
||||
// 管理 API 路由
|
||||
.merge(management_routes)
|
||||
// Kiro凭证管理API路由
|
||||
.merge(kiro_api_routes)
|
||||
.layer(DefaultBodyLimit::max(body_limit))
|
||||
.with_state(state);
|
||||
|
||||
@@ -1123,10 +1151,13 @@ async fn list_routes(State(state): State<AppState>) -> impl IntoResponse {
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
// 获取默认 Provider
|
||||
let default_provider = state.default_provider.read().await.clone();
|
||||
|
||||
// 添加默认路由
|
||||
let mut all_routes = vec![RouteInfo {
|
||||
selector: "default".to_string(),
|
||||
provider_type: "kiro".to_string(),
|
||||
provider_type: default_provider.clone(),
|
||||
credential_count: 1,
|
||||
endpoints: vec![
|
||||
crate::models::route_model::RouteEndpoint {
|
||||
@@ -1147,7 +1178,7 @@ async fn list_routes(State(state): State<AppState>) -> impl IntoResponse {
|
||||
|
||||
let response = RouteListResponse {
|
||||
base_url: state.base_url.clone(),
|
||||
default_provider: "kiro".to_string(),
|
||||
default_provider,
|
||||
routes: all_routes,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
//! Kiro 凭证事件服务
|
||||
//!
|
||||
//! 负责管理 Kiro 凭证相关的实时事件推送,包括:
|
||||
//! - 凭证状态更新
|
||||
//! - Token 刷新事件
|
||||
//! - 健康检查结果
|
||||
//! - 凭证池统计
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use tokio::sync::{broadcast, RwLock};
|
||||
|
||||
use crate::websocket::{KiroTokenInfo, WsKiroEvent};
|
||||
|
||||
/// Kiro 事件服务
|
||||
#[derive(Debug)]
|
||||
pub struct KiroEventService {
|
||||
/// 事件发送器
|
||||
event_sender: broadcast::Sender<WsKiroEvent>,
|
||||
/// 凭证状态缓存
|
||||
credential_states: RwLock<HashMap<String, CachedCredentialState>>,
|
||||
}
|
||||
|
||||
/// 缓存的凭证状态
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct CachedCredentialState {
|
||||
uuid: String,
|
||||
is_healthy: bool,
|
||||
is_disabled: bool,
|
||||
error_count: u32,
|
||||
health_score: Option<f64>,
|
||||
last_used: Option<DateTime<Utc>>,
|
||||
last_updated: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl KiroEventService {
|
||||
/// 创建新的 Kiro 事件服务
|
||||
pub fn new() -> Self {
|
||||
let (event_sender, _) = broadcast::channel(1000);
|
||||
Self {
|
||||
event_sender,
|
||||
credential_states: RwLock::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 订阅 Kiro 事件
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<WsKiroEvent> {
|
||||
self.event_sender.subscribe()
|
||||
}
|
||||
|
||||
/// 发送凭证状态更新事件
|
||||
pub async fn emit_credential_status_update(
|
||||
&self,
|
||||
uuid: String,
|
||||
is_healthy: bool,
|
||||
is_disabled: bool,
|
||||
error_count: u32,
|
||||
health_score: Option<f64>,
|
||||
last_used: Option<DateTime<Utc>>,
|
||||
) {
|
||||
let now = Utc::now();
|
||||
|
||||
// 更新缓存
|
||||
{
|
||||
let mut states = self.credential_states.write().await;
|
||||
states.insert(
|
||||
uuid.clone(),
|
||||
CachedCredentialState {
|
||||
uuid: uuid.clone(),
|
||||
is_healthy,
|
||||
is_disabled,
|
||||
error_count,
|
||||
health_score,
|
||||
last_used,
|
||||
last_updated: now,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 发送事件
|
||||
let event = WsKiroEvent::CredentialStatusUpdate {
|
||||
uuid,
|
||||
is_healthy,
|
||||
is_disabled,
|
||||
error_count,
|
||||
health_score,
|
||||
last_used,
|
||||
};
|
||||
|
||||
if let Err(e) = self.event_sender.send(event) {
|
||||
tracing::debug!("Failed to send credential status update event: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送凭证刷新开始事件
|
||||
pub async fn emit_refresh_started(&self, uuid: String, credential_name: Option<String>) {
|
||||
let event = WsKiroEvent::RefreshStarted {
|
||||
uuid,
|
||||
credential_name,
|
||||
};
|
||||
|
||||
if let Err(e) = self.event_sender.send(event) {
|
||||
tracing::debug!("Failed to send refresh started event: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送凭证刷新成功事件
|
||||
pub async fn emit_refresh_success(
|
||||
&self,
|
||||
uuid: String,
|
||||
credential_name: Option<String>,
|
||||
expires_at: DateTime<Utc>,
|
||||
auth_method: String,
|
||||
provider: String,
|
||||
region: String,
|
||||
) {
|
||||
let new_token_info = KiroTokenInfo {
|
||||
expires_at,
|
||||
auth_method,
|
||||
provider,
|
||||
region,
|
||||
};
|
||||
|
||||
let event = WsKiroEvent::RefreshSuccess {
|
||||
uuid,
|
||||
credential_name,
|
||||
new_token_info,
|
||||
};
|
||||
|
||||
if let Err(e) = self.event_sender.send(event) {
|
||||
tracing::debug!("Failed to send refresh success event: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送凭证刷新失败事件
|
||||
pub async fn emit_refresh_failed(
|
||||
&self,
|
||||
uuid: String,
|
||||
credential_name: Option<String>,
|
||||
error: String,
|
||||
error_code: Option<String>,
|
||||
) {
|
||||
let event = WsKiroEvent::RefreshFailed {
|
||||
uuid,
|
||||
credential_name,
|
||||
error,
|
||||
error_code,
|
||||
};
|
||||
|
||||
if let Err(e) = self.event_sender.send(event) {
|
||||
tracing::debug!("Failed to send refresh failed event: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送健康检查完成事件
|
||||
pub async fn emit_health_check_completed(
|
||||
&self,
|
||||
uuid: String,
|
||||
credential_name: Option<String>,
|
||||
is_healthy: bool,
|
||||
health_score: Option<f64>,
|
||||
) {
|
||||
let last_check = Utc::now();
|
||||
let event = WsKiroEvent::HealthCheckCompleted {
|
||||
uuid,
|
||||
credential_name,
|
||||
is_healthy,
|
||||
health_score,
|
||||
last_check,
|
||||
};
|
||||
|
||||
if let Err(e) = self.event_sender.send(event) {
|
||||
tracing::debug!("Failed to send health check completed event: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送凭证池统计更新事件
|
||||
pub async fn emit_pool_stats_update(
|
||||
&self,
|
||||
total_credentials: u32,
|
||||
healthy_credentials: u32,
|
||||
available_credentials: u32,
|
||||
average_health_score: Option<f64>,
|
||||
last_rotation: Option<DateTime<Utc>>,
|
||||
) {
|
||||
let event = WsKiroEvent::PoolStatsUpdate {
|
||||
total_credentials,
|
||||
healthy_credentials,
|
||||
available_credentials,
|
||||
average_health_score,
|
||||
last_rotation,
|
||||
};
|
||||
|
||||
if let Err(e) = self.event_sender.send(event) {
|
||||
tracing::debug!("Failed to send pool stats update event: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送凭证轮换事件
|
||||
pub async fn emit_credential_rotated(
|
||||
&self,
|
||||
from_uuid: Option<String>,
|
||||
to_uuid: String,
|
||||
reason: String,
|
||||
) {
|
||||
let rotation_time = Utc::now();
|
||||
let event = WsKiroEvent::CredentialRotated {
|
||||
from_uuid,
|
||||
to_uuid,
|
||||
reason,
|
||||
rotation_time,
|
||||
};
|
||||
|
||||
if let Err(e) = self.event_sender.send(event) {
|
||||
tracing::debug!("Failed to send credential rotated event: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送凭证自动禁用事件
|
||||
pub async fn emit_credential_auto_disabled(
|
||||
&self,
|
||||
uuid: String,
|
||||
credential_name: Option<String>,
|
||||
reason: String,
|
||||
error_type: String,
|
||||
) {
|
||||
let disable_time = Utc::now();
|
||||
let event = WsKiroEvent::CredentialAutoDisabled {
|
||||
uuid,
|
||||
credential_name,
|
||||
reason,
|
||||
error_type,
|
||||
disable_time,
|
||||
};
|
||||
|
||||
if let Err(e) = self.event_sender.send(event) {
|
||||
tracing::debug!("Failed to send credential auto disabled event: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取当前活跃订阅者数量
|
||||
pub fn subscriber_count(&self) -> usize {
|
||||
self.event_sender.receiver_count()
|
||||
}
|
||||
|
||||
/// 获取缓存的凭证状态
|
||||
pub async fn get_credential_state(&self, uuid: &str) -> Option<CachedCredentialState> {
|
||||
let states = self.credential_states.read().await;
|
||||
states.get(uuid).cloned()
|
||||
}
|
||||
|
||||
/// 获取所有凭证状态
|
||||
pub async fn get_all_credential_states(&self) -> Vec<CachedCredentialState> {
|
||||
let states = self.credential_states.read().await;
|
||||
states.values().cloned().collect()
|
||||
}
|
||||
|
||||
/// 清理过期的凭证状态缓存
|
||||
pub async fn cleanup_expired_states(&self, retention_hours: u64) {
|
||||
let cutoff = Utc::now() - chrono::Duration::hours(retention_hours as i64);
|
||||
let mut states = self.credential_states.write().await;
|
||||
states.retain(|_, state| state.last_updated > cutoff);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KiroEventService {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_credential_status_update_event() {
|
||||
let service = KiroEventService::new();
|
||||
let mut receiver = service.subscribe();
|
||||
|
||||
// 发送凭证状态更新事件
|
||||
let uuid = "test-uuid".to_string();
|
||||
service
|
||||
.emit_credential_status_update(uuid.clone(), true, false, 0, Some(85.5), None)
|
||||
.await;
|
||||
|
||||
// 验证事件被正确接收
|
||||
let event = receiver.recv().await.unwrap();
|
||||
match event {
|
||||
WsKiroEvent::CredentialStatusUpdate {
|
||||
uuid: event_uuid,
|
||||
is_healthy,
|
||||
health_score,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(event_uuid, uuid);
|
||||
assert!(is_healthy);
|
||||
assert_eq!(health_score, Some(85.5));
|
||||
}
|
||||
_ => panic!("Expected CredentialStatusUpdate event"),
|
||||
}
|
||||
|
||||
// 验证状态被缓存
|
||||
let cached_state = service.get_credential_state(&uuid).await.unwrap();
|
||||
assert_eq!(cached_state.uuid, uuid);
|
||||
assert!(cached_state.is_healthy);
|
||||
assert_eq!(cached_state.health_score, Some(85.5));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_refresh_events() {
|
||||
let service = KiroEventService::new();
|
||||
let mut receiver = service.subscribe();
|
||||
|
||||
let uuid = "test-uuid".to_string();
|
||||
let credential_name = Some("test-credential".to_string());
|
||||
|
||||
// 测试刷新开始事件
|
||||
service
|
||||
.emit_refresh_started(uuid.clone(), credential_name.clone())
|
||||
.await;
|
||||
|
||||
let event = receiver.recv().await.unwrap();
|
||||
match event {
|
||||
WsKiroEvent::RefreshStarted {
|
||||
uuid: event_uuid,
|
||||
credential_name: event_name,
|
||||
} => {
|
||||
assert_eq!(event_uuid, uuid);
|
||||
assert_eq!(event_name, credential_name);
|
||||
}
|
||||
_ => panic!("Expected RefreshStarted event"),
|
||||
}
|
||||
|
||||
// 测试刷新成功事件
|
||||
let expires_at = Utc::now() + chrono::Duration::hours(1);
|
||||
service
|
||||
.emit_refresh_success(
|
||||
uuid.clone(),
|
||||
credential_name.clone(),
|
||||
expires_at,
|
||||
"IdC".to_string(),
|
||||
"BuilderId".to_string(),
|
||||
"us-east-1".to_string(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let event = receiver.recv().await.unwrap();
|
||||
match event {
|
||||
WsKiroEvent::RefreshSuccess {
|
||||
uuid: event_uuid,
|
||||
new_token_info,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(event_uuid, uuid);
|
||||
assert_eq!(new_token_info.auth_method, "IdC");
|
||||
assert_eq!(new_token_info.provider, "BuilderId");
|
||||
}
|
||||
_ => panic!("Expected RefreshSuccess event"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_subscribers() {
|
||||
let service = KiroEventService::new();
|
||||
let mut receiver1 = service.subscribe();
|
||||
let mut receiver2 = service.subscribe();
|
||||
|
||||
assert_eq!(service.subscriber_count(), 2);
|
||||
|
||||
// 发送事件
|
||||
service
|
||||
.emit_refresh_started("test-uuid".to_string(), None)
|
||||
.await;
|
||||
|
||||
// 两个订阅者都应该收到事件
|
||||
let event1 = receiver1.recv().await.unwrap();
|
||||
let event2 = receiver2.recv().await.unwrap();
|
||||
|
||||
matches!(event1, WsKiroEvent::RefreshStarted { .. });
|
||||
matches!(event2, WsKiroEvent::RefreshStarted { .. });
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_credential_state_caching() {
|
||||
let service = KiroEventService::new();
|
||||
|
||||
// 添加多个凭证状态
|
||||
service
|
||||
.emit_credential_status_update("uuid1".to_string(), true, false, 0, Some(90.0), None)
|
||||
.await;
|
||||
|
||||
service
|
||||
.emit_credential_status_update("uuid2".to_string(), false, true, 5, Some(30.0), None)
|
||||
.await;
|
||||
|
||||
// 验证所有状态都被缓存
|
||||
let all_states = service.get_all_credential_states().await;
|
||||
assert_eq!(all_states.len(), 2);
|
||||
|
||||
// 验证可以按 UUID 获取特定状态
|
||||
let state1 = service.get_credential_state("uuid1").await.unwrap();
|
||||
assert_eq!(state1.health_score, Some(90.0));
|
||||
|
||||
let state2 = service.get_credential_state("uuid2").await.unwrap();
|
||||
assert_eq!(state2.error_count, 5);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod backup_service;
|
||||
pub mod kiro_event_service;
|
||||
pub mod live_sync;
|
||||
pub mod mcp_service;
|
||||
pub mod mcp_sync;
|
||||
|
||||
@@ -166,7 +166,13 @@ impl ProviderPoolService {
|
||||
ProviderPoolDao::delete(&conn, uuid).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 选择一个可用的凭证(轮询负载均衡)
|
||||
/// 选择一个可用的凭证(智能轮换策略)
|
||||
///
|
||||
/// 增强版轮换策略,考虑以下因素:
|
||||
/// - 健康状态:优先选择健康的凭证
|
||||
/// - 使用频率:优先选择使用次数较少的凭证
|
||||
/// - 错误率:避免选择错误次数过多的凭证
|
||||
/// - 冷却时间:避免短时间内重复使用同一凭证
|
||||
pub fn select_credential(
|
||||
&self,
|
||||
db: &DbConnection,
|
||||
@@ -193,35 +199,95 @@ impl ProviderPoolService {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// 轮询选择
|
||||
let index_key = match model {
|
||||
Some(m) => format!("{}:{}", provider_type, m),
|
||||
None => provider_type.to_string(),
|
||||
};
|
||||
|
||||
let index = {
|
||||
let indices = self.round_robin_index.read().unwrap();
|
||||
indices
|
||||
.get(&index_key)
|
||||
.map(|i| i.load(Ordering::SeqCst))
|
||||
.unwrap_or(0)
|
||||
};
|
||||
|
||||
let selected_index = index % available.len();
|
||||
let selected = available.remove(selected_index);
|
||||
|
||||
// 更新轮询索引
|
||||
{
|
||||
let mut indices = self.round_robin_index.write().unwrap();
|
||||
let counter = indices
|
||||
.entry(index_key)
|
||||
.or_insert_with(|| AtomicUsize::new(0));
|
||||
counter.store((index + 1) % usize::MAX, Ordering::SeqCst);
|
||||
// 如果只有一个可用凭证,直接返回
|
||||
if available.len() == 1 {
|
||||
return Ok(Some(available.into_iter().next().unwrap()));
|
||||
}
|
||||
|
||||
// 智能选择:基于权重分数选择最优凭证
|
||||
let selected = self.select_best_credential_by_weight(&available);
|
||||
|
||||
Ok(Some(selected))
|
||||
}
|
||||
|
||||
/// 基于权重分数选择最优凭证
|
||||
fn select_best_credential_by_weight(
|
||||
&self,
|
||||
credentials: &[ProviderCredential],
|
||||
) -> ProviderCredential {
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let mut best_score = f64::MIN;
|
||||
let mut best_credential = None;
|
||||
|
||||
for cred in credentials {
|
||||
let score = self.calculate_credential_score(cred, now, credentials);
|
||||
if score > best_score {
|
||||
best_score = score;
|
||||
best_credential = Some(cred);
|
||||
}
|
||||
}
|
||||
|
||||
best_credential.unwrap().clone()
|
||||
}
|
||||
|
||||
/// 计算凭证的综合分数(分数越高越优先)
|
||||
fn calculate_credential_score(
|
||||
&self,
|
||||
cred: &ProviderCredential,
|
||||
now: chrono::DateTime<chrono::Utc>,
|
||||
all_credentials: &[ProviderCredential],
|
||||
) -> f64 {
|
||||
let mut score = 0.0;
|
||||
|
||||
// 1. 健康状态权重 (40分)
|
||||
if cred.is_healthy {
|
||||
score += 40.0;
|
||||
} else {
|
||||
score -= 20.0; // 不健康的凭证严重扣分
|
||||
}
|
||||
|
||||
// 2. 使用频率权重 (30分) - 使用次数越少分数越高
|
||||
let max_usage = all_credentials
|
||||
.iter()
|
||||
.map(|c| c.usage_count)
|
||||
.max()
|
||||
.unwrap_or(1);
|
||||
if max_usage > 0 {
|
||||
let usage_ratio = cred.usage_count as f64 / max_usage as f64;
|
||||
score += 30.0 * (1.0 - usage_ratio); // 使用越少分数越高
|
||||
} else {
|
||||
score += 30.0; // 如果都没使用过,给满分
|
||||
}
|
||||
|
||||
// 3. 错误率权重 (20分) - 错误越少分数越高
|
||||
let total_requests = cred.usage_count + cred.error_count as u64;
|
||||
if total_requests > 0 {
|
||||
let error_ratio = cred.error_count as f64 / total_requests as f64;
|
||||
score += 20.0 * (1.0 - error_ratio); // 错误率越低分数越高
|
||||
} else {
|
||||
score += 20.0; // 没有历史记录给满分
|
||||
}
|
||||
|
||||
// 4. 冷却时间权重 (10分) - 距离上次使用时间越长分数越高
|
||||
if let Some(last_used) = &cred.last_used {
|
||||
let duration_since_last_use = now.signed_duration_since(*last_used);
|
||||
let minutes_since_last_use = duration_since_last_use.num_minutes() as f64;
|
||||
|
||||
// 超过5分钟的冷却时间给满分,否则按比例给分
|
||||
let cooldown_score = if minutes_since_last_use >= 5.0 {
|
||||
10.0
|
||||
} else {
|
||||
10.0 * (minutes_since_last_use / 5.0)
|
||||
};
|
||||
score += cooldown_score;
|
||||
} else {
|
||||
score += 10.0; // 从未使用过给满分
|
||||
}
|
||||
|
||||
score
|
||||
}
|
||||
|
||||
/// 记录凭证使用
|
||||
pub fn record_usage(&self, db: &DbConnection, uuid: &str) -> Result<(), String> {
|
||||
let conn = db.lock().map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -14,11 +14,44 @@ use crate::models::provider_pool_model::{
|
||||
use crate::providers::gemini::GeminiProvider;
|
||||
use crate::providers::kiro::KiroProvider;
|
||||
use crate::providers::qwen::QwenProvider;
|
||||
use crate::services::kiro_event_service::KiroEventService;
|
||||
use chrono::Utc;
|
||||
use dashmap::DashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
/// Token 刷新错误类型
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RefreshErrorType {
|
||||
/// Token被截断或格式问题
|
||||
TokenTruncated,
|
||||
/// Token格式异常(长度过短等)
|
||||
TokenFormat,
|
||||
/// 网络连接问题
|
||||
Network,
|
||||
/// 服务不可用
|
||||
ServiceUnavailable,
|
||||
/// 认证失败(401, 403等)
|
||||
AuthenticationFailed,
|
||||
/// 未知错误
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Token 刷新错误分类结果
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RefreshErrorClassification {
|
||||
/// 错误类型
|
||||
pub error_type: RefreshErrorType,
|
||||
/// 错误描述
|
||||
pub error_description: String,
|
||||
/// 建议重试次数
|
||||
pub retry_count: u32,
|
||||
/// 是否支持降级策略
|
||||
pub supports_fallback: bool,
|
||||
/// 是否应该自动禁用凭证(永久性错误)
|
||||
pub should_disable_credential: bool,
|
||||
}
|
||||
|
||||
/// Token 缓存服务
|
||||
pub struct TokenCacheService {
|
||||
/// 每凭证一把锁,防止并发刷新
|
||||
@@ -69,77 +102,144 @@ impl TokenCacheService {
|
||||
match self.refresh_and_cache(db, uuid, false).await {
|
||||
Ok(token) => Ok(token),
|
||||
Err(refresh_error) => {
|
||||
// 刷新失败时,检查是否是因为 refreshToken 被截断
|
||||
// 如果是,尝试直接使用源文件中的 accessToken(可能仍然有效)
|
||||
if refresh_error.contains("截断") || refresh_error.contains("truncated") {
|
||||
tracing::warn!(
|
||||
"[TOKEN_CACHE] refreshToken 被截断,尝试使用源文件中的 accessToken: {}",
|
||||
&uuid[..8]
|
||||
);
|
||||
// 增强的错误处理机制 - 智能检测各种token问题
|
||||
let error_classification = self.classify_refresh_error(&refresh_error);
|
||||
|
||||
// 获取凭证信息
|
||||
let credential = {
|
||||
let conn = db.lock().map_err(|e| e.to_string())?;
|
||||
ProviderPoolDao::get_by_uuid(&conn, uuid)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| format!("Credential not found: {}", uuid))?
|
||||
};
|
||||
tracing::warn!(
|
||||
"[TOKEN_CACHE] Token 刷新失败,错误类型: {:?}, 详情: {}",
|
||||
error_classification.error_type,
|
||||
&refresh_error
|
||||
);
|
||||
|
||||
// 尝试从源文件读取 accessToken
|
||||
match self.read_token_from_source(&credential).await {
|
||||
Ok(token_info) => {
|
||||
if let Some(token) = token_info.access_token {
|
||||
match error_classification.error_type {
|
||||
RefreshErrorType::TokenTruncated | RefreshErrorType::TokenFormat => {
|
||||
tracing::warn!(
|
||||
"[TOKEN_CACHE] 检测到 token 问题,尝试使用源文件中的 accessToken: {}",
|
||||
&uuid[..8]
|
||||
);
|
||||
|
||||
// 获取凭证信息
|
||||
let credential = {
|
||||
let conn = db.lock().map_err(|e| e.to_string())?;
|
||||
ProviderPoolDao::get_by_uuid(&conn, uuid)
|
||||
.map_err(|e| e.to_string())?
|
||||
.ok_or_else(|| format!("Credential not found: {}", uuid))?
|
||||
};
|
||||
|
||||
// 尝试从源文件读取 accessToken
|
||||
match self.read_token_from_source(&credential).await {
|
||||
Ok(token_info) => {
|
||||
if let Some(token) = token_info.access_token {
|
||||
tracing::info!(
|
||||
"[TOKEN_CACHE] 使用源文件中的 accessToken 作为降级方案: {}",
|
||||
&uuid[..8]
|
||||
);
|
||||
|
||||
// 缓存这个 token 但标记为降级状态
|
||||
let cache_info = CachedTokenInfo {
|
||||
access_token: Some(token.clone()),
|
||||
refresh_token: token_info.refresh_token,
|
||||
expiry_time: None, // 无法确定过期时间
|
||||
last_refresh: Some(Utc::now()),
|
||||
refresh_error_count: error_classification.retry_count,
|
||||
last_refresh_error: Some(format!(
|
||||
"{}(降级使用源文件 accessToken): {}",
|
||||
error_classification.error_description, refresh_error
|
||||
)),
|
||||
};
|
||||
|
||||
// 缓存到数据库
|
||||
if let Ok(conn) = db.lock() {
|
||||
let _ = ProviderPoolDao::update_token_cache(
|
||||
&conn,
|
||||
uuid,
|
||||
&cache_info,
|
||||
);
|
||||
}
|
||||
|
||||
return Ok(token);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
"[TOKEN_CACHE] 降级策略失败,无法从源文件读取 accessToken: {}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
RefreshErrorType::Network | RefreshErrorType::ServiceUnavailable => {
|
||||
tracing::warn!("[TOKEN_CACHE] 网络/服务问题,建议稍后重试: {}", &uuid[..8]);
|
||||
// 可以考虑使用缓存中的过期 token 作为临时方案
|
||||
if let Some(cache) = cached {
|
||||
if let Some(token) = cache.access_token {
|
||||
tracing::info!(
|
||||
"[TOKEN_CACHE] 使用源文件中的 accessToken(可能已过期): {}",
|
||||
"[TOKEN_CACHE] 网络问题时使用过期缓存 token: {}",
|
||||
&uuid[..8]
|
||||
);
|
||||
// 注意:这个 token 可能已过期,但至少可以尝试使用
|
||||
// 缓存这个 token(但不设置过期时间,因为我们不知道它何时过期)
|
||||
let cache_info = CachedTokenInfo {
|
||||
access_token: Some(token.clone()),
|
||||
refresh_token: token_info.refresh_token,
|
||||
expiry_time: None, // 不知道过期时间
|
||||
last_refresh: Some(Utc::now()),
|
||||
refresh_error_count: 1,
|
||||
last_refresh_error: Some(format!(
|
||||
"refreshToken 被截断,使用源文件 accessToken: {}",
|
||||
refresh_error
|
||||
)),
|
||||
};
|
||||
|
||||
// 缓存到数据库
|
||||
if let Ok(conn) = db.lock() {
|
||||
let _ = ProviderPoolDao::update_token_cache(
|
||||
&conn,
|
||||
uuid,
|
||||
&cache_info,
|
||||
);
|
||||
}
|
||||
|
||||
return Ok(token);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("[TOKEN_CACHE] 无法从源文件读取 accessToken: {}", e);
|
||||
}
|
||||
}
|
||||
RefreshErrorType::AuthenticationFailed => {
|
||||
tracing::error!("[TOKEN_CACHE] 认证失败,凭证可能已被撤销: {}", &uuid[..8]);
|
||||
// 认证失败通常需要用户重新授权,不进行降级
|
||||
}
|
||||
RefreshErrorType::Unknown => {
|
||||
tracing::warn!("[TOKEN_CACHE] 未知错误类型,使用默认处理: {}", &uuid[..8]);
|
||||
}
|
||||
}
|
||||
|
||||
// 返回原始刷新错误
|
||||
Err(refresh_error)
|
||||
// 更新错误计数
|
||||
if let Ok(conn) = db.lock() {
|
||||
let _ = ProviderPoolDao::record_token_refresh_error(
|
||||
&conn,
|
||||
uuid,
|
||||
&format!(
|
||||
"{}(分类: {:?}): {}",
|
||||
error_classification.error_description,
|
||||
error_classification.error_type,
|
||||
refresh_error
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 返回分类后的错误信息
|
||||
Err(format!(
|
||||
"{}: {}",
|
||||
error_classification.error_description, refresh_error
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新 Token 并缓存到数据库
|
||||
/// 刷新 Token 并缓存到数据库(带事件发送)
|
||||
///
|
||||
/// - force: 是否强制刷新(忽略缓存状态)
|
||||
pub async fn refresh_and_cache(
|
||||
/// - kiro_event_service: 可选的事件服务,用于发送 Kiro 凭证刷新事件
|
||||
///
|
||||
/// 优化说明:添加了随机延迟机制,避免多个凭证同时刷新造成请求过于集中
|
||||
pub async fn refresh_and_cache_with_events(
|
||||
&self,
|
||||
db: &DbConnection,
|
||||
uuid: &str,
|
||||
force: bool,
|
||||
kiro_event_service: Option<Arc<KiroEventService>>,
|
||||
) -> Result<String, String> {
|
||||
// 添加随机延迟,避免多个凭证同时刷新
|
||||
// 基于凭证UUID生成0-30秒的随机延迟,确保同一凭证的延迟时间一致但不同凭证间分散
|
||||
if !force {
|
||||
let delay_ms = self.calculate_refresh_delay(uuid);
|
||||
if delay_ms > 0 {
|
||||
tracing::debug!(
|
||||
"[TOKEN_CACHE] Adding {}ms delay before refreshing token for {}",
|
||||
delay_ms,
|
||||
&uuid[..8]
|
||||
);
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取该凭证的锁
|
||||
let lock = self
|
||||
.locks
|
||||
@@ -183,6 +283,15 @@ impl TokenCacheService {
|
||||
credential.provider_type
|
||||
);
|
||||
|
||||
// 发送刷新开始事件(仅针对 Kiro 凭证)
|
||||
if let Some(event_service) = &kiro_event_service {
|
||||
if credential.provider_type == PoolProviderType::Kiro {
|
||||
event_service
|
||||
.emit_refresh_started(uuid.to_string(), credential.name.clone())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// 执行刷新
|
||||
match self.do_refresh(&credential).await {
|
||||
Ok(token_info) => {
|
||||
@@ -203,6 +312,24 @@ impl TokenCacheService {
|
||||
token_info.expiry_time
|
||||
);
|
||||
|
||||
// 发送刷新成功事件(仅针对 Kiro 凭证)
|
||||
if let Some(event_service) = &kiro_event_service {
|
||||
if credential.provider_type == PoolProviderType::Kiro {
|
||||
event_service
|
||||
.emit_refresh_success(
|
||||
uuid.to_string(),
|
||||
credential.name.clone(),
|
||||
token_info
|
||||
.expiry_time
|
||||
.unwrap_or_else(|| Utc::now() + chrono::Duration::hours(1)),
|
||||
"IdC".to_string(), // 默认为IdC认证
|
||||
"BuilderId".to_string(),
|
||||
"us-east-1".to_string(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(token)
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -218,6 +345,77 @@ impl TokenCacheService {
|
||||
e
|
||||
);
|
||||
|
||||
// 分析错误并决定是否自动禁用凭证
|
||||
let error_classification = self.classify_refresh_error(&e);
|
||||
|
||||
// 如果是永久性错误,自动禁用凭证
|
||||
if error_classification.should_disable_credential {
|
||||
let disable_result = {
|
||||
let conn = db.lock().map_err(|e| e.to_string())?;
|
||||
// 简化禁用逻辑:直接在数据库中标记为禁用
|
||||
let sql = "UPDATE credentials SET is_disabled = true WHERE uuid = ?";
|
||||
conn.execute(sql, &[&uuid]).map_err(|e| e.to_string())
|
||||
};
|
||||
|
||||
match disable_result {
|
||||
Ok(_) => {
|
||||
tracing::warn!(
|
||||
"[TOKEN_CACHE] Auto-disabled credential {} due to permanent failure: {:?}",
|
||||
&uuid[..8],
|
||||
error_classification.error_type
|
||||
);
|
||||
|
||||
// 发送凭证禁用事件
|
||||
if let Some(event_service) = &kiro_event_service {
|
||||
if credential.provider_type == PoolProviderType::Kiro {
|
||||
// 发送状态更新事件
|
||||
event_service
|
||||
.emit_credential_status_update(
|
||||
uuid.to_string(),
|
||||
false, // is_healthy
|
||||
true, // is_disabled
|
||||
credential.error_count + 1,
|
||||
Some(0.0), // health_score降为0
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
// 发送自动禁用事件
|
||||
event_service
|
||||
.emit_credential_auto_disabled(
|
||||
uuid.to_string(),
|
||||
credential.name.clone(),
|
||||
error_classification.error_description.clone(),
|
||||
format!("{:?}", error_classification.error_type),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(disable_err) => {
|
||||
tracing::error!(
|
||||
"[TOKEN_CACHE] Failed to auto-disable credential {}: {}",
|
||||
&uuid[..8],
|
||||
disable_err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发送刷新失败事件(仅针对 Kiro 凭证)
|
||||
if let Some(event_service) = &kiro_event_service {
|
||||
if credential.provider_type == PoolProviderType::Kiro {
|
||||
event_service
|
||||
.emit_refresh_failed(
|
||||
uuid.to_string(),
|
||||
credential.name.clone(),
|
||||
e.clone(),
|
||||
Some(format!("{:?}", error_classification.error_type)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
@@ -883,4 +1081,135 @@ impl TokenCacheService {
|
||||
let conn = db.lock().map_err(|e| e.to_string())?;
|
||||
ProviderPoolDao::get_token_cache(&conn, uuid).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 计算刷新延迟时间(毫秒)
|
||||
///
|
||||
/// 基于凭证UUID生成确定性但分散的延迟时间,避免多个凭证同时刷新
|
||||
/// 延迟范围:0-30秒,确保同一凭证每次的延迟一致
|
||||
fn calculate_refresh_delay(&self, uuid: &str) -> u64 {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
// 使用凭证UUID作为种子生成确定性的延迟
|
||||
let mut hasher = DefaultHasher::new();
|
||||
uuid.hash(&mut hasher);
|
||||
let hash_value = hasher.finish();
|
||||
|
||||
// 生成0-30秒的延迟(转换为毫秒)
|
||||
(hash_value % 30000) as u64
|
||||
}
|
||||
|
||||
/// 智能错误分类方法
|
||||
///
|
||||
/// 基于错误信息智能识别错误类型,提供针对性的处理建议
|
||||
fn classify_refresh_error(&self, error_message: &str) -> RefreshErrorClassification {
|
||||
let error_lower = error_message.to_lowercase();
|
||||
|
||||
// Token 被截断问题检测(最严重的问题,优先检查)
|
||||
if error_lower.contains("截断") || error_lower.contains("truncated") {
|
||||
return RefreshErrorClassification {
|
||||
error_type: RefreshErrorType::TokenTruncated,
|
||||
error_description: "Token 被截断,需检查配置文件".to_string(),
|
||||
retry_count: 1,
|
||||
supports_fallback: true,
|
||||
should_disable_credential: true, // 永久性问题,自动禁用
|
||||
};
|
||||
}
|
||||
|
||||
// Token 格式问题检测
|
||||
if error_lower.contains("格式异常")
|
||||
|| error_lower.contains("长度过短")
|
||||
|| error_lower.contains("format")
|
||||
|| error_lower.contains("invalid")
|
||||
|| error_lower.contains("malformed")
|
||||
{
|
||||
return RefreshErrorClassification {
|
||||
error_type: RefreshErrorType::TokenFormat,
|
||||
error_description: "Token 格式异常,需重新配置".to_string(),
|
||||
retry_count: 1,
|
||||
supports_fallback: true,
|
||||
should_disable_credential: true, // 配置问题,自动禁用
|
||||
};
|
||||
}
|
||||
|
||||
// 认证失败检测
|
||||
if error_lower.contains("unauthorized")
|
||||
|| error_lower.contains("forbidden")
|
||||
|| error_lower.contains("401")
|
||||
|| error_lower.contains("403")
|
||||
|| error_lower.contains("认证失败")
|
||||
|| error_lower.contains("invalid_grant")
|
||||
|| error_lower.contains("access_denied")
|
||||
|| error_lower.contains("refresh_token")
|
||||
|| error_lower.contains("expired")
|
||||
{
|
||||
return RefreshErrorClassification {
|
||||
error_type: RefreshErrorType::AuthenticationFailed,
|
||||
error_description: "认证失败,凭证已过期或无效".to_string(),
|
||||
retry_count: 0, // 不建议重试
|
||||
supports_fallback: false,
|
||||
should_disable_credential: true, // 认证失效,自动禁用
|
||||
};
|
||||
}
|
||||
|
||||
// 网络问题检测
|
||||
if error_lower.contains("network")
|
||||
|| error_lower.contains("connection")
|
||||
|| error_lower.contains("timeout")
|
||||
|| error_lower.contains("dns")
|
||||
|| error_lower.contains("connect")
|
||||
|| error_lower.contains("网络")
|
||||
|| error_lower.contains("连接")
|
||||
{
|
||||
return RefreshErrorClassification {
|
||||
error_type: RefreshErrorType::Network,
|
||||
error_description: "网络连接问题".to_string(),
|
||||
retry_count: 3,
|
||||
supports_fallback: true,
|
||||
should_disable_credential: false, // 临时问题,不禁用
|
||||
};
|
||||
}
|
||||
|
||||
// 服务不可用检测
|
||||
if error_lower.contains("service unavailable")
|
||||
|| error_lower.contains("502")
|
||||
|| error_lower.contains("503")
|
||||
|| error_lower.contains("504")
|
||||
|| error_lower.contains("internal server error")
|
||||
|| error_lower.contains("服务不可用")
|
||||
{
|
||||
return RefreshErrorClassification {
|
||||
error_type: RefreshErrorType::ServiceUnavailable,
|
||||
error_description: "服务暂时不可用".to_string(),
|
||||
retry_count: 2,
|
||||
supports_fallback: true,
|
||||
should_disable_credential: false, // 临时问题,不禁用
|
||||
};
|
||||
}
|
||||
|
||||
// 未知错误(默认分类)
|
||||
RefreshErrorClassification {
|
||||
error_type: RefreshErrorType::Unknown,
|
||||
error_description: "未知错误".to_string(),
|
||||
retry_count: 1,
|
||||
supports_fallback: false,
|
||||
should_disable_credential: false, // 未知错误暂不自动禁用
|
||||
}
|
||||
}
|
||||
|
||||
/// 刷新 Token 并缓存到数据库(兼容版本)
|
||||
///
|
||||
/// - force: 是否强制刷新(忽略缓存状态)
|
||||
///
|
||||
/// 此方法保持与旧版本的兼容性,不发送任何事件。
|
||||
/// 如需事件支持,请使用 refresh_and_cache_with_events 方法。
|
||||
pub async fn refresh_and_cache(
|
||||
&self,
|
||||
db: &DbConnection,
|
||||
uuid: &str,
|
||||
force: bool,
|
||||
) -> Result<String, String> {
|
||||
self.refresh_and_cache_with_events(db, uuid, force, None)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,6 +261,20 @@ async fn handle_message(
|
||||
"FlowEvent messages are server-to-client only",
|
||||
)))
|
||||
}
|
||||
WsMessage::SubscribeKiroEvents => {
|
||||
// TODO: 实现Kiro事件订阅
|
||||
None
|
||||
}
|
||||
WsMessage::UnsubscribeKiroEvents => {
|
||||
// TODO: 实现Kiro事件取消订阅
|
||||
None
|
||||
}
|
||||
WsMessage::KiroCredentialEvent(_) => {
|
||||
// Kiro事件是服务端到客户端的消息,客户端不应该发送
|
||||
Some(WsMessage::Error(WsError::invalid_message(
|
||||
"KiroCredentialEvent messages are server-to-client only",
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,9 @@ pub use lifecycle::{
|
||||
pub use processor::MessageProcessor;
|
||||
pub use stream::{BackpressureController, StreamForwarder};
|
||||
pub use types::{
|
||||
WsApiRequest, WsApiResponse, WsConfig, WsConnection, WsConnectionStatus, WsEndpoint, WsError,
|
||||
WsErrorCode, WsFlowEvent, WsMessage, WsStats, WsStatsSnapshot, WsStreamChunk, WsStreamEnd,
|
||||
KiroTokenInfo, WsApiRequest, WsApiResponse, WsConfig, WsConnection, WsConnectionStatus,
|
||||
WsEndpoint, WsError, WsErrorCode, WsFlowEvent, WsKiroEvent, WsMessage, WsStats,
|
||||
WsStatsSnapshot, WsStreamChunk, WsStreamEnd,
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
|
||||
@@ -80,6 +80,12 @@ pub enum WsMessage {
|
||||
UnsubscribeFlowEvents,
|
||||
/// Flow 事件通知
|
||||
FlowEvent(WsFlowEvent),
|
||||
/// 订阅 Kiro 凭证状态事件
|
||||
SubscribeKiroEvents,
|
||||
/// 取消订阅 Kiro 凭证状态事件
|
||||
UnsubscribeKiroEvents,
|
||||
/// Kiro 凭证状态事件通知
|
||||
KiroCredentialEvent(WsKiroEvent),
|
||||
}
|
||||
|
||||
/// WebSocket API 请求
|
||||
@@ -369,3 +375,78 @@ impl From<FlowEvent> for WsFlowEvent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// WebSocket Kiro 凭证事件
|
||||
///
|
||||
/// 用于通过 WebSocket 推送 Kiro 凭证状态变化
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "event_type", rename_all = "snake_case")]
|
||||
pub enum WsKiroEvent {
|
||||
/// 凭证状态更新
|
||||
CredentialStatusUpdate {
|
||||
uuid: String,
|
||||
is_healthy: bool,
|
||||
is_disabled: bool,
|
||||
error_count: u32,
|
||||
health_score: Option<f64>,
|
||||
last_used: Option<DateTime<Utc>>,
|
||||
},
|
||||
/// 凭证刷新开始
|
||||
RefreshStarted {
|
||||
uuid: String,
|
||||
credential_name: Option<String>,
|
||||
},
|
||||
/// 凭证刷新成功
|
||||
RefreshSuccess {
|
||||
uuid: String,
|
||||
credential_name: Option<String>,
|
||||
new_token_info: KiroTokenInfo,
|
||||
},
|
||||
/// 凭证刷新失败
|
||||
RefreshFailed {
|
||||
uuid: String,
|
||||
credential_name: Option<String>,
|
||||
error: String,
|
||||
error_code: Option<String>,
|
||||
},
|
||||
/// 凭证健康检查完成
|
||||
HealthCheckCompleted {
|
||||
uuid: String,
|
||||
credential_name: Option<String>,
|
||||
is_healthy: bool,
|
||||
health_score: Option<f64>,
|
||||
last_check: DateTime<Utc>,
|
||||
},
|
||||
/// 凭证池统计更新
|
||||
PoolStatsUpdate {
|
||||
total_credentials: u32,
|
||||
healthy_credentials: u32,
|
||||
available_credentials: u32,
|
||||
average_health_score: Option<f64>,
|
||||
last_rotation: Option<DateTime<Utc>>,
|
||||
},
|
||||
/// 凭证轮换事件
|
||||
CredentialRotated {
|
||||
from_uuid: Option<String>,
|
||||
to_uuid: String,
|
||||
reason: String,
|
||||
rotation_time: DateTime<Utc>,
|
||||
},
|
||||
/// 凭证自动禁用事件
|
||||
CredentialAutoDisabled {
|
||||
uuid: String,
|
||||
credential_name: Option<String>,
|
||||
reason: String,
|
||||
error_type: String,
|
||||
disable_time: DateTime<Utc>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Kiro Token 信息(用于刷新成功事件)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct KiroTokenInfo {
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub auth_method: String,
|
||||
pub provider: String,
|
||||
pub region: String,
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import type {
|
||||
import {
|
||||
getKiroCredentialFingerprint,
|
||||
type KiroFingerprintInfo,
|
||||
kiroCredentialApi,
|
||||
} from "@/lib/api/providerPool";
|
||||
import { usageApi, type UsageInfo } from "@/lib/api/usage";
|
||||
import { UsageDisplay } from "./UsageDisplay";
|
||||
@@ -74,6 +75,12 @@ export function CredentialCard({
|
||||
const [fingerprintExpanded, setFingerprintExpanded] = useState(false);
|
||||
const [fingerprintCopied, setFingerprintCopied] = useState(false);
|
||||
|
||||
// Kiro 增强状态管理
|
||||
const [kiroHealthScore, setKiroHealthScore] = useState<number | null>(null);
|
||||
const [kiroStatusLoading, setKiroStatusLoading] = useState(false);
|
||||
const [kiroRefreshing, setKiroRefreshing] = useState(false);
|
||||
const [kiroStatusExpanded, setKiroStatusExpanded] = useState(false);
|
||||
|
||||
// 查询指纹信息
|
||||
const handleCheckFingerprint = async () => {
|
||||
if (fingerprintExpanded && fingerprintInfo) {
|
||||
@@ -129,6 +136,51 @@ export function CredentialCard({
|
||||
}
|
||||
};
|
||||
|
||||
// 获取 Kiro 详细状态
|
||||
const handleCheckKiroStatus = async () => {
|
||||
if (kiroStatusExpanded) {
|
||||
setKiroStatusExpanded(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setKiroStatusExpanded(true);
|
||||
setKiroStatusLoading(true);
|
||||
|
||||
try {
|
||||
const status = await kiroCredentialApi.getCredentialStatus(
|
||||
credential.uuid,
|
||||
);
|
||||
setKiroHealthScore(status.health_score || 0);
|
||||
} catch (e) {
|
||||
console.error("获取 Kiro 状态失败:", e);
|
||||
} finally {
|
||||
setKiroStatusLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 快速刷新 Kiro Token
|
||||
const handleQuickRefresh = async () => {
|
||||
setKiroRefreshing(true);
|
||||
|
||||
try {
|
||||
const result = await kiroCredentialApi.refreshCredential(credential.uuid);
|
||||
if (result.success) {
|
||||
// 刷新成功,可以显示成功消息
|
||||
console.log("Token 刷新成功:", result.message);
|
||||
// 可以触发页面数据刷新
|
||||
if (onRefreshToken) {
|
||||
onRefreshToken();
|
||||
}
|
||||
} else {
|
||||
console.error("Token 刷新失败:", result.error || result.message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Token 刷新异常:", e);
|
||||
} finally {
|
||||
setKiroRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateStr?: string) => {
|
||||
if (!dateStr) return "从未";
|
||||
const date = new Date(dateStr);
|
||||
@@ -334,6 +386,42 @@ export function CredentialCard({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Kiro 详细状态按钮 - 仅 Kiro 凭证显示 */}
|
||||
{isKiroCredential && (
|
||||
<button
|
||||
onClick={handleCheckKiroStatus}
|
||||
disabled={kiroStatusLoading}
|
||||
className={`rounded-lg p-2.5 transition-colors ${
|
||||
kiroStatusExpanded
|
||||
? "bg-emerald-200 text-emerald-800 dark:bg-emerald-800 dark:text-emerald-200"
|
||||
: "bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400"
|
||||
} disabled:opacity-50`}
|
||||
title="查看详细状态和健康分数"
|
||||
>
|
||||
<Activity
|
||||
className={`h-4 w-4 ${kiroStatusLoading ? "animate-pulse" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Kiro 快速刷新按钮 - 仅 Kiro 凭证显示 */}
|
||||
{isKiroCredential && (
|
||||
<button
|
||||
onClick={handleQuickRefresh}
|
||||
disabled={kiroRefreshing}
|
||||
className={`rounded-lg p-2.5 transition-colors ${
|
||||
kiroRefreshing
|
||||
? "bg-blue-200 text-blue-800 dark:bg-blue-800 dark:text-blue-200"
|
||||
: "bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400"
|
||||
} disabled:opacity-50`}
|
||||
title="快速刷新 Token"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${kiroRefreshing ? "animate-spin" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onReset}
|
||||
className="rounded-lg bg-orange-100 p-2.5 text-orange-700 hover:bg-orange-200 dark:bg-orange-900/30 dark:text-orange-400 transition-colors"
|
||||
@@ -430,8 +518,42 @@ export function CredentialCard({
|
||||
<div /> /* 占位 */
|
||||
)}
|
||||
|
||||
{/* 健康检查 */}
|
||||
{credential.last_health_check_time ? (
|
||||
{/* 健康检查/健康分数 */}
|
||||
{isKiroCredential && kiroHealthScore !== null ? (
|
||||
// 为 Kiro 凭证显示健康分数
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={`h-5 w-5 shrink-0 rounded-full flex items-center justify-center text-xs font-bold text-white ${
|
||||
kiroHealthScore >= 80
|
||||
? "bg-green-500"
|
||||
: kiroHealthScore >= 60
|
||||
? "bg-yellow-500"
|
||||
: kiroHealthScore >= 40
|
||||
? "bg-orange-500"
|
||||
: "bg-red-500"
|
||||
}`}
|
||||
>
|
||||
★
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">健康分数</div>
|
||||
<div
|
||||
className={`font-bold text-xl tabular-nums ${
|
||||
kiroHealthScore >= 80
|
||||
? "text-green-600 dark:text-green-400"
|
||||
: kiroHealthScore >= 60
|
||||
? "text-yellow-600 dark:text-yellow-400"
|
||||
: kiroHealthScore >= 40
|
||||
? "text-orange-600 dark:text-orange-400"
|
||||
: "text-red-600 dark:text-red-400"
|
||||
}`}
|
||||
>
|
||||
{Math.round(kiroHealthScore)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : credential.last_health_check_time ? (
|
||||
// 为其他凭证显示健康检查时间
|
||||
<div className="flex items-center gap-3">
|
||||
<Activity className="h-5 w-5 text-emerald-500 shrink-0" />
|
||||
<div>
|
||||
@@ -564,6 +686,155 @@ export function CredentialCard({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Kiro 详细状态面板 - 仅 Kiro 凭证 */}
|
||||
{isKiroCredential && kiroStatusExpanded && (
|
||||
<div className="mx-4 mb-4 p-4 rounded-lg bg-emerald-50 dark:bg-emerald-950/30 border border-emerald-200 dark:border-emerald-800">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-medium text-emerald-700 dark:text-emerald-300 flex items-center gap-2">
|
||||
<Activity className="h-4 w-4" />
|
||||
Kiro 详细状态
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setKiroStatusExpanded(false)}
|
||||
className="text-emerald-500 hover:text-emerald-700 dark:hover:text-emerald-300"
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{kiroStatusLoading ? (
|
||||
<div className="flex items-center gap-2 text-sm text-emerald-600 dark:text-emerald-400">
|
||||
<div className="animate-spin h-4 w-4 border-2 border-current border-t-transparent rounded-full" />
|
||||
加载中...
|
||||
</div>
|
||||
) : kiroHealthScore !== null ? (
|
||||
<div className="space-y-4">
|
||||
{/* 健康分数详情 */}
|
||||
<div className="bg-white dark:bg-emerald-950/50 rounded-lg p-3 border border-emerald-200 dark:border-emerald-800">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-emerald-700 dark:text-emerald-300">
|
||||
健康分数
|
||||
</span>
|
||||
<div
|
||||
className={`px-3 py-1 rounded-full text-sm font-bold ${
|
||||
kiroHealthScore >= 80
|
||||
? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
|
||||
: kiroHealthScore >= 60
|
||||
? "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
|
||||
: kiroHealthScore >= 40
|
||||
? "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400"
|
||||
: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
|
||||
}`}
|
||||
>
|
||||
{Math.round(kiroHealthScore)} / 100
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 健康分数条 */}
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all duration-300 ${
|
||||
kiroHealthScore >= 80
|
||||
? "bg-green-500"
|
||||
: kiroHealthScore >= 60
|
||||
? "bg-yellow-500"
|
||||
: kiroHealthScore >= 40
|
||||
? "bg-orange-500"
|
||||
: "bg-red-500"
|
||||
}`}
|
||||
style={{
|
||||
width: `${Math.max(0, Math.min(100, kiroHealthScore))}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
{/* 健康状态描述 */}
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
{credential.is_disabled
|
||||
? "凭证已被自动禁用,需手动重新启用"
|
||||
: kiroHealthScore >= 80
|
||||
? "凭证状态良好,可正常使用"
|
||||
: kiroHealthScore >= 60
|
||||
? "凭证状态一般,建议注意监控"
|
||||
: kiroHealthScore >= 40
|
||||
? "凭证状态较差,可能有风险"
|
||||
: "凭证状态异常,需要立即处理"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 状态指标 */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="bg-white dark:bg-emerald-950/50 rounded-lg p-3 border border-emerald-200 dark:border-emerald-800">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Timer className="h-4 w-4 text-emerald-600" />
|
||||
<span className="text-sm font-medium text-emerald-700 dark:text-emerald-300">
|
||||
冷却时间
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
根据使用频率计算的建议等待时间
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-emerald-950/50 rounded-lg p-3 border border-emerald-200 dark:border-emerald-800">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<BarChart3 className="h-4 w-4 text-emerald-600" />
|
||||
<span className="text-sm font-medium text-emerald-700 dark:text-emerald-300">
|
||||
使用权重
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
在轮询池中的权重分配
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 快速操作 */}
|
||||
<div className="flex gap-2 pt-2 border-t border-emerald-200 dark:border-emerald-800">
|
||||
{credential.is_disabled ? (
|
||||
// 已禁用凭证显示重新启用按钮
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700 transition-colors text-sm"
|
||||
>
|
||||
<Power className="h-4 w-4" />
|
||||
重新启用
|
||||
</button>
|
||||
) : (
|
||||
// 正常凭证显示刷新和检查按钮
|
||||
<>
|
||||
<button
|
||||
onClick={handleQuickRefresh}
|
||||
disabled={kiroRefreshing}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-emerald-600 text-white rounded-md hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm"
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${kiroRefreshing ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{kiroRefreshing ? "刷新中..." : "立即刷新"}
|
||||
</button>
|
||||
<button
|
||||
onClick={onCheckHealth}
|
||||
disabled={checkingHealth}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-white dark:bg-emerald-950/50 border border-emerald-300 dark:border-emerald-700 text-emerald-700 dark:text-emerald-300 rounded-md hover:bg-emerald-50 dark:hover:bg-emerald-900/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm"
|
||||
>
|
||||
<Activity
|
||||
className={`h-4 w-4 ${checkingHealth ? "animate-pulse" : ""}`}
|
||||
/>
|
||||
{checkingHealth ? "检查中..." : "重新检查"}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
无法获取状态信息,请重试
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 用量信息展示区域 - 仅 Kiro 凭证 */}
|
||||
{isKiroCredential && usageExpanded && (
|
||||
<div className="mx-4 mb-4 p-4 rounded-lg bg-cyan-50 dark:bg-cyan-950/30 border border-cyan-200 dark:border-cyan-800">
|
||||
|
||||
@@ -598,6 +598,134 @@ export async function getKiroCredentialFingerprint(
|
||||
return invoke("get_kiro_credential_fingerprint", { uuid });
|
||||
}
|
||||
|
||||
// ============ Kiro 凭证池管理 HTTP API ============
|
||||
|
||||
/** 可用凭证信息 */
|
||||
export interface AvailableCredential {
|
||||
/** 凭证UUID */
|
||||
uuid: string;
|
||||
/** 凭证名称 */
|
||||
name: string;
|
||||
/** 是否可用 */
|
||||
available: boolean;
|
||||
/** Token过期时间 */
|
||||
expires_at?: string;
|
||||
/** 最后使用时间 */
|
||||
last_used?: string;
|
||||
/** 健康状态分数 (0-100) */
|
||||
health_score: number;
|
||||
/** 错误计数 */
|
||||
error_count: number;
|
||||
/** 最后错误信息 */
|
||||
last_error?: string;
|
||||
}
|
||||
|
||||
/** 获取可用凭证列表的响应 */
|
||||
export interface AvailableCredentialsResponse {
|
||||
/** 可用凭证列表 */
|
||||
credentials: AvailableCredential[];
|
||||
/** 总凭证数 */
|
||||
total: number;
|
||||
/** 可用凭证数 */
|
||||
available: number;
|
||||
/** 系统状态 */
|
||||
status: string;
|
||||
}
|
||||
|
||||
/** 选择凭证请求参数 */
|
||||
export interface SelectCredentialRequest {
|
||||
/** 指定模型(可选) */
|
||||
model?: string;
|
||||
/** 强制选择特定UUID(可选) */
|
||||
force_uuid?: string;
|
||||
}
|
||||
|
||||
/** 选择凭证响应 */
|
||||
export interface SelectCredentialResponse {
|
||||
/** 选中��凭证UUID */
|
||||
uuid: string;
|
||||
/** 凭证名称 */
|
||||
name: string;
|
||||
/** Access Token(脱敏显示) */
|
||||
access_token_preview: string;
|
||||
/** Token过期时间 */
|
||||
expires_at?: string;
|
||||
/** 选择原因 */
|
||||
selection_reason: string;
|
||||
}
|
||||
|
||||
/** 刷新凭证响应 */
|
||||
export interface RefreshCredentialResponse {
|
||||
/** 凭证UUID */
|
||||
uuid: string;
|
||||
/** 刷新是否成功 */
|
||||
success: boolean;
|
||||
/** 新的过期时间 */
|
||||
new_expires_at?: string;
|
||||
/** 刷新结果信息 */
|
||||
message: string;
|
||||
/** 错误信息(如果有) */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Kiro 凭证池管理 API */
|
||||
export const kiroCredentialApi = {
|
||||
/** 获取可用凭证列表 */
|
||||
async getAvailableCredentials(): Promise<AvailableCredentialsResponse> {
|
||||
const response = await fetch("/api/kiro/credentials/available");
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/** 智能选择凭证 */
|
||||
async selectCredential(
|
||||
request: SelectCredentialRequest,
|
||||
): Promise<SelectCredentialResponse> {
|
||||
const response = await fetch("/api/kiro/credentials/select", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.message || `HTTP ${response.status}: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/** 手动刷新指定凭证 */
|
||||
async refreshCredential(uuid: string): Promise<RefreshCredentialResponse> {
|
||||
const response = await fetch(`/api/kiro/credentials/${uuid}/refresh`, {
|
||||
method: "PUT",
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.message || `HTTP ${response.status}: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/** 获取凭证详细状态 */
|
||||
async getCredentialStatus(uuid: string): Promise<Record<string, any>> {
|
||||
const response = await fetch(`/api/kiro/credentials/${uuid}/status`);
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(
|
||||
errorData.message || `HTTP ${response.status}: ${response.statusText}`,
|
||||
);
|
||||
}
|
||||
return response.json();
|
||||
},
|
||||
};
|
||||
|
||||
// ============ Playwright 指纹浏览器登录 ============
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user