Files
proxycast/docs/plugins/droid-provider.md
T

62 KiB
Raw Permalink Blame History

Droid Provider 插件文档

版本: 1.0.0 仓库: aiclientproxy/droid-provider 类型: OAuth Provider Plugin


一、概述

1.1 插件简介

Droid Provider 是 Lime 的 Factory.ai (Droid) 插件,通过 WorkOS OAuth 集成 Factory.ai 平台,支持 Claude、OpenAI、Gemini 等多种模型的统一访问。

1.2 支持的认证方式

认证方式 说明 适用场景
WorkOS OAuth WorkOS OAuth 2.0 认证 Factory.ai 标准账户
API Key 直接 API Key 模式 多 Key 池负载均衡
Manual 手动提供 Refresh Token 离线授权

1.3 核心能力

能力 说明
多端点支持 Anthropic、OpenAI、Comm 三种 API 格式
自动 Token 刷新 每 6 小时自动刷新,Token 有效期 8 小时
API Key 池 支持多 API Key 随机选择和粘性会话
粘性会话调度 同一会话保持使用同一账户
账号分组调度 支持按分组选择账户
凭证加密存储 AES-256-CBC 加密 + scrypt 密钥派生
订阅过期管理 支持账户订阅到期时间检查
代理支持 支持 SOCKS5/HTTP 代理

1.4 支持的模型

模型 端点类型 提供商
claude-opus-4-1-20250805 anthropic Anthropic
claude-sonnet-4-5-20250929 anthropic Anthropic
claude-sonnet-4-20250514 anthropic Anthropic
gpt-5-2025-08-07 openai OpenAI
gemini-* comm Google
glm-* comm Fireworks

1.5 Factory.ai 端点

端点类型 API 路径 说明
anthropic /a/v1/messages Anthropic Messages API
openai /o/v1/responses OpenAI Responses API
comm /o/v1/chat/completions OpenAI Chat Completions API

二、插件架构

2.1 项目结构

droid-provider/
├── plugin/
│   ├── plugin.json              # 插件元数据
│   └── config.json              # 默认配置
│
├── src-tauri/src/               # 后端 Rust 代码
│   ├── lib.rs                   # 插件入口
│   ├── commands.rs              # Tauri 命令
│   ├── provider.rs              # DroidProvider 核心实现
│   ├── auth/
│   │   ├── mod.rs
│   │   ├── workos.rs            # WorkOS OAuth
│   │   └── api_key.rs           # API Key 模式
│   ├── credentials.rs           # 凭证管理
│   ├── token_refresh.rs         # Token 刷新
│   ├── scheduler.rs             # 账号调度
│   └── api/
│       ├── mod.rs
│       ├── relay.rs             # 请求转发
│       └── factory.rs           # Factory.ai API
│
├── src/                         # 前端 React UI
│   ├── index.tsx                # 插件 UI 入口
│   ├── components/
│   │   ├── CredentialList.tsx   # 凭证列表
│   │   ├── CredentialCard.tsx   # 凭证卡片
│   │   ├── AuthMethodTabs.tsx   # 认证方式选择
│   │   ├── OAuthForm.tsx        # OAuth 表单
│   │   ├── ApiKeyForm.tsx       # API Key 表单
│   │   ├── EndpointSelect.tsx   # 端点类型选择
│   │   └── SettingsPanel.tsx    # 插件设置
│   └── types/
│       └── index.ts             # 类型定义
│
└── .github/
    └── workflows/
        └── release.yml          # 自动构建发布

2.2 plugin.json

{
  "name": "droid-provider",
  "version": "1.0.0",
  "description": "Droid Provider - Factory.ai 集成,支持 WorkOS OAuth 和 API Key 模式",
  "author": "Lime Team",
  "homepage": "https://github.com/aiclientproxy/droid-provider",
  "license": "MIT",

  "plugin_type": "oauth_provider",
  "entry": "droid-provider-cli",
  "min_lime_version": "1.0.0",

  "provider": {
    "id": "droid",
    "display_name": "Droid (Factory.ai)",
    "target_protocol": "anthropic",
    "supported_models": ["claude-*", "gpt-*", "gemini-*", "glm-*"],
    "auth_types": ["workos_oauth", "api_key", "manual"],
    "endpoint_types": ["anthropic", "openai", "comm"],
    "credential_schemas": {
      "workos_oauth": {
        "type": "object",
        "properties": {
          "access_token": { "type": "string" },
          "refresh_token": { "type": "string" },
          "organization_id": { "type": "string" },
          "expires_at": { "type": "string" },
          "user_id": { "type": "string" },
          "owner_email": { "type": "string" }
        },
        "required": ["access_token", "refresh_token"]
      },
      "api_key": {
        "type": "object",
        "properties": {
          "api_keys": {
            "type": "array",
            "items": { "type": "string" }
          }
        },
        "required": ["api_keys"]
      },
      "manual": {
        "type": "object",
        "properties": {
          "refresh_token": { "type": "string" }
        },
        "required": ["refresh_token"]
      }
    }
  },

  "binary": {
    "binary_name": "droid-provider-cli",
    "github_owner": "aiclientproxy",
    "github_repo": "droid-provider",
    "platform_binaries": {
      "macos-arm64": "droid-provider-aarch64-apple-darwin",
      "macos-x64": "droid-provider-x86_64-apple-darwin",
      "linux-x64": "droid-provider-x86_64-unknown-linux-gnu",
      "windows-x64": "droid-provider-x86_64-pc-windows-msvc.exe"
    },
    "checksum_file": "checksums.txt"
  },

  "ui": {
    "surfaces": ["oauth_providers"],
    "icon": "Bot",
    "title": "Droid Provider",
    "entry": "dist/index.js",
    "styles": "dist/styles.css",
    "default_width": 950,
    "default_height": 750,
    "permissions": [
      "database:read",
      "database:write",
      "http:request",
      "crypto:encrypt",
      "shell:open"
    ]
  }
}

2.3 config.json

{
  "enabled": true,
  "timeout_ms": 600000,
  "settings": {
    "workos": {
      "client_id": "client_01HNM792M5G5G1A2THWPXKFMXB",
      "auth_url": "https://api.workos.com/user_management/authenticate"
    },
    "factory": {
      "api_base_url": "https://api.factory.ai/api/llm",
      "app_base_url": "https://app.factory.ai",
      "endpoints": {
        "anthropic": "/a/v1/messages",
        "openai": "/o/v1/responses",
        "comm": "/o/v1/chat/completions"
      }
    },
    "token_refresh": {
      "auto_refresh": true,
      "refresh_interval_hours": 6,
      "token_valid_hours": 8,
      "max_retry": 3,
      "retry_delay_ms": 1000
    },
    "api_key": {
      "strategy": "random_sticky",
      "sticky_ttl_seconds": 3600
    },
    "encryption": {
      "algorithm": "aes-256-cbc",
      "salt": "droid-account-salt",
      "key_derivation": "scrypt"
    },
    "request": {
      "user_agent": "factory-cli/0.32.1",
      "system_prompt": "You are Droid, an AI software engineering agent built by Factory."
    }
  }
}

三、认证方式详解

3.1 WorkOS OAuth 认证

WorkOS OAuth 配置

const WORKOS_CLIENT_ID: &str = "client_01HNM792M5G5G1A2THWPXKFMXB";
const WORKOS_TOKEN_URL: &str = "https://api.workos.com/user_management/authenticate";
const FACTORY_ORG_API: &str = "https://app.factory.ai/api/cli/org";

Token 刷新实现

use reqwest::Client;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize)]
struct RefreshTokenRequest {
    grant_type: String,
    refresh_token: String,
    client_id: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    organization_id: Option<String>,
}

#[derive(Debug, Deserialize)]
struct WorkOSTokenResponse {
    access_token: String,
    refresh_token: Option<String>,
    expires_in: Option<i64>,
    expires_at: Option<String>,
    token_type: String,
    user: Option<WorkOSUser>,
    organization_id: Option<String>,
    authentication_method: Option<String>,
}

#[derive(Debug, Deserialize)]
struct WorkOSUser {
    id: String,
    email: String,
    first_name: Option<String>,
    last_name: Option<String>,
    #[serde(rename = "display_name")]
    display_name: Option<String>,
}

/// 使用 WorkOS Refresh Token 刷新凭证
pub async fn refresh_tokens_with_workos(
    refresh_token: &str,
    organization_id: Option<&str>,
    proxy_config: Option<&ProxyConfig>,
) -> Result<WorkOSTokenResponse> {
    let client = build_http_client(proxy_config)?;

    let form = RefreshTokenRequest {
        grant_type: "refresh_token".to_string(),
        refresh_token: refresh_token.to_string(),
        client_id: WORKOS_CLIENT_ID.to_string(),
        organization_id: organization_id.map(|s| s.to_string()),
    };

    let response = client
        .post(WORKOS_TOKEN_URL)
        .header("Content-Type", "application/x-www-form-urlencoded")
        .form(&form)
        .timeout(Duration::from_secs(30))
        .send()
        .await?;

    if !response.status().is_success() {
        let error_text = response.text().await?;
        return Err(Error::WorkOSAuthFailed(error_text));
    }

    let token_response: WorkOSTokenResponse = response.json().await?;

    if token_response.access_token.is_empty() {
        return Err(Error::InvalidTokenResponse);
    }

    Ok(token_response)
}

/// 获取 Factory 组织 ID 列表
pub async fn fetch_factory_org_ids(
    access_token: &str,
    proxy_config: Option<&ProxyConfig>,
) -> Result<Vec<String>> {
    let client = build_http_client(proxy_config)?;

    let response = client
        .get(FACTORY_ORG_API)
        .header("Authorization", format!("Bearer {}", access_token))
        .header("Content-Type", "application/json")
        .header("Accept", "application/json")
        .header("x-factory-client", "cli")
        .timeout(Duration::from_secs(15))
        .send()
        .await?;

    if !response.status().is_success() {
        return Ok(vec![]);
    }

    #[derive(Deserialize)]
    struct OrgResponse {
        #[serde(rename = "workosOrgIds")]
        workos_org_ids: Option<Vec<String>>,
    }

    let org_response: OrgResponse = response.json().await?;

    Ok(org_response.workos_org_ids.unwrap_or_default())
}

3.2 API Key 模式

use uuid::Uuid;
use sha2::{Sha256, Digest};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiKeyEntry {
    pub id: String,
    pub hash: String,
    pub encrypted_key: String,
    pub created_at: String,
    pub last_used_at: String,
    pub usage_count: u64,
    pub status: ApiKeyStatus,
    pub error_message: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ApiKeyStatus {
    Active,
    Error,
}

impl DroidProvider {
    /// 构建 API Key 条目列表
    pub fn build_api_key_entries(
        &self,
        api_keys: &[String],
        existing_entries: &[ApiKeyEntry],
        clear_existing: bool,
    ) -> Vec<ApiKeyEntry> {
        let now = Utc::now().to_rfc3339();

        let mut entries = if clear_existing {
            vec![]
        } else {
            existing_entries
                .iter()
                .filter(|e| !e.id.is_empty() && !e.encrypted_key.is_empty())
                .cloned()
                .collect()
        };

        let existing_hashes: HashSet<_> = entries
            .iter()
            .filter_map(|e| Some(e.hash.clone()))
            .collect();

        for raw_key in api_keys {
            let trimmed = raw_key.trim();
            if trimmed.is_empty() {
                continue;
            }

            // 计算 SHA256 哈希用于去重
            let mut hasher = Sha256::new();
            hasher.update(trimmed.as_bytes());
            let hash = format!("{:x}", hasher.finalize());

            if existing_hashes.contains(&hash) {
                continue;
            }

            entries.push(ApiKeyEntry {
                id: Uuid::new_v4().to_string(),
                hash,
                encrypted_key: self.encrypt_sensitive_data(trimmed)?,
                created_at: now.clone(),
                last_used_at: String::new(),
                usage_count: 0,
                status: ApiKeyStatus::Active,
                error_message: None,
            });
        }

        entries
    }

    /// 选择一个可用的 API Key(随机 + 粘性会话)
    pub async fn select_api_key(
        &self,
        account_id: &str,
        endpoint_type: &str,
        session_hash: Option<&str>,
    ) -> Result<ApiKeyEntry> {
        let entries = self.get_decrypted_api_key_entries(account_id).await?;

        if entries.is_empty() {
            return Err(Error::NoApiKeyConfigured);
        }

        // 过滤掉异常状态的 API Key
        let active_entries: Vec<_> = entries
            .into_iter()
            .filter(|e| matches!(e.status, ApiKeyStatus::Active))
            .collect();

        if active_entries.is_empty() {
            return Err(Error::AllApiKeysInError);
        }

        // 检查粘性会话映射
        if let Some(hash) = session_hash {
            let sticky_key = self.compose_sticky_key(account_id, endpoint_type, hash);

            if let Some(mapped_key_id) = self.get_session_mapping(&sticky_key).await? {
                if let Some(entry) = active_entries.iter().find(|e| e.id == mapped_key_id) {
                    self.extend_session_mapping_ttl(&sticky_key).await?;
                    self.touch_api_key_usage(account_id, &entry.id).await?;
                    return Ok(entry.clone());
                }
                // 映射的 Key 不可用,清除映射
                self.delete_session_mapping(&sticky_key).await?;
            }
        }

        // 随机选择一个可用的 API Key
        let selected = active_entries
            .choose(&mut rand::thread_rng())
            .ok_or(Error::NoApiKeyAvailable)?;

        // 建立粘性会话映射
        if let Some(hash) = session_hash {
            let sticky_key = self.compose_sticky_key(account_id, endpoint_type, hash);
            self.set_session_mapping(&sticky_key, &selected.id).await?;
        }

        self.touch_api_key_usage(account_id, &selected.id).await?;

        Ok(selected.clone())
    }

    /// 标记 API Key 为异常状态
    pub async fn mark_api_key_as_error(
        &self,
        account_id: &str,
        key_id: &str,
        error_message: &str,
    ) -> Result<()> {
        let mut account = self.get_account(account_id).await?;

        for entry in &mut account.api_key_entries {
            if entry.id == key_id {
                entry.status = ApiKeyStatus::Error;
                entry.error_message = Some(error_message.to_string());
                break;
            }
        }

        self.save_account(&account).await?;

        log::warn!(
            "⚠️ 已标记 Droid API Key {} 为异常状态(Account: {}):{}",
            key_id, account_id, error_message
        );

        Ok(())
    }
}

3.3 手动模式(Manual Provision)

/// 手动提供 Refresh Token
pub async fn create_manual_account(
    name: &str,
    refresh_token: &str,
    proxy_config: Option<&ProxyConfig>,
) -> Result<DroidAccount> {
    // 使用 Refresh Token 刷新获取 Access Token
    let token_response = refresh_tokens_with_workos(
        refresh_token,
        None,
        proxy_config,
    ).await?;

    let now = Utc::now();
    let expires_at = if let Some(expires_in) = token_response.expires_in {
        now + Duration::seconds(expires_in)
    } else {
        now + Duration::hours(8) // 默认 8 小时
    };

    let mut account = DroidAccount {
        id: Uuid::new_v4().to_string(),
        name: name.to_string(),
        access_token: token_response.access_token,
        refresh_token: token_response.refresh_token
            .unwrap_or_else(|| refresh_token.to_string()),
        expires_at: Some(expires_at),
        organization_id: token_response.organization_id,
        authentication_method: "manual".to_string(),
        status: AccountStatus::Active,
        ..Default::default()
    };

    // 提取用户信息
    if let Some(user) = token_response.user {
        account.owner_email = Some(user.email.clone());
        account.owner_name = user.display_name
            .or_else(|| {
                let parts: Vec<_> = [user.first_name, user.last_name]
                    .into_iter()
                    .flatten()
                    .collect();
                if parts.is_empty() { None } else { Some(parts.join(" ")) }
            });
        account.user_id = Some(user.id);
    }

    Ok(account)
}

四、Token 刷新机制

4.1 自动刷新逻辑

impl DroidProvider {
    /// Token 刷新间隔(6 小时)
    const REFRESH_INTERVAL_HOURS: i64 = 6;

    /// Token 有效期(8 小时)
    const TOKEN_VALID_HOURS: i64 = 8;

    /// 检查 Token 是否需要刷新
    pub fn should_refresh_token(&self, account: &DroidAccount) -> bool {
        // API Key 模式不需要刷新
        if account.authentication_method == "api_key" {
            return false;
        }

        // 从未刷新过
        let last_refresh = match &account.last_refresh_at {
            Some(t) => t,
            None => return true,
        };

        let last_refresh_time = DateTime::parse_from_rfc3339(last_refresh)
            .map(|t| t.with_timezone(&Utc))
            .unwrap_or(Utc::now() - Duration::hours(Self::REFRESH_INTERVAL_HOURS + 1));

        let hours_since_refresh = (Utc::now() - last_refresh_time).num_hours();

        hours_since_refresh >= Self::REFRESH_INTERVAL_HOURS
    }

    /// 刷新 Token(带重试)
    pub async fn refresh_access_token(
        &self,
        account_id: &str,
        proxy_config: Option<&ProxyConfig>,
    ) -> Result<TokenRefreshResult> {
        let mut account = self.get_account(account_id).await?;

        let refresh_token = account.refresh_token.as_ref()
            .ok_or(Error::MissingRefreshToken)?;

        log::info!("🔄 Refreshing Droid account token: {} ({})", account.name, account_id);

        let mut last_error = None;
        const MAX_RETRIES: u32 = 3;

        for attempt in 0..MAX_RETRIES {
            match refresh_tokens_with_workos(
                refresh_token,
                account.organization_id.as_deref(),
                proxy_config,
            ).await {
                Ok(response) => {
                    // 更新账户信息
                    account.access_token = response.access_token.clone();
                    if let Some(new_refresh) = response.refresh_token {
                        account.refresh_token = Some(new_refresh);
                    }

                    let expires_at = if let Some(expires_in) = response.expires_in {
                        Utc::now() + Duration::seconds(expires_in)
                    } else {
                        Utc::now() + Duration::hours(Self::TOKEN_VALID_HOURS)
                    };
                    account.expires_at = Some(expires_at);
                    account.last_refresh_at = Some(Utc::now().to_rfc3339());
                    account.status = AccountStatus::Active;
                    account.error_message = None;

                    // 更新用户信息
                    if let Some(user) = response.user {
                        account.owner_email = Some(user.email);
                        if let Some(name) = user.display_name
                            .or_else(|| {
                                let parts: Vec<_> = [user.first_name, user.last_name]
                                    .into_iter()
                                    .flatten()
                                    .collect();
                                if parts.is_empty() { None } else { Some(parts.join(" ")) }
                            }) {
                            account.owner_name = Some(name);
                        }
                        account.user_id = Some(user.id);
                    }

                    if let Some(org_id) = response.organization_id {
                        account.organization_id = Some(org_id);
                    }

                    self.save_account(&account).await?;

                    log::info!("✅ Droid account token refreshed successfully: {}", account_id);

                    return Ok(TokenRefreshResult {
                        access_token: response.access_token,
                        refresh_token: account.refresh_token.clone(),
                        expires_at,
                    });
                }
                Err(e) => {
                    last_error = Some(e);
                    // 指数退避
                    let delay = Duration::milliseconds(1000 * 2_i64.pow(attempt));
                    tokio::time::sleep(delay.to_std().unwrap()).await;
                }
            }
        }

        // 所有重试失败,更新账户状态
        account.status = AccountStatus::Error;
        account.error_message = last_error.as_ref().map(|e| e.to_string());
        self.save_account(&account).await?;

        Err(last_error.unwrap())
    }

    /// 获取有效的 Access Token(自动刷新)
    pub async fn get_valid_access_token(&self, account_id: &str) -> Result<String> {
        let account = self.get_account(account_id).await?;

        // API Key 模式抛出错误
        if account.authentication_method == "api_key" {
            return Err(Error::ApiKeyModeNoAccessToken);
        }

        // 检查是否需要刷新
        if self.should_refresh_token(&account) {
            log::info!("🔄 Droid account token needs refresh: {}", account_id);
            let proxy_config = account.proxy.as_ref();
            self.refresh_access_token(account_id, proxy_config).await?;
        }

        let account = self.get_account(account_id).await?;

        account.access_token.ok_or(Error::NoValidAccessToken)
    }
}

五、账号调度机制

5.1 调度器实现

pub struct DroidScheduler {
    sticky_prefix: String,
}

impl DroidScheduler {
    pub fn new() -> Self {
        Self {
            sticky_prefix: "droid".to_string(),
        }
    }

    /// 检查账户是否活跃
    fn is_account_active(&self, account: &DroidAccount) -> bool {
        if !account.is_active {
            return false;
        }

        let unhealthy_statuses = ["error", "unauthorized", "blocked"];
        let status = account.status.to_string().to_lowercase();

        !unhealthy_statuses.contains(&status.as_str())
    }

    /// 检查账户是否可调度
    fn is_account_schedulable(&self, account: &DroidAccount) -> bool {
        account.schedulable.unwrap_or(true)
    }

    /// 检查订阅是否过期
    fn is_subscription_expired(&self, account: &DroidAccount) -> bool {
        match &account.subscription_expires_at {
            Some(expires_at) => {
                let expiry = DateTime::parse_from_rfc3339(expires_at)
                    .map(|t| t.with_timezone(&Utc))
                    .unwrap_or(Utc::now() + Duration::days(365));
                expiry <= Utc::now()
            }
            None => false, // 未设置视为永不过期
        }
    }

    /// 检查端点类型匹配
    fn matches_endpoint(&self, account: &DroidAccount, endpoint_type: &str) -> bool {
        let account_endpoint = account.endpoint_type.as_deref().unwrap_or("anthropic");

        if account_endpoint == endpoint_type {
            return true;
        }

        // comm 端点可以使用任何类型的账户
        if endpoint_type == "comm" {
            return true;
        }

        // anthropic 和 openai 可以互换
        let shared_endpoints = ["anthropic", "openai"];
        shared_endpoints.contains(&endpoint_type) && shared_endpoints.contains(&account_endpoint)
    }

    /// 排序候选账户
    fn sort_candidates(&self, mut candidates: Vec<DroidAccount>) -> Vec<DroidAccount> {
        candidates.sort_by(|a, b| {
            // 1. 按优先级排序(低优先级优先)
            let priority_a = a.priority.unwrap_or(50);
            let priority_b = b.priority.unwrap_or(50);

            if priority_a != priority_b {
                return priority_a.cmp(&priority_b);
            }

            // 2. 按最后使用时间排序(最久未使用优先)
            let last_used_a = a.last_used_at.as_ref()
                .and_then(|t| DateTime::parse_from_rfc3339(t).ok())
                .map(|t| t.timestamp())
                .unwrap_or(0);
            let last_used_b = b.last_used_at.as_ref()
                .and_then(|t| DateTime::parse_from_rfc3339(t).ok())
                .map(|t| t.timestamp())
                .unwrap_or(0);

            if last_used_a != last_used_b {
                return last_used_a.cmp(&last_used_b);
            }

            // 3. 按创建时间排序
            let created_a = a.created_at.as_ref()
                .and_then(|t| DateTime::parse_from_rfc3339(t).ok())
                .map(|t| t.timestamp())
                .unwrap_or(0);
            let created_b = b.created_at.as_ref()
                .and_then(|t| DateTime::parse_from_rfc3339(t).ok())
                .map(|t| t.timestamp())
                .unwrap_or(0);

            created_a.cmp(&created_b)
        });

        candidates
    }

    /// 选择一个可用账户
    pub async fn select_account(
        &self,
        api_key_data: Option<&ApiKeyData>,
        endpoint_type: &str,
        session_hash: Option<&str>,
    ) -> Result<DroidAccount> {
        let mut candidates = vec![];
        let mut is_dedicated_binding = false;

        // 检查 API Key 绑定
        if let Some(api_key) = api_key_data {
            if let Some(binding) = &api_key.droid_account_id {
                if binding.starts_with("group:") {
                    // 分组调度
                    let group_id = &binding[6..];
                    candidates = self.load_group_accounts(group_id).await?;
                } else {
                    // 专用账户绑定
                    if let Ok(account) = self.get_account(binding).await {
                        candidates = vec![account];
                        is_dedicated_binding = true;
                    }
                }
            }
        }

        // 无绑定时获取所有可调度账户
        if candidates.is_empty() {
            candidates = self.get_schedulable_accounts(endpoint_type).await?;
        }

        // 过滤有效账户
        let filtered: Vec<_> = candidates
            .into_iter()
            .filter(|a| {
                self.is_account_active(a) &&
                self.is_account_schedulable(a) &&
                !self.is_subscription_expired(a) &&
                self.matches_endpoint(a, endpoint_type)
            })
            .collect();

        if filtered.is_empty() {
            return Err(Error::NoAvailableAccount {
                endpoint_type: endpoint_type.to_string(),
            });
        }

        // 检查粘性会话
        if let Some(hash) = session_hash {
            if !is_dedicated_binding {
                let sticky_key = self.compose_sticky_key(endpoint_type, hash, api_key_data);

                if let Some(mapped_id) = self.get_session_mapping(&sticky_key).await? {
                    if let Some(account) = filtered.iter().find(|a| a.id == mapped_id) {
                        self.extend_session_mapping_ttl(&sticky_key).await?;
                        self.touch_last_used(account).await?;
                        return Ok(account.clone());
                    }
                    // 映射的账户不可用,清除映射
                    self.delete_session_mapping(&sticky_key).await?;
                }
            }
        }

        // 排序并选择
        let sorted = self.sort_candidates(filtered);
        let selected = sorted.first().ok_or(Error::NoSchedulableAccount)?;

        // 建立粘性会话映射
        if let Some(hash) = session_hash {
            if !is_dedicated_binding {
                let sticky_key = self.compose_sticky_key(endpoint_type, hash, api_key_data);
                self.set_session_mapping(&sticky_key, &selected.id).await?;
            }
        }

        self.touch_last_used(selected).await?;

        log::info!(
            "🤖 选择 Droid 账号 {}(endpoint: {}, priority: {})",
            selected.name.as_deref().unwrap_or(&selected.id),
            endpoint_type,
            selected.priority.unwrap_or(50)
        );

        Ok(selected.clone())
    }

    /// 构建粘性会话 Key
    fn compose_sticky_key(
        &self,
        endpoint_type: &str,
        session_hash: &str,
        api_key_data: Option<&ApiKeyData>,
    ) -> String {
        let api_key_part = api_key_data
            .map(|k| k.id.as_str())
            .unwrap_or("default");

        format!("{}:{}:{}:{}", self.sticky_prefix, endpoint_type, api_key_part, session_hash)
    }
}

六、请求转发服务

6.1 RelayService 实现

const FACTORY_API_BASE_URL: &str = "https://api.factory.ai/api/llm";
const SYSTEM_PROMPT: &str = "You are Droid, an AI software engineering agent built by Factory.";

pub struct DroidRelayService {
    factory_api_base_url: String,
    user_agent: String,
    system_prompt: String,
    endpoints: HashMap<String, String>,
}

impl DroidRelayService {
    pub fn new() -> Self {
        let mut endpoints = HashMap::new();
        endpoints.insert("anthropic".to_string(), "/a/v1/messages".to_string());
        endpoints.insert("openai".to_string(), "/o/v1/responses".to_string());
        endpoints.insert("comm".to_string(), "/o/v1/chat/completions".to_string());

        Self {
            factory_api_base_url: FACTORY_API_BASE_URL.to_string(),
            user_agent: "factory-cli/0.32.1".to_string(),
            system_prompt: SYSTEM_PROMPT.to_string(),
            endpoints,
        }
    }

    /// 构建请求头
    fn build_headers(
        &self,
        access_token: &str,
        endpoint_type: &str,
        request_body: &Value,
        account: &DroidAccount,
    ) -> HeaderMap {
        let mut headers = HeaderMap::new();

        // 基础头
        headers.insert("Content-Type", "application/json".parse().unwrap());
        headers.insert("Authorization", format!("Bearer {}", access_token).parse().unwrap());
        headers.insert("User-Agent", account.user_agent.as_deref()
            .unwrap_or(&self.user_agent).parse().unwrap());
        headers.insert("x-factory-client", "cli".parse().unwrap());
        headers.insert("Connection", "keep-alive".parse().unwrap());

        // Anthropic 特定头
        if endpoint_type == "anthropic" {
            headers.insert("Accept", "application/json".parse().unwrap());
            headers.insert("anthropic-version", "2023-06-01".parse().unwrap());
            headers.insert("x-api-key", "placeholder".parse().unwrap());
            headers.insert("x-api-provider", "anthropic".parse().unwrap());

            // 推理模式
            if self.is_thinking_requested(request_body) {
                headers.insert("anthropic-beta", "interleaved-thinking-2025-05-14".parse().unwrap());
            }
        }

        // OpenAI 特定头
        if endpoint_type == "openai" {
            let model = request_body.get("model")
                .and_then(|m| m.as_str())
                .unwrap_or("");

            // -max 模型使用 openai provider,其他使用 azure_openai
            let provider = if model.to_lowercase().contains("-max") {
                "openai"
            } else {
                "azure_openai"
            };
            headers.insert("x-api-provider", provider.parse().unwrap());
        }

        // Comm 端点根据模型动态设置 provider
        if endpoint_type == "comm" {
            let model = request_body.get("model")
                .and_then(|m| m.as_str())
                .unwrap_or("");
            let provider = self.infer_provider_from_model(model);
            headers.insert("x-api-provider", provider.parse().unwrap());
        }

        headers
    }

    /// 根据模型推断 API Provider
    fn infer_provider_from_model(&self, model: &str) -> &'static str {
        let lower_model = model.to_lowercase();

        if lower_model.starts_with("gemini-") || lower_model.contains("gemini") {
            return "google";
        }
        if lower_model.starts_with("claude-") || lower_model.contains("claude") {
            return "anthropic";
        }
        if lower_model.starts_with("gpt-") || lower_model.contains("gpt") {
            return "azure_openai";
        }
        if lower_model.starts_with("glm-") || lower_model.contains("glm") {
            return "fireworks";
        }

        "baseten"
    }

    /// 处理请求体(注入 system prompt)
    fn process_request_body(
        &self,
        mut body: Value,
        endpoint_type: &str,
        stream_requested: bool,
    ) -> Value {
        // 删除 metadata 字段
        body.as_object_mut().map(|obj| obj.remove("metadata"));

        // 设置 stream 字段
        if stream_requested {
            body["stream"] = json!(true);
        } else if body.get("stream").is_some() {
            body["stream"] = json!(false);
        }

        // Anthropic 端点:注入系统提示
        if endpoint_type == "anthropic" && !self.system_prompt.is_empty() {
            let prompt_block = json!({ "type": "text", "text": self.system_prompt });

            if let Some(system) = body.get_mut("system") {
                if let Some(arr) = system.as_array_mut() {
                    // 检查是否已存在
                    let has_prompt = arr.iter().any(|item| {
                        item.get("type").and_then(|t| t.as_str()) == Some("text") &&
                        item.get("text").and_then(|t| t.as_str()) == Some(&self.system_prompt)
                    });
                    if !has_prompt {
                        arr.insert(0, prompt_block);
                    }
                }
            } else {
                body["system"] = json!([prompt_block]);
            }
        }

        // OpenAI 端点:前置系统提示到 instructions
        if endpoint_type == "openai" && !self.system_prompt.is_empty() {
            if let Some(instructions) = body.get_mut("instructions") {
                if let Some(inst_str) = instructions.as_str() {
                    if !inst_str.starts_with(&self.system_prompt) {
                        body["instructions"] = json!(format!("{}{}", self.system_prompt, inst_str));
                    }
                }
            } else {
                body["instructions"] = json!(self.system_prompt);
            }
        }

        // Comm 端点:在 messages 前注入 system 消息
        if endpoint_type == "comm" && !self.system_prompt.is_empty() {
            if let Some(messages) = body.get_mut("messages") {
                if let Some(arr) = messages.as_array_mut() {
                    let has_system = arr.iter().any(|m| {
                        m.get("role").and_then(|r| r.as_str()) == Some("system")
                    });

                    if !has_system {
                        arr.insert(0, json!({
                            "role": "system",
                            "content": self.system_prompt
                        }));
                    }
                }
            }
        }

        // 处理 temperature 和 top_p 冲突
        if body.get("temperature").is_some() && body.get("top_p").is_some() {
            body.as_object_mut().map(|obj| obj.remove("top_p"));
        }

        body
    }

    /// 模型名称映射
    fn normalize_request_body(&self, mut body: Value, endpoint_type: &str) -> Value {
        if let Some(model) = body.get("model").and_then(|m| m.as_str()) {
            let model_lower = model.to_lowercase();

            // Anthropic 端点:haiku 映射为 sonnet
            if endpoint_type == "anthropic" && model_lower.contains("haiku") {
                body["model"] = json!("claude-sonnet-4-20250514");
                log::info!("🔄 将请求模型从 {} 映射为 claude-sonnet-4-20250514", model);
            }

            // OpenAI 端点:gpt-5 映射为具体版本
            if endpoint_type == "openai" && model_lower == "gpt-5" {
                body["model"] = json!("gpt-5-2025-08-07");
                log::info!("🔄 将请求模型从 {} 映射为 gpt-5-2025-08-07", model);
            }
        }

        body
    }

    /// 转发请求
    pub async fn relay_request(
        &self,
        request_body: Value,
        api_key_data: Option<&ApiKeyData>,
        endpoint_type: &str,
        session_hash: Option<&str>,
    ) -> Result<RelayResponse> {
        let normalized_body = self.normalize_request_body(request_body.clone(), endpoint_type);

        // 选择账户
        let scheduler = DroidScheduler::new();
        let account = scheduler.select_account(
            api_key_data,
            endpoint_type,
            session_hash,
        ).await?;

        // 获取认证凭据
        let access_token = if account.authentication_method == "api_key" {
            let api_key = self.select_api_key(&account, endpoint_type, session_hash).await?;
            api_key.key
        } else {
            self.get_valid_access_token(&account.id).await?
        };

        // 构建 API URL
        let endpoint_path = self.endpoints.get(endpoint_type)
            .ok_or(Error::InvalidEndpointType)?;
        let api_url = format!("{}{}", self.factory_api_base_url, endpoint_path);

        // 构建请求头
        let headers = self.build_headers(&access_token, endpoint_type, &normalized_body, &account);

        // 处理请求体
        let stream_requested = normalized_body.get("stream")
            .and_then(|s| s.as_bool())
            .unwrap_or(false);
        let processed_body = self.process_request_body(normalized_body, endpoint_type, stream_requested);

        // 获取代理配置
        let proxy_config = account.proxy.as_ref();

        // 发送请求
        if stream_requested {
            self.handle_stream_request(api_url, headers, processed_body, proxy_config).await
        } else {
            self.handle_non_stream_request(api_url, headers, processed_body, proxy_config).await
        }
    }
}

七、SSE 响应解析

7.1 Anthropic SSE 解析

/// 从 SSE 流中解析 Anthropic usage 数据
fn parse_anthropic_usage_from_sse(chunk: &str, current_usage: &mut UsageData) {
    for line in chunk.lines() {
        if !line.starts_with("data: ") || line.len() <= 6 {
            continue;
        }

        let json_str = &line[6..];
        if let Ok(data) = serde_json::from_str::<Value>(json_str) {
            // message_start 包含 input tokens 和 cache tokens
            if data.get("type").and_then(|t| t.as_str()) == Some("message_start") {
                if let Some(usage) = data.get("message").and_then(|m| m.get("usage")) {
                    current_usage.input_tokens = usage.get("input_tokens")
                        .and_then(|v| v.as_u64())
                        .unwrap_or(0);
                    current_usage.cache_creation_input_tokens = usage
                        .get("cache_creation_input_tokens")
                        .and_then(|v| v.as_u64())
                        .unwrap_or(0);
                    current_usage.cache_read_input_tokens = usage
                        .get("cache_read_input_tokens")
                        .and_then(|v| v.as_u64())
                        .unwrap_or(0);

                    // 详细的缓存类型
                    if let Some(cache_creation) = usage.get("cache_creation") {
                        current_usage.ephemeral_5m_input_tokens = cache_creation
                            .get("ephemeral_5m_input_tokens")
                            .and_then(|v| v.as_u64())
                            .unwrap_or(0);
                        current_usage.ephemeral_1h_input_tokens = cache_creation
                            .get("ephemeral_1h_input_tokens")
                            .and_then(|v| v.as_u64())
                            .unwrap_or(0);
                    }
                }
            }

            // message_delta 包含 output tokens
            if data.get("type").and_then(|t| t.as_str()) == Some("message_delta") {
                if let Some(usage) = data.get("usage") {
                    current_usage.output_tokens = usage.get("output_tokens")
                        .and_then(|v| v.as_u64())
                        .unwrap_or(0);
                }
            }
        }
    }
}

7.2 OpenAI SSE 解析

/// 从 SSE 流中解析 OpenAI usage 数据
fn parse_openai_usage_from_sse(chunk: &str, current_usage: &mut UsageData) {
    for line in chunk.lines() {
        if !line.starts_with("data: ") || line.len() <= 6 {
            continue;
        }

        let json_str = &line[6..];
        if json_str == "[DONE]" {
            continue;
        }

        if let Ok(data) = serde_json::from_str::<Value>(json_str) {
            // 传统 Chat Completions usage 字段
            if let Some(usage) = data.get("usage") {
                current_usage.input_tokens = usage.get("prompt_tokens")
                    .and_then(|v| v.as_u64())
                    .unwrap_or(0);
                current_usage.output_tokens = usage.get("completion_tokens")
                    .and_then(|v| v.as_u64())
                    .unwrap_or(0);
                current_usage.total_tokens = usage.get("total_tokens")
                    .and_then(|v| v.as_u64())
                    .unwrap_or(0);

                // 缓存 tokens
                if let Some(details) = usage.get("input_tokens_details") {
                    current_usage.cache_read_input_tokens = details
                        .get("cached_tokens")
                        .and_then(|v| v.as_u64())
                        .unwrap_or(0);
                }
            }

            // 新 Response API 在 response.usage 中返回统计
            if let Some(response) = data.get("response") {
                if let Some(usage) = response.get("usage") {
                    current_usage.input_tokens = usage.get("input_tokens")
                        .or_else(|| usage.get("prompt_tokens"))
                        .and_then(|v| v.as_u64())
                        .unwrap_or(0);
                    current_usage.output_tokens = usage.get("output_tokens")
                        .or_else(|| usage.get("completion_tokens"))
                        .and_then(|v| v.as_u64())
                        .unwrap_or(0);
                    current_usage.total_tokens = usage.get("total_tokens")
                        .and_then(|v| v.as_u64())
                        .unwrap_or(0);
                }
            }
        }
    }
}

/// 检测流式响应完成标记
fn detect_stream_completion(window: &str, endpoint_type: &str) -> bool {
    let lower = window.to_lowercase();
    let compact = lower.replace(char::is_whitespace, "");

    match endpoint_type {
        "anthropic" => {
            lower.contains("event: message_stop") ||
            compact.contains("\"type\":\"message_stop\"")
        }
        "openai" | "comm" => {
            lower.contains("data: [done]") ||
            compact.contains("\"finish_reason\"") ||
            lower.contains("event: response.done") ||
            lower.contains("event: response.completed") ||
            compact.contains("\"type\":\"response.done\"") ||
            compact.contains("\"type\":\"response.completed\"")
        }
        _ => false,
    }
}

八、凭证加密

8.1 加密实现

use aes::Aes256;
use block_modes::{BlockMode, Cbc};
use block_modes::block_padding::Pkcs7;
use scrypt::{scrypt, Params};

type Aes256Cbc = Cbc<Aes256, Pkcs7>;

const ENCRYPTION_ALGORITHM: &str = "aes-256-cbc";
const ENCRYPTION_SALT: &[u8] = b"droid-account-salt";

impl DroidProvider {
    /// 派生加密密钥(使用 scrypt)
    fn derive_encryption_key(&self, master_key: &str) -> [u8; 32] {
        // 缓存派生的密钥以提高性能
        if let Some(cached) = self.encryption_key_cache.get() {
            return *cached;
        }

        let params = Params::new(15, 8, 1).unwrap();
        let mut key = [0u8; 32];
        scrypt(master_key.as_bytes(), ENCRYPTION_SALT, &params, &mut key).unwrap();

        self.encryption_key_cache.set(key);
        key
    }

    /// 加密敏感数据
    pub fn encrypt_sensitive_data(&self, plaintext: &str) -> Result<String> {
        if plaintext.is_empty() {
            return Ok(String::new());
        }

        let key = self.derive_encryption_key(&self.config.encryption_key);
        let iv: [u8; 16] = rand::random();

        let cipher = Aes256Cbc::new_from_slices(&key, &iv)?;
        let ciphertext = cipher.encrypt_vec(plaintext.as_bytes());

        // 格式:iv_hex:ciphertext_hex
        Ok(format!("{}:{}", hex::encode(iv), hex::encode(ciphertext)))
    }

    /// 解密敏感数据(带 LRU 缓存)
    pub fn decrypt_sensitive_data(&self, encrypted: &str) -> Result<String> {
        if encrypted.is_empty() {
            return Ok(String::new());
        }

        // 检查缓存
        let cache_key = {
            let mut hasher = Sha256::new();
            hasher.update(encrypted.as_bytes());
            format!("{:x}", hasher.finalize())
        };

        if let Some(cached) = self.decrypt_cache.get(&cache_key) {
            return Ok(cached.clone());
        }

        let key = self.derive_encryption_key(&self.config.encryption_key);

        let parts: Vec<&str> = encrypted.split(':').collect();
        if parts.len() != 2 {
            return Err(Error::InvalidEncryptedFormat);
        }

        let iv = hex::decode(parts[0])?;
        let ciphertext = hex::decode(parts[1])?;

        let cipher = Aes256Cbc::new_from_slices(&key, &iv)?;
        let plaintext = cipher.decrypt_vec(&ciphertext)?;
        let result = String::from_utf8(plaintext)?;

        // 存入缓存(5 分钟过期)
        self.decrypt_cache.insert(cache_key, result.clone(), Duration::minutes(5));

        Ok(result)
    }
}

九、错误处理

9.1 4xx 错误处理

impl DroidRelayService {
    /// 处理上游 4xx 响应
    async fn handle_upstream_client_error(
        &self,
        status_code: u16,
        context: ErrorContext,
    ) -> Result<()> {
        if status_code < 400 || status_code >= 500 {
            return Ok(());
        }

        let account_id = context.account.as_ref()
            .map(|a| a.id.as_str())
            .ok_or(Error::MissingAccountInfo)?;

        // API Key 模式:标记 Key 为异常
        if context.account.as_ref()
            .map(|a| a.authentication_method == "api_key")
            .unwrap_or(false)
        {
            if let Some(api_key) = &context.selected_api_key {
                self.mark_api_key_as_error(
                    account_id,
                    &api_key.id,
                    &format!("{}", status_code),
                ).await?;

                // 清理粘性会话映射
                self.clear_api_key_sticky_mapping(
                    account_id,
                    &context.endpoint_type,
                    context.session_hash.as_deref(),
                ).await?;

                // 检查是否还有可用的 API Key
                let entries = self.get_decrypted_api_key_entries(account_id).await?;
                let active_entries: Vec<_> = entries
                    .into_iter()
                    .filter(|e| matches!(e.status, ApiKeyStatus::Active))
                    .collect();

                if active_entries.is_empty() {
                    self.stop_account_scheduling(
                        account_id,
                        status_code,
                        "所有 API Key 均已异常",
                    ).await?;
                }

                return Ok(());
            }
        }

        // OAuth 模式:停止账号调度
        self.stop_account_scheduling(account_id, status_code, "凭证不可用").await?;
        self.clear_account_sticky_mapping(
            &context.endpoint_type,
            context.session_hash.as_deref(),
            context.client_api_key_id.as_deref(),
        ).await?;

        Ok(())
    }

    /// 停止账号调度
    async fn stop_account_scheduling(
        &self,
        account_id: &str,
        status_code: u16,
        reason: &str,
    ) -> Result<()> {
        let mut account = self.get_account(account_id).await?;

        account.schedulable = Some(false);
        account.status = AccountStatus::Error;
        account.error_message = Some(format!("上游返回 {}:{}", status_code, reason));

        self.save_account(&account).await?;

        log::warn!(
            "🚫 已停止调度 Droid 账号 {}(状态码 {},原因:{})",
            account_id, status_code, reason
        );

        Ok(())
    }

    /// 网络错误状态码映射
    fn map_network_error_status(&self, error: &Error) -> u16 {
        match error {
            Error::Timeout => 408,
            Error::ConnectionReset | Error::BrokenPipe => 424,
            Error::DnsResolution => 424,
            _ => 424,
        }
    }
}

9.2 错误类型

错误类型 说明 处理方式
WorkOSAuthFailed WorkOS OAuth 失败 重新授权
TokenRefreshFailed Token 刷新失败 重试或重新授权
NoApiKeyConfigured 未配置 API Key 添加 API Key
AllApiKeysInError 所有 API Key 异常 检查 Key 或添加新 Key
NoAvailableAccount 无可用账户 检查账户配置
SubscriptionExpired 订阅已过期 续订或使用其他账户
ConnectionReset 连接重置 自动重试
Timeout 请求超时 自动重试

十、前端 UI 实现

10.1 插件入口

// src/index.tsx
import { LimePluginSDK } from '@lime/plugin-sdk';
import { CredentialList } from './components/CredentialList';
import { AuthMethodTabs } from './components/AuthMethodTabs';
import { SettingsPanel } from './components/SettingsPanel';

interface PluginProps {
  sdk: LimePluginSDK;
  pluginId: string;
}

export default function DroidProviderUI({ sdk, pluginId }: PluginProps) {
  const [view, setView] = useState<'list' | 'add' | 'settings'>('list');
  const [credentials, setCredentials] = useState<DroidCredential[]>([]);

  useEffect(() => {
    loadCredentials();
  }, []);

  const loadCredentials = async () => {
    const result = await sdk.database.query<DroidCredential>(
      'SELECT * FROM plugin_credentials WHERE plugin_id = ? ORDER BY created_at DESC',
      [pluginId]
    );
    setCredentials(result);
  };

  return (
    <div className="droid-provider-ui">
      <Header>
        <Title>Droid Provider</Title>
        <Subtitle>Factory.ai 集成 - 支持 Claude, OpenAI, Gemini</Subtitle>
        <Actions>
          <Button onClick={() => setView('add')}>添加凭证</Button>
          <Button onClick={() => setView('settings')}>设置</Button>
        </Actions>
      </Header>

      {view === 'list' && (
        <CredentialList
          credentials={credentials}
          onRefresh={loadCredentials}
          sdk={sdk}
        />
      )}

      {view === 'add' && (
        <AuthMethodTabs
          sdk={sdk}
          onSuccess={() => {
            loadCredentials();
            setView('list');
          }}
          onCancel={() => setView('list')}
        />
      )}

      {view === 'settings' && (
        <SettingsPanel
          sdk={sdk}
          pluginId={pluginId}
          onClose={() => setView('list')}
        />
      )}
    </div>
  );
}

10.2 认证方式选择

// src/components/AuthMethodTabs.tsx

type AuthMethod = 'workos_oauth' | 'api_key' | 'manual';

interface AuthMethodTabsProps {
  sdk: LimePluginSDK;
  onSuccess: () => void;
  onCancel: () => void;
}

export function AuthMethodTabs({ sdk, onSuccess, onCancel }: AuthMethodTabsProps) {
  const [method, setMethod] = useState<AuthMethod>('workos_oauth');

  return (
    <div className="auth-method-tabs">
      <Tabs value={method} onChange={setMethod}>
        <Tab value="workos_oauth">
          <Icon name="Key" />
          WorkOS OAuth
        </Tab>
        <Tab value="api_key">
          <Icon name="Lock" />
          API Key
        </Tab>
        <Tab value="manual">
          <Icon name="Edit" />
          手动模式
        </Tab>
      </Tabs>

      <div className="tab-content">
        {method === 'workos_oauth' && <OAuthForm sdk={sdk} onSuccess={onSuccess} />}
        {method === 'api_key' && <ApiKeyForm sdk={sdk} onSuccess={onSuccess} />}
        {method === 'manual' && <ManualForm sdk={sdk} onSuccess={onSuccess} />}
      </div>

      <FormActions>
        <Button variant="secondary" onClick={onCancel}>取消</Button>
      </FormActions>
    </div>
  );
}

10.3 API Key 表单

// src/components/ApiKeyForm.tsx

export function ApiKeyForm({ sdk, onSuccess }: FormProps) {
  const [form, setForm] = useState({
    name: '',
    apiKeys: '',
    endpointType: 'anthropic' as EndpointType,
    proxy: '',
    subscriptionExpiresAt: '',
  });
  const [loading, setLoading] = useState(false);

  const handleSubmit = async () => {
    const apiKeyList = form.apiKeys
      .split('\n')
      .map(k => k.trim())
      .filter(k => k.length > 0);

    if (apiKeyList.length === 0) {
      sdk.notification.error('请输入至少一个 API Key');
      return;
    }

    setLoading(true);
    try {
      await sdk.http.request('/api/droid/account/create', {
        method: 'POST',
        body: JSON.stringify({
          name: form.name || `Droid API Key (${apiKeyList.length} keys)`,
          apiKeys: apiKeyList,
          endpointType: form.endpointType,
          authenticationMethod: 'api_key',
          proxy: form.proxy ? JSON.parse(form.proxy) : null,
          subscriptionExpiresAt: form.subscriptionExpiresAt || null,
        }),
      });
      sdk.notification.success(`成功添加 ${apiKeyList.length} 个 API Key`);
      onSuccess();
    } catch (error) {
      sdk.notification.error(`添加失败: ${error.message}`);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="api-key-form">
      <Alert type="info">
        支持多个 API Key,每行一个。系统将使用随机 + 粘性会话策略选择 Key。
      </Alert>

      <FormField>
        <Label>凭证名称(可选)</Label>
        <Input
          value={form.name}
          onChange={(e) => setForm({ ...form, name: e.target.value })}
          placeholder="我的 Droid API Keys"
        />
      </FormField>

      <FormField>
        <Label>API Keys *</Label>
        <TextArea
          rows={5}
          value={form.apiKeys}
          onChange={(e) => setForm({ ...form, apiKeys: e.target.value })}
          placeholder="每行一个 API Key&#10;sk-xxxx&#10;sk-yyyy&#10;sk-zzzz"
        />
        <HelpText>支持批量导入,每行一个 API Key</HelpText>
      </FormField>

      <FormField>
        <Label>端点类型 *</Label>
        <Select
          value={form.endpointType}
          onChange={(value) => setForm({ ...form, endpointType: value })}
        >
          <Option value="anthropic">Anthropic (Claude)</Option>
          <Option value="openai">OpenAI (GPT)</Option>
          <Option value="comm">Comm (通用)</Option>
        </Select>
        <HelpText>选择此账户支持的 API 端点类型</HelpText>
      </FormField>

      <FormField>
        <Label>订阅过期时间(可选)</Label>
        <Input
          type="datetime-local"
          value={form.subscriptionExpiresAt}
          onChange={(e) => setForm({ ...form, subscriptionExpiresAt: e.target.value })}
        />
        <HelpText>设置后,过期账户将不再被调度</HelpText>
      </FormField>

      <FormField>
        <Label>代理配置(可选)</Label>
        <TextArea
          rows={3}
          value={form.proxy}
          onChange={(e) => setForm({ ...form, proxy: e.target.value })}
          placeholder='{"type": "socks5", "host": "127.0.0.1", "port": 1080}'
        />
        <HelpText>JSON 格式的代理配置</HelpText>
      </FormField>

      <Button onClick={handleSubmit} loading={loading}>
        添加 API Key
      </Button>
    </div>
  );
}

10.4 凭证卡片

// src/components/CredentialCard.tsx

const ENDPOINT_TYPE_LABELS: Record<string, string> = {
  anthropic: 'Anthropic',
  openai: 'OpenAI',
  comm: 'Comm',
};

const AUTH_METHOD_LABELS: Record<string, string> = {
  workos_oauth: 'WorkOS OAuth',
  api_key: 'API Key',
  manual: '手动模式',
};

export function CredentialCard({ credential, onRefresh, onDelete }: CredentialCardProps) {
  const data = JSON.parse(credential.credential_data);
  const authMethod = data.authentication_method || 'workos_oauth';
  const endpointType = data.endpoint_type || 'anthropic';
  const isHealthy = credential.status === 'active';
  const apiKeyCount = data.api_key_count || 0;

  return (
    <Card className={`credential-card ${isHealthy ? 'healthy' : 'unhealthy'}`}>
      <CardHeader>
        <div className="status-indicator">
          <StatusDot status={isHealthy ? 'green' : 'red'} />
          <span>{isHealthy ? '健康' : '异常'}</span>
        </div>
        <div className="badges">
          <Badge color="blue">{AUTH_METHOD_LABELS[authMethod]}</Badge>
          <Badge color="purple">{ENDPOINT_TYPE_LABELS[endpointType]}</Badge>
        </div>
      </CardHeader>

      <CardBody>
        <div className="info-row">
          <label>名称</label>
          <span>{credential.name || '未命名'}</span>
        </div>

        {data.owner_email && (
          <div className="info-row">
            <label>邮箱</label>
            <span>{data.owner_email}</span>
          </div>
        )}

        {data.organization_id && (
          <div className="info-row">
            <label>组织 ID</label>
            <span className="truncate">{data.organization_id}</span>
          </div>
        )}

        {authMethod === 'api_key' && (
          <div className="info-row">
            <label>API Key 数量</label>
            <span>{apiKeyCount} 个</span>
          </div>
        )}

        {data.subscription_expires_at && (
          <div className="info-row">
            <label>订阅过期</label>
            <span>{formatDate(data.subscription_expires_at)}</span>
          </div>
        )}

        {data.last_refresh_at && (
          <div className="info-row">
            <label>上次刷新</label>
            <span>{formatDate(data.last_refresh_at)}</span>
          </div>
        )}

        {data.error_message && (
          <div className="info-row error">
            <label>错误信息</label>
            <span>{data.error_message}</span>
          </div>
        )}
      </CardBody>

      <CardFooter>
        {authMethod !== 'api_key' && (
          <Button size="small" onClick={onRefresh}>刷新 Token</Button>
        )}
        <Button size="small" variant="danger" onClick={onDelete}>删除</Button>
      </CardFooter>
    </Card>
  );
}

十一、凭证文件格式

11.1 WorkOS OAuth 凭证

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "wrkos_rt_...",
  "expires_at": "2025-01-05T12:34:56+00:00",
  "organization_id": "org_01HXXX...",
  "user_id": "user_01HXXX...",
  "owner_email": "user@example.com",
  "owner_name": "John Doe",
  "authentication_method": "oauth",
  "type": "droid_oauth"
}

11.2 API Key 凭证

{
  "api_keys": [
    {
      "id": "uuid-1",
      "hash": "sha256-hash",
      "encrypted_key": "iv:ciphertext",
      "created_at": "2025-01-05T10:00:00+00:00",
      "last_used_at": "2025-01-05T11:30:00+00:00",
      "usage_count": 42,
      "status": "active",
      "error_message": null
    }
  ],
  "endpoint_type": "anthropic",
  "authentication_method": "api_key",
  "type": "droid_api_key"
}

11.3 手动模式凭证

{
  "refresh_token": "wrkos_rt_...",
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_at": "2025-01-05T18:00:00+00:00",
  "authentication_method": "manual",
  "type": "droid_manual"
}

十二、客户端验证

12.1 Droid CLI 验证器

/// Droid CLI 验证器
/// 检查请求是否来自 Factory Droid CLI
pub struct DroidCliValidator;

impl DroidCliValidator {
    pub fn validate(headers: &HeaderMap) -> bool {
        let user_agent = headers
            .get("user-agent")
            .and_then(|v| v.to_str().ok())
            .unwrap_or("");

        let factory_client = headers
            .get("x-factory-client")
            .and_then(|v| v.to_str().ok())
            .unwrap_or("")
            .to_lowercase();

        // 检查 User-Agent 格式:factory-cli/x.x.x
        let ua_match = regex::Regex::new(r"factory-cli/(\d+\.\d+\.\d+)")
            .unwrap()
            .is_match(user_agent);

        // 检查 x-factory-client header
        let has_factory_header = factory_client.contains("droid") ||
                                  factory_client.contains("factory-cli");

        ua_match || has_factory_header
    }
}

十三、开发指南

13.1 本地开发

# 克隆仓库
git clone https://github.com/aiclientproxy/droid-provider.git
cd droid-provider

# 安装依赖
pnpm install
cd src-tauri && cargo build

# 前端开发
pnpm dev

# 后端开发
cargo watch -x run

13.2 测试

# 单元测试
cargo test

# WorkOS OAuth 测试
cargo test --test workos_oauth

# API Key 选择测试
cargo test --test api_key_selection

# 调度器测试
cargo test --test scheduler

# 前端测试
pnpm test

13.3 路由端点

路由 方法 说明
/droid/claude/v1/messages POST Anthropic Messages API
/droid/openai/v1/responses POST OpenAI Responses API
/droid/comm/v1/chat/completions POST Chat Completions API
/droid/*/v1/models GET 模型列表

附录

A. 环境变量

变量 说明 默认值
DROID_ENCRYPTION_KEY 加密主密钥 必填
DROID_DEBUG 调试模式 false
DROID_TIMEOUT_MS 请求超时 600000
DROID_REFRESH_INTERVAL_HOURS Token 刷新间隔 6

B. 参考链接