From 3374339bff5da15875f529dfea7737e989041f67 Mon Sep 17 00:00:00 2001
From: coso
Date: Sat, 20 Dec 2025 14:53:24 +0800
Subject: [PATCH] =?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",