From 9a71162c0cdc9c67535e8d4490b1d76a855dda99 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 21 May 2026 23:16:30 +0800 Subject: [PATCH 1/5] update --- .github/workflows/release.yml | 82 +---------------------------- src-tauri/src/ai_translator.rs | 96 ++++++++++++++++++++++++++-------- src-tauri/src/store.rs | 9 +++- src-tauri/tauri.conf.json | 2 +- src/pages/Settings.jsx | 73 +++++++++++++++++++++----- 5 files changed, 145 insertions(+), 117 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef7327d..850459b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,21 +9,7 @@ jobs: publish-tauri: permissions: contents: write - strategy: - fail-fast: false - matrix: - include: - - platform: 'macos-latest' - args: '--target aarch64-apple-darwin' - name: 'macOS ARM64' - - platform: 'macos-latest' - args: '--target x86_64-apple-darwin' - name: 'macOS Intel' - - platform: 'windows-latest' - args: '' - name: 'Windows' - - runs-on: ${{ matrix.platform }} + runs-on: 'windows-latest' steps: - uses: actions/checkout@v4 @@ -35,85 +21,21 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable - with: - targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} - name: Install dependencies run: npm install - - name: Import Apple Developer Certificate - if: matrix.platform == 'macos-latest' - env: - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} - run: | - echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12 - security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain - security default-keychain -s build.keychain - security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain - security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain - security find-identity -v -p codesigning build.keychain - - - name: Verify Certificate - if: matrix.platform == 'macos-latest' - run: | - CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application") - CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}') - echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV - echo "Certificate imported." - - name: Build and Release uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }} with: tagName: v__VERSION__ releaseName: 'DeepRant v__VERSION__' releaseBody: | - 请查看附件下载对应系统的安装包: + 请查看附件下载 Windows 安装包: - Windows: .msi 安装包 - - macOS Intel: .dmg (x64) 安装包 - - macOS Apple Silicon: .dmg (arm64) 安装包 releaseDraft: true prerelease: false - args: ${{ matrix.args }} - - - name: Notarize DMG (macOS) - if: matrix.platform == 'macos-latest' - env: - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - run: | - if [[ "${{ matrix.args }}" == *"aarch64"* ]]; then - xcrun notarytool submit "src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/DeepRant_"*"_aarch64.dmg" \ - --apple-id "$APPLE_ID" \ - --password "$APPLE_ID_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" \ - --wait - else - xcrun notarytool submit "src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/DeepRant_"*"_x64.dmg" \ - --apple-id "$APPLE_ID" \ - --password "$APPLE_ID_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" \ - --wait - fi - - - name: Staple DMG (macOS) - if: matrix.platform == 'macos-latest' - run: | - if [[ "${{ matrix.args }}" == *"aarch64"* ]]; then - xcrun stapler staple "src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/DeepRant_"*"_aarch64.dmg" - else - xcrun stapler staple "src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/DeepRant_"*"_x64.dmg" - fi diff --git a/src-tauri/src/ai_translator.rs b/src-tauri/src/ai_translator.rs index fa08723..f30b4ab 100644 --- a/src-tauri/src/ai_translator.rs +++ b/src-tauri/src/ai_translator.rs @@ -170,16 +170,19 @@ fn get_model_config(settings: &crate::store::AppSettings) -> crate::store::Model auth: "sk-jleighwqdtyssxeycgmwxqrhbofpsbkhtobofxhbeyebupyh".to_string(), api_url: "https://api.siliconflow.cn/v1/chat/completions".to_string(), model_name: "deepseek-ai/DeepSeek-V3".to_string(), + api_type: "openai".to_string(), }, "deepseek-R1" => crate::store::ModelConfig { auth: "sk-jleighwqdtyssxeycgmwxqrhbofpsbkhtobofxhbeyebupyh".to_string(), api_url: "https://api.siliconflow.cn/v1/chat/completions".to_string(), model_name: "deepseek-ai/DeepSeek-R1".to_string(), + api_type: "openai".to_string(), }, "stepfun" => crate::store::ModelConfig { auth: "605JU1zU7cGmFp0ibbZlZZ3Qra3lRH7FDtpvICyf2pTrRrUaO6CQgW8p3sQatd5Wh".to_string(), api_url: "https://api.stepfun.com/v1/chat/completions".to_string(), model_name: "step-2-16k".to_string(), + api_type: "openai".to_string(), }, "custom" => settings.custom_model.clone(), _ => settings.custom_model.clone(), @@ -212,7 +215,23 @@ pub async fn translate_with_gpt(app: &AppHandle, original: &str) -> Result Result Result { - let text = text.trim(); - // 如果找到标签,只保留其后内容 - if let Some(end_pos) = text.find("") { - text[(end_pos + 8)..].trim().to_string() - } else { - text.to_string() + let translated = if api_type == "anthropic" { + match response + .get("content") + .and_then(|content| content.as_array()) + .and_then(|content| content.first()) + .and_then(|item| item.get("text")) + .and_then(|text| text.as_str()) + { + Some(text) => { + let text = text.trim(); + // 如果找到标签,只保留其后内容 + if let Some(end_pos) = text.find("") { + text[(end_pos + 8)..].trim().to_string() + } else { + text.to_string() + } + } + None => { + println!("无法从Anthropic响应中提取翻译结果: {:?}", response); + return Ok("[错误] 服务器返回的数据格式异常".to_string()); } } - None => { - println!("无法从响应中提取翻译结果: {:?}", response); - return Ok("[错误] 服务器返回的数据格式异常".to_string()); + } else { + match response + .get("choices") + .and_then(|choices| choices.as_array()) + .and_then(|choices| choices.first()) + .and_then(|choice| choice.get("message")) + .and_then(|message| message.get("content")) + .and_then(|content| content.as_str()) + { + Some(text) => { + let text = text.trim(); + // 如果找到标签,只保留其后内容 + if let Some(end_pos) = text.find("") { + text[(end_pos + 8)..].trim().to_string() + } else { + text.to_string() + } + } + None => { + println!("无法从响应中提取翻译结果: {:?}", response); + return Ok("[错误] 服务器返回的数据格式异常".to_string()); + } } }; diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index 69ea4ea..26cc85c 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -10,6 +10,12 @@ pub struct ModelConfig { pub auth: String, pub api_url: String, pub model_name: String, + #[serde(default = "default_api_type")] + pub api_type: String, +} + +fn default_api_type() -> String { + "openai".to_string() } // 添加常用语结构体 @@ -103,7 +109,8 @@ pub fn initialize_settings(app: &AppHandle) -> Result<(), anyhow::Error> { "custom_model": { "auth": "", "api_url": "https://api.openai.com/v1/chat/completions", - "model_name": "gpt-3.5-turbo" + "model_name": "gpt-3.5-turbo", + "api_type": "openai" }, "phrases": phrases }); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index aee161d..1ec0ed2 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -74,7 +74,7 @@ }, "plugins": { "updater": { - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDEyRUQyRTlGMEZGRjZCNUQKUldSZGEvOFBueTd0RXB1eStiY2ovR1c0M2lIK0JzTXR0TW9nTjR1QXNsbU9VOGNndG12VHg5L3UK", + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDc3NjE5NEI5NUUwRDk4NEEKUldSS21BMWV1WlJoZDFZTEk0QldhNFNGWkwvVEZBazVxRERmWVAwekNZdC9GdHJzd2IyQmxWOUQK", "endpoints": [ "https://github.com/liseami/DeepRant/releases/latest/download/latest.json" ], diff --git a/src/pages/Settings.jsx b/src/pages/Settings.jsx index b6bae98..d674785 100644 --- a/src/pages/Settings.jsx +++ b/src/pages/Settings.jsx @@ -5,17 +5,17 @@ import { useStore } from '../components/StoreProvider'; import { showSuccess, showError } from '../utils/toast'; // 添加测试函数 -const testOpenAIConnection = async (apiKey, baseUrl, modelName) => { +const testOpenAIConnection = async (apiKey, baseUrl, modelName, apiType = 'openai') => { try { - const headers = { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}` - }; + let headers, body; - const response = await fetch(`${baseUrl}`, { - method: 'POST', - headers: headers, - body: JSON.stringify({ + if (apiType === 'anthropic') { + headers = { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01' + }; + body = JSON.stringify({ model: modelName, messages: [ { @@ -24,7 +24,29 @@ const testOpenAIConnection = async (apiKey, baseUrl, modelName) => { } ], max_tokens: 10 - }) + }); + } else { + // openai 和 opencode-go 都使用 OpenAI 兼容格式 + headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }; + body = JSON.stringify({ + model: modelName, + messages: [ + { + role: "user", + content: "Hello, this is a test message. Please reply with 'OK' if you receive this." + } + ], + max_tokens: 10 + }); + } + + const response = await fetch(`${baseUrl}`, { + method: 'POST', + headers: headers, + body: body }); const data = await response.json(); @@ -33,8 +55,14 @@ const testOpenAIConnection = async (apiKey, baseUrl, modelName) => { throw new Error(data.error.message || '未知错误'); } - if (data.choices && data.choices[0] && data.choices[0].message) { - return true; + if (apiType === 'anthropic') { + if (data.content && data.content[0] && data.content[0].text) { + return true; + } + } else { + if (data.choices && data.choices[0] && data.choices[0].message) { + return true; + } } throw new Error('响应格式不正确'); @@ -148,6 +176,24 @@ export default function Settings() { 自定义API配置
+
+ + +
Date: Thu, 21 May 2026 23:21:23 +0800 Subject: [PATCH 2/5] 1.2.0 --- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3c3ffd9..dd2693a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "DeepRant" -version = "1.1.9" +version = "1.2.0" description = "A Tauri App" authors = ["you"] license = "" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 1ec0ed2..92e8be3 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "DeepRant", - "version": "1.1.9", + "version": "1.2.0", "identifier": "com.DeepRant.app", "build": { "beforeDevCommand": "npm run dev", From 1d071ef257fe6290066ac82884d6ab2827cb4554 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 21 May 2026 23:39:52 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E8=87=AA=E5=AE=9A=E4=B9=89API?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=8F=AA=E5=A1=ABbase=20URL=EF=BC=8C?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E6=A0=B9=E6=8D=AEapi=5Ftype=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E8=A1=A5=E5=85=A8=20(v1.2.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.toml | 2 +- src-tauri/src/ai_translator.rs | 31 ++++++++++++++++++++++++++----- src-tauri/src/store.rs | 2 +- src-tauri/tauri.conf.json | 2 +- src/pages/Settings.jsx | 15 +++++++++++++-- 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index dd2693a..e489c33 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "DeepRant" -version = "1.2.0" +version = "1.2.1" description = "A Tauri App" authors = ["you"] license = "" diff --git a/src-tauri/src/ai_translator.rs b/src-tauri/src/ai_translator.rs index f30b4ab..6ad71e9 100644 --- a/src-tauri/src/ai_translator.rs +++ b/src-tauri/src/ai_translator.rs @@ -189,6 +189,26 @@ fn get_model_config(settings: &crate::store::AppSettings) -> crate::store::Model } } +/// 根据 API 类型自动补全完整请求 URL。 +/// 用户只需填写 base URL(如 https://hk.routeai.cc),路径自动追加。 +/// 如果用户已经填写了完整 URL,则不重复补全。 +fn resolve_api_url(base_url: &str, api_type: &str) -> String { + let trimmed = base_url.trim().trim_end_matches('/'); + + // 已包含路径关键字时直接使用,向后兼容 + if trimmed.contains("/chat/completions") || trimmed.ends_with("/messages") { + return trimmed.to_string(); + } + + let suffix = match api_type.to_lowercase().as_str() { + "anthropic" => "/v1/messages", + // openai 与 opencode-go 都使用 OpenAI 兼容路径 + _ => "/v1/chat/completions", + }; + + format!("{}{}", trimmed, suffix) +} + pub async fn translate_with_gpt(app: &AppHandle, original: &str) -> Result { let settings = crate::store::get_settings(app)?; println!("当前翻译设置:"); @@ -201,9 +221,12 @@ pub async fn translate_with_gpt(app: &AppHandle, original: &str) -> Result Result Result Result<(), anyhow::Error> { "model_type": "deepseek", "custom_model": { "auth": "", - "api_url": "https://api.openai.com/v1/chat/completions", + "api_url": "https://api.openai.com", "model_name": "gpt-3.5-turbo", "api_type": "openai" }, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 92e8be3..7e07032 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "DeepRant", - "version": "1.2.0", + "version": "1.2.1", "identifier": "com.DeepRant.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/pages/Settings.jsx b/src/pages/Settings.jsx index d674785..cb866ed 100644 --- a/src/pages/Settings.jsx +++ b/src/pages/Settings.jsx @@ -4,10 +4,21 @@ import { useState, useEffect } from 'react'; import { useStore } from '../components/StoreProvider'; import { showSuccess, showError } from '../utils/toast'; +// 根据 API 类型自动补全完整请求 URL +const resolveApiUrl = (baseUrl, apiType) => { + const trimmed = (baseUrl || '').trim().replace(/\/+$/, ''); + if (trimmed.includes('/chat/completions') || trimmed.endsWith('/messages')) { + return trimmed; + } + const suffix = apiType === 'anthropic' ? '/v1/messages' : '/v1/chat/completions'; + return `${trimmed}${suffix}`; +}; + // 添加测试函数 const testOpenAIConnection = async (apiKey, baseUrl, modelName, apiType = 'openai') => { try { let headers, body; + const fullUrl = resolveApiUrl(baseUrl, apiType); if (apiType === 'anthropic') { headers = { @@ -43,7 +54,7 @@ const testOpenAIConnection = async (apiKey, baseUrl, modelName, apiType = 'opena }); } - const response = await fetch(`${baseUrl}`, { + const response = await fetch(fullUrl, { method: 'POST', headers: headers, body: body @@ -223,7 +234,7 @@ export default function Settings() { } })} className="w-full px-4 py-2.5 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg text-sm text-zinc-700 dark:text-zinc-300 disabled:cursor-not-allowed disabled:opacity-50" - placeholder="例如:https://api.openai.com/v1/chat/completions" + placeholder="例如:https://hk.routeai.cc (路径将根据API类型自动补全)" />
From 5efb4dbeeb90430e3cfb52e232c526265ca0df2b Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 21 May 2026 23:46:24 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=E6=B5=8B=E8=AF=95=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E5=B1=95=E7=A4=BA=E8=AF=A6=E7=BB=86=E8=AF=8A=E6=96=AD?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=EF=BC=88URL/=E7=8A=B6=E6=80=81=E7=A0=81/?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E4=BD=93/=E9=94=99=E8=AF=AF=E6=A0=88?= =?UTF-8?q?=EF=BC=89(v1.2.2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- src/pages/Settings.jsx | 205 +++++++++++++++++++++++++++----------- 3 files changed, 149 insertions(+), 60 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e489c33..aa2fa67 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "DeepRant" -version = "1.2.1" +version = "1.2.2" description = "A Tauri App" authors = ["you"] license = "" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7e07032..6418fef 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "DeepRant", - "version": "1.2.1", + "version": "1.2.2", "identifier": "com.DeepRant.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/pages/Settings.jsx b/src/pages/Settings.jsx index cb866ed..1332eb3 100644 --- a/src/pages/Settings.jsx +++ b/src/pages/Settings.jsx @@ -14,72 +14,113 @@ const resolveApiUrl = (baseUrl, apiType) => { return `${trimmed}${suffix}`; }; -// 添加测试函数 +// 添加测试函数。失败时抛出的 Error 会带 `.details` 字段,包含完整诊断信息。 const testOpenAIConnection = async (apiKey, baseUrl, modelName, apiType = 'openai') => { - try { - let headers, body; - const fullUrl = resolveApiUrl(baseUrl, apiType); + const fullUrl = resolveApiUrl(baseUrl, apiType); + const startedAt = Date.now(); - if (apiType === 'anthropic') { - headers = { - 'Content-Type': 'application/json', - 'x-api-key': apiKey, - 'anthropic-version': '2023-06-01' - }; - body = JSON.stringify({ - model: modelName, - messages: [ - { - role: "user", - content: "Hello, this is a test message. Please reply with 'OK' if you receive this." - } - ], - max_tokens: 10 - }); - } else { - // openai 和 opencode-go 都使用 OpenAI 兼容格式 - headers = { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}` - }; - body = JSON.stringify({ - model: modelName, - messages: [ - { - role: "user", - content: "Hello, this is a test message. Please reply with 'OK' if you receive this." - } - ], - max_tokens: 10 - }); - } + let headers, body; + if (apiType === 'anthropic') { + headers = { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + 'anthropic-version': '2023-06-01' + }; + } else { + headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }; + } + body = JSON.stringify({ + model: modelName, + messages: [ + { + role: "user", + content: "Hello, this is a test message. Please reply with 'OK' if you receive this." + } + ], + max_tokens: 10 + }); + + // 屏蔽密钥用于诊断展示 + const maskedHeaders = { ...headers }; + if (maskedHeaders.Authorization) { + maskedHeaders.Authorization = `Bearer ${apiKey.slice(0, 4)}...${apiKey.slice(-4)}`; + } + if (maskedHeaders['x-api-key']) { + maskedHeaders['x-api-key'] = `${apiKey.slice(0, 4)}...${apiKey.slice(-4)}`; + } - const response = await fetch(fullUrl, { + const baseDetails = { + request: { + url: fullUrl, method: 'POST', - headers: headers, - body: body - }); + apiType, + headers: maskedHeaders, + bodyPreview: body + } + }; - const data = await response.json(); + let response; + try { + response = await fetch(fullUrl, { method: 'POST', headers, body }); + } catch (e) { + // 网络层错误(CORS、DNS、SSL、未连接……)通常没有 response + const err = new Error(`API测试失败:网络层错误 - ${e.message || e.name}`); + err.details = { + ...baseDetails, + elapsedMs: Date.now() - startedAt, + errorType: e.name || 'Error', + errorMessage: e.message || String(e), + errorStack: e.stack || null, + hint: '常见原因:1) Tauri webview 的 CORS 限制;2) URL 错误或域名解析失败;3) HTTPS 证书问题;4) 防火墙/代理拦截。建议改用后端发请求。' + }; + throw err; + } - if (data.error) { - throw new Error(data.error.message || '未知错误'); - } + // 拿到响应 -> 先以文本形式读取,再尝试 JSON 解析(防止响应不是 JSON 时丢失信息) + const rawText = await response.text(); + let data = null; + let parseError = null; + try { + data = JSON.parse(rawText); + } catch (e) { + parseError = e.message; + } - if (apiType === 'anthropic') { - if (data.content && data.content[0] && data.content[0].text) { - return true; - } - } else { - if (data.choices && data.choices[0] && data.choices[0].message) { - return true; - } - } + const responseInfo = { + status: response.status, + statusText: response.statusText, + ok: response.ok, + bodyPreview: rawText.length > 2000 ? rawText.slice(0, 2000) + '...(truncated)' : rawText, + parseError + }; - throw new Error('响应格式不正确'); - } catch (error) { - throw new Error(`API测试失败: ${error.message}`); + if (!response.ok) { + const apiMsg = data?.error?.message || data?.message || rawText.slice(0, 200); + const err = new Error(`API测试失败:HTTP ${response.status} - ${apiMsg}`); + err.details = { ...baseDetails, elapsedMs: Date.now() - startedAt, response: responseInfo }; + throw err; } + + if (data?.error) { + const err = new Error(`API测试失败:${data.error.message || '未知错误'}`); + err.details = { ...baseDetails, elapsedMs: Date.now() - startedAt, response: responseInfo }; + throw err; + } + + const success = apiType === 'anthropic' + ? !!(data?.content?.[0]?.text) + : !!(data?.choices?.[0]?.message); + + if (!success) { + const err = new Error('API测试失败:响应格式不正确'); + err.details = { ...baseDetails, elapsedMs: Date.now() - startedAt, response: responseInfo }; + throw err; + } + + return { ok: true, details: { ...baseDetails, elapsedMs: Date.now() - startedAt, response: responseInfo } }; }; const MODEL_OPTIONS = [ @@ -109,6 +150,8 @@ export default function Settings() { const { settings, updateSettings } = useStore(); const [activeModel, setActiveModel] = useState(settings?.model_type || 'deepseek'); const [isTestingConnection, setIsTestingConnection] = useState(false); + // testResult: { ok: boolean, message: string, details: object } | null + const [testResult, setTestResult] = useState(null); useEffect(() => { if (settings?.model_type) { @@ -274,6 +317,7 @@ export default function Settings() { } setIsTestingConnection(true); + setTestResult(null); try { const result = await testOpenAIConnection( settings.custom_model.auth, @@ -281,11 +325,13 @@ export default function Settings() { settings.custom_model.model_name, settings.custom_model.api_type ); - if (result) { + if (result?.ok) { showSuccess('API连接测试成功!'); + setTestResult({ ok: true, message: 'API连接测试成功', details: result.details }); } } catch (error) { showError(error.message); + setTestResult({ ok: false, message: error.message, details: error.details || { errorMessage: error.message, errorStack: error.stack } }); } finally { setIsTestingConnection(false); } @@ -322,6 +368,49 @@ export default function Settings() { )}
+ {/* 测试详情面板 */} + {activeModel === 'custom' && testResult && ( +
+
+ + {testResult.ok ? '✓ ' : '✗ '}{testResult.message} + +
+ + +
+
+
+{JSON.stringify(testResult.details, null, 2)}
+                                
+ {!testResult.ok && testResult.details?.hint && ( +
+ 💡 {testResult.details.hint} +
+ )} +
+ )}
From 30b2e52417b1a6f6e5e3f95ea7107501facab247 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 21 May 2026 23:59:25 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=E6=B5=8B=E8=AF=95=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E6=94=B9=E8=B5=B0Rust=E5=90=8E=E7=AB=AFreqwest=EF=BC=8C?= =?UTF-8?q?=E7=BB=95=E8=BF=87webview=20CORS=E9=99=90=E5=88=B6=20(v1.2.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.toml | 2 +- src-tauri/src/ai_translator.rs | 226 +++++++++++++++++++++++++++++++++ src-tauri/src/lib.rs | 13 +- src-tauri/tauri.conf.json | 2 +- src/pages/Settings.jsx | 125 ++---------------- 5 files changed, 252 insertions(+), 116 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index aa2fa67..56a40ad 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "DeepRant" -version = "1.2.2" +version = "1.2.3" description = "A Tauri App" authors = ["you"] license = "" diff --git a/src-tauri/src/ai_translator.rs b/src-tauri/src/ai_translator.rs index 6ad71e9..04f2877 100644 --- a/src-tauri/src/ai_translator.rs +++ b/src-tauri/src/ai_translator.rs @@ -209,6 +209,232 @@ fn resolve_api_url(base_url: &str, api_type: &str) -> String { format!("{}{}", trimmed, suffix) } +/// 屏蔽 API key 中间字符,仅展示前后 4 位 +fn mask_secret(s: &str) -> String { + let len = s.chars().count(); + if len <= 8 { + return "*".repeat(len); + } + let prefix: String = s.chars().take(4).collect(); + let suffix: String = s.chars().skip(len.saturating_sub(4)).collect(); + format!("{}...{}", prefix, suffix) +} + +/// 测试自定义 API 连接性。返回完整诊断信息(成功或失败均带 details)。 +/// 走 Rust 后端 reqwest,不受前端 CORS 限制。 +pub async fn test_api_connection( + api_key: String, + base_url: String, + model_name: String, + api_type: String, +) -> Value { + let started = std::time::Instant::now(); + let api_type_lc = api_type.to_lowercase(); + let resolved_url = resolve_api_url(&base_url, &api_type_lc); + + let body = if api_type_lc == "anthropic" { + json!({ + "model": model_name, + "messages": [ + { + "role": "user", + "content": "Hello, this is a test message. Please reply with 'OK' if you receive this." + } + ], + "max_tokens": 10 + }) + } else { + json!({ + "model": model_name, + "messages": [ + { + "role": "user", + "content": "Hello, this is a test message. Please reply with 'OK' if you receive this." + } + ], + "max_tokens": 10 + }) + }; + + let masked_headers = if api_type_lc == "anthropic" { + json!({ + "Content-Type": "application/json", + "x-api-key": mask_secret(&api_key), + "anthropic-version": "2023-06-01" + }) + } else { + json!({ + "Content-Type": "application/json", + "Authorization": format!("Bearer {}", mask_secret(&api_key)) + }) + }; + + let request_info = json!({ + "url": resolved_url, + "method": "POST", + "apiType": api_type_lc, + "headers": masked_headers, + "bodyPreview": body.to_string() + }); + + let client = match Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + { + Ok(c) => c, + Err(e) => { + return json!({ + "ok": false, + "message": format!("API测试失败:HTTP 客户端初始化错误 - {}", e), + "details": { + "request": request_info, + "elapsedMs": started.elapsed().as_millis() as u64, + "errorType": "ClientBuildError", + "errorMessage": e.to_string() + } + }); + } + }; + + let req = client + .post(&resolved_url) + .header("Content-Type", "application/json"); + + let req = if api_type_lc == "anthropic" { + req.header("x-api-key", &api_key) + .header("anthropic-version", "2023-06-01") + } else { + req.header("Authorization", format!("Bearer {}", api_key)) + }; + + let resp = match req.json(&body).send().await { + Ok(r) => r, + Err(e) => { + return json!({ + "ok": false, + "message": format!("API测试失败:网络层错误 - {}", e), + "details": { + "request": request_info, + "elapsedMs": started.elapsed().as_millis() as u64, + "errorType": "NetworkError", + "errorMessage": e.to_string(), + "hint": "可能的原因:1) URL 错误或域名解析失败;2) HTTPS 证书问题;3) 防火墙/代理拦截;4) 服务端无响应/超时。" + } + }); + } + }; + + let status = resp.status(); + let status_code = status.as_u16(); + let status_text = status.canonical_reason().unwrap_or("").to_string(); + + let raw_text = match resp.text().await { + Ok(t) => t, + Err(e) => { + return json!({ + "ok": false, + "message": format!("API测试失败:读取响应体错误 - {}", e), + "details": { + "request": request_info, + "elapsedMs": started.elapsed().as_millis() as u64, + "errorType": "ReadBodyError", + "errorMessage": e.to_string() + } + }); + } + }; + + let parsed: Option = serde_json::from_str(&raw_text).ok(); + let body_preview: String = if raw_text.chars().count() > 2000 { + let truncated: String = raw_text.chars().take(2000).collect(); + format!("{}...(truncated)", truncated) + } else { + raw_text.clone() + }; + + let response_info = json!({ + "status": status_code, + "statusText": status_text, + "ok": status.is_success(), + "bodyPreview": body_preview, + "parseError": if parsed.is_none() && !raw_text.is_empty() { Some("响应非有效 JSON") } else { None } + }); + + if !status.is_success() { + let api_msg = parsed + .as_ref() + .and_then(|v| v.get("error").and_then(|e| e.get("message")).and_then(|m| m.as_str())) + .or_else(|| parsed.as_ref().and_then(|v| v.get("message")).and_then(|m| m.as_str())) + .map(|s| s.to_string()) + .unwrap_or_else(|| raw_text.chars().take(200).collect()); + + return json!({ + "ok": false, + "message": format!("API测试失败:HTTP {} - {}", status_code, api_msg), + "details": { + "request": request_info, + "elapsedMs": started.elapsed().as_millis() as u64, + "response": response_info + } + }); + } + + if let Some(ref v) = parsed { + if let Some(err) = v.get("error") { + let msg = err.get("message").and_then(|m| m.as_str()).unwrap_or("未知错误"); + return json!({ + "ok": false, + "message": format!("API测试失败:{}", msg), + "details": { + "request": request_info, + "elapsedMs": started.elapsed().as_millis() as u64, + "response": response_info + } + }); + } + } + + let success = if api_type_lc == "anthropic" { + parsed + .as_ref() + .and_then(|v| v.get("content")) + .and_then(|c| c.as_array()) + .and_then(|arr| arr.first()) + .and_then(|item| item.get("text")) + .is_some() + } else { + parsed + .as_ref() + .and_then(|v| v.get("choices")) + .and_then(|c| c.as_array()) + .and_then(|arr| arr.first()) + .and_then(|c| c.get("message")) + .is_some() + }; + + if !success { + return json!({ + "ok": false, + "message": "API测试失败:响应格式不正确", + "details": { + "request": request_info, + "elapsedMs": started.elapsed().as_millis() as u64, + "response": response_info + } + }); + } + + json!({ + "ok": true, + "message": "API连接测试成功", + "details": { + "request": request_info, + "elapsedMs": started.elapsed().as_millis() as u64, + "response": response_info + } + }) +} + pub async fn translate_with_gpt(app: &AppHandle, original: &str) -> Result { let settings = crate::store::get_settings(app)?; println!("当前翻译设置:"); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c14bb8e..36684b8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -34,6 +34,16 @@ async fn get_settings(app_handle: tauri::AppHandle) -> Result serde_json::Value { + ai_translator::test_api_connection(api_key, base_url, model_name, api_type).await +} + pub fn run() { println!("Starting application..."); @@ -74,7 +84,8 @@ pub fn run() { update_translator_shortcut, log_to_backend, get_settings, - get_version + get_version, + test_api_connection ]); // 只在非Windows系统上添加窗口事件监听 diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6418fef..c3f7c1a 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "DeepRant", - "version": "1.2.2", + "version": "1.2.3", "identifier": "com.DeepRant.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/pages/Settings.jsx b/src/pages/Settings.jsx index 1332eb3..32db0b1 100644 --- a/src/pages/Settings.jsx +++ b/src/pages/Settings.jsx @@ -3,124 +3,23 @@ import { Server, Crown, Sparkles, Cube } from '../icons'; import { useState, useEffect } from 'react'; import { useStore } from '../components/StoreProvider'; import { showSuccess, showError } from '../utils/toast'; +import { invoke } from '@tauri-apps/api/core'; -// 根据 API 类型自动补全完整请求 URL -const resolveApiUrl = (baseUrl, apiType) => { - const trimmed = (baseUrl || '').trim().replace(/\/+$/, ''); - if (trimmed.includes('/chat/completions') || trimmed.endsWith('/messages')) { - return trimmed; - } - const suffix = apiType === 'anthropic' ? '/v1/messages' : '/v1/chat/completions'; - return `${trimmed}${suffix}`; -}; - -// 添加测试函数。失败时抛出的 Error 会带 `.details` 字段,包含完整诊断信息。 +// 通过 Rust 后端测试连接(绕过前端 webview 的 CORS 限制)。 +// 后端返回 { ok, message, details } 结构,前端原样展示。 const testOpenAIConnection = async (apiKey, baseUrl, modelName, apiType = 'openai') => { - const fullUrl = resolveApiUrl(baseUrl, apiType); - const startedAt = Date.now(); - - let headers, body; - if (apiType === 'anthropic') { - headers = { - 'Content-Type': 'application/json', - 'x-api-key': apiKey, - 'anthropic-version': '2023-06-01' - }; - } else { - headers = { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}` - }; - } - body = JSON.stringify({ - model: modelName, - messages: [ - { - role: "user", - content: "Hello, this is a test message. Please reply with 'OK' if you receive this." - } - ], - max_tokens: 10 + const result = await invoke('test_api_connection', { + apiKey, + baseUrl, + modelName, + apiType, }); - - // 屏蔽密钥用于诊断展示 - const maskedHeaders = { ...headers }; - if (maskedHeaders.Authorization) { - maskedHeaders.Authorization = `Bearer ${apiKey.slice(0, 4)}...${apiKey.slice(-4)}`; - } - if (maskedHeaders['x-api-key']) { - maskedHeaders['x-api-key'] = `${apiKey.slice(0, 4)}...${apiKey.slice(-4)}`; - } - - const baseDetails = { - request: { - url: fullUrl, - method: 'POST', - apiType, - headers: maskedHeaders, - bodyPreview: body - } - }; - - let response; - try { - response = await fetch(fullUrl, { method: 'POST', headers, body }); - } catch (e) { - // 网络层错误(CORS、DNS、SSL、未连接……)通常没有 response - const err = new Error(`API测试失败:网络层错误 - ${e.message || e.name}`); - err.details = { - ...baseDetails, - elapsedMs: Date.now() - startedAt, - errorType: e.name || 'Error', - errorMessage: e.message || String(e), - errorStack: e.stack || null, - hint: '常见原因:1) Tauri webview 的 CORS 限制;2) URL 错误或域名解析失败;3) HTTPS 证书问题;4) 防火墙/代理拦截。建议改用后端发请求。' - }; + if (!result?.ok) { + const err = new Error(result?.message || 'API测试失败'); + err.details = result?.details || {}; throw err; } - - // 拿到响应 -> 先以文本形式读取,再尝试 JSON 解析(防止响应不是 JSON 时丢失信息) - const rawText = await response.text(); - let data = null; - let parseError = null; - try { - data = JSON.parse(rawText); - } catch (e) { - parseError = e.message; - } - - const responseInfo = { - status: response.status, - statusText: response.statusText, - ok: response.ok, - bodyPreview: rawText.length > 2000 ? rawText.slice(0, 2000) + '...(truncated)' : rawText, - parseError - }; - - if (!response.ok) { - const apiMsg = data?.error?.message || data?.message || rawText.slice(0, 200); - const err = new Error(`API测试失败:HTTP ${response.status} - ${apiMsg}`); - err.details = { ...baseDetails, elapsedMs: Date.now() - startedAt, response: responseInfo }; - throw err; - } - - if (data?.error) { - const err = new Error(`API测试失败:${data.error.message || '未知错误'}`); - err.details = { ...baseDetails, elapsedMs: Date.now() - startedAt, response: responseInfo }; - throw err; - } - - const success = apiType === 'anthropic' - ? !!(data?.content?.[0]?.text) - : !!(data?.choices?.[0]?.message); - - if (!success) { - const err = new Error('API测试失败:响应格式不正确'); - err.details = { ...baseDetails, elapsedMs: Date.now() - startedAt, response: responseInfo }; - throw err; - } - - return { ok: true, details: { ...baseDetails, elapsedMs: Date.now() - startedAt, response: responseInfo } }; + return result; }; const MODEL_OPTIONS = [