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:
coso
2025-12-24 21:09:47 +08:00
parent 05b2e24484
commit b26e222e43
17 changed files with 2135 additions and 109 deletions
+2 -20
View File
@@ -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",
+8 -6
View File
@@ -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))
+12
View File
@@ -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)
}
+21 -3
View File
@@ -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)
}
+2
View File
@@ -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",
)))
}
}
}
+33 -2
View File
@@ -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
View File
@@ -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;
+91 -25
View File
@@ -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())?;
+378 -49
View File
@@ -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
}
}
+14
View File
@@ -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",
)))
}
}
}
+3 -2
View File
@@ -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;
+81
View File
@@ -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,
}
+273 -2
View File
@@ -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">
+128
View File
@@ -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 指纹浏览器登录 ============
/**