@0nGWT@t$2$YfP9)qr_TE-Hfd;nQ
zNc;iuv(}_OSQB@AJnJ}ZUb4dZn&4jdqkBp?%IZ+bx_7Tenq?M;$FY0YH5Ceel@<&R
z*UX<~buL`-jwZ-MZSo9|qQ8H50NRxYH?=obLeVzV=PzuN*L05d_z)X#j#`P&PwAz_
z@GuT%6Z-Cdo_~x{Lzr|G#IKoKD#{hUP6-SyvRC{9$)2)q5S63a$|3l_d+xLrp}s26*@{@TCa-;kRic_
zybQg*KKHDee11pzaTpz7q2E%ZX)$;fWj{^ezHJyd{A!RcE4*f_Q%)8tMNf3CDu>YhA487J8t6T7Eps8RCPQPc4wV
z(ym1ZNsTth=YOMfXj`}%PFr}_Ly5T~i7&rAg#U4Yx=bjAAkF%Njgl_YTN`lxq%X&f
z)Dg3reCupp9qJM=+zu)~?L6|(9ib*0K04GGGU#EmP=&>je;4>RtUFKDY(o{@$Q4sgE9eUSHUu9Z+q
zRhlh4@3*|8g;-Fe&Y-4WK}iWyrC(lcf=HGjlI1?;kzm&i1*&q>-v?m82<0<3{>6Y9
z`xgV|*=<>o;O+pXtTnfIaz)R&qtnlOcGENp7<0wE;NYs-z#S&8qS67Pt)rjs;Y4av
z1B<1_fsGc~u!w?gM}kDwVCxt&7H)x>?~--e`CeW}-1*DOfz){gS!T9hvz{jn#Zmck
z0T$-W5hEs_UkDN}qhc>vMFySm17~rXBvV(Cv@YgsCciy_F3H{9*?(dA{#~2tf93}I
zNN+GUf7ipSN~|%JBEi8MohQ|2l^7_&nTgh$SprH)*azAmZl<^7;LU>KAODY0Wy8=4
zPS?-_H|8q^e7Ox+njECr+Una3VK#{`MC_R6aJD1t@RSG7rVQs$!7+$s3mf>bXv!L+
z1v<2vI8j|`#nag27ts&N^nYCWAK+W7;M_lQT(OBq&AtA7s2Zss7M8$yzd{FZ&4e{}
z7;kC-%zpdbOZ~fT)T^@cF&6f92Z^cf)$VP>v!m-}2?WWDIZP98y1Z=TfuDUj*@Y&I
ztI@-bhMLPpGfE~*v7OYX#=LH!oe!Tw7CxCAXAo)tb(-@Jx80WG-@Cb{jTNaaWB2Da
z@Jxwus5a`5|6{NCh-PW?Q2u=Sz$xO
z%3$;+P>b)UPnRz{`@`7DFCt`A;y>kOlrtU*tJWneND(}#gJ3eCzbOJIXTrNLl>Dbx9Kv5R^_Gdesn@HljelfCD1#bVs`Dt<5
zB1gt}oN5z2yv*gO*|Ik2f)zz4&YZ8@m1<&v63GOCO
zOMg@;LjpkFXR-iBHWH9qRStXUdggBHQ7`Dt{2GS7h$ZFjv$F$9wq_9KendV*A@>EB
z_I0y@Gy^Z7X{d+sG9851?WsOw2SU}4=jUYYt(p6=e$de=$vmUNqLxySB~|~&K{&(L
zBYZ>_7*`Kts2I)6S&=);F21DetoWhzI`L?PjBjXG@OhY4%#JMA;Kl5)(s)>{I`6_qs_YbI>1r(cKm5wqD3ug2IP8twyzRbF&6O1LW#V7fbd%gdKh
z2SnVqzmtC&|I(ma-~1af!vGmE;PL1
z{qUn<^d){jTeiZ`0ZIg?21k%*AFvLxZUQUH&m#EM6RWp`DaAKNKV-sdZM~KzBbqzH
zrZwEZzJ7gCBGA`A9>90wEcCJ`O0FFJz23Jz@YM~c`QnN>f|qksuJ-PNtj%&N;j5dE
zhCXY|<(q?JHZBi-bQCT99@Vvc|xZc8i)p+&z22|YL
zmk2#kJ=q|ezrSdY%e3WSWxc_vP
z9_$=^YF8h$Et_Q2bN+aI>1=%xLF%+^N7qMkyCiEngI-lg!#RvaoPWnn{R8UeG!Lbt
z(EehqV0lhXAGASxt8wSNL+<*}9)>sa(=3{-6-R<7<%E#lD#2qaz!_KbD-|>63te!~
z4L_&ta!D!e!sD?R?c2R)YpKyAN;uo0&>hw@@rG-)3DSm;y4M`4V)T(5C8`!LOeC!^
z8284Pna&0%?loppu6ep_HFS9c{lH>v7^jX2-ZOVf@#1fu81s-|xKUe0cD$aT^SX|y
zN)jP#0WzH>jFLuM`hHRvru7p
ztsV!$cR)!`xT+;^I+)O;hIYPI>3I@4)=kcoZHzN#Ehltc=6VC+xRPn};T~?#HRis7
z6uk7Lu!}h1nNVi-6^3m@g))?Dq#V?H7rDVSLl^XGsVypN)vN7AMLvuntgZ}0LFgyD
z=N{-Ff{d!dTp9k-6}JP@E}Rlz;O~uG!Y4K&aO061IWQw0vapI4j|}kLBx{ys1iRj5
z`v}0&?;p|Sg)L7PkIH~jbn~Ko9F#x|0j`Un8*kSm+*Zh}UMi8SC*A;7lS*4HC6n*|
zWXa8IwE8k4SU_(G`I5J`TCLy(VQJ_UgR1@=E-F2CX#ks!<)v?{1r^tc(1y7rRC0h<
z`1@Z8oh33W`4Rg~3~u06gL=OM7qL!SjyZ*TC#pTMDgSLqWw`lPgm(M$sx_4L0mcV3
z@7-T8RkHPTTJ+fxzO@O%+`TRfz`Us)ozw$P-oBd3v8+prEC78|s;{mK4i5~JT0PCc
zb^13d-v4Q}{LfUr8*%x}q?9yU*k|fb54qarKxxX8bmI-BJoxD6Id}8|4@PItNyye4
z)H^(KzEk?h`AP_%ON|TG{{dkvMp(g;i&tuKKc2*N+x-5Cb&lQ0xsvGj$%n~JOOZk)
z6Z=$J*B#i8&ku@MiCT$eDYw37V5q6!#CTDmpXJ{Oj@z7(j6RNBY|-pn6$m{<=W5N9ix=wm*W9
zQO%!nrdW?$QvGZY^5pG(4<1JS>YY!uPme#x7kZr?XZ%W=J{%x`r5oNuORf@9zi`m8
z9I!SX@OO1i8upCbM3J2ywja)Px82e4&cgyKzny`ic{?~3dKZt74l1OejK6|RK(f}h
z_(h!f6{_vT;T+7sap}I>)V>L@k{|wSB|py|Z7@17?!Mui7k0h95BzXMK<&L(*5h>l
z6gOo~nl73us}C&FGTfMdKm?+lqSoed4|S}SYJE;gp{9f!Ys-NgFKy&V@y1p)44Rtz
zihkwyYROzg0&t>G9+D?gYu6fb%#@GCCbMSVj=Fm~Oz%0W!7T!Uy|ZM)a5AabQTa~V
zT)!>?@Gvnqxh9KrXXZs13ywV#m$XtxWj+CeckmnXMaJIE+4A|knD@u8JIlKTW9s!D
ze%yQOG^Z1l%o_WK^fbZ_;{tMPRVFC98L0mtT2&~fxdVz*D`+#OS04(key|`aNB8J#
zm2_96s93t!4VRO)P>x^f{7y40)`(b05%Q&@wk%2&b@i?;s^TS2g0chQ+ercLk@Yy~
z=7Y5`tsq;
zjO4p}0%{*;za)enxsN@eOl4ZSbv!K@l103&_!%>~3)O0t2#I1(7f4?Wuoo6;Qp$Z{
zAXNK3u{$$vibbvn{B)eDmqOfEQipo3>_{&E++F}mspM`fXFo+>+1G28JCSYpI~6q{
z*qXo=z{vI%A;dT03D=t`X^9bkD&gu@Gf%#E+S+|m%xM&?Sk2G!6bDD71t}CW2u*>S
zo6l4B;G{ch&bH1>%*;>{%;$V>TS0$=Nkr3!aV
z(VfN!H?J(vRw9vmkz*o*o8zwD*V&G96_G&}<9Zzj&s9O;$u-@D%_f%lCe3roVyZcfv0;BV|Lcqz_OrZ=gpGz+tE~&oI+=@i
z)H^;5z0G>x?iz=Hn&o(${^H>;^aGVXz^mkm;O@$p6+w_%nzdxXKWq#eQ
z-1UQBTQ7OXn^+2Hz#;|F0Vw@8u;8NPz}K>I9eDRVvZwoYdH7CnTO+HQ3J`2TW;Q#0
zs1l&3!GAziP9rg_zQ(c)0Jo=Ka9zwiw-61}prCdhSoj9VHF5f6^4=TY2Qh`edKlnC
zdQ>OAOa00CEb-m}LyRgKI80)KUC^%A%2p|EkyBkR&9l4&;0X}!Q~Dey+tH?Elu=ZJ
z@T&Mruzx|TXqO(bOHX>Ivfy!4hw@BehaOeRZaaTTsd4QGhKC~b-#mnu4cP#I3mv8g
zW7s!*>G{Uh=m)nSG0%uY(cQ~I`%ERm4-Mtz!XeQL;
zea{Zn>jQic_-|1Tf%b~U=e-;6WzIB0mFQ(Cm1(2~AOJiSIRu6)$k-co01DLp*2S9|
z#-)cb%yOybqu9M`WDQueQ@~6p;gB0iCIb_hrYcNy3bXbJt+p(RMs?~_(~~Xxt(f2b
zTy5PFuazdl5lOtKojw6#oCO4#emf6-RC#D2OgMWjOklPww;Q2sYVgNT@2&qkE1!Sf
z*MD{e^w*Htv5E1hInYbon=ZY&ct+O%`ngnr5g!;;$m(2Ghg2xiZ*Y36YnD}{8`aW|
zam1oY^b$@w9q%dHGArPPQqd8}U=I5m5#NoZ;~ZIUc6z@#=HkA(n%s|@?Z-2lvKbaS
z8l!xHalU3#CWoZKmcxkm>tVb=0w*{tZ^DjhBht+pA=fEtuTr_H2-;6u*w|9tGO1Gl
z1{UWTRT?(E9P>ywC~09`*)kRUf8q`11TUz0!`k8=dvEGQ=KR
zmez=PBlH{FJ~(&n&UsgJb+M8)rW1AE-|M0{n`$+u#4l0ccBhe35?BqkNEz&EjY@W~
z$)ghr$ABiSzn{xxJ&HXcY%7qi_c77qd-q1lU`~4)Lm{Cvw2J@W4@etbP3AC4nJ#La
z@UmN~vFG&rdb&cO!KTYdsE+1pnh~kUJPB7&*Ie!^KcVnrd85Ic^mfzS8Gjsh%d>)6
zLS|7z;)m?1=h(&HDa0HyKs^XvI+XaN_1#
zrm&Nr{OQbT;Z(wwvS)MywlIfBL8qb_$0doZ@Ks6E(p6}+kBO@;3(E#5ddmtI*A&)v
z!J1oS;uI;iI_x>AuICtAI*Rrt%LoCGnMtJdOUDmyU|+}AW4JVDDNLDM((o_UKwwOHiku-*n-81U;FqRod2>8LQ~!&i=fP@s#;}hexEW
z{Z%H8-q&!3Q~=o^cF+L%E?E^`)yFF_iI0#LDUtgFQZ}zl*?%0ol8Cpkr}Q+|VdEGW
zc)&Ux@~Y|V%O0>0wVB4!#!R;kh&@G+BZ=p33nFKPxm`}W^=^^A}
zaedjXuJ0Wu%^8;h^G$Spm4`}J563%E-E~39{4kKu?;EhGxlG+;E|RsU)|>At&2)bt
zJ3mOMCaXDqlT(z6uEBCx9`2>u
zNjpUsmMxSrmC-nAo>5k3i!)tgdp{O7B&!K!3CL6{tF1qWsUJ$|pCY6pns$e(;#qIV1@-
z1&2ev+{Iy>IyiAtMftQWq@o8eH#Mshay%8XJb{$jua;v
z1A|~s60rszpRh8^Kbjtt#}q;8qg{`N+Q9rot>@W!R?nrbxn@A6|6m=R7=<>Mi;nZy
zI@@9!<~Y9H=hS&;mmAVMeOtQ+YvJ~xberwhQxv_C{2o@etdV0{0L~1e{L4J=5!#2!
zt(-^xOLR6Lq^?j=Ikuaq(nC6&n-IfL7(1N&IB@1(nBL>
z-AX<{-$$E91Zu{u5vl#ZOi(9&zD};4z@wX9rshUP5!B?(z#j;
zIhA~*voh(d&-sj%N;ch0deN^~2Y}4P0Far8uD{+)Dcq+fG!J?R{XcudPTKWQ)e-tD
zg1;Q5trUUz5pS|;B-ASqfBVoV>xIgFYe91bT<2Qxe4_7Pud1+>H?=+j#ny@PEG`tG
zi6Z{yhPN`h&uuC@=yKng)nv>EE_Pb?0PC^b%bbZ8G=Zl@pao5n>R6MNf|>apef;@Z
zH-LV%=Y-m409lCh279ZJP}54}K&Fjoj>6*34$}xIYU|6IYJ5E?J3D@?E;c
z_qLH>UvA-jfzJY%G_wDIRsf~y7ln5CDbF?o`aWRHCsFOsmR9QSP03LUJYF|IKAx8B
zHqlKOsQFO17l^73nqIGvpSJL
z0Hi`QA7k5^=u!_7rAFVS?^{sa#idb!vu!pz>#WRJAPY-cX{P`}Yeber;K_utNxGu9
zhXliN4g-6Tll^HK@M?$T08ZvSojArLxtVQ}RX&%0QmffZ)P;XXmYR
zCMdbnNuDxVwjte00U2!S1rwlpcbDgZ*Bg`tya|?s{$9Ltgh4OV#FnQ2@pe1WRMkJu
ziXBI{iBcBXS6}@~xKU>4ONo_-6?Bv`y?s0f?Zwibg`fh_CN76)`4`p;%^$*mhm!-w
zMb%n&x2G`K8vO)bs*aI828Z;5>@`K}Y=NOUdlkY`(e5pd{rUR2={}ZT&yQWduhW%x
zjdYs7NC-3Czk%ck5PX@Dv%gXYg7mlbd#%w!I!YNVlrI_N$E!f~|B%3g=$+
z=|56Imj5diB-vH|x%FRJF&P9ns|;=4>?^Y2){ZQCI|)5n3)^ybgs7hfoGZ;#H@rti
zU(PH_0oiPb9Nbm1!J5?}N=53HQ1D7zo)lwav3wx1H1Fo=NO6O7IonDQC+(s)%X1h1
z?QW~nlmb_{rA1W5Z&`8=+fSM}N8s=vd^q89N}pyyI~2+s$9gU|K^8#~;^Yp+g%fxW
za=w{dp^00x<#^NdeTH;Ziv(?&B=SZ)%j0QQ1uN&t(5mw12KnVf8W@FGV_N&%I|Jwo
z?GFkpg?ua<`=`j^zQFU*g{ztUL&iuq+7H>$GMt2VA2CnLTOg!2noI7n*9_|MMXi^`
z>})F#ziogR-P{hJ)s~*Zb4lm$zotJ6zGv3ovZy`*r!GIjIsb&3tZ!Ovk91OK1Fzr@
zmtGhy26){TdV$b>CPm(XT#SZt`nb+mh3iB+&{l@v48q1Gz()xE;2gt_5f3bcyUIlj
z_bf@a=)3L~*^JI~{;s#3
zSba{x*TuEwhZ}$)#n}xo=De9uB(f6~gaLIz2xOqb)C;zMe9SPi;n)7Vf08
z9%0Q!?2Q>%39B98*ChN_#(;m#ARKG)eg?yB8D5-23%m-`HlF>g+fkmd+x&jed)L8N
zWGL*8U-=%IP<0YRx$?)Q%lCHiTpLg~(2s9WUb5)E)$RauN68$yRhuIT#9)DFZ$iG6kYAmI)Ldz0ElK}Fpv~dTUepo
zgv-qJ*uT4n7yfP-XB=K$-HD3`zQSMG`PoTd@V>9CLI#()%H=0r9g$(|insbTNqb#F
z!TM#hX|;P5+|w#H^oVeJNQT_--ngK}TPWPI67UV#V?&Y!cOaA&Fx!9y_IOH8Tq<7W
z0?G`+qz2LJx9B2t%Hp*l&9jPHvxlriot5#>ZUFMK*Yg`nEwa@@eba^-$8}wi#y35lp4uH%xacPjDp9m5LSc5
zI-_B!^D!Jmj#iuwgFriE3q{49Llb
z(4<<)z;|Ro&b?CR~T5t3I1UCq)y%Ats62?aB+-=-uQhk-n&MF<<|FhvH`iYRH(r-f88dE14-z7+#g7_k*k5jr6
zq|1|{e58iI+C{-_C0^L*9x&U$cU?ovU6b#6jENVvwXD+iUG7-4KF&w-0bV>s=z1*$
z{6~HI{fVOD@{c5SPBC|lY`iehatQoS+}u3evXkW=q2>I9^IaL6R#ztrzLE~f|CQNV
zWY73~up2|_KoQ)ebXx>|Hjgh2{Ij~^lM;Qu_6oK$h+>wwA~?kuy?J|^_hNwhK%elz
zEO&tzaj`Rt@niT*H`kSR%)7hMCUAoBh;n{g&KmRFJ)9~<$dXaZE`pA6;%tVkY*_xP
zG5aDBr|I`8{n_bc8Va(!AD<;9A!;o&a#v<<;dmyRL5PqLzE(x2j5<*tAk4n&Z*K7U
zldR5tzvbLU1}+kv5(bOe&N@MRl0hfMW-SRU2a=MV?o+_2*?bWfMy}q!oMX+R+RFFE
zc)0YG!^4N{xHzpl#D^5U$!7L*;uC^c&Sx)d%acD^iXlfsTL87%nUF4o@<9~Oe2{JP
zIhI)KS`Ab`6&(A=u>@zZxe~y&2?8Fh?K-lqAPdQ}%Iw}vnMVm`T;W1EHV;bV&p)Dt
zeP)l{5<3%!XIsr3Ev>~qNH9yIpze8I&Mh
zNbkXeSq!rD(nJ&HxB+Ks6TMiq+Mo5N8gX)d6D?crTlR+jfN;Hy+STud&pOMJDg5zz
zirn?6=uTJp9Wi=`FxZgLUoY_YY>z=!0uXi23q;-T8l-_t(S=%3GN^_8f?swC35UW)
zFO8C`%c0+`)RopMF8C9{d)<)9ruEyx%I=Xn;`#Q6OlJ>crdY1?Hy~M4Jw`YcOe;nq
z9t0_UdZOGi&DVt|4C_)ujXvZbG20mko#@yjP7`}4YRBu0^s)=EO0vL_*eDD6cZ38cICa|Jvl;SUn#IQ
zSsz{NrIZw3=qe9yq)9O_`OFFRrL&>-2bH$DOw}ZL+ixu4#tK*bU4dfi#a-=@?O0N
z>J9=whfPN3O|yO5=fk+20e|u_o!avDO1!q(!sb$zo-zsEPZ6q&?tY8pM6{$xxi*&`
z)HyQ*kywsG>lDS-zhchNMBl>Z*JPuq=ww*%9(xcOQTTK%Fka(C9Xh!)5LpRzd}oi?
z?zt7#p4Ir`<9g$YQb)6_JjphrCi-Bvu>s_Yl}6g5X`|YjzTQSjXg@(Ng*6haCcj9=
z0SFTj$5J;BAi1sxiYC%+2=U&&)}6aGk2Unz<^$*}p(f)Q-7`RY?L%RSz<+Vw<8+-|
za=<;KXSoitplSg*(WZb-0n%jBePDX<*zk9^r8Vmbugr@D=F14;Io9$P}QYy!}
z<{sxT_{k2JZEm+E31|xcB=Q=l_KnEa&rYZo*!rwOJ-CL9c(UTWv*#m$u3oz&6TXL0
z+j>``!$KM1y5XlEI!pJUTlYP>zgW}Aq57dNLVVa-IN#|CCwrIXeQZK#buN7l6ALYf
zUuU$>5ig)54@
zfYvyG>xpbHF}BSY6F@Jn|KVLJDW|$#<4C+yfkwV$^ctzw26EY3;jIj7w0krGl0oEZ
zlFseynNn=UM3l5Z_{7RVZK5bMAi
zECxnc=WM%Fk@G#*nj9%o&58uwYe-zl?;c=i0kcuF093gP)gKKT!Wufq{5XIdJ6YD2cwpTATRphKg5_l;FmE7@w|Wi)Q*W__uW$3
zyXdLQCQq|AMz@8Z+6StMFh;4sHj#7M?O);MTj|UZnl@N@gX;6G_Y;o+7UUwB*6qz)
z%N^m*Ve2QBqcgxv;(a|aRAj#wiGN#J+3$op2+G37-qDYB(x4Cip+R!Sb)|v{H#X4H
ztx$fv?Lcf?ogx~W{;d@!3XShAV8rXw;o($}@0O_QeEX|R_K}cSJQiYua~t1Nt75pq
zvr$A&NVa;0z%MoDgj
zWiGnKs%V8&%4$@Hhci4Uc4a8IGd#sOwFVXrN<)z&vg$8DDhf=y2urJ!0)%K-Y-GUSHcyleEToMWDR?
zPJ6F-NoFOAHp1{oAa9#8Zp{vu$lUt_5&(4V6L72sS}!8hq2lPeXua9Vfn<))>l-m2
zvUxy-YL6h7xNmtx(iW9%_)fBv`1y#EKO3z@sw*X{HU$c{WDwNeXzs1&EnCSY)nyzK
z?xWX{Ux3NZ8p%1cm4St%z5Q_@SO^a&qq|?CBG`wUSIx>BhEubjR*N*r>!ntpVGS(_
z=hX}ZOl`;Vd)Dqqg_ShDXSwyU^4CI}C%T~zUGZy%d@9cdWUOJfR^PZXLP>Y41Rp`(
zr1*lpZ!4_Hje_(HdbN14Gct&VvE>HgOokNKoCcN1F6(EnLO;9Z1#dQ~e1-6GZu86z
zPXG3zZG0~Utl_cIQybl9$k@AkO4dz0ZXa3(Z9e-;5wP-#}*`fB0R;$ZJW>Kzxvo&5gk1xAq0_a3yc27H$q#MVG1&^yZZUtxdU%3TZqZpMJjTjiJ5Y7yL?kP
zlDqd4tutw-W_>1VF{D-hIX*@lZG9t}ZEnWPLI7Az+G-&bZWTNBmAS#DRYSkJ3!_ja
z3LCZ_rQZ4TI%Q$sS;Oplo{ICu2GA6?lC=?PX0J1Zryc5v$&YkaKdSe%KC}P
zz2lcOpC{4?+xh3Er(6%u^~q?G=RV67-4@%P-pE@)^4Z%54p|
zKQ8EmxM8^6kYoM@Z~0@g`LJ)rpYzo`>Ql#=3s%@3?0wMAzTvFlvhc)!0Jhl<_*vl~
z(BM(MIAcKLoRxUxR)$724Dj`x>TWry%6f+^0fo{3Y~=jAuK)8@1pj4M*Y1K>2DN*p
zv3HcYmA~~vC^j?xj-xs@J5bKBHPXB0FSODUs!Iz$cSXrd7WJvU{%y)D838y4uOVLU
zQSB~vXxA=$7R!);$_7k(U$}FST613$;^)z3fbD*Qr_p1}C>lKf^8kiS!IS(Br%+dlU+jRYaX1#e9{AY>BGn|)Ev1q(Hilq@)(U^DU=WRq_C=N?BJ)lLP@
z`FZiSh!j{gCgzMB6#2vF@eSh$n6_n1Ki0FbuimsF-cIUN^@ing7<;M>a=&v4f;s)M
zN3(>?t3R0{yU*u{em-cl$^G_c1KjmVF@t|mYPThE&V{V<)g%i?{mw+I0t`T%|mK!_N;v-EC`=za#Y9H-C
z;IfG8h4TS*(KfPmD+cd@X9Tn}F$ev^Ym>m5Fq=XJzE-kdQSHPa)Q^2d4ry-c3
zlW%l+0n#*32dK&wmxq-PSF0=h6nZ&R?_)OVm9VTr0(m4ppfsV`a1-O=JJ`#39tC6!2BI`CjG
z?Rt)y8QbeeAofVdl&b|_;!w`b!zc*t83Tq}78l8@vHgu?V_oxvRaUQg5az5e*e|OF
z?w>9KdF(}ET@3xiWrJIx{wkk?$|HIYR+B*}e4AD5Glg>Ouw1_kU?RrYuJjx55=S*H
z=>cuLz0O}Bc(VFEyxN*>(UmsbO;qRl&l1_t?@DW^TE{8`LwF1U~
zeXO!CW6rdCzXKspdoT4-A~yqozYdr<<0N`1`Nir6e)?b(cJdoZ9s{KW)l<07aa%11eWU1}unkrMs8;jrt-l%}>H6**yG
zjw9BTiUrvwlQJbToFmVaO_;?UQJTq}m)T8#)i(ISIJcR)+x-Ta*i>@J%QsGZBFXce
z^Pdak!at=49=6k{6MS=YTD>RN0&W9>$bW}5S{=W4C}x`VE*!a0_^yxkl-B3uFBEvv
zGww`Z1nYa)+gp;_rJAio`LR*!E{{@SJhc#)
zCI^CJT?G%o3_q#OqphvL$2`Bh^$YT2MjVTFoTPtQSXoI90C59NMMt0ioz=^KzpwxB
zO6I>;NY8jB?{krY-v1@s`uht*TcgSU?O2JeEvwG{{;DNi(h>+;(Z~8m?nrhh+{>b1
z*VS}mL(SwJDms!(e%QFFnfKSu6NefIT4bTp`Zm2gX~7lgtZB{X)i|<8U*SWzrHO6TxXyU$5H(_M@apgS2`oA
zq#WN{K<_aWfQ?@rtF%0hAh%l+Ck5mUZ2b=nKO8A6H3u~@?R>S;3$NBnEX|0>sb<+!
z9eyn9;fH*Bl-M1GjaKd`+e-N|SG)E)HCsq11!u`7iPfBvHxwkG1pT??$>D2@+Y#G2Fq!X#AKF_p)v^F
zP>n5hsL6P{+BvQwC4s3p?1~-EkHnL+luR_nA@`o1g5GOXV07J>1|F;~!$X+I`f(G=
zz3Y_-K)tsDMu>&`Q|73tu+v6E95a}HfJ{`CNo{38$4q6UCzndu&Q0)g
zcXcSBiEX&S!aH|%tGfDe^4#9&OA@i3FYCZwnqje}nbu~YCu2|a%aOS!f1D^}ggayU6=&7Sy%@**yU-h3r|h|I6l4+hNJd-&`dA2Vzqj)uE>f
zL%41!fbgnOD{`mLJp8-|Cy+ta-zeXCX_5X8TXFT)3LhrjSXD7D^dKMFI>$&yAmWaIf-MG1456P*I1UUPz&^C7(=WqQ1-CAu_-aa~%
zt$8MP@C6a0I?~)Ssp!?gCz8sxoo~xB6VIW!FI3{%j4EXX*2McjUKrxm3bJer{R096
z;~_ZYYNAzD`?etT4&F*~#vvJbaYxRII9Y1GH~~C48psCs?@9sItC57~bqe2y7DPk?(ovD#6e$V_HoAh;
z&?8NnfFK=0P^3te8hY;~NS7j^7Xgtj9YXJf8bSzn;W_&p#~b6__r`t4d-pz1##m$h
zj3g^r|3BwHzd6^O--IYn^OJSueJ&ZxlQBLoCw_U4X=1lqKJxLS1Eaj>CGi|B8;_t^
zm*lvB*Eza5l+ti}3XDkMv_9f-`5Qo%Xf9lry^$+M8dlp);uB04`(`p{8|(k2^dMSU
zimKo6pK%CQUtBkWJNMPpFlo{;;CLR{7_H29`-F$z{yp)GW|;A-%6qaQp$i!pbAFwgfquhuz9;R|Z4sY`n=f`{Zj5^*SN388Z~Z
znnR?yqFf7!01KQCic*l@&-KMn?3pmabh*O0_Oh>uF-$(ybl(GEXu?fwB%XD1MtdOn
zCF14-m5V@+68Aj(HWt#zOfLQq$=;Mey*&A}#;v{32*RepcCS<9w}tmou$zZw?XD6`
zOSYpv!a?rj^-rA}gvSn_*BI?R-K>dSXu=Aey~=BnDcx5c8j4B{DZB&d^?L5fogS#7
z5vxmRiVOIA{SH=igw|Ejzjw|-o8&8WLoLOYz?fFf@Y&}{
zzdW@Q)Qws@izZiUgp%&+J$w|3t|4Z*#I6|`eY$ZHurs~HyzX*`qIhKtF;JgKE}}Ka
zw_(2~7s!WDi8)=#%zZBLyffZ%t?K)HZB6*zM?_3NiZu17>w)eISnj5IPom|
zVAY22#I4>Mhv_zlAj)qIvuUkY8e1B3KLBl2Uu=-->a2`7y+achSf^1JPr+KB$`gPl
zHO7QC4-rxRK$Fe2vDYsgl%w|zUN8T;#^
z0-@gmAz#5QKEf+>NewkIv&yK-TUB?$6(-(Ih1T&1t;$#wi}jIj>?l9uzF{U{fd~RLf>`O(kTS<#)-JYT~E4RQEvk
zrxT>yz&CalWujt>>Q(ZW#o9fA7`WlTGz|PBkUAOvg}~I0!0K#WdtrCs2y@FqOA=+F
z$?NvMe7Umng*V`>$#>mL&}6&hFk$J#{NE*m+`Y3sI%0Pw_#&Efz9n3g>p1A_n)hq}
zB0RHUG~p>`^KGc{vZ(3XGG4m{uB!Og1`m+QzYAa4sBMj#;cpc~H96QaQ)PkwI1;
zA);g=D#!0Za_(hn;n>LZs4DML5#io*;;L614A#jtzV6)Rzh5Ri!k1)eaOw`H&4qhP
zIj!0~f3H~!zLOd{%o0kA*dTr;i-e=^_xpP8_N5>Qz0_eYOwft(;E7!ff1MBJ2DT#Y
z=>0O2O%ef7?IDgrPbRHQcuEiefj-5Jwi%^X2**gDi7Bz>N!#*V6?lak8Wq}-TuYkn
z7{r_pMc%jKi3+i{nhgcN52U}stVj+5>PCyKb6C6Ilm$sh8zSTN1Hb679+uxmY~%rI_NWddU&@&g27Bh-xfaF0-#gTS8G0j7Oz_TU^J%
z%Ca&n7@dYt=kB`L%n+{MqGSW=;MH7PS=$7MgT6C}s^7}P*bdzeX}7zH8I`d9M~@0)
zB!oS>wX@#gO(ZI(bpkkgcYHtkIC7RNuHHC*f8`$2%I0J1KlK9u_&x^O
zN>We(509y)tW@5X-WSO!(K6zAc`;b9=r@{%=%3
zLNZ#t`3|3iVUikU{_CF|Mz0GX)xGrvBTjWXOV;VrZoc|LVCc14&4@Ob)s14g)1q{H
z519yT_$R{gqv`vf82>pLnf_mZ5vW%sRvx7NKdBQxe)qqPe*9N->ZgC^PoA0gqageh
z$G?OZ0OmhUh<^z$@I2w4nMwcZ@h{>1CA>dX!2Z<&`{VU~3GXlA{Uy9V&Gi0E;riLv
z{skL;s$%@wd4Hw={}SF`!uyxq)qe@^FX8ZtBrrbhCc^vsP}Ewev#xd
zA-Rvh--4tJ3Lc;a#v42eLKbv4o4@94c1gxqM+`20{bzfoNY>!{5e_yfT~dE9jVtI=
zG9(jJmNxhNA-nUBEYDwODcT>COJe^n`1T+yyg%zOHA$Hz_dcy7sc>)>mA;-z%LS+L
z2a;3I`W4d+Fk<9vdwe~bCC-S)A?eNCshqi}O@t=ATP);o79R9Uf!QZG*m6T~Pe?K{
zo(ilC1-YfY4HmtLUv3iho@HJ}nn0hmF*6oRM%ZlD2~aLUtm`(xr_|@vl0&}^J-+>>7@HasS69EMzN;vCV1qiE
zqq3H~3@ya9O~aF$?OP=p@2=NA5~q?XnpD;`))%ljur!mL;@3c9ykpR^&sDK>OFmX_
z6Z`O;iFCP(4qy$iK1UyOetyt=T
zvG*91m&k^@A!JwujwpZb_(55UHuDtF{{q;!(*!}v(q{kK`qTe4@#w$j|E|BMZN%RC
zTX_=|IK25XuANdMGq<)Vt2JThy=k9w{(?rl1m|^?0Jm#LQ~~Vo>`S|G(ldn5;p!$B
zm_O{>JUqE|8|m_##%x2GI$I5Hyu^W&P@)YatoQiszfhNRhdK{$rM{D>84}!%zwR+W
zc-&*aRqN{{<e*WXRkQCa6gV(-G6EF}aK{DmU7-NaGK*);ZFp*tDV)<%Lz=7Jgv1!Yflgnq^OT)GD-+5GC=Tr7&uGSV760<{zV{
zv;INyq5gxjjORI5#PjxzZLxzGH2Yo-L2~nV=kvXH9sRfbcIn#)Yn^GBJWk}YT<_8`
zh{@8V30zhGep6tvTQRj}UJ`6!ehlJOuFcU2f4%I-rQ
znVXFY1pRA_qyejO{Th!wuJ$o!^imX3iLTJO!A3tS;YrIia|K2$Hy%~J(xdh8MBJ-SaKTOz)VmWm>s<>ZfxlYPk;ZpMvqQ8&W(_*qp($UOGTx)_OyKH|-t5WV
z(=Oomb6`IPA;M7Wff@qcA6ATqEs2#AWA0yZxB`i+&4aN|R3o-?*IwO%Eu~9tvS=#!
zneTWVc4(e;FwOlmQ1xgo%ImpG_&Na*yuDHq6I26o9s@@UVzQM2C{|EcR31RLPHnlVVRdAXl`Y*&=K
zqop$E6hP5r=V6a2Ts9);Ct>mklPUPZlQ)br#Ywqj;je2)1KJgi&CUwBXaU~_@#~_OB%l#HkGU>|tFU6na!n*+;O?Of#R&H16U>&u;s?GznK^X>ULvG-LlvsL7
zq+EvZtfE=?J*C4bWuWp!G3OX`7ibm_hT9laOIE_8VbWQli=_~tyt+XZc&%IQ7*tEl
zycw|!-yV?Mc&&8kMm~Jhfp1(E5)XNlTC+j8n4m;(%{toT0P6Iz07#e!e9E0vvf{&d
z(io~!B2BezN4$6bhc|MfHX|&OwugbM$?WaXmmH#|eETh2m;w}Hx4qD?rvS7Ba&T*j
zMF2>;*@SoZw>C6X(fCiHMVT5xQ@Ig|^&j4^el7{sx3qqpcmKMz02Pf;3
zrB=%xSBct4lf#hny@z|k9>BWQQ)}WU$18MV;2UX=MN|_@WB~+A;b+hrcla6S1&Gs9
z<|AlL9qftA)!2svV+6s5PS+y*GV}s60$5S(sa+C$Ye88j*?wjqOjGSS;VWz2%`cxtsI`(LZbN65f?QkX!!8M1C(y;JV*Y7WdPh}i+i~g@i4`PdYL4(PQoAF
zk^%=?yto~CdGSpV@D_u2Zouv-?=7^aoJ+VeKwmIQky?mV|dxV82w1CdX}BmTKlN
zRhw8D?Mrcv%0M;xbrY3B7WvV56YxS%A42*mOq#oJbL?7o;}9f_ZJV9zH7r=16P1DE>qaQEK}>#
z(T;F?Qi9K_BV(7N2%Y#tt@6ZP1v4YRz!uOsS!5^Q=}a~T%*Ya4-$&iDC9dSQJa_R>
zDSLJQ{70QUdx~I&sZ9_elEin%K3qQ0G9tje@j{F*S>X0TQg4}yp3RFT`yr^(_Ep1_
z_D;KM9oqWV0#>KcC7R8?VOT0lWyw6zR`7j7Da3}t
zL>bqw#9u%vpQc?K<+RIbcx4tHsLg@Q6awM*UPar5E$0S``hb`h)CE`cz&uOYV;;M##u~H-5icpIdU(o9ucry8
zd8}P1x?zE-K43Va8iXMljzQa7lW}lw(GA&N#UNW3L%qG65hn&yOfI1#yU)Vb5z;dT
z)dE<14}+!sISK_Af1*2ZVk`d4$9qPEHy?SS&qkJdhxBS46INLrJ}pqiXqAHRQku?A
zgp*oOHi^h~E~oWJQUw8QF7tUnVY4aW5Q-^GP0&^2P|Pf^_)7BlyvWNka=DAM#H!D?V&;=E#wA)>OCj_$b3q|2MWg;5wS7nj
z-W$s;w&M8$ZE5S-Ga+z`$xGTVbJUfgHKh)#VUJV;O=z}JPAUVa0PE);{cqV&e5rrc
z{mS1aBn0R1@feh8dN3rGWt*ezdI0>s6w*a;7|^khMsfwQE7VY=ZcD=zoL~4nD1k$@
zfJuGKtD$&w^z2;h4&aquA->K#o2Do=!<`jPy7M8z*>kVeUjKf+
zX?y215}Qbva4E=NqZb~;>NUQ1X1x27Sc54w%U3mr*kp48SG_W1%oRGH-4D&lLzCZn
z5``U`m?$OZIKwxQe9l!(V{Z>P5R_3<(bPd_OUFS2IA(?kdCRdk%o5TLjXxm!LDM7)
zKwFbsoO_i=!1EE$7?a8b#Wvr~*&TrIZ^(F@--h42)x`pzST$U|8XDnJNxi>lUT2;?Pg7sum|=dblWU%p3CvnWHFM<2iVc>H@AWmQ
z{E-@Dwr6|^!>M{?@cjDW(;-$bx!NE@O5ZaVARq3Y+C>W5Q&morMe{nyU9XHZRN_GD
z46)$31NfUpzB#T@Z@7J1Y*PKS%&RRu^sVOpG3dUmq$HsweAVO1F(^5o8Stmj7LhFp
zX-cnnd?BQI0aSG}Y%xcnY4ESlJRW?y%n4OG5`A?Yl^qK;z!1U46^XFNpgqA@;p|1k
z=5j?SZJJ>~rRSGh@v4Gmr3rkC-AvhG7c*Xtk)fDF*|w;itqU+;)e6r=PDdE
z%)XpuuDrE$3kO4xu
z-GIAZ3q+w_HN2!RB%i3u2e+*zCk~%%l;?q&Px6DaKp%=a=X}lBbzHL!9#^_6A8*R%
zF-s7(!)e5K+I89~)%Iw4fCo?|>w&5wIKdiWSI*r6D8to&$9-SpBgK?v0gj*UXh_02
zr_$gh{j)>@xV?^#RGUg-iSgbyiD#1%r`+)M}RVcZ?s|%Q32BpJmbFhAbVNsAG~f
zmG)#aO&_UN6JcF5DcL~^vZY1k6(7y7l6~}UyG^sE?AheT(LXd%p5Pkk=X)!}
zIwRU$y0|lxFUF?w8`t&?1DT#k)2)LjoKMI5>eINHyCFkmt}k&jDK3hAZtR~+cg*Gy#j3^Hyc4O5t~H`T
zn-!Kcm$($C+h;bBrcFe21K)^ZM>U+NMy-cq)Wnz|n_C#2SkXOFaUVfj3R>LIt&jlQINJZz|*_D*w4lPI@m}a80
z;*q%;6LRim2Ip(hm1Ni?ecc+Oev(O0(ucxSUL-KS)VE|yXSp>sx5(GDBhjp|9SjTZ>GDE?LaYQw=CK)qO|;-!;qS;i=J$i
z^@`52k@lCpp_@ufA}#ekmPj9*o=l_mrzTX>TB)P5cO6oo>KH_Jj}LzfcIv$}(}U`e
z`;pqMToGd#^8C_?iH`i#sbO*JQd1iTB+JL3b^a}j&!v4rsvt{e6UvY0NI%lj;D!`3
zH6NybTP9iyeQXkv+%N+=1Of$Cg%(fmh0467x4&74Xm7l%>fRePBbS_Wlbwo
z2OK#n1Y`+_)~Pyz)^FU#n;S4OsZV8BP8?a;)N$qdQzY<*dKC(<83GovPlFjjC7r(<
zRIOTVqXjfvo@vR0gzHV@C&h{8#p1KzG|D}unhpYc@bGBF-=@>S5qBg%EVax8Z
zO8K5-Czsh0Kuw7Nt>~n5N@`i8YnXq4E3cm#I?i#-JzS3Cw2Tu`Ci=;XMc0GgHGS&x
zOLHB+@mDaM7iE@jOikdClE1Z(xl5>inI?~=6_u1}z+
zyjWcAiN314a%*Fkv2W8Xd5@Cg!$=vEL)W^@yU^%h=?*
zh*FG>i0vZORXe$R-@K)Rvf3(9ob8B}S~=3#e#*Xf=_YiYpX@-LvKb7yzAT?MA6G3H
z$U8bwjMak*)-ZV5ni*0-SS&^P4YffVx
z<8jTnv{*Abn5NtoYgj4IT}7M777u3OX3gx_zTs?P=iWuyRB};KZnjrh>Sdhwzlt66
z-|#vOz~jC9{|sjS5!C#3{@={Wjlab^IW7PJ-*Z3^_00dTJM6Ff{yF4ff2Mhb9}j<&
a-
Date: Fri, 22 Aug 2025 13:27:38 +0800
Subject: [PATCH 14/15] =?UTF-8?q?=E6=8F=90=E4=BA=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Dockerfile | 9 +++++----
entrypoint.sh | 11 ++++-------
2 files changed, 9 insertions(+), 11 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 108b1f6..822bf45 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -101,9 +101,10 @@ EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
-# 复制启动脚本
+# 复制启动脚本并设置权限
COPY entrypoint.sh /app/entrypoint.sh
-RUN chmod +x /app/entrypoint.sh
+RUN chmod +x /app/entrypoint.sh && \
+ dos2unix /app/entrypoint.sh 2>/dev/null || true
-# 启动命令
-CMD ["/app/entrypoint.sh"]
\ No newline at end of file
+# 启动命令(使用ENTRYPOINT确保脚本被执行)
+ENTRYPOINT ["/bin/bash", "/app/entrypoint.sh"]
\ No newline at end of file
diff --git a/entrypoint.sh b/entrypoint.sh
index c472e48..a2867f6 100644
--- a/entrypoint.sh
+++ b/entrypoint.sh
@@ -1,15 +1,12 @@
#!/bin/bash
-set -e
-echo "🚀 启动闲鱼自动回复系统..."
-echo "📊 数据库将在应用启动时自动初始化..."
-echo "🎯 启动主应用..."
+echo "Starting xianyu-auto-reply system..."
-# 确保数据目录存在
+# Create necessary directories
mkdir -p /app/data /app/logs /app/backups /app/static/uploads/images
-# 设置目录权限
+# Set permissions
chmod 777 /app/data /app/logs /app/backups /app/static/uploads /app/static/uploads/images
-# 启动主应用
+# Start the application
exec python Start.py
From e00e7d09a14f83ea5e6792ba2e711ebbab0d35cf Mon Sep 17 00:00:00 2001
From: zhinianboke <115088296+zhinianboke@users.noreply.github.com>
Date: Sun, 24 Aug 2025 11:04:45 +0800
Subject: [PATCH 15/15] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=89=AB=E7=A0=81?=
=?UTF-8?q?=E7=99=BB=E5=BD=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
XianyuAutoAsync.py | 360 ++++++++++++++++++++++++++++++++++++++
reply_server.py | 355 ++++++++++++++++++++++++++++++++-----
static/css/components.css | 10 ++
static/index.html | 6 +-
static/js/app.js | 194 +++++++++++++++++++-
5 files changed, 878 insertions(+), 47 deletions(-)
diff --git a/XianyuAutoAsync.py b/XianyuAutoAsync.py
index c317e58..af33f9c 100644
--- a/XianyuAutoAsync.py
+++ b/XianyuAutoAsync.py
@@ -202,6 +202,10 @@ class XianyuLive:
self.cookie_refresh_running = False # 防止重复执行Cookie刷新
self.cookie_refresh_enabled = True # 是否启用Cookie刷新功能
+ # 扫码登录Cookie刷新标志
+ self.last_qr_cookie_refresh_time = 0 # 记录上次扫码登录Cookie刷新时间
+ self.qr_cookie_refresh_cooldown = 600 # 扫码登录Cookie刷新后的冷却时间:10分钟
+
# WebSocket连接监控
@@ -3485,6 +3489,7 @@ class XianyuLive:
logger.error(f"【{self.cookie_id}】清理任务失败: {self._safe_str(e)}")
await asyncio.sleep(300) # 出错后也等待5分钟再重试
+
async def cookie_refresh_loop(self):
"""Cookie刷新定时任务 - 每小时执行一次"""
while True:
@@ -3578,6 +3583,347 @@ class XianyuLive:
status = "启用" if enabled else "禁用"
logger.info(f"【{self.cookie_id}】Cookie刷新功能已{status}")
+
+ async def refresh_cookies_from_qr_login(self, qr_cookies_str: str, cookie_id: str = None, user_id: int = None):
+ """使用扫码登录获取的cookie访问指定界面获取真实cookie并存入数据库
+
+ Args:
+ qr_cookies_str: 扫码登录获取的cookie字符串
+ cookie_id: 可选的cookie ID,如果不提供则使用当前实例的cookie_id
+ user_id: 可选的用户ID,如果不提供则使用当前实例的user_id
+
+ Returns:
+ bool: 成功返回True,失败返回False
+ """
+ playwright = None
+ browser = None
+ target_cookie_id = cookie_id or self.cookie_id
+ target_user_id = user_id or self.user_id
+
+ try:
+ import asyncio
+ from playwright.async_api import async_playwright
+ from utils.xianyu_utils import trans_cookies
+
+ logger.info(f"【{target_cookie_id}】开始使用扫码登录cookie获取真实cookie...")
+ logger.info(f"【{target_cookie_id}】扫码cookie长度: {len(qr_cookies_str)}")
+
+ # 解析扫码登录的cookie
+ qr_cookies_dict = trans_cookies(qr_cookies_str)
+ logger.info(f"【{target_cookie_id}】扫码cookie字段数: {len(qr_cookies_dict)}")
+
+ # Docker环境下修复asyncio子进程问题
+ is_docker = os.getenv('DOCKER_ENV') or os.path.exists('/.dockerenv')
+
+ if is_docker:
+ logger.debug(f"【{target_cookie_id}】检测到Docker环境,应用asyncio修复")
+
+ # 创建一个完整的虚拟子进程监视器
+ class DummyChildWatcher:
+ def __enter__(self):
+ return self
+ def __exit__(self, *args):
+ pass
+ def is_active(self):
+ return True
+ def add_child_handler(self, *args, **kwargs):
+ pass
+ def remove_child_handler(self, *args, **kwargs):
+ pass
+ def attach_loop(self, *args, **kwargs):
+ pass
+ def close(self):
+ pass
+ def __del__(self):
+ pass
+
+ # 创建自定义事件循环策略
+ class DockerEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
+ def get_child_watcher(self):
+ return DummyChildWatcher()
+
+ # 临时设置策略
+ old_policy = asyncio.get_event_loop_policy()
+ asyncio.set_event_loop_policy(DockerEventLoopPolicy())
+
+ try:
+ # 添加超时机制,避免无限等待
+ playwright = await asyncio.wait_for(
+ async_playwright().start(),
+ timeout=30.0 # 30秒超时
+ )
+ logger.debug(f"【{target_cookie_id}】Docker环境下Playwright启动成功")
+ except asyncio.TimeoutError:
+ logger.error(f"【{target_cookie_id}】Docker环境下Playwright启动超时")
+ return False
+ finally:
+ # 恢复原策略
+ asyncio.set_event_loop_policy(old_policy)
+ else:
+ # 非Docker环境,正常启动(也添加超时保护)
+ try:
+ playwright = await asyncio.wait_for(
+ async_playwright().start(),
+ timeout=30.0 # 30秒超时
+ )
+ except asyncio.TimeoutError:
+ logger.error(f"【{target_cookie_id}】Playwright启动超时")
+ return False
+
+ # 启动浏览器(参照商品搜索的配置)
+ browser_args = [
+ '--no-sandbox',
+ '--disable-setuid-sandbox',
+ '--disable-dev-shm-usage',
+ '--disable-accelerated-2d-canvas',
+ '--no-first-run',
+ '--no-zygote',
+ '--disable-gpu',
+ '--disable-background-timer-throttling',
+ '--disable-backgrounding-occluded-windows',
+ '--disable-renderer-backgrounding',
+ '--disable-features=TranslateUI',
+ '--disable-ipc-flooding-protection',
+ '--disable-extensions',
+ '--disable-default-apps',
+ '--disable-sync',
+ '--disable-translate',
+ '--hide-scrollbars',
+ '--mute-audio',
+ '--no-default-browser-check',
+ '--no-pings'
+ ]
+
+ # 在Docker环境中添加额外参数
+ if os.getenv('DOCKER_ENV'):
+ browser_args.extend([
+ '--single-process',
+ '--disable-background-networking',
+ '--disable-client-side-phishing-detection',
+ '--disable-hang-monitor',
+ '--disable-popup-blocking',
+ '--disable-prompt-on-repost',
+ '--disable-web-resources',
+ '--metrics-recording-only',
+ '--safebrowsing-disable-auto-update',
+ '--enable-automation',
+ '--password-store=basic',
+ '--use-mock-keychain'
+ ])
+
+ # 使用无头浏览器
+ browser = await playwright.chromium.launch(
+ headless=True, # 改回无头模式
+ args=browser_args
+ )
+
+ # 创建浏览器上下文
+ context_options = {
+ 'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36'
+ }
+
+ # 使用标准窗口大小
+ context_options['viewport'] = {'width': 1920, 'height': 1080}
+
+ context = await browser.new_context(**context_options)
+
+ # 设置扫码登录获取的Cookie
+ cookies = []
+ for cookie_pair in qr_cookies_str.split('; '):
+ if '=' in cookie_pair:
+ name, value = cookie_pair.split('=', 1)
+ cookies.append({
+ 'name': name.strip(),
+ 'value': value.strip(),
+ 'domain': '.goofish.com',
+ 'path': '/'
+ })
+
+ await context.add_cookies(cookies)
+ logger.info(f"【{target_cookie_id}】已设置 {len(cookies)} 个扫码Cookie到浏览器")
+
+ # 打印设置的扫码Cookie详情
+ logger.info(f"【{target_cookie_id}】=== 设置到浏览器的扫码Cookie ===")
+ for i, cookie in enumerate(cookies, 1):
+ logger.info(f"【{target_cookie_id}】{i:2d}. {cookie['name']}: {cookie['value'][:50]}{'...' if len(cookie['value']) > 50 else ''}")
+
+ # 创建页面
+ page = await context.new_page()
+
+ # 等待页面准备
+ await asyncio.sleep(0.1)
+
+ # 访问指定页面获取真实cookie
+ target_url = "https://www.goofish.com/im?spm=a21ybx.home.sidebar.1.4c053da6vYwnmf"
+ logger.info(f"【{target_cookie_id}】访问页面获取真实cookie: {target_url}")
+
+ # 使用更灵活的页面访问策略
+ try:
+ # 首先尝试较短超时
+ await page.goto(target_url, wait_until='domcontentloaded', timeout=15000)
+ logger.info(f"【{target_cookie_id}】页面访问成功")
+ except Exception as e:
+ if 'timeout' in str(e).lower():
+ logger.warning(f"【{target_cookie_id}】页面访问超时,尝试降级策略...")
+ try:
+ # 降级策略:只等待基本加载
+ await page.goto(target_url, wait_until='load', timeout=20000)
+ logger.info(f"【{target_cookie_id}】页面访问成功(降级策略)")
+ except Exception as e2:
+ logger.warning(f"【{target_cookie_id}】降级策略也失败,尝试最基本访问...")
+ # 最后尝试:不等待任何加载完成
+ await page.goto(target_url, timeout=25000)
+ logger.info(f"【{target_cookie_id}】页面访问成功(最基本策略)")
+ else:
+ raise e
+
+ # 等待页面完全加载并获取真实cookie
+ logger.info(f"【{target_cookie_id}】页面加载完成,等待获取真实cookie...")
+ await asyncio.sleep(2)
+
+ # 执行一次刷新以确保获取最新的cookie
+ logger.info(f"【{target_cookie_id}】执行页面刷新获取最新cookie...")
+ try:
+ await page.reload(wait_until='domcontentloaded', timeout=12000)
+ logger.info(f"【{target_cookie_id}】页面刷新成功")
+ except Exception as e:
+ if 'timeout' in str(e).lower():
+ logger.warning(f"【{target_cookie_id}】页面刷新超时,使用降级策略...")
+ await page.reload(wait_until='load', timeout=15000)
+ logger.info(f"【{target_cookie_id}】页面刷新成功(降级策略)")
+ else:
+ raise e
+ await asyncio.sleep(1)
+
+ # 获取更新后的真实Cookie
+ logger.info(f"【{target_cookie_id}】获取真实Cookie...")
+ updated_cookies = await context.cookies()
+
+ # 构造新的Cookie字典
+ real_cookies_dict = {}
+ for cookie in updated_cookies:
+ real_cookies_dict[cookie['name']] = cookie['value']
+
+ # 生成真实cookie字符串
+ real_cookies_str = '; '.join([f"{k}={v}" for k, v in real_cookies_dict.items()])
+
+ logger.info(f"【{target_cookie_id}】真实Cookie已获取,包含 {len(real_cookies_dict)} 个字段")
+
+ # 打印完整的真实Cookie内容
+ logger.info(f"【{target_cookie_id}】=== 完整真实Cookie内容 ===")
+ logger.info(f"【{target_cookie_id}】Cookie字符串长度: {len(real_cookies_str)}")
+ logger.info(f"【{target_cookie_id}】Cookie完整内容:")
+ logger.info(f"【{target_cookie_id}】{real_cookies_str}")
+
+ # 打印所有Cookie字段的详细信息
+ logger.info(f"【{target_cookie_id}】=== Cookie字段详细信息 ===")
+ for i, (name, value) in enumerate(real_cookies_dict.items(), 1):
+ # 对于长值,显示前后部分
+ if len(value) > 50:
+ display_value = f"{value[:20]}...{value[-20:]}"
+ else:
+ display_value = value
+ logger.info(f"【{target_cookie_id}】{i:2d}. {name}: {display_value}")
+
+ # 打印原始扫码Cookie对比
+ logger.info(f"【{target_cookie_id}】=== 扫码Cookie对比 ===")
+ logger.info(f"【{target_cookie_id}】扫码Cookie长度: {len(qr_cookies_str)}")
+ logger.info(f"【{target_cookie_id}】扫码Cookie字段数: {len(qr_cookies_dict)}")
+ logger.info(f"【{target_cookie_id}】真实Cookie长度: {len(real_cookies_str)}")
+ logger.info(f"【{target_cookie_id}】真实Cookie字段数: {len(real_cookies_dict)}")
+ logger.info(f"【{target_cookie_id}】长度增加: {len(real_cookies_str) - len(qr_cookies_str)} 字符")
+ logger.info(f"【{target_cookie_id}】字段增加: {len(real_cookies_dict) - len(qr_cookies_dict)} 个")
+
+ # 检查Cookie变化
+ changed_cookies = []
+ new_cookies = []
+ for name, new_value in real_cookies_dict.items():
+ old_value = qr_cookies_dict.get(name)
+ if old_value is None:
+ new_cookies.append(name)
+ elif old_value != new_value:
+ changed_cookies.append(name)
+
+ # 显示Cookie变化统计
+ if changed_cookies:
+ logger.info(f"【{target_cookie_id}】发生变化的Cookie字段 ({len(changed_cookies)}个): {', '.join(changed_cookies)}")
+ if new_cookies:
+ logger.info(f"【{target_cookie_id}】新增的Cookie字段 ({len(new_cookies)}个): {', '.join(new_cookies)}")
+ if not changed_cookies and not new_cookies:
+ logger.info(f"【{target_cookie_id}】Cookie无变化")
+
+ # 打印重要Cookie字段的完整详情
+ important_cookies = ['_m_h5_tk', '_m_h5_tk_enc', 'cookie2', 't', 'sgcookie', 'unb', 'uc1', 'uc3', 'uc4']
+ logger.info(f"【{target_cookie_id}】=== 重要Cookie字段完整详情 ===")
+ for cookie_name in important_cookies:
+ if cookie_name in real_cookies_dict:
+ cookie_value = real_cookies_dict[cookie_name]
+
+ # 标记是否发生了变化
+ change_mark = " [已变化]" if cookie_name in changed_cookies else " [新增]" if cookie_name in new_cookies else " [无变化]"
+
+ # 显示完整的cookie值
+ logger.info(f"【{target_cookie_id}】{cookie_name}{change_mark}:")
+ logger.info(f"【{target_cookie_id}】 值: {cookie_value}")
+ logger.info(f"【{target_cookie_id}】 长度: {len(cookie_value)}")
+
+ # 如果有对应的扫码cookie值,显示对比
+ if cookie_name in qr_cookies_dict:
+ old_value = qr_cookies_dict[cookie_name]
+ if old_value != cookie_value:
+ logger.info(f"【{target_cookie_id}】 原值: {old_value}")
+ logger.info(f"【{target_cookie_id}】 原长度: {len(old_value)}")
+ logger.info(f"【{target_cookie_id}】 ---")
+ else:
+ logger.info(f"【{target_cookie_id}】{cookie_name}: [不存在]")
+
+ # 保存真实Cookie到数据库
+ from db_manager import db_manager
+ success = db_manager.save_cookie(target_cookie_id, real_cookies_str, target_user_id)
+
+ if success:
+ logger.info(f"【{target_cookie_id}】真实Cookie已成功保存到数据库")
+
+ # 如果当前实例的cookie_id匹配,更新实例的cookie信息
+ if target_cookie_id == self.cookie_id:
+ self.cookies = real_cookies_dict
+ self.cookies_str = real_cookies_str
+ logger.info(f"【{target_cookie_id}】已更新当前实例的Cookie信息")
+
+ # 更新扫码登录Cookie刷新时间标志
+ self.last_qr_cookie_refresh_time = time.time()
+ logger.info(f"【{target_cookie_id}】已更新扫码登录Cookie刷新时间标志,_refresh_cookies_via_browser将等待{self.qr_cookie_refresh_cooldown//60}分钟后执行")
+
+ return True
+ else:
+ logger.error(f"【{target_cookie_id}】保存真实Cookie到数据库失败")
+ return False
+
+ except Exception as e:
+ logger.error(f"【{target_cookie_id}】使用扫码cookie获取真实cookie失败: {self._safe_str(e)}")
+ return False
+ finally:
+ # 确保资源清理
+ try:
+ if browser:
+ await browser.close()
+ if playwright:
+ await playwright.stop()
+ except Exception as cleanup_e:
+ logger.warning(f"【{target_cookie_id}】清理浏览器资源时出错: {self._safe_str(cleanup_e)}")
+
+ def reset_qr_cookie_refresh_flag(self):
+ """重置扫码登录Cookie刷新标志,允许立即执行_refresh_cookies_via_browser"""
+ self.last_qr_cookie_refresh_time = 0
+ logger.info(f"【{self.cookie_id}】已重置扫码登录Cookie刷新标志")
+
+ def get_qr_cookie_refresh_remaining_time(self) -> int:
+ """获取扫码登录Cookie刷新剩余冷却时间(秒)"""
+ current_time = time.time()
+ time_since_qr_refresh = current_time - self.last_qr_cookie_refresh_time
+ remaining_time = max(0, self.qr_cookie_refresh_cooldown - time_since_qr_refresh)
+ return int(remaining_time)
+
async def _refresh_cookies_via_browser(self):
"""通过浏览器访问指定页面刷新Cookie"""
@@ -3588,6 +3934,19 @@ class XianyuLive:
import asyncio
from playwright.async_api import async_playwright
+ # 检查是否需要等待扫码登录Cookie刷新的冷却时间
+ current_time = time.time()
+ time_since_qr_refresh = current_time - self.last_qr_cookie_refresh_time
+
+ if time_since_qr_refresh < self.qr_cookie_refresh_cooldown:
+ remaining_time = self.qr_cookie_refresh_cooldown - time_since_qr_refresh
+ remaining_minutes = int(remaining_time // 60)
+ remaining_seconds = int(remaining_time % 60)
+
+ logger.info(f"【{self.cookie_id}】扫码登录Cookie刷新冷却中,还需等待 {remaining_minutes}分{remaining_seconds}秒")
+ logger.info(f"【{self.cookie_id}】跳过本次浏览器Cookie刷新")
+ return False
+
logger.info(f"【{self.cookie_id}】开始通过浏览器刷新Cookie...")
logger.info(f"【{self.cookie_id}】刷新前Cookie长度: {len(self.cookies_str)}")
logger.info(f"【{self.cookie_id}】刷新前Cookie字段数: {len(self.cookies)}")
@@ -3855,6 +4214,7 @@ class XianyuLive:
except Exception as cleanup_e:
logger.warning(f"【{self.cookie_id}】清理浏览器资源时出错: {self._safe_str(cleanup_e)}")
+
async def send_msg_once(self, toid, item_id, text):
headers = {
"Cookie": self.cookies_str,
diff --git a/reply_server.py b/reply_server.py
index 1c9df63..0bdd996 100644
--- a/reply_server.py
+++ b/reply_server.py
@@ -14,6 +14,8 @@ import os
import uvicorn
import pandas as pd
import io
+import asyncio
+from collections import defaultdict
import cookie_manager
from db_manager import db_manager
@@ -36,9 +38,30 @@ TOKEN_EXPIRE_TIME = 24 * 60 * 60 # token过期时间:24小时
# HTTP Bearer认证
security = HTTPBearer(auto_error=False)
+# 扫码登录检查锁 - 防止并发处理同一个session
+qr_check_locks = defaultdict(lambda: asyncio.Lock())
+qr_check_processed = {} # 记录已处理的session: {session_id: {'processed': bool, 'timestamp': float}}
+
# 不再需要单独的密码初始化,由数据库初始化时处理
+def cleanup_qr_check_records():
+ """清理过期的扫码检查记录"""
+ current_time = time.time()
+ expired_sessions = []
+
+ for session_id, record in qr_check_processed.items():
+ # 清理超过1小时的记录
+ if current_time - record['timestamp'] > 3600:
+ expired_sessions.append(session_id)
+
+ for session_id in expired_sessions:
+ if session_id in qr_check_processed:
+ del qr_check_processed[session_id]
+ if session_id in qr_check_locks:
+ del qr_check_locks[session_id]
+
+
def load_keywords() -> List[Tuple[str, str]]:
"""读取关键字→回复映射表
@@ -1038,26 +1061,57 @@ async def generate_qr_code(current_user: Dict[str, Any] = Depends(get_current_us
async def check_qr_code_status(session_id: str, current_user: Dict[str, Any] = Depends(get_current_user)):
"""检查扫码登录状态"""
try:
- # 清理过期会话
- qr_login_manager.cleanup_expired_sessions()
+ # 清理过期记录
+ cleanup_qr_check_records()
- # 获取会话状态
- status_info = qr_login_manager.get_session_status(session_id)
+ # 检查是否已经处理过
+ if session_id in qr_check_processed:
+ record = qr_check_processed[session_id]
+ if record['processed']:
+ log_with_user('debug', f"扫码登录session {session_id} 已处理过,直接返回", current_user)
+ # 返回简单的成功状态,避免重复处理
+ return {'status': 'already_processed', 'message': '该会话已处理完成'}
- if status_info['status'] == 'success':
- # 登录成功,处理Cookie
- cookies_info = qr_login_manager.get_session_cookies(session_id)
- if cookies_info:
- account_info = await process_qr_login_cookies(
- cookies_info['cookies'],
- cookies_info['unb'],
- current_user
- )
- status_info['account_info'] = account_info
+ # 获取该session的锁
+ session_lock = qr_check_locks[session_id]
- log_with_user('info', f"扫码登录成功处理完成: {session_id}, 账号: {account_info.get('account_id', 'unknown')}", current_user)
+ # 使用非阻塞方式尝试获取锁
+ if session_lock.locked():
+ log_with_user('debug', f"扫码登录session {session_id} 正在被其他请求处理,跳过", current_user)
+ return {'status': 'processing', 'message': '正在处理中,请稍候...'}
- return status_info
+ async with session_lock:
+ # 再次检查是否已处理(双重检查)
+ if session_id in qr_check_processed and qr_check_processed[session_id]['processed']:
+ log_with_user('debug', f"扫码登录session {session_id} 在获取锁后发现已处理,直接返回", current_user)
+ return {'status': 'already_processed', 'message': '该会话已处理完成'}
+
+ # 清理过期会话
+ qr_login_manager.cleanup_expired_sessions()
+
+ # 获取会话状态
+ status_info = qr_login_manager.get_session_status(session_id)
+
+ if status_info['status'] == 'success':
+ # 登录成功,处理Cookie(现在包含获取真实cookie的逻辑)
+ cookies_info = qr_login_manager.get_session_cookies(session_id)
+ if cookies_info:
+ account_info = await process_qr_login_cookies(
+ cookies_info['cookies'],
+ cookies_info['unb'],
+ current_user
+ )
+ status_info['account_info'] = account_info
+
+ log_with_user('info', f"扫码登录处理完成: {session_id}, 账号: {account_info.get('account_id', 'unknown')}", current_user)
+
+ # 标记该session已处理
+ qr_check_processed[session_id] = {
+ 'processed': True,
+ 'timestamp': time.time()
+ }
+
+ return status_info
except Exception as e:
log_with_user('error', f"检查扫码登录状态异常: {str(e)}", current_user)
@@ -1065,7 +1119,7 @@ async def check_qr_code_status(session_id: str, current_user: Dict[str, Any] = D
async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[str, Any]) -> Dict[str, Any]:
- """处理扫码登录获取的Cookie"""
+ """处理扫码登录获取的Cookie - 先获取真实cookie再保存到数据库"""
try:
user_id = current_user['user_id']
@@ -1083,20 +1137,11 @@ async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[st
except:
continue
+ # 确定账号ID
if existing_account_id:
- # 更新现有账号的Cookie
- db_manager.save_cookie(existing_account_id, cookies, user_id)
-
- # 更新cookie_manager中的Cookie
- if cookie_manager.manager:
- cookie_manager.manager.update_cookie(existing_account_id, cookies)
-
- log_with_user('info', f"扫码登录更新现有账号Cookie: {existing_account_id}, UNB: {unb}", current_user)
-
- return {
- 'account_id': existing_account_id,
- 'is_new_account': False
- }
+ account_id = existing_account_id
+ is_new_account = False
+ log_with_user('info', f"扫码登录找到现有账号: {account_id}, UNB: {unb}", current_user)
else:
# 创建新账号,使用unb作为账号ID
account_id = unb
@@ -1108,25 +1153,255 @@ async def process_qr_login_cookies(cookies: str, unb: str, current_user: Dict[st
account_id = f"{original_account_id}_{counter}"
counter += 1
- # 保存新账号
- db_manager.save_cookie(account_id, cookies, user_id)
+ is_new_account = True
+ log_with_user('info', f"扫码登录准备创建新账号: {account_id}, UNB: {unb}", current_user)
- # 添加到cookie_manager
- if cookie_manager.manager:
- cookie_manager.manager.add_cookie(account_id, cookies)
+ # 第一步:使用扫码cookie获取真实cookie
+ log_with_user('info', f"开始使用扫码cookie获取真实cookie: {account_id}", current_user)
- log_with_user('info', f"扫码登录创建新账号: {account_id}, UNB: {unb}", current_user)
+ try:
+ # 创建一个临时的XianyuLive实例来执行cookie刷新
+ from XianyuAutoAsync import XianyuLive
- return {
- 'account_id': account_id,
- 'is_new_account': True
- }
+ # 使用扫码登录的cookie创建临时实例
+ temp_instance = XianyuLive(
+ cookies_str=cookies,
+ cookie_id=account_id,
+ user_id=user_id
+ )
+
+ # 执行cookie刷新获取真实cookie
+ refresh_success = await temp_instance.refresh_cookies_from_qr_login(
+ qr_cookies_str=cookies,
+ cookie_id=account_id,
+ user_id=user_id
+ )
+
+ if refresh_success:
+ log_with_user('info', f"扫码登录真实cookie获取成功: {account_id}", current_user)
+
+ # 从数据库获取刚刚保存的真实cookie
+ updated_cookie_info = db_manager.get_cookie_by_id(account_id)
+ if updated_cookie_info:
+ real_cookies = updated_cookie_info['cookies_str']
+ log_with_user('info', f"已获取真实cookie,长度: {len(real_cookies)}", current_user)
+
+ # 第二步:将真实cookie添加到cookie_manager(如果是新账号)或更新现有账号
+ if cookie_manager.manager:
+ if is_new_account:
+ cookie_manager.manager.add_cookie(account_id, real_cookies)
+ log_with_user('info', f"已将真实cookie添加到cookie_manager: {account_id}", current_user)
+ else:
+ cookie_manager.manager.update_cookie(account_id, real_cookies)
+ log_with_user('info', f"已更新cookie_manager中的真实cookie: {account_id}", current_user)
+
+ return {
+ 'account_id': account_id,
+ 'is_new_account': is_new_account,
+ 'real_cookie_refreshed': True,
+ 'cookie_length': len(real_cookies)
+ }
+ else:
+ log_with_user('error', f"无法从数据库获取真实cookie: {account_id}", current_user)
+ # 降级处理:使用原始扫码cookie
+ return await _fallback_save_qr_cookie(account_id, cookies, user_id, is_new_account, current_user, "无法从数据库获取真实cookie")
+ else:
+ log_with_user('warning', f"扫码登录真实cookie获取失败: {account_id}", current_user)
+ # 降级处理:使用原始扫码cookie
+ return await _fallback_save_qr_cookie(account_id, cookies, user_id, is_new_account, current_user, "真实cookie获取失败")
+
+ except Exception as refresh_e:
+ log_with_user('error', f"扫码登录真实cookie获取异常: {str(refresh_e)}", current_user)
+ # 降级处理:使用原始扫码cookie
+ return await _fallback_save_qr_cookie(account_id, cookies, user_id, is_new_account, current_user, f"获取真实cookie异常: {str(refresh_e)}")
except Exception as e:
log_with_user('error', f"处理扫码登录Cookie失败: {str(e)}", current_user)
raise e
+async def _fallback_save_qr_cookie(account_id: str, cookies: str, user_id: int, is_new_account: bool, current_user: Dict[str, Any], error_reason: str) -> Dict[str, Any]:
+ """降级处理:当无法获取真实cookie时,保存原始扫码cookie"""
+ try:
+ log_with_user('warning', f"降级处理 - 保存原始扫码cookie: {account_id}, 原因: {error_reason}", current_user)
+
+ # 保存原始扫码cookie到数据库
+ if is_new_account:
+ db_manager.save_cookie(account_id, cookies, user_id)
+ log_with_user('info', f"降级处理 - 新账号原始cookie已保存: {account_id}", current_user)
+ else:
+ db_manager.save_cookie(account_id, cookies, user_id)
+ log_with_user('info', f"降级处理 - 现有账号原始cookie已更新: {account_id}", current_user)
+
+ # 添加到或更新cookie_manager
+ if cookie_manager.manager:
+ if is_new_account:
+ cookie_manager.manager.add_cookie(account_id, cookies)
+ log_with_user('info', f"降级处理 - 已将原始cookie添加到cookie_manager: {account_id}", current_user)
+ else:
+ cookie_manager.manager.update_cookie(account_id, cookies)
+ log_with_user('info', f"降级处理 - 已更新cookie_manager中的原始cookie: {account_id}", current_user)
+
+ return {
+ 'account_id': account_id,
+ 'is_new_account': is_new_account,
+ 'real_cookie_refreshed': False,
+ 'fallback_reason': error_reason,
+ 'cookie_length': len(cookies)
+ }
+
+ except Exception as fallback_e:
+ log_with_user('error', f"降级处理失败: {str(fallback_e)}", current_user)
+ raise fallback_e
+
+
+@app.post("/qr-login/refresh-cookies")
+async def refresh_cookies_from_qr_login(
+ request: Dict[str, Any],
+ current_user: Dict[str, Any] = Depends(get_current_user)
+):
+ """使用扫码登录获取的cookie访问指定界面获取真实cookie并存入数据库"""
+ try:
+ qr_cookies = request.get('qr_cookies')
+ cookie_id = request.get('cookie_id')
+
+ if not qr_cookies:
+ return {'success': False, 'message': '缺少扫码登录cookie'}
+
+ if not cookie_id:
+ return {'success': False, 'message': '缺少cookie_id'}
+
+ log_with_user('info', f"开始使用扫码cookie刷新真实cookie: {cookie_id}", current_user)
+
+ # 创建一个临时的XianyuLive实例来执行cookie刷新
+ from XianyuAutoAsync import XianyuLive
+
+ # 使用扫码登录的cookie创建临时实例
+ temp_instance = XianyuLive(
+ cookies_str=qr_cookies,
+ cookie_id=cookie_id,
+ user_id=current_user['user_id']
+ )
+
+ # 执行cookie刷新
+ success = await temp_instance.refresh_cookies_from_qr_login(
+ qr_cookies_str=qr_cookies,
+ cookie_id=cookie_id,
+ user_id=current_user['user_id']
+ )
+
+ if success:
+ log_with_user('info', f"扫码cookie刷新成功: {cookie_id}", current_user)
+
+ # 如果cookie_manager存在,更新其中的cookie
+ if cookie_manager.manager:
+ # 从数据库获取更新后的cookie
+ updated_cookie_info = db_manager.get_cookie_by_id(cookie_id)
+ if updated_cookie_info:
+ cookie_manager.manager.update_cookie(cookie_id, updated_cookie_info['cookies_str'])
+ log_with_user('info', f"已更新cookie_manager中的cookie: {cookie_id}", current_user)
+
+ return {
+ 'success': True,
+ 'message': '真实cookie获取并保存成功',
+ 'cookie_id': cookie_id
+ }
+ else:
+ log_with_user('error', f"扫码cookie刷新失败: {cookie_id}", current_user)
+ return {'success': False, 'message': '获取真实cookie失败'}
+
+ except Exception as e:
+ log_with_user('error', f"扫码cookie刷新异常: {str(e)}", current_user)
+ return {'success': False, 'message': f'刷新cookie失败: {str(e)}'}
+
+
+@app.post("/qr-login/reset-cooldown/{cookie_id}")
+async def reset_qr_cookie_refresh_cooldown(
+ cookie_id: str,
+ current_user: Dict[str, Any] = Depends(get_current_user)
+):
+ """重置指定账号的扫码登录Cookie刷新冷却时间"""
+ try:
+ log_with_user('info', f"重置扫码登录Cookie刷新冷却时间: {cookie_id}", current_user)
+
+ # 检查cookie是否存在
+ cookie_info = db_manager.get_cookie_by_id(cookie_id)
+ if not cookie_info:
+ return {'success': False, 'message': '账号不存在'}
+
+ # 如果cookie_manager中有对应的实例,直接重置
+ if cookie_manager.manager and cookie_id in cookie_manager.manager.instances:
+ instance = cookie_manager.manager.instances[cookie_id]
+ remaining_time_before = instance.get_qr_cookie_refresh_remaining_time()
+ instance.reset_qr_cookie_refresh_flag()
+
+ log_with_user('info', f"已重置账号 {cookie_id} 的扫码登录冷却时间,原剩余时间: {remaining_time_before}秒", current_user)
+
+ return {
+ 'success': True,
+ 'message': '扫码登录Cookie刷新冷却时间已重置',
+ 'cookie_id': cookie_id,
+ 'previous_remaining_time': remaining_time_before
+ }
+ else:
+ # 如果没有活跃实例,返回成功(因为没有冷却时间需要重置)
+ log_with_user('info', f"账号 {cookie_id} 没有活跃实例,无需重置冷却时间", current_user)
+ return {
+ 'success': True,
+ 'message': '账号没有活跃实例,无需重置冷却时间',
+ 'cookie_id': cookie_id
+ }
+
+ except Exception as e:
+ log_with_user('error', f"重置扫码登录冷却时间异常: {str(e)}", current_user)
+ return {'success': False, 'message': f'重置冷却时间失败: {str(e)}'}
+
+
+@app.get("/qr-login/cooldown-status/{cookie_id}")
+async def get_qr_cookie_refresh_cooldown_status(
+ cookie_id: str,
+ current_user: Dict[str, Any] = Depends(get_current_user)
+):
+ """获取指定账号的扫码登录Cookie刷新冷却状态"""
+ try:
+ # 检查cookie是否存在
+ cookie_info = db_manager.get_cookie_by_id(cookie_id)
+ if not cookie_info:
+ return {'success': False, 'message': '账号不存在'}
+
+ # 如果cookie_manager中有对应的实例,获取冷却状态
+ if cookie_manager.manager and cookie_id in cookie_manager.manager.instances:
+ instance = cookie_manager.manager.instances[cookie_id]
+ remaining_time = instance.get_qr_cookie_refresh_remaining_time()
+ cooldown_duration = instance.qr_cookie_refresh_cooldown
+ last_refresh_time = instance.last_qr_cookie_refresh_time
+
+ return {
+ 'success': True,
+ 'cookie_id': cookie_id,
+ 'remaining_time': remaining_time,
+ 'cooldown_duration': cooldown_duration,
+ 'last_refresh_time': last_refresh_time,
+ 'is_in_cooldown': remaining_time > 0,
+ 'remaining_minutes': remaining_time // 60,
+ 'remaining_seconds': remaining_time % 60
+ }
+ else:
+ return {
+ 'success': True,
+ 'cookie_id': cookie_id,
+ 'remaining_time': 0,
+ 'cooldown_duration': 600, # 默认10分钟
+ 'last_refresh_time': 0,
+ 'is_in_cooldown': False,
+ 'message': '账号没有活跃实例'
+ }
+
+ except Exception as e:
+ log_with_user('error', f"获取扫码登录冷却状态异常: {str(e)}", current_user)
+ return {'success': False, 'message': f'获取冷却状态失败: {str(e)}'}
+
+
@app.put('/cookies/{cid}/status')
def update_cookie_status(cid: str, status_data: CookieStatusIn, current_user: Dict[str, Any] = Depends(get_current_user)):
"""更新账号的启用/禁用状态"""
diff --git a/static/css/components.css b/static/css/components.css
index 2d286c0..10feca8 100644
--- a/static/css/components.css
+++ b/static/css/components.css
@@ -1,6 +1,16 @@
/* ================================
通用卡片样式 - 适用于所有菜单的卡片
================================ */
+
+/* 旋转动画 */
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+.spin {
+ animation: spin 1s linear infinite;
+}
.card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
diff --git a/static/index.html b/static/index.html
index 494e5ed..a7e4ac6 100644
--- a/static/index.html
+++ b/static/index.html
@@ -244,17 +244,17 @@
-
diff --git a/static/js/app.js b/static/js/app.js
index 9e38b55..26e4a81 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -1347,6 +1347,157 @@ function copyCookie(id, value) {
});
}
+// 刷新真实Cookie
+async function refreshRealCookie(cookieId) {
+ if (!cookieId) {
+ showToast('缺少账号ID', 'warning');
+ return;
+ }
+
+ // 获取当前cookie值
+ try {
+ const cookieDetails = await fetchJSON(`${apiBase}/cookies/details`);
+ const currentCookie = cookieDetails.find(c => c.id === cookieId);
+
+ if (!currentCookie || !currentCookie.value) {
+ showToast('未找到有效的Cookie信息', 'warning');
+ return;
+ }
+
+ // 确认操作
+ if (!confirm(`确定要刷新账号 "${cookieId}" 的真实Cookie吗?\n\n此操作将使用当前Cookie访问闲鱼IM界面获取最新的真实Cookie。`)) {
+ return;
+ }
+
+ // 显示加载状态
+ const button = event.target.closest('button');
+ const originalContent = button.innerHTML;
+ button.disabled = true;
+ button.innerHTML = '
';
+
+ // 调用刷新API
+ const response = await fetch(`${apiBase}/qr-login/refresh-cookies`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${authToken}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ qr_cookies: currentCookie.value,
+ cookie_id: cookieId
+ })
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ showToast(`账号 "${cookieId}" 真实Cookie刷新成功`, 'success');
+ // 刷新账号列表以显示更新后的cookie
+ loadCookies();
+ } else {
+ showToast(`真实Cookie刷新失败: ${result.message}`, 'danger');
+ }
+
+ } catch (error) {
+ console.error('刷新真实Cookie失败:', error);
+ showToast(`刷新真实Cookie失败: ${error.message || '未知错误'}`, 'danger');
+ } finally {
+ // 恢复按钮状态
+ const button = event.target.closest('button');
+ if (button) {
+ button.disabled = false;
+ button.innerHTML = '
';
+ }
+ }
+}
+
+// 显示冷却状态
+async function showCooldownStatus(cookieId) {
+ if (!cookieId) {
+ showToast('缺少账号ID', 'warning');
+ return;
+ }
+
+ try {
+ const response = await fetch(`${apiBase}/qr-login/cooldown-status/${cookieId}`, {
+ method: 'GET',
+ headers: {
+ 'Authorization': `Bearer ${authToken}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ const { remaining_time, cooldown_duration, is_in_cooldown, remaining_minutes, remaining_seconds } = result;
+
+ let statusMessage = `账号: ${cookieId}\n`;
+ statusMessage += `冷却时长: ${cooldown_duration / 60}分钟\n`;
+
+ if (is_in_cooldown) {
+ statusMessage += `冷却状态: 进行中\n`;
+ statusMessage += `剩余时间: ${remaining_minutes}分${remaining_seconds}秒\n\n`;
+ statusMessage += `在冷却期间,_refresh_cookies_via_browser 方法将被跳过。\n\n`;
+ statusMessage += `是否要重置冷却时间?`;
+
+ if (confirm(statusMessage)) {
+ await resetCooldownTime(cookieId);
+ }
+ } else {
+ statusMessage += `冷却状态: 无冷却\n`;
+ statusMessage += `可以正常执行 _refresh_cookies_via_browser 方法`;
+ alert(statusMessage);
+ }
+ } else {
+ showToast(`获取冷却状态失败: ${result.message}`, 'danger');
+ }
+
+ } catch (error) {
+ console.error('获取冷却状态失败:', error);
+ showToast(`获取冷却状态失败: ${error.message || '未知错误'}`, 'danger');
+ }
+}
+
+// 重置冷却时间
+async function resetCooldownTime(cookieId) {
+ if (!cookieId) {
+ showToast('缺少账号ID', 'warning');
+ return;
+ }
+
+ try {
+ const response = await fetch(`${apiBase}/qr-login/reset-cooldown/${cookieId}`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${authToken}`,
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ const previousTime = result.previous_remaining_time || 0;
+ const previousMinutes = Math.floor(previousTime / 60);
+ const previousSeconds = previousTime % 60;
+
+ let message = `账号 "${cookieId}" 的扫码登录冷却时间已重置`;
+ if (previousTime > 0) {
+ message += `\n原剩余时间: ${previousMinutes}分${previousSeconds}秒`;
+ }
+
+ showToast(message, 'success');
+ } else {
+ showToast(`重置冷却时间失败: ${result.message}`, 'danger');
+ }
+
+ } catch (error) {
+ console.error('重置冷却时间失败:', error);
+ showToast(`重置冷却时间失败: ${error.message || '未知错误'}`, 'danger');
+ }
+}
+
// 删除Cookie
async function delCookie(id) {
if (!confirm(`确定要删除账号 "${id}" 吗?此操作不可恢复。`)) return;
@@ -7015,6 +7166,16 @@ async function checkQRCodeStatus() {
clearQRCodeCheck();
showVerificationRequired(data);
break;
+ case 'processing':
+ document.getElementById('statusText').textContent = '正在处理中...';
+ // 继续轮询,不清理检查
+ break;
+ case 'already_processed':
+ document.getElementById('statusText').textContent = '登录已完成';
+ document.getElementById('statusSpinner').style.display = 'none';
+ clearQRCodeCheck();
+ showToast('该扫码会话已处理完成', 'info');
+ break;
}
}
} catch (error) {
@@ -7078,12 +7239,37 @@ function showVerificationRequired(data) {
// 处理扫码成功
function handleQRCodeSuccess(data) {
if (data.account_info) {
- const { account_id, is_new_account } = data.account_info;
+ const { account_id, is_new_account, real_cookie_refreshed, fallback_reason, cookie_length } = data.account_info;
+ // 构建成功消息
+ let successMessage = '';
if (is_new_account) {
- showToast(`新账号添加成功!账号ID: ${account_id}`, 'success');
+ successMessage = `新账号添加成功!账号ID: ${account_id}`;
} else {
- showToast(`账号Cookie已更新!账号ID: ${account_id}`, 'success');
+ successMessage = `账号Cookie已更新!账号ID: ${account_id}`;
+ }
+
+ // 添加cookie长度信息
+ if (cookie_length) {
+ successMessage += `\nCookie长度: ${cookie_length}`;
+ }
+
+ // 添加真实cookie获取状态信息
+ if (real_cookie_refreshed === true) {
+ successMessage += '\n✅ 真实Cookie获取并保存成功';
+ document.getElementById('statusText').textContent = '登录成功!真实Cookie已获取并保存';
+ showToast(successMessage, 'success');
+ } else if (real_cookie_refreshed === false) {
+ successMessage += '\n⚠️ 真实Cookie获取失败,已保存原始扫码Cookie';
+ if (fallback_reason) {
+ successMessage += `\n原因: ${fallback_reason}`;
+ }
+ document.getElementById('statusText').textContent = '登录成功,但使用原始Cookie';
+ showToast(successMessage, 'warning');
+ } else {
+ // 兼容旧版本,没有真实cookie刷新信息
+ document.getElementById('statusText').textContent = '登录成功!';
+ showToast(successMessage, 'success');
}
// 关闭模态框
@@ -7093,7 +7279,7 @@ function handleQRCodeSuccess(data) {
// 刷新账号列表
loadCookies();
- }, 2000);
+ }, 3000); // 延长显示时间以便用户看到详细信息
}
}