From 8562658d09f2ecd81a2ab0a4a434741f914a7e8f Mon Sep 17 00:00:00 2001 From: coso Date: Sat, 20 Dec 2025 12:57:39 +0800 Subject: [PATCH 1/8] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E4=B8=AD=E7=9A=84=E9=BB=98=E8=AE=A4=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 primaryModel 默认值从 claude-sonnet-4-20250514 改为空字符串 - 用户可以自行选择是否设置模型,留空则使用系统默认 --- src-tauri/Cargo.lock | 2 +- src/components/clients/ProviderForm.tsx | 6 +++--- src/components/switch/ProviderForm.tsx | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 12123f63c..7660bd74f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3367,7 +3367,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.13.0" +version = "0.14.0" dependencies = [ "anyhow", "async-stream", diff --git a/src/components/clients/ProviderForm.tsx b/src/components/clients/ProviderForm.tsx index 8045d86c7..0cda51dae 100644 --- a/src/components/clients/ProviderForm.tsx +++ b/src/components/clients/ProviderForm.tsx @@ -315,7 +315,7 @@ export function ProviderForm({ return { apiKey: "", baseUrl: "", - primaryModel: "claude-sonnet-4-20250514", + primaryModel: "", haikuModel: "", sonnetModel: "", opusModel: "", @@ -328,7 +328,7 @@ export function ProviderForm({ return { apiKey: "", baseUrl: "", - primaryModel: "claude-sonnet-4-20250514", + primaryModel: "", haikuModel: "", sonnetModel: "", opusModel: "", @@ -337,7 +337,7 @@ export function ProviderForm({ return { apiKey: env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN || "", baseUrl: env.ANTHROPIC_BASE_URL || "", - primaryModel: env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514", + primaryModel: env.ANTHROPIC_MODEL || "", haikuModel: env.ANTHROPIC_DEFAULT_HAIKU_MODEL || "", sonnetModel: env.ANTHROPIC_DEFAULT_SONNET_MODEL || "", opusModel: env.ANTHROPIC_DEFAULT_OPUS_MODEL || "", diff --git a/src/components/switch/ProviderForm.tsx b/src/components/switch/ProviderForm.tsx index a1f783768..542e50e16 100644 --- a/src/components/switch/ProviderForm.tsx +++ b/src/components/switch/ProviderForm.tsx @@ -287,7 +287,7 @@ export function ProviderForm({ return { apiKey: "", baseUrl: "", - primaryModel: "claude-sonnet-4-20250514", + primaryModel: "", haikuModel: "", sonnetModel: "", opusModel: "", @@ -300,7 +300,7 @@ export function ProviderForm({ return { apiKey: "", baseUrl: "", - primaryModel: "claude-sonnet-4-20250514", + primaryModel: "", haikuModel: "", sonnetModel: "", opusModel: "", @@ -309,7 +309,7 @@ export function ProviderForm({ return { apiKey: env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN || "", baseUrl: env.ANTHROPIC_BASE_URL || "", - primaryModel: env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514", + primaryModel: env.ANTHROPIC_MODEL || "", haikuModel: env.ANTHROPIC_DEFAULT_HAIKU_MODEL || "", sonnetModel: env.ANTHROPIC_DEFAULT_SONNET_MODEL || "", opusModel: env.ANTHROPIC_DEFAULT_OPUS_MODEL || "", From 86fe6c3f3958cdb0d08d439f3aa027a08e6a12b0 Mon Sep 17 00:00:00 2001 From: coso Date: Sat, 20 Dec 2025 12:59:47 +0800 Subject: [PATCH 2/8] =?UTF-8?q?fix:=20=E5=90=8C=E6=97=B6=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=20OPENAI=5FAPI=5FKEY=20=E5=92=8C=20ANTHROPIC=5FAPI=5FKEY=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=8F=8C=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 generateJsonFromFields 中同时设置两个 API Key - 同时设置 OPENAI_BASE_URL 和 ANTHROPIC_BASE_URL - 解决 Claude Code 无法使用 OpenAI 协议的问题 --- src/components/clients/ProviderForm.tsx | 7 ++++++- src/components/switch/ProviderForm.tsx | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/clients/ProviderForm.tsx b/src/components/clients/ProviderForm.tsx index 0cda51dae..1243df53a 100644 --- a/src/components/clients/ProviderForm.tsx +++ b/src/components/clients/ProviderForm.tsx @@ -373,9 +373,14 @@ export function ProviderForm({ const generateJsonFromFields = useCallback(() => { const env: Record = {}; if (apiKey) { + // 同时设置两个 API Key,支持 OpenAI 和 Anthropic 两种协议 env.ANTHROPIC_API_KEY = apiKey; + env.OPENAI_API_KEY = apiKey; + } + if (baseUrl) { + env.ANTHROPIC_BASE_URL = baseUrl; + env.OPENAI_BASE_URL = baseUrl; } - if (baseUrl) env.ANTHROPIC_BASE_URL = baseUrl; if (primaryModel) env.ANTHROPIC_MODEL = primaryModel; if (haikuModel) env.ANTHROPIC_DEFAULT_HAIKU_MODEL = haikuModel; if (sonnetModel) env.ANTHROPIC_DEFAULT_SONNET_MODEL = sonnetModel; diff --git a/src/components/switch/ProviderForm.tsx b/src/components/switch/ProviderForm.tsx index 542e50e16..b21e66629 100644 --- a/src/components/switch/ProviderForm.tsx +++ b/src/components/switch/ProviderForm.tsx @@ -345,9 +345,14 @@ export function ProviderForm({ const generateJsonFromFields = useCallback(() => { const env: Record = {}; if (apiKey) { + // 同时设置两个 API Key,支持 OpenAI 和 Anthropic 两种协议 env.ANTHROPIC_API_KEY = apiKey; + env.OPENAI_API_KEY = apiKey; + } + if (baseUrl) { + env.ANTHROPIC_BASE_URL = baseUrl; + env.OPENAI_BASE_URL = baseUrl; } - if (baseUrl) env.ANTHROPIC_BASE_URL = baseUrl; if (primaryModel) env.ANTHROPIC_MODEL = primaryModel; if (haikuModel) env.ANTHROPIC_DEFAULT_HAIKU_MODEL = haikuModel; if (sonnetModel) env.ANTHROPIC_DEFAULT_SONNET_MODEL = sonnetModel; From c151cc46655cd60614f766f97789ca46d7073582 Mon Sep 17 00:00:00 2001 From: coso Date: Sat, 20 Dec 2025 13:03:17 +0800 Subject: [PATCH 3/8] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=20API=20Server=20?= =?UTF-8?q?=E5=87=AD=E8=AF=81=E5=88=97=E8=A1=A8=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除红色背景,改用统一的中性背景色 - 健康状态用绿色圆点,非健康用黄色圆点(不再用红色) - 添加定时刷新凭证池数据(5秒),使用次数能够实时更新 --- src/components/api-server/ApiServerPage.tsx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/components/api-server/ApiServerPage.tsx b/src/components/api-server/ApiServerPage.tsx index 501b3507c..631bf7f72 100644 --- a/src/components/api-server/ApiServerPage.tsx +++ b/src/components/api-server/ApiServerPage.tsx @@ -188,6 +188,9 @@ export function ApiServerPage() { useEffect(() => { loadPoolOverview(); + // 定时刷新凭证池数据,以便使用次数能够更新 + const poolInterval = setInterval(loadPoolOverview, 5000); + return () => clearInterval(poolInterval); }, []); // 自动清除 provider 切换提示 @@ -627,22 +630,12 @@ export function ApiServerPage() { {credentials.map((cred) => (
{cred.name || cred.uuid.slice(0, 8)} From 3b5c3ec037b57edce9b9173c13291eafa6910b65 Mon Sep 17 00:00:00 2001 From: coso Date: Sat, 20 Dec 2025 13:08:10 +0800 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20=E6=B5=8B=E8=AF=95=E6=88=90=E5=8A=9F?= =?UTF-8?q?=E5=90=8E=E7=AB=8B=E5=8D=B3=E5=88=B7=E6=96=B0=E5=87=AD=E8=AF=81?= =?UTF-8?q?=E6=B1=A0=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API 测试成功后立即调用 loadPoolOverview 刷新数据 - 使用次数能够即时更新显示 --- src/components/api-server/ApiServerPage.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/api-server/ApiServerPage.tsx b/src/components/api-server/ApiServerPage.tsx index 631bf7f72..fdf633e0a 100644 --- a/src/components/api-server/ApiServerPage.tsx +++ b/src/components/api-server/ApiServerPage.tsx @@ -311,6 +311,11 @@ export function ApiServerPage() { httpStatus: result.status, }, })); + + // 测试成功后立即刷新凭证池数据,更新使用次数 + if (result.success) { + await loadPoolOverview(); + } } catch (e: unknown) { const errMsg = e instanceof Error ? e.message : String(e); setTestResults((prev) => ({ From 88285f521b38971c7dde38e4ca0072b7a97b66a6 Mon Sep 17 00:00:00 2001 From: coso Date: Sat, 20 Dec 2025 13:26:47 +0800 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20=E6=89=98=E7=9B=98=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=BB=9F=E4=B8=80=E7=9A=84=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E4=B8=BB=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 所有状态(运行/警告/错误/停止)都使用应用主图标 32x32.png - 解决托盘图标显示为白色火箭的问题 - 保持跨平台一致性 --- src-tauri/src/tray/manager.rs | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src-tauri/src/tray/manager.rs b/src-tauri/src/tray/manager.rs index ee5c9338f..6ac32056a 100644 --- a/src-tauri/src/tray/manager.rs +++ b/src-tauri/src/tray/manager.rs @@ -77,17 +77,23 @@ impl TrayIcons { let default = Self::load_default_icon(app); // 尝试加载各状态图标 + // 为了保持一致性,所有状态都使用应用主图标 let tray_icons_dir = resource_path.join("icons").join("tray"); info!("托盘图标目录: {:?}", tray_icons_dir); - let running = Self::load_png_file(&tray_icons_dir.join("tray-running.png")); - let warning = Self::load_png_file(&tray_icons_dir.join("tray-warning.png")); - let error = Self::load_png_file(&tray_icons_dir.join("tray-error.png")); - let stopped = Self::load_png_file(&tray_icons_dir.join("tray-stopped.png")); + // 检查是否有自定义托盘图标,如果没有则使用默认图标 + let running = Self::load_png_file(&tray_icons_dir.join("tray-running.png")) + .or_else(|| Some(default.clone())); + let warning = Self::load_png_file(&tray_icons_dir.join("tray-warning.png")) + .or_else(|| Some(default.clone())); + let error = Self::load_png_file(&tray_icons_dir.join("tray-error.png")) + .or_else(|| Some(default.clone())); + let stopped = Self::load_png_file(&tray_icons_dir.join("tray-stopped.png")) + .or_else(|| Some(default.clone())); - if running.is_none() || warning.is_none() || error.is_none() || stopped.is_none() { - warn!("部分托盘图标加载失败,将使用默认图标作为后备"); - // 尝试从内嵌资源加载 + // 如果所有托盘图标都加载失败,使用内嵌资源 + if running.is_none() && warning.is_none() && error.is_none() && stopped.is_none() { + warn!("所有托盘图标加载失败,将使用内嵌资源"); return Self::load_embedded(); } @@ -104,14 +110,16 @@ impl TrayIcons { fn load_embedded() -> Self { info!("从内嵌资源加载托盘图标"); + // 使用应用主图标作为默认托盘图标 let default = Image::from_bytes(include_bytes!("../../icons/32x32.png")) .expect("内嵌默认图标加载失败"); - // 尝试加载内嵌的托盘图标 - let running = Image::from_bytes(include_bytes!("../../icons/tray/tray-running.png")).ok(); - let warning = Image::from_bytes(include_bytes!("../../icons/tray/tray-warning.png")).ok(); - let error = Image::from_bytes(include_bytes!("../../icons/tray/tray-error.png")).ok(); - let stopped = Image::from_bytes(include_bytes!("../../icons/tray/tray-stopped.png")).ok(); + // 所有状态都使用应用主图标,保持一致性 + // macOS 托盘图标建议使用单色 Template 图标,但为了跨平台一致性,使用应用图标 + let running = Image::from_bytes(include_bytes!("../../icons/32x32.png")).ok(); + let warning = Image::from_bytes(include_bytes!("../../icons/32x32.png")).ok(); + let error = Image::from_bytes(include_bytes!("../../icons/32x32.png")).ok(); + let stopped = Image::from_bytes(include_bytes!("../../icons/32x32.png")).ok(); Self { running, From 3374339bff5da15875f529dfea7737e989041f67 Mon Sep 17 00:00:00 2001 From: coso Date: Sat, 20 Dec 2025 14:53:24 +0800 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20Plugin=20UI=20=E9=9B=86=E6=88=90?= =?UTF-8?q?=E5=88=B0=20Extensions=20=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 lib.rs 中注册所有 plugin 命令到 invoke_handler - 在 ExtensionsPage 添加 Plugins 标签页 - 集成 PluginManager 组件显示插件列表 - 支持启用/禁用/重新加载/卸载插件操作 --- src-tauri/icons/tray/tray-error.png | Bin 653 -> 776 bytes src-tauri/icons/tray/tray-running.png | Bin 727 -> 776 bytes src-tauri/icons/tray/tray-stopped.png | Bin 715 -> 776 bytes src-tauri/icons/tray/tray-warning.png | Bin 715 -> 776 bytes src-tauri/icons/tray/trayTemplate.png | Bin 378 -> 776 bytes src-tauri/icons/tray/trayTemplate@2x.png | Bin 731 -> 1459 bytes src-tauri/src/commands/provider_pool_cmd.rs | 92 +++++++++ src-tauri/src/lib.rs | 21 ++ src-tauri/src/providers/iflow.rs | 183 ++++++++++++++++++ src-tauri/src/services/token_cache_service.rs | 59 +++++- src/components/Dashboard.tsx | 16 +- src/components/extensions/ExtensionsPage.tsx | 7 +- src/components/prompts/PromptsPage.tsx | 22 ++- .../provider-pool/AddCredentialModal.tsx | 5 +- src/icons/providers/utils.ts | 2 +- 15 files changed, 386 insertions(+), 21 deletions(-) diff --git a/src-tauri/icons/tray/tray-error.png b/src-tauri/icons/tray/tray-error.png index b33cbfb7019cbbeb69e31b3bb97ec1ed0cf932a7..918a9e377644525b8201f32c7670d25ce21daa7a 100644 GIT binary patch delta 753 zcmVvq_p7VlhcQ7%8oXdNS*wilC56 zq2QrZ4<33kBGzMblGc+~J>}v}AyNcyr9Eh^hf3O>q>voMqS*R@f}cr@waspladz|F z>?RSLRd8XFoi}gh_h#OkH#;K!@+1C}23)zEngCc35G3MtwSRyW{>$~!J1e~t^mMg0 zLQDXKZ{Bg<-N~ZrN+`s-er~1~`A`xJ2K;S!t!}JyuXFHvn35%?P;Ee);i{&TNo_7x zGs^~ykEcRyhHG_Rm97PU1nUeS#xArQj*4tTCkS^b;F`dT?NXfqCc^c%p_y>f%>yRF z$!G;kgR=$(w0{Z@42w8)*s96a*RqH{Qd)+SULI%!_jVK9o{AyogLZ6fF)S@)u*{1h z=P@#7E05v!5{v#sZZ2;*6!|_3j z)<5xyK{_Gg^r+Qfcaeb$s^DHei?K^~WEH-|FY*{3v43f8{$nC$?`^~5E!ghr65#7$ z`1pb0T+jxWD_iz)Z@&e7lLBUL%SaX{Ksz>KRVJ(tB9`Y00;#8&W+73nuCGTxI+@4& zH+fB_$i>+fb=AUjW==-Oadp6b<;!^M#&%)mT<~~S*5oqxy_@?tlL4N`b6Dd=h3`K= zSbCt~>3@@~rZ;rfidWBah|Vc>aALYas2}YMb23&}6fLmSj%u{~dPQ_}0Iy%>;BWv3 z`v^`KmErri?rJpK6y7uym*j~rtj0w%5gDg=FrFN;XtE8y4IkcD%Kg+J)LZt-7%imY zW=UpwUct(|(r~L8!JDSyKgn>dz#FDwQ{YCa*lH|zgKcOI+~_Qbn+T8B8Nk2WBj&)> zUm>h}FSvR!9pi)f+X*MI_~i5+N%u75mxcfpm#1BDyQMQ=crz+J>UTSD2&sqP?)h8@ jPfa@_4X{em-e#N=k7*s;>0Gl+Y2L& zwA%IVjKBHWZyo1ZGKzMb7phXJOQN@m=vWe+DWXTH8Ds89x_@r1aKn;$UlN6iS!Ypv zD}{K2y}67JJkM3kr}p6U^>uw+uj^5(rSIBpec$ayiZSM}cJBLeXHG++wUvwynOlc+80@63ic$CZ=TGw=rJ@_GRI) z$vO1Ay~Te|e@gA9*(}VsRV1-V%CkfVRHah4rB(>^vncd>QxpcC_c{swchkfx*tpf} zTFs(xIFA6)F&5EK%5q?|y=@(MKaT*>0E^>Jr!)s}wtsP;pM<~!i$!LCQZ<;KFiCRY zN9hg(f$az1lR^-d9Rgfh3rkTEefZx=cvN;7elcaJm=ES1aNU}1!(}(3kf^$-75Af7 z+-Us!Rty1+D&;Z?0zI@$iD=hZN<5iA6@DF8tJe5QDo+fF)<7Q0AS4$;ymYdN6rz)x zk_CajjBZh~f#e!Gdn45HsdiLKbe$T0fz&Pn$mLLkFHcSlyQufV~jb}`Cqi0 z=p>Z~rU{Y;G%9-iWPh08rL>#-VKOUVG9VLZrN+nvim{nHOk_LH;w+ZmBRAv*%?*{P P00000NkvXXu0mjfKB6ji diff --git a/src-tauri/icons/tray/tray-running.png b/src-tauri/icons/tray/tray-running.png index 856fbd780ede269e22f708fbe3e8c3d87ed5f93a..918a9e377644525b8201f32c7670d25ce21daa7a 100644 GIT binary patch delta 753 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 jPfa@_4Xbq&z{%2`EpHmN!7a0m20? zi@w+!s>F$%#1@y1G?JB#XU6mM&9|0yDe)y^yM+}lsKA8kGJm0ZOwcc1@vOQ`Dvvv~ zoVa7=&>9o!oJ*zSDE^z3-hI5gkmvP>#cN#9sWB`zx3tV93co)up|fuCZ3*{mg9?h*z6464NCHxyR;fR?M%`)yohwq>UdOP!!L*Wnstj; zqx@%>RG-Y3FY4UrFcXqKOl?f&(3+|FJZYa+j$v~O@80CE7yhV~_Uef_H~MV1Zu+4Y z-gs_d#f)0Dm8|oDwvM$3val`-H^u_sQWcYGyy%7`HGf`zp}IH(prjSRi%QFs;Upvg z`i#R$?R9Da98}u@O43`z2_n z766vk#8Kpu-u>Sul*&tc46&|*F?=$f0f&}-7)~FESdOlTLNS+g=0kBU@c>|RDilLN zqs%f&a(^1o{jdq!wo7-dh;{tYU&rOd0)VZ66TFY)iMB&aAP;E}k_S1uV#N{e(&|M@ zHcvT__D8{&gOb_8yJgKwN^=`Uy;H8nCZ(m9^~7 diff --git a/src-tauri/icons/tray/tray-warning.png b/src-tauri/icons/tray/tray-warning.png index 225717887a7832764e03869b96e5c34a48dd86a8..918a9e377644525b8201f32c7670d25ce21daa7a 100644 GIT binary patch delta 753 zcmVvq_p7VlhcQ7%8oXdNS*wilC56 zq2QrZ4<33kBGzMblGc+~J>}v}AyNcyr9Eh^hf3O>q>voMqS*R@f}cr@waspladz|F z>?RSLRd8XFoi}gh_h#OkH#;K!@+1C}23)zEngCc35G3MtwSRyW{>$~!J1e~t^mMg0 zLQDXKZ{Bg<-N~ZrN+`s-er~1~`A`xJ2K;S!t!}JyuXFHvn35%?P;Ee);i{&TNo_7x zGs^~ykEcRyhHG_Rm97PU1nUeS#xArQj*4tTCkS^b;F`dT?NXfqCc^c%p_y>f%>yRF z$!G;kgR=$(w0{Z@42w8)*s96a*RqH{Qd)+SULI%!_jVK9o{AyogLZ6fF)S@)u*{1h z=P@#7E05v!5{v#sZZ2;*6!|_3j z)<5xyK{_Gg^r+Qfcaeb$s^DHei?K^~WEH-|FY*{3v43f8{$nC$?`^~5E!ghr65#7$ z`1pb0T+jxWD_iz)Z@&e7lLBUL%SaX{Ksz>KRVJ(tB9`Y00;#8&W+73nuCGTxI+@4& zH+fB_$i>+fb=AUjW==-Oadp6b<;!^M#&%)mT<~~S*5oqxy_@?tlL4N`b6Dd=h3`K= zSbCt~>3@@~rZ;rfidWBah|Vc>aALYas2}YMb23&}6fLmSj%u{~dPQ_}0Iy%>;BWv3 z`v^`KmErri?rJpK6y7uym*j~rtj0w%5gDg=FrFN;XtE8y4IkcD%Kg+J)LZt-7%imY zW=UpwUct(|(r~L8!JDSyKgn>dz#FDwQ{YCa*lH|zgKcOI+~_Qbn+T8B8Nk2WBj&)> zUm>h}FSvR!9pi)f+X*MI_~i5+N%u75mxcfpm#1BDyQMQ=crz+J>UTSD2&sqP?)h8@ jPfa@_4X<)}No|^|0s`U3QV2nZa}WSZF@$b-6Z07oGR;18M{Wat3x?T6rd&;jn1;6DjLSSc!jFaK*2 zN&)LMN!M-GWvDpb4LH!^Y|(8*mFydl6zpo$MbvX!@pcBScq^tAlMszE?Rb#(W%R&@ zzX&TU&)P0T#9Jxvcy>DxwZ0B68 aX!#BHZe?&}P8}8i0000nMc$B diff --git a/src-tauri/icons/tray/trayTemplate.png b/src-tauri/icons/tray/trayTemplate.png index 8a22bf131b7968f50a572216e8da5e8fdf977701..918a9e377644525b8201f32c7670d25ce21daa7a 100644 GIT binary patch delta 753 zcmVvq_p7VlhcQ7%8oXdNS*wilC56 zq2QrZ4<33kBGzMblGc+~J>}v}AyNcyr9Eh^hf3O>q>voMqS*R@f}cr@waspladz|F z>?RSLRd8XFoi}gh_h#OkH#;K!@+1C}23)zEngCc35G3MtwSRyW{>$~!J1e~t^mMg0 zLQDXKZ{Bg<-N~ZrN+`s-er~1~`A`xJ2K;S!t!}JyuXFHvn35%?P;Ee);i{&TNo_7x zGs^~ykEcRyhHG_Rm97PU1nUeS#xArQj*4tTCkS^b;F`dT?NXfqCc^c%p_y>f%>yRF z$!G;kgR=$(w0{Z@42w8)*s96a*RqH{Qd)+SULI%!_jVK9o{AyogLZ6fF)S@)u*{1h z=P@#7E05v!5{v#sZZ2;*6!|_3j z)<5xyK{_Gg^r+Qfcaeb$s^DHei?K^~WEH-|FY*{3v43f8{$nC$?`^~5E!ghr65#7$ z`1pb0T+jxWD_iz)Z@&e7lLBUL%SaX{Ksz>KRVJ(tB9`Y00;#8&W+73nuCGTxI+@4& zH+fB_$i>+fb=AUjW==-Oadp6b<;!^M#&%)mT<~~S*5oqxy_@?tlL4N`b6Dd=h3`K= zSbCt~>3@@~rZ;rfidWBah|Vc>aALYas2}YMb23&}6fLmSj%u{~dPQ_}0Iy%>;BWv3 z`v^`KmErri?rJpK6y7uym*j~rtj0w%5gDg=FrFN;XtE8y4IkcD%Kg+J)LZt-7%imY zW=UpwUct(|(r~L8!JDSyKgn>dz#FDwQ{YCa*lH|zgKcOI+~_Qbn+T8B8Nk2WBj&)> zUm>h}FSvR!9pi)f+X*MI_~i5+N%u75mxcfpm#1BDyQMQ=crz+J>UTSD2&sqP?)h8@ jPfa@_4X3b;c;^acun+o;I7fQ`|30ZYAs#ErlPHkwKPCPs#K%5;7b<4fLVlJJj!v-)` z1h}^W%oG78Hh?!p0CQFEXr%~XvUapo1o*T8q>2F6qc}bGCLAdO+}Z$|iU6nkuHv~u z_$xW;D*#;Q0a-*+qe6v0y7a`8fQ7_#Oeb}vMpYGV-qoR`+7*^k(=iWHEH%n!=4PZ` yiljy*C6C$-0o0H(F%h7ZIFYqG;4DNie^%f3l3|{2yHk7s0000zWV% diff --git a/src-tauri/icons/tray/trayTemplate@2x.png b/src-tauri/icons/tray/trayTemplate@2x.png index 0a5c853e0c0434849513dd64201c50b0ead28597..666f52965b248a7268129ba3557f05ce17e6932f 100644 GIT binary patch delta 1442 zcmV;T1zq~v1+xo~BYy>YNkl4WV>1Y^Y)3#JdIx#(Mp^$Pl6A4;tCLBWss(8LFGX-k@j zFZlrPb9bEoY!dHd_ICC@B)hqRyN&=5dF0Lp1-DA~e{ zOcxN=0SsCIXgkS!F0NVLb>7+bn#t}H7kS0SZ{y&3t;Gt)jkv0cwt5_ z!2frj5Yj(#ZGT71wE!&5v#-1izwt{cIsU;|b6i;f=<}wL{?&VCUkWd*fV%_J1JB6X z1S%7NX=b8+fw!`7mb4L7vH(l_nS@{9^XsgG)*6i!S^<@4zuS-Sc+P^+?dSr8L5CmV ztxMLS&<3yreuQ5y{eVI!yhXy>n!}6K?kE5s!@C;{`F|0fs0v`az}vgPHi4I0fREwb zxqu(xnW_tH6L?1p@Ebe<7Zy-Y_=ATuaqNA<3Pcwln(*hW0f|41{}tXj4yZBw-u)C_ zAJ93tChqev{>|aAxHtA;3R3^Lg)bEYb#wtm_k`UkH`L%O^ytfKN(H@~Jj$)5MFv zdt)Lcj>mUXIQ&8&&mAn&Z{@-(t^=BVz7Phu5U?(lz}xo%3Ju<-*`r3ehTp%k!?uuI zmVZgVyKvv!)O>My_|8jRA#s#TD1?44gwcyBM|h$-7Eo&KmHt$-|1Kspe@y7}Gt-7+ z)K;#&ssvuK7m$DGQ*QF7&P9ZOFK~0b`cbJ1;JK4)u=BN$v;tS&OWJmIfgpj~H#2ZM zo-HXG{T@vpwZFv69v?}m!835%3)p`MeSZ;!@QC|Ay28*q5#fu)nwXolG>iSbJjgAD zZ*8-xm6$b@;IS?$@Gg4+=MSOAM~j3|oEHVK$e&}7qlc}hYby+X`YtPl$2>>5#Vql6 z60>HBplS(UrWa6;@bZN~h&=BkkL-5tqP)A)1>o8;3Lqzif2Egqi4g){ewLA134b27 zf|s@pX9m|;Jw|oU9SMpE4;#Gic|V`vU0s3MAEI!y%Ywhb1@_C`T*X07xq7_&c{8a| zDC}-Xi?Kb1w>^U)IM%CMgP|o$2#@Wy3=z#9HIE^1J>V-F4c2fxk6@o%N{I*$gPc9< z!{Oh5%!vk1=JU0LcRw1e(`3fnjDHD<8MDe!UG?zgU0@5&O{2G0naxDZoZD@^rZl)tj#7kIwpF07*qoM6N<$f&`zyDgXcg delta 708 zcmV;#0z3V)3)=;dBYyx1a7bBm000XU000XU0RWnu7ytkQjY&j7RCt{2noVmHQ51$B zXiPP_GPOt-qw&KcH5nTSd?iSMDs>zo6Kqv~*wX__5NOiq(a2Gcs@r zWgzCx%sn$_GV{U%A)oJi$b6i857eqvtCp@BKnGX^`oLRY8-LgZ{sKdJye+Tw<-HyG zY$??Q=7CjU4+vB86L%QY4u1yNG!TBD4PYj(@aKW|213XA2+V4SzX*IY5Ox$_ zfeTvUPXmF0kbmR&0-V)2;cXfS=gK-T?Xv(q1EG@g#8>!v17XtA%^JQ{lH>k=H%poB z7?%x%$V$|kq*5FBWgvi$-XT>3^}v96U))U^zNysG7R64|^@N)SO#9|~k^-z6FzlNZ z*YFKliV7!;AHbAL0j?S_tHouP0xTLZs>Lmr0z5ZhRDX-8rx~j?14gxY<5GaYfKe@? z&6lwn7%-}NLo`-H14gy@Qy0KNgQ_0_fdQk1o&?tn7%lWN*f(HQi)StcxNX3w77H!~ z=om1n#U+;lG^*YXBV#Gygz=J!Y=(qBHc6TjwkR6yXdfb>mJAs8#T`l%|agOZPNRcuCZiIo%>ocHXY@S=EwhtUP*$ zJ-NL-fTO98+CQZDS?_Yld~dAQE5Z($~{U>H%&ipMN_~P8BXZQ+WZIEff@<} qdG58mXF;YpoY(Mb)v8sC6#54Wgp#zV=#CZu0000, + pool_service: State<'_, ProviderPoolServiceState>, + creds_file_path: String, + name: Option, +) -> Result { + // 复制并重命名文件到应用存储目录 + let stored_file_path = copy_and_rename_credential_file(&creds_file_path, "codex")?; + + pool_service.0.add_credential( + &db, + "codex", + CredentialData::CodexOAuth { + creds_file_path: stored_file_path, + }, + name, + Some(true), + None, + ) +} + +/// 添加 Claude OAuth 凭证(通过文件路径) +#[tauri::command] +pub fn add_claude_oauth_credential( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + creds_file_path: String, + name: Option, +) -> Result { + // 复制并重命名文件到应用存储目录 + let stored_file_path = copy_and_rename_credential_file(&creds_file_path, "claude_oauth")?; + + pool_service.0.add_credential( + &db, + "claude_oauth", + CredentialData::ClaudeOAuth { + creds_file_path: stored_file_path, + }, + name, + Some(true), + None, + ) +} + +/// 添加 iFlow OAuth 凭证(通过文件路径) +#[tauri::command] +pub fn add_iflow_oauth_credential( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + creds_file_path: String, + name: Option, +) -> Result { + // 复制并重命名文件到应用存储目录 + let stored_file_path = copy_and_rename_credential_file(&creds_file_path, "iflow")?; + + pool_service.0.add_credential( + &db, + "iflow", + CredentialData::IFlowOAuth { + creds_file_path: stored_file_path, + }, + name, + Some(true), + None, + ) +} + +/// 添加 iFlow Cookie 凭证(通过文件路径) +#[tauri::command] +pub fn add_iflow_cookie_credential( + db: State<'_, DbConnection>, + pool_service: State<'_, ProviderPoolServiceState>, + creds_file_path: String, + name: Option, +) -> Result { + // 复制并重命名文件到应用存储目录 + let stored_file_path = copy_and_rename_credential_file(&creds_file_path, "iflow_cookie")?; + + pool_service.0.add_credential( + &db, + "iflow", + CredentialData::IFlowCookie { + creds_file_path: stored_file_path, + }, + name, + Some(true), + None, + ) +} + /// 刷新凭证的 OAuth Token #[tauri::command] pub async fn refresh_pool_credential_token( diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index daeefec0d..579e3cee3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -24,6 +24,7 @@ use std::sync::Arc; use tauri::{Manager, Runtime}; use tokio::sync::RwLock; +use commands::plugin_cmd::PluginManagerState; use commands::provider_pool_cmd::{CredentialSyncServiceState, ProviderPoolServiceState}; use commands::resilience_cmd::ResilienceConfigState; use commands::router_cmd::RouterConfigState; @@ -1373,6 +1374,10 @@ pub fn run() { // Initialize ResilienceConfigState let resilience_config_state = ResilienceConfigState::default(); + // Initialize PluginManager + let plugin_manager = plugin::PluginManager::with_defaults(); + let plugin_manager_state = PluginManagerState(Arc::new(RwLock::new(plugin_manager))); + // Initialize shared telemetry instances for both TelemetryState and RequestProcessor // This allows the frontend monitoring page to display data recorded by the request processor let shared_stats = Arc::new(parking_lot::RwLock::new( @@ -1427,6 +1432,7 @@ pub fn run() { .manage(router_config_state) .manage(resilience_config_state) .manage(telemetry_state) + .manage(plugin_manager_state) .setup(move |app| { // 初始化托盘管理器 // Requirements 1.4: 应用启动时显示停止状态图标 @@ -1666,6 +1672,10 @@ pub fn run() { commands::provider_pool_cmd::add_antigravity_oauth_credential, commands::provider_pool_cmd::add_openai_key_credential, commands::provider_pool_cmd::add_claude_key_credential, + commands::provider_pool_cmd::add_codex_oauth_credential, + commands::provider_pool_cmd::add_claude_oauth_credential, + commands::provider_pool_cmd::add_iflow_oauth_credential, + commands::provider_pool_cmd::add_iflow_cookie_credential, commands::provider_pool_cmd::refresh_pool_credential_token, commands::provider_pool_cmd::get_pool_credential_oauth_status, commands::provider_pool_cmd::debug_kiro_credentials, @@ -1724,6 +1734,17 @@ pub fn run() { commands::tray_cmd::get_tray_state, commands::tray_cmd::refresh_tray_menu, commands::tray_cmd::refresh_tray_with_stats, + // Plugin commands + commands::plugin_cmd::get_plugin_status, + commands::plugin_cmd::get_plugins, + commands::plugin_cmd::get_plugin_info, + commands::plugin_cmd::enable_plugin, + commands::plugin_cmd::disable_plugin, + commands::plugin_cmd::update_plugin_config, + commands::plugin_cmd::get_plugin_config, + commands::plugin_cmd::reload_plugins, + commands::plugin_cmd::unload_plugin, + commands::plugin_cmd::get_plugins_dir, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/providers/iflow.rs b/src-tauri/src/providers/iflow.rs index ebb69b7e1..c75f254be 100644 --- a/src-tauri/src/providers/iflow.rs +++ b/src-tauri/src/providers/iflow.rs @@ -104,6 +104,16 @@ impl Default for IFlowCredentials { } } +/// iFlow API Key 数据 +/// 与 CLIProxyAPI 的 iFlowKeyData 对齐 +#[derive(Debug, Clone)] +pub struct IFlowKeyData { + pub api_key: String, + pub expire_time: String, + pub name: String, + pub has_expired: bool, +} + /// Enum representation of iFlow credentials for type-safe handling #[derive(Debug, Clone)] pub enum IFlowCredentialsType { @@ -802,6 +812,179 @@ impl IFlowProvider { } } + /// 检查 Cookie API Key 是否需要刷新(距离过期 2 天内) + /// 与 CLIProxyAPI 的 ShouldRefreshAPIKey 对齐 + pub fn should_refresh_api_key(&self) -> bool { + if let Some(expire_str) = &self.credentials.expire { + // 尝试解析 "2006-01-02 15:04" 格式(iFlow 返回的格式) + if let Ok(expire) = chrono::NaiveDateTime::parse_from_str(expire_str, "%Y-%m-%d %H:%M") + { + let expire_utc = expire.and_utc(); + let now = chrono::Utc::now(); + let two_days_from_now = now + chrono::Duration::hours(48); + return expire_utc < two_days_from_now; + } + // 尝试解析 RFC3339 格式 + if let Ok(expire) = chrono::DateTime::parse_from_rfc3339(expire_str) { + let now = chrono::Utc::now(); + let two_days_from_now = now + chrono::Duration::hours(48); + return expire < two_days_from_now; + } + } + // 如果没有过期时间,默认需要刷新 + true + } + + /// 通过 Cookie 刷新 API Key + /// 与 CLIProxyAPI 的 RefreshAPIKey 对齐 + pub async fn refresh_api_key_with_cookie( + &mut self, + ) -> Result> { + let cookie = self + .credentials + .cookies + .as_ref() + .ok_or_else(|| create_config_error("没有可用的 Cookie"))? + .clone(); + + let email = self + .credentials + .email + .as_ref() + .ok_or_else(|| create_config_error("没有可用的用户标识(email)"))? + .clone(); + + tracing::info!("[IFLOW] 正在通过 Cookie 刷新 API Key,用户: {}", email); + + // 首先获取当前 API Key 信息(GET 请求) + let key_info = self.fetch_api_key_info(&cookie).await?; + + // 然后刷新 API Key(POST 请求) + let refreshed_key = self.refresh_api_key(&cookie, &key_info.name).await?; + + // 更新凭证 + self.credentials.api_key = Some(refreshed_key.api_key.clone()); + self.credentials.expire = Some(refreshed_key.expire_time.clone()); + self.credentials.last_refresh = Some(chrono::Utc::now().to_rfc3339()); + + // 保存凭证 + self.save_credentials().await?; + + tracing::info!( + "[IFLOW] Cookie API Key 刷新成功,新过期时间: {}", + refreshed_key.expire_time + ); + + Ok(refreshed_key.api_key) + } + + /// 获取 API Key 信息(GET 请求) + async fn fetch_api_key_info( + &self, + cookie: &str, + ) -> Result> { + let resp = self + .client + .get(IFLOW_API_KEY_URL) + .header("Cookie", cookie) + .header("Accept", "application/json, text/plain, */*") + .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") + .header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") + .send() + .await + .map_err(|e| Box::new(ProviderError::from(e)) as Box)?; + + if !resp.status().is_success() { + let status = resp.status().as_u16(); + let body = resp.text().await.unwrap_or_default(); + tracing::error!("[IFLOW] 获取 API Key 信息失败: {} - {}", status, body); + return Err(create_auth_error(&format!( + "获取 API Key 信息失败: {} - {}", + status, body + ))); + } + + let data: serde_json::Value = resp + .json() + .await + .map_err(|e| Box::new(ProviderError::from(e)) as Box)?; + + if !data["success"].as_bool().unwrap_or(false) { + let message = data["message"].as_str().unwrap_or("未知错误"); + return Err(create_auth_error(&format!( + "获取 API Key 信息失败: {}", + message + ))); + } + + let key_data = &data["data"]; + Ok(IFlowKeyData { + api_key: key_data["apiKey"] + .as_str() + .or_else(|| key_data["apiKeyMask"].as_str()) + .unwrap_or("") + .to_string(), + expire_time: key_data["expireTime"].as_str().unwrap_or("").to_string(), + name: key_data["name"].as_str().unwrap_or("").to_string(), + has_expired: key_data["hasExpired"].as_bool().unwrap_or(false), + }) + } + + /// 刷新 API Key(POST 请求) + async fn refresh_api_key( + &self, + cookie: &str, + name: &str, + ) -> Result> { + let body = serde_json::json!({ "name": name }); + + let resp = self + .client + .post(IFLOW_API_KEY_URL) + .header("Cookie", cookie) + .header("Content-Type", "application/json") + .header("Accept", "application/json, text/plain, */*") + .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") + .header("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8") + .header("Origin", "https://platform.iflow.cn") + .header("Referer", "https://platform.iflow.cn/") + .json(&body) + .send() + .await + .map_err(|e| Box::new(ProviderError::from(e)) as Box)?; + + if !resp.status().is_success() { + let status = resp.status().as_u16(); + let body = resp.text().await.unwrap_or_default(); + tracing::error!("[IFLOW] 刷新 API Key 失败: {} - {}", status, body); + return Err(create_auth_error(&format!( + "刷新 API Key 失败: {} - {}", + status, body + ))); + } + + let data: serde_json::Value = resp + .json() + .await + .map_err(|e| Box::new(ProviderError::from(e)) as Box)?; + + if !data["success"].as_bool().unwrap_or(false) { + let message = data["message"].as_str().unwrap_or("未知错误"); + return Err(create_auth_error(&format!( + "刷新 API Key 失败: {}", + message + ))); + } + + let key_data = &data["data"]; + Ok(IFlowKeyData { + api_key: key_data["apiKey"].as_str().unwrap_or("").to_string(), + expire_time: key_data["expireTime"].as_str().unwrap_or("").to_string(), + name: key_data["name"].as_str().unwrap_or("").to_string(), + has_expired: key_data["hasExpired"].as_bool().unwrap_or(false), + }) + } + /// Refresh token with retry mechanism pub async fn refresh_token_with_retry( &mut self, diff --git a/src-tauri/src/services/token_cache_service.rs b/src-tauri/src/services/token_cache_service.rs index 1609c967c..ccab11b5d 100644 --- a/src-tauri/src/services/token_cache_service.rs +++ b/src-tauri/src/services/token_cache_service.rs @@ -518,6 +518,7 @@ impl TokenCacheService { } /// 刷新 iFlow Cookie Token + /// 与 CLIProxyAPI 的 refreshCookieBased 对齐 async fn refresh_iflow_cookie(&self, creds_path: &str) -> Result { use crate::providers::iflow::IFlowProvider; @@ -527,8 +528,46 @@ impl TokenCacheService { .await .map_err(|e| format!("加载 iFlow Cookie 凭证失败: {}", e))?; - // iFlow Cookie 凭证使用 API Key,不需要刷新 OAuth Token - // 直接从凭证中获取 API Key + // 检查是否需要刷新 API Key(距离过期 2 天内) + if provider.should_refresh_api_key() { + tracing::info!("[IFLOW] Cookie API Key 需要刷新"); + + // 通过 Cookie 刷新 API Key + let api_key = provider + .refresh_api_key_with_cookie() + .await + .map_err(|e| format!("刷新 iFlow Cookie API Key 失败: {}", e))?; + + // 解析新的过期时间 + let expiry_time = provider + .credentials + .expire + .as_ref() + .and_then(|s| { + // 尝试解析 "2006-01-02 15:04" 格式 + chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M") + .ok() + .map(|dt| dt.and_utc()) + .or_else(|| { + // 尝试解析 RFC3339 格式 + chrono::DateTime::parse_from_rfc3339(s) + .ok() + .map(|dt| dt.with_timezone(&Utc)) + }) + }) + .unwrap_or_else(|| Utc::now() + chrono::Duration::days(30)); + + return Ok(CachedTokenInfo { + access_token: Some(api_key), + refresh_token: None, + expiry_time: Some(expiry_time), + last_refresh: Some(Utc::now()), + refresh_error_count: 0, + last_refresh_error: None, + }); + } + + // 不需要刷新,直接返回现有的 API Key let api_key = provider .credentials .api_key @@ -540,9 +579,19 @@ impl TokenCacheService { .credentials .expire .as_ref() - .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) - .map(|dt| dt.with_timezone(&Utc)) - .unwrap_or_else(|| Utc::now() + chrono::Duration::days(30)); // Cookie 通常有效期较长 + .and_then(|s| { + // 尝试解析 "2006-01-02 15:04" 格式 + chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M") + .ok() + .map(|dt| dt.and_utc()) + .or_else(|| { + // 尝试解析 RFC3339 格式 + chrono::DateTime::parse_from_rfc3339(s) + .ok() + .map(|dt| dt.with_timezone(&Utc)) + }) + }) + .unwrap_or_else(|| Utc::now() + chrono::Duration::days(30)); Ok(CachedTokenInfo { access_token: Some(api_key), diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 3d5b7236f..9196f9727 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -25,6 +25,7 @@ import { useAllOAuthCredentials } from "@/hooks/useOAuthCredentials"; import { StatsOverview } from "./monitoring/StatsOverview"; import { LogViewer } from "./monitoring/LogViewer"; import { TokenStats } from "./monitoring/TokenStats"; +import { ProviderIcon } from "@/icons/providers"; import { getDashboardData, getRequestLogs, @@ -376,12 +377,15 @@ function OverviewTab({ key={cred.provider} className="flex items-center justify-between rounded-lg border bg-background p-3" > -
-
- {getProviderName(cred.provider)} -
-
- {cred.has_access_token ? "Token 已加载" : "未配置"} +
+ +
+
+ {getProviderName(cred.provider)} +
+
+ {cred.has_access_token ? "Token 已加载" : "未配置"} +
} {activeTab === "prompts" && } {activeTab === "skills" && } + {activeTab === "plugins" && }
); diff --git a/src/components/prompts/PromptsPage.tsx b/src/components/prompts/PromptsPage.tsx index 55669c992..66548e42e 100644 --- a/src/components/prompts/PromptsPage.tsx +++ b/src/components/prompts/PromptsPage.tsx @@ -12,11 +12,22 @@ import { usePrompts } from "@/hooks/usePrompts"; import { cn } from "@/lib/utils"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import { HelpTip } from "@/components/HelpTip"; +import { ProviderIcon } from "@/icons/providers"; -const apps: { id: AppType; label: string; filename: string }[] = [ - { id: "claude", label: "Claude Code", filename: "CLAUDE.md" }, - { id: "codex", label: "Codex", filename: "AGENTS.md" }, - { id: "gemini", label: "Gemini", filename: "GEMINI.md" }, +const apps: { + id: AppType; + label: string; + filename: string; + iconType: string; +}[] = [ + { + id: "claude", + label: "Claude Code", + filename: "CLAUDE.md", + iconType: "claude", + }, + { id: "codex", label: "Codex", filename: "AGENTS.md", iconType: "openai" }, + { id: "gemini", label: "Gemini", filename: "GEMINI.md", iconType: "gemini" }, ]; /** Toggle Switch Component */ @@ -212,12 +223,13 @@ export function PromptsPage({ hideHeader = false }: PromptsPageProps) { key={app.id} onClick={() => handleAppChange(app.id)} className={cn( - "px-4 py-2 rounded-t-lg text-sm font-medium transition-colors", + "flex items-center gap-2 px-4 py-2 rounded-t-lg text-sm font-medium transition-colors", activeApp === app.id ? "bg-primary text-primary-foreground" : "hover:bg-muted text-muted-foreground", )} > + {app.label} ))} diff --git a/src/components/provider-pool/AddCredentialModal.tsx b/src/components/provider-pool/AddCredentialModal.tsx index db70df8f6..ed604fe96 100644 --- a/src/components/provider-pool/AddCredentialModal.tsx +++ b/src/components/provider-pool/AddCredentialModal.tsx @@ -17,7 +17,7 @@ const defaultCredsPath: Record = { antigravity: "~/.antigravity/oauth_creds.json", codex: "~/.codex/oauth.json", claude_oauth: "~/.claude/oauth.json", - iflow: "~/.iflow/oauth.json", + iflow: "~/.iflow/oauth_creds.json", }; export function AddCredentialModal({ @@ -207,7 +207,8 @@ export function AddCredentialModal({ {providerType === "codex" && "默认路径: ~/.codex/oauth.json"} {providerType === "claude_oauth" && "默认路径: ~/.claude/oauth.json"} - {providerType === "iflow" && "默认路径: ~/.iflow/oauth.json"} + {providerType === "iflow" && + "默认路径: ~/.iflow/oauth_creds.json"}

diff --git a/src/icons/providers/utils.ts b/src/icons/providers/utils.ts index 252bab937..741f724e7 100644 --- a/src/icons/providers/utils.ts +++ b/src/icons/providers/utils.ts @@ -11,7 +11,7 @@ const providerTypeToIcon: Record = { anthropic: "anthropic", codex: "openai", claude_oauth: "claude", - iflow: "openai", + iflow: "alibaba", amp: "amp", google: "google", alibaba: "alibaba", From bc10b355a7a507cb8f99f291cbceceba507555c0 Mon Sep 17 00:00:00 2001 From: coso Date: Sat, 20 Dec 2025 15:29:32 +0800 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=E5=88=87=E6=8D=A2=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=97=B6=E8=87=AA=E5=8A=A8=E6=B8=85=E7=90=86=20Claude?= =?UTF-8?q?=20=E8=AE=A4=E8=AF=81=E5=86=B2=E7=AA=81=E5=B9=B6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=93=8D=E4=BD=9C=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 clean_claude_auth_conflict 函数,切换 Claude 配置时自动移除冲突的 ANTHROPIC_API_KEY - 安装 sonner 并添加 Toaster 组件 - 切换/添加/更新/删除配置时显示成功提示 - 版本更新至 0.14.1 --- package-lock.json | 11 ++++++++++ package.json | 3 ++- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/services/live_sync.rs | 32 +++++++++++++++++++++++++++++ src-tauri/tauri.conf.json | 2 +- src/components/ui/sonner.tsx | 20 ++++++++++++++++++ src/hooks/useSwitch.ts | 5 +++++ src/main.tsx | 2 ++ 9 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 src/components/ui/sonner.tsx diff --git a/package-lock.json b/package-lock.json index fb18b6a49..3e48a06f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "lucide-react": "^0.460.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "sonner": "^2.0.7", "tailwind-merge": "^2.5.4" }, "devDependencies": { @@ -5346,6 +5347,16 @@ "dev": true, "license": "ISC" }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/package.json b/package.json index 37d3cfa78..c6037050c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.14.0", + "version": "0.14.1", "type": "module", "repository": { "type": "git", @@ -37,6 +37,7 @@ "lucide-react": "^0.460.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "sonner": "^2.0.7", "tailwind-merge": "^2.5.4" }, "devDependencies": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7660bd74f..cb0c72663 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3367,7 +3367,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.14.0" +version = "0.14.1" dependencies = [ "anyhow", "async-stream", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7750be4e4..27592d4c4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.14.0" +version = "0.14.1" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/services/live_sync.rs b/src-tauri/src/services/live_sync.rs index 34c3bf263..da91bde2f 100644 --- a/src-tauri/src/services/live_sync.rs +++ b/src-tauri/src/services/live_sync.rs @@ -27,6 +27,35 @@ pub fn sync_to_live( } } +/// 清理 Claude 配置中冲突的认证环境变量 +/// +/// Claude Code 同时检测到 ANTHROPIC_AUTH_TOKEN 和 ANTHROPIC_API_KEY 时会报警告。 +/// 此函数确保只保留一个认证变量: +/// - 优先保留 ANTHROPIC_AUTH_TOKEN(OAuth token) +/// - 如果只有 ANTHROPIC_API_KEY,则保留它 +fn clean_claude_auth_conflict(settings: &mut Value) { + if let Some(env) = settings.get_mut("env").and_then(|v| v.as_object_mut()) { + let has_auth_token = env + .get("ANTHROPIC_AUTH_TOKEN") + .and_then(|v| v.as_str()) + .map(|s| !s.is_empty()) + .unwrap_or(false); + let has_api_key = env + .get("ANTHROPIC_API_KEY") + .and_then(|v| v.as_str()) + .map(|s| !s.is_empty()) + .unwrap_or(false); + + // 如果两者都存在,移除 ANTHROPIC_API_KEY(优先使用 AUTH_TOKEN) + if has_auth_token && has_api_key { + tracing::info!( + "检测到 Claude 认证冲突:同时存在 ANTHROPIC_AUTH_TOKEN 和 ANTHROPIC_API_KEY,移除 ANTHROPIC_API_KEY" + ); + env.remove("ANTHROPIC_API_KEY"); + } + } +} + /// Sync Claude settings to ~/.claude/settings.json fn sync_claude_settings( provider: &Provider, @@ -69,6 +98,9 @@ fn sync_claude_settings( settings = provider.settings_config.clone(); } + // 清理冲突的认证环境变量 + clean_claude_auth_conflict(&mut settings); + // Write settings let content = serde_json::to_string_pretty(&settings)?; std::fs::write(&config_path, content)?; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 73f427bed..f136f095f 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": "ProxyCast", - "version": "0.14.0", + "version": "0.14.1", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 000000000..28f2d29cb --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,20 @@ +import { Toaster as SonnerToaster } from "sonner"; + +export function Toaster() { + return ( + + ); +} diff --git a/src/hooks/useSwitch.ts b/src/hooks/useSwitch.ts index dc4f18b88..ee6146018 100644 --- a/src/hooks/useSwitch.ts +++ b/src/hooks/useSwitch.ts @@ -1,4 +1,5 @@ import { useState, useEffect, useCallback } from "react"; +import { toast } from "sonner"; import { switchApi, Provider, AppType } from "@/lib/api/switch"; export function useSwitch(appType: AppType) { @@ -40,21 +41,25 @@ export function useSwitch(appType: AppType) { }; await switchApi.addProvider(newProvider); await fetchProviders(); + toast.success("配置已添加"); }; const updateProvider = async (provider: Provider) => { await switchApi.updateProvider(provider); await fetchProviders(); + toast.success("配置已更新"); }; const deleteProvider = async (id: string) => { await switchApi.deleteProvider(appType, id); await fetchProviders(); + toast.success("配置已删除"); }; const switchToProvider = async (id: string) => { await switchApi.switchProvider(appType, id); await fetchProviders(); + toast.success("切换成功"); }; return { diff --git a/src/main.tsx b/src/main.tsx index 27481e022..326daaa08 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,12 @@ import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; +import { Toaster } from "./components/ui/sonner"; import "./index.css"; ReactDOM.createRoot(document.getElementById("root")!).render( + , ); From cce3b8fb516bec20b7576aa440f184b6dca3c004 Mon Sep 17 00:00:00 2001 From: coso Date: Sat, 20 Dec 2025 15:53:13 +0800 Subject: [PATCH 8/8] =?UTF-8?q?feat:=20=E5=87=AD=E8=AF=81=E6=B1=A0?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=E5=8C=BA=E5=88=86=20API=20Key=20=E5=92=8C=20?= =?UTF-8?q?OAuth=20=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API Key 类型凭证直接使用 api_key 和 base_url,无需代理 - OAuth 类型凭证继续通过 ProxyCast 代理访问 - 更新版本号至 0.14.2 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- src/components/clients/ProviderForm.tsx | 41 ++++++++++++++++++++++--- 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index c6037050c..647f24b52 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.14.1", + "version": "0.14.2", "type": "module", "repository": { "type": "git", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index cb0c72663..1ca411650 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3367,7 +3367,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.14.1" +version = "0.14.2" dependencies = [ "anyhow", "async-stream", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 27592d4c4..f927f4e65 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.14.1" +version = "0.14.2" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f136f095f..e40d75a5a 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": "ProxyCast", - "version": "0.14.1", + "version": "0.14.2", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/components/clients/ProviderForm.tsx b/src/components/clients/ProviderForm.tsx index 1243df53a..ed67f76a4 100644 --- a/src/components/clients/ProviderForm.tsx +++ b/src/components/clients/ProviderForm.tsx @@ -640,10 +640,43 @@ export function ProviderForm({ const handleCredentialSelect = async (credential: CredentialDisplay) => { setSelectedCredentialUuid(credential.uuid); setName(credential.name || `${credential.provider_type} 凭证`); - setIconColor("#22c55e"); - // 根据凭证类型和 appType 设置配置 - // 使用 ProxyCast 代理,凭证池中的凭证通过本地代理访问 + // 判断是否为 API Key 类型凭证(非 OAuth) + const isApiKeyType = !credential.credential_type.includes("oauth"); + + // API Key 类型凭证:直接使用凭证中的 api_key 和 base_url + if (isApiKeyType && credential.api_key) { + setIconColor("#22c55e"); + + if (appType === "claude") { + setApiKey(credential.api_key); + setBaseUrl(credential.base_url || ""); + setJsonManuallyEdited(false); + } else if (appType === "codex") { + setCodexAuth( + JSON.stringify( + { + api_key: credential.api_key, + api_base_url: credential.base_url || "", + }, + null, + 2, + ), + ); + } else if (appType === "gemini") { + setGeminiEnv( + `GEMINI_API_KEY=${credential.api_key}\nGOOGLE_GEMINI_BASE_URL=${credential.base_url || ""}\nGEMINI_MODEL=gemini-2.0-flash`, + ); + } + + setNotes( + `API Key 凭证: ${credential.provider_type} - ${credential.uuid.slice(0, 8)}`, + ); + return; + } + + // OAuth 类型凭证:使用 ProxyCast 代理 + setIconColor("#3b82f6"); try { const config = await getConfig(); const proxyApiKey = config.server.api_key || ""; @@ -673,7 +706,7 @@ export function ProviderForm({ } setNotes( - `使用凭证池: ${credential.provider_type} - ${credential.uuid.slice(0, 8)}`, + `代理凭证: ${credential.provider_type} - ${credential.uuid.slice(0, 8)}`, ); } catch (e) { console.error("Failed to load config:", e);