mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-10-16 07:16:36 +08:00
- 添加 get_enabled_api_keys_by_type 方法按 Provider 类型获取 API Keys - 添加 get_next_api_key_by_type 方法支持按类型轮询负载均衡 - 在 chat_completions 端点添加 API Key Provider 回退支持 - 在 anthropic_messages 端点添加 API Key Provider 回退支持 - 支持自定义 base_url 的 AnthropicKey 使用 OpenAI 兼容格式调用 - 添加 convert_openai_response_to_anthropic 响应转换函数 - 更新版本号到 v0.28.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1256 lines
37 KiB
Markdown
1256 lines
37 KiB
Markdown
# Kiro Provider 插件文档
|
||
|
||
> 版本: 1.0.0
|
||
> 仓库: `aiclientproxy/kiro-provider`
|
||
> 类型: OAuth Provider Plugin
|
||
|
||
---
|
||
|
||
## 一、概述
|
||
|
||
### 1.1 插件简介
|
||
|
||
Kiro Provider 是 ProxyCast 的 OAuth Provider 插件,用于对接 **AWS CodeWhisperer** (Kiro IDE) 服务。它将 Anthropic/OpenAI 协议请求转换为 CodeWhisperer 协议,并处理复杂的风控逻辑。
|
||
|
||
### 1.2 核心能力
|
||
|
||
| 能力 | 说明 |
|
||
|------|------|
|
||
| 协议转换 | Anthropic/OpenAI → CodeWhisperer → Anthropic SSE |
|
||
| 多认证方式 | Social Auth (Google/GitHub) + AWS Builder ID (IdC) |
|
||
| 风控适配 | Machine ID 生成、版本伪装、指纹隔离 |
|
||
| Token 管理 | 自动刷新、健康检查、凭证池负载均衡 |
|
||
| 前端 UI | 凭证管理、在线登录、状态展示 |
|
||
|
||
### 1.3 支持的模型
|
||
|
||
| 请求模型名 | 实际模型 |
|
||
|-----------|---------|
|
||
| `claude-opus-4-5`, `claude-opus-4-5-20251101` | `claude-opus-4.5` |
|
||
| `claude-haiku-4-5`, `claude-haiku-4-5-20251001` | `claude-haiku-4.5` |
|
||
| `claude-sonnet-4-5`, `claude-sonnet-4-5-20250929` | `CLAUDE_SONNET_4_5_20250929_V1_0` |
|
||
| `claude-sonnet-4-20250514` | `CLAUDE_SONNET_4_20250514_V1_0` |
|
||
| `claude-3-7-sonnet-20250219` | `CLAUDE_3_7_SONNET_20250219_V1_0` |
|
||
|
||
---
|
||
|
||
## 二、插件架构
|
||
|
||
### 2.1 项目结构
|
||
|
||
```
|
||
kiro-provider/
|
||
├── plugin/
|
||
│ ├── plugin.json # 插件元数据
|
||
│ └── config.json # 默认配置
|
||
│
|
||
├── src-tauri/src/ # 后端 Rust 代码
|
||
│ ├── lib.rs # 插件入口
|
||
│ ├── commands.rs # Tauri 命令
|
||
│ ├── provider.rs # KiroProvider 核心实现
|
||
│ ├── credentials.rs # 凭证管理
|
||
│ ├── fingerprint.rs # 指纹绑定
|
||
│ ├── risk_control.rs # 风控逻辑
|
||
│ ├── token_refresh.rs # Token 刷新
|
||
│ └── translator/ # 协议转换
|
||
│ ├── mod.rs
|
||
│ ├── openai_to_cw.rs # OpenAI → CodeWhisperer
|
||
│ ├── anthropic_to_cw.rs # Anthropic → CodeWhisperer
|
||
│ └── cw_to_anthropic.rs # CodeWhisperer → Anthropic SSE
|
||
│
|
||
├── src/ # 前端 React UI
|
||
│ ├── index.tsx # 插件 UI 入口
|
||
│ ├── components/
|
||
│ │ ├── CredentialList.tsx # 凭证列表
|
||
│ │ ├── CredentialCard.tsx # 凭证卡片
|
||
│ │ ├── KiroForm.tsx # 凭证添加表单
|
||
│ │ ├── LoginModes.tsx # 登录模式选择
|
||
│ │ ├── BrowserModeSelector.tsx # 浏览器模式
|
||
│ │ └── SettingsPanel.tsx # 插件设置
|
||
│ ├── hooks/
|
||
│ │ ├── useCredentials.ts # 凭证 hooks
|
||
│ │ └── usePlaywright.ts # Playwright hooks
|
||
│ └── types/
|
||
│ └── index.ts # 类型定义
|
||
│
|
||
└── .github/
|
||
└── workflows/
|
||
└── release.yml # 自动构建发布
|
||
```
|
||
|
||
### 2.2 plugin.json
|
||
|
||
```json
|
||
{
|
||
"name": "kiro-provider",
|
||
"version": "1.0.0",
|
||
"description": "Kiro (AWS CodeWhisperer) OAuth Provider - 支持 Claude 模型",
|
||
"author": "ProxyCast Team",
|
||
"homepage": "https://github.com/aiclientproxy/kiro-provider",
|
||
"license": "MIT",
|
||
|
||
"plugin_type": "oauth_provider",
|
||
"entry": "kiro-provider-cli",
|
||
"min_proxycast_version": "1.0.0",
|
||
|
||
"provider": {
|
||
"id": "kiro",
|
||
"display_name": "Kiro (CodeWhisperer)",
|
||
"target_protocol": "anthropic",
|
||
"supported_models": ["claude-*"],
|
||
"auth_types": ["oauth"],
|
||
"credential_schema": {
|
||
"type": "object",
|
||
"properties": {
|
||
"accessToken": { "type": "string", "title": "Access Token" },
|
||
"refreshToken": { "type": "string", "title": "Refresh Token" },
|
||
"profileArn": { "type": "string", "title": "Profile ARN" },
|
||
"clientId": { "type": "string", "title": "Client ID" },
|
||
"clientSecret": { "type": "string", "title": "Client Secret" },
|
||
"region": { "type": "string", "default": "us-east-1" },
|
||
"authMethod": { "type": "string", "enum": ["social", "idc"] }
|
||
},
|
||
"required": ["accessToken", "refreshToken"]
|
||
}
|
||
},
|
||
|
||
"binary": {
|
||
"binary_name": "kiro-provider-cli",
|
||
"github_owner": "aiclientproxy",
|
||
"github_repo": "kiro-provider",
|
||
"platform_binaries": {
|
||
"macos-arm64": "kiro-provider-aarch64-apple-darwin",
|
||
"macos-x64": "kiro-provider-x86_64-apple-darwin",
|
||
"linux-x64": "kiro-provider-x86_64-unknown-linux-gnu",
|
||
"windows-x64": "kiro-provider-x86_64-pc-windows-msvc.exe"
|
||
},
|
||
"checksum_file": "checksums.txt"
|
||
},
|
||
|
||
"ui": {
|
||
"surfaces": ["oauth_providers"],
|
||
"icon": "Cloud",
|
||
"title": "Kiro Provider",
|
||
"entry": "dist/index.js",
|
||
"styles": "dist/styles.css",
|
||
"default_width": 900,
|
||
"default_height": 700,
|
||
"permissions": [
|
||
"database:read",
|
||
"database:write",
|
||
"http:request",
|
||
"crypto:encrypt",
|
||
"shell:open",
|
||
"playwright:browser"
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2.3 config.json
|
||
|
||
```json
|
||
{
|
||
"enabled": true,
|
||
"timeout_ms": 60000,
|
||
"settings": {
|
||
"risk_control": {
|
||
"machine_id_rotation": true,
|
||
"version_spoofing": true,
|
||
"fingerprint_isolation": true,
|
||
"hour_slot_variation": true
|
||
},
|
||
"token_refresh": {
|
||
"auto_refresh": true,
|
||
"refresh_threshold_minutes": 5,
|
||
"max_retry": 3
|
||
},
|
||
"health_check": {
|
||
"enabled": true,
|
||
"interval_seconds": 300,
|
||
"unhealthy_threshold": 3
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 三、后端实现
|
||
|
||
### 3.1 核心数据结构
|
||
|
||
#### 凭证结构
|
||
|
||
```rust
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct KiroCredentials {
|
||
pub access_token: String,
|
||
pub refresh_token: String,
|
||
pub profile_arn: Option<String>,
|
||
pub client_id: Option<String>,
|
||
pub client_secret: Option<String>,
|
||
pub region: String, // 默认 "us-east-1"
|
||
pub auth_method: AuthMethod, // Social | IdC
|
||
pub client_id_hash: Option<String>,
|
||
pub expire: Option<DateTime<Utc>>,
|
||
}
|
||
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub enum AuthMethod {
|
||
Social, // Google/GitHub OAuth
|
||
IdC, // AWS Builder ID (Device Code Flow)
|
||
}
|
||
```
|
||
|
||
#### 指纹绑定
|
||
|
||
```rust
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
pub struct KiroFingerprintBinding {
|
||
pub credential_uuid: String,
|
||
pub machine_id: String,
|
||
pub created_at: DateTime<Utc>,
|
||
pub last_switched_at: Option<DateTime<Utc>>,
|
||
}
|
||
```
|
||
|
||
### 3.2 风控实现
|
||
|
||
#### Machine ID 生成
|
||
|
||
```rust
|
||
/// 为每个凭证生成独立的 Machine ID
|
||
///
|
||
/// 策略:
|
||
/// 1. 基于 profile_arn 或 client_id 生成唯一标识
|
||
/// 2. 添加时间变化因子(每小时变化)避免指纹固化
|
||
/// 3. 每个凭证独立 Machine ID,防止多账号共用指纹被检测
|
||
pub fn generate_machine_id_from_credentials(
|
||
profile_arn: Option<&str>,
|
||
client_id: Option<&str>,
|
||
) -> String {
|
||
// 1. 确定唯一标识
|
||
let unique_key = profile_arn
|
||
.or(client_id)
|
||
.unwrap_or("default-kiro-key");
|
||
|
||
// 2. 添加时间变化因子(每小时变化)
|
||
let now = SystemTime::now()
|
||
.duration_since(UNIX_EPOCH)
|
||
.unwrap()
|
||
.as_secs();
|
||
let hour_slot = now / 3600;
|
||
|
||
// 3. SHA256 哈希
|
||
let mut hasher = Sha256::new();
|
||
hasher.update(unique_key.as_bytes());
|
||
hasher.update(&hour_slot.to_le_bytes());
|
||
let hash = hasher.finalize();
|
||
|
||
// 4. 格式化为 UUID 格式
|
||
format!(
|
||
"{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
|
||
u32::from_be_bytes(hash[0..4].try_into().unwrap()),
|
||
u16::from_be_bytes(hash[4..6].try_into().unwrap()),
|
||
u16::from_be_bytes(hash[6..8].try_into().unwrap()),
|
||
u16::from_be_bytes(hash[8..10].try_into().unwrap()),
|
||
u64::from_be_bytes([0, 0, hash[10], hash[11], hash[12], hash[13], hash[14], hash[15]])
|
||
)
|
||
}
|
||
```
|
||
|
||
#### 系统信息获取
|
||
|
||
```rust
|
||
/// 获取系统运行时信息
|
||
pub fn get_system_runtime_info() -> RuntimeInfo {
|
||
RuntimeInfo {
|
||
os_name: get_os_name(), // "macos", "linux", "windows"
|
||
os_version: get_os_version(), // "14.0.0"
|
||
node_version: get_node_version(), // "20.10.0" (模拟)
|
||
}
|
||
}
|
||
|
||
/// 获取 Kiro IDE 版本(macOS 从 Info.plist 读取)
|
||
pub fn get_kiro_version() -> String {
|
||
#[cfg(target_os = "macos")]
|
||
{
|
||
let plist_path = "/Applications/Kiro.app/Contents/Info.plist";
|
||
if let Ok(plist) = plist::from_file::<_, plist::Value>(plist_path) {
|
||
if let Some(version) = plist.as_dictionary()
|
||
.and_then(|d| d.get("CFBundleShortVersionString"))
|
||
.and_then(|v| v.as_string())
|
||
{
|
||
return version.to_string();
|
||
}
|
||
}
|
||
}
|
||
"1.0.0".to_string() // 默认版本
|
||
}
|
||
```
|
||
|
||
#### User-Agent 构造
|
||
|
||
```rust
|
||
/// Social Auth Token 刷新 User-Agent
|
||
fn build_social_auth_user_agent(kiro_version: &str, machine_id: &str) -> String {
|
||
format!("KiroIDE-{}-{}", kiro_version, machine_id)
|
||
}
|
||
|
||
/// IdC Auth Token 刷新 User-Agent
|
||
fn build_idc_auth_user_agent(kiro_version: &str, machine_id: &str) -> String {
|
||
format!(
|
||
"aws-sdk-js/3.738.0 ua/2.1 os/other lang/js api/sso-oidc#3.738.0 m/E KiroIDE-{}-{}",
|
||
kiro_version, machine_id
|
||
)
|
||
}
|
||
|
||
/// API 调用 User-Agent
|
||
fn build_api_user_agent(
|
||
os_name: &str,
|
||
node_version: &str,
|
||
kiro_version: &str,
|
||
machine_id: &str,
|
||
) -> String {
|
||
format!(
|
||
"aws-sdk-js/1.0.0 ua/2.1 os/{} lang/js md/nodejs#{} api/codewhispererruntime#1.0.0 m/E KiroIDE-{}-{}",
|
||
os_name, node_version, kiro_version, machine_id
|
||
)
|
||
}
|
||
```
|
||
|
||
#### 请求头构造
|
||
|
||
```rust
|
||
/// 构造 API 请求头
|
||
fn build_api_headers(credential: &KiroCredentials, machine_id: &str) -> HeaderMap {
|
||
let mut headers = HeaderMap::new();
|
||
|
||
// 认证
|
||
headers.insert(
|
||
"Authorization",
|
||
format!("Bearer {}", credential.access_token).parse().unwrap()
|
||
);
|
||
|
||
// 风控关键头
|
||
let runtime_info = get_system_runtime_info();
|
||
let kiro_version = get_kiro_version();
|
||
|
||
headers.insert(
|
||
"User-Agent",
|
||
build_api_user_agent(
|
||
&runtime_info.os_name,
|
||
&runtime_info.node_version,
|
||
&kiro_version,
|
||
machine_id
|
||
).parse().unwrap()
|
||
);
|
||
|
||
headers.insert(
|
||
"x-amz-user-agent",
|
||
format!("aws-sdk-js/1.0.0 KiroIDE-{}-{}", kiro_version, machine_id)
|
||
.parse().unwrap()
|
||
);
|
||
|
||
headers.insert("x-amzn-kiro-agent-mode", "vibe".parse().unwrap());
|
||
headers.insert("amz-sdk-invocation-id", Uuid::new_v4().to_string().parse().unwrap());
|
||
headers.insert("amz-sdk-request", "attempt=1; max=1".parse().unwrap());
|
||
headers.insert("Connection", "close".parse().unwrap()); // 避免连接复用被检测
|
||
|
||
headers
|
||
}
|
||
```
|
||
|
||
### 3.3 Token 刷新
|
||
|
||
```rust
|
||
impl KiroProvider {
|
||
/// 刷新 Token
|
||
pub async fn refresh_token(&self, credential: &mut KiroCredentials) -> Result<()> {
|
||
// 验证 refreshToken 完整性
|
||
if credential.refresh_token.len() < 100 {
|
||
return Err(Error::InvalidRefreshToken("Token appears truncated"));
|
||
}
|
||
|
||
let machine_id = self.get_or_create_machine_id(&credential.credential_uuid);
|
||
let kiro_version = get_kiro_version();
|
||
|
||
match credential.auth_method {
|
||
AuthMethod::Social => {
|
||
self.refresh_social_token(credential, &machine_id, &kiro_version).await
|
||
}
|
||
AuthMethod::IdC => {
|
||
self.refresh_idc_token(credential, &machine_id, &kiro_version).await
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Social Auth Token 刷新
|
||
async fn refresh_social_token(
|
||
&self,
|
||
credential: &mut KiroCredentials,
|
||
machine_id: &str,
|
||
kiro_version: &str,
|
||
) -> Result<()> {
|
||
let url = format!(
|
||
"https://prod.{}.auth.desktop.kiro.dev/refreshToken",
|
||
credential.region
|
||
);
|
||
|
||
let response = self.http_client
|
||
.post(&url)
|
||
.header("User-Agent", build_social_auth_user_agent(kiro_version, machine_id))
|
||
.header("Content-Type", "application/json")
|
||
.json(&json!({
|
||
"refreshToken": credential.refresh_token
|
||
}))
|
||
.send()
|
||
.await?;
|
||
|
||
let result: TokenResponse = response.json().await?;
|
||
credential.access_token = result.access_token;
|
||
credential.expire = Some(Utc::now() + Duration::hours(1));
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// IdC Auth Token 刷新
|
||
async fn refresh_idc_token(
|
||
&self,
|
||
credential: &mut KiroCredentials,
|
||
machine_id: &str,
|
||
kiro_version: &str,
|
||
) -> Result<()> {
|
||
let url = format!(
|
||
"https://oidc.{}.amazonaws.com/token",
|
||
credential.region
|
||
);
|
||
|
||
let response = self.http_client
|
||
.post(&url)
|
||
.header("User-Agent", build_idc_auth_user_agent(kiro_version, machine_id))
|
||
.header("x-amz-user-agent", format!(
|
||
"aws-sdk-js/3.738.0 KiroIDE-{}-{}", kiro_version, machine_id
|
||
))
|
||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||
.form(&[
|
||
("grant_type", "refresh_token"),
|
||
("client_id", credential.client_id.as_deref().unwrap_or("")),
|
||
("client_secret", credential.client_secret.as_deref().unwrap_or("")),
|
||
("refresh_token", &credential.refresh_token),
|
||
])
|
||
.send()
|
||
.await?;
|
||
|
||
let result: TokenResponse = response.json().await?;
|
||
credential.access_token = result.access_token;
|
||
credential.expire = Some(Utc::now() + Duration::hours(1));
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.4 协议转换
|
||
|
||
#### Anthropic → CodeWhisperer
|
||
|
||
```rust
|
||
/// 将 Anthropic 请求转换为 CodeWhisperer 格式
|
||
pub fn convert_anthropic_to_codewhisperer(
|
||
request: &AnthropicRequest,
|
||
profile_arn: Option<&str>,
|
||
) -> CodeWhispererRequest {
|
||
// 模型映射
|
||
let model = map_model_name(&request.model);
|
||
|
||
// 消息转换
|
||
let conversation_state = ConversationState {
|
||
current_message: convert_messages(&request.messages),
|
||
chat_trigger_type: "MANUAL".to_string(),
|
||
user_intent: "CHAT".to_string(),
|
||
customization_arn: None,
|
||
};
|
||
|
||
// 系统提示
|
||
let system_prompt = request.system.clone();
|
||
|
||
// 工具转换
|
||
let tools = request.tools.as_ref().map(|t| convert_tools(t));
|
||
|
||
CodeWhispererRequest {
|
||
conversation_state,
|
||
profile_arn: profile_arn.map(String::from),
|
||
source: "CHAT".to_string(),
|
||
assistant_response_config: AssistantResponseConfig {
|
||
max_output_tokens: request.max_tokens,
|
||
temperature: request.temperature,
|
||
response_style: Some(ResponseStyle {
|
||
system_prompt_user_customization: system_prompt,
|
||
}),
|
||
},
|
||
tools,
|
||
}
|
||
}
|
||
|
||
/// 模型名称映射
|
||
fn map_model_name(model: &str) -> String {
|
||
let mappings = [
|
||
("claude-opus-4-5", "claude-opus-4.5"),
|
||
("claude-opus-4-5-20251101", "claude-opus-4.5"),
|
||
("claude-haiku-4-5", "claude-haiku-4.5"),
|
||
("claude-haiku-4-5-20251001", "claude-haiku-4.5"),
|
||
("claude-sonnet-4-5", "CLAUDE_SONNET_4_5_20250929_V1_0"),
|
||
("claude-sonnet-4-5-20250929", "CLAUDE_SONNET_4_5_20250929_V1_0"),
|
||
("claude-sonnet-4-20250514", "CLAUDE_SONNET_4_20250514_V1_0"),
|
||
("claude-3-7-sonnet-20250219", "CLAUDE_3_7_SONNET_20250219_V1_0"),
|
||
("claude-3-5-sonnet-20241022", "CLAUDE_3_7_SONNET_20250219_V1_0"),
|
||
];
|
||
|
||
for (from, to) in mappings {
|
||
if model.contains(from) {
|
||
return to.to_string();
|
||
}
|
||
}
|
||
|
||
model.to_string()
|
||
}
|
||
```
|
||
|
||
#### CodeWhisperer → Anthropic SSE
|
||
|
||
```rust
|
||
/// 将 AWS Event Stream 转换为 Anthropic SSE
|
||
pub struct CwToAnthropicTranslator {
|
||
message_id: String,
|
||
model: String,
|
||
current_index: u32,
|
||
input_tokens: u32,
|
||
output_tokens: u32,
|
||
}
|
||
|
||
impl CwToAnthropicTranslator {
|
||
pub fn translate_chunk(&mut self, chunk: &AwsEventChunk) -> Vec<AnthropicSseEvent> {
|
||
let mut events = Vec::new();
|
||
|
||
match &chunk.event_type {
|
||
EventType::MessageStart => {
|
||
events.push(AnthropicSseEvent::MessageStart {
|
||
message: MessageStartData {
|
||
id: self.message_id.clone(),
|
||
type_: "message".to_string(),
|
||
role: "assistant".to_string(),
|
||
model: self.model.clone(),
|
||
},
|
||
});
|
||
}
|
||
EventType::ContentBlockStart { content_type } => {
|
||
events.push(AnthropicSseEvent::ContentBlockStart {
|
||
index: self.current_index,
|
||
content_block: ContentBlock {
|
||
type_: content_type.clone(),
|
||
},
|
||
});
|
||
}
|
||
EventType::ContentBlockDelta { delta } => {
|
||
events.push(AnthropicSseEvent::ContentBlockDelta {
|
||
index: self.current_index,
|
||
delta: Delta::TextDelta { text: delta.clone() },
|
||
});
|
||
}
|
||
EventType::ContentBlockStop => {
|
||
events.push(AnthropicSseEvent::ContentBlockStop {
|
||
index: self.current_index,
|
||
});
|
||
self.current_index += 1;
|
||
}
|
||
EventType::MessageDelta { stop_reason, usage } => {
|
||
self.input_tokens = usage.input_tokens;
|
||
self.output_tokens = usage.output_tokens;
|
||
events.push(AnthropicSseEvent::MessageDelta {
|
||
delta: MessageDelta {
|
||
stop_reason: stop_reason.clone(),
|
||
},
|
||
usage: Usage {
|
||
input_tokens: self.input_tokens,
|
||
output_tokens: self.output_tokens,
|
||
},
|
||
});
|
||
}
|
||
EventType::MessageStop => {
|
||
events.push(AnthropicSseEvent::MessageStop);
|
||
}
|
||
}
|
||
|
||
events
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 四、前端 UI 实现
|
||
|
||
### 4.1 插件入口
|
||
|
||
```tsx
|
||
// src/index.tsx
|
||
import { ProxyCastPluginSDK } from '@proxycast/plugin-sdk';
|
||
import { CredentialList } from './components/CredentialList';
|
||
import { KiroForm } from './components/KiroForm';
|
||
import { SettingsPanel } from './components/SettingsPanel';
|
||
|
||
interface PluginProps {
|
||
sdk: ProxyCastPluginSDK;
|
||
pluginId: string;
|
||
}
|
||
|
||
export default function KiroProviderUI({ sdk, pluginId }: PluginProps) {
|
||
const [view, setView] = useState<'list' | 'add' | 'settings'>('list');
|
||
const [credentials, setCredentials] = useState<Credential[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
loadCredentials();
|
||
}, []);
|
||
|
||
const loadCredentials = async () => {
|
||
setLoading(true);
|
||
const result = await sdk.database.query<Credential>(
|
||
'SELECT * FROM plugin_credentials WHERE plugin_id = ? ORDER BY created_at DESC',
|
||
[pluginId]
|
||
);
|
||
setCredentials(result);
|
||
setLoading(false);
|
||
};
|
||
|
||
return (
|
||
<div className="kiro-provider-ui">
|
||
<Header>
|
||
<Title>Kiro Provider</Title>
|
||
<Actions>
|
||
<Button onClick={() => setView('add')}>添加凭证</Button>
|
||
<Button onClick={() => setView('settings')}>设置</Button>
|
||
</Actions>
|
||
</Header>
|
||
|
||
{view === 'list' && (
|
||
<CredentialList
|
||
credentials={credentials}
|
||
onRefresh={loadCredentials}
|
||
sdk={sdk}
|
||
/>
|
||
)}
|
||
|
||
{view === 'add' && (
|
||
<KiroForm
|
||
sdk={sdk}
|
||
onSuccess={() => {
|
||
loadCredentials();
|
||
setView('list');
|
||
}}
|
||
onCancel={() => setView('list')}
|
||
/>
|
||
)}
|
||
|
||
{view === 'settings' && (
|
||
<SettingsPanel
|
||
sdk={sdk}
|
||
pluginId={pluginId}
|
||
onClose={() => setView('list')}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
### 4.2 凭证添加表单
|
||
|
||
```tsx
|
||
// src/components/KiroForm.tsx
|
||
|
||
type AddMode = 'login' | 'json' | 'file';
|
||
type BrowserMode = 'system' | 'playwright';
|
||
type OAuthProvider = 'google' | 'github' | 'builder_id';
|
||
|
||
interface KiroFormProps {
|
||
sdk: ProxyCastPluginSDK;
|
||
onSuccess: () => void;
|
||
onCancel: () => void;
|
||
}
|
||
|
||
export function KiroForm({ sdk, onSuccess, onCancel }: KiroFormProps) {
|
||
const [mode, setMode] = useState<AddMode>('login');
|
||
const [browserMode, setBrowserMode] = useState<BrowserMode>('system');
|
||
const [oauthProvider, setOAuthProvider] = useState<OAuthProvider>('google');
|
||
const [loading, setLoading] = useState(false);
|
||
const [playwrightStatus, setPlaywrightStatus] = useState<PlaywrightStatus>();
|
||
|
||
// 检查 Playwright 可用性
|
||
useEffect(() => {
|
||
checkPlaywrightAvailable().then(setPlaywrightStatus);
|
||
}, []);
|
||
|
||
return (
|
||
<div className="kiro-form">
|
||
{/* 模式选择 */}
|
||
<Tabs value={mode} onChange={setMode}>
|
||
<Tab value="login">在线登录</Tab>
|
||
<Tab value="json">粘贴 JSON</Tab>
|
||
<Tab value="file">导入文件</Tab>
|
||
</Tabs>
|
||
|
||
{mode === 'login' && (
|
||
<LoginMode
|
||
browserMode={browserMode}
|
||
setBrowserMode={setBrowserMode}
|
||
oauthProvider={oauthProvider}
|
||
setOAuthProvider={setOAuthProvider}
|
||
playwrightStatus={playwrightStatus}
|
||
sdk={sdk}
|
||
onSuccess={onSuccess}
|
||
/>
|
||
)}
|
||
|
||
{mode === 'json' && (
|
||
<JsonMode sdk={sdk} onSuccess={onSuccess} />
|
||
)}
|
||
|
||
{mode === 'file' && (
|
||
<FileMode sdk={sdk} onSuccess={onSuccess} />
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
### 4.3 在线登录模式
|
||
|
||
```tsx
|
||
// src/components/LoginModes.tsx
|
||
|
||
interface LoginModeProps {
|
||
browserMode: BrowserMode;
|
||
setBrowserMode: (mode: BrowserMode) => void;
|
||
oauthProvider: OAuthProvider;
|
||
setOAuthProvider: (provider: OAuthProvider) => void;
|
||
playwrightStatus?: PlaywrightStatus;
|
||
sdk: ProxyCastPluginSDK;
|
||
onSuccess: () => void;
|
||
}
|
||
|
||
export function LoginMode(props: LoginModeProps) {
|
||
const { browserMode, setBrowserMode, oauthProvider, setOAuthProvider, playwrightStatus, sdk, onSuccess } = props;
|
||
const [loading, setLoading] = useState(false);
|
||
const [builderIdData, setBuilderIdData] = useState<BuilderIdLoginData | null>(null);
|
||
|
||
const handleLogin = async () => {
|
||
setLoading(true);
|
||
|
||
try {
|
||
if (oauthProvider === 'builder_id') {
|
||
// AWS Builder ID - Device Code Flow
|
||
await startBuilderIdLogin();
|
||
} else {
|
||
// Google/GitHub OAuth
|
||
if (browserMode === 'system') {
|
||
await startSystemBrowserLogin(oauthProvider);
|
||
} else {
|
||
await startPlaywrightLogin(oauthProvider);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
sdk.notification.error(`登录失败: ${error.message}`);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const startBuilderIdLogin = async () => {
|
||
// 1. 获取设备码
|
||
const { userCode, verificationUri, interval } = await sdk.http.request(
|
||
'/api/kiro/builder-id/start',
|
||
{ method: 'POST' }
|
||
);
|
||
|
||
setBuilderIdData({ userCode, verificationUri });
|
||
|
||
// 2. 打开浏览器
|
||
await sdk.shell.open(verificationUri);
|
||
|
||
// 3. 轮询检查授权状态
|
||
const pollInterval = setInterval(async () => {
|
||
const result = await sdk.http.request('/api/kiro/builder-id/poll');
|
||
if (result.status === 'complete') {
|
||
clearInterval(pollInterval);
|
||
setBuilderIdData(null);
|
||
onSuccess();
|
||
} else if (result.status === 'expired') {
|
||
clearInterval(pollInterval);
|
||
setBuilderIdData(null);
|
||
sdk.notification.error('授权已过期,请重试');
|
||
}
|
||
}, interval * 1000);
|
||
};
|
||
|
||
return (
|
||
<div className="login-mode">
|
||
{/* OAuth Provider 选择 */}
|
||
<div className="oauth-providers">
|
||
<Button
|
||
variant={oauthProvider === 'google' ? 'primary' : 'secondary'}
|
||
onClick={() => setOAuthProvider('google')}
|
||
>
|
||
<GoogleIcon /> Google
|
||
</Button>
|
||
<Button
|
||
variant={oauthProvider === 'github' ? 'primary' : 'secondary'}
|
||
onClick={() => setOAuthProvider('github')}
|
||
>
|
||
<GitHubIcon /> GitHub
|
||
</Button>
|
||
<Button
|
||
variant={oauthProvider === 'builder_id' ? 'primary' : 'secondary'}
|
||
onClick={() => setOAuthProvider('builder_id')}
|
||
>
|
||
<AwsIcon /> Builder ID
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 浏览器模式选择(非 Builder ID) */}
|
||
{oauthProvider !== 'builder_id' && (
|
||
<BrowserModeSelector
|
||
value={browserMode}
|
||
onChange={setBrowserMode}
|
||
playwrightStatus={playwrightStatus}
|
||
/>
|
||
)}
|
||
|
||
{/* Builder ID 设备码显示 */}
|
||
{builderIdData && (
|
||
<div className="builder-id-code">
|
||
<p>请在浏览器中输入以下代码:</p>
|
||
<code className="user-code">{builderIdData.userCode}</code>
|
||
<p>等待授权中...</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* 登录按钮 */}
|
||
<Button
|
||
onClick={handleLogin}
|
||
loading={loading}
|
||
disabled={loading}
|
||
>
|
||
{loading ? '登录中...' : '开始登录'}
|
||
</Button>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
### 4.4 浏览器模式选择器
|
||
|
||
```tsx
|
||
// src/components/BrowserModeSelector.tsx
|
||
|
||
interface BrowserModeSelectorProps {
|
||
value: BrowserMode;
|
||
onChange: (mode: BrowserMode) => void;
|
||
playwrightStatus?: PlaywrightStatus;
|
||
}
|
||
|
||
export function BrowserModeSelector({ value, onChange, playwrightStatus }: BrowserModeSelectorProps) {
|
||
const playwrightAvailable = playwrightStatus?.available ?? false;
|
||
|
||
return (
|
||
<div className="browser-mode-selector">
|
||
<label>浏览器模式</label>
|
||
|
||
<div className="mode-options">
|
||
<RadioOption
|
||
value="system"
|
||
checked={value === 'system'}
|
||
onChange={() => onChange('system')}
|
||
label="系统浏览器"
|
||
description="使用默认浏览器进行 OAuth 登录"
|
||
/>
|
||
|
||
<RadioOption
|
||
value="playwright"
|
||
checked={value === 'playwright'}
|
||
onChange={() => onChange('playwright')}
|
||
disabled={!playwrightAvailable}
|
||
label="指纹浏览器"
|
||
description={
|
||
playwrightAvailable
|
||
? "使用 Playwright 反爬虫浏览器绕过机器人检测"
|
||
: "Playwright 未安装"
|
||
}
|
||
/>
|
||
</div>
|
||
|
||
{!playwrightAvailable && (
|
||
<Alert type="info">
|
||
<p>Playwright 未安装。</p>
|
||
<a href="#" onClick={() => installPlaywright()}>点击安装</a>
|
||
</Alert>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
### 4.5 凭证列表
|
||
|
||
```tsx
|
||
// src/components/CredentialList.tsx
|
||
|
||
interface CredentialListProps {
|
||
credentials: Credential[];
|
||
onRefresh: () => void;
|
||
sdk: ProxyCastPluginSDK;
|
||
}
|
||
|
||
export function CredentialList({ credentials, onRefresh, sdk }: CredentialListProps) {
|
||
const handleRefreshToken = async (credentialId: string) => {
|
||
try {
|
||
await sdk.http.request(`/api/kiro/credentials/${credentialId}/refresh`, {
|
||
method: 'POST',
|
||
});
|
||
sdk.notification.success('Token 刷新成功');
|
||
onRefresh();
|
||
} catch (error) {
|
||
sdk.notification.error(`刷新失败: ${error.message}`);
|
||
}
|
||
};
|
||
|
||
const handleDelete = async (credentialId: string) => {
|
||
if (!confirm('确定删除此凭证?')) return;
|
||
|
||
await sdk.database.execute(
|
||
'DELETE FROM plugin_credentials WHERE id = ?',
|
||
[credentialId]
|
||
);
|
||
sdk.notification.success('凭证已删除');
|
||
onRefresh();
|
||
};
|
||
|
||
const handleSwitchToLocal = async (credentialId: string) => {
|
||
try {
|
||
await sdk.http.request(`/api/kiro/credentials/${credentialId}/switch-to-local`, {
|
||
method: 'POST',
|
||
});
|
||
sdk.notification.success('已切换到本地 Kiro IDE');
|
||
} catch (error) {
|
||
sdk.notification.error(`切换失败: ${error.message}`);
|
||
}
|
||
};
|
||
|
||
if (credentials.length === 0) {
|
||
return (
|
||
<EmptyState>
|
||
<p>暂无凭证</p>
|
||
<p>点击"添加凭证"开始使用</p>
|
||
</EmptyState>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="credential-list">
|
||
{credentials.map((credential) => (
|
||
<CredentialCard
|
||
key={credential.id}
|
||
credential={credential}
|
||
onRefresh={() => handleRefreshToken(credential.id)}
|
||
onDelete={() => handleDelete(credential.id)}
|
||
onSwitchToLocal={() => handleSwitchToLocal(credential.id)}
|
||
/>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
### 4.6 凭证卡片
|
||
|
||
```tsx
|
||
// src/components/CredentialCard.tsx
|
||
|
||
interface CredentialCardProps {
|
||
credential: Credential;
|
||
onRefresh: () => void;
|
||
onDelete: () => void;
|
||
onSwitchToLocal: () => void;
|
||
}
|
||
|
||
export function CredentialCard({ credential, onRefresh, onDelete, onSwitchToLocal }: CredentialCardProps) {
|
||
const data = JSON.parse(credential.credential_data) as KiroCredentials;
|
||
const isHealthy = credential.status === 'active';
|
||
const isExpiringSoon = data.expire && new Date(data.expire) < new Date(Date.now() + 5 * 60 * 1000);
|
||
|
||
return (
|
||
<Card className={`credential-card ${isHealthy ? 'healthy' : 'unhealthy'}`}>
|
||
<CardHeader>
|
||
<div className="status-indicator">
|
||
<StatusDot status={isHealthy ? 'green' : 'red'} />
|
||
<span>{isHealthy ? '健康' : '异常'}</span>
|
||
</div>
|
||
<div className="auth-method">
|
||
<Badge>{data.authMethod === 'social' ? 'Social' : 'Builder ID'}</Badge>
|
||
</div>
|
||
</CardHeader>
|
||
|
||
<CardBody>
|
||
<div className="info-row">
|
||
<label>名称</label>
|
||
<span>{credential.name || '未命名'}</span>
|
||
</div>
|
||
<div className="info-row">
|
||
<label>区域</label>
|
||
<span>{data.region}</span>
|
||
</div>
|
||
<div className="info-row">
|
||
<label>Profile ARN</label>
|
||
<span className="truncate">{data.profileArn || '-'}</span>
|
||
</div>
|
||
<div className="info-row">
|
||
<label>过期时间</label>
|
||
<span className={isExpiringSoon ? 'warning' : ''}>
|
||
{data.expire ? formatDate(data.expire) : '-'}
|
||
{isExpiringSoon && <Badge variant="warning">即将过期</Badge>}
|
||
</span>
|
||
</div>
|
||
<div className="info-row">
|
||
<label>使用次数</label>
|
||
<span>{credential.usage_count}</span>
|
||
</div>
|
||
<div className="info-row">
|
||
<label>错误次数</label>
|
||
<span>{credential.error_count}</span>
|
||
</div>
|
||
</CardBody>
|
||
|
||
<CardFooter>
|
||
<Button size="small" onClick={onRefresh}>刷新 Token</Button>
|
||
<Button size="small" onClick={onSwitchToLocal}>切换到本地</Button>
|
||
<Button size="small" variant="danger" onClick={onDelete}>删除</Button>
|
||
</CardFooter>
|
||
</Card>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 五、凭证文件格式
|
||
|
||
### 5.1 主凭证文件
|
||
|
||
**路径**: `~/.aws/sso/cache/kiro-auth-token.json`
|
||
|
||
```json
|
||
{
|
||
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||
"refreshToken": "rt-xxxxxxxxxxxxxxxxxxxxxxxxxx...",
|
||
"profileArn": "arn:aws:iam::123456789012:user/username",
|
||
"region": "us-east-1",
|
||
"authMethod": "social",
|
||
"clientIdHash": "a1b2c3d4e5f6...",
|
||
"expire": "2025-01-04T12:00:00.000Z"
|
||
}
|
||
```
|
||
|
||
### 5.2 Client 注册文件
|
||
|
||
**路径**: `~/.aws/sso/cache/{clientIdHash}.json`
|
||
|
||
```json
|
||
{
|
||
"clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||
"clientSecret": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||
"expiresAt": "2025-01-04T12:00:00.000Z",
|
||
"scopes": [
|
||
"codewhisperer:conversations",
|
||
"codewhisperer:transformations"
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 六、API 调用流程
|
||
|
||
### 6.1 完整调用链路
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────┐
|
||
│ Kiro Provider 调用流程 │
|
||
├─────────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ 1. 客户端请求 (Anthropic/OpenAI 格式) │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ 2. 协议识别 & 路由 │
|
||
│ ├── /v1/messages → Anthropic 入口 │
|
||
│ └── /v1/chat/completions → OpenAI 入口 │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ 3. 凭证获取 │
|
||
│ ├── 从凭证池选择健康凭证 │
|
||
│ ├── 检查 Token 是否过期 │
|
||
│ └── 必要时刷新 Token │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ 4. 风控处理 │
|
||
│ ├── 获取/生成 Machine ID │
|
||
│ ├── 获取 Kiro 版本号 │
|
||
│ ├── 获取系统信息 │
|
||
│ └── 构造请求头 │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ 5. 协议转换 (Request) │
|
||
│ ├── Anthropic → CodeWhisperer │
|
||
│ └── OpenAI → CodeWhisperer │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ 6. AWS CodeWhisperer API 调用 │
|
||
│ └── POST https://codewhisperer.{region}.amazonaws.com/generateChat │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ 7. 响应转换 (Response) │
|
||
│ └── AWS Event Stream → Anthropic SSE │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ 8. 返回客户端 │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 6.2 AWS CodeWhisperer API
|
||
|
||
**端点**: `POST https://codewhisperer.{region}.amazonaws.com/generateChat`
|
||
|
||
**请求格式**:
|
||
|
||
```json
|
||
{
|
||
"conversationState": {
|
||
"currentMessage": {
|
||
"userInputMessage": {
|
||
"content": "Hello, how are you?",
|
||
"userInputMessageContext": {}
|
||
}
|
||
},
|
||
"chatTriggerType": "MANUAL",
|
||
"userIntent": "CHAT"
|
||
},
|
||
"profileArn": "arn:aws:iam::...",
|
||
"source": "CHAT",
|
||
"assistantResponseConfig": {
|
||
"maxOutputTokens": 4096,
|
||
"temperature": 0.7,
|
||
"responseStyle": {
|
||
"systemPromptUserCustomization": "You are a helpful assistant."
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**响应格式**: AWS Event Stream
|
||
|
||
---
|
||
|
||
## 七、错误处理
|
||
|
||
### 7.1 错误类型
|
||
|
||
| 错误码 | 说明 | 处理方式 |
|
||
|--------|------|---------|
|
||
| 401 | Token 过期 | 自动刷新 Token 重试 |
|
||
| 403 | 权限不足 | 标记凭证异常 |
|
||
| 429 | 请求限流 | 冷却期后重试 |
|
||
| 500 | 服务器错误 | 切换凭证重试 |
|
||
|
||
### 7.2 Playwright 错误
|
||
|
||
```tsx
|
||
type PlaywrightErrorType =
|
||
| 'USER_CANCELLED' // 用户取消登录
|
||
| 'BROWSER_CLOSED' // 浏览器被关闭
|
||
| 'TIMEOUT' // 登录超时
|
||
| 'NETWORK_ERROR' // 网络错误
|
||
| 'CAPTCHA_FAILED' // 验证码失败
|
||
| 'UNKNOWN'; // 未知错误
|
||
```
|
||
|
||
---
|
||
|
||
## 八、开发指南
|
||
|
||
### 8.1 本地开发
|
||
|
||
```bash
|
||
# 克隆仓库
|
||
git clone https://github.com/aiclientproxy/kiro-provider.git
|
||
cd kiro-provider
|
||
|
||
# 安装依赖
|
||
pnpm install
|
||
cd src-tauri && cargo build
|
||
|
||
# 前端开发
|
||
pnpm dev
|
||
|
||
# 后端开发
|
||
cargo watch -x run
|
||
```
|
||
|
||
### 8.2 构建发布
|
||
|
||
```bash
|
||
# 构建前端
|
||
pnpm build
|
||
|
||
# 构建后端(各平台)
|
||
cargo build --release --target aarch64-apple-darwin
|
||
cargo build --release --target x86_64-apple-darwin
|
||
cargo build --release --target x86_64-unknown-linux-gnu
|
||
cargo build --release --target x86_64-pc-windows-msvc
|
||
|
||
# 生成 checksum
|
||
sha256sum target/*/release/kiro-provider-cli* > checksums.txt
|
||
```
|
||
|
||
### 8.3 测试
|
||
|
||
```bash
|
||
# 单元测试
|
||
cargo test
|
||
|
||
# 集成测试
|
||
cargo test --test integration
|
||
|
||
# 前端测试
|
||
pnpm test
|
||
```
|
||
|
||
---
|
||
|
||
## 附录
|
||
|
||
### A. 环境变量
|
||
|
||
| 变量 | 说明 | 默认值 |
|
||
|------|------|--------|
|
||
| `KIRO_REGION` | AWS 区域 | `us-east-1` |
|
||
| `KIRO_DEBUG` | 调试模式 | `false` |
|
||
| `KIRO_TIMEOUT_MS` | 请求超时 | `60000` |
|
||
|
||
### B. 参考链接
|
||
|
||
- [AWS CodeWhisperer 文档](https://docs.aws.amazon.com/codewhisperer/)
|
||
- [Kiro IDE](https://kiro.dev/)
|
||
- [ProxyCast 插件开发指南](../prd/credential-provider-plugin-architecture.md)
|