Skip to content

Commit be869ef

Browse files
committed
feat(ai): DeepSeek 默认化——默认 Provider + vision-exp 模型链(v0.12.0 M4)
M4 配置债: - preset_templates DeepSeek 提首位(effective_default_id 取第一个 enabled)+ models 加 deepseek-v4-flash-vision-exp + default_model 切该模型(视觉多模态,图文/视频精修主力) - migrate_from_legacy 迁移目标 SiliconFlow → DeepSeek 官方端点(旧 SiliconFlow 链路废除, 直接生成 DeepSeek 默认 Provider);app_setup 密钥迁移 scope 同步 legacy-deepseek - ai_cost builtin_prices 登记 deepseek-v4-flash-vision-exp 分段价(保守上界 9.0 元/百万,标注待实测校准) - commands_ai_providers 默认 Provider 密钥 env 读 SILICONFLOW_API_KEY → DEEPSEEK_API_KEY - 前端 AiProviderSettings PRESET_OPTIONS 同步(DeepSeek 首位 + vision);授权/复核/精修 文案 SiliconFlow → DeepSeek - 测试:ai_provider_tests(首位/迁移目标)、ai_cost_tests(vision 单价)断言适配
1 parent c33db00 commit be869ef

10 files changed

Lines changed: 84 additions & 44 deletions

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ fn builtin_prices() -> HashMap<&'static str, f64> {
4040
m.insert("deepseek-ai/DeepSeek-R1-0528-Qwen3-8B", 0.0);
4141
m.insert("deepseek-ai/DeepSeek-V3-0324", 2.0);
4242
m.insert("Qwen/Qwen3-235B-A22B", 2.0);
43+
// v0.12.0 M4(默认链 DeepSeek):deepseek-v4-flash-vision-exp 分段价
44+
// (官方 2026-08:输入缓存未命中 1.5-3.0 元/百万 token,输出 4.5-9.0 元/百万)
45+
// ——取保守上界 9.0 登记(宁可高估不可低估,待 golden 冒烟实测校准后分段)。
46+
m.insert("deepseek-v4-flash-vision-exp", 9.0);
4347
m
4448
}
4549

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,22 @@ fn price_model_mapping_known_and_unknown() {
100100
});
101101
}
102102

103+
/// v0.12.0 M4(默认链 DeepSeek):vision 模型单价登记(保守上界 9.0——官方
104+
/// 分段价输入 1.5-3.0 / 输出 4.5-9.0 元/百万 token,取上界宁可高估)。
105+
#[test]
106+
fn deepseek_vision_model_price_registered() {
107+
with_env_locked(|| {
108+
std::env::remove_var("SILICONFLOW_PRICE_PER_1M_TOKENS");
109+
let (p, known) = price_for_model("deepseek-v4-flash-vision-exp");
110+
assert_eq!(p, 9.0);
111+
assert!(known, "vision 模型单价已登记——必须无未知警告");
112+
let est = estimate_for_content_model(1000, "deepseek-v4-flash-vision-exp");
113+
assert!(est.price_known);
114+
// 估算含输出 token(输入 ×2),1000→2000 token × ¥9/1M = ¥0.018
115+
assert!((est.est_cost_yuan - 0.018).abs() < 1e-9, "实得 {}", est.est_cost_yuan);
116+
});
117+
}
118+
103119
/// F1 修复:按模型估算——已知免费档 ¥0、未知模型带警告标记。
104120
#[test]
105121
fn estimate_for_content_model_respects_mapping() {

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

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
//! "缺失/损坏回退内置默认"加载模式;密钥不在此层(DPAPI
99
//! per-provider,ai_credentials.rs)。
1010
//! @ai-context: 旧版(v0.11.5 及之前)单 Provider 配置(ai_settings.json 的
11-
//! base_url/model + 单密钥)首启自动迁移为 SiliconFlow Provider
12-
//! ——用户无感,4 处 AiClient::from_settings 调用点零改动。
11+
//! base_url/model + 单密钥)首启自动迁移为 DeepSeek Provider
12+
//! (v0.12.0 M4 默认链切换——旧 SiliconFlow 链路废除)——用户无感,
13+
//! 4 处 AiClient::from_settings 调用点零改动。
1314
1415
use serde::{Deserialize, Serialize};
1516

@@ -27,7 +28,8 @@ pub enum ProviderKind {
2728
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2829
#[serde(rename_all = "camelCase", default)]
2930
pub struct AiProviderConfig {
30-
/// 唯一 id(生成时 uuid/slug;迁移时固定 "legacy-siliconflow")
31+
/// 唯一 id(生成时 uuid/slug;迁移时固定 "legacy-deepseek"——v0.12.0 M4
32+
/// 迁移目标由 SiliconFlow 改为 DeepSeek 新默认链)
3133
pub id: String,
3234
/// 显示名("SiliconFlow")
3335
pub name: String,
@@ -149,20 +151,22 @@ pub fn preset_templates() -> Vec<AiProviderConfig> {
149151
fallback_order: Vec::new(),
150152
};
151153
vec![
154+
// v0.12.0 M4(默认链 DeepSeek):DeepSeek 提首位——effective_default_id
155+
// 取第一个 enabled → 新装/迁移用户默认 Provider 即 DeepSeek。
156+
mk(
157+
"deepseek",
158+
"DeepSeek",
159+
"https://api.deepseek.com/v1",
160+
&["deepseek-v4-flash-vision-exp", "deepseek-chat", "deepseek-reasoner"],
161+
"deepseek-v4-flash-vision-exp",
162+
),
152163
mk(
153164
"siliconflow",
154165
"SiliconFlow",
155166
"https://api.siliconflow.cn/v1",
156167
&["deepseek-ai/DeepSeek-R1-0528-Qwen3-8B", "deepseek-ai/DeepSeek-V3.2"],
157168
"deepseek-ai/DeepSeek-R1-0528-Qwen3-8B",
158169
),
159-
mk(
160-
"deepseek",
161-
"DeepSeek",
162-
"https://api.deepseek.com/v1",
163-
&["deepseek-chat", "deepseek-reasoner"],
164-
"deepseek-chat",
165-
),
166170
mk(
167171
"openrouter",
168172
"OpenRouter",
@@ -188,24 +192,28 @@ pub fn provider_scope(provider_id: &str) -> String {
188192
format!("provider:{}", provider_id)
189193
}
190194

191-
/// 旧配置迁移(首启):旧 ai_settings 的 base_url/model → SiliconFlow Provider。
195+
/// 旧配置迁移(首启):旧版单 Provider 配置 → DeepSeek 默认链(v0.12.0 M4)。
192196
///
193197
/// @ai-context: 迁移幂等——ai_providers.json 已存在时不触发(调用方判断);
194198
/// 密钥迁移在 command 层(旧凭据条目 → provider:<id> 新条目)。
195-
pub fn migrate_from_legacy(legacy: &crate::ai_settings::AiSettings) -> (Vec<AiProviderConfig>, Option<String>) {
196-
let mut p = AiProviderConfig {
197-
id: "legacy-siliconflow".to_string(),
198-
name: "SiliconFlow".to_string(),
199-
base_url: legacy.base_url.clone(),
200-
default_model: legacy.model.clone(),
201-
models: vec![legacy.model.clone()],
199+
/// @ai-context: v0.12.0 M4(默认链 DeepSeek):迁移目标由 SiliconFlow 改为
200+
/// DeepSeek 官方端点 + deepseek-v4-flash-vision-exp 默认模型——
201+
/// 旧 key 已废除,legacy.base_url/model(旧 SiliconFlow 链路)不再
202+
/// 沿用,直接生成 DeepSeek 默认 Provider(保可用不崩)。
203+
pub fn migrate_from_legacy(_legacy: &crate::ai_settings::AiSettings) -> (Vec<AiProviderConfig>, Option<String>) {
204+
// 取 DeepSeek 预设(id=="deepseek")为迁移模板——默认链目标。
205+
let ds = preset_templates()
206+
.into_iter()
207+
.find(|p| p.id == "deepseek")
208+
.expect("DeepSeek 预设必须存在(M4 默认链)");
209+
let p = AiProviderConfig {
210+
id: "legacy-deepseek".to_string(),
211+
name: "DeepSeek".to_string(),
212+
base_url: ds.base_url,
213+
default_model: ds.default_model,
214+
models: ds.models,
202215
..Default::default()
203216
};
204-
if p.validate().is_err() {
205-
// 旧配置非法 → 回退预设 SiliconFlow(保可用不崩)
206-
p = preset_templates().remove(0);
207-
p.id = "legacy-siliconflow".to_string();
208-
}
209217
let default_id = p.id.clone();
210218
(vec![p], Some(default_id))
211219
}

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

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
//! ai_provider.rs 单测(AAA 模式)。
22
//!
3-
//! @ai-context: 覆盖:预设模板完整性(4 Provider)、配置校验(合法/非法 URL、
4-
//! 空模型、默认模型不在列表/空白)、JSON roundtrip(顺序与默认
5-
//! 保持)、幽灵 id 回退(含全禁用边界)、旧版单 Provider 配置
6-
//! 迁移(SiliconFlow,含非法配置回退预设)。
3+
//! @ai-context: 覆盖:预设模板完整性(4 Provider、DeepSeek 首位 + vision 默认)、
4+
//! 配置校验(合法/非法 URL、空模型、默认模型不在列表/空白)、JSON
5+
//! roundtrip(顺序与默认保持)、幽灵 id 回退(含全禁用边界)、旧版
6+
//! 单 Provider 配置迁移(DeepSeek——v0.12.0 M4 默认链,含非法配置
7+
//! 仍产出合法 Provider)。
78
89
use crate::ai_provider::*;
910

@@ -12,6 +13,10 @@ fn preset_templates_cover_four_providers() {
1213
let presets = preset_templates();
1314
assert_eq!(presets.len(), 4);
1415
assert!(presets.iter().any(|p| p.kind == ProviderKind::Ollama));
16+
// v0.12.0 M4:DeepSeek 提首位(effective_default_id 取第一个 enabled → 默认链)
17+
assert_eq!(presets[0].id, "deepseek");
18+
assert_eq!(presets[0].default_model, "deepseek-v4-flash-vision-exp");
19+
assert!(presets[0].models.iter().any(|m| m == "deepseek-v4-flash-vision-exp"));
1520
let sf = presets.iter().find(|p| p.name == "SiliconFlow").unwrap();
1621
assert!(sf.base_url.starts_with("https://"));
1722
assert!(!sf.models.is_empty());
@@ -72,30 +77,37 @@ fn store_ghost_default_falls_back_to_first_enabled() {
7277
}
7378

7479
#[test]
75-
fn migrate_from_legacy_creates_siliconflow() {
80+
fn migrate_from_legacy_creates_deepseek() {
81+
// v0.12.0 M4:迁移目标由 SiliconFlow 改为 DeepSeek 默认链——legacy 配置
82+
// (旧 SiliconFlow 链路)不再沿用,直接生成 DeepSeek 官方端点 + vision 默认模型。
7683
let s = crate::ai_settings::AiSettings {
7784
base_url: "https://api.siliconflow.cn/v1".to_string(),
7885
model: "deepseek-ai/DeepSeek-R1-0528-Qwen3-8B".to_string(),
7986
..Default::default()
8087
};
8188
let (providers, default_id) = migrate_from_legacy(&s);
8289
assert_eq!(providers.len(), 1);
83-
assert_eq!(providers[0].name, "SiliconFlow");
84-
assert_eq!(providers[0].base_url, s.base_url);
85-
assert_eq!(providers[0].default_model, s.model);
90+
assert_eq!(providers[0].name, "DeepSeek");
91+
assert_eq!(providers[0].base_url, "https://api.deepseek.com/v1");
92+
assert_eq!(providers[0].default_model, "deepseek-v4-flash-vision-exp");
93+
assert!(providers[0].models.iter().any(|m| m == "deepseek-v4-flash-vision-exp"));
94+
assert_eq!(providers[0].id, "legacy-deepseek");
8695
assert_eq!(default_id, Some(providers[0].id.clone()));
8796
}
8897

8998
#[test]
90-
fn migrate_from_legacy_falls_back_on_invalid_config() {
99+
fn migrate_from_legacy_always_produces_valid_deepseek() {
100+
// 旧配置非法 → 迁移仍产出合法 DeepSeek 默认 Provider(保可用不崩;
101+
// 旧 SiliconFlow 链路已废除,不再回退旧端点)。
91102
let s = crate::ai_settings::AiSettings {
92103
base_url: "ftp://bad".to_string(),
93104
model: String::new(),
94105
..Default::default()
95106
};
96107
let (providers, default_id) = migrate_from_legacy(&s);
97108
assert_eq!(providers.len(), 1);
98-
assert_eq!(providers[0].id, "legacy-siliconflow");
109+
assert_eq!(providers[0].id, "legacy-deepseek");
99110
assert!(providers[0].validate().is_ok());
100-
assert_eq!(default_id, Some("legacy-siliconflow".to_string()));
111+
assert_eq!(providers[0].default_model, "deepseek-v4-flash-vision-exp");
112+
assert_eq!(default_id, Some("legacy-deepseek".to_string()));
101113
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ pub fn setup_app_state(app: &mut tauri::App) -> Result<(), String> {
157157
let ai_credentials = crate::ai_credentials::platform_store(&data_dir.join("ai_credentials.bin"));
158158
// v0.11.6 M1:AI Provider 装配——providers 为空即迁移(新用户亦生成
159159
// 无密钥预设 Provider,行为自洽);密钥迁移同处完成(旧凭据条目 →
160-
// provider:legacy-siliconflow 新条目)。
160+
// provider:legacy-deepseek 新条目,v0.12.0 M4 迁移目标改 DeepSeek)。
161161
// 锁序契约:启动单线程装配、锁序(providers→settings)与运行时调用点
162162
// (settings→providers)相反,禁止在命令层持 providers 锁读 settings。
163163
// save 决策:保存失败阻断启动是有意 fail-fast(数据目录不可写时后续
@@ -175,8 +175,8 @@ pub fn setup_app_state(app: &mut tauri::App) -> Result<(), String> {
175175
store.default_provider_id = default_id;
176176
// 密钥迁移:旧凭据存在且新 scope 不存在 → 复制(旧条目保留供回退)
177177
if let Ok(Some(key)) = ai_credentials.load_key("default") {
178-
if ai_credentials.load_key("provider:legacy-siliconflow").ok().flatten().is_none() {
179-
let _ = ai_credentials.save_key("provider:legacy-siliconflow", &key);
178+
if ai_credentials.load_key("provider:legacy-deepseek").ok().flatten().is_none() {
179+
let _ = ai_credentials.save_key("provider:legacy-deepseek", &key);
180180
}
181181
}
182182
store.save(&ai_providers_path)?;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ pub fn ai_provider_test(state: State<'_, AppState>, id: String) -> Result<String
217217
/// 解析默认 Provider 密钥(env 优先 > per-provider 凭据——Provider 面板
218218
/// 配置的密钥在此生效;无默认 Provider 回退旧 default scope 兼容迁移前)。
219219
pub fn resolve_default_provider_key(state: &AppState) -> Result<Option<String>, String> {
220-
let env_key = std::env::var("SILICONFLOW_API_KEY").ok().filter(|k| !k.is_empty());
220+
let env_key = std::env::var("DEEPSEEK_API_KEY").ok().filter(|k| !k.is_empty());
221221
if env_key.is_some() {
222222
return Ok(env_key);
223223
}

‎app/src/components/AiProviderSettings.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ const inputStyle: React.CSSProperties = {
3232
minWidth: 0,
3333
};
3434

35-
/** 预设模板(前端向导数据源;与后端 preset_templates 对应) */
35+
/** 预设模板(前端向导数据源——I-3 兜底;后端 ai_provider_presets 优先,v0.12.0 M4 默认链 DeepSeek 首位) */
3636
const PRESET_OPTIONS: { kind: AiProviderInput["kind"]; name: string; baseUrl: string; models: string }[] = [
37+
{ kind: "openAiCompat", name: "DeepSeek", baseUrl: "https://api.deepseek.com/v1", models: "deepseek-v4-flash-vision-exp" },
3738
{ kind: "openAiCompat", name: "SiliconFlow", baseUrl: "https://api.siliconflow.cn/v1", models: "deepseek-ai/DeepSeek-R1-0528-Qwen3-8B" },
38-
{ kind: "openAiCompat", name: "DeepSeek", baseUrl: "https://api.deepseek.com/v1", models: "deepseek-chat" },
3939
{ kind: "openAiCompat", name: "OpenRouter", baseUrl: "https://openrouter.ai/api/v1", models: "openai/gpt-4o-mini" },
4040
{ kind: "ollama", name: "Ollama(本地)", baseUrl: "http://127.0.0.1:11434/v1", models: "qwen2.5:7b" },
4141
];

‎app/src/components/AiRefineCard.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export default function AiRefineCard({ sessionId, onApplied }: { sessionId: numb
171171
{phase === "consent" && (
172172
<div style={{ border: "1px solid #f59e0b", background: "#fffbeb", borderRadius: 6, padding: 8, marginBottom: 6, fontSize: 12 }}>
173173
<div style={{ fontWeight: 600, marginBottom: 4 }}>首次使用需授权</div>
174-
精修将上传<strong>转写文本与最小上下文</strong>至 SiliconFlow;本地优先铁律:<strong>音视频/图像永不出本机</strong>。是否同意?
174+
精修将上传<strong>转写文本与最小上下文</strong>至 DeepSeek;本地优先铁律:<strong>音视频/图像永不出本机</strong>。是否同意?
175175
<div style={{ marginTop: 6, display: "flex", gap: 6 }}>
176176
<button style={{ ...btn, background: "#0d9488", color: "#fff", border: "none" }} onClick={async () => {
177177
await invoke("ai_set_authorized", { authorized: true }).catch(() => undefined);

‎app/src/components/EnrichPanel.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ export default function EnrichPanel({ noteId, onUpdated }: { noteId: number; onU
236236
{phase === "consent" && (
237237
<div style={{ border: "1px solid #f59e0b", background: "#fffbeb", borderRadius: 6, padding: 8, marginBottom: 6, fontSize: 12 }}>
238238
<div style={{ fontWeight: 600, marginBottom: 4 }}>首次使用需授权</div>
239-
知识补充将上传<strong>笔记文本与最小上下文</strong>至 SiliconFlow;本地优先铁律:<strong>音视频/图像永不出本机</strong>。是否同意?
239+
知识补充将上传<strong>笔记文本与最小上下文</strong>至 DeepSeek;本地优先铁律:<strong>音视频/图像永不出本机</strong>。是否同意?
240240
<div style={{ marginTop: 6, display: "flex", gap: 6 }}>
241241
<button style={{ ...btn, background: "#0d9488", color: "#fff", border: "none" }} onClick={async () => {
242242
await invoke("ai_set_authorized", { authorized: true }).catch(() => undefined);

‎app/src/components/NotePreviewView.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,11 @@ export default function NotePreviewView({ sessionId }: { sessionId: number }) {
139139
const aiReview = async () => {
140140
const enabled = aiStatus?.enabled;
141141
if (!enabled) {
142-
setStatus("AI 复核未启用(需配置 SILICONFLOW_API_KEY 与 AI_TEXT_FILTER_ENABLED)");
142+
setStatus("AI 复核未启用(需配置 DEEPSEEK_API_KEY 与 AI_TEXT_FILTER_ENABLED)");
143143
return;
144144
}
145145
const count = preview?.filtered.length ?? 0;
146-
if (!window.confirm(`将发送 ${count} 段边界文本至 SiliconFlow(模型 ${aiStatus?.model})进行删除/保留/合并判定。是否继续?`)) {
146+
if (!window.confirm(`将发送 ${count} 段边界文本至 DeepSeek(模型 ${aiStatus?.model})进行删除/保留/合并判定。是否继续?`)) {
147147
setStatus("已取消(预览保持纯规则结果)");
148148
return;
149149
}

0 commit comments

Comments
 (0)