feat: v0.27.0 - 重构 Agent 架构并新增 API Key Provider 管理系统

主要变更:
- 引入基于 Protocol 的 Agent 架构(支持 OpenAI/Anthropic/Kiro/Gemini)
- 新增 API Key Provider 管理系统,支持多种第三方 AI 服务商
- 重构流式响应处理,新增统一的 Stream 处理模块
- 新增协议转换器(Translator)模块,支持 Kiro 到 OpenAI/Anthropic 的转换
- 前端新增 API Key Provider 配置界面和导入/导出功能
- 新增 50+ AI 服务商图标支持
- 优化 Agent 工具调用显示和流式渲染
- 完善数据库 schema,新增 api_key_providers 表

技术细节:
- 重构 native_agent.rs,引入策略模式的 Protocol trait
- 新增 parsers 和 generators 模块处理不同协议的 SSE 流
- 新增完整的 API Key 验证和连接测试功能
- 改进前端 TypeScript 类型定义

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
coso
2026-01-01 03:59:58 +08:00
co-authored by Claude Sonnet 4.5
parent 74fa3202ce
commit 59cd4ecfbd
161 changed files with 21741 additions and 1937 deletions
+813
View File
@@ -0,0 +1,813 @@
//! API Key Provider 属性测试
//!
//! 使用 proptest 进行属性测试,验证 API Key Provider 服务的正确性。
//!
//! **Feature: provider-ui-refactor**
use proptest::prelude::*;
use std::collections::HashSet;
use std::sync::Arc;
use tempfile::TempDir;
use proxycast_lib::database::dao::api_key_provider::{
ApiKeyEntry, ApiKeyProvider, ApiKeyProviderDao, ApiProviderType, ProviderGroup,
};
use proxycast_lib::database::{init_database, DbConnection};
use proxycast_lib::services::api_key_provider_service::ApiKeyProviderService;
use rusqlite::Connection;
/// 测试上下文
struct TestContext {
pub temp_dir: TempDir,
pub db: DbConnection,
pub service: ApiKeyProviderService,
}
impl TestContext {
/// 创建测试上下文
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
let temp_dir = TempDir::new()?;
let db_path = temp_dir.path().join("test.db");
let conn = Connection::open(&db_path)?;
// 创建表结构
conn.execute(
"CREATE TABLE IF NOT EXISTS api_key_providers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
api_host TEXT NOT NULL,
is_system INTEGER NOT NULL DEFAULT 0,
group_name TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0,
api_version TEXT,
project TEXT,
location TEXT,
region TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS api_keys (
id TEXT PRIMARY KEY,
provider_id TEXT NOT NULL,
api_key_encrypted TEXT NOT NULL,
alias TEXT,
enabled INTEGER NOT NULL DEFAULT 1,
usage_count INTEGER NOT NULL DEFAULT 0,
error_count INTEGER NOT NULL DEFAULT 0,
last_used_at TEXT,
created_at TEXT NOT NULL,
FOREIGN KEY (provider_id) REFERENCES api_key_providers(id) ON DELETE CASCADE
)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS provider_ui_state (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)",
[],
)?;
let db = Arc::new(std::sync::Mutex::new(conn));
let service = ApiKeyProviderService::new();
Ok(Self {
temp_dir,
db,
service,
})
}
/// 创建测试 Provider
pub fn create_test_provider(&self, id: &str) -> Result<ApiKeyProvider, String> {
let now = chrono::Utc::now();
let provider = ApiKeyProvider {
id: id.to_string(),
name: format!("Test Provider {}", id),
provider_type: ApiProviderType::Openai,
api_host: "https://api.test.com".to_string(),
is_system: false,
group: ProviderGroup::Custom,
enabled: true,
sort_order: 0,
api_version: None,
project: None,
location: None,
region: None,
created_at: now,
updated_at: now,
};
let conn = self.db.lock().map_err(|e| e.to_string())?;
ApiKeyProviderDao::insert_provider(&conn, &provider).map_err(|e| e.to_string())?;
Ok(provider)
}
/// 添加测试 API Key
pub fn add_test_api_key(
&self,
provider_id: &str,
api_key: &str,
) -> Result<ApiKeyEntry, String> {
self.service
.add_api_key(&self.db, provider_id, api_key, None)
}
}
// ============================================================================
// Property 12: 轮询负载均衡正确性
// **Validates: Requirements 7.3**
// ============================================================================
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
/// Property 12: 轮询负载均衡正确性
///
/// *对于任意* 拥有 N 个启用的 API Key 的 Provider,连续 N 次获取 API Key 应各返回不同的 Key
///
/// **Feature: provider-ui-refactor, Property 12: 轮询负载均衡正确性**
/// **Validates: Requirements 7.3**
#[test]
fn test_round_robin_load_balancing(num_keys in 2usize..10) {
let ctx = TestContext::new().expect("Failed to create test context");
// 创建测试 Provider
let provider_id = format!("test-provider-{}", uuid::Uuid::new_v4());
ctx.create_test_provider(&provider_id).expect("Failed to create provider");
// 添加 N 个 API Keys
let mut expected_keys = Vec::new();
for i in 0..num_keys {
let api_key = format!("sk-test-key-{}-{}", provider_id, i);
ctx.add_test_api_key(&provider_id, &api_key).expect("Failed to add API key");
expected_keys.push(api_key);
}
// 连续获取 N 次 API Key
let mut retrieved_keys = Vec::new();
for _ in 0..num_keys {
let key = ctx.service
.get_next_api_key(&ctx.db, &provider_id)
.expect("Failed to get next API key")
.expect("No API key returned");
retrieved_keys.push(key);
}
// 验证:连续 N 次获取应返回 N 个不同的 Key
let unique_keys: HashSet<_> = retrieved_keys.iter().collect();
prop_assert_eq!(
unique_keys.len(),
num_keys,
"Expected {} unique keys, but got {}. Keys: {:?}",
num_keys,
unique_keys.len(),
retrieved_keys
);
// 验证:所有返回的 Key 都在预期列表中
for key in &retrieved_keys {
prop_assert!(
expected_keys.contains(key),
"Unexpected key returned: {}",
key
);
}
}
/// Property 12 补充测试:轮询循环性
///
/// *对于任意* 拥有 N 个启用的 API Key 的 Provider,获取 2N 次应该循环使用所有 Key
///
/// **Feature: provider-ui-refactor, Property 12: 轮询负载均衡正确性**
/// **Validates: Requirements 7.3**
#[test]
fn test_round_robin_cycling(num_keys in 2usize..8) {
let ctx = TestContext::new().expect("Failed to create test context");
// 创建测试 Provider
let provider_id = format!("test-provider-cycle-{}", uuid::Uuid::new_v4());
ctx.create_test_provider(&provider_id).expect("Failed to create provider");
// 添加 N 个 API Keys
for i in 0..num_keys {
let api_key = format!("sk-cycle-key-{}-{}", provider_id, i);
ctx.add_test_api_key(&provider_id, &api_key).expect("Failed to add API key");
}
// 获取 2N 次 API Key
let mut first_cycle = Vec::new();
let mut second_cycle = Vec::new();
for i in 0..(num_keys * 2) {
let key = ctx.service
.get_next_api_key(&ctx.db, &provider_id)
.expect("Failed to get next API key")
.expect("No API key returned");
if i < num_keys {
first_cycle.push(key);
} else {
second_cycle.push(key);
}
}
// 验证:第一轮和第二轮应该返回相同的 Key 序列
prop_assert_eq!(
first_cycle,
second_cycle,
"Round robin should cycle through keys in the same order"
);
}
}
// ============================================================================
// Property 13: API Key 使用统计正确性
// **Validates: Requirements 7.4**
// ============================================================================
proptest! {
#![proptest_config(ProptestConfig::with_cases(20))]
/// Property 13: API Key 使用统计正确性
///
/// *对于任意* API Key 使用记录操作,使用次数应正确递增
///
/// **Feature: provider-ui-refactor, Property 13: API Key 使用统计正确性**
/// **Validates: Requirements 7.4**
#[test]
fn test_usage_count_increment(num_usages in 1usize..10) {
let ctx = TestContext::new().expect("Failed to create test context");
// 创建测试 Provider
let provider_id = format!("test-provider-usage-{}", uuid::Uuid::new_v4());
ctx.create_test_provider(&provider_id).expect("Failed to create provider");
// 添加 API Key
let api_key = format!("sk-usage-test-{}", provider_id);
let entry = ctx.add_test_api_key(&provider_id, &api_key)
.expect("Failed to add API key");
// 初始使用次数应为 0
prop_assert_eq!(entry.usage_count, 0, "Initial usage count should be 0");
// 记录 N 次使用
for _ in 0..num_usages {
ctx.service.record_usage(&ctx.db, &entry.id)
.expect("Failed to record usage");
}
// 获取更新后的 API Key
let conn = ctx.db.lock().expect("Failed to lock db");
let updated = ApiKeyProviderDao::get_api_key_by_id(&conn, &entry.id)
.expect("Failed to get API key")
.expect("API key not found");
// 验证:使用次数应等于记录次数
prop_assert_eq!(
updated.usage_count as usize,
num_usages,
"Usage count should equal number of record_usage calls"
);
// 验证:最后使用时间应被更新
prop_assert!(
updated.last_used_at.is_some(),
"last_used_at should be set after usage"
);
}
/// Property 13 补充测试:错误次数递增
///
/// *对于任意* API Key 错误记录操作,错误次数应正确递增
///
/// **Feature: provider-ui-refactor, Property 13: API Key 使用统计正确性**
/// **Validates: Requirements 7.4**
#[test]
fn test_error_count_increment(num_errors in 1usize..10) {
let ctx = TestContext::new().expect("Failed to create test context");
// 创建测试 Provider
let provider_id = format!("test-provider-error-{}", uuid::Uuid::new_v4());
ctx.create_test_provider(&provider_id).expect("Failed to create provider");
// 添加 API Key
let api_key = format!("sk-error-test-{}", provider_id);
let entry = ctx.add_test_api_key(&provider_id, &api_key)
.expect("Failed to add API key");
// 初始错误次数应为 0
prop_assert_eq!(entry.error_count, 0, "Initial error count should be 0");
// 记录 N 次错误
for _ in 0..num_errors {
ctx.service.record_error(&ctx.db, &entry.id)
.expect("Failed to record error");
}
// 获取更新后的 API Key
let conn = ctx.db.lock().expect("Failed to lock db");
let updated = ApiKeyProviderDao::get_api_key_by_id(&conn, &entry.id)
.expect("Failed to get API key")
.expect("API key not found");
// 验证:错误次数应等于记录次数
prop_assert_eq!(
updated.error_count as usize,
num_errors,
"Error count should equal number of record_error calls"
);
}
/// Property 13 补充测试:使用和错误统计独立
///
/// *对于任意* API Key,使用次数和错误次数应独立递增
///
/// **Feature: provider-ui-refactor, Property 13: API Key 使用统计正确性**
/// **Validates: Requirements 7.4**
#[test]
fn test_usage_and_error_independent(
num_usages in 1usize..5,
num_errors in 1usize..5
) {
let ctx = TestContext::new().expect("Failed to create test context");
// 创建测试 Provider
let provider_id = format!("test-provider-mixed-{}", uuid::Uuid::new_v4());
ctx.create_test_provider(&provider_id).expect("Failed to create provider");
// 添加 API Key
let api_key = format!("sk-mixed-test-{}", provider_id);
let entry = ctx.add_test_api_key(&provider_id, &api_key)
.expect("Failed to add API key");
// 交替记录使用和错误
for i in 0..(num_usages + num_errors) {
if i < num_usages {
ctx.service.record_usage(&ctx.db, &entry.id)
.expect("Failed to record usage");
}
if i < num_errors {
ctx.service.record_error(&ctx.db, &entry.id)
.expect("Failed to record error");
}
}
// 获取更新后的 API Key
let conn = ctx.db.lock().expect("Failed to lock db");
let updated = ApiKeyProviderDao::get_api_key_by_id(&conn, &entry.id)
.expect("Failed to get API key")
.expect("API key not found");
// 验证:使用次数和错误次数应独立
prop_assert_eq!(
updated.usage_count as usize,
num_usages,
"Usage count should equal number of record_usage calls"
);
prop_assert_eq!(
updated.error_count as usize,
num_errors,
"Error count should equal number of record_error calls"
);
}
}
// ============================================================================
// Property 16: 数据持久化 Round-Trip
// **Validates: Requirements 9.1**
// ============================================================================
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
/// Property 16: 数据持久化 Round-Trip
///
/// *对于任意* Provider 配置,保存后重新加载应得到等价的配置数据
///
/// **Feature: provider-ui-refactor, Property 16: 数据持久化 Round-Trip**
/// **Validates: Requirements 9.1**
#[test]
fn test_provider_persistence_round_trip(
name in "[a-zA-Z0-9 ]{3,30}",
api_host in "https://[a-z]{3,10}\\.[a-z]{2,5}/[a-z]{0,10}"
) {
let ctx = TestContext::new().expect("Failed to create test context");
// 创建 Provider
let provider = ctx.service
.add_custom_provider(
&ctx.db,
name.clone(),
ApiProviderType::Openai,
api_host.clone(),
None,
None,
None,
None,
)
.expect("Failed to create provider");
// 重新加载 Provider
let loaded = ctx.service
.get_provider(&ctx.db, &provider.id)
.expect("Failed to get provider")
.expect("Provider not found");
// 验证:加载的数据应与保存的数据等价
prop_assert_eq!(&loaded.provider.id, &provider.id, "ID should match");
prop_assert_eq!(&loaded.provider.name, &name, "Name should match");
prop_assert_eq!(&loaded.provider.api_host, &api_host, "API host should match");
prop_assert_eq!(loaded.provider.is_system, false, "Should not be system provider");
prop_assert_eq!(loaded.provider.group, ProviderGroup::Custom, "Group should be Custom");
}
/// Property 16 补充测试:UI 状态持久化 Round-Trip
///
/// *对于任意* UI 状态键值对,保存后重新加载应得到相同的值
///
/// **Feature: provider-ui-refactor, Property 16: 数据持久化 Round-Trip**
/// **Validates: Requirements 9.1, 8.4**
#[test]
fn test_ui_state_persistence_round_trip(
key in "[a-z_]{3,20}",
value in "[a-zA-Z0-9_,\\[\\]\"{}:]{1,100}"
) {
let ctx = TestContext::new().expect("Failed to create test context");
// 保存 UI 状态
ctx.service
.set_ui_state(&ctx.db, &key, &value)
.expect("Failed to set UI state");
// 重新加载 UI 状态
let loaded = ctx.service
.get_ui_state(&ctx.db, &key)
.expect("Failed to get UI state")
.expect("UI state not found");
// 验证:加载的值应与保存的值相同
prop_assert_eq!(&loaded, &value, "UI state value should match");
}
/// Property 16 补充测试:Provider 排序持久化 Round-Trip
///
/// *对于任意* Provider 排序顺序,保存后重新加载应保持相同的顺序
///
/// **Feature: provider-ui-refactor, Property 16: 数据持久化 Round-Trip**
/// **Validates: Requirements 9.1, 8.4**
#[test]
fn test_provider_sort_order_persistence(num_providers in 2usize..6) {
let ctx = TestContext::new().expect("Failed to create test context");
// 创建多个 Provider
let mut provider_ids = Vec::new();
for i in 0..num_providers {
let provider = ctx.service
.add_custom_provider(
&ctx.db,
format!("Provider {}", i),
ApiProviderType::Openai,
format!("https://api{}.test.com", i),
None,
None,
None,
None,
)
.expect("Failed to create provider");
provider_ids.push(provider.id);
}
// 反转排序顺序
let reversed_ids: Vec<_> = provider_ids.iter().rev().cloned().collect();
let sort_orders: Vec<(String, i32)> = reversed_ids
.iter()
.enumerate()
.map(|(i, id)| (id.clone(), i as i32))
.collect();
// 更新排序顺序
ctx.service
.update_provider_sort_orders(&ctx.db, sort_orders)
.expect("Failed to update sort orders");
// 重新加载所有 Provider
let loaded = ctx.service
.get_all_providers(&ctx.db)
.expect("Failed to get providers");
// 过滤出我们创建的 Provider
let our_providers: Vec<_> = loaded
.iter()
.filter(|p| provider_ids.contains(&p.provider.id))
.collect();
// 验证:排序顺序应与更新后的顺序一致
for (i, expected_id) in reversed_ids.iter().enumerate() {
let provider = our_providers
.iter()
.find(|p| &p.provider.id == expected_id)
.expect("Provider not found");
prop_assert_eq!(
provider.provider.sort_order,
i as i32,
"Sort order should match for provider {}",
expected_id
);
}
}
/// Property 16 补充测试:API Key 持久化 Round-Trip
///
/// *对于任意* API Key,保存后重新加载应得到等价的数据(除了加密的 key)
///
/// **Feature: provider-ui-refactor, Property 16: 数据持久化 Round-Trip**
/// **Validates: Requirements 9.1**
#[test]
fn test_api_key_persistence_round_trip(
api_key in "[a-zA-Z0-9_-]{20,50}",
alias in proptest::option::of("[a-zA-Z0-9 ]{3,20}")
) {
let ctx = TestContext::new().expect("Failed to create test context");
// 创建 Provider
let provider_id = format!("test-provider-key-rt-{}", uuid::Uuid::new_v4());
ctx.create_test_provider(&provider_id).expect("Failed to create provider");
// 添加 API Key
let entry = ctx.service
.add_api_key(&ctx.db, &provider_id, &api_key, alias.clone())
.expect("Failed to add API key");
// 重新加载 Provider(包含 API Keys)
let loaded = ctx.service
.get_provider(&ctx.db, &provider_id)
.expect("Failed to get provider")
.expect("Provider not found");
// 找到我们添加的 API Key
let loaded_key = loaded.api_keys
.iter()
.find(|k| k.id == entry.id)
.expect("API Key not found");
// 验证:加载的数据应与保存的数据等价
prop_assert_eq!(&loaded_key.id, &entry.id, "ID should match");
prop_assert_eq!(&loaded_key.provider_id, &provider_id, "Provider ID should match");
prop_assert_eq!(&loaded_key.alias, &alias, "Alias should match");
prop_assert_eq!(loaded_key.enabled, true, "Should be enabled by default");
prop_assert_eq!(loaded_key.usage_count, 0, "Usage count should be 0");
prop_assert_eq!(loaded_key.error_count, 0, "Error count should be 0");
// 验证:解密后的 API Key 应与原始值相同
let decrypted = ctx.service
.decrypt_api_key(&loaded_key.api_key_encrypted)
.expect("Failed to decrypt");
prop_assert_eq!(&decrypted, &api_key, "Decrypted API key should match original");
}
}
// ============================================================================
// Property 17: API Key 加密存储
// **Validates: Requirements 9.2**
// ============================================================================
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
/// Property 17: API Key 加密存储
///
/// *对于任意* 存储的 API Key,数据库中的值不应为明文
///
/// **Feature: provider-ui-refactor, Property 17: API Key 加密存储**
/// **Validates: Requirements 9.2**
#[test]
fn test_api_key_encryption(api_key in "[a-zA-Z0-9_-]{20,50}") {
let ctx = TestContext::new().expect("Failed to create test context");
// 创建测试 Provider
let provider_id = format!("test-provider-enc-{}", uuid::Uuid::new_v4());
ctx.create_test_provider(&provider_id).expect("Failed to create provider");
// 添加 API Key
let entry = ctx.add_test_api_key(&provider_id, &api_key)
.expect("Failed to add API key");
// 验证:存储的值不是明文
prop_assert_ne!(
&entry.api_key_encrypted,
&api_key,
"API Key should be encrypted, not stored as plaintext"
);
// 验证:加密后的值看起来像 Base64
prop_assert!(
entry.api_key_encrypted.chars().all(|c| c.is_alphanumeric() || c == '+' || c == '/' || c == '='),
"Encrypted value should be Base64 encoded"
);
// 验证:可以正确解密
let decrypted = ctx.service.decrypt_api_key(&entry.api_key_encrypted)
.expect("Failed to decrypt API key");
prop_assert_eq!(
&decrypted,
&api_key,
"Decrypted key should match original"
);
}
/// Property 17 补充测试:加密 Round-Trip
///
/// *对于任意* API Key,加密后解密应得到原始值
///
/// **Feature: provider-ui-refactor, Property 17: API Key 加密存储**
/// **Validates: Requirements 9.2**
#[test]
fn test_encryption_round_trip(api_key in "[a-zA-Z0-9_-]{10,100}") {
let service = ApiKeyProviderService::new();
// 加密
let encrypted = service.encrypt_api_key(&api_key);
// 验证:加密后不等于原文
prop_assert_ne!(
&encrypted,
&api_key,
"Encrypted value should differ from original"
);
// 解密
let decrypted = service.decrypt_api_key(&encrypted)
.expect("Failed to decrypt");
// 验证:解密后等于原文
prop_assert_eq!(
&decrypted,
&api_key,
"Decrypted value should match original"
);
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
/// 单元测试:基本的 Provider CRUD 操作
#[test]
fn test_provider_crud() {
let ctx = TestContext::new().expect("Failed to create test context");
// 创建 Provider
let provider = ctx
.service
.add_custom_provider(
&ctx.db,
"Test Provider".to_string(),
ApiProviderType::Openai,
"https://api.test.com".to_string(),
None,
None,
None,
None,
)
.expect("Failed to create provider");
assert!(provider.id.starts_with("custom-"));
assert_eq!(provider.name, "Test Provider");
assert!(!provider.is_system);
// 获取 Provider
let retrieved = ctx
.service
.get_provider(&ctx.db, &provider.id)
.expect("Failed to get provider")
.expect("Provider not found");
assert_eq!(retrieved.provider.id, provider.id);
// 更新 Provider
let updated = ctx
.service
.update_provider(
&ctx.db,
&provider.id,
Some("Updated Name".to_string()),
None,
Some(false),
None,
None,
None,
None,
None,
)
.expect("Failed to update provider");
assert_eq!(updated.name, "Updated Name");
assert!(!updated.enabled);
// 删除 Provider
let deleted = ctx
.service
.delete_custom_provider(&ctx.db, &provider.id)
.expect("Failed to delete provider");
assert!(deleted);
// 验证已删除
let not_found = ctx
.service
.get_provider(&ctx.db, &provider.id)
.expect("Failed to get provider");
assert!(not_found.is_none());
}
/// 单元测试:API Key CRUD 操作
#[test]
fn test_api_key_crud() {
let ctx = TestContext::new().expect("Failed to create test context");
// 创建 Provider
let provider_id = "test-provider-key-crud";
ctx.create_test_provider(provider_id)
.expect("Failed to create provider");
// 添加 API Key
let key = ctx
.add_test_api_key(provider_id, "sk-test-key-123")
.expect("Failed to add API key");
assert!(!key.id.is_empty());
assert_eq!(key.provider_id, provider_id);
assert!(key.enabled);
// 切换启用状态
let toggled = ctx
.service
.toggle_api_key(&ctx.db, &key.id, false)
.expect("Failed to toggle API key");
assert!(!toggled.enabled);
// 更新别名
let aliased = ctx
.service
.update_api_key_alias(&ctx.db, &key.id, Some("My Key".to_string()))
.expect("Failed to update alias");
assert_eq!(aliased.alias, Some("My Key".to_string()));
// 删除 API Key
let deleted = ctx
.service
.delete_api_key(&ctx.db, &key.id)
.expect("Failed to delete API key");
assert!(deleted);
}
/// 单元测试:系统 Provider 不能删除
#[test]
fn test_system_provider_cannot_be_deleted() {
let ctx = TestContext::new().expect("Failed to create test context");
// 创建系统 Provider
let now = chrono::Utc::now();
let provider = ApiKeyProvider {
id: "system-openai".to_string(),
name: "OpenAI".to_string(),
provider_type: ApiProviderType::Openai,
api_host: "https://api.openai.com".to_string(),
is_system: true, // 系统 Provider
group: ProviderGroup::Mainstream,
enabled: true,
sort_order: 1,
api_version: None,
project: None,
location: None,
region: None,
created_at: now,
updated_at: now,
};
{
let conn = ctx.db.lock().expect("Failed to lock db");
ApiKeyProviderDao::insert_provider(&conn, &provider).expect("Failed to insert");
}
// 尝试删除系统 Provider
let result = ctx.service.delete_custom_provider(&ctx.db, "system-openai");
assert!(result.is_err());
assert!(result.unwrap_err().contains("不允许删除系统 Provider"));
}
}