From 3e5c22335dfb288b96fa7dc9759e9659842ed6a7 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sat, 8 Aug 2026 08:04:22 +0800 Subject: [PATCH] feat: implement stream buffering for chat sessions and add stream resume functionality --- app/composables/useAgentChat.ts | 54 ++++++++++++++ packages/drizzle-pkg/db.sqlite | Bin 1634304 -> 1634304 bytes server/api/agent/chat/index.post.ts | 50 ++++++++++++- server/api/agent/chat/stream.get.ts | 89 +++++++++++++++++++++++ server/service/agent/stream-buffer.ts | 129 ++++++++++++++++++++++++++++++++++ 5 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 server/api/agent/chat/stream.get.ts create mode 100644 server/service/agent/stream-buffer.ts diff --git a/app/composables/useAgentChat.ts b/app/composables/useAgentChat.ts index c099d09..5824418 100644 --- a/app/composables/useAgentChat.ts +++ b/app/composables/useAgentChat.ts @@ -90,11 +90,65 @@ export function useAgentChat(options: UseAgentChatOptions) { ...m, parts: m.parts ? (typeof m.parts === "string" ? JSON.parse(m.parts) : m.parts) : undefined, })); + + await tryResumeStream(sid); } catch { messages.value = []; } } + async function tryResumeStream(sid: string) { + const lastMsg = messages.value[messages.value.length - 1]; + if (!lastMsg || lastMsg.role !== "user") return; + + let res: Response; + try { + res = await fetch(`/api/agent/chat/stream?sessionId=${encodeURIComponent(sid)}`, { + method: "GET", + }); + } catch { + return; + } + + if (!res.ok) return; + + const isStreamResume = res.headers.get("X-Stream-Resume") === "true"; + if (!isStreamResume) return; + + const modelIdHeader = res.headers.get("X-Stream-Model-Id"); + const userMessageIdHeader = res.headers.get("X-Stream-User-Message-Id"); + + const assistantMsg: AgentMessage = { + id: generateId(), + role: "assistant", + content: "", + parts: [], + modelId: modelIdHeader ? Number(modelIdHeader) : null, + }; + messages.value.push(assistantMsg); + const assistantIdx = messages.value.length - 1; + + if (userMessageIdHeader && lastMsg) { + lastMsg.id = userMessageIdHeader; + } + + isLoading.value = true; + abortController = new AbortController(); + + try { + await processStream(res, assistantIdx); + validateAssistantContent(assistantIdx); + onStreamComplete?.(); + } catch (err: any) { + if (err.name !== "AbortError") { + errorMessage.value = err.message || "流式恢复失败"; + } + } finally { + isLoading.value = false; + abortController = null; + } + } + async function cleanupStaleApprovals() { const staleMessages: { msg: AgentMessage; parts: MessagePart[] }[] = []; for (const msg of messages.value) { diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index e0f01eda90826f297af9407d16f9c1e769561bd4..31f118b4bea2182ac1759cae2beb41ad72a817d0 100644 GIT binary patch delta 91464 zcmeFa33yXimM&@&-W0S{a$@v-+o|yOSueH}M{UoFGlZ?ja<`_dlLK{LtLN4K_xoLT7sPX0p|Lade zlTRJ^LB)X|gyLt$=z$+Radl^8%)ETG2|Xaq7Q$YQI63dT^N!EUAALgzOPUvh=UFEc z@jLgV9>4QW#-ZsiPOg+H6T-gx#Foh4ND;cQx93MyM*e2rlXHJFCuFvF_Q$iwqVl8O zmk#K{5~IeVx})BTS|0V}=mlNafk;WWI&4vBa?2|GPGY|k*>8jN@f&!g=l|$z>DhmYf&OUr*zD`GyJt7guAKed(F<>d{dkr*>xr2& zLc4x^{YFA)vj6Y%{}7rK`1`5QME~ECKGdN8eeDn9L-m2bUkcUv|NeevXoCOmfsxR7 z|KD}5g~su}e`JdOtI(C|-xdF~HEi^sw$6p`065>IM~OBVXz!TCn~v>GxpCP6Ktl?!K;zGm5hh z<&_*xOV3D4oF&SmeRA2T+Pv$pA} z%hmUFc|1ScaOikqMqyHhtzedXc0ew><7qqXxqjU}c1pS4BTt?3j^1Y7E%~45=xU-w z-7Wp!x9YZ1f3HvPFFhy6VmxFj$u=L?Tk>YD+2rl7@|B;#%9Zw3xva{4uf{thp}QaD zMONZ>Ze=U|J$pt)){EiziNH_fXx58S7mSgy^EMMsTcp{gQE!FK+c|IZ=+4rpf1EoT zPk-)s5nt_bJU6m@Mi>M!jK9M*(Rf_mk)TYSU8)vVX{RTem{8iMOqbX|h^?1_-jMEYf*UwZnb8{P8HQR6SoJPS~mK>5*2W)|bm5=kdSUz`#0H>3ZT5M30y z=398SA3q=9=R5c@sed0(|IWm}+4wnvA2WWkG(Q&nZLO${jUKIyomD*NiT&Z{!7XN8 zdNN{v#OjFHi16^g4*x;;Ncj2NOLaeDInBZ7XJk zbZK9k_Fc#Y?Q3kKzU@ck86ln8_onS)NQdUV>H9b%q+Rpc^zTC2M(wGy8kaw@JL0Mo z@xiRtCnGTe^@BNuV*ngYx zLoE8g&PczQ<5)5$IeAHK$>_I3XUqtlsV;Wfl2p8ywuw^Qf#~PLJsn;0;00;>f#{{u zAHN$NHScGoC*3ZWT;1axoRkOKq^{)11tPIe@X@Y5!JsoF3-Y}Yxop7Gc*^Z`$_FaJpBFmAYLM9@kmr!fCm-MXBzSo2oI<6)U2nmC7c0 zxLm2N@|5)pD^^H9dOC918}ae+D^{?%OkVZW5BW+Pl^cCZRlm2V79A@?4eqh4c;dU- z>pL~!p6vAAX~Fc}W2Jc5@+VI({5F`dpclLogIKvdUMlz3aG*p-uUN4=$6A=3V<%GG z=Xh~ea`J*T*qM;10OYkO^H9itygynHe|#U{}=%w~D zjhKu!o7rYJTI??gkCGkH(Q=(6--M7NapxYp0g*z0?HG2Ci8WZReDfqe``Yiy4Wmkj zQ>mN+RORwfxo*UFsa~n7kUIwC-Vt998Z^;POw=p4s%Vd;9^cp{>gyC>S&BCDDCF0K z+*u*lmQsVJak+5}y?LiveHFFJ`Jq_vonBwZ=~(aGGLN(0D>5X&pwt+W?sNn7R@coY z*Ngq}-n|VBlRKRByaHHK;c=a&Np^K$R*K_{GIoW2-(m=PWe6I7Se<)ZZ1Z#scsj=E=HD{P17ssrE#MtSdyVp=LXl~sAG z&&ZR5^7u{A3%CpL4gG<2=nX_qgdE1i9OEJJ!=EYFn7JJb|FNxY3AXtyu7 z$jyyV{?GvLc`ujC^)3-=tJ_&Fx3>cU<=!@*qmF>qQ3|YLo7g8$Nr0qW4oo0k0p2O! zkbo?)?t5L{!7*RQEoFEFOd9y%>FV=2&eCR&-o*TryOmIlF~3*v`wBYRql*>C+`6&-q9hC za|%5WN_6(%BQ1G&%sbVADJr*myx2xC@4;FQs>I~LOyoiFqS8?Zb|Pa?C*v`Lr@R-h zDb?q^Lse`<(!ptZ?chC5^h{78%j5D8(h4h3#Ge4xjhg|g1EOrIalUu z>xji;@6f1n^CYKybgSAjUY$?zf*dO(wt3+$cE;0nLuoq+Rwj#s zRld$vxvtvN4;w*cCtB9;tEf|2hLwg!5GEQBqj#0M?^SZpf~tw^rz$Xm=x9&-2yhZZ z1DR>u$|j$q0#5)(Z>d9W8epyXpP&m8IHF(T5PG{tls*{9DCrJxD^mSfTZugKBxpP*YlxPMumuZ03L1l!{ z8v_tE0xv3?)PW(Ks_Hz~Kr^NE)oDUcrKS%5`zo5`n-}EX7Woq4hsd}Yh{;ur84^iN zwxfF@$#bOJ09)LyK6FMMK;l$agD1-qXTc|Ee;fM1W?_7ihE&#vHrN8Cz7HFsxOxEx z25|R8d0;2Dh_@HB=_8}}A>msFvFUPO`%2N}NCZclcHc<%hWbCR>8uVQ} zkGT;bI)?x}niiuRxuuhEqPI;RZh~z4<#<;Bt%00qN$`R$TI6`y@%(HkqYfCH8f|o% zEuzsQ#enOf&}J$CPhpI{)H76(sMG1d>1luZF{Dc#Zs3yFJ9x`GrOw#zUZBy>l>w7z z!j~Pym`;^>t53SehUFm=!yZ>TK6-~P%fl1o#5n50?0#;FtqA|#T>Q+#&y)Ch3O`Td z=b6#)@6G>Laqi5JYaz2fiu&6pHq7V@xpsD`u2pn61jlo8p)CKV$WggXqbpChs`whG zTaX%EnX6K8h5$FBqhESSzz#r9F1s$5oigZ?QW=bEJ&tS83YEbY1yJu8js+(aVbsX& zH)56gez_9#K*}_&o{0od1=cE;j(Zwz1o)V8K>|uBH!cx}=<4%y)WmXEOe~juc-uSG z2)WA96sQy(?QcQagVX7`>5}Wq<%V0pE0JWg6AYvu@2%OywIZOo2a0rKIgpLdS?+RT z!Za}mXJ-BUtkj_l_d^SDPqqV%@lh@Z4{0N`9PE|P)IvK&9dHb!D=#0Ark6C-8=5ej()3l9v_JJsaAR}b;) znXD%zvwRd12g8Ge!MWJ9NLB+tP_NnQImG0a4(0kqxfEDk53GKcW3{M7>-1^n^24_L zE63_3pMlmt%Dr}Ibi=v@uC24lN}mz&N=U@V5wE~PUmP*lvFv2>>=6A6p^g_q7G+42 z^^tK>z9TG5`skC$*;4;6BA0Fa&{R-hEs)RF5u2H+z#gv>^!h}7TB<=fTw*ubgyp{0 zMxdRb*BO#@x>rN~9JB-5nRMykUuWU$uWo`N^Ym*XEQ}w#Uq$H{7@5AQL&>N(mu8w$4#60ElGWXad zXn;=`I1l_%swLo;QVC99rL;jQBnN@qP^a8J=k1$PT88*n-cucN<&`uY^b7RlJ6{F| zD$v>Eyed!KksF4Udz0{c%Dr{|CV(r1GB8b$8!pQYPcapyZ^|n{({96vDU#6?{;c%z zGhxq5c31*LM{>)sduq@-co9_Q zsk!04SLGe)m&c$6Rc_P&a=Z%G9FbSUC3*5RErmCR){geu^qg#!I}Yf|7{NO-+yAZ( z?@E7~KJz77$@5R-9Lbd8J`Rr$&ovt@hpkf6tjOn=^1uBn%dlGPCX4-0sxCP_Jxv#% zW6zU*{ZZun|ETNAkBSVRC45@y6x20^{$u27Y2Lp@>KCNw^y)_G_33P*5)FE3%^ML5 zq))~pBc*TuRYZ&^w@(_FgNyKzF*=|lJQ^@(yd(FN<{@Y|azizoP|(!_4a)||;l-LO z&3DntJ?0`w$i%$>Pn6*883^{bx}#XvQg(zf1Z!jG5bT3-tk~Vfe|_ z?S!yU0r5z0040#L;R7{Rl484PtR8x*eDfT%CXcIv{zfRIqg49SMIgyvl|;^b&cDXk z55N7PtuQ0QWV3~^$j$kGxZJ%!2qP&W8KWW>#3nq(<^YmD#^(Q!*c`^_x93d6@)?OK z(zcwK7o~rmnERsiqko(~TZB)|SJ43Xfjl~>`oLI>5uP~ZY`u4?S#G~TCYsbOm)#~| zLH=Sj#^WLb z2906AGM^Y3JkTAz{SI`f4D={9@U}XYR#@F_@0r$Q*r5Tz^n!!(hPVHuJk$j<2`zUV zhMjWUXg6Cea>EeR8uB|emcxv~%zYiD;JG~3%j3Qn@Sx?OCE%`Kg@6E$(Q?S4M?ur2 zphDXZYTqew0<-17g_0r<-oxr1gdm4?0LmQb0f4&4ngak)2lvB-0|bT@yn0ypL5n_K zoga47*F5MIhwAt``P!8B5znnjD9V_IT;0vxv9zi1;SRRJ;{e|VV1CeIh!@m1cw8E; znT8=>XEy=6vk!_sZS+vHeEB4I87S3fvE54Zl9d z*d%NN11Ve+Y#eF$U!GndYP;ma@-eYmd?bg)aHn^OtZ=?yHa5x6!Mp0%H%lSBk|2S# zbHSPs>ag45v#pSU7XLaqNCS}Zv`OuIqN5S5f(70JeB|4e;2j=SZq#zJIcQ0Y7x*Oj z+wObky^dz6g&1Sq&UI_hH%D(;sdw-S+5QMbQQ!!3juWy>c0>g8GKKqQ4GR)*685~e zb;c?|>iMh47*g%OA<{cGld&KpN8r=p^AHY_a!u>JkI3LF_cfQoD!l=nU9M|57^~?) zyAt%UcK>?f@JWKvVktD{3N~Y|$u{kUgWra?R$U-#svS_SKPjIp$7o=hA4^sl;_9m0FoLQAG#0HCy~7Y|S& zF%7trCk7uze;RHVpwj>ya$+L_@Ut{5$1Nf>$aosI4paPGnp4_F;JXG-3V2;UZR!@3 z+t5lF;eqa&z+7&~tykgAh0TDF#iva@DphIINXXSyRU-yae8mc-`yOd=TE+tXoGe_H zjIv?{fWztK;pp?4puy(TocsdQ5?&MF2qqIjBL{v~`nt;Kf<(y?RwZnn@WLn`9Vm4d zIRcVvylWjeEc=Pf1*QT5}W05Z#0W7**QV`Cdhful{1cnA3f68tZrGNT zosyiGEIqY+=CaKJYRf9j%@s;O)12Uhq5d)9I+1EIzhix8_p$xk zx0&*CvJ8po1h7|Mew$1ZrOX7FJUtDZBw5cTb{hZYNEtV7StT&uEV94ETvgt6JW8a#&{AGw- z@_B#XI$#mKDrSlJv(jq$-juJs>>DrX z(-L2Y9*Efd&r0E>ZTqye1M>%;^tGQx)B#K!7IUYk>$Y$FtkQyTPPN=P3Csz!#z)_k zo5r{G?3QtaxXlO=jrcP5?@!kd*E-Pe*B~LV8Cgk zNlS|&UwDe`wszUluhxdYIKRkbIa(OcBpy)>BMs>%uSYBp@B1d0F*7a0WXRI#Q&Lir4Ts6?mphHmun?qEt{Q?qhQ)G@0ww}K z7ylc0g%|{$08`LFge(H)KoRowkspZExXKoJ;v$SH-i18wq##1|0J>&M4`l_h@Y`kd zh>Rf~SM*k1r%s{USKn1YMs26@E$D+H5my~(gKse&OdCEAPvuQK`SbvC8^}?cuz>c6KHF;u$#d;lpVOSZPSy;gA&uws%8}w-(ekkQ13SZPRF;`fWYuK`8 zvwfF+Q)-DN|LvS~-P-ND()DT5&Cc-U1_;P6>67Q;V@Ju(Eo;~BJ(jsW|M;4Hd)_X} zIrgs6uwfSyAOD_SgTmyobN>Ge&Uul$FgaU;0`{G*gOmZHR3RS{@ewsDjZ_e&b(p-S zy-wyp0b%gaA5WPC`=PeX{st+kx$A=$e63~NZvz7cmadw#1wRAJ3B_9n#h?w$k|24$ z3iUVCKpE;RzNi|;YFgwfL8(V-XgOJ}szoC3QTVv@Tw@-s68K~8ydql>Q>DxaM;l_^ z@Bv+gp@^YqNi64{_eE2&0y+brg|B_Zdtn_h@VkiU1jXH~RQ*%nH515e1hO+~VYoJ68!W zc>vtJH|pQnuA0)!b|c^hV9Nuh>LeN~DVht7;*NuogwQ>o3*VFg7$8Zx6$lN1xg@yH zN$rzZ0T+T)qh))T^ut$fR_%v;C)hHl7Hp?ywLi@VqR{ zjqfm?@X4sX-8UYzNv<0ow4gJ?{hi&{2M?ccyG7Kzm-spF(ad>hlMRz}e{+uu8L1R; z2#g1cfE$rPkLzpP(Zya*X0MBF!8KC`5ENb4S?{1TxOYg|43H5=J8S_Z+?7?TRyeoS zDS`DW$AEx6uWlAcWZEnZ+7bqIkBjnn16lEOQ=KIi$J0W! z1c&!E^acc0hhhF>bqcDVi_th9aSsi!oLp(@GFb97Coe4`GvbHq^5HwrEqDJW!ZR5# z($x0;ui%t}(xbJiSc44+>oWJ$K}!-F;|qyVwmZy+B~OeiH+rQfw=P%!n~0Gd2N1F( zppp1J=H%7_CUCbY!w2r?<}Od}4~Jg{s}Fd-X$&N35_~vkWPWUn!PysSJn-PPRKQ1% z=op?$>;Gx~E7HGhg}#}0FZ>xP_sz(t*`(g1Qu<0PTn*Wm49Mim z{#Do68i9ZBX&*#>Aaf|uZg)4cEuhr=`>0PF`UgsZ%!zlDx;S7lgJ}c@?f# z2ZTU0)HTcc=k!H7e}$7WCGVV0P<>R4&v4>U0Zm97NX%y}RN(R=ko5RL0BtL|%=qQ_ zvsIs#pHbxorm4kaX_hPeWs$z--B4teK5aBx;QCx+u@#vLKA>%hi;JUwrh6q3Id7vB zTE`HAw7z!OI@9PA3#NCbMGH8bIZ|+Dsg$mB!BrEFQ3!vc6 zRMle_L`P}+PBiGDDl+%*L4xJ!B z9DWC94XDc3gS7ej0A_N5B_Iw09vOH_Tomg^);0x4kji@jtT{j^c%r73AlZLt43y}* zST1*U!Sju%G!m~!o+G_$R;5dBIPdGcf&nEVZNU;r=!{q*{p4RGb)%&(M<;?Ea}I8JAbhl_{e-3XqRBo^FX~W5!neDDM^WGsT93tx`>}E&40Y_Vo zlPaEYu8jZ~mkH1i;AmFy74Jho(+CvGx8!d#ZW5=m5qtoOAv;a!YNRB%29{{RN-EweTvJ$mIYDIsxs?A z!Ax2%LYtG4MjTFqq*f0LfNI5xgR!5XMX7|L@&{tCB4rg5)DQ_}nIN8e zPGYB^CM5SF>Zz()u^5Qf%wz%EqCi0K^NEk62#ftGB2zfDOr7|>KP*f8>KC-Msm*Nw z3)dZiAbw#BaU4+SXr@~J#zgVM_ML5*uS29rEQnBL?j#18K>z?esGJZ~pjTB6Yxabm zmDhP({T|m1b$=jEmC8|Q`3GZp!1y!FAn|LkgRn7L73nk0Adw#r`%3MgFJlIYUxOX= zWy~P)Yp{d9j2R@t@kr_KL`?xI{tOEKB6bkE`od>y%EEauHA|1-@X@pBX<&ZYhQSj80o z20y|M`Z8wF;y{WoQ2aLLWoJCLgS0-02kfBdq@Vwv3zx?{(im#`yG74S zvoD9OT=d(;qWL(B9ubh1KKaSQ`BMC!FN~JHdm((qR~3&|7x&1ZG+W#s915TPbwxDP z2|O~a@zbff3!;M=2O|r^bfU>FgZ{%6= zg0T=!F<{0jK|C01w_0M1TBDt^4ZW9lN5~EMzbZJ#KtZ=B3-8J5<$$e7U11Hd_l7af_@b_ z^7mk5HMy!*m>Lz>hi0Y;q5=w4BxuyB;sCXN`~qucVFoBq$YSn1z9oDJ zb$c?57Q$L}H*BJ{5PNG&09Bg`>_AU@37fkL{7fs`T8pXJE)-B+;wphH5{uyo3-t73 zNivY5&|)W)U8dO&8`9#G>j{DeS z*zZ7Z3y?ok*v=8}Gf|x|{Q;cFGb5vtaKQvFmIvGb-ciWt~#jz7>T zAA%OFQ-9JLh)~-gm?*h#LRvOe}Zzd)Y!xCA}8%-%$of3dy1FhcQ+>l1i1Fci2 z^-kkaR>0)J#;or^A z$E8Sr`BB8PBHDo$FxE{7SYR^dVhZMrl|nA3S&lY&*kFNeE71kRLW@SW(6>RvVrUd( zG{jsETGSaBb8S|Rw8UrL6Dhl$+G8VEfh~jG6qq5`vH=a%U2-s%@YaF{+y@-|tTA2= z#s=2NNXt&*?6}#+*bwiYdSx?V%pc^H0Xm)|a4t=&GG$6H>LM0NvtlD=OSR8NKPyr? zH#_fnRdu_U4N#I^?@jMWA3uM<88s7FYj$qJK^{7SAiT4C_f8UEb^wCOC>&6BbN z=R_~6-f%9n=x{7^aI#kj>H+sXrotr^yiagz;rN{o3Fyvdq-c3daWEW{Xv1px>$J$q za<_;wCk^ZX-uWR_&T;nvIlcYI6?haDN1WyII~bONJqZ3HQ~HL>bLkuxrerL#_%hOg zS!%r7rS^=e_{F7GSK;#zM^QWuOJ{yBxXf@&ijR+Rc!XIEBCz8h2y0QHf%Y*(6P40_ z91+2=s$;e8qBe;bepc_$MdaC_9|98MB5)%JNosW?KEws35*?Yt*~BxoCfX%(_02`WH}%4?K?Fm{=H zEPc&w@XP}&SVcM=dO+^AW;AT0+j$0y4+QSv28Lgo7K~yHbc~WbJ<_(9B2pLY4(-^# zCnbC1hU3R}>5sj$=h&v~UB`~4>QY#G=)0ezXFQ7e#!X|sLR!4uFQO@I!ZT*0+#hg0)2bJlFNr=_mhzwzz-tn_39 z%MD6@K(ey|?-Lt-=B(xJ!SQ3#a`(KwGv(M`v!TeEUc9X+am%`QwiU0>k$(P4gqqr{ z+q%tY*>62?w7_!2cqDb>`-LZt8VqV`v*7`yVfw1IG9}ZLkqagp9Q_;Xj%OurII+&Q z<(&;%jfL-<-cMP39ErpmSo)qh6WMtAhxDn^v0u!5EoQ@>cXEvT?Qa*9nBUJzTL12` z_3!OX)ur+b>vVP1j}SQlAaL2k)2*T2*$QpJ*~IKzdrZGZzpd!RiTyc;Q*=j8Y)Rf= z%sKp?v|JaF`fSo6!=}s(BvEfKGHx-YoY=a3&7O>-DM{%%mgAj>-B2aiBc)Sd8mc@C zjxBC}r|8I*y<75+4bw&_aV%QJ4|nbb3W&T2WH{{Gh$fYE;yOU3tP>)u!9O)WdzcecoHJZ;zMfE zwCqD1lz3Drt1t4prg+uk!0_p#LVeShDjQQqx7L+}VG>v#`L3YA%Ljn&Mmn5uvXZ!1$wy%GE z&#pDEZ(6_o^_^?BBp@_w&Y%pGgk3x1ckb93zkA2-t>5H9xz$H(R?9c?3M{7MtM$^K z{zc?+Q75bt-uPy5UM?jZ*x(~teK1xZuS3|+q-K8LL7_b>F706KH{U#1aM1F`()Bym z?cTq0gFpwfg`IoWZr!vF@xr)-gnfzY5)#($UN5}=&hBkn5iHONy9FPTKjV-D5_ma2)1G-Smd}71N*^72>bVc} z>FMdbwfdbPypdxvX0mny{(FPfba?YU6LLaqc3}h6IejC6Kcv;LHa2@nuIbISJJ#

VlQ#I zrYyu%b@|1CC&+zKAABghq}H-T|I^whvKLs#HuDM7Dj_~qXUcmW={}j1brW~kYPVZA zE> zBNiSGCK|Ei4_g2eS;h_pvKa4N<9pdR)OcC!nstm1od8lOdF6k2JmCaSj18MHXo(b zBU_2hZps5EId00G)+cs`R>hG|lh@?Wsbl#U_u+#!uWmW}aEQ47%0K}*7Hbi4 zq@zG%4RR6&tFUBv>QBf5W<3L}i}RzF295!K|nI?%t=Lpx z3{}~FmF?&`6Ve8eI6nX)9N` z$`|99wD`|t9D;OU4k~wp01uNqCt~DGfW5^TpAlP;2ObGjWI46qqtlj2tOdBU>^O)n zm+%)ml4C8h@SHhbMNq#Ah|2ESLLQ2`^UDt#DQ6216ahk(2B(%B)HZ$~&2KsFCP$8y z$TidK0xJf|yGYb_K_ZjwF6FSYBw3CIl=`wA1Zx)oCM}Qya_m9OB8aM_tYSW4JIF48 z>nt7AzeI?4@Pd`XkwQRMoh%rV5m}Bb(UsGLND8K~ocI>dncg9I1dCH9aNJ}HBJpA* zr;_3oM0_OWT7%SUJZ?prA)47t-(+GbfZ8;3aALfO28@ZaL@epMOCp|G3`!(%5bVdF zR7|x%=f`R%f&4tmtY&B*lsi(>tB~eI+eJpELikB|EA;g2UFHH|vpG-Lma(3+*GyrD zMc9#Hw;qO2*Xvf212A!=unh^f`@!_zF&!xccijoWT_WrWFzW?A`}(b;)SnK zH5(PSP}vAqXpPmQVk4b?##JX>XFXDx@BA&gih|cp#p~55T<46aCe|D%|{6bTMkTd=;|Zr~9;^e6h-3X%2*QDX)nGqnT2rcBJ0 zLS7-IP3~j7grOgMjcJ#e=Bz9*4QNRjxww`C6o$8e!I?H3=P$h++!*O6i^AqgfBaEQ zjP!r>Ka(si`9b)mXY}d%6jZcJ#yRm6U2>XUmtvs9;)|tM#;AIE*DcB6^`n-?sZ<1hi@W5(OYxVJ9L8zX!H!A9-4B&;jO@JU{tmpDQ{lfFLe&L zM&V#G?zZcvyBAP6nU$NOnq^h!w3E{Rq;3=gp>)T(b4Y6wshnGH9SSC>RjnGXB6-n0 z(Tz%+?uiCWgb#z4=~x!(fv{8QN-0XhSEAxMUZm>?>2^BQziGgYFf;}#8?!33xZp;? zrHeQZexB|p;tRk>kK+#R;GmQ^>F~~o7o{hE7?~8N$JGQ>I`Kv6nm25g^w)c%7fIh6 z51+S#bPmQ=kN{7@7&;I)4%ml+0uy64Q0BRy62Nm&K@*BLSj|d-vc{nT?f^sO3`_dt zj~CAWfCM&U8Po|0E|e__3a@||rZ?b5!TJ6S7%kBMCQ zI65$rOa?A#uqVqU3#l&;d3Gr#iSD4cMLH#^MZRzuD){q?6{rY}s>-~;GtVpMRnU0~oc6+TqIw89 zDg=q#Xu8x!z4&Fk6t&3IzS-?)n6sK)zj@b=?UX=AHQdE+6t~7-D3{?KLUmF~ex^dm zxX+GfO|x?9)9?g9fk;FPo|lbvqOuxFX2=ay{CZw=LA0x8g(s?6;d}*dSG_tZXCozs zhz%_SOg0uqr}rLGtA-Or@RR_%PT;=$a47XiU#DCCdP#-hlSAKuT38T}Bv%`T_-NQJ zWP-mINChV`$jhSCRH1r32Za`4(xrF9X1%uX{Vn^g*}Kw1qW9Ftx`QM3-mFDh^{KnpVBc0%ddQBRxYPw}IcPJjil}NfBTsF`SNY`Jk3S{^) zp+y;?anVD*vIGrDNb}y0UigBl&%iW?DhNA)j^q}Y;9|5vWt3L-gfDrMpXcT-KE!;; zfdpqPc^c^qbG|v_DBfddPMLcS?yo}VZSaB6F;$!NNm$f^3}$8q=ZG`OMdTiYuFg-l zzD*uzs9Tw4D4huTkdKtyps-qV?dCWfqK5miz?=`IiK(8OELgPNjMKj~X*lQ28PdB? zMJ-lSWaQvrtBvcL8_fkUy;VOEX>&HxsGzoHGj(&VKNO-`Zt(EH z4T+=O)RcTL%-OY(o%YU$-_XYDI3bE=&cZmxIsD)}Y0NmxDLwgjkoD})Dkp|JPPLej6kz=(&ZRT4=5PlEN7d9KRnBaJa z`>KS&!mqK?^)7|X7r^vp_!rk9B9OhzB;>*`Uch|)Y9NiYdjtj$ykLMV!SUh}A#**p z$Cki0U?sz>LTnd-2xmgGDVV^mJiMzOH0Dt#d_z7(Isi^v@x6$s#T%GMmJm9KUN~M1 zWCVW0GUaXz|0dzkFjJd%!>PH7iV^ZN<%v@5{}a9x=X2>GI}Va(kcS&*j7xBk-v|#$ zEr6kQ6{u_jEJf8~qo|8|RGFPXXT~m3JQv*4{O|~yZepQHa0W!&sw80hitt#z`UXgqM~1+`kDGxDY(d0GIW_o;pn((8aY4dnvA_!CAj6Og z2mwBR6(a*1=C1$&+dr9FYGlYNMjH>b{76$ZLDRP((02fDMRKU4#>6zlCI7+0N1~JA zU_!uv&oSbG7l9=qjG!)w0ZgTNP!C3q@-J?=^yKrXQ;3a*)ebq!T=&yf$_WTK80a`@ zm7m)MEPs-<;4j?0ww$}Kf^>uMd^QgmsL)Pc6<~{uYlw{h28j>RaaK$v7V-(rQi1-p}r3-%u{8H?AZ*>vyi-(%mpA!!e;tHP~1d> z4aMxL>EG?m%a3m;RxdIBPkcjh;7-?<7vf@_l+@|h#w4YsOaImv79;)5rujT1Cz5_g z5lUFGq;61{8rNLH2xqxu?B2_jR73`v7_>pI!Ua?{Roh@?%lxJW%W)wk@BuE!N?aju zg75bE<7d*Fo98c=R*yx`VHIFFN9T@oxLf7kF8DO$3KUGQC0{M-O4Ah>RN5Xa-zT}i z@71y+{tHS;(5V-VR-%4AZr4O$6QVOAFr=w~J8VhDsg7p@U3j`Jy3PAeoG{woUBAzmm6Mc~S8~LVoXRS{C$n(-Ba~f%Y$?(m zxndmD7^#p&YD{A49@FvM1G~)`>4qJfcI?Q{+>(~FZCxr8uKF+CvGijh<`ph&S4+b` z3ZEH9T}b+M;oo2h1__AcC+TpP(X>nh?+|L%)Un$x5Kc!#rVjCKYyfU2#yKwBi=Z^# z@Ri~6sW!RYN!QYv2bX^tF30T3QPX(j_^j`vUc}r{QWqt`3zx4_g)Rl9A>+xQA zjlQ3D9U48jFG^d;Uu$&&R$wM~r@hrDP%9-(p++kdA`s^k^OJ&!wloK0A|MZSm+ldfn@Y z@McJX%1fq?ZiOF{A!MO zzhkr$rybfLO#0oTUG#sj^Ba4=8jPbOMA!cCrQ(wbvqJPQgo=`@K2j&;JHlpe_Ulsq z+NKYl(ohkHC0DR~P*dg;OR2-}XEvpuj0XxY4t2zJ7t$FwCKw;(t9qpPh5&uk*=G_K zU!uPmxyHw0`#+M{7QcG2{iTv?SALo7T50L(h!-B`(`Hy6=hOaY$){Z?9r|T>#M2>6 z0t}2$$%@d3UxY;b;`UP?xFhB?&OI5&8F4l8wo0^I}%H;sY0{^`&|7ZU1wZ~?b6+=Oe>aDzEFz!9K$#^2u8dK$TgI3lgo zO27VD#8T;_PaHEIsfhx$la zt$U4l-xHzV3YmR+_KMJN!PMUty6W#=cPyQooV=td(edYRht8N0I@6eUD9?5zx8#^H z?a+yAYnshom}j){XXZ59p(DvfS^25OHtA1yM#o5HKa5=PC9cAxhR37wpMMP|-GaGL4U1*%??35QpdcjpLGSM zCijmeF}hrGbr1LM`Y*ubCBasIMHgU-4_<&dJvEN6+#gft34ZMQ%jvg!8n85;fK9>N zb0haH1OZRDZjq{|%_k7NBLgCpw`NJ=JS&^$T$2O){18tJ=h@9(4* z*Mx&VH{{l<@}*(82!A=={%KQ>z+sOBbGfmJCz6v3x~hiz*U>pjjCbE7o~B735V#AS z!(+t?fQCE9$(uQd;6v9~_t_K+djzP_)y>sN{+lmZ{yni5ewn%En#{mm;Xqdorog!VtcP0n#1!!qR`H(3S1a{C^j2}~ zf;?CYmp4IPL{WD(S-ce}P|vpqmEM&a+>*++N3&jj0VF_5)0loOskRfA@-Wm4H$DJJ zkm<$^n$%ZtyfK$gym%fo2ZBR8b}opO@j=jLv`=YuVjDOL^XrMdcWdP8DSlfdyOfkp zKd61G_by`w>QMrAF>qi>G#eG`rroeAKJtpyz*^Y;e_c1MicZSQdDs=Jzl-AAyK|%& z3!-8kU$WYPYaah|U$P1#_s~9vFe^EE!5S*e7!^?KjvjqccgSum%FDMT=RZ`h`*?q} z@Xa4zU)|`qe8{ncltamSa%7iQIV?V^w;wuuq$ImAIaRNAQk+qoeJHQwa9Vl>Zh?+F z5dGW(d0q3s;5m)*8`7n&`Y z$kkmm&%p~kk5L({@t|x$#{iDn%QsJw0EB)zDBcO=0@4jRu0EcCNQVlms{sOKP;G!L zLqx?<%s?4BgL&g3Ac%TjTeIgf<(0T6%02bN=pCI($z6p-sA{|Gl7O@HKtNz^ppp6XP_t|*;DD5)TeGucAllq$2)y( zt;(2$o00$;^nj)L+V9E@B2qP+O68PKf`WU=(?6v4JKUf&RijfrWxR$4I2!96yTTiI zPqz`U&Y?C#ja*kvL~w6P8Lz_sG|>xqIE)6qwg$QGHf%h%s|C;T7P5l8W4I{k7T-aU zZi{AXWw>+|IIgtcTC)k?g3h3JDye3$B#stWWBThP~a zTb>|#r|cKDNL;7IHF2UMsYae8lSd^g_R)J$yr$w0_{oJv&R*ooUdmhmuMQ78(|!?3$a|) zA{Ix0WM(%h{EgR@ij!DGpdIIiG|ToO>@22F))qrFd1?ap#G~2;F~Kpi;OH1~$5jmD zonlZazXiLD4+fJ;r1AKXX-udcnd>A%RJ0qc z@^!W{BrEk?xK3Ml`YuwJwCG6oJ94}m8o`d#o;l^py&Zrn-U$jbQ6tSDc8~`JzJ<-> z>+;X*wu^ibH!f4sJC*@K$?Sa^-FeYlbqf=g%c_W`d+sWi+JNhDgy3c3+tmX+RUR8j zQVGWC5Nf>uJ(WwP${n$Ws=u(!ic3|`{xdG@ox10}*+YPGIlY5}o@=1!+g#qjhrxG> zrWdQq-21pbsDdzz;S5IL0z~d?q&d2pls2Lhn!eak!;u*gGl?;e&UogGPeTxkJhX z_!)pjUr(LKvha}TYoZ&S5Sy%G*TebO&0leko#Nun|FKH0gJfyGU%2oc<{e^gQ>`Lo zS+G+O1AS<$_jO(jh&{+d6~-|1u|=F9;A;ZNXxi1_zHGR$Q|<}M0fZ70z+jGx-iZmh zOud;9sOWaKliQ78C-$N#d>{A>=`T;64}A0v_JRds7RtFYUt0&Bb5=k@;zqqn7q${p zfsmKW%lI&E=U@Oaq@K7rf-KD>@LzETp%cngyzVoO7vgi-k5yghx?e_6k$3aO$F zEghLg(hHVJAZYt3JFyQ(o8PQ5Ds zX2=4JLtp5EWGB`P^@%B~{4Kb+AXY`W-Ua_1{-<_Kno(&au;H`F*{lT=1A;(lpF%GI z9u6Lg26%-MZkc{W8cX9nRY?VJ8-s3e1E+h%L0{T$I6t}0a+}&!AmRg1%XM+ zqdnjZm<=(!t`49(y71QZ;D3OY=A?290)A};oQxjF5CH9Q+~BKKZ=__>kW?n9iBRqV zdR1(-ku};|!O@D~FOS{|@)JN;-HB^VTLzo%AWZ_UToRxSJ)B)YeUdAxe5DOEi`GG9 z9Rnchlmd8ap3%a2BHeBV34}o;JLGGwjMsIR1jP}Zyx^eiHRL6!sASEV8c!LSL z$H&MkefBc?!f%Fu(2&nwR_X)770cj~oij1ExeNqnaR!Or?tp4i>zQ&9K8=<7F1K*Q z3Df3(bDf{SQQpco%`R$5RzA zq4u$LE4VPT{eoPk3R0!|y4*Vg>cK1cjVXAmZz`Qrz&kDkF*SCXX&;aj11oJUKG*4h z#8l3U-NYAME#A{(cqh0`-U$~}N;X@FN>1VKdtKhic5e-BoqUcpW>SE267-IKlq%XI zPD5Cj=NcQex*8lqgY}$qL6|p*)rdHdl1i9`k$5XICHvStprg0IB*43oT#a4#RNqxf z9W+ZeFvbhWNt)c?7&BK;hmCYfZn_XPR89dw$>_m8b<`nHCASWMSrF3}TR<1Ywb3cC zi4b49OD{9>WiOQSm+^vf@-+U37=Wi7gaSn1G|j1rGe*csK6Sz&2#?Ap^o-xwH82l}$kk7Gvl}3n{1Q$AE6oS!=@VwvR!+C?b<&#JMYn z0UtgdlVf~55V?1#DWC!2btdnL=H=!_kUbjUJ@4gm2oj(Yp=NtKLT-p3_#DKExOGo_ zQDdJ#+2*?^GN2QRGyLI_6AB(U2uCfhXVK>0t;7Y#1mjqe&xzn_e2QGj^j(?sUAfD7 zzX}!zF>q|4+E^823<#e0P0C*aP83{k8=uL5H5E1 z;3F-1cnnG!=1BS&b`vBvh@ykEIf>-DQ8#DsBJdj+Px$R59*I{;)2TiOyN8WZ%$lfIV_~ZcS-#gTaNpbU%I&8eoRUFa>Mur$T2=NKqp{YWH8K{w+ z4gN!>ZCvYC^?f3E@?|OA709Sy#18qRN3ZYFvY0048zS+yxS&)nRRLMse1l@|DUa zobw>js$+pr9U`~77eOb2%~&M{q-s{sk3qrWEeB1YcLalib;EIm zkEB*K-y)|4@Tb%TQKXDWB-852xj+lPbq`|_yB%u4r2J^s3pB%dBQhPC5<^{awV|dS z#2j$E;5m9G9s9OO`h;@lEZw?^8PrheO07OYl*#m5bzrU#U4z@Bl)gGmh^o}o;eTI6 zlYH}n+}k2wx{ZflOmw(K=A5EVfIzTnrdqk9kM_Bt555KzP^0tDW#qIWuV8&2Hb!yv zYSe{^av%oZ8@_d%w%o3}&`#V=(ky8k1ClfV2dMjm|7cDy?SefGx+l5D4OL%j9UN6O zH>MqNClYN5QZ{agl>tV{jU%_$F9ht^D<~RNgr>3 zwr>sPN}N`4uZhxEymh7BjWZyH}%F( zdaAAfcJ9eLXyqM32g9VEv@%VFWTz_X?(yIwaQzJD&f$Jm;Wy=iL_o?1IqTR2;T&Z8Cu;V=frA;kT<(c; zoPzm*3CsY*tE1-2`51)lG~DqjcUD6Whcp{CfD`A zV@5B6-#f|a4E>Y5k+K7vqM7(v1H?2K&n51P4$cGU!Z-xj1I^koEDw|aguv-+=N~CQ z3EUWtW9csTQ=(Ic}aa0O6gVf0Dh+$cK{+t0l{r)ppKMgUhLM%BVn{Q}dyOF`!nH-4tOm%x&z z#LxFIk>Fz?9S|d*2qq6?R7yKBBh}Hy=mM24iGKhZO!*^B8fpNI;5jf1je~wizuQp+ zCqVcO3K+H);WPk+`2*T?@p?Vpz(48iA4AH08$**|-Zuz#^4c?9A6tUwXi417A7+x< zCxLN9^Oe`8>&)_yc#i}#&|$nAfaM+_9^2xY6CA1NkU!`9%Ks)Y6zCuRgL7qQMq6_k zu2WU>gFKb{?g0eoAGu#ddJV8f9O{!!_%m*b9_KxZD={AwDFK7Ep2>Rh@sB$RegU2N z0WSvl#^saBg$@kCJvOu%%oBtoxtJTC)6C&@l}N1*I!c+V8~ddC-{9`1fi(VRdhNn> zHQoU%X1)MW0H(*ekE-C2$})ryC-u%%Q0^e~lMed9azFum6__6?pnxy>&X)lr@D?;f z5xkJdoGP7?LJp;Y^M7RI5kF#HeRQNHD}pP`m^QFZ#8F8gPhG>V03ggK$weK7X{)YK zOpe7UX-z%XJ?-a%RyI&X?iDo=UM2&z9woeaQ(fCqx|$;w-q3%Dz=69gZz{etI4GmrcDT^^_4&1Ff8b+Gebkh4z9xj-fQ z=&z&Hss2m8ifR&P%o$33wGS(&RBKn2zN~X~dpO_lT!$S?P9#-76k`ecr>Eo_jhuBN z=z%5SA=-E<$M8Q5A8_$Gn@BKU0`rA8f0{Q1dJRS*kTp~?pb&R{&^>tHi?|=|;a)=O zsJxDM1c}9BEAq5cvnFk&M zVFippBIs+4Dv$!fq6(BlrIEpLsVuSpVf#CZEszcS(BQ)FD7GLzD!A}FiYh+Eun?E}?5MNLwA2{mQ;)sMykJk3PSBb_cT5jSlH zYEPl+C%q?9Ed#+{vYC1i@J4nHiZSE39gaj4vNBWVhjyBRP7P@~9joQYFBC(~S=88} zs#G)@W<%3LrB__y$7)BT#2;O3$IDpa_D$SMfvP;HHfqNeRH#)%Ri{x}mcOh{3(Zgg zlU`(PQLGdv!WvUsMZ2tz7q)325S*y72<2HtR(~7N;r*+37gDiZjuyBEE1sPrd9V+v z!^_srDh69v5o$}KIvQ%$;*t>5Eat_jF%Lhi=@J=MTggTjf{6j_6LO5WsF7-F=NmHs z9@<(eOMMjos&_jwXd6pVv^EKd0pw7}_hV=f(F#mO1*rSY>ku(G(w(@xxGXCHo<$XT zaZ_uwx=z%QmM;8x^b&D1U6{sS20ozdEX_k5A2n)iRL%%rV6HTC)(r}vvRg!1(IeEV zHrv&EdIH5t2{qYfpb{;|o>f8+>4IBwFjn;5>GgHMcvN?SZWRT2;`Tt?+@^j&WmS`a z`%iLnX-!n@7=(tK+xXY?ttHb~fNew7SKz0L`lz!T*eKy8y@8c&eaVo{+HLTow`f9@ z?M+hz%|s1!-ZuuoxoPc34zwYc81D=Etf8B1?CdQF#sEJ;~G)vM}(iR1vmhQ zY!Gy)Pe(fw@4s_H+Wf<(mxw$H55)k=E9^~pY2>L>aKKRSa-9S}5_bturrQ+xxDnXl zQ9EQ0Nq1JT@+m)5lkNJrJ-(B`CQ0*p=dNkWc-OGe70l-Rw3d` z7NMtOz|%2K@1F01RjOLo%ox4^ZwnUBCr?1BC`WNSbjlq9Xv2L*NCy?Umjm4+Nfd6z z`_dw47r1rAoyeZSQUm}{eGF;bzOqqBB$8U^hT$f_3bE`pn|P)-whMhys!xRzG7mN` zAjlHxm{>+jo)zu48Mp_HIdc6gsVemjpAEi6Q)Kyn2xZCT6u9EfUBuwsW4EZr+ayA0 zZQNnZkE+4Ero?H$fi`5Q4)Z2o&e&y~FT`4LVp0}|?s*4qcw9IFN(aOL-|o)kx323t z<4BRT0f822fFkLnW!a{7jL5K)0BKxgU?2smQPRev!h=vEWw8-Og`}c-&~DN+DN3S5 z$)-d~q)bVcESVM`5+y|J}Ez3=Z{?RMcnApux!uNtYCW~)BWZ|tH0{BhF z??bx+_)W&|gK~uJ2;esvzYk3W@NcgY!`HBr(!Q9LuRDG(ZHSk^UY|?X1hN*{yRc=w+Rz#p>gWyJ%hte9M{nR>wg$q+wrg+TUbY5ee>`jAy@B)R@>SEgSFVP2U3@t_j&mMThw9# zDq-HUcEE5!DctIQKr24_0@?wo`>xp!=zjN$+5u?;m|`h}mrwNt#!MAp_MxtetJK7( z5pK6O!9cZqBYlGHZW9c&t$Nc`eSLySy^dlIt+xs07lKu>HQFuMu4cj9)u2v5RuQdN zJE8T6r!C_zXdYyB%lZcwhz|@CuRKKPn_w#>*!#g|hYzpk2mxwQEy4Mv*m;Cv?>q{b zPEThsSg}dx%Rl39{{#aUKl~ZN-k$DvJAXm^6Y@F2|HQ|K2$2&v?;a+gP89Osz~JEV zV`Aq%M7&bb^X|^W0Ja?^UjTVEKQrElZUxmv3w`1+bm9b3;e{mr%&!Kac~fW*DvN`M zp~!o+6G$b6%I#Bo#P<|@J=EI^saZ;Qrx(Gy2p_d3r~tnryF+}F7ZdyiW1i?~@|Yp! zNQl|#DS4Pi0I@iP83gAC6fNxy2Z{Q=#XyD+6Y)Kah~DU=>e9tG)O+O6K4Rw(WFvvj zA!ZHj-I2cj1B9P>y~5byJR2Zx`bMM#WGMQXg+>$iEtQ@eEVq+6n{Mo zJTMM@7&)vJ0Dc$Fw0?W`35mOB;GNPM@LT6lBN+R=G-F{24c2|9tWEq0NwyTa5vUH8-hs#yqBGWo1$VYZ zmi2p`NGJ|cA!n)3oBN4|E7PN8XPAcvPH1(zj~ydFLx}t6z#va0uD!4G03@5qPY!qg zh93km4Gu~nJTz}g+zA(1uQ=#f^i)%$PC8GecAowN z5>nKTH}?+uU$Z-#ji7DM?;!59QCX1Q$K;dDnscX>xuh*Kw_{;44w<|EadM-PFIHeUrOTrzZ*Ft31-G>(>B)6qXQ=_gEqVR)31Fk zq|)ty$?>2#aHK9WRkoLgJr7Z1&}<%2!m=x4`b|GDT}5jK`wkvX%Jmo3v2mHNuR9u%BYlbDM!8H z%tNs8IGQN!E^H_z~okF%1xs5D!ywV-;kyoxbNa#(TjmB&F0s);Qp<oAmYMji4~ zp^5Ikb~g8gCc66;rD<(y?(-WnTPT}(mOj6ryRV(ieIQjd+}+pC=DyHGcV8XNeMqpr z{=Q&M-6}wwS@hsVT@8MiI#v2w0=}jGzF@#o{-6dL{Hpu=)V%ZfaiNheKW2~%Eq3~~ z+UOUs011oV*Do0KUQ5k>E%o|^`kMWS2OQr4^2ia7nvG!u z;relCb{S4({Sk}ua7FWN3PA{bc5`D89Ub+69d+&0jfN`M-=p9YE(U9A@VX2XsZpS# zY=c5aM@^I?6YP!{M>EfGTCsjXFAZf;I{;G#WNt-PlI$?Ma|hE%Dh>^q4AaBk8IG z8@0V5Aw+yRuT`eesk(k33-KcD$lw}vbi_VP%c15~3>8`cQ*sRA{U z&Skm}>?>&}Jn$Jw(>kY7{fI`Ev9zwVN=Ty=(MW(s6+H*&vu^LTz>GR}#WL#1LK#88 zAP`c=G7?vXSF)9$jCSreKr4Oq)u_=dj8S+Yj!|W+fyq1zVzeWM5ra!D1>*H3sRl5j zY&Qd7WOm6rr8DqF^`aN?O5W5UbWy0nF3PkQ&~qyPzFFuZv}A|gf>LY2pZeKvs72+V zMYG*+eEp{}JlAc->ig1Sb%N)L%&Uyr>cpuw{!QDq;Yc@I-0&7V3`` z<%AtG(br_wsep*r)-%;il=+~7l1w=^bi~N%YpR7Oo>`JC;q+r9asH*RpELx~(oFPM z!Qa{c%761uX20>*fArNL30su&E!DMml9uRxqm!U5o-RJ8m+um%MK*kBxmnd~XFpF9 zH5NSmxt`bgrY$MsA!J?(T&wQ9kCJvmusyw~!oac4P@Zq0y}%j(6bEMf!`<%;9POiG z-_}{>`AQuTN>^?v$s%|Lp|2O6_?c%1Ogg0~{+s*N(ZaN?UqN}NxVk+9ykCUdmPW9p zIMV&Dhq}GPMB`v_dHsQ2Q!E1XQB@97oPV%y;P{YI1~Ic@U-q2W5a|zP7^I+YcO63Q zPS6QMMuOx9jVNkEh`FfF0p2pmYxt^qHhd2#ZJ{Ogyo){`>O6kTM7&=~05$PQ(@&Uq zY}H)o_I#7q+gKQE0VvqhkP=b&#e9_dLd`gh(ru8Al=!%z?O z(pR7aOq{OiH?-uxGMOIoXuS5$yDW|}7x6(FT{<5<&T?*KBx9cfeFqdm5+ z?`qaP1I$+YHKv71DL-vl%ac_P>rJ>zJ?BKhD0I9LQ zVDdGwqCBXQzVL=+2VILuzqoz!f45R;6P@+sWGLyTtJ(e} zOZ(3Q2j1w!&`6-YC(w$c!-r8R>$qZ9W28@z;YDYx%cgvl_I395(;PEEA5rI_BYg*d zg<0UI$_@|qS`JVRI|q*s_Jx5Vi7v^VC;D$1+}r=XQX-S7W$zK1VDGPw_Z{o+#c}^l z(jiFDDvNEz^~qWpfB^t4z+LZhWTQ|6+DNz1rlTDV20KCxXrnr#9Ue|-3fHKkBWa2V z6|hF{M^K|mOe1?)8MaV+5RF0x(x}Ofh?W{6f}-3J(x}Ofh$b2$N{B`wf*tSF5z*1X zwP6qOj10z^{3(#o?ZPwSPP|k2$h@C2jEgYE7TvK4LSDJxVD}EkD2NTD-rfg=&ywZ9 zpKeFvL-=x0j0&My6r;vKjC>)zI#1zc3rDKmK{NubAsCs8G>%^sn;k+^_(f&#MGc@A z5x=Y;r-{k240^UA6`W#qU>7yeR||gWeJ(An&7BCO$=S=cNx1;7k*9VgYZdY3QDnMRXSDG zz~JYu3j4FLj!e(D086PBckeW7h}kIApf=je_62mVjM*quQX7?KidI_J_nz_^1&p-< zpb5{BK(AeIta+hPUL%a)4y;BykQ#*qIDog-)5Q=EvIIESfnZqvm?dbK(I{jnjT&!Z zu<5$IBKU2Be27^2TyT+ zOg{61NZ)RCav3Ful>=R|U=dODKshg8%@zg5)C!N$#IbKZI`-|~dkwTdBcr3B^ZYqx z2NvV@N!+iE+~Nk6ohnC~8Wn`Z+HFv5odKAy74ED<>c!j@NwyfS@rqfpP)7*VD_2DP zCkCcxC)I!K#DRRRyKo9OVPDo}L)H~n7j^tBwk7`LuI-&}$gnE?Sxs_}tB z!F`3>DgfFMR^*hE5;|IEaq^yI0)8Y`e0eTE^Lc69!3Q2uoDDlLN>yO1me50tPJH2& zO~n=Hsa#Y7)nFoA{}KyuMayIO6=*FB;d3MWL%^LOu^9stfqW`h zQ&qQ&8PO8w#**V_YAjuP2(;&*(&_)XRH$4nLOWn6lqd!d!^RaJ+y1%>tX~6 zK(NzM1hp|!qu(fZ^;&KstQD51t4juI68Dh_WYjvU$R$b2n{Z5=VLYx|h?`3nFsA-x zm7t=n=Y^vRIj-+f0J$acEXdr-Lsbl~jj{|<_La2&yP2dozK*I!qqVT8qN>UH?GaFP zi7rMFm>lg_pNq(R{>Jv)qEz+lMc$%eSV^yIgFx#RAUYKH5OUYBtzg#G1tj#B^7kjV zFD|?!#*7rp-xb$=o$tR+%ifa*+;27F6^gvZxFoOY9F`fw>bv_h-xerqdbn{+OiVc64nCBU0VUiUvax^Br zY}BHrU3&3@O;+9Ck?u#&=(3^UgJBBe>**pz8b}q6hbPQOp@y+(9S!rJ16PoKy8x*0 z!nN(W$=t|Og1-*f1A;m);-SbAFjEP5^`bzLO<8z|XO`0m+W?4vqIYPLr%98P`QqEb zw3`UADZoo^OJ36Yf|bskVHcSf4G3ocXX5_t2J#6F~$T*NvaBliVlwgUaxx~9FCXdMXcEU-_*sN=+y`s`$S z!st}om{OUYaXz}l+>&Bv6R`&f80S9A8I_+M9tmrb%)b#5b%^!?BfN2&Fm!(Ndf~xm#o3#p{y<4KmbWk8@FAl@+*%9khBrS0lb;Z$ieoUQ zZs}}Nv=Lg6Rmc$U=RSOzo6+y{7fu#dSKtq2=#0iE&aBa2w-Q+^%LSfm5L1PHiQl3) zW02LX=8XO_#UbKeh2c(Z46;%ko`ypE=ri4=Bo{Jb%=yWrD?MUo8=!}#gbZZ+#!Uzc zus0lo=`Yy#{`X{;@=$ICaKmiRE)~Y`+l1uqP}hDSato!HH-K+#fKu2OZa|XO<~hC6 zXcc%!;l?=w%UUj4gNhzI{Ry%_qMoH-2tGu&tdb_cR+AUvmbLpXU(gSs+WL5n?`vBF zz9{lWI)|@7#T}lWCOFnr2YiikTX-xF? z{wIsk!fjpSZPBVfTtXcw4q`LIO2p?ny|ABZSe?oT+3UOM(U}{+$CxFI`4!fVi-Gg} z`Tmw{Z+|RJx0@heE3Pf1^C~QXV||J5U}uD8h93-8+T66$8$iFpg@G^UKb>=kF}OhSOTR-nBGdMb+2V&+Be@wo409^~Pl46)@7>K$K@W5%w{kMKa@k3}G?es) zL~c?#%B}ClEI;<&J6V83IDIinF|PCcmu92j?Xq7<)_TpWkb*`UzkJq~s*9+Ik`Bc;REqJQUta>3*O_r3nA^T_T7~oH^(Gn1gb0B1=lm)GOsvSv zitDQ{s~dmk7x7vY{@0NE404T`%Hapw@9K3%0sdYQ>W5`nGdBhjC2EXYQMDoYlZzWN zW;990hP}Qbh8e)jH6heO%7Pi)ysJ~zObv>v`A1g^195YKekPt&n4jh%?f(a%#C1%7 z!V0jHaG+ylX0KsTc*!>X*OQ+~%8#8k=I|+?0jZBf(Y09X37H6YFA;a$HluoL9lU+s<2))ZZ@E3NW+xX% z?C|!ShzbjzKI4S+GWy)jrR}-(!hvN?1_E}iD-sbZ&){V1*MAu_>knrO3% z^1G4qGG>?9i4eYAaL&8m{|`9t{u<8obLR_`N9^6=YrlY%)srlo|_uapW(__!!Opdg7Hh+2j}gBP33uv z)(PF@n&fv#Q2mR~E~0d|ZM@J*Myv>*?A zxPcQTO4$PJ(KSMU&`Wn zxFpyT?AGT`bV-7E8@t5I;BZh%JN**e%egi1`p@$qzE@mbF5WTkgi4(9{Ak}pc67IgC)hEJDmnE!1MDI-7jee0XaFKfBW zOW1C{ssqI&qkDq0iY4^oIuoWo&Yu$RU*YN~3hFygVsvKE>B;o}Yr6J1i;p>SH3e0j zh7V3=w*1s7OgQ!=pHjq@>_EJX+bw_rxh+%empRSESTXdBwXk^^O^}RipnR>crMm-R zyiCmC?XGl~AwT-Wv z%F3P5ta*RoqYxRGQpb6ym)ee85g1dQTUf{9%@ABwX^E}ov36C&A(;?a%nrV`Fbl^quy ze1yr3Zh2O=x64%MqjjlONh;(leyJyUtZEZk&A}eW9h_(AzI_u5a&wQNayRq zBOtSpszhxE4629BeW5uBD-8(;I%Wb&U>v8{UwsWJao;HkN-VlJ7nQD^8s);p6_>ZW zrSUn2qwE`b#O4G5rA;$CtILtf7yZsbFe`Unf{y5_MM!<*mQoLF-x0{(#SN0Z zY5HAedvuxAlu{CJo4zl{fE@PcdbnS-gNMy|y7D=j%kB&rBiHpGeLoHM`?s|07jEZQ zM}0Gp%Ar9{BflvS+0-O#&ppgdvOH6fy3La6iY;t?sP|+uM(bg-u(`;ki>LhXZz4C> z#CYz(r}>Sss0B(f%+G7Hr#o1*hITV3jx7L8CYeso<+hBj)-J|rs)#>%oDIjENIsG% zF?~v?he; zF5U!v0Jp6~+U(Qf3byl-q5{cvmW9R6>GJf+DBfLsKqA~SiOFz6{%pR7_Gc3Oly1aZ z2JRzys9E`cue_po{tf=|d|NF(VY}JIdB#pzbIM(0+SCh+x`Uluj}uC-uMT#bUXzQf zXz+X5f`p$>-PhY6f1;b_{ST0gAb`dKM5fo=-O4{$(nk~bIU>`OlyCD@mJ)H>&@%gt zgD_sslI7$+$|AtO%fG~ijQai3yzBHsoud|q{Ih9UG02MNfiSXY>*#9ky0$4dlH$on z|NiZ-egpQ_HhH@`gQ}d}XhOQQP&HlMVBtbt*A3)fn%b1FTaT-omL7lsb@`4vx?v|z zmokj>+}_2{-O0`65uQzdfs-3r_Ht7vx1*yG7njN~`J(hhw&3Bm?%)P)L~?Lz__rM$ z?%i4?if->&-?nAfwxh!w+fd=z@@chW%ZFLNwx(O#QJ+_v=md&xK5dw8(5Y>+Pa9G6 zu`G2uwb7-m^k~&3ZD&d3D)Ga1cW)LHc|USecec!%EjhDo_hsFc4f16xU0L=u3=>ZOadJlA9hx?;7-Z-sfX+Hq~bZ%u~uVz1S^(`&h`VX@3*-N|ER zF?aJ zp22<3%-q`HdA`{3%obnM_RwJUb>a>n6H*g6a;T4+(CXUj#Lgs@*VTpBp|Q&AtehII zyN*eYxUs$HTIWyn65TMgyXHDG;%sb(cmu+@^#m3H0Vvh3x2B}FbRa1jWHzOjIrpu{I%U@nfjXcsiYLi`3LtR=?6;Ov57jCG4x{dYIVZkpjM2Fck zbrw*&A;ogEUOydnE1*Vp3WGf!OSV~c>9iU0CJU!SLzUC*FPsjlH7qNf76czNs;Zi< zD4IsUFf27Irghb;D46EkoKI9sm-e7c!AioPX-%@*}uQiOg6etqRU`tNI?Pr$D-knJ^y{c0Kjc)Cg7=NVlUt8UfT( z9BsWoW!2GbE{;yN?GB~UVN{(=bLR?|x40sCF;qoqv~BNt3Zs2XG*%TIN@dZds%T`_ zVo5YZsjnV7s34ku)%DN}B1VMnLZzT`#5#+iV=Z)DrO>KT^9&HyeD6x3k*C!_DCLPO5 zOAwW>Duo@%g&b*iIyPpzMC0I0{Ch8ctgWsU=U;C}!7MCLc delta 34163 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 { isApprovalContinue ? "yes" : "no", ); + const streamBuffer = createStreamBuffer(sessionId, { + modelId: model.id, + userMessageId: userMessage?.id ?? null, + }); + const result = streamText({ model: languageModel, system: systemPrompt || undefined, @@ -569,6 +575,33 @@ export default defineEventHandler(async (event) => { headers: responseHeaders, }); + function wrapStreamWithBuffer(originalBody: ReadableStream): ReadableStream { + return new ReadableStream({ + start(controller) { + const reader = originalBody.getReader(); + function pump() { + reader.read().then(({ done, value }) => { + if (done) { + markBufferDone(sessionId); + controller.close(); + return; + } + if (value) { + appendChunk(sessionId, value); + controller.enqueue(value); + } + pump(); + }).catch((err) => { + logger.error("[%s] [AGENT-CHAT] buffer stream read error: %s", event.context.requestId ?? "-", err instanceof Error ? err.message : String(err)); + markBufferDone(sessionId); + controller.close(); + }); + } + pump(); + }, + }); + } + if (continueAfterApproval && Object.keys(approvalToolResults).length > 0) { const originalBody = response.body; if (originalBody) { @@ -581,6 +614,7 @@ export default defineEventHandler(async (event) => { })}\n\n`, ); const prefix = new TextEncoder().encode(prefixChunks.join("")); + appendChunk(sessionId, prefix); const transformed = new ReadableStream({ async start(controller) { @@ -590,15 +624,20 @@ export default defineEventHandler(async (event) => { while (true) { const { done, value } = await reader.read(); if (done) break; - controller.enqueue(value); + if (value) { + appendChunk(sessionId, value); + controller.enqueue(value); + } } } catch (err) { logger.error("[%s] [AGENT-CHAT] stream read error: %s", event.context.requestId ?? "-", err instanceof Error ? err.message : String(err)); const errorChunk = new TextEncoder().encode( `data: ${JSON.stringify({ type: "error", errorText: "流式响应中断" })}\n\n`, ); + appendChunk(sessionId, errorChunk); controller.enqueue(errorChunk); } finally { + markBufferDone(sessionId); controller.close(); } }, @@ -612,5 +651,14 @@ export default defineEventHandler(async (event) => { } } + if (response.body) { + const bufferedBody = wrapStreamWithBuffer(response.body); + return new Response(bufferedBody, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); + } + return response; }); diff --git a/server/api/agent/chat/stream.get.ts b/server/api/agent/chat/stream.get.ts new file mode 100644 index 0000000..bf050fb --- /dev/null +++ b/server/api/agent/chat/stream.get.ts @@ -0,0 +1,89 @@ +import { defineEventHandler, getQuery } from "h3"; +import { R } from "#server/utils/response"; +import { getCurrentUser } from "#server/utils/context"; +import { getTempTokenFromCookie } from "#server/service/agent/temp-token"; +import { getSessionByIdAndUser } from "#server/service/agent/session"; +import { getStreamBuffer, subscribeToBuffer } from "#server/service/agent/stream-buffer"; +import log4js from "logger"; + +const logger = log4js.getLogger("APP"); + +export default defineEventHandler(async (event) => { + const user = await getCurrentUser(event); + const tempToken = getTempTokenFromCookie(event); + + if (!user && !tempToken) { + throw createError({ statusCode: 401, statusMessage: "请先登录或创建临时会话" }); + } + + const query = getQuery(event); + const sessionId = query.sessionId as string | undefined; + + if (!sessionId) { + throw createError({ statusCode: 400, statusMessage: "缺少 sessionId 参数" }); + } + + const session = await getSessionByIdAndUser(sessionId, user?.id ?? null, tempToken); + if (!session) { + throw createError({ statusCode: 404, statusMessage: "会话不存在" }); + } + + const buf = getStreamBuffer(sessionId); + + if (!buf) { + return R.success({ active: false, reason: "no-buffer" }); + } + + if (buf.done) { + return R.success({ active: false, reason: "already-done", modelId: buf.modelId }); + } + + const stream = new ReadableStream({ + start(controller) { + for (const chunk of buf.chunks) { + controller.enqueue(chunk); + } + + if (buf.done) { + controller.close(); + return; + } + + const unsubscribe = subscribeToBuffer( + sessionId, + (chunk) => { + try { + controller.enqueue(chunk); + } catch (e) { + logger.error("[STREAM-RESUME] enqueue error: %s", e instanceof Error ? e.message : String(e)); + } + }, + () => { + try { + controller.close(); + } catch { + } + }, + ); + + event.node.req.on("close", () => { + unsubscribe(); + try { + controller.close(); + } catch { + } + }); + }, + }); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream; charset=utf-8", + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "X-Stream-Resume": "true", + "X-Stream-Model-Id": String(buf.modelId ?? ""), + "X-Stream-User-Message-Id": buf.userMessageId ?? "", + }, + }); +}); diff --git a/server/service/agent/stream-buffer.ts b/server/service/agent/stream-buffer.ts new file mode 100644 index 0000000..0b20479 --- /dev/null +++ b/server/service/agent/stream-buffer.ts @@ -0,0 +1,129 @@ +import log4js from "logger"; + +const logger = log4js.getLogger("APP"); + +const BUFFER_TTL_MS = 5 * 60 * 1000; +const CLEANUP_INTERVAL_MS = 60 * 1000; + +export interface StreamBuffer { + chunks: Uint8Array[]; + done: boolean; + createdAt: number; + doneAt: number | null; + modelId: number | null; + userMessageId: string | null; + subscribers: Array<(chunk: Uint8Array) => void>; + doneSubscribers: Array<() => void>; +} + +const buffers = new Map(); + +let cleanupTimer: ReturnType | null = null; + +function ensureCleanupTimer() { + if (cleanupTimer) return; + cleanupTimer = setInterval(() => { + const now = Date.now(); + for (const [sid, buf] of buffers) { + const expireAt = buf.doneAt ?? buf.createdAt + BUFFER_TTL_MS; + if (now > expireAt) { + buffers.delete(sid); + logger.info("[STREAM-BUFFER] expired and removed sessionId=%s", sid); + } + } + if (buffers.size === 0 && cleanupTimer) { + clearInterval(cleanupTimer); + cleanupTimer = null; + } + }, CLEANUP_INTERVAL_MS); + cleanupTimer.unref?.(); +} + +export function createStreamBuffer( + sessionId: string, + meta: { modelId?: number | null; userMessageId?: string | null }, +): StreamBuffer { + const buf: StreamBuffer = { + chunks: [], + done: false, + createdAt: Date.now(), + doneAt: null, + modelId: meta.modelId ?? null, + userMessageId: meta.userMessageId ?? null, + subscribers: [], + doneSubscribers: [], + }; + buffers.set(sessionId, buf); + ensureCleanupTimer(); + logger.info("[STREAM-BUFFER] created sessionId=%s", sessionId); + return buf; +} + +export function appendChunk(sessionId: string, chunk: Uint8Array): void { + const buf = buffers.get(sessionId); + if (!buf || buf.done) return; + buf.chunks.push(chunk); + for (const sub of buf.subscribers) { + try { + sub(chunk); + } catch (e) { + logger.error("[STREAM-BUFFER] subscriber error: %s", e instanceof Error ? e.message : String(e)); + } + } +} + +export function markBufferDone(sessionId: string): void { + const buf = buffers.get(sessionId); + if (!buf) return; + buf.done = true; + buf.doneAt = Date.now(); + const doneSubs = buf.doneSubscribers; + buf.doneSubscribers = []; + buf.subscribers = []; + for (const sub of doneSubs) { + try { + sub(); + } catch (e) { + logger.error("[STREAM-BUFFER] done subscriber error: %s", e instanceof Error ? e.message : String(e)); + } + } + logger.info("[STREAM-BUFFER] done sessionId=%s chunks=%d", sessionId, buf.chunks.length); +} + +export function getStreamBuffer(sessionId: string): StreamBuffer | undefined { + return buffers.get(sessionId); +} + +export function removeStreamBuffer(sessionId: string): void { + buffers.delete(sessionId); +} + +export function hasActiveStream(sessionId: string): boolean { + const buf = buffers.get(sessionId); + return !!buf && !buf.done; +} + +export function subscribeToBuffer( + sessionId: string, + onChunk: (chunk: Uint8Array) => void, + onDone: () => void, +): () => void { + const buf = buffers.get(sessionId); + if (!buf) { + onDone(); + return () => {}; + } + + if (buf.done) { + onDone(); + return () => {}; + } + + buf.subscribers.push(onChunk); + buf.doneSubscribers.push(onDone); + + return () => { + buf.subscribers = buf.subscribers.filter((s) => s !== onChunk); + buf.doneSubscribers = buf.doneSubscribers.filter((s) => s !== onDone); + }; +}