From 9b45c272b4ac6b7dfa9ea79a8edaae92cf75e0c9 Mon Sep 17 00:00:00 2001 From: Kirigaya <1193466151@qq.com> Date: Sun, 18 May 2025 21:05:49 +0800 Subject: [PATCH 1/3] update --- CHANGELOG.md | 5 +- renderer/.env.development | 1 + renderer/.env.production | 1 + renderer/README.md | 35 ++--- renderer/public/CascadiaCode.woff2 | Bin 0 -> 65732 bytes renderer/src/App.vue | 6 +- renderer/src/api/message-bridge.ts | 6 +- renderer/src/hook/type.ts | 8 +- renderer/src/views/connect/connection-item.ts | 58 ++++++++ renderer/src/views/connect/connection.ts | 133 +++++++++--------- servers/uv.lock | 10 +- service/src/common/index.dto.ts | 8 +- service/src/common/router.ts | 3 +- service/src/hook/adapter.ts | 26 ++-- service/src/llm/llm.controller.ts | 8 +- service/src/main.ts | 16 +-- service/src/mcp/client.controller.ts | 44 +++--- service/src/mcp/client.dto.ts | 4 +- service/src/mcp/client.service.ts | 4 +- service/src/mcp/connect.controller.ts | 15 +- service/src/mcp/connect.service.ts | 29 ++-- service/src/panel/panel.controller.ts | 28 ++-- service/src/server.ts | 16 +-- service/src/setting/setting.controller.ts | 17 +-- software/src/main.ts | 6 +- software/src/util.ts | 10 +- src/global.ts | 20 +-- src/sidebar/installed.service.ts | 14 +- src/sidebar/workspace.service.ts | 14 +- src/webview/webview.controller.ts | 2 +- src/webview/webview.service.ts | 8 +- 31 files changed, 320 insertions(+), 235 deletions(-) create mode 100644 renderer/public/CascadiaCode.woff2 create mode 100644 renderer/src/views/connect/connection-item.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 76cae35..ac6a847 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # Change Log ## [main] 0.0.9 -- 修复 0.0.8 引入的bug:system prompt 返回的是索引而非真实内容 -- +- 修复 0.0.8 引入的 bug:system prompt 返回的是索引而非真实内容 +- 新特性:支持同时连入多个 mcp server +- 新特性:更新协议内容,支持 streamable http 协议,未来将逐步取代 SSE 的连接方式 ## [main] 0.0.8 - 大模型 API 测试时更加完整的报错 diff --git a/renderer/.env.development b/renderer/.env.development index 254e7de..7d53432 100644 --- a/renderer/.env.development +++ b/renderer/.env.development @@ -1 +1,2 @@ +VITE_USE_AUTH=false VITE_WEBSOCKET_URL=ws://localhost:8282 \ No newline at end of file diff --git a/renderer/.env.production b/renderer/.env.production index 254e7de..7d53432 100644 --- a/renderer/.env.production +++ b/renderer/.env.production @@ -1 +1,2 @@ +VITE_USE_AUTH=false VITE_WEBSOCKET_URL=ws://localhost:8282 \ No newline at end of file diff --git a/renderer/README.md b/renderer/README.md index 7493165..2b9f1f2 100644 --- a/renderer/README.md +++ b/renderer/README.md @@ -1,33 +1,18 @@ -# test-vite +## dev -This template should help get you started developing with Vue 3 in Vite. +如果想要部署到公网中,想要通过密码认证才能进入,进行如下步骤: -## Recommended IDE Setup - -[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). - -## Type Support for `.vue` Imports in TS - -TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. - -## Customize configuration - -See [Vite Configuration Reference](https://vite.dev/config/). - -## Project Setup - -```sh -npm install +```bash +touch .env.website.local ``` -### Compile and Hot-Reload for Development +写入: -```sh -npm run dev +```toml +VITE_USE_AUTH=true +VITE_WEBSOCKET_URL=wss:///<路径> ``` -### Type-Check, Compile and Minify for Production +使用 `npm run serve:website` 进行测试(服务端使用 ts-node src/server.ts) -```sh -npm run build -``` +使用 `npm run build:website` 进行打包 \ No newline at end of file diff --git a/renderer/public/CascadiaCode.woff2 b/renderer/public/CascadiaCode.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..ed0335c2c1d1dbdf52330e628d33c3bbb67f4a37 GIT binary patch literal 65732 zcmV)9K*hgzPew8T0RR910RY4R5dZ)H18BSe0RUeI)h1>D00000000000000000000 z0000QgJ2u31RQ~;G6rA(n{Eh#773pN5fBQ9pICoTnB;t z4-ADfTPyjp2Z{~Z-5yovMk7WA6#x}vr$|XghaOMq?`O}@Z0v#DtZu67=x9;!){Aff z9k)Fa+%{n6=IZntN&o-<|NsC0|NsA6mr0nek?dt8t-ZDr=W(8|p`kAgP~bR_Dh+PZ z)u5WG8f$Apv_x_10@g>{^h3<%XqqUB`#6ez%(?ZBp7SD>WrPoXpGVQHmTL@h(l=tJ zS){Vv^ z^Aty3(Doif7~Bx3uC@?6R)kY9A_t!5(>rWh99%5ZyP?xo?-+z5^oz;_&g#Q$iS5+Qk*lwC zRxj24yS_&E3JQCalQ897u!glVRe!I1dvNXh6oS_vhCd!g6f%SL#3lLK%$Xfk;V20e>MmlY!B}#90QGl1f>rS z2T=^8TOe0#U2E0V{FhuHZx!P6r1v|esV!Fce3B;KU53+6CIW6IJ`b%H)q7>5b4P{0 z$vWN=IH|AP|B?%Mg11Aj4m^vG3ip)!F3X>9kD`dEoGY;Y^FqC+w~@Whrj90ozv{%c zmuq->%MapBgcuZ7$Vu8z&pj!#SuIh2QHTF#v+{%iZGDZ->PNMFUx;}T|7vTQ!cTFL zFQ)%Xml}CZ_Wv))M{*Hrqdt(yMrw}%blmzYfgl`>v$9zXVj~X1*F47kV}a-8!J&i& zDvoSF&(H18z3=U9j1-9+C^b@SqhA&3$#rWnqG9o^aBVvs6jGUQ~GmnhSn2`~4 zxdkTu`x!EF`{CyQKMp`CK)4g(4$7BfG`*m<%AwE!T=Ewb;c_y zD#nLCD&~jq&;R}Nzd6_1d%aKn0Taq<_~ZC%*}(v#Fibekew0Uc-~aDsJ;|CT4kTQ7 zh-iQrUlYrIGcQd~Z`z6%rMA0cD&&Ih4Z)Wu0R-q;$vex*4tIe;8gpUV@I;)@R*rxnqF_r z2~cSRA0dmNPr!!~_WRrT;G=dT|YVV^?Lb2jLL|_6*6t> zgky2&ZWH@I38h^gI2~9+@tJ^>6^y|G5*m`e9 zV|^dj$Mr&eK;44KmV0$;zUBB{U`wk-t!4v?Gl zCJh1rHf<`|ev%TN*Am>06tbnD;ks{)grPhO$HXH9Cw(4kKZAA(#V50+|E9=jfwj@Y z*!`DUjyYy>s>onWlxm&l_iw8{<54`!0K|9`34x2-}TK{`=(`!|0U zf8@{V342KLPkA+Q74rVQ``*K=danwh0FniO6ai3yL7@mLkdSPn0McCqX*WfyrzzV0 zM{ax0QD@oTbB^sv04ds3a9a{BC)pk3pMoc4**`^lQi_YsY*&+4lNY#46(Ehl>zgP)RpZlJM#=D_EY35JC`o1Lr!pK=dRH&+FOG5 z5U`T|bLCup>x}ro!zW*58>fJ~+!TfV=coBeJw^6z(os+)FwB3P$^u{rv~SVz>Z$zR zU|NGZ_i5Honr#(s01Cjt0hENr|8G`5`>IkoeS1Qk(|=t6hf)0(#Yea=G3KRG)v0s{ z|4rV^kQugST}C+An{81H!fZm~kVpyqf9_KOGUax*KNE<;E-fJ)UI1my&G9NFJ1ar> zsq0IT!s_=fQ+g_MWs6fffFbOs4@|<0Se{nvpQ2M*W1$t$^&J_zBWR-RjCY5c!w17( z>k)F{|8uWNbtbEflnQC-ki}YSo{O&SEq_s95OII2v_cpT?3O?Kl78<=Z-7>o0s~+u zv}(Fj)2{7r8td(WN5gV(KOg@NzZr-EUd^1{{Z5!`pjCqJ) zjBrz}FhZ!p2;mALge#02LiWm@=U4Xls@YaRZFhu3M2tWn5D^FjB4Pvrk=jBa5c|yj z_xal`%(Wk{xxD4giin6DAp{W#B9eCh9wll`klBNMa~0u-Z-1$4AV6&1qmKVBGrIX25|UAERy9PfW`Oh5UP3mAMU@8 z7=SR5At1)cChRf79~8k_*0k!=;Zc2TT2 z%9KZynyAN%X3fz`5F+A`lEznqAtw(7MX0G`#4IuZVj89aVg_acVnb{Mh%M3qu}uKP zV}d~J91g^;5kSn23}VlyAohs?;>ig>9FPpeGm?XNRxltAjuVLgi93iFh6v(S2>}UA z&;SWK!2l!-2tz=^m@oz;%-R77`vOSuZ#RUJH-XRuMFOE2lpYY;avhMi9SPD7qe0qf zEJ*v#fdB#mzyb!T__qyEzcrvm6BG&70%==(7!z3M7(zkQF6u9S?#byde*YnuFCKX3 zi!;8`on7+k-OXxa7rr&LI=Q%bF|E6c`MRfAuPeTZ{A7fJctRlfcUuoeJAc|Oh-RGt zV0*6tmHHFp?w$-F7=Zl?t>nHxS*$?7jVFd2~6T^ zxZ%NhOko-q;KfDw@C`zsdJqr92Q}k|rt6U*fdwR2l-g1USpW!s5`*H3cux!mi&fUO zH2=(k4@6mG9D_XgT|Tt)z?+??(Oiw@Vl?4k zYnjrtzO!P+o$BJQ^>?ooeC}|nDeFmN7t~BdaFbJB zIjF1AT>Mv9wZDx&6CiraomyvRZiip6H)yMji!xlY$cLRp%1oCt=lws<7P`g)+{6y1 z<@Vb*8>^hN=Tsi4nctR;XTf~a zAnR9>zw#K3fJLj=d2W}*Se)lYzr%{UXS?7U_v^q6Ac803b zC9!}}TSDy!^>mO5!RDs(B1mzIu(NhO#CuVgLoAjDncO!RCsM@(+V_GSRh5Bh(=0I; zIcZ03$}*s~`w-~y=-91|xTi%@I18iK6RGuZ+A@)~GU$m0%R7g%jqE>y9ZpP$p7~?6 za?E)YLcv1`T(s5#OP*@qG&6X0QJ`ED;`T)k z<%Z=R>_^!-MVXw4_QW6~9tlwdvSOJAb;O+7en*iXNa-Y*GnGj8mX=s@(YiOuRG>Yx z1sQHD$K~a0|JDMAWS@Ez_t5h5iX!oOZX3F2K`ZB0{0$1RFW(V(SiBx~0c)iw)#tg^ zz|+Uj#_9Sg;>yAQvgXqNb=;r-BMMAhz@BEIbg+)O4m9A2g13q!AG9ihS? zxicVJKhShYG!_sM2G6oMi?izNR20IDkkWxt0JT6mcMWwzozJ!8=FQ?CV_n(Ci2>Ki zYayEX4x#w zrJ1FMEEa@j%q1oY6Li2nDK79N1Zn-K7;aF<&%&`B6kw2NX}q$W8A%9m9nkSCA&X>; z60KbxYR!KNkPIlW2^iqr%g?YV;kp`F2mz5F3WX4eXTPjKf zY|xC{cfjw<@u_iJy^8wEz_9ua?o^gQKaloM1&EM9K!Jw6)WL)yOqjD` z$H7>x+<6&iqA8{cFjJ7Z=KEWS#g+-R+BzF;v0a#5_S)~DBaVp><)jK#YBgxmqC=M+ z{f1mLZVJ&gdnQaaLqP9N;k%%}XARejAP`Eaqi$XG>2qj2Ex=rgBbCvRN^Xl`D30P{)F9QPZ z0?(S$p9R*^#J=V~mPD9rudqJ~oI{RGH+p0o)3u>)kY1?S4(rs<~lJ6iot+ZiJCmUwEsICUn zhq|Ym&DwAju92|#J@^`dC-xGZB%p@GlDCl-GTlS<^iD@_xKHYj_5;Il{W+?hl2fs_ z?bH%9=RpZ^Zd<0s?X;RHL%bwY&J39W1fY!`^1li)vxRIq``qZN=;BC($nmP;UK^(` zbxH5?pmQBx#lJSKni}qK3gGl_AExlSFgg9#k)TKrHL(FbSbH_Z27kJ~I(R2DZ-|#>7eSSC~1wn96FcP9d zFdT;Za6Zh0xd@KLku#c!%i%$q?IbusWg_gWNle@7Ri_PKj zd=Sh@d7eL#+w=7TUz{y;#axjt?v>MQDchxnH1eKuk3q9&E*&)ra<1VR^pw78U}4qC z>L!d?lL%SRIOH$`7VW9Yr-rRb8$DUf)wnlt^|r;q@3r>cf*r{Z<;LT=n$ z@ur`aK0WQt`Bn90ed9jbzLopLFd1H`_6_@0I1hL_Oh(`{YoFY>Z7(OFfM=GfZZ(lAWC7CNKFZ2xVI5feDk9 zX}fa&JGt}F#nruKYfwZ{lpY|=gJ5SBjtx$usyA!Z1EMUY1WO&n8zb2o+q~d4OD(g~ zTI;O$e;aJ{i!BOmv)vB6?Dl6Kq$tHHNomSbo{CUYnTd>rH|J-n^1wrnJodzqr=EH4 z*bA?o4|R$G1ONmE5If^6#8Mn#4v7|{Ri6<|EZ6Y~br_&&99c;zsOcEF(NH5zw%=#2 zhm{3x*Z{L~(Gv!5n|K)tRgE1o6{#L!vJ|TsJ7p_TJ9f!Y8ZmauRTeq+$WtCQ_R3cg zJ@zR;jT!qt;fY9Oq7t2$#HKqv>CJ@?s)%9X!`wu(dmjsJJ|049R-YFG}-xf%Zl;0;IKrf>KG+X=sXSt$Jdmr)zw z@Qc)O_p5*KOT)kP;b#2Lz*oaV8f4$;M-bs^xGrp|L*pOt-vP_y30Ks9W4#oct;=^S z<`?8scp~F@5awru6_9h6R{+u(2U$U!<a)5r=^YlsYfel_m$shr0c%~K#C-@ z(H5sX*0XQ*%i_>gB&L!$i{+qr%xJ?~yvvQWeODjmrr8^9sy?1#Fn?xSkes|24&~Lk z+=6f#!vV)vN1HeioK|JOF*EV`o>w$?~!{#AXr;R=FJ~ z{l<62I*D~D%%|1@oRMnRiyVR#!*Lm&C5bx?C77x=o*Y|J+^PbO`R6}DK;*}rd6kQz zVaU|btc1|RiHaJJ3hHi9%ZkgfF}3TP9fF18Sc?ELZX*XnsN@7Snn}Dv0dx7sdjG}; z+9954t9$iLCO%+`s%?%CAPBp@alUexRoj74Yc2VA^Mj`&&_HV9pH66uua+2XA*MCL z$bW>7KGgpvh^(~Q9=G@F3rt)~DuMkyPG}k((#7Vc6vo5Tc9?Ju+aEI)&1AJ5rd3FH zSksR=$jM+o#JG)Sviv=<6Pq_Dl}uChAoGBvFv(D(WN*X#hOzS44?-fUb=}EhT;{83 znk5Bb<&VpO1Lti{V48rA6Q(@KFNV!Hik0NURg65+Zbk!^?C1JYBQ~GF(1BINZUdvT z+P9R7MtDY;H!PzJ=XM`HveH`nG_nJ6ElG%#oY2Gy7n3QQ*u*ld8cw=sbBEdu1+@w^ zwP?sX;FJ*(jN52iWu@^WBx!5X0+kUFbez3JifoI+{KB_t^DRu0 zYGIaY20jBPe%89TH}5)A*?RerD0OD-C%qN|F!;u`BdjP*Z>=7}H*DP35nEU{%MkTpZn zL(Ww`>&)O2BK>JtGOx(uU45e7>M6Dp3+w zFrS3h5;3j`a-g&#dV3sLvQ#?BT@9pq9WOl0%0LdxNq067=%u&bCpdw4D76|{%KgUuWazHEGjxZu{Cxj2*qf7NG$My z10fXKDsn6|xQKvY4V1-+5}#0`2_bGM51RPEUI*OG z#9nID#ks}r2Dfqc>=?)Vh|}P!tQmZM(Dt#Q2bR~4!imbj6_qwQC-5d7wR`!h^Bv<+ z&KESDIOO>DQePFOs;N5d6-VHq0fQxxt!fNKMXW2F0duZ$Ip+$uDD&dmu#KObRsACJ z^8)IerW`m<;D%Y}q@+y3{H!nBJB(60&4JRLWK10yOCCU(E2VAdlgNeYhDnWC8uoS+kby@PC}NiYfFv-l(}THF?ILPa1o>~cHe_%6_xv)t%z6V|8D-*H5_#1k8JR|4mc zModKJ6y^m8oCX6A_(I+kqi_Zmxqp$$>K5U^+|*%p(4hhAj4{ z>oD($b+RjA2#$J|M3!P+PZd!eUkiJ{MPo3CHcskl$FE4_jzQHDPpn%eDfB-Qx=~T5 zriA4+$HtmTb)^F?4jL!AbW+zx9s`Y1A!-tY62vX;OU+)QCBwy8iCV}9}%30=LK zv0?lN@lQKL{QQtLtdGAvakczLQ+(a6q2uX}Twcm0wPvoB5}O2-nyan?E|ZA%no?Md zAAI8LAmy3$$(+w{a#fY~#4y*DprRv8(?-UZqXu&oL^WX^`fLgmN1@XJmkQM<&^)QD z)eaoGC-O^GjsAdPizz8!I8&p^--yVF+72vP@?1_+Wu+)3*%UlqYBqPD@KD_H)#D-$ zUz93C`nKsKZ5tfbdvK#p9gaBd@?8@^c^;0Vvq9NwWx5Z#`89q@LdxttR z7aJL!$VJhw^?R{o^C3DgBr6eh`4CC|F8xrj>NifUu?oH4Pp#92fauuN8m)Iz zfn2CY*=7sD*XFgLkd%eQeywI{tAFr`Q*vWIKl3SDw%_oC=GuxfBTW+#-i3J@wZ-5H zrCtZh5*ioknAFt@-z!pnaX!6(=`@|W{cSOd2Q4c2X}QKIzV(G?$~vr=@U$DB<~czh zdj`O%=>a-2?(Fsq+l{ehn|)GMJ~P#JFsh(+WG%V_t`38WaU0F*`TLsEu+JVu;zEpgLBxKUpH-_@-Z+`453F@PMpG8bc3cYV5{L<`3q;FE&VM?|c zJ!e-~FxZ<$==)w1ddbKqmT`e6l%G<#YEnxQV1g5xDCq*2hauaF&ZW@pfNKM^GH#<; zJAY5^yBtk7ZPa$7treak>87O#cT6JJW4>)II;jRNs%ph$CsAH&R)zW>e9|oZrZ=DT zbN?`*L~P;%kp&3ML@aNVv>wcN#kL!*4TM~ENY@R5<*UNqeC_$SX)8qZTMRuciX(xz zmLy;=Cp4)@uEIQa7QIAJY<9r)0qYpI(d+_$PgG(v(iN?n)RF|W#0gE5G%n1em>oc4 zQRH*L(Se+d+h}ROnH09i-kB&FvX%#-1QK}02e4!B_qN+Fs_UG2kA zk=Ad%U!bosRk^K*#0iR$+NHJwR(51vU!47hgWM?kCB|*EH8%cz`w&)AS{@FoPXS2+ zEtaPVd{%0Wb*Qf46h&1gL|uCtkgGVsWIlcBOov62ul>+}-gF|_1e~y7dp8x}9AH~a zO2;;`4^*a^4l$U60A~a^$1&AQ>jYA$_K+^^mnf4Iy1)xs`%8v$x4YhUcRr7uO9SKz zaOZUlcX>A$ZM|!rZL&AAGh3I>*)fW$8pt&&63sCgQx%Tpp);ruV*YEWcw+ALGW0VA zHP7GW#AAcb=q6E30Hz{lxj41Te9~r@lq2v#efu7B|Cc4>egJ zNpZ`@aJ8&(6RksSfT6H;3O;*9f(-e44z9B4`_dSoTfNWW#;6KA7ig?%*U|aQr=@2i zj!{J$aul)I>BF|mi5ll{-9APx*hH`GOhTA$DbO){K_BtUsHn&~TJnPud z9&tla;nJYyY=xweLQh$`DsV0tmEILAOZqj18yMchsB>z9p+BX48^g3?0w=zk!!r`5 zidYQS6>WCcf1}sq9Ivr;g0J}{w?-~*$u{sUJ`&Yrz>vB{;qK%nD^2yrI^E1+czZ2K zzd3}TPP3GXz8p!M2tyo6#0Dyyg0p)RjceDO55)@WY0_(hV?-rO?etrK#!Ax@h6};~ zl(Pg72!|-fmD*{d1kP&Vo(zS}=zVfbPud5M@_hmhW9e>PlLeJ(H_`(7g1skffrjmg zng~L1)&uPHWKIlUdevD@DMVfbbyqZaWnE3 z=lU9}+JqPHO{E^@5_%bM?v=1;Q!iS(7wSteVESEjIQD0J1TtTfjXM^e_oS%&3YMX} z_+oG`T`oAC396K1YxhDN<=vNf3!AXK)9mx)Et46pg?&fK!Q zT{)IipLsJnU(~Smri)}_PPOc&ps6nc-lA%$rF*ZzIa*UXa~c1t$*4E&4v~!_vAQH0@YN~BOIS4~eoYz$P4#=- zwW3Xj+iw6M{+)SC2UrYjv2=-T%*m?E>(~PIC2-whkLJ9_i3v9WFx4`Gzatxf8))pb z0xZ3Y<7IqcduXe`fqu;DvnR`ZF2?}`i}hZaPjhJ}iV_Muk14|;mVfrI#&n3N^g@wI zI$mC?xU$>8SEfi*ql3JbY<$(Hm?ypV)@xcD$5-tb0I#lO^nTEQrut!>t{A{ryP`br z%=-FQddTma;8!rc=WYzY*wlRup7P6vzb8dyn-Nb=a+AvOFJ8l=fnUbc$MLm$F>+<3 zWlV>1^=P0^pbZ|iVV`i63#$E`#wCt-Q1vzx-E)_1n+WtsRIT_%IDP>`yszB`1qLy- zsjD*uAoNp9?Y7(*FF!2LgQJ`pK5RoKve3X9+ZLFLwfzc zKjwwzvPuoAY+o1A{ikTriMo#aGZ5$)E|cR+E7#ELv~ymC)tPc~O(H;w!^o`t0Z%XD z;mGl)8?7axV$WC7zSgB9t=s<(MhkV1{Qxk zgjt03FkRQwmm8jQL?!v;UP>J`6S;?N=^#u+wsGK;QPPOYQWFR#OzB@sE3$lAx9`{J z^gqvi`Y{5n>SI_q%>=zate{=tnW;^6NujgnZl7%;bVQ=sy-=J`6q{&1(Hw}Pez*R# zw?UgZ#)`x^y9+O6%3tW3r;dU zHY;n;;cJ_6C!lC#C6ydDR2I<&ydc+yZFp&0JT<0CrVfZUvb6>S9tWtma%_oTWQf}* z$z@+83T`vExn?s+XB8*U@YA&XET=t2Lf(sD^fh`|VD=8xXO;xuVCDy2dqia~lxeVg zlyXH6mO+Q58Mpx|K}n0q2ojbN4HW}Gh zvh?ed^H%MuJIzQ-;#>$TAiNAV6&1Yr%Qnwdqo*JBEDNKb8%rS>$Ez>)*H2P}) z960*`rO4SKHwy0D4$B8BBq1*N8G{+W?RpS@fsf)iFjX8*H-drZ?NRy4%I>9iFOm!m zwzQ$JRX)M+0Pe|fQR=wVL8-Uyac-59!sj;4Ycp>{?7Zcfts*~sLG!}qIdbn^-26z- zn^NZj!{%ekKiBTXldSF4M$tEyvAO!4sReAsg}9i_A5VZL{BMx_~)SNV|xz&rq-#=(Ar^_pqfA$mmln+~Z^S6K>>{Drv}{xgmey0-TrYJDf zdagHi4mJXb^BTgP-VaaWtoj_plZ+kx%biFeSU;#nE_AZ;H%jVXvIND*tOr?Mth9%G ze}fD~vYbjYZT&+NAw`G|k-6-_m`yv8$^w)#5|lG0Ft;=EFAez@$Q>(lSNGwKUK;Bw zqbIT7ip8y1Uhc8am-!D#E`2P0EyRi(4=B2#g!hiRBqlASzuWNl)( z((eR=lS0-Cbi&dKfIMF5S%HT<8d+0H${i5I$}=z*D>d$cGvfoqet=Y!Y2PgE7-(8M z7;460VC1#^|L=eflT}OzOmP1cAmtmI;RC1%=X(IpG@pjwXTo*EaFWu)VATFH+|A^l z{~o?U0hHeV3WR)mBmOeGUI@TC@A0>QUi`lUnY|AHz(oV#3?@?%Li67P$d*=svSMK1 zOb><_XPW4R(OY_P1U6r;Gxb-uuT<1=IuJKuCgPu8x-90eWVIQSLl2B*3ulh15n zIt*e2#$fg`A2Bv&fw_SY2!$R*SkyuU0wg09O{14t49l?Z@Pqsb{{FF03#+wa4>=U>eI$Zl4e=es+Q0B0B!*v z2EjL0flsP!aV;1vcM`OMXFwM?33?!i|H{vAA5b4}EFlt9i3)aL2AA*6OU=w1roY$4 zP_4&OzZnsp_n5)}JPH8h699DdDJr_C(}#E>o~ zr{IrLo`U4%G`HU0Khb|U{i!bBBjUm}qL%-TDK{z<+yB*EGHIu?BE0xL^hPE0l8(7C z&%X{O|L>l%lJ_J5wS`IK9T%$q{84V?UxnN#>-pIv8#l6r7{Fc(rnpqNAT4+cQ^M<# zE(E~WagoeW+6N%T14!Bc5M2Tg*GO8#CSqgJPtkYLAb_au=oA1za07^x+}{QGDn8^( zi?)TyX@%&!8UVtOD7l5!fOxbRLFnlNs+i~|VURM^t^5npVnYR+?=MR&PyYfyGv*La zFaS!eRdwFIOLKFaO3(~wMl{yoqb|Av)D$Yn5TJ@M3Du22Hl!E1ilqL#{LA^zmcaV^ za`jCDaFd_Ul{~Hh5qN@fHBU1UM%D6cE~h)A=}B*Ot5(m;hxm-~F9$y}1PZaxVxd;z z2p1{JDYY668Zv6kFznhjWO1= zOrGdW)u#!<-eC30%1w=TEcE6>O~acG_T*V|E-3j<~Ezi&o<(IZ`Ft75}SHr$MuR zQtf&pKpPN&FneV|#z3)(o*VVhOK;<}@C2jPAdo{sEjxS;S~zNDuZa^8XJV5Ka3kZZ z&t!wx+WcY2G`~$ZEP%o+YBQ<)n;`rhKg^+ncL~5eBZA_C1%Y5uqR0rA1YucFmL|^f zV5~@jRUufD46Bp2%i3f)lpz~akE3?i9+SB6(Ik&&@_Ay$s%$t`jknVs~0x&3}ji{^Sm4goS$68Xe7q z^WjL$ZL(u|9jr5X?)lsAPM!Jauk*8fa&AUQKlC6CJ@eR6`8`?fZ*u7p#bR@LHd!dJ zLy4%pxP2Ib(k(WaDBe5gJ@eEHFTIk6>$Qtg-15+AXPk9Tj93X0B}w+5+wQpMuKON% z?1@L7d*iL*OGaZ=8-%okG)WmC{KREFx_tA~FV$-NR<7Sy0|tH9tIrQS4a7QKP~knf zbVEK+_-NQCN}s7@P|Ku|Wke30&A@huULVG_Ih0@#JT3|ol}uG*qL=|n8KR64%ITv7 ziCp~1M@In$3ek|qz*iG}GsSlvI(X^gt(%V?o;v*%FU*e{!cu2fnru#qttqoD6}G3! zmeA};n|Bz1TD_)B#GK_BMk}$0a8LFPOxwsIdfr6ogy2iFtjPn zkXf8QJt~xw@+lX;pzk^Ok*zP;`I@CS67FQ-#JrW9v&+K&Ic1ZXe`DvCxfk)gtv2o+ z;WPETy#ajgyi4?a2@)evW?YnQ$KY$((OvXW(9NSPv^~?bO~8B(bh3CZB}QR1^j( z4du*;#qiv?^We#gHy^=*5D^3l;4eg+cnSP;`n83n(U~kZhs)y&h$I3Thu0a|`sA&# zonl$m1}64Oyf<}FD%%DI$GFnug97;qB}$Ph4Oylv=`s{6QR)E%hCl&8-&LqoCJI9& zmPpOaEqpX4S16sxkt@%EDm7lHHCn&FeipfM-JTl#W#o02uSTsJ6g;zBv)|e@slM2T zea37OPO8%!&qA(Ui-vEH+jxsvG>vaoWga8?^m)Xtz&&dY886dio^LgzLz}M20k6rH z`(+vYu%UcX1&+YpCvlbIbLLVA za@`gLHVU0O49yPt{7>CK$K;nS4o{m!#7cCy0YAmvg+vPgeLIy?Us}+fh zS`U&#XrISKOF0akLF;m_bQ~Zcb&b>W=23|XJr;#_!@6{tS}Jrxl#{z}_wRu{xaT_S zOxKF-O6R+^QzGoP*V%^7b(NzoblT~z5z`g++3%oe;e}^^s*r@?J`Obd3o^cwrR^wu zpj__K#_<|Ag_(cll2W?PJ)3^_TJMLl<;%ECjPt-}J#NXTE%w2fqtV0GE95Q**BN_y`{blnb1N4)|F8eLuBi%&zz_&L z+}}k~2JikKtm*afK&soLY#TWE>oQN{(06T9S7ni}WFO3Zu z!kZ5SCezueRrpJUc2Av9yG3vk9F3SV|Ou2f7u3 z&}4-O0XEtv^809|azI7{ZdC@XKpY&ZTE6x%Z@~mqY0Jt8P06bx6&wVI!RLUPbS9ks zE3bQ<0Gn_@QVUI$*gwu)rTCmTX*Pn*ibU=G=~sJVSc9-kaF-E8<$sj_YA6+A?ob zhy=ya_laiDFq8E3EHx_=Yv*htv#cYTsZ-uCTg~=q#q@&u`u&@^#Vxw`M(R8xLNN4e zvkD3B904cDOxhON+! z%`}@~jMifDvw=^KmQX7Ybvlm+=W?ROYT^jeBcQ~!RhAOYY3aq;z*&6ad*>^u(mWX& z2}Yv1&vVVonvN9Z*-cLj7JTH>-hB1myK+@6@(cz0vm&A98q!xb7i4jNHq!UgkSyZX z!U7#Fi9ARd>}I%mi)eQ`BBb|_z?M(4 zZ)LH~U`KE~OC`iL61U(d+$E#cj?&T9Cll9GauRy$;gQ-OPkv@>fb`F!vf&WN1D|Rk zb~%+KqZU)yTd+4D2jV=%kn7P@3UO<+sxq`n(FIx87`l@gmj1faNlLdCq9c@tDf>fv z$+iQ63_*p^z|3|Za5eza(#XhJiVVG-P9P{B+V7Q6E!pEZE#XFenhPL_U55Avlnt5| z+pxeA0tucgRE%0TSC>rkUHY2npu7v|6!)>*{1$5p1?kVPqGL-1wP~f}$#mca(F5`D z6ew%R_J}kN!JNHzy1ZuJ!|--$w_|U${QU_idgOVeffS|fHPYPg(Gr}usaOV5E|mpA zkN4u85?I!ZM|G0R1yf3ko~UUsiVSFyb%kw3>Li6TujwT1p8V zhWZTFLNE$%`;xLZ2Nzx@AZcs{2A$J1<$3PEflV_XzJn600;NxQKkiF*?39hl7Gp8a z+)LqUqIrnka`dqbjAR~3yZDP^`77mNUM>1-BDJ(`zir`5yT52WG18C-halZ^2)?|t$lWk!G zl8)XA|Da)PFOkt-tyF(YvECWkd69S{1*jdQW zEG-PFSX1wanOwDmcf8OGulzfi+Cn>rm5da??y=YR6ux7f~onDZicu_ zJGjR9KBe#$X(aPMz#|(VP!E@jA`+C1lg@QNq+zvCY93(aX_1B5Z0S=pI}F2#5XtcQ z1@2A4j(~k0LV4f)pK}hEp6e7h4@6bm3w+dMgznu=GUzLtf?FyV)=m9);H&iiNUM}N z-*$BKS4XFVE#9@;J?M(>!Ygl8hl&k}-XzFSXRP%RNxO;cI3!6fEsg3}q#=D5qumqM zVElM|vPtQ=AU#9PFz;sA`wWLCNY9T;9|zF-s@dC}ugJ|$bULG^;!<(3L0Y>ZvsK)& zxQrp0fLI^2&xI@eNJ&Eo%r80N#^MR2|CUTU68e$gFIY$X)CjsD=@L)|>-bB%5JR|$ zzp`UwFJCIo70OwvR?YXC?Q~7Y z)qQXlmHM5d4#6g~XjP9?i1h<`lFI1EBQ~i+R?AAZnT0s(CZ=}4r6OP?5ZQ`v=&xS^ z1Oj?sVglo4(^~+bCx8IXCcu6VI1m7@eE{_R0zmr{fR%q3xOr!eH1|@V-c`W2M+IS3 zhwd;>&%*C4b+27sw80Gs>}!`m78sylNNM7@`S4Z4V9i>Dcm z7_rj^yDKZJo{9YK{~3m_DoFP6z=*}CT?dPZlD1CKd`)o|iI5d%!5a+($$ImS2?tO> z=L(Ptf=E&z3=RvLZHWeHAZ;b=q{1tL!$BX#luWVIBJ`YH&7F@FMX%*i_q{Lmw4+H`olIATPC|8MsLIv8m~4Kx)jk{J}fd!lB6h|krw z_pgzmHpTz)_!YPJuaTj=fdD^1z`vss)*8yN?{sb z1Q#gPZvzi`ZCA)BCgy#w8Y8#D$|7vIor@FK82b*GkyVEneeE|ee|WH|VAk!y7{bCU zo+}gQ9JZsFpF^)F+G{t5c2UxUPNb0Qg{QX7lIPu4+GQoTS%6iM!w9XvRsaX<_|Z&u}syrT>ihhz_fQ{F~^n%6hT#) z*j8ppzENN zG*SQss>{u#>o`56zN_~DmCfv^LD&3gV}rM`Pf zstQmE<%l|g_QbM|PIxAIjHUJfpuT%sTHnHi3{r_sZq{SCm3!*uAm_W=d2sGwxxUyd zMI<}7RcST&ZI$x85`|p zG((FH?=4zI5%|Paw4lm@Zh#qaV4|_sTFpFD#r$5ZXjF@tOHO6LM$;;3;|3>gGN|Qk zMszp6bo|~bjs}jM0Yokx3;{00277VAK;sGc0Pq&YB33K}nh=h8qDky-n{#8nFdTGk z1Q*cyAS8r>0z5cerI6bP0~ZcwouFR&hzG%5YH}j$J#(DG2Jt8IS+Sg_2X%kj)Q?K_ zktW`?e)oOO88;kXK=$6>Q-wSmYqE=G((NiNG-TtPD`AH8pak67FE^)u{6*V(Wj)H{a}XxB(d1v}X2Xt+%DPF5XnJ?%J45SU z9o6}|y1^A8I~eU20OT|h$}rN8Qa?7k{sXW z8X+TtMg|6Q3GFqFeolE3lL@k}KwqgG=&EyAx#vkV@&PoE&A?S#np3*^ctuL!pBdlv zLOI7Ei<7y!{!=2cBnoHBx>w}OV`kT8ZT z5|xEC5(=cfeWf<+K4j@nlUJj@w19k0#}VJUcz2&056G_`KA8M+$%YH=Q^b}P4XccX zJQ#6*K*PSWDnRksxDbrS#{dcqYZEsmbKrDI^iLO<2v%d7N`ATg55Y?w${^;O=LQ*_ zcWX0-Bb&DBtUW~#A&HCuOuSm*;# zj$d`F2a$23c)N(EGKLb2V`9%>{W&ghW@(YZ!aAP+#8>0)oF`)CZWPYa0M3j2x*9Z* zopbN2@P?=fkg_S@;L`(MvpyBrCx6a$%NT!Vo0eSMC)H;j+4I6~G0<|Gzv3oFC&%k~ zHlp#u$E8b=fS+akvAdZB+9^VhTMEQfwiS1eZU0mvOAk!gJue!Sfi$qN6EcpRd2dx5 zdua;}T_u`nRuK-3<=~`yw5xyzF6J~MnDjVvAOR73r-7lRYLascQE50q?IM!4%8>k! z55)<=a=mO%5iRDiz-6~5o`_W+FE*?M32|j?CW>8~NS5SiFlsMkk0Xv9@ypZC32LgH zp-7o-ue}Q|Mr&0N?cWGD-w~xNCrpPX`3s>jL8OeH-yqX%qu>DtmG1xK=S|63D@2uQd%Ubbj+=~}agoxx@)4||+$00> z>%Co1W&PP#(l&UAs>&n|k@#qk0Xwy*HV zbQ5hQ{I80e7YoQ^Np1S<+`K#Kdq8U|>)uQSke$U|K`@@)3zVZtB)t}#EELJQqKbR^ z9XXRJfg1>!3ReU-Fc1KBIj&V4E&7;oZBx>ETWi;q#cb)BzgW@8B{K#mhv_sughQGX z73snpRkq!0nL4t=JzW`yg6T20O@Nh|0);T9d+v~Nh;SH)6P;^cZ%H|vXs$P88T=Hg zDT8e8Zglh5LWo+u=&UUke!XSg?WdGoa;GN46DcNm=+p}>>|LQHHaeUAa7<{)Ac6$j8~ywdB)q3E99uWzBSoSKzFN|r zkA-JQ+OPA%vwK5}`19Uy3>p4eU25f`(*1Yqxmo+i0n-H^L)m{o2UD`jV_z?cAmigV zdR&(0DLPe3;9`8xKA@omhMjalKNmVh{gMG?R40Z1{AfSja@?mz#O&7n+*)_r7E&FI%C(Qu`G z`BwqB7hm$!;IaLZ969|uMFV%rk{lW(Ex*ZiMKrt)12weBJY4|NX1TBGX1pnQ=%yWk zF`&udVgJFY%^fJq0W-^Fy|KLG+j+{=U`S$G>A{SRlMklb)(g( zlmK|K9E1gLM{uW&;2R{UCS4iKRaBEH7s17F7I*C4ZlxL(`dQDH*%O$* zJ0iTthabj|+X4`}>vK}D=qDjDf{YzF}2d8ZJbH3e$v`Zn9J?*+T8v zOxcK6cB$_oIW6hAS1KP_&fNHwr`iiAm&q5CiUy=i@;uI1^xC#qy!=JNLdna%g5a11 z#tUB+(Q1|ob{V2F2Trua%rCZgdw5uIz!0;7u z%tbx#NKh=m>)?f8wK5%EuB2~4j<6)OYEn(4Nzk8$OMy#ML$@N&qZ&#&L2|P!*FLg= zZ|*{u9Mg*EK)1A?db9rZ(=~G*5JWR(mR`IQW@p{_ANdXh)COpAN$Z&o^ry)}n3aLVUbvCBNi`0t)M zcgO9e_@V*YN>TGwbBGlxXYHbF^n->At%9nc_%348KbdNg(uxK@eZ5LFt03N0^q-Jy zDit#G1zF9ag)+`}iIF!s9^SId0E_=0wh___c9#N)EXRT5MkukpbIeBW`6KJk4y&g? zwn9u;Fc|zd6umTly3{fh6qu>ENc`IV!k7XDYP^i8mHOc7A*>cg2nY>TNR>n563P_F z@ZlS{hfK27^#lHCZxAO2^*$K^S^DTbO)!JNW=W}Nq%`1@q8~AVB+CmO8;wmgG`-i) zR1B{i#n-Pu$kd6Sp{iEJ2j3P1O}(=_O0RR0V{u#I#b@OqY+vflPBqz_3U zA7h4PW-)s}Gm0$=P;nW-hM!)*AN~cb3=$d`tQlc?W0kT!LS-&cm-+-x`3R2r6i@gV zj#jV$m|$!5rXWw!hnGAIvg$Rxp4HBao`> z1Q!qc!(_-Z27j~54PgoUweBu!T;`!!`VbHFOa)ZOR1FLa?7@C25lR%5AP_iWKHVl^)vv0~vV>d@HNPOx4bw?nk&otx?eMFkiy8Iu{28VTC0y6a999^K8s~46+6`Gfp)$^Z>5uMRsT=DLmiVgeo-PNYaBP zVIaTTAFYqO+n-T$Mlbwx4;Ef0O}a~DI!u2hWqBrKs^&FrzZPI7gHmwN9Gb=b(e=C5 zzwkWN55rlsDc|Y*J~xylZ>TOo2kD`~X8}l7A{|v7a&>!oT0?>Qv7Y6sT^&dcZuhw1 zeE%eXB56V<#0bCO3kb7a6|}O_at{Y)5gCE%BwcvD8!7 zz+KCVZ#R=i5uF{>+h!&ciZ_{gsCN8DHyIHpdxRDMj)oNa4DZH&V5+VzSAr&a2%Ng0P><1E-B`(J(8Ca%kJ+BOtLo3*^%LDf& zQe&wbx$GDT1Bsk%$)fs5pcC8T@bI0SXj=s~?sUOza(*+iz$RI$%C$n!MZ&0|oiK}6)-JR-+zh+8O49lfp(QxT>}5hW$V;5@bLR?o7y@V$O~`SFGulTZ z39K^rJOGHyCA5q%7R}7KDn^4*eH-r_`Br>VA7eTdV1BoN!Qr8qr6a_D^HK)Sygs1=_sWUQ{kX`j$QOoD~AKy zLv97(wr1QvR2)I(SZld*ITV7%I2hf-1bv7C4@V-dQK&N*lgr7QyNXbCUn2#!)fBeV zwC!5cwY_HDHqElqovfPyg36{MK`jH+kDDm>^Pa9Bv+7_N^=nz*rJHqs75BYZKP^zH zg<>O2C=KRZ;3<5j8StjD%AuzcxW!x;z#=A_4EjOR?|nItSBfbn(RYSt0v9wDHj$aea%yw)vJwX zNlOi0H=VUbZM>7*s~|15b@ug`Fa=qCc4*>r9^(h`^<*WaQlwzo2A=JfI90#UsR;s#tsdDLLbr^ z%w~^*=nQS#q-~@QpMc7$YV(OWoszj;NZm@_+Sh(pTBoboSR z(?xgvBbQpM8GpK@RvuOXaP{_8+v>4N_cHlmcdXUwXeOqfcwxe8wsgnwKg7;IoG&YV zvKc5&{pE)fc=;;*x19Errh!d5(u78W`$U9w;;G?ML4|6s@7>Zd20}saYBqoaj$V@L zyTqbU<$BPeJm}IA1tu4I$-3bz8KP#h@HQFK5E;}$OYu<#rOua#PE6Ag-!j5)WI%-a z*bXG5s}q}Q-&B;wqx+SxHfm%Un52EsZs^cbqM@o$3)BfUa=H-yt47{Y;R#2@z zFiHZn6naSHcw`0?Xiw1O&wGXKUp1f$*>nr;p8cyrBx{dU6jPU8Xjb@Xf^mYh% zAjFv&z80jxti$%yWrbXVK*LZRqJoA(Y-%CP{P4NZnScjukY)qOpV5vEgh7UD2r|gM zDQNjX0aYoA>6;xi@{E$DGTS+#jm&Ap0Sk)b^fEY-MW3LSkSET_mJcJ)yre-v;&Jj6 zeGsGg!KYBJ$l7oU$QS@341iamNd?Qi0y1ytQc$~Q`#)^=Jdnm*o?aFwfvG1m zyGsNSd8Ji;a5-a^tn?`Woi>7I+44-Pf}y8GYRpY5Fy4|ChPoWon5aca{@JirpVBB` zl_(~2>&;I`ckg#k$+jU2-yhOK9+q7J6!v=B%7t(C$U`a1KRqQvZM*uqKHWQG#?fHI z-%ESMtV=oZAYmV9=Qzq$%;X`W{ut>9;w409 z)(T$4h`8La!X!IXJ_0Y8yZA;&@O%s6*)O{zRVea+*9~51IV5SE1WcK^Tix|8AxB86 zqTh^)`l6JbWE(HkjI?Att@cRowfUVvQ&nLAVGw)Bqq?y&dbJR@?Bhl2XePVpQGNzJ z{b=hNNJsZ$z-adAkdvFOm6t!^8Ot7@)*#Kf?lOx zMA6rO${|!hHapznjBTj-Q9Jg=j=z9lxgm4=1M~C>r zY4?N1(`<4=g|g<&2hQQX?X9bPcmQp`(cxykaX8fG;BdDZmpl9UXpk(QKibY|oJ5E1 ztaUUQsIipD(x>e{vEfJ$`ihw0e~F!_|E}!b*zvJ?Xt%o7`l>iIc_3M+f?5N&U8_kP z?tUTA@Q%(ldx!|{O|}u}=c~WYNHU(HtZHn&D6U@H@~q{~Ule$d;%Rbxw^g*KiO<5m z6qPqn^|UBou$aw@KCfAA@HGm1nNFhKIQ62@11dQ*C9GGjXJ(cnS9Pk(kvGl%iHUGXTJSW~F4(FEkQQP~rhS^;jTCV);k#TCm9ikrhjF z>!Duhd>7nxF4qE5B=w1$8mNq_f&Ks*fli~iQ|8w+4+3JmSJ@tRW4-IR5##+}Ujh{{ zvBDq2&|Sy1(dk_TYz3^!i3y?vf4kAUQ}HyOKCW~_Wy-qYIY}>FBSSBHon)|^Sx67> zHlp)ldSqrGytc(U_&&VwLhhGuVz0RCo@9ZtGmvH$yN7pU=i!BaWzPaN-tzdc)yQ3|Kyv+fK8%tq^nY6oKnNp+Sp+p$4``B9zelz#uAhNnJi-y`r7 z0DAk|4c=iyw*_>($-lB^0UM(J{9J;%VtZ$E9v%&)P%Vb-|zT;z8(LY zx}d=;8`_3{u&3xa$q~1IJ#lu;cVQnIa9oCs4MlUfDIK8e`77klhyGm;|NWAm3IDCx zmonftQsQ_`k4o91X8`|u7(p-FKASX0|JUdnbV|0gjgtr>|M~4~C!S&Mcg%PBvn{c1 zo}CyM+sqG)^k`nBDM7Bxw7Dgf1Oda3bs43uXy~5&Q;98)crd!ort_i^d*YbOvx>Y zDvBp^J0*7-h^gD%sD?t>!~(NfC0TUMreX>-dIpuMa-0y-I*DqXPW(6Jkby!4a-AP) zwn7$%Dd}LDP5H~&R|Rt-OH{ELZ@mhOmn3{Tw2TO+pmA@6Q_Kq+QThUpd{*0~qR$BXJOzvX{fGGYBkjz>=#U^no+o^~u5t2m z(Uj(^Z=J_1@y_$EVCK)hYT>@X&yK-!j{iDpf#?9Ae&o@g?&)%7&k#i94)^PoQP%UD zA@tRQu`(95C7Kl46`^JexueirzPuE2R{{D9tn(rODlSyH~H?mEhlG*IeCY8}hN4ASPRc-4B{&Yk)u?+ub5F z^Vcpx1>_9-<3HZ7yTk^G)d92oMNNTuX*Ax!osP1`-<0754yn zM3IPA)eJKGIqpIAimw5DDJA(*E~$%G&k%$J5nv*RCxs#>hYu`SN@-hKYD>XG4KVRp zK@`+7x{-@}(p3;U+H~QIaSlucHmKO!n#(+OQrQz9R}XiS{w+pfx3UUPeN!+OdLlBG z?iM~;h0nfKvYdJ-GM(umRko^`rhH;isw=_%T7)Tcv*6 zJyS23Zu8N&kY+Ryrn97eUtL9qoUbGV+#!qty!Ig;nV+vel=AUBUKSlbcr1L}Kix%Z zpB|K5f+sqfL&*Mk^I;nw_v54AGd|Im?wF<{Em8Zk%YqDt%6Y6*KW!t({Z}k(t~bqk zfsDT2z~4Pxd4BK-o07o=2eLD%eQW0{$(M|6=)N0Ov-O<65e~+S}K~ffp?UAiK|M8Drz0b|h^Z(*#jts@I?pW6Uak@vkXm>b>o`k-@3-4G9XP@W%EGm z$kqbWdDtYXH?xi>j2pHP#?333$n?5?xwT*DlM=VH&P`ZlV@rm`U(yq~L?MmB%ei{# z(h_Mk`7*FpCtNcR9W{t+wtVYN$VGn@vUzjdtWrLC9Fg`gs5U(VXr}rpBH-4x<+ojISuWe|4bYX!#=G`ZX z2O(Ah#g{<0jZ^Z$ZXDtHKu?wT{*$>Uk;jRCV_p{$pOz8|?h192jsGf=4(Nlki?5-b zVb4GD4cxta(_J>3DjzM*&y&WuU8OdYl#t+c!NMTwy>ok}qMzE#iiv-QxAvAkACKI} zB`Jx<+ViC)HB?Gh*|4dTa?*TMdMC?FIsMts6SlR4`m_Le5#IlG?E0=I#m>YgbCMHh z61UC!`qo{CiK)tgcz+;I9xmm5cO~u1Qm@p|9ZJCVBGM~i;=Mvb=vGS0d65R0rs{6g zEiZ_e{OCPev8s-^OlYbdm1E#!0UntTqXu$?J@^y(P4ub2?8T)okra9$)({mr5YG?A z*DKtYv9bC*s)W_M(!wCd(cGoRDzdHdM~V17{;{Q-tXa-%OjL64Pkn#cvF*f}#AHr#>`W|) zURML8p}cPs7-(Y~7(nL~5MXN)5Qyg8pJ~5NWNPO)&7kP@^}ECw6OmBng&B)384ldex)ji6>oSu4E`K&w?MHT4QVttkv|NDk|4WgxsW!1MhF+uaWmP%9j=L}vTXF%|Ebzjl2NGFnVZ91R_ATfKX?4Hx54 zO?`8}ly^o21OoHH@+L%`#1I?tVstT=!Dd@WeO>p@HhpTX*?BXBzJ`J+b(1OkD;*a~LP&xe04{hxh#u-!=^MAsn_t?Xi9fbpLh z^$#{nb#9#>xadU@6Gd#efYTsa#bj3(l4OPl-Z0X|$soSrrg4m`xr;3p*q2|PYtEq> zj!+|+k)5iK@={>%C3}HrKE3}*99jKiiH{F<;*01RVGIZJa^~exS3!D%VVbbBAY#Q4 zL@@PQJJlZ&<}tsKUC7MEbpi@X*1j8lfVXfebqa0M>{0(o%8B^H1R z5U2lTBro-8iC78jlzZ>@G?}L|(aJ|~22~7VL_LC`uUoCRzuo&=gV-EQX#FCr7(7fd zFCPyzC!HC8C4ouAg55|`jdpL~`no18x zx;j$bf;WR$vRu3-3BCu86Qh9hw*^I8CJncF72@~+?f7evw%5F)g)*wN1utZk_)p^K zWD-IQkt1QOh=lN~eh5^72oEaOcm;)o7}%@t-bf{FFSlT%A75O0bZC4nxg^GkpF3rS zdznYLWe}JHruu7sB#Mx~^N|^{b-~s~)kK1wvf&uX9j~HN!QHdE)YZMZ*wf2e?CxSM z^>jD~#Q6D>?yN)pertzAVUop6p$t)XKYQog@`5xvtGXyNuQBJPZWSWCIT>HRnUuIy zQJ%OBrz*E#K;A%#yDC#syQs){OHm{H`oTk>=5OSWO?S^8A8eIJ9fz_t2jsTESA5mf z4vLR=i6=@TybazW>(U!)Bcl+=2ofYZ%tpYTDyiH1JL7m-v|Dhz{>~V6rKd0iJ2;RK z8t-mqWZ4=w5!Y&kHgk_Bh6V%+tC&u_2HFNVo}hhu05L4?DzsI|(&*QtI6_z;P823P zJJD3{i>tGY?qgDOG8PKjJK?Jz1?u7w{o2!~Rbxe?#Lc9ZVq>5busQt8MsE?le@R0m zfp^g_?LtNom30=Q_O}1PO5Z>-qKP4ZD|hw_$`k|%a})_HztNl)0j(J4XgO>cJkM{-Ma4d+G45cPj|8? z>3Z$)jRPuD9q$sQOE2?}&OMp{;HZGIW5Nfiff7*)uujA)r9S%j@5J)W^u${<8X%(E&4q-ry)dm>Kv_u72#>CYYpC+_idzXEFI$k#}<{+kFS(R`R}O( zZ82gE3rwRQMc=z$EoE%$=DAC^7PU|7R+@c%j6c+2>#E$_{>*Me)Du|s9|1)Frl;US zGmV_%d=(QKvUQ|x|G6+b$n(EgS1>Nw5d@1$wCL%<(UdW}IsWc%-sG=UyH(v$wdA{F zNN<3qXD40|_PnS&xjam&qzwBnPhZp$bh{A!f#Hw;PeujL@9&T2SCPU0r{m1aMyWl3 zI-$#uVAncG-cl;220 zgAabPjoBH&?5j)b57^*e%60Z4dfs)Jy zSX)D(H~#M!Fgt0g6HpMIjx6PJsOl%s}O6 zbD7BKk!^+6v8K(4kwB-`tVo1H>QY|i{IxH~zuyG}pK{9wT5GB8`P8P`QYX}yvLs`+ zr7F5Bqm~!e$30bDP&wM#Ex;2CIaR>>*jiW+ZPlC1rJV3SE?6wl1#PYTiLDST4zgCZ z<=x$`iiN9*?#CGU>-;@X{$FhI{%+AM7*w&;qw-lRbbE}91c|z`wGdXwDtkZYE50(m zSN)tZRf?7fEw*cXa&XLq8INh!>ilY!DUaDiOiFN^eZ3@Ju?nMJRjzc{S0UXbzsR^0 z^!+!bc$^lj;144|kC--Rqh$?zQKLJ?kM4a#QX!iO$e)EM&>j?c5LrpQN10F!}&5Y>T+YsoYm!3PF0g_^>F{oemNHYA-0|UFS|V! z213KivwqpBWeZk}irN{W9dAgUyikz)6t_aXMS>i0zwi8l{tmd_1un~i6uwP{%(;px&2AW zE980dDw(5|s>S{K$oi6Gv8vvnx5Q1*HDXh#GG<73I&vArxX zLHkBTPo5>fIV?oAMR%PoWr`MOcZ6{j;;AF&W6owyva5CCLa8=z2h87Aae_JS2j1OF z?ZdDuuN#ZE0FuR=n6 ziwtRz)|UhENGhU44+{=w4(B=Zq#079gxI~a{^^@6OO9nsP_p+M{Ue=J6whJJ|D0Yh*oocjoTxRkngS?+ouK@`P;%d#Y4uf6fWV>nr1n<2~n$DO7q2vtzp^ zXNJbpl}8(KtMpY|(}*%Xew0df71l7W2w3N2pZk9L_IJ@C{1IX2=qe+{j^+1CTxM9Z z%YIp*cxU$kKLL`bTEr_;=Kzd~t=iu$YOPL%et+|ak`u&B=$tMTru$N${+&FSo#r5j88G9T}CJ6cLyZ7Jh2_ zKpRyD`|ndO(aQM?D<3yYE?e0<@Lc`x{_C^Fwb{urRVkDbT1Mg{Svu_I6IYJ2 z#@<(-RywYSOdenQG=E-=oK4N?$V^E}%ou~zCri-#a=>F<162H)ra}0u?FPqBF9_QVZqBD`7tw;1%wG{5iW?JV4W*u;s?$+3^!{Y#fK0RP%pgGcjKo_hF(@q_e4z*>53l z+1>2lA-}UR?dX#uS7Bp^C$_PZL$1Fv=0)90ejug&-~mlazJUvNukL8&8uZGwIL@dZ zFo_;DQJ?iE&?#W~Y3x=1=ihqpd%*Rkl>zS$r|I4+ua|;WG|pHT)e{x{lK2nnqxY+- zY7|+g2v1Aa?uc^HFIBsPYPX)hzz*``HyGyTXHJWPre!|i+fQxaCZOKPE7$i_LR;9A zO9fi1V?bdNg<||O>12kpH1mG|RpoE;M)3YodtXw`;zo3%hjso830KzRxvwh6yK^y? zhjaLPSOxEMUXpAutocD|_HNZEb8ARYlBQE!6+85jdHd+g#F(LCugUCqYn)|>bDBcC z`e9lMo8FZB;NfeHOL@%`4Fw%-g_-sJ71ZVdm2}v?_Mw4>1k7=dn=M;tftjy0UHVp7 zPZvYXD^|A@e=8hPIW=W_!4IX;|34?q?O0?Px|m^wltll%lWr$#$#Q2UzHy>#j2_Nx zH1zZ|h@(D~?knaLe+)fNo&Y<05>#4B=2?Hd81CQve2(BB-y;-_7ih;7JE_4of(7+0XN zvi=HE0Wu_SyrrxfVnxfWv^Pzo4T7dl-r$S3`M}Euvb4_6=XEKRtEE5BLHFJSG?GKh zj4LE%W!DaQa`HY9{0&dzIde$!ISk#Uu|}>`t@J*~s0%{n&zv_O;y2tmM-hRd$uFgN zm8D}~d;S+zR-YiDx4`4wby+3lB?!1StA_P#68s(%D7t=;XG8Y)39ECyDYf**&-u%d z0xdmwnRU2h$Jh||byY82Cok|7G-Abxqt{m6H?99O7CCxT=NvlU8_9d*x=^7H zo5%2Z7gYAQlz;dX$}T@A%|*y21)IfXaR)vB^abI!T`#*ixQwfzr>=Z-8RZLme2V1% z?^NP(7`sRcBNUxrITX>BcC>q>dObB~b&8S0OwFV)8B>&1MDTM_8ha|6zTf=NAxdEF zGk~3kf^81F|3YzLFOYt^xw%5)&}ItX>HN=9 zncn@Assu33-x(Z4hX%RZhvunJ+p|p@(p7ljr=!pM+LgmX+_0qQQgyz-ye$s&jo;tg>x7@XS?6RKvnW1k z2r@62f4cM`BuEqjrAje%-uA8E9i(M^t=BnSMhYRTe_Ph75nv~WDLC!yjk91WZ7^6{ z3SMRt7-;teR>&?8(f^$rC_BN5gZ1-&CePBl)AMZpD zmlfpIw)WI1YF202lVPv^XPLr6M>8Q-&z{^~Oy&JhOMsp7*zxIi^^J~5z}E9+=1tPa)^k zSC*C4SLTuHDazmYbZ1xpd2ZO~&b~1`v$QtWq*e#?G*D`&=n#C<+__y0KKI-ivqc9( zPH&AW084a?-f1F%Uk1Gs|#e3=4*D@=Wyt3<)gZe&~i-PMBN(^ZXuv>IWz2&tx z*r>|RxCA4V8O|S|BxOL3@Q{;eN0LSDC{!n`u@BxsR0=N~>}DI*9i<|{Qx9{0JZ$P) znbnu&SnrtFmnf^kkS?Lcgl-tR2RLEF&7YJCX5Xn-BHQ>uQp)Yqqmx|9G@$&@Z{ z4kuz0e8Po^!r^W)^*C|m`LvBNspVeDo6{eArX@>hbAMk@%^ z5An7T&rS1(yS%ooBnQ6s^0F~0-oq_E+7pjlF$k%)RO${X1M{K%*LC3s>FyybcKle= zYE+PCQKc)setH|&!F=8WdZ%%dv9oeC`fFY+^Ref?<{hC?M!qe-eVRDr2cN^y249V5AOyRJNN z4nBR_U*enXtMaQuLDTzuVJ`pwcS)6@w6-{}K7|>-lOppSQ#fwj z&{>c}+d$@q86V_J;JZMG%1Vys zQi|uwBA*N4yWkgsk93y9@Szdof*!)?-20ZEBi5lYd~J~-ejgKZE{kj>M@grB@j=kS ztBjoA`j(8@#c_YGflysvyCjrazebXjx2-vmi{XE1b{m~}A25sz_5{G^nP9_j+@I

&V}NWrUoQg(QHtha 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 - + @@ -37,8 +37,8 @@ bridge.addCommandListener('hello', data => { const route = useRoute(); const router = useRouter(); -const password = Boolean(import.meta.env.VITE_USE_PASSWORD); -privilegeStatus.allow = !Boolean(password); +const useAuth = Boolean(import.meta.env.VITE_USE_AUTH); +privilegeStatus.allow = !Boolean(useAuth); onMounted(async () => { // 初始化 css diff --git a/renderer/src/api/message-bridge.ts b/renderer/src/api/message-bridge.ts index 70ecbed..1229688 100644 --- a/renderer/src/api/message-bridge.ts +++ b/renderer/src/api/message-bridge.ts @@ -7,9 +7,9 @@ export interface VSCodeMessage { callbackId?: string; } -export interface RestFulResponse { +export interface RestFulResponse { code: number; - msg: any; + msg: T; } export type MessageHandler = (message: VSCodeMessage) => void; @@ -206,7 +206,7 @@ export class MessageBridge { * @param data * @returns */ - public commandRequest(command: string, data?: any) { + public commandRequest(command: string, data?: any): Promise> { return new Promise((resolve, reject) => { this.addCommandListener(command, (data) => { resolve(data as RestFulResponse); diff --git a/renderer/src/hook/type.ts b/renderer/src/hook/type.ts index 1967de9..728404f 100644 --- a/renderer/src/hook/type.ts +++ b/renderer/src/hook/type.ts @@ -149,7 +149,7 @@ export type APIRequest = | ToolCallRequest; export interface IStdioConnectionItem { - type: 'stdio'; + type: 'STDIO'; name: string; command: string; args: string[]; @@ -159,7 +159,7 @@ export interface IStdioConnectionItem { } export interface ISSEConnectionItem { - type: 'sse'; + type: 'SSE'; name: string; url: string; oauth?: string; @@ -169,13 +169,13 @@ export interface ISSEConnectionItem { export interface IStdioLaunchSignature { - type: 'stdio'; + type: 'STDIO'; commandString: string; cwd: string; } export interface ISSELaunchSignature { - type:'sse'; + type:'SSE'; url: string; oauth: string; } diff --git a/renderer/src/views/connect/connection-item.ts b/renderer/src/views/connect/connection-item.ts new file mode 100644 index 0000000..891b733 --- /dev/null +++ b/renderer/src/views/connect/connection-item.ts @@ -0,0 +1,58 @@ +import { reactive, type Reactive } from "vue"; + +export type ConnectionType = 'STDIO' | 'SSE' | 'STREAMABLE_HTTP'; + +export interface ConnectionTypeOptionItem { + value: ConnectionType; + label: string; +} + +export const connectionSelectDataViewOption: ConnectionTypeOptionItem[] = [ + { + value: 'STDIO', + label: 'STDIO' + }, + { + value: 'SSE', + label: 'SSE' + }, + { + value: 'STREAMABLE_HTTP', + label: 'STREAMABLE_HTTP' + } +] + +export interface IConnectionArgs { + type: ConnectionType; + commandString?: string; + cwd?: string; + urlString?: string; +} + +export class McpClient { + public clientId?: string; + public name?: string; + public version?: string; + public connectionArgs: Reactive; + + constructor() { + this.connectionArgs = reactive({ + type: 'STDIO', + commandString: '', + cwd: '', + urlString: '' + }); + } + + async connect() { + + } +} + +// 用于描述一个连接的数据结构 +export interface McpServer { + type: ConnectionType; + clientId: string; + name: string; + +} \ No newline at end of file diff --git a/renderer/src/views/connect/connection.ts b/renderer/src/views/connect/connection.ts index 82d23a2..8715b99 100644 --- a/renderer/src/views/connect/connection.ts +++ b/renderer/src/views/connect/connection.ts @@ -5,8 +5,15 @@ import { ElLoading, ElMessage } from 'element-plus'; import { getPlatform, type OpenMcpSupportPlatform } from '@/api/platform'; import { getTour, loadSetting } from '@/hook/setting'; import { loadPanels } from '@/hook/panel'; +import type { ConnectionType } from './connection-item'; -export const connectionMethods = reactive({ +export const connectionMethods = reactive<{ + current: ConnectionType, + data: { + value: ConnectionType, + label: string + }[] +}>({ current: 'STDIO', data: [ { @@ -16,6 +23,10 @@ export const connectionMethods = reactive({ { value: 'SSE', label: 'SSE' + }, + { + value: 'STREAMABLE_HTTP', + label: 'STREAMABLE_HTTP' } ] }); @@ -23,6 +34,7 @@ export const connectionMethods = reactive({ export const connectionSettingRef = ref(null); export const connectionLogRef = ref(null); +// 主 mcp 服务器的连接参数 export const connectionArgs = reactive({ commandString: '', cwd: '', @@ -41,6 +53,13 @@ export interface IConnectionEnv { newValue: string } +export interface ConnectionResult { + status: string + clientId: string + name: string + version: string +} + export const connectionEnv = reactive({ data: [], newKey: '', @@ -56,9 +75,6 @@ export function makeEnv() { } -// 定义连接类型 -type ConnectionType = 'STDIO' | 'SSE'; - // 定义命令行参数接口 export interface McpOptions { connectionType: ConnectionType; @@ -74,6 +90,14 @@ export interface McpOptions { clientVersion?: string; } +/** + * @description 试图启动 mcp 服务器,它会 + * 1. 请求启动参数 + * 2. 启动 mcp 服务器 + * 3. 将本次的启动参数同步到本地 + * @param option + * @returns + */ export async function doConnect( option: { namespace: OpenMcpSupportPlatform @@ -86,32 +110,21 @@ export async function doConnect( updateCommandString = true } = option; + // 如果是初始化,则需要请求启动参数 if (updateCommandString) { pinkLog('请求启动参数'); const connectionItem = await getLaunchSignature(namespace + '/launch-signature'); - - if (connectionItem.type ==='stdio') { - connectionMethods.current = 'STDIO'; - connectionArgs.commandString = connectionItem.commandString; - connectionArgs.cwd = connectionItem.cwd; - - if (connectionArgs.commandString.length === 0) { - return; - } - } else { - connectionMethods.current = 'SSE'; - connectionArgs.urlString = connectionItem.url || ''; - - if (connectionArgs.urlString.length === 0) { - return; - } - } + connectionMethods.current = connectionItem.type; + connectionArgs.commandString = connectionItem.commandString || ''; + connectionArgs.cwd = connectionItem.cwd || ''; + connectionArgs.oauth = connectionItem.oauth || ''; + connectionArgs.urlString = connectionItem.url || ''; } if (connectionMethods.current === 'STDIO') { - await launchStdio(namespace); + return await launchStdio(namespace); } else { - await launchSSE(namespace); + return await launchRemote(namespace); } } @@ -128,26 +141,25 @@ async function launchStdio(namespace: string) { command: command, args: commandComponents, cwd: connectionArgs.cwd, - clientName: 'openmcp.connect.stdio', + clientName: 'openmcp.connect.STDIO', clientVersion: '0.0.1', env }; - const { code, msg } = await bridge.commandRequest('connect', connectOption); + const { code, msg } = await bridge.commandRequest('connect', connectOption); connectionResult.success = (code === 200); if (code === 200) { - connectionResult.logString.push({ - type: 'info', - message: msg - }); - const res = await getServerVersion() as { name: string, version: string }; - connectionResult.serverInfo.name = res.name || ''; - connectionResult.serverInfo.version = res.version || ''; + const message = `connect to ${msg.name} ${msg.version} success, clientId: ${msg.clientId}`; + connectionResult.logString.push({ type: 'info', message }); - // 同步信息到 vscode + connectionResult.serverInfo.name = msg.name || ''; + connectionResult.serverInfo.version = msg.version || ''; + connectionResult.clientId = msg.clientId || ''; + + // 同步信息到 后端 const commandComponents = connectionArgs.commandString.split(/\s+/g); const command = commandComponents[0]; commandComponents.shift(); @@ -155,7 +167,7 @@ async function launchStdio(namespace: string) { const clientStdioConnectionItem = { serverInfo: connectionResult.serverInfo, connectionType: 'STDIO', - name: 'openmcp.connect.stdio', + name: 'openmcp.connect.STDIO', command: command, args: commandComponents, cwd: connectionArgs.cwd, @@ -168,46 +180,49 @@ async function launchStdio(namespace: string) { }); } else { + const messaage = msg.toString(); connectionResult.logString.push({ type: 'error', - message: msg + message: messaage }); - ElMessage.error(msg); + ElMessage.error(messaage); } } -async function launchSSE(namespace: string) { +async function launchRemote(namespace: string) { const bridge = useMessageBridge(); const env = makeEnv(); const connectOption: McpOptions = { - connectionType: 'SSE', + connectionType: connectionMethods.current, url: connectionArgs.urlString, - clientName: 'openmcp.connect.sse', + clientName: 'openmcp.connect.' + connectionMethods.current, clientVersion: '0.0.1', env }; - const { code, msg } = await bridge.commandRequest('connect', connectOption); + const { code, msg } = await bridge.commandRequest('connect', connectOption); connectionResult.success = (code === 200); if (code === 200) { + const message = `connect to ${msg.name} ${msg.version} success, clientId: ${msg.clientId}`; + connectionResult.logString.push({ type: 'info', - message: msg + message: message }); - const res = await getServerVersion() as { name: string, version: string }; - connectionResult.serverInfo.name = res.name || ''; - connectionResult.serverInfo.version = res.version || ''; + connectionResult.serverInfo.name = msg.name || ''; + connectionResult.serverInfo.version = msg.version || ''; + connectionResult.clientId = msg.clientId || ''; // 同步信息到 vscode const clientSseConnectionItem = { serverInfo: connectionResult.serverInfo, - connectionType: 'SSE', - name: 'openmcp.connect.sse', + connectionType: connectionMethods.current, + name: 'openmcp.connect.' + connectionMethods.current, url: connectionArgs.urlString, oauth: connectionArgs.oauth, env: env @@ -219,12 +234,13 @@ async function launchSSE(namespace: string) { }); } else { + const message = msg.toString(); connectionResult.logString.push({ type: 'error', - message: msg + message: message }); - ElMessage.error(msg); + ElMessage.error(message); } } @@ -247,32 +263,19 @@ export const connectionResult = reactive<{ serverInfo: { name: string, version: string - } + }, + clientId: string }>({ success: false, logString: [], serverInfo: { name: '', version: '' - } + }, + clientId: '' }); -export function getServerVersion() { - return new Promise((resolve, reject) => { - const bridge = useMessageBridge(); - bridge.addCommandListener('server/version', data => { - if (data.code === 200) { - resolve(data.msg); - } else { - reject(data.msg); - } - }, { once: true }); - bridge.postMessage({ - command: 'server/version', - }); - }); -} export const envVarStatus = { launched: false diff --git a/servers/uv.lock b/servers/uv.lock index 71c408f..d34f9b7 100644 --- a/servers/uv.lock +++ b/servers/uv.lock @@ -175,10 +175,10 @@ wheels = [ ] [[package]] -name = "httpx-sse" +name = "httpx-SSE" version = "0.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } +sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-SSE-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } wheels = [ { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, ] @@ -258,10 +258,10 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "httpx" }, - { name = "httpx-sse" }, + { name = "httpx-SSE" }, { name = "pydantic" }, { name = "pydantic-settings" }, - { name = "sse-starlette" }, + { name = "SSE-starlette" }, { name = "starlette" }, { name = "uvicorn" }, ] @@ -477,7 +477,7 @@ wheels = [ ] [[package]] -name = "sse-starlette" +name = "SSE-starlette" version = "2.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ diff --git a/service/src/common/index.dto.ts b/service/src/common/index.dto.ts index e6ab551..9d302f7 100644 --- a/service/src/common/index.dto.ts +++ b/service/src/common/index.dto.ts @@ -3,9 +3,13 @@ import { McpClient } from "../mcp/client.service"; export type RequestClientType = McpClient | undefined; +export interface RequestData { + clientId?: string; + [key: string]: any; +} + export type RequestHandler = ( - client: RequestClientType, - data: T, + data: T & RequestData, webview: PostMessageble ) => Promise; diff --git a/service/src/common/router.ts b/service/src/common/router.ts index 297ebd1..5ff9ab9 100644 --- a/service/src/common/router.ts +++ b/service/src/common/router.ts @@ -3,7 +3,6 @@ import type { PostMessageble } from "../hook/adapter"; import { LlmController } from "../llm/llm.controller"; import { ClientController } from "../mcp/client.controller"; import { ConnectController } from "../mcp/connect.controller"; -import { client } from "../mcp/connect.service"; import { OcrController } from "../mcp/ocr.controller"; import { PanelController } from "../panel/panel.controller"; import { SettingController } from "../setting/setting.controller"; @@ -24,7 +23,7 @@ export async function routeMessage(command: string, data: any, webview: PostMess try { // TODO: select client based on something - const res = await handler(client, data, webview); + const res = await handler(data, webview); // res.code = -1 代表当前请求不需要返回发送 if (res.code >= 0) { diff --git a/service/src/hook/adapter.ts b/service/src/hook/adapter.ts index b037468..825625e 100644 --- a/service/src/hook/adapter.ts +++ b/service/src/hook/adapter.ts @@ -2,7 +2,7 @@ import { WebSocket } from 'ws'; import { EventEmitter } from 'events'; import { routeMessage } from '../common/router'; import { McpOptions } from '../mcp/client.dto'; -import { client, connectService } from '../mcp/connect.service'; +import { clientMap, connectService } from '../mcp/connect.service'; // WebSocket 消息格式 export interface WebSocketMessage { @@ -114,9 +114,12 @@ export class TaskLoopAdapter { * @param mcpOption */ public async connectMcpServer(mcpOption: McpOptions) { - const res = await connectService(undefined, mcpOption); + const res = await connectService(mcpOption); if (res.code === 200) { console.log('✅ 成功连接 mcp 服务器: ' + res.msg); + + const uuid = res.msg.uuid; + const client = clientMap.get(uuid); const version = client?.getServerVersion(); console.log(version); } else { @@ -129,14 +132,19 @@ export class TaskLoopAdapter { * @returns */ public async listTools() { - const tools = await client?.listTools(); - if (tools?.tools) { - return tools.tools.map((tool) => { - const enabledTools = { ...tool, enabled: true }; - return enabledTools; - }); + const tools = []; + for (const client of clientMap.values()) { + const clientTools = await client?.listTools(); + if (clientTools?.tools) { + const enabledTools = clientTools.tools.map((tool) => { + const enabledTools = {...tool, enabled: true }; + return enabledTools; + }); + tools.push(...enabledTools); + } } - return []; + + return tools; } } diff --git a/service/src/llm/llm.controller.ts b/service/src/llm/llm.controller.ts index 4252c51..08804bf 100644 --- a/service/src/llm/llm.controller.ts +++ b/service/src/llm/llm.controller.ts @@ -1,13 +1,17 @@ import { Controller, RequestClientType } from "../common"; +import { RequestData } from "../common/index.dto"; import { PostMessageble } from "../hook/adapter"; +import { getClient } from "../mcp/connect.service"; import { abortMessageService, streamingChatCompletion } from "./llm.service"; export class LlmController { @Controller('llm/chat/completions') - async chatCompletion(client: RequestClientType, data: any, webview: PostMessageble) { + async chatCompletion(data: RequestData, webview: PostMessageble) { let { tools = [] } = data; + const client = getClient(data.clientId); + if (tools.length > 0 && !client) { return { code: 501, @@ -37,7 +41,7 @@ export class LlmController { } @Controller('llm/chat/completions/abort') - async abortChatCompletion(client: RequestClientType, data: any, webview: PostMessageble) { + async abortChatCompletion(data: RequestData, webview: PostMessageble) { return abortMessageService(data, webview); } diff --git a/service/src/main.ts b/service/src/main.ts index 6fdde5a..e2983e1 100644 --- a/service/src/main.ts +++ b/service/src/main.ts @@ -28,13 +28,13 @@ const logger = pino({ export type MessageHandler = (message: VSCodeMessage) => void; interface IStdioLaunchSignature { - type: 'stdio'; + type: 'STDIO'; commandString: string; cwd: string; } interface ISSELaunchSignature { - type:'sse'; + type:'SSE'; url: string; oauth: string; } @@ -43,7 +43,7 @@ export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature; function refreshConnectionOption(envPath: string) { const defaultOption = { - type:'stdio', + type:'STDIO', command: 'mcp', args: ['run', 'main.py'], cwd: '../server' @@ -76,7 +76,7 @@ function updateConnectionOption(data: any) { if (data.connectionType === 'STDIO') { const connectionItem = { - type: 'stdio', + type: 'STDIO', command: data.command, args: data.args, cwd: data.cwd.replace(/\\/g, '/') @@ -85,7 +85,7 @@ function updateConnectionOption(data: any) { fs.writeFileSync(envPath, JSON.stringify(connectionItem, null, 4)); } else { const connectionItem = { - type: 'sse', + type: 'SSE', url: data.url, oauth: data.oauth }; @@ -124,14 +124,14 @@ wss.on('connection', (ws: any) => { switch (command) { case 'web/launch-signature': - const launchResultMessage: ILaunchSigature = option.type === 'stdio' ? + const launchResultMessage: ILaunchSigature = option.type === 'STDIO' ? { - type: 'stdio', + type: 'STDIO', commandString: option.command + ' ' + option.args.join(' '), cwd: option.cwd || '' } : { - type: 'sse', + type: 'SSE', url: option.url, oauth: option.oauth || '' }; diff --git a/service/src/mcp/client.controller.ts b/service/src/mcp/client.controller.ts index 0238fb6..19efaf7 100644 --- a/service/src/mcp/client.controller.ts +++ b/service/src/mcp/client.controller.ts @@ -1,11 +1,14 @@ -import { Controller, RequestClientType } from "../common"; +import { Controller } from "../common"; +import { RequestData } from "../common/index.dto"; import { PostMessageble } from "../hook/adapter"; import { postProcessMcpToolcallResponse } from "./client.service"; +import { getClient } from "./connect.service"; export class ClientController { @Controller('server/version') - async getServerVersion(client: RequestClientType, data: any, webview: PostMessageble) { + async getServerVersion(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); if (!client) { return { code: 501, @@ -21,7 +24,8 @@ export class ClientController { } @Controller('prompts/list') - async listPrompts(client: RequestClientType, data: any, webview: PostMessageble) { + async listPrompts(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); if (!client) { const connectResult = { code: 501, @@ -39,7 +43,8 @@ export class ClientController { } @Controller('prompts/get') - async getPrompt(client: RequestClientType, option: any, webview: PostMessageble) { + async getPrompt(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); if (!client) { return { code: 501, @@ -47,7 +52,7 @@ export class ClientController { }; } - const prompt = await client.getPrompt(option.promptId, option.args || {}); + const prompt = await client.getPrompt(data.promptId, data.args || {}); return { code: 200, msg: prompt @@ -55,7 +60,8 @@ export class ClientController { } @Controller('resources/list') - async listResources(client: RequestClientType, data: any, webview: PostMessageble) { + async listResources(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); if (!client) { return { code: 501, @@ -71,8 +77,8 @@ export class ClientController { } @Controller('resources/templates/list') - async listResourceTemplates(client: RequestClientType, data: any, webview: PostMessageble) { - + async listResourceTemplates(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); if (!client) { return { code: 501, @@ -88,7 +94,8 @@ export class ClientController { } @Controller('resources/read') - async readResource(client: RequestClientType, option: any, webview: PostMessageble) { + async readResource(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); if (!client) { return { code: 501, @@ -96,7 +103,7 @@ export class ClientController { }; } - const resource = await client.readResource(option.resourceUri); + const resource = await client.readResource(data.resourceUri); console.log(resource); return { @@ -106,7 +113,8 @@ export class ClientController { } @Controller('tools/list') - async listTools(client: RequestClientType, data: any, webview: PostMessageble) { + async listTools(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); if (!client) { return { code: 501, @@ -122,7 +130,8 @@ export class ClientController { } @Controller('tools/call') - async callTool(client: RequestClientType, option: any, webview: PostMessageble) { + async callTool(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); if (!client) { return { code: 501, @@ -131,18 +140,13 @@ export class ClientController { } const toolResult = await client.callTool({ - name: option.toolName, - arguments: option.toolArgs, - callToolOption: option.callToolOption + name: data.toolName, + arguments: data.toolArgs, + callToolOption: data.callToolOption }); - // console.log(JSON.stringify(toolResult, null, 2)); - postProcessMcpToolcallResponse(toolResult, webview); - // console.log(JSON.stringify(toolResult, null, 2)); - - return { code: 200, msg: toolResult diff --git a/service/src/mcp/client.dto.ts b/service/src/mcp/client.dto.ts index cd30ce0..84f1ce6 100644 --- a/service/src/mcp/client.dto.ts +++ b/service/src/mcp/client.dto.ts @@ -1,5 +1,5 @@ -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/STDIO.js"; +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/SSE.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { Implementation } from "@modelcontextprotocol/sdk/types"; diff --git a/service/src/mcp/client.service.ts b/service/src/mcp/client.service.ts index 29ebe99..2106012 100644 --- a/service/src/mcp/client.service.ts +++ b/service/src/mcp/client.service.ts @@ -1,7 +1,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/STDIO.js"; +import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/SSE.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import type { McpOptions, McpTransport, IServerVersion, ToolCallResponse, ToolCallContent } from './client.dto'; import { PostMessageble } from "../hook/adapter"; diff --git a/service/src/mcp/connect.controller.ts b/service/src/mcp/connect.controller.ts index 6f979bc..92c13fc 100644 --- a/service/src/mcp/connect.controller.ts +++ b/service/src/mcp/connect.controller.ts @@ -1,17 +1,19 @@ -import { Controller, RequestClientType } from '../common'; +import { Controller } from '../common'; import { PostMessageble } from '../hook/adapter'; -import { connectService } from './connect.service'; +import { RequestData } from '../common/index.dto'; +import { connectService, getClient } from './connect.service'; export class ConnectController { @Controller('connect') - async connect(client: RequestClientType, data: any, webview: PostMessageble) { - const res = await connectService(client, data); + async connect(data: any, webview: PostMessageble) { + const res = await connectService(data); return res; } @Controller('lookup-env-var') - async lookupEnvVar(client: RequestClientType, data: any, webview: PostMessageble) { + async lookupEnvVar(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); const { keys } = data; const values = keys.map((key: string) => process.env[key] || ''); @@ -22,7 +24,8 @@ export class ConnectController { } @Controller('ping') - async ping(client: RequestClientType, data: any, webview: PostMessageble) { + async ping(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); if (!client) { const connectResult = { code: 501, diff --git a/service/src/mcp/connect.service.ts b/service/src/mcp/connect.service.ts index 83f2cef..ed9461f 100644 --- a/service/src/mcp/connect.service.ts +++ b/service/src/mcp/connect.service.ts @@ -3,10 +3,12 @@ import { RequestClientType } from '../common'; import { connect } from './client.service'; import { RestfulResponse } from '../common/index.dto'; import { McpOptions } from './client.dto'; +import { randomUUID } from 'node:crypto'; - -// TODO: 更多的 client -export let client: RequestClientType = undefined; +export const clientMap: Map = new Map(); +export function getClient(clientId?: string): RequestClientType | undefined { + return clientMap.get(clientId || ''); +} export function tryGetRunCommandError(command: string, args: string[] = [], cwd?: string): string | null { try { @@ -15,7 +17,7 @@ export function tryGetRunCommandError(command: string, args: string[] = [], cwd? const result = spawnSync(command, args, { cwd: cwd || process.cwd(), - stdio: 'pipe', + STDIO: 'pipe', encoding: 'utf-8' }); @@ -32,7 +34,6 @@ export function tryGetRunCommandError(command: string, args: string[] = [], cwd? } export async function connectService( - _client: RequestClientType, option: McpOptions ): Promise { try { @@ -48,19 +49,25 @@ export async function connectService( }); } - client = await connect(option); + const client = await connect(option); + const uuid = randomUUID(); + clientMap.set(uuid, client); + + const versionInfo = client.getServerVersion(); + const connectResult = { code: 200, - msg: 'Connect to OpenMCP successfully\nWelcome back, Kirigaya' + msg: { + status: 'success', + clientId: uuid, + name: versionInfo?.name, + version: versionInfo?.version + } }; return connectResult; } catch (error) { - - console.log('meet error'); - console.log(error); - // TODO: 这边获取到的 error 不够精致,如何才能获取到更加精准的错误 // 比如 error: Failed to spawn: `server.py` // Caused by: No such file or directory (os error 2) diff --git a/service/src/panel/panel.controller.ts b/service/src/panel/panel.controller.ts index f7239a1..92332ef 100644 --- a/service/src/panel/panel.controller.ts +++ b/service/src/panel/panel.controller.ts @@ -1,13 +1,16 @@ -import { Controller, RequestClientType } from "../common"; +import { Controller } from "../common"; import { PostMessageble } from "../hook/adapter"; +import { RequestData } from "../common/index.dto"; +import { getClient } from "../mcp/connect.service"; import { systemPromptDB } from "../hook/db"; import { loadTabSaveConfig, saveTabSaveConfig } from "./panel.service"; export class PanelController { @Controller('panel/save') - async savePanel(client: RequestClientType, data: any, webview: PostMessageble) { + async savePanel(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); const serverInfo = client?.getServerVersion(); - saveTabSaveConfig(serverInfo, data); + saveTabSaveConfig(serverInfo, data); return { code: 200, @@ -15,11 +18,11 @@ export class PanelController { }; } - @Controller('panel/load') - async loadPanel(client: RequestClientType, data: any, webview: PostMessageble) { + async loadPanel(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); const serverInfo = client?.getServerVersion(); - const config = loadTabSaveConfig(serverInfo); + const config = loadTabSaveConfig(serverInfo); return { code: 200, @@ -28,7 +31,8 @@ export class PanelController { } @Controller('system-prompts/set') - async setSystemPrompt(client: RequestClientType, data: any, webview: PostMessageble) { + async setSystemPrompt(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); const { name, content } = data; await systemPromptDB.insert({ @@ -44,7 +48,8 @@ export class PanelController { } @Controller('system-prompts/delete') - async deleteSystemPrompt(client: RequestClientType, data: any, webview: PostMessageble) { + async deleteSystemPrompt(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); const { name } = data; await systemPromptDB.delete(name); return { @@ -54,7 +59,8 @@ export class PanelController { } @Controller('system-prompts/save') - async saveSystemPrompts(client: RequestClientType, data: any, webview: PostMessageble) { + async saveSystemPrompts(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); const { prompts } = data; await Promise.all(prompts.map((prompt: any) => { @@ -72,8 +78,8 @@ export class PanelController { } @Controller('system-prompts/load') - async loadSystemPrompts(client: RequestClientType, data: any, webview: PostMessageble) { - + async loadSystemPrompts(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); const queryPrompts = await systemPromptDB.findAll(); const prompts = []; for (const prompt of queryPrompts) { diff --git a/service/src/server.ts b/service/src/server.ts index d5dfb4b..b036b45 100644 --- a/service/src/server.ts +++ b/service/src/server.ts @@ -29,13 +29,13 @@ const logger = pino({ export type MessageHandler = (message: VSCodeMessage) => void; interface IStdioLaunchSignature { - type: 'stdio'; + type: 'STDIO'; commandString: string; cwd: string; } interface ISSELaunchSignature { - type: 'sse'; + type: 'SSE'; url: string; oauth: string; } @@ -44,7 +44,7 @@ export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature; function refreshConnectionOption(envPath: string) { const defaultOption = { - type: 'stdio', + type: 'STDIO', command: 'mcp', args: ['run', 'main.py'], cwd: '../server' @@ -84,7 +84,7 @@ function updateConnectionOption(data: any) { if (data.connectionType === 'STDIO') { const connectionItem = { - type: 'stdio', + type: 'STDIO', command: data.command, args: data.args, cwd: data.cwd.replace(/\\/g, '/') @@ -93,7 +93,7 @@ function updateConnectionOption(data: any) { fs.writeFileSync(envPath, JSON.stringify(connectionItem, null, 4)); } else { const connectionItem = { - type: 'sse', + type: 'SSE', url: data.url, oauth: data.oauth }; @@ -155,14 +155,14 @@ wss.on('connection', (ws: any) => { switch (command) { case 'web/launch-signature': - const launchResultMessage: ILaunchSigature = option.type === 'stdio' ? + const launchResultMessage: ILaunchSigature = option.type === 'STDIO' ? { - type: 'stdio', + type: 'STDIO', commandString: option.command + ' ' + option.args.join(' '), cwd: option.cwd || '' } : { - type: 'sse', + type: 'SSE', url: option.url, oauth: option.oauth || '' }; diff --git a/service/src/setting/setting.controller.ts b/service/src/setting/setting.controller.ts index 12ddc3b..3a0a6b2 100644 --- a/service/src/setting/setting.controller.ts +++ b/service/src/setting/setting.controller.ts @@ -1,11 +1,14 @@ -import { Controller, RequestClientType } from "../common"; +import { Controller } from "../common"; import { PostMessageble } from "../hook/adapter"; +import { RequestData } from "../common/index.dto"; +import { getClient } from "../mcp/connect.service"; import { getTour, loadSetting, saveSetting, setTour } from "./setting.service"; export class SettingController { @Controller('setting/save') - async saveSetting(client: RequestClientType, data: any, webview: PostMessageble) { + async saveSetting(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); saveSetting(data); console.log('Settings saved successfully'); @@ -16,8 +19,8 @@ export class SettingController { } @Controller('setting/load') - async loadSetting(client: RequestClientType, data: any, webview: PostMessageble) { - + async loadSetting(data: RequestData, webview: PostMessageble) { + const client = getClient(data.clientId); const config = loadSetting(); return { code: 200, @@ -26,10 +29,8 @@ export class SettingController { } @Controller('setting/set-tour') - async setTourController(client: RequestClientType, data: any, webview: PostMessageble) { - + async setTourController(data: any, webview: PostMessageble) { const { userHasReadGuide } = data; - setTour(userHasReadGuide); return { @@ -39,7 +40,7 @@ export class SettingController { } @Controller('setting/get-tour') - async getTourController(client: RequestClientType, data: any, webview: PostMessageble) { + async getTourController(data: any, webview: PostMessageble) { const { userHasReadGuide } = getTour(); diff --git a/software/src/main.ts b/software/src/main.ts index bbbdc87..3d098da 100644 --- a/software/src/main.ts +++ b/software/src/main.ts @@ -40,14 +40,14 @@ function createWindow(): void { switch (command) { case 'electron/launch-signature': - const launchResultMessage: ILaunchSigature = option.type === 'stdio' ? + const launchResultMessage: ILaunchSigature = option.type === 'STDIO' ? { - type: 'stdio', + type: 'STDIO', commandString: option.command + ' ' + option.args.join(' '), cwd: option.cwd || '' } : { - type: 'sse', + type: 'SSE', url: option.url, oauth: option.oauth || '' }; diff --git a/software/src/util.ts b/software/src/util.ts index 21fa7d2..43584f6 100644 --- a/software/src/util.ts +++ b/software/src/util.ts @@ -24,13 +24,13 @@ export class ElectronIPCLike { interface IStdioLaunchSignature { - type: 'stdio'; + type: 'STDIO'; commandString: string; cwd: string; } interface ISSELaunchSignature { - type:'sse'; + type:'SSE'; url: string; oauth: string; } @@ -39,7 +39,7 @@ export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature; export function refreshConnectionOption(envPath: string) { const defaultOption = { - type:'stdio', + type:'STDIO', command: 'mcp', args: ['run', 'main.py'], cwd: '../server' @@ -80,7 +80,7 @@ export function updateConnectionOption(data: any) { if (data.connectionType === 'STDIO') { const connectionItem = { - type: 'stdio', + type: 'STDIO', command: data.command, args: data.args, cwd: data.cwd.replace(/\\/g, '/') @@ -89,7 +89,7 @@ export function updateConnectionOption(data: any) { fs.writeFileSync(envPath, JSON.stringify(connectionItem, null, 4)); } else { const connectionItem = { - type: 'sse', + type: 'SSE', url: data.url, oauth: data.oauth }; diff --git a/src/global.ts b/src/global.ts index 504733f..9cef060 100644 --- a/src/global.ts +++ b/src/global.ts @@ -7,7 +7,7 @@ export type FsPath = string; export const panels = new Map(); export interface IStdioConnectionItem { - type: 'stdio'; + type: 'STDIO'; name: string; version?: string; command: string; @@ -18,7 +18,7 @@ export interface IStdioConnectionItem { } export interface ISSEConnectionItem { - type: 'sse'; + type: 'SSE'; name: string; version: string; url: string; @@ -29,13 +29,13 @@ export interface ISSEConnectionItem { interface IStdioLaunchSignature { - type: 'stdio'; + type: 'STDIO'; commandString: string; cwd: string; } interface ISSELaunchSignature { - type:'sse'; + type:'SSE'; url: string; oauth: string; } @@ -123,7 +123,7 @@ export function getWorkspaceConnectionConfig() { if (item.filePath && item.filePath.startsWith('{workspace}')) { item.filePath = item.filePath.replace('{workspace}', workspacePath).replace(/\\/g, '/'); } - if (item.type === 'stdio' && item.cwd && item.cwd.startsWith('{workspace}')) { + if (item.type === 'STDIO' && item.cwd && item.cwd.startsWith('{workspace}')) { item.cwd = item.cwd.replace('{workspace}', workspacePath).replace(/\\/g, '/'); } } @@ -169,7 +169,7 @@ export function saveWorkspaceConnectionConfig(workspace: string) { if (item.filePath && item.filePath.replace(/\\/g, '/').startsWith(workspacePath)) { item.filePath = item.filePath.replace(workspacePath, '{workspace}').replace(/\\/g, '/'); } - if (item.type ==='stdio' && item.cwd && item.cwd.replace(/\\/g, '/').startsWith(workspacePath)) { + if (item.type ==='STDIO' && item.cwd && item.cwd.replace(/\\/g, '/').startsWith(workspacePath)) { item.cwd = item.cwd.replace(workspacePath, '{workspace}').replace(/\\/g, '/'); } } @@ -213,7 +213,7 @@ export function updateWorkspaceConnectionConfig( if (data.connectionType === 'STDIO') { const connectionItem: IStdioConnectionItem = { - type: 'stdio', + type: 'STDIO', name: data.serverInfo.name, version: data.serverInfo.version, command: data.command, @@ -234,7 +234,7 @@ export function updateWorkspaceConnectionConfig( } else { const connectionItem: ISSEConnectionItem = { - type: 'sse', + type: 'SSE', name: data.serverInfo.name, version: data.serverInfo.version, url: data.url, @@ -267,7 +267,7 @@ export function updateInstalledConnectionConfig( if (data.connectionType === 'STDIO') { const connectionItem: IStdioConnectionItem = { - type: 'stdio', + type: 'STDIO', name: data.serverInfo.name, version: data.serverInfo.version, command: data.command, @@ -287,7 +287,7 @@ export function updateInstalledConnectionConfig( } else { const connectionItem: ISSEConnectionItem = { - type: 'sse', + type: 'SSE', name: data.serverInfo.name, version: data.serverInfo.version, url: data.url, diff --git a/src/sidebar/installed.service.ts b/src/sidebar/installed.service.ts index 42b2720..542ebc3 100644 --- a/src/sidebar/installed.service.ts +++ b/src/sidebar/installed.service.ts @@ -52,7 +52,7 @@ export async function validateAndGetCommandPath(commandString: string, cwd?: str export async function acquireInstalledConnection(): Promise { // 让用户选择连接类型 - const connectionType = await vscode.window.showQuickPick(['stdio', 'sse'], { + const connectionType = await vscode.window.showQuickPick(['STDIO', 'SSE'], { placeHolder: '请选择连接类型', canPickMany: false }); @@ -61,7 +61,7 @@ export async function acquireInstalledConnection(): Promise { // 让用户选择连接类型 - const connectionType = await vscode.window.showQuickPick(['stdio', 'sse'], { + const connectionType = await vscode.window.showQuickPick(['STDIO', 'SSE'], { placeHolder: '请选择连接类型' }); @@ -14,7 +14,7 @@ export async function acquireUserCustomConnection(): Promise Date: Mon, 19 May 2025 04:38:36 +0800 Subject: [PATCH 2/3] update --- .vscode/tabs.example-servers_puppeteer.json | 28 ++ package.json | 2 +- renderer/src/api/message-bridge.ts | 7 +- renderer/src/hook/panel.ts | 94 ++--- .../src/views/connect/connection-args.vue | 6 +- renderer/src/views/connect/connection-item.ts | 278 +++++++++++-- renderer/src/views/connect/connection.ts | 386 ------------------ renderer/src/views/connect/env-var.vue | 1 - renderer/src/views/connect/index.ts | 30 ++ renderer/src/views/connect/type.ts | 80 ++++ 10 files changed, 444 insertions(+), 468 deletions(-) delete mode 100644 renderer/src/views/connect/connection.ts create mode 100644 renderer/src/views/connect/index.ts create mode 100644 renderer/src/views/connect/type.ts diff --git a/.vscode/tabs.example-servers_puppeteer.json b/.vscode/tabs.example-servers_puppeteer.json index e8d179d..48644ce 100644 --- a/.vscode/tabs.example-servers_puppeteer.json +++ b/.vscode/tabs.example-servers_puppeteer.json @@ -331,6 +331,34 @@ "state": "success", "serverName": "Huoshan DeepSeek" } + }, + { + "role": "user", + "content": "请将下面的更新日志翻译成 GitHub release 风格的英文说明,请只返回翻译后的结果,不要出现任何多余的前缀: ", + "extraInfo": { + "created": 1747598248965, + "state": "success", + "serverName": "deepseek" + } + }, + { + "role": "assistant", + "content": "Here’s the translation in GitHub release style:\n\n---\n\n### **New Features**\n- Added support for real-time data synchronization.\n- Introduced a new dashboard for analytics.\n\n### **Improvements**\n- Optimized query performance for large datasets.\n- Enhanced error handling for API requests.\n\n### **Bug Fixes**\n- Fixed an issue where the system crashed on invalid input.\n- Resolved a memory leak in the background service.\n\n### **Breaking Changes**\n- Removed deprecated endpoints (`/v1/old`).\n- Updated the authentication mechanism to use OAuth 2.0.\n\n### **Documentation**\n- Added detailed guides for API integration.\n- Improved inline code comments.", + "extraInfo": { + "created": 1747598260799, + "state": "success", + "serverName": "deepseek", + "usage": { + "prompt_tokens": 7946, + "completion_tokens": 136, + "total_tokens": 8082, + "prompt_tokens_details": { + "cached_tokens": 896 + }, + "prompt_cache_hit_tokens": 896, + "prompt_cache_miss_tokens": 7050 + } + } } ], "settings": { diff --git a/package.json b/package.json index 7bfea14..23a83f8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "openmcp", "displayName": "OpenMCP", "description": "An all in one MCP Client/TestTool", - "version": "0.0.8", + "version": "0.1.0", "publisher": "kirigaya", "author": { "name": "kirigaya", diff --git a/renderer/src/api/message-bridge.ts b/renderer/src/api/message-bridge.ts index 1229688..f652321 100644 --- a/renderer/src/api/message-bridge.ts +++ b/renderer/src/api/message-bridge.ts @@ -19,6 +19,11 @@ interface AddCommandListenerOption { once: boolean // 只调用一次就销毁 } +export interface ICommandRequestData { + clientId?: string; + [key: string]: any; +} + export class MessageBridge { private ws: WebSocket | null = null; private handlers = new Map>(); @@ -206,7 +211,7 @@ export class MessageBridge { * @param data * @returns */ - public commandRequest(command: string, data?: any): Promise> { + public commandRequest(command: string, data?: ICommandRequestData): Promise> { return new Promise((resolve, reject) => { this.addCommandListener(command, (data) => { resolve(data as RestFulResponse); diff --git a/renderer/src/hook/panel.ts b/renderer/src/hook/panel.ts index 0713523..4fae94c 100644 --- a/renderer/src/hook/panel.ts +++ b/renderer/src/hook/panel.ts @@ -3,6 +3,7 @@ import { pinkLog } from "@/views/setting/util"; import { debugModes, tabs } from "@/components/main-panel/panel"; import { markRaw, ref, nextTick } from "vue"; import { v4 as uuidv4 } from 'uuid'; +import type { McpClient } from "@/views/connect/connection-item"; interface SaveTabItem { name: string; @@ -12,64 +13,55 @@ interface SaveTabItem { storage: Record; } -interface SaveTab { +export interface SaveTab { tabs: SaveTabItem[] currentIndex: number } export const panelLoaded = ref(false); -export function loadPanels() { - - return new Promise((resolve, reject) => { - const bridge = useMessageBridge(); - - bridge.addCommandListener('panel/load', data => { - if (data.code !== 200) { - pinkLog('tabs 加载失败'); - console.log(data.msg); - - } else { - const persistTab = data.msg as SaveTab; - - pinkLog('tabs 加载成功'); - - if (persistTab.tabs.length === 0) { - // 空的,直接返回不需要管 - panelLoaded.value = true; - resolve(void 0); - return; - } - - tabs.activeIndex = 0; - tabs.content = []; - - for (const tab of persistTab.tabs || []) { - - const component = tab.componentIndex >= 0? markRaw(debugModes[tab.componentIndex]) : undefined; - - tabs.content.push({ - id: uuidv4(), - name: tab.name, - icon: tab.icon, - type: tab.type, - componentIndex: tab.componentIndex, - component: component, - storage: tab.storage - }); - } - - tabs.activeIndex = persistTab.currentIndex; - } - - panelLoaded.value = true; - resolve(void 0); - }, { once: true }); - - bridge.postMessage({ - command: 'panel/load' - }); +export async function loadPanels(client: McpClient) { + const bridge = useMessageBridge(); + const { code, msg } = await bridge.commandRequest('panel/load', { + clientId: client.clientId }); + if (code !== 200) { + pinkLog('tabs 加载失败'); + console.log(msg); + + } else { + const persistTab = msg; + + pinkLog('tabs 加载成功'); + + if (persistTab.tabs.length === 0) { + // 空的,直接返回不需要管 + panelLoaded.value = true; + return; + } + + tabs.activeIndex = 0; + tabs.content = []; + + for (const tab of persistTab.tabs || []) { + + const component = tab.componentIndex >= 0? markRaw(debugModes[tab.componentIndex]) : undefined; + + tabs.content.push({ + id: uuidv4(), + name: tab.name, + icon: tab.icon, + type: tab.type, + componentIndex: tab.componentIndex, + component: component, + storage: tab.storage + }); + } + + tabs.activeIndex = persistTab.currentIndex; + } + + panelLoaded.value = true; } let debounceHandler: NodeJS.Timeout; diff --git a/renderer/src/views/connect/connection-args.vue b/renderer/src/views/connect/connection-args.vue index 1632b0e..148bae5 100644 --- a/renderer/src/views/connect/connection-args.vue +++ b/renderer/src/views/connect/connection-args.vue @@ -25,10 +25,10 @@ {{ t('connect-sigature') }} - +

@@ -65,7 +65,7 @@ const rules = reactive({ oauth: [ { required: false, trigger: 'blur' } ], - urlString: [ + url: [ { required: true, message: 'URL不能为空', trigger: 'blur' } ] }) diff --git a/renderer/src/views/connect/connection-item.ts b/renderer/src/views/connect/connection-item.ts index 891b733..8745b03 100644 --- a/renderer/src/views/connect/connection-item.ts +++ b/renderer/src/views/connect/connection-item.ts @@ -1,11 +1,10 @@ +import { useMessageBridge } from "@/api/message-bridge"; import { reactive, type Reactive } from "vue"; +import type { IConnectionResult, ConnectionTypeOptionItem, IConnectionArgs, IConnectionEnvironment, McpOptions } from "./type"; +import { ElMessage } from "element-plus"; +import { loadPanels, type SaveTab } from "@/hook/panel"; +import { getPlatform } from "@/api/platform"; -export type ConnectionType = 'STDIO' | 'SSE' | 'STREAMABLE_HTTP'; - -export interface ConnectionTypeOptionItem { - value: ConnectionType; - label: string; -} export const connectionSelectDataViewOption: ConnectionTypeOptionItem[] = [ { @@ -22,37 +21,266 @@ export const connectionSelectDataViewOption: ConnectionTypeOptionItem[] = [ } ] -export interface IConnectionArgs { - type: ConnectionType; - commandString?: string; - cwd?: string; - urlString?: string; +export async function getLaunchSignature(platform: string): Promise { + const bridge = useMessageBridge(); + const { code, msg } = await bridge.commandRequest(platform + '/launch-signature'); + + if (code !== 200) { + const message = msg.toString(); + ElMessage.error(message); + return []; + } + + // 判断一下版本,新版本的 msg 应该是数组,老版本是对象 + // 返回的数组的第一个为主节点,其余为从节点 + if (Array.isArray(msg)) { + return msg; + } + return [msg]; } -export class McpClient { - public clientId?: string; - public name?: string; - public version?: string; - public connectionArgs: Reactive; - constructor() { + +export class McpClient { + + public connectionArgs: Reactive; + public connectionResult: Reactive; + + public presetsEnvironment: string[] = ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER']; + public connectionEnvironment: Reactive; + + constructor( + public clientVersion: string = '0.0.1', + public clientNamePrefix: string = 'openmcp.connect' + ) { + // 连接入参 this.connectionArgs = reactive({ type: 'STDIO', commandString: '', cwd: '', - urlString: '' + url: '', + oauth: '' + }); + + // 连接出参 + this.connectionResult = reactive({ + success: false, + status: 'disconnected', + clientId: '', + name: '', + version: '', + logString: [] + }); + + // 环境变量 + this.connectionEnvironment = reactive({ + data: [], + newKey: '', + newValue: '' }); } - async connect() { + async acquireConnectionSignature(args: IConnectionArgs) { + this.connectionArgs.type = args.type; + this.connectionArgs.commandString = args.commandString || ''; + this.connectionArgs.cwd = args.cwd || ''; + this.connectionArgs.url = args.url || ''; + this.connectionArgs.oauth = args.oauth || ''; + } + get clientId() { + return this.connectionResult.clientId; + } + + get name() { + return this.connectionResult.name; + } + + get version() { + return this.connectionResult.version; + } + + get status() { + return this.connectionResult.status; + } + + get connected() { + return this.connectionResult.success; + } + + get env() { + const env = {} as Record; + this.connectionEnvironment.data.forEach(item => { + env[item.key] = item.value; + }); + return env; + } + + private get commandAndArgs() { + const commandString = this.connectionArgs.commandString; + + if (!commandString) { + return { command: '', args: [] }; + } + + const args = commandString.split(' '); + const command = args.shift() || ''; + + return { command, args }; + } + + get connectOption() { + const { command, args } = this.commandAndArgs; + const env = this.env; + const url = this.connectionArgs.url; + const oauth = this.connectionArgs.oauth; + const connectionType = this.connectionArgs.type; + + const clientName = this.clientNamePrefix + '.' + this.connectionArgs.type; + const clientVersion = this.clientVersion; + + const option: McpOptions = { + connectionType, + command, + args, + url, + oauth, + clientName, + clientVersion, + env, + serverInfo: { + name: this.connectionResult.name, + version: this.connectionResult.version + } + }; + + return option; + } + + public async connect(platform: string) { + const bridge = useMessageBridge(); + const { code, msg } = await bridge.commandRequest('connect', this.connectOption); + + this.connectionResult.success = (code === 200); + + if (code !== 200) { + const message = msg.toString(); + this.connectionResult.logString.push({ + type: 'error', + message + }); + + ElMessage.error(message); + return; + } + + this.connectionResult.status = msg.status; + this.connectionResult.clientId = msg.clientId; + this.connectionResult.name = msg.name; + this.connectionResult.version = msg.version; + + // 同步成功的连接参数到后端,更新 vscode treeview 中的列表 + const deserializeOption = JSON.parse(JSON.stringify(this.connectOption)); + + bridge.postMessage({ + command: platform + '/update-connection-sigature', + data: deserializeOption + }); + } + + /** + * @description 处理环境变量开关 + * - 开启时,刷新预设环境变量的数值 + * - 关闭时,清空预设环境变量的数值 + * @param enabled + */ + public async handleEnvSwitch(enabled: boolean) { + const presetVars = this.presetsEnvironment; + if (enabled) { + const values = await this.lookupEnvVar(presetVars); + + if (values) { + // 将 key values 合并进 connectionEnv.data 中 + // 若已有相同的 key, 则替换 value + for (let i = 0; i < presetVars.length; i++) { + const key = presetVars[i]; + const value = values[i]; + const sameNameItems = this.connectionEnvironment.data.filter(item => item.key === key); + if (sameNameItems.length > 0) { + const conflictItem = sameNameItems[0]; + conflictItem.value = value; + } else { + this.connectionEnvironment.data.push({ + key: key, value: value + }); + } + } + } + } else { + // 清空 connectionEnv.data 中所有 key 为 presetVars 的项 + const reserveItems = this.connectionEnvironment.data.filter(item => !presetVars.includes(item.key)); + this.connectionEnvironment.data = reserveItems; + } + } + + + /** + * @description 查询环境变量 + * @param varNames + * @returns + */ + public async lookupEnvVar(varNames: string[]) { + const bridge = useMessageBridge(); + const { code, msg } = await bridge.commandRequest('lookup-env-var', { keys: varNames }); + + if (code === 200) { + this.connectionResult.logString.push({ + type: 'info', + message: '预设环境变量同步完成' + }); + + return msg; + } else { + this.connectionResult.logString.push({ + type: 'error', + message: '预设环境变量同步失败: ' + msg + }); + } } } -// 用于描述一个连接的数据结构 -export interface McpServer { - type: ConnectionType; - clientId: string; - name: string; -} \ No newline at end of file +class McpClientAdapter { + public clients: McpClient[] = []; + + constructor( + public platform: string + ) {} + + public async launch() { + const launchSignature = await getLaunchSignature(this.platform); + + for (const item of launchSignature) { + const client = new McpClient(); + + // 同步连接参数 + await client.acquireConnectionSignature(item); + + // 同步环境变量 + await client.handleEnvSwitch(true); + + // 连接 + await client.connect(this.platform); + + this.clients.push(client); + } + } + + public async loadPanels() { + const masterNode = this.clients[0]; + await loadPanels(masterNode); + } +} + +const platform = getPlatform(); +export const mcpClientAdapter = new McpClientAdapter(platform); \ No newline at end of file diff --git a/renderer/src/views/connect/connection.ts b/renderer/src/views/connect/connection.ts deleted file mode 100644 index 8715b99..0000000 --- a/renderer/src/views/connect/connection.ts +++ /dev/null @@ -1,386 +0,0 @@ -import { useMessageBridge } from '@/api/message-bridge'; -import { reactive, ref } from 'vue'; -import { pinkLog } from '../setting/util'; -import { ElLoading, ElMessage } from 'element-plus'; -import { getPlatform, type OpenMcpSupportPlatform } from '@/api/platform'; -import { getTour, loadSetting } from '@/hook/setting'; -import { loadPanels } from '@/hook/panel'; -import type { ConnectionType } from './connection-item'; - -export const connectionMethods = reactive<{ - current: ConnectionType, - data: { - value: ConnectionType, - label: string - }[] -}>({ - current: 'STDIO', - data: [ - { - value: 'STDIO', - label: 'STDIO' - }, - { - value: 'SSE', - label: 'SSE' - }, - { - value: 'STREAMABLE_HTTP', - label: 'STREAMABLE_HTTP' - } - ] -}); - -export const connectionSettingRef = ref(null); -export const connectionLogRef = ref(null); - -// 主 mcp 服务器的连接参数 -export const connectionArgs = reactive({ - commandString: '', - cwd: '', - oauth: '', - urlString: '' -}); - -export interface EnvItem { - key: string - value: string -} - -export interface IConnectionEnv { - data: EnvItem[] - newKey: string - newValue: string -} - -export interface ConnectionResult { - status: string - clientId: string - name: string - version: string -} - -export const connectionEnv = reactive({ - data: [], - newKey: '', - newValue: '' -}); - -export function makeEnv() { - const env = {} as Record; - connectionEnv.data.forEach(item => { - env[item.key] = item.value; - }); - return env; -} - - -// 定义命令行参数接口 -export interface McpOptions { - connectionType: ConnectionType; - // STDIO 特定选项 - command?: string; - args?: string[]; - cwd?: string; - env?: Record; - // SSE 特定选项 - url?: string; - // 通用客户端选项 - clientName?: string; - clientVersion?: string; -} - -/** - * @description 试图启动 mcp 服务器,它会 - * 1. 请求启动参数 - * 2. 启动 mcp 服务器 - * 3. 将本次的启动参数同步到本地 - * @param option - * @returns - */ -export async function doConnect( - option: { - namespace: OpenMcpSupportPlatform - updateCommandString?: boolean - } -) { - const { - // updateCommandString 为 true 代表是初始化阶段 - namespace, - updateCommandString = true - } = option; - - // 如果是初始化,则需要请求启动参数 - if (updateCommandString) { - pinkLog('请求启动参数'); - const connectionItem = await getLaunchSignature(namespace + '/launch-signature'); - connectionMethods.current = connectionItem.type; - connectionArgs.commandString = connectionItem.commandString || ''; - connectionArgs.cwd = connectionItem.cwd || ''; - connectionArgs.oauth = connectionItem.oauth || ''; - connectionArgs.urlString = connectionItem.url || ''; - } - - if (connectionMethods.current === 'STDIO') { - return await launchStdio(namespace); - } else { - return await launchRemote(namespace); - } -} - -async function launchStdio(namespace: string) { - const bridge = useMessageBridge(); - const env = makeEnv(); - - const commandComponents = connectionArgs.commandString.split(/\s+/g); - const command = commandComponents[0]; - commandComponents.shift(); - - const connectOption = { - connectionType: 'STDIO', - command: command, - args: commandComponents, - cwd: connectionArgs.cwd, - clientName: 'openmcp.connect.STDIO', - clientVersion: '0.0.1', - env - }; - - const { code, msg } = await bridge.commandRequest('connect', connectOption); - - connectionResult.success = (code === 200); - - if (code === 200) { - - const message = `connect to ${msg.name} ${msg.version} success, clientId: ${msg.clientId}`; - connectionResult.logString.push({ type: 'info', message }); - - connectionResult.serverInfo.name = msg.name || ''; - connectionResult.serverInfo.version = msg.version || ''; - connectionResult.clientId = msg.clientId || ''; - - // 同步信息到 后端 - const commandComponents = connectionArgs.commandString.split(/\s+/g); - const command = commandComponents[0]; - commandComponents.shift(); - - const clientStdioConnectionItem = { - serverInfo: connectionResult.serverInfo, - connectionType: 'STDIO', - name: 'openmcp.connect.STDIO', - command: command, - args: commandComponents, - cwd: connectionArgs.cwd, - env - }; - - bridge.postMessage({ - command: namespace + '/update-connection-sigature', - data: JSON.parse(JSON.stringify(clientStdioConnectionItem)) - }); - - } else { - const messaage = msg.toString(); - connectionResult.logString.push({ - type: 'error', - message: messaage - }); - - ElMessage.error(messaage); - } -} - -async function launchRemote(namespace: string) { - const bridge = useMessageBridge(); - const env = makeEnv(); - - const connectOption: McpOptions = { - connectionType: connectionMethods.current, - url: connectionArgs.urlString, - clientName: 'openmcp.connect.' + connectionMethods.current, - clientVersion: '0.0.1', - env - }; - - const { code, msg } = await bridge.commandRequest('connect', connectOption); - - connectionResult.success = (code === 200); - - if (code === 200) { - const message = `connect to ${msg.name} ${msg.version} success, clientId: ${msg.clientId}`; - - connectionResult.logString.push({ - type: 'info', - message: message - }); - - connectionResult.serverInfo.name = msg.name || ''; - connectionResult.serverInfo.version = msg.version || ''; - connectionResult.clientId = msg.clientId || ''; - - // 同步信息到 vscode - const clientSseConnectionItem = { - serverInfo: connectionResult.serverInfo, - connectionType: connectionMethods.current, - name: 'openmcp.connect.' + connectionMethods.current, - url: connectionArgs.urlString, - oauth: connectionArgs.oauth, - env: env - }; - - bridge.postMessage({ - command: namespace + '/update-connection-sigature', - data: JSON.parse(JSON.stringify(clientSseConnectionItem)) - }); - - } else { - const message = msg.toString(); - connectionResult.logString.push({ - type: 'error', - message: message - }); - - ElMessage.error(message); - } -} - - -async function getLaunchSignature(signatureName: string) { - const bridge = useMessageBridge(); - const { code, msg } = await bridge.commandRequest(signatureName); - - return msg; -} - -export function doReconnect() { - // TODO: finish this - console.log(); -} - -export const connectionResult = reactive<{ - success: boolean, - logString: { type: 'info' | 'error' | 'warning', message: string }[], - serverInfo: { - name: string, - version: string - }, - clientId: string -}>({ - success: false, - logString: [], - serverInfo: { - name: '', - version: '' - }, - clientId: '' -}); - - - -export const envVarStatus = { - launched: false -}; - -function lookupEnvVar(varNames: string[]) { - const bridge = useMessageBridge(); - - return new Promise((resolve, reject) => { - bridge.addCommandListener('lookup-env-var', data => { - const { code, msg } = data; - - if (code === 200) { - connectionResult.logString.push({ - type: 'info', - message: '预设环境变量同步完成' - }); - - resolve(msg); - } else { - connectionResult.logString.push({ - type: 'error', - message: '预设环境变量同步失败: ' + msg - }); - - resolve(undefined); - } - }, { once: true }); - - console.log(varNames); - - - bridge.postMessage({ - command: 'lookup-env-var', - data: { - keys: varNames - } - }) - }); -} - - -export async function handleEnvSwitch(enabled: boolean) { - const presetVars = ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER']; - - if (enabled) { - const values = await lookupEnvVar(presetVars); - - if (values) { - // 将 key values 合并进 connectionEnv.data 中 - // 若已有相同的 key, 则替换 value - for (let i = 0; i < presetVars.length; i++) { - const key = presetVars[i]; - const value = values[i]; - const sameNameItems = connectionEnv.data.filter(item => item.key === key); - if (sameNameItems.length > 0) { - const conflictItem = sameNameItems[0]; - conflictItem.value = value; - } else { - connectionEnv.data.push({ - key: key, value: value - }); - } - } - } - } else { - // 清空 connectionEnv.data 中所有 key 为 presetVars 的项 - const reserveItems = connectionEnv.data.filter(item => !presetVars.includes(item.key)); - connectionEnv.data = reserveItems; - } -} - -export async function loadEnvVar() { - return await handleEnvSwitch(true); -} - -export async function initialise() { - - pinkLog('准备请求设置'); - - const loading = ElLoading.service({ - fullscreen: true, - lock: true, - text: 'Loading', - background: 'rgba(0, 0, 0, 0.7)' - }); - const platform = getPlatform(); - - // 加载全局设置 - loadSetting(); - - // 设置环境变量 - await loadEnvVar(); - - // 获取引导状态 - await getTour(); - - // 尝试进行初始化连接 - await doConnect({ - namespace: platform, - updateCommandString: true - }); - - // loading panels - await loadPanels(); - - loading.close(); -} \ No newline at end of file diff --git a/renderer/src/views/connect/env-var.vue b/renderer/src/views/connect/env-var.vue index f56f284..73024de 100644 --- a/renderer/src/views/connect/env-var.vue +++ b/renderer/src/views/connect/env-var.vue @@ -44,7 +44,6 @@