Skip to content

Commit c80c1f0

Browse files
committed
fix(ai): 任务预检密钥源统一与 Ollama 免密钥测试连接(v0.11.6 M1)
1 parent 161fb8b commit c80c1f0

5 files changed

Lines changed: 43 additions & 9 deletions

File tree

‎app/src-tauri/src/ai_client.rs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ pub struct AiClientConfig {
4949
pub timeout_secs: u64,
5050
pub max_retries: u32,
5151
pub max_tokens: u32,
52+
/// 本地端点(Ollama 等免密钥推理;空密钥不触发 Auth 检查——本地优先叙事)
53+
pub is_local: bool,
5254
}
5355

5456
/// 归一化 AI 错误(REQ-145 失败原因四类 + 服务端/解析补充)。
@@ -121,6 +123,7 @@ impl AiClient {
121123
timeout_secs: env_parse("SILICONFLOW_TIMEOUT_SECS", DEFAULT_TIMEOUT_SECS),
122124
max_retries: env_parse("SILICONFLOW_RETRIES", DEFAULT_MAX_RETRIES as u64) as u32,
123125
max_tokens: env_parse(MAX_TOKENS_ENV, DEFAULT_MAX_TOKENS as u64) as u32,
126+
is_local: false,
124127
})
125128
}
126129

@@ -138,6 +141,8 @@ impl AiClient {
138141
timeout_secs: env_parse("SILICONFLOW_TIMEOUT_SECS", DEFAULT_TIMEOUT_SECS),
139142
max_retries: env_parse("SILICONFLOW_RETRIES", DEFAULT_MAX_RETRIES as u64) as u32,
140143
max_tokens: env_parse(MAX_TOKENS_ENV, DEFAULT_MAX_TOKENS as u64) as u32,
144+
// Why: Ollama 本地端点无需密钥;空密钥不触发 Auth 检查——本地优先叙事
145+
is_local: provider.kind == crate::ai_provider::ProviderKind::Ollama,
141146
})
142147
}
143148

@@ -162,7 +167,7 @@ impl AiClient {
162167
/// parse_response 做 TextFilterResponse 反序列化);
163168
/// chat_json = chat_text + parse_json_object 的组合。
164169
pub fn chat_text(&self, system: &str, user: &str) -> Result<String, AiClientError> {
165-
if self.config.api_key.trim().is_empty() {
170+
if !self.config.is_local && self.config.api_key.trim().is_empty() {
166171
return Err(AiClientError::Auth("未配置 API 密钥(设置页保存或配置环境变量)".to_string()));
167172
}
168173
let payload = build_chat_payload(&self.config.model, system, user, self.config.max_tokens);

‎app/src-tauri/src/ai_client_tests.rs‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,13 +155,31 @@ fn chat_json_without_key_is_auth_error() {
155155
timeout_secs: 5,
156156
max_retries: 0,
157157
max_tokens: 20000,
158+
is_local: false,
158159
});
159160
match client.chat_json("sys", "usr") {
160161
Err(AiClientError::Auth(_)) => {}
161162
other => panic!("期望 Auth 错误,实际 {:?}", other.map(|_| ())),
162163
}
163164
}
164165

166+
#[test]
167+
fn chat_json_without_key_skips_auth_when_local() {
168+
// Why: Ollama 本地端点无需密钥——is_local=true 时空密钥不触发 Auth 拒绝
169+
let client = AiClient::new(AiClientConfig {
170+
base_url: "http://127.0.0.1:11434/v1".to_string(),
171+
api_key: "".to_string(),
172+
model: "llama3".to_string(),
173+
timeout_secs: 5,
174+
max_retries: 0,
175+
max_tokens: 20000,
176+
is_local: true,
177+
});
178+
// is_local 时空密钥应跳过 Auth 检查,实际会触发网络/传输错误(非 Auth)
179+
let result = client.chat_json("sys", "usr");
180+
assert!(!matches!(result, Err(AiClientError::Auth(_))), "is_local 不应返回 Auth 错误");
181+
}
182+
165183
// ---- v0.11.6 M1:Provider 化 + 降级链(Task 3 纯函数层)----
166184

167185
#[test]
@@ -172,6 +190,18 @@ fn from_provider_builds_config_from_provider() {
172190
assert_eq!(client.config.base_url, p.base_url);
173191
assert_eq!(client.config.model, p.default_model);
174192
assert_eq!(client.config.api_key, "sk-p1");
193+
// Why: Ollama 预设 is_local=true;云端预设(SiliconFlow)is_local=false
194+
assert!(!client.config.is_local, "云端 Provider is_local=false");
195+
}
196+
197+
#[test]
198+
fn from_provider_sets_is_local_for_ollama() {
199+
// Why: Ollama 本地端点无需密钥——is_local=true 确保空密钥不触发 Auth 拒绝
200+
let ollama_preset = preset_templates().into_iter().find(|p| p.kind == crate::ai_provider::ProviderKind::Ollama)
201+
.expect("Ollama 预设存在");
202+
let client = AiClient::from_provider(&ollama_preset, None);
203+
assert!(client.config.is_local, "Ollama Provider is_local=true");
204+
assert!(client.config.api_key.is_empty(), "Ollama 空密钥预期");
175205
}
176206

177207
#[test]

‎app/src-tauri/src/ai_text_filter.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ impl AiTextFilterAdapter {
168168
// 2026-08-21 真机排查:显式输出上限(缺省时 DeepSeek 8192 token
169169
// 硬切 JSON 截断)——与共享 client 同默认,env 可覆盖
170170
max_tokens: self.config.max_tokens,
171+
is_local: false,
171172
});
172173
let raw = client
173174
.chat_text(&system, &user)

‎app/src-tauri/src/commands_ai_enrich.rs‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,9 @@ pub async fn ai_enrich_start(
8484
}
8585
let mock = std::env::var(MOCK_ENV).map(|v| v == "1").unwrap_or(false);
8686
if !mock {
87-
let env_key = std::env::var("SILICONFLOW_API_KEY").ok().filter(|k| !k.is_empty());
88-
let stored = st.ai_credentials.load_key("default")?;
89-
if env_key.is_none() && stored.is_none() {
90-
return Err("未配置 API 密钥(设置页保存密钥或配置环境变量 SILICONFLOW_API_KEY)".to_string());
87+
let has_key = crate::commands_ai_providers::resolve_default_provider_key(&st)?.is_some();
88+
if !has_key {
89+
return Err("未配置 API 密钥(请在设置页 AI 服务提供商中配置)".to_string());
9190
}
9291
}
9392
// F1 修复(2026-08-21)+ 审查修复(2026-08-21):任务去重——按

‎app/src-tauri/src/commands_ai_refine.rs‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,9 @@ pub async fn ai_refine_start(
133133
let mock = std::env::var(MOCK_ENV).map(|v| v == "1").unwrap_or(false);
134134
// ② 密钥解析(env > 凭据库);非 mock 且无密钥 → 明确错误(不创建任务)
135135
if !mock {
136-
let env_key = std::env::var("SILICONFLOW_API_KEY").ok().filter(|k| !k.is_empty());
137-
let stored = st.ai_credentials.load_key("default")?;
138-
if env_key.is_none() && stored.is_none() {
139-
return Err("未配置 API 密钥(设置页保存密钥或配置环境变量 SILICONFLOW_API_KEY)".to_string());
136+
let has_key = crate::commands_ai_providers::resolve_default_provider_key(&st)?.is_some();
137+
if !has_key {
138+
return Err("未配置 API 密钥(请在设置页 AI 服务提供商中配置)".to_string());
140139
}
141140
}
142141
// ②b F1 修复(2026-08-21)+ 审查修复(2026-08-21):任务去重——按

0 commit comments

Comments
 (0)