feat: add Apple notarization for macOS builds (v0.47.2)

This commit is contained in:
coso
2026-01-16 00:08:19 +08:00
parent c9f1ba44a6
commit f7cf0aff4c
8 changed files with 276 additions and 12 deletions
+4
View File
@@ -113,6 +113,10 @@ jobs:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
# Apple 公证环境变量(解决 Gatekeeper 警告)
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
with:
tauriScript: npx tauri
tagName: ${{ github.event.inputs.version || github.ref_name }}
+1
View File
@@ -4,6 +4,7 @@ node_modules/
# Build
dist/
src-tauri/target/
src-tauri/.cargo/
*.exe
*.pdb
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "proxycast",
"private": true,
"version": "0.47.1",
"version": "0.47.2",
"type": "module",
"repository": {
"type": "git",
+1 -2
View File
@@ -181,7 +181,6 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "aster"
version = "0.3.0"
source = "git+https://github.com/astercloud/aster-rust?branch=main#2487b9349d12c86fbe6ee61d5a9396769978df68"
dependencies = [
"ahash",
"anyhow",
@@ -6060,7 +6059,7 @@ dependencies = [
[[package]]
name = "proxycast"
version = "0.47.1"
version = "0.47.2"
dependencies = [
"anyhow",
"arboard",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "proxycast"
version = "0.47.1"
version = "0.47.2"
description = "AI API Proxy Desktop App"
authors = ["you"]
edition = "2021"
+24 -1
View File
@@ -9,7 +9,7 @@ use crate::database::dao::api_key_provider::{
ApiKeyEntry, ApiKeyProvider, ApiProviderType, ProviderWithKeys,
};
use crate::database::DbConnection;
use crate::services::api_key_provider_service::{ApiKeyProviderService, ImportResult};
use crate::services::api_key_provider_service::{ApiKeyProviderService, ConnectionTestResult, ImportResult};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tauri::State;
@@ -584,3 +584,26 @@ pub fn delete_legacy_api_key_credential(
) -> Result<bool, String> {
pool_service.0.delete_credential(&db, &uuid)
}
// ============================================================================
// 连接测试命令
// ============================================================================
/// 测试 API Key Provider 连接
///
/// 方案 C 实现:
/// 1. 默认使用 /v1/models 端点测试
/// 2. 如果提供了 model_name,用该模型发送简单请求
///
/// # 参数
/// - `provider_id`: Provider ID
/// - `model_name`: 可选的模型名称,用于发送测试请求
#[tauri::command]
pub async fn test_api_key_provider_connection(
db: State<'_, DbConnection>,
service: State<'_, ApiKeyProviderServiceState>,
provider_id: String,
model_name: Option<String>,
) -> Result<ConnectionTestResult, String> {
service.0.test_connection(&db, &provider_id, model_name).await
}
@@ -1169,6 +1169,249 @@ impl ApiKeyProviderService {
proxy_url: None,
})
}
// ==================== 连接测试 ====================
/// 测试 Provider 连接
///
/// 方案 C 实现:
/// 1. 默认使用 /v1/models 端点测试
/// 2. 如果 Provider 配置了自定义模型列表,用第一个模型发送简单请求
///
/// # 参数
/// - `db`: 数据库连接
/// - `provider_id`: Provider ID
/// - `model_name`: 可选的模型名称,用于发送测试请求
///
/// # 返回
/// - `ConnectionTestResult`: 测试结果
pub async fn test_connection(
&self,
db: &DbConnection,
provider_id: &str,
model_name: Option<String>,
) -> Result<ConnectionTestResult, String> {
use std::time::Instant;
// 获取 Provider 信息
let provider_with_keys = self
.get_provider(db, provider_id)?
.ok_or_else(|| format!("Provider not found: {}", provider_id))?;
let provider = &provider_with_keys.provider;
// 获取一个可用的 API Key
let api_key = self
.get_next_api_key(db, provider_id)?
.ok_or_else(|| "没有可用的 API Key".to_string())?;
let start_time = Instant::now();
// 根据 Provider 类型选择测试方式
let result = match provider.provider_type {
ApiProviderType::Anthropic => {
// Anthropic 不支持 /models 端点,需要发送测试请求
let test_model = model_name
.or_else(|| provider.custom_models.first().cloned())
.unwrap_or_else(|| "claude-3-haiku-20240307".to_string());
self.test_anthropic_connection(&api_key, &provider.api_host, &test_model)
.await
}
ApiProviderType::Gemini => {
// Gemini 使用 /models 端点
self.test_gemini_connection(&api_key, &provider.api_host)
.await
}
_ => {
// OpenAI 兼容类型,优先使用 /models 端点
let models_result = self
.test_openai_models_endpoint(&api_key, &provider.api_host)
.await;
// 如果 /models 端点失败且有模型名称,尝试发送测试请求
if models_result.is_err() && model_name.is_some() {
let test_model = model_name.unwrap();
self.test_openai_chat_completion(&api_key, &provider.api_host, &test_model)
.await
} else {
models_result
}
}
};
let latency_ms = start_time.elapsed().as_millis() as u64;
match result {
Ok(models) => Ok(ConnectionTestResult {
success: true,
latency_ms: Some(latency_ms),
error: None,
models: Some(models),
}),
Err(e) => Ok(ConnectionTestResult {
success: false,
latency_ms: Some(latency_ms),
error: Some(e),
models: None,
}),
}
}
/// 测试 OpenAI 兼容的 /models 端点
async fn test_openai_models_endpoint(
&self,
api_key: &str,
api_host: &str,
) -> Result<Vec<String>, String> {
use crate::providers::openai_custom::OpenAICustomProvider;
let provider = OpenAICustomProvider::with_config(api_key.to_string(), Some(api_host.to_string()));
let response = provider
.list_models()
.await
.map_err(|e| format!("获取模型列表失败: {}", e))?;
// 解析模型列表
let models = response["data"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|m| m["id"].as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
if models.is_empty() {
Err("未获取到任何模型".to_string())
} else {
Ok(models)
}
}
/// 测试 OpenAI 兼容的 chat/completions 端点
async fn test_openai_chat_completion(
&self,
api_key: &str,
api_host: &str,
model: &str,
) -> Result<Vec<String>, String> {
use crate::models::openai::{ChatCompletionRequest, Message, MessageContent};
use crate::providers::openai_custom::OpenAICustomProvider;
let provider = OpenAICustomProvider::with_config(api_key.to_string(), Some(api_host.to_string()));
let request = ChatCompletionRequest {
model: model.to_string(),
messages: vec![Message {
role: "user".to_string(),
content: Some(MessageContent::Text("hi".to_string())),
name: None,
tool_calls: None,
tool_call_id: None,
}],
max_tokens: Some(1),
stream: false,
..Default::default()
};
let response = provider
.call_api(&request)
.await
.map_err(|e| format!("API 调用失败: {}", e))?;
if response.status().is_success() {
Ok(vec![model.to_string()])
} else {
let status = response.status();
let body = response.text().await.unwrap_or_default();
Err(format!("API 返回错误: {} - {}", status, body))
}
}
/// 测试 Anthropic 连接
async fn test_anthropic_connection(
&self,
api_key: &str,
api_host: &str,
model: &str,
) -> Result<Vec<String>, String> {
use crate::providers::claude_custom::ClaudeCustomProvider;
let provider = ClaudeCustomProvider::with_config(api_key.to_string(), Some(api_host.to_string()));
// 发送一个简单的测试请求
let request = serde_json::json!({
"model": model,
"max_tokens": 1,
"messages": [{"role": "user", "content": "hi"}]
});
let response = provider
.messages(&request)
.await
.map_err(|e| format!("API 调用失败: {}", e))?;
if response.status().is_success() {
Ok(vec![model.to_string()])
} else {
let status = response.status();
let body = response.text().await.unwrap_or_default();
Err(format!("API 返回错误: {} - {}", status, body))
}
}
/// 测试 Gemini 连接
async fn test_gemini_connection(
&self,
api_key: &str,
api_host: &str,
) -> Result<Vec<String>, String> {
use reqwest::Client;
use std::time::Duration;
let client = Client::builder()
.connect_timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(30))
.build()
.map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?;
// Gemini API 的模型列表端点
let base = api_host.trim_end_matches('/');
let url = format!("{}/v1beta/models?key={}", base, api_key);
let response = client
.get(&url)
.send()
.await
.map_err(|e| format!("请求失败: {}", e))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(format!("API 返回错误: {} - {}", status, body));
}
let data: serde_json::Value = response
.json()
.await
.map_err(|e| format!("解析响应失败: {}", e))?;
let models = data["models"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|m| m["name"].as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
if models.is_empty() {
Err("未获取到任何模型".to_string())
} else {
Ok(models)
}
}
}
/// 导入结果
@@ -1180,9 +1423,3 @@ pub struct ImportResult {
pub skipped_providers: usize,
pub errors: Vec<String>,
}
use serde::{Deserialize, Serialize};
use crate::models::provider_pool_model::{
CredentialData, CredentialSource, PoolProviderType, ProviderCredential,
};
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "ProxyCast",
"version": "0.47.1",
"version": "0.47.2",
"identifier": "com.proxycast.app",
"build": {
"beforeDevCommand": "npm run dev",