V(q
zEGMvL;jyK^(-X6zusg}UjiHb4Yd6*F@sIK$cjQ5+syy1g@N6cs1MQYNAsq&wHUVL5feG=NJEe%`_D2k*DDSF9&3D@)RT`#XM94Q30EC
zX!VtJ6rC23X$m@&1!3^e?qw$a=`*ohX1-E<@0AZ31PU7aj7;d)!J
znDW)r(ne&4u%ASd85Wf)?k^+I1wBFItbvhGhs}wrm$?9zSs<
zmqwzmsNP151c!GqQ1t#c{fhq9{ajO`3Ml9WL?7AsN^OkG944)fBafvQu_i}Td>uw%ioP&9O^lO%{J
zfG89lrK5e*#&sx1N5P=2eiTGi8)rpQ|G0|tJKQL#a!Hamwjd1Ex=Gp9ct^^?)Qb&7
z$R{1Z>&V0bZO%j_KII-f5{Z@(W6Q3NUaGij#9}k0OC@c7j^FIBxMsLEIeQe|0yzMQ
zDKUY-Y&QWgC>R+GPQJe?9!zX#!=M7|5V)txF7EpY)#UYoYtt~(8fPv;$nW6^J=!Vb
z%4LlLC=iG-3{#e{hs~p)1Z{0_&N3Z?sf~>d8#=y~-MP98-w%2ctwNqXx^w=cPQ~}lpGkk
z$G{u=&j>!^sJDbf%#21x!W*xB7=9t3@Fd494KV3Y>&a_Vq>^W`S#<_j(K%9iVnRlWRDv9k+tKm}
zdW)XcVUHwa|Ecb18Y^3SxA*18Zflj?ph=FpV-vL(dd{-<&PNgfB~tL
zyLYY!fUu-;N;%Q;FI9wyA}mkMl~LlQ;{kIS_o%A#N1?WV+NrIRIG;yGj!T;eQPHjS
zg44pVgM?nMHYef=8HVQ%`<^zP5CmnC70HsMh8KC_R4y{s1#%dwC;=Jb+c@gU+dSRJL)TC0au|W!ha=t}xam^jc^O>Bf@DitIB7ESHtMTxu-d
z4=&6ag)=S&QnBWOVlGw!#k71&e3s-*0SdDbrmQhPK^iZPr00TlB$)wzMU*boLD8jZ
z@l?soQmW~8thr=rcT?t}Er2S{+`%yJzPUiXX;p?OWgpkPwWmKnm0<@0DnOcO>02$>
zNQyA=BCzC;0RzF7=sP035c-jPQ2olU4}e3Zc{dQS?eYh4y;J9PKfH8&B_D_g491b}
zi)v>Ky-K+xe;QN0iNO_TDik9XzpJcYJl<8U{rx6s7
z9K&3BIj>pLCSA}oipBy$CPIbHXdBWBtI#X-3LFHbhcgfY&1Kr;H7tkpLeA{b5Lh@L
z?P@WRqE8UZ;I2Nd2>F&;9^LmYK6?-L~&|
zDgwb%vI_r;!=qamC$=P7pL{1MvA*7W*@$hsVTq^Jew
z5a#ok|17&+p%zF^SD*yqyTt0KUVnV8Rfro^G*7
z(9vyI@vGhoE#+y9lUZ^#hu=mOuaU5$NYx_xNSBT!TMmvImobaJsr?z%!1sOIiT7S$>+XRYe+(dkp!0@1
zkZgNXIs!lYhm`AlO2D92TfD#=-&*Sd!c0fQEAiL>o;i%&s*a;(_U|#jUtv&Kj7iC1
z4=}WLX`XH?AwXjrB1y2{Ea>-c8jJoA3i#R-o`z^$2j3;_`6QC$%$t#oitM~BIX`ZKu
zC=fV3^(`(I`E?)0<*IVBvOghohA
zA=+ApCF`ms4R?91{)h#EAzlF<189N_O{R_HX#!Q&mS#H>ibE5s3S#jIEu*Caj`sI>
zy_or~MO+N~v*(d7M~4D!?`e&3ffz%I<~&O@PEh5wLyAhFMU`zH8714(*U|{`Pjx!_
z^qo>P&TM9P|H<~~oIc%aZ;w(vRny)&Uf<>0P^qOCTpZ|XHZ7W|x^f>q9q1>le)9vW
zBQ4R{-8i7l=g4I^hr3O_pVA2aNpzTn`GW8}Rm4~pe8=f8U8I}PwiQz{C=vlpA85%PGMn%J~lO!&fJvB
z%Ln_7a^E`gKX0(~dj1wUc_Pu-?*8wqyUO>R#^(~lJ3?7}imxWyZe*4(tU1LNO&y(b
zN`adQmO6dCthHn@-lK0m5$tzP39U|MkhXiOT|2N=xs?Id*s_#{P1^Xg`|><08>^)T
z7wvl7lL7X3J?tm1sl#uFt@{IK-iN(HHo^zcygVPu*%w{-?6%pHLEosMW7+--xx7VX
zJFf9^DKFW#3A0yVXq;z81^mihDb#!M3_%ojO&^vv=M><3d&lPnOt!bA&{0OOb~?^j|vmtrtQfo6+kE-
zr@e41?1AG$xQX=RV3m^!Cd`1zj(7X9<{T>ir~%B8o`@Sc$ByWdpruMM5WEc5e35r
zHdDt#iHADl*yGCupqZBnfMjxk9au_^IJLT73Si0A1#2O+An`s?b2k@w6sGa{@K|y%-kcxNr!pwZwlegmX9aPufqAXj@g~M=I!e
zYANiyGxlgjOd}ORWhnVIV4N{i!NhaI8*`D`KvIiLjDnObbOy|t!cnc`T+TfEmI`wb
zLz5?2;Zb44iFLrnfE9tA8(}os%z?81_%oq5edLfv>mxT}iR?(QJ1knTQ%d+WqI&eJ
zi*n9;4a0a|^{n-ehJz5ETR4CwI6$e2uRv*QDDGIKa7c1XXW(Gy4M+4-HqTST(XjP}
z5k${}BoBC9!H9{lDR)>HJMVgF>n6X_V=2)_7fH+sNEM)jxAybmYXMz3-FP7J3+S$=
z;%NXAB74XB{^52(Vz7403f>EHjSre70xX)r%c3<2S$hiycQ7*
zBmv1Zs2?U3WmE$ik5G*mM&Vh&Q=|o+(7CgIWG{x>vx04!hJh)H#p}Q75bE4|ul58j
z5auVDt4!MJJivMxZSBSWd3<<1cALq)l&QUn_UmQfcrDOWDq8U5Is-}*zeVzAhfeS9
zTZ!a6mvo6oAi^p!j*&A(ZEnIz>LeDlT)LgB8SAG@n!yeGbseU672?MexCDJ8hoGdE
zolfgA<{|Yw>(X2ce{MOvE2x^KREb-5k*sk|siwn9HfC%kj8&5fgrBik!+S84=FHDW
zGCkoXfiS)`RAb=oQv)AIaEXf%e|nj|LUQehMnok;7e*(1uXzjo=o3AcKj{k0O#>Z~
z8?@GBk=lLWNizA29oX$UG8kbTEI}jn)8H`)?CQKD_ngP=*M;Z%^K`huH;~mjNas56
z#fQcK)PG^^^O=fXUdZ6l!PVmge?tNJaXe{juxnaKP^4+e5fr{bUuGn*I5j
zZY_BjZHFsOstM)PqI-97>!n67YJ=&P**}+p!MFM5_0s=Sk58hJ-HL2tpHXv#$o=}=
zZ20gou9tICb@ck)eAj*9q}t@rRva^*Svr1fVDeB7W=>rVjLAgfe|x+iOVC&yD6zn7
zDIe3gTHeDyHMux>E-1M@JAu>7U09Lk#J$;gv&;290F-XKuJ&tp*y#3hk8RJ+ZNRIS
zxA&vnvpdk3Hg_B9#dp#su3?aY`;fIl9{GDB#+G0-C?^-nL_*o`=^YEd%YWmr`sm|T
zZ}dFmmzIZh=y5BNW<$h8X>(hULt5QXsHu_D2L$BGuV#wPCn{4%Vn6}KUb
z?7O4qz?yGrYQ-_ZOIn;FKLKqW73m&XxM(GpXkjgf9+yQp&Y0T|
zpPn%RV!H{jzLlCgm_*ihoyUXN<4^-;3!GKjU{@TO;fU|3AK*uZO$!H*mJOtp-Dl4p
z%nUcAJVLn3kEjmg2wh189}$;GcrTs)aq-GilPZ|?z*WBBXpwA+oc*iC;g=H|WknDz
zPJ9EbGZq{XwGMF^rMM8_m-c0CGxIh^?i>-?D`oDuMy14`iNFHtV<6#dQNoqy{!?Xk
ze~b6}kSN;x5D6EUIdcYFez1%h+6g{rc2pP0FmcUnb`R8{L279buj^KudnmOoN~#Yv@0>oc3wAFJZ4ZOUFcf1FK(E27Iaas=tpuA0
zX;Gmc(@2kXztv;_SiZJfx=ut03vN{&L%lLc>+G3v
znUR%B0uo#WWTJeX$3!wqR7Eo$O6*T`6{Sw!bMFhtbIy2}j`r2Y=-z0ZYxzC@S7wQr`-z
zSPXO#Un}rvZE2Lxk=e%onfE8gS+ui5BC;#%E7!)8$=reGyTBg5TrJ!`OIH>k=UgY9*q-gjcKSRX%<~&`Y
z9CI(q(P2VkFjB&ZEJn#ztpm?8Hpu5y1opI^x7^}O0VtjVeM`ucu-
zR!tqTxm)*O1aA)$v|qeA5fhOS(k(m%?%FK=9(l4^bJidCpsJfYsfUKBYX82NxVY&d
zFaOr&)N0r)tmi;ZmJl)z7Lo^*IZ+c
zQOK&8V@;ap`wFY$wWr_?`
zs#F9v!%sS2C`Y>^h7vGx!H(I!p>1MfJ*zj;LHN#Gg_6+BnXS*^r$#K*Q6eS4D=a^6
z7nwRcTW}FHqoF?Kc^Gq@l!D?V4~7<%mQP-r>xP+_ND0>y*eHk;$pBRnNL9*S)J^E&xpOM_fFQrcb#~EH1faYa
zCTFr0rYjlt4$xtd9BWsFjSZQF{r%gT^h4_6w;Q<94y$b66CZt8K^-h*QoHu0C
z_h+h$;}Bgi7LQv?1)(__ArrIVK7dmF;MT9!&%HZMUt=9~1Xyms>X)LAY%1_!vQRvj
z50Q#Na-`zcv_AsLznXZuRIFcmuCz^sl>w6~+NfhYCWVOy6KqK$W8?$oHk8*`3WWg|
zG3ws;#tPFgg^#{*laj$e#-2!cm)RQeEbZs3MNq0~Idyu4u{$!Cn^vh%IV6UDP`h@mQcA+UK6$O?AOr99Sq>v(T#4(GM>7iy|d;I=}0Byn0{v6;e;;hgH5C^%k
zm@`nJ+SAC7U~Jl9R@7BrG2I%j5AOX$iHtA|ulC1r6xV>n9kZZvQMs1zz`NFuf4Frt
zeJ1~8_ppDUI}nSLJDT$;QV?7eIrNMbw)2QJifek1GjTYDEPWT09+}$`9KPy7r~FpM
z)~T|A>!Fe|?ya7WMsxwyKkTb+wS8ODYxcu#RR;R`B!1c!(hcd*I7cFW0#XY}a3850
zwLLpz$XE&wT9^5Nk@uwAYrFI#kv`b2!TcRa#at95wMlA4sC6?7O-<3^3|A~Y%xF}1
zJ+t^5Shutk69bl-1mK5;@RCItTE#KMjVCCwGEC3MANvOy8jYLypsF_QxmJamRfa>c
zr_uu$3$%q}qtYT>EFrT}-S*o&0Ngc&3X)7MYhiLL$o9q&&4Y@(hW&^Gv$RIC3U&8*
z4I`1gUvJI|6t~4YdnxQ?kO}p$uW&(0!}-4TG&t!>fiO{XuPL=~5;bwkMFV~yS(gr#
z!kxR{CPPrvTgXsd7q+Dpt4BLIJ$nQ^D|TeYT&FbSMTp21uTvAnC#x5Syr$O>-~PpOTaVMz
zRUki3yJC4uszEWl7yGQSzNd8jq2e5Z_K;INt(7nf1kNUv
zR!i&`limA-nWI;3>bU|&&gjGQui>>8qB_KicZ^v0E-Yax)P}UXt5IrdrtBiQ9Al`T
zzeA#7h1Wm%X}twh%J*Et7Sm*%*OvJryQK0w7t}9Uvx;%G-Jsb!~{4%zVl&
zKJ~+Ejbdxv^vXnax2DSwE9-e!@=Hit50Z7gia~$2Z7J7Xemd!a5xl2#?XSOx3c|LG8FJH!qoL3=){E3R!;8vI&vKa1gkXIfxmI|)
z{}%I@cFO2;<&ZzvOkpiiZ*~A2H;?z*my$J)Ei$F1$`Zgq*U~1M<Itlgxe
zlpsAW{(6(mbmqLw?=*fqn4KMLbbn5+PVfDhp4>6q&0k*90>38Pbyzotrl
zAdpXD46wmqftbbh#y_O_bTR@~4G=z-jjK`0x16=z$T2&>kw+()V?D{FSoL7;
z3vJckSV-~l_0%?|s?h0j4$&0-NbQ$eX@_phps_;;1jBZV#^^;Gd7-+B3VOgf4Av~N
z?)@@(iw?Yzs-&cZRFP$+JkO#B)te~bMbBxHBTN%9jkN=B8{Z%OTHHXY@sP3
zRA3N8VEz0k)BHx&(yzBQ3_J(D_LxHcJz{cpiVx}X{uA@bEV!l&6_rKjDVz{<>BD(X
zJSC_{P1|10tPy6KOsAe(Y_8I0gO(HLN#%@O`})AJ;EA$L+Yse^)Yp)@(0Xi;o>H%_
zs!=)dc-KPZBWuw+4-zOvgwjrIDY3R%wm6HOuLG+qwRifrgB{v4e9Sca;h~&o^ob&Z
zC6>8caqfis=-iLN4Bw#WR
zvFgkAN{$N$z%zAo%U0&;2)sD&^U1obxYo|GUr0$bMmE=WWj@Y>OlYrEo?>5UUNpEC
zSWY=n26LPRuTGg}UIrbc5gxn7xV^&n>`<9}i{aVpWjObn$*UG=gnYpsZ7^v-D0@nU
zNv%8@uwn)}(MTp|#w2viXv%F_O>YEhjBv?Hd9ImY!falEq64^JVe)8
zk+>S7=mQz33q6^ad*Hb+m}g=N%mcD!&=g3Y`w0}o+uu%
z(WE1!V@o~(hc)#v&vcb`NqRP{mX+G5+cbj(gSu;+>>7|Q=`ptGF&(?+%u*lYxZOJm
zLX&e5$zmnCs@6^VQ;o6-mXR2w34G$a$HXjd0uETu(Y0e&N
zkKr^D_>gJDvmdEESzHwSL3hOEctS}!V
zUR@e59_ZafUj}*QU-$4z7@^rB@2taCUM}Wc_n6%RQ^=2E(3vabRaNluzOtq`8;R;#
zoxlAhl(w$RqwM^i&Ha)7|H$UzPS#fM*}Oshy%#Zv`reCR(AN&ONSd#l0rPQb2%uv?
z>UM$g(9V{<561au7$t6E(wt-k(k!608i7q7oMvrVbRmSD|IX?Y*Q=sCj-pIlSueUJ
zza5W`7Y}Wsv}k%26#OLb-u$9GEZC@t10Ah8Vc
zopw6z?~W%wMT)IFulDilw{lbaBmh&ZR4=I4wqh8CAg%-}@>nYoN4PP#W#Ou&u}Kf3_WmnD<%y=P-$fhn(c0G6!FNI
z4)EGCXo=dbfjEHyW*DQIXaW38DqwrW&$zzYy-XtY!18SR
z9JK|&^`?6uu#i1ARwWGp-PRaljZiallH&2*J!H-a+@yZZ9EKP+NP!x>S;gyq#ca`+
z9O0a**n?Q4r{v~k#wxW?RLa0xNidNO4;ynwgEy~!-e>D?1n%FAZ+CbM_lH$dbvpJ_
zRo<)0_e%MTy#EAuAW^qYq@WLjlD93n`>oU4NZt|OCH26vC&+%NTP-uH2(bCggvZQ{
z>C5>L`06Hw`N!Z0h*WookOB;Emy)V^yih!w%a`XH%Eci;7u_F}
ztuN-cGeH_Myt43w5Xm>FdIdgc(^Yb$SD?zDROE1#xn~!*RNPzY2vOGMRpy5z9$QSR
zZlRt#vP`T~{p}CLVrx4}y#|Uh)k{bM2$QB_?VSMV(qd6Yh<*=m6OWlgVdga
zP6Po6O%mnyi)zg1j0TQ}H95TXQSWSdc$)3WU5++xWS_h{y0pSvgXRToOuWlv=UmyC
z&S_riyNSvI+TyJjb0Qq)Y8U-?+xkb(j4Ui7W%&^j>`U&D@PpwAn#aK77yYaD*Ud4qZxu?59=~$!V
zsywkn4!u*C)|Kc=3#LV~v<0*V>l;u-&CpSkRburzphey9&>)iW*UknyOawYZ+P1?g
z#-1yIS>)I^7CiwPpkSU}hGQ}N%+EJqD^a>|y#&FRX^)GO3>-+n`<1`fbbi87U=i75
zKp5B}%rGgC2(VD^(9**CXg|PS1wQ>y0g)aiWZD44j3LijHVYD^SWx&olEp4TjMnr$
zbO#tTXNGcr0+k{u;hopUg0bAk=Fp77jRmbt0`fh<7%;izdU>cZw40^VVj=CMPmrN8JA*O*y%`;XnzFjqE!c#vvD&y@QN(ZO0&A?38PVv2N*B)VtSTl0BVTA22uk>t~{gaXiQua$_OhLkL4(HYLA0f3ZW4UTmPg|9y%DwBWlc3NMaNvZ8awf?Lnz_+a*RL_*
zIrN4>H{)gEfKo&!l%C;3rn`&T3I#w%w)A-Mh;bzW52n-LR6pv#`Gl+2;dx0-(Jf-Y
zmKIcwteG0v-AzO4&dMV{5ph3)NPBk(doSP);3_q80&(^(z}m~jyRD6ig?1ZsO7z_r
ze`3?Egxy3LJMq7`BLsF8hwPK
zILO8I6_jpf{V=M_!`|a(?`a%$a|Dk=TV6)j7Q%+B{zqn3?Fp{qcBNv-3jN725f-tB-1Gp`~Kj
zUMlc%#u{4b?N$9^=PukY!d}IhDKqE{!Ghp&+x@(>1M-6xO|I@L88^HmC{o$ab)wS7
z^BW8zP*X^WjY;%uDU^!BN@(gRdZ?++7D9h4p@5ae2?$GrO5Uwu`mS`_z)R)vlg!ru
zGNUeBq|D)OEsV7Whk*%qMmuUHDt8bY=A|WVbM*^f6rw+wVD6D`90xTJOZJ0oaAqJ0~`rlNx5nX
za?aTLrBFMYMkg!yT~D8r2S>z7ER-yRz#p5Ucm>OO%)QJ2MI=gxIS_)4n!;2!_qHp2
z+0U5~?~Nl0>kAi1H1|1SVWHMZD1}wcUaFbH6incUz?1X-iha3gmG@)YsgWMpt-#K8
zbY8x=)ejq4D?j$}e7~S&L#yH*8KsM3ihrjU`(o#DF2ZEnHsBuhMYa2X>K)MzG1#Pc
zv8?a!5-k6H_lp02TB1L9ug*^9R*rP84*GWQHBN`hs?Dk>g4dGjhj^m-0W#cM>Co8E
z{Jvo3P<-OGW1(fiVA4=)1sT02(E!pkY_j4bF}sU=#0s;DpL6Qk
z#oI@u6|$N2v`t(lQ&O0?B(On<1)W<&=LY7BbZNJ-_DQvpls$!(Qq_udjju8a3l$mt
zX2ZggwU$VPdX6#c1670vxfPh+C
zQi4Q9k8xB{1eNb*zI^I?Jzlsv8`3Z0Icl$Mg4>E%_)O1z_NbG^ETXL?ib6za`P_0h
zCiEcJ-$^A|@ALKeLN{+6x9o1{tH99ww6gD7^Wx;a-sPzrOphC>QoaLh1(3EVS094u
zTC;i-x5pmmXtQ3Dq#3Q5epJ|gHBBV!!FO$GMMx2-`858DWOUioP$?!Yk^S0%;LTD+
zj^orrok|&;lhjDYI5Y9GHu*eH5Wu^sOjx{gIGs#>dqLJjNUnZAf1>Va^DTKLQ5si?D}p~cCmEBy5zXJ}Yf_h%PP-Ph;lWpD0d
zoR9jSJej$_Pn@r;&P`yru0%u1Vx{JYvpGpsclu%qC9S^2fFWPmxH{QR#n~5~Q*lsi
zeeC}zq<)I&`Yz!2HAw@5Nhamc?Q+a2wwD_)aFWKn7E37^GefuuzkAI(MAKtBV!acd
z*!1%4_?{$ry_sRHGjp$Eq<8uoXjY)#?{50d
z;zwE0JDYr|(bg4XPz?AiE?>SFr+>I4UzDU4xvbTxL8x>@bZY
zVM}>*$7m9rvN|>gZR@)Zb+9t0wVZkgH+mOfHTpUz2lmi|XQ5KXI6>(g`eA9Iq!seh
zmx~J&DiXT(sc%a3`Nl__{(2^f8u6w(g~nG_bDYe%l%JNcAo{{E1u01N`WGh`rc>Je
zkR7Rn)7if}YWJn}VPZx}*E4+_W6(l)+jGN4@C!r+;s{L!xj~5o8Oi+a3!N^lHgV8}
z5Jk;f!fxW*zY+1!_KnAzII60Y!^~a0Bw!jLnbL9e@~peaKmvi5AQ#geRyW9&;AP5I
zhq|Wv2a=Dx?dxAVt@QOfWSaNGnD5a4Uo(%1`MV~#h2w;(rtSL)%;%}9-YG_g}L)SZZkz7oc$e@x+KoS}@%G4kzCGY{EjTU(l#cM!aWT*ME
zKLRqBQ|@%yBNa518;Gi0CsUX<9}l=3$qK$8_k%iG%a_XB(F+|;F25hd@H%)6-KomM
zV&?8UIBfyM%2J!M%@jxut7*0HTj^6mbB2eTY_0YdbP7-xD+*YDPh*>TLh*e+ju!R6
zTd-h3`3`5^%4;u0A|}^{N2+yxTZQmlJ$tulK&x@Y2q;=+kp
zT9B$I9?8KA^@u+80>G(PR)PEqLL)e^g4YpL$Hz4p5%%l6ZEG1|oSz8{#(>@;;&9Iqtz+8tk$Nn)(|uw~3$)L&d1l1EGoN8>l$>78C~pZq>?8Vpcy7Et>G#T6oH
zJMJjuU1JBa*F7(lftgY$u)eXtOh0qYs6#4AUSL2YNsVS)5n6-wzFnLrTY3B>Ssm|M
zRirG2g7DQ$*&q=HJ~nHm6dB~JM*T@n#YYX?lYHbjIeqbOuu)1GDR~CjGPTL$v51xP
zO__p-Ln)Pnd{^8|(h}AnGPF7ZwBh5*+t-8^r{&w{TLL0lZ5ONrQZzFeDSXQ?pgBY}
zfPVBnQqke60xRn{#4?y-`LI&Jo}pnrTJZ{da!8P}^2$e8(V&P4J}&ij&JZv~pm6+r&|0G5v5ZMZ*VJ!hSx2jTlwGuF6(O0k#hyIl91_!%
zJmZt&6b>fS7YAim_v_p1bd9<%h&pXpulItQCVAxOYR#;@o)b+z{Aa^*qR5>xJxJzW
zkd2Ad!~w{ipw|VVcKB_KeCkxVNys)4pyk;P1gZG(6TPj|ISK;|PsWD&P0<1vc^`Bq
za|g`uAM>LkCF<;X<8JacU9)&~_%Ex(9CIoOjW@~aOWL
zR!iH6MGVy9Zks>XOA=EyE=-WaXlmg2c8%Lvqd#wqpYRS5C}?HcLwO<}NaOmG%gm?w
zZ`EB`02Nv6GbI{p2F|D&1WuZAk
zlLeF@fRdxEyOu!^5fV6+xUm~!c;mhKI$IH#u63M^@7$iZ0AO#86yMv*h+4m0=%Uy*
zRTjl`uCIK58}omI=G>;tvGW=b3YuQq2;Eizca7kux)Y$!tUN8^gda$Ec#W+$23R>nd4lq}oUNkY
z9y2D~F;@=5=d}*V(&AicpFlXo#_1#2_=Y6>wFNj9Yumc`GQk7htinI57MYlC3Z;XM
zlabpuK`)guNs?PIhC35dHFv$2uHX|Jb^t_>ZORU$n~qd$zA^Z97Mg
z@Y<VbqQi)r9a0YOl@&{APH%uzr@PYFgWB
zo;;75+2%RWN>oKA?MqLedX&f4W`+XD7AbxzcqgzRQh70t%DPs5cI(95h!`2I*>vvI
z0Hv}5t%X4JzDRl}BO5?vZ39CdP*aQWFTLr4bV*v>*<}&F4w-Z&)qWYKO_rUS96HYA
zL2>a}!qOM#%Af04`&EtiB
zHkn>V2v^3c?|iZm`@osr2co2nvdvEUQ8v;~jKF+s3x~bIM#c2;YX=>kBFE;0*
zw~bA6i&A(zf>9i9yL<05dU#XN1JYrH*Q|>^zsEfr@P-|whD8XWth8!9PdDsLJ>a^?
zqEB_=n)q3BaLD+S1{Mt22KP1?A8+7gYoHRv6_-FU%y-`+Dj(IXjh@+~SWY1Q*2a=a
zyHNuQ?lvfB?0hfg#tltv>~8F!(Qi3iuy7&KPBctW*QzEPYYdhQ_za(0yjJsA;|uLQ
zrEKvnIk~DEoG_$vP9{N;MZ5C&=>*+)546exN(#R
z`8;@z+kdPN9lHZ0#%Arb6jMxSr>Xv<{4I&A%|M@PG(F~RT{pqpKe3l0+GgSQ5Rg%{_!73~m-G?zs6RiGt$7ubdt!xNFJ8fcrjV$}QRkk*06
ziv%{@ftAzR0mUDH!R;$RnAHa)M$^RZqfixdx0Swb#_yKdjM+;|RFXTRl@>q+Z_v5O
z(ZrsTdM%PbFqj2dPCucI+Yf}zXQo`1jCo_&FEe`7ciNxz48H5wd~nm47K2n?{Ze;j
zVc%`PkTBU)A!XDudtX-&r7xW^qdJ!hY?*@_yfVsXZ%WZQwh;p0eqUGXImN^b*D1sY
zWji+ygFbc~qhG*GZz`Q`MmY)F@-fcRIAMJs!>0L|6oxpnu)$rtOpC4=PBGrHP2EMp
z%sYarF{&hyJt(Fn>0oqaNc=#?2uY(5~ZY+C;Hl<7<@A_o*L9L!0
zohe!*73`&O9BxnR8zcnhqqRiTCZTz0VO0{H*KTZp8q#?AoVJ~N9fPd?PP05XCBacL
zOjGtH9t_rDL{OnJAaT~s*cZNZ6xa&kr6PCgR9rh~`6_{3j921%nopdni2CtFqU2c$
zA;~BBGXcgQ;>i>kX6<#4`@ee-GS}9yMXl2y6s%#e10NTu_Z<^Cs9JjW-eJYe#v8xO-!D>7qDVxMIga+EY#(~X1@5&F_C{m!->_^edTa$!x>*0nCQ
zH%?9*qGmxiyq|VuW*|srjKLWfDl%eB$Fkiao(lHxZf$?)Q-W$o%x*lY)Cq>^lR{ps
z;LU&vb6u;HjW=|=!9#IkWmmavLGH?y$7L|;0MsDSU#%6G~4!a^OCvniE){X_R>
zT95~h9_e^b3u^yJzhnOIY2jyX5HNIl_j41nH8PfZw|4tIJt$B7Y(RCMQVv=m(jX>E
zc-B*aR3s>#bJ|Vc6F>|GYDr+nww+xlG@c&CTSkgbiybkc2xOAk=;qZ)R*Ju{M`#Kl
zG~C=wu+=g3eJ;R!_hPVm%G-#gYHkij%?tFJ66qbl8RQ@LwfuAoamKVV3x0I~8(pVb
zrEZn00X+3Ctr85I3To8cg}pD2*eBShOKU<(+ZR&OJRyzeq0RamsA~J|
z_{Bt3*1X<}oh@n-bJbO~e*)e6r#FZ8?$@)##YNM>Fa@fc*)4sxA{BMh-5H-xX3#`*
z1Mp`WdEVF^)<8!`4~_d#^?ED4O-I-lyy1rI84;`~B})9|taa}s-}~4!`hs^OhSOLP
z=u$4gyySzH)AuQd(n2K>4rIr=}7;QF+}3jByh>9XjLYz&g9
zB8?m0kH*!j@xW5*d#n;ek6WK@I6rVt_FxDn911j1?6PoRbAv?2k`sG+LaWEIWa9Bv
zLMKT-!sx$i+kvj8Ma-iJj=@p~!wHNTxk9xb^vG;m?6;bgeaW79)z?#PL}iH-)+A>`
zoq+l3V`n&xA&Q(?xY^Q+Kjxo7pMz*Qf^4#CM(O73e6+nY3zo#da7jiq)R@G{hEB*$rzZUYH-}sVsaY$oenowqtZFt+5iki!Z(Ma5gqbi}
zc50=!!u$|{{%|jbsnL&X?WUJvRv32sbioU8w00)U=tza0j+$;|UFJw5h
zkTvhAlegSuxTiSyG)*3cXQ!Xyo=`f(Am2mdL`$Htqa#lmr8+-HaA?
zca6aAgnNCNeaB599iIzg)XC6Z7K>TD*ByG63Q>J$z>}lh-(?rcs?1X
zD=6PQ8ajM7so*6mDLObyw`X+DNSQ3u<&ryCiE(4pufzdLwXcOTa~>S2Y)Kf2@RFtN
z0-kQwb&s}h(5I=4o-=g#PLlM=qHyQHo$6B2<#N^PgiptjQ$PLk)5rlCyNpf*ah9)8
zC;pBhug;#*LMOBcY34@thn;d9z4rwcAHl@q_Ec<)hopENog8kG4#q3vsH*kT
z&!d-7Mm}gjKeq%ZTmG28!wP?}N5W4;#m6WN?wTC3P4B9292_1*I!a^mAaRp2fv?D5
z+I4`^8n%&*8-e_0LfLff+s_2Bgna}X>>G$Glivg&X2LpA@vxM_a`mC<8mH7ot6*eO
z(a~*CtCP8i(a3?csXCU2Jv)%^hQN;X!62m_w`uae8w@3iq^n`F(t*x2TXgZU&>qPZ
z_YH&vFvTXPE5|$D=-eQ-aK|CU#Cj2w2>G(y^>Dt|*34`kkUT?Y2)L@{Gykz8(iB4}5AWyH`^0o?x6tddx2jB70xp
zwrCGO?v$5=f&%#+KIuL@h9B2
zT&u7M_kF_~C|wNyl#PQXB7L9ki^R7;k4s8`>?)g(Y+HX7Zn?aV%2?L&2B1
z`_pYuu+ff@?dS;4L=#sgP$v9f5%94?#0QxtIid0ExCn}G#Du}9FNSvaO(QoYNYR8C
zbUR{%Un>vMs)m=TLxtvsL)EaRdpUh88(&{eh^NEHa}sm}v1mv|PPgjm#|#CGB|`z=B$h+3)dT5xbc
zZyla*YaNe;3R0{qk!G(D%9C-lJD_q&r%F)+n`3yn_$&}pv1>-E7NZACN8ji9z2xp9
zbr|x8iPS1p#gyqaZR;{k1|En{D|M{%V1f#3Pzvfn+!IdvYRTOzJ}8oOqtyU?ER=R`
z!e@oC4k(fmv2NE9cEQthj5E4VaZC5lF)Jt>Bk@Ntuz6n#I8}g}x&^h{;yD5UIOd?A
zhPN(o4{V=v!hZ2-0^DI$7K=hH_~k^?ZnbB3s}7DW4y!*}QO8lZe$51k`lXaw}BR9Q)%egbFh5oM|qSMotCz2k%YJ5Gg|iZbEy{vp+7Ql2@@?e&sKxjkLOT3~#`_738wP2(~y8`?C>n>`K3+6%6
z6rmAdx7Zl4%U97|b@Q-WBgVI%=@lQ_EC!q{UH${~bD43**5Oo~^K7SFZ
zWihuQ4MVdoPrbjzZvG;o2FAy!;rPO3>DYu(rwJHD$=AbcTE3i{kUzJMya;}#7sjH^nHRBBU>39G(|M+)$uXG_TG
z5Dsddr5|fMkF6if8DdEh?2_7tPV|UX`%7cRZq^3%oy6cHEzZVvca~;UpZ)wc9n5{K
zA@a)uftP!Rdcn6zKwn
z&_A~&QS?QQZ||Lo%fI@DfIxh}|J_jfeQ)wt)cgGW^6!S>@AlGil7Gke)7tlc5C8z`
zeOli=C;o%c?=OH~=D5F`xBfJy`xW^BGcfR}S^!h9EeVE<}
zkN<}G-S_o(jGrEPzx!+beJMOJzhnH>PyM%J-tRC!&A5Jds`}~u@+8Fd#?+g0rZ1pSheK6i327gzh7KA>VLxdx6RF;NWY7N{&XMt75P5$82<_BUu%3XZ2!diU2x#1oanE}
z_Zfe0iT~>->3;R${v|H@C(!RRx<98Henq}dKlh(Nf8Fu_rdIGL@bA-kKSz9jMZV99
zz&`;08ueMC+D?cI~C{e
zz*-9b?|}c8Zu}?k?C-U#x&Yyj%Uy<+A
wZ2e!6|FLWJ=R5rVVf6Rg3+(Wp-r;YXSvg72_wz3R0JQf<=le5R_n)u+AKEMW#sB~S
literal 0
HcmV?d00001
diff --git a/tools/rst-optimizer/test-sample.rst b/tools/rst-optimizer/test-sample.rst
new file mode 100644
index 0000000000..1a2508c06c
--- /dev/null
+++ b/tools/rst-optimizer/test-sample.rst
@@ -0,0 +1,28 @@
+测试文档
+========
+
+这是一个测试文档。它有一些问题需要优化。
+
+功能介绍
+--------
+
+这个函数很有用。它可以做很多事情。但是描述不够清楚。
+
+.. code-block:: python
+
+ def test_function():
+ # 这是一个测试函数
+ pass
+
+参考链接
+--------
+
+参考 :ref:`other-section` 了解更多。
+
+.. note::
+ 这是一个注意事项。
+
+结论
+----
+
+总之,这个文档需要优化。
\ No newline at end of file
diff --git a/tools/rst-optimizer/tsconfig.json b/tools/rst-optimizer/tsconfig.json
new file mode 100644
index 0000000000..4fa57348c4
--- /dev/null
+++ b/tools/rst-optimizer/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "module": "commonjs",
+ "target": "ES2020",
+ "outDir": "out",
+ "lib": [
+ "ES2020"
+ ],
+ "sourceMap": true,
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true
+ },
+ "include": [
+ "src/**/*",
+ "test/**/*"
+ ],
+ "exclude": [
+ "node_modules",
+ ".vscode-test"
+ ]
+}
--
Gitee
From 7b08a1c630f909691becab06010c10c846f7ec62 Mon Sep 17 00:00:00 2001
From: xvjiawei2025 <2920904163@qq.com>
Date: Mon, 17 Nov 2025 15:37:21 +0800
Subject: [PATCH 2/2] feat: add src for rst-optimizer
---
tools/rst-optimizer/src/config.ts | 65 +++
.../src/diff/diffActionCodeLensProvider.ts | 37 ++
.../src/diff/virtualDocProvider.ts | 42 ++
tools/rst-optimizer/src/extension.ts | 452 ++++++++++++++++++
tools/rst-optimizer/src/history.ts | 58 +++
tools/rst-optimizer/src/llm/client.ts | 117 +++++
.../rst-optimizer/src/test/extension.test.ts | 15 +
tools/rst-optimizer/src/types.ts | 46 ++
tools/rst-optimizer/src/utils/file.ts | 117 +++++
tools/rst-optimizer/src/utils/wrap.ts | 112 +++++
.../src/views/batchResultsHtml.ts | 122 +++++
.../src/views/batchResultsView.ts | 92 ++++
12 files changed, 1275 insertions(+)
create mode 100644 tools/rst-optimizer/src/config.ts
create mode 100644 tools/rst-optimizer/src/diff/diffActionCodeLensProvider.ts
create mode 100644 tools/rst-optimizer/src/diff/virtualDocProvider.ts
create mode 100644 tools/rst-optimizer/src/extension.ts
create mode 100644 tools/rst-optimizer/src/history.ts
create mode 100644 tools/rst-optimizer/src/llm/client.ts
create mode 100644 tools/rst-optimizer/src/test/extension.test.ts
create mode 100644 tools/rst-optimizer/src/types.ts
create mode 100644 tools/rst-optimizer/src/utils/file.ts
create mode 100644 tools/rst-optimizer/src/utils/wrap.ts
create mode 100644 tools/rst-optimizer/src/views/batchResultsHtml.ts
create mode 100644 tools/rst-optimizer/src/views/batchResultsView.ts
diff --git a/tools/rst-optimizer/src/config.ts b/tools/rst-optimizer/src/config.ts
new file mode 100644
index 0000000000..a4d79a030e
--- /dev/null
+++ b/tools/rst-optimizer/src/config.ts
@@ -0,0 +1,65 @@
+import * as vscode from 'vscode';
+import { OptimizationConfig } from './types';
+
+export class ConfigManager {
+ private static readonly CONFIG_SECTION = 'rstOptimizer';
+
+ static getConfig(): OptimizationConfig {
+ const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION);
+
+ // 优先从环境变量读取 API Key
+ const apiKey = process.env.RST_OPTIMIZER_API_KEY || config.get('api.apiKey', '');
+
+ return {
+ provider: config.get('api.provider', 'openai-compatible'),
+ baseUrl: config.get('api.baseUrl', 'https://api.openai.com/v1'),
+ model: config.get('api.model', 'gpt-4o-mini'),
+ apiKey,
+ userPrompt: config.get('prompt.userPrompt',"你是一名 **RST 技术文档审校与修复专家**。\n你的任务是:在**不改变技术语义**的前提下,**严格检查并修复**给定 RST 文档在语法、结构、格式与中文排版上的问题,并**保持与 API 定义及文件名的一致性**。\n如无明确要求,**不要引入新的内容段落或编造默认值/异常**;仅在缺漏“必须存在且可从上下文直接确定”的字段时做最小增补。\n\n## 输入\n\n* 文件名:${fileName}\n* 原始 RST 文本(保持原样)\n\n## 总体原则\n\n1. **最小侵入修复**:仅修复错误或不规范之处;保留原有信息结构与术语。\n2. **禁止误改代码/公式**:`code-block`、`literalinclude`、`math` 等指令体及反引号行内代码的技术内容不得改写(仅可做空格与围栏修复)。\n3. **一致性优先**:文件名、章节标题、API 名称必须一致(见规则 2)。\n4. **中文排版**:面向中文读者的正文需优化中文标点与用语,但不得改变技术意义。\n5. **不可臆测**:若无默认值/异常/类型信息,不得凭空添加。无法确定时保持空或原状,并在总结里标注“需要人工确认”。\n\n## 必查必修规则(逐条执行)\n\n### 1)通用 RST 语法与特殊标记\n\n* 检查并修复常见指令语法:`.. note::`、`.. warning::`、`.. seealso::`、`.. include::`、`.. math::` 等的缩进与空行:\n * 指令与其内容之间需空一行(除确有嵌套要求的场景外)。\n * 指令体内容相对指令行统一缩进(建议 3–4 空格),全文保持一致。\n* 行内特殊标记规则:\n * `*斜体*`、`**加粗**`、``行内代码``:标记内部不得出现空格(确需空格时改为转义或拆分)。\n * 标记与上下文:标记前后各保留 1 个空格(句首或紧邻标点处除外)。\n* 修复多余反引号、未闭合标记、错误嵌套。\n\n### 2)文件名 / 章节标题 / API 定义名一致性\n\n* 要求:文件名、文档首个最高层级标题、正文首个 API 定义名应一致。\n* 例外:若文件名匹配 `mindspore.xxx.func_yyy.rst`,则章节标题与 API 定义名统一为 `mindspore.xxx.yyy`(去掉 `func_`)。\n* 实施:\n * 若三者不一致,以 API 定义名为准统一章节标题;遇到上述例外时按例外规则处理。\n * 若文件名与 API 命名空间明显冲突(如包路径不同),不改文件名,仅统一标题与文内 API 显示。\n\n### 3)“参数/关键字参数”模块格式(**包含关键字专用参数的严格规则**)\n\n* 目标格式(逐项以无序列表 `-` 起):\n\n```\n参数:\n- **参数名** (数据类型[, 可选]) – 参数说明。默认值:`None`,表示……\n 关键字参数:\n- **参数名** (数据类型[, 可选]) – 参数说明。默认值:`None`,表示……\n```\n\n* 严格对齐 Python 函数签名的五类参数并分流到正确模块:\n\n1) **仅位置参数(positional-only)**:位于 `/` 左侧(若签名包含 `/`)。归入“参数”模块,**不要展示 `/` 本身**。\n2) **位置或关键字参数(positional-or-keyword)**:常见的 `name` 或 `name=...`。归入“参数”模块。\n3) **可变位置参数**:`*args`。归入“参数”模块,名称保留星号前缀 `*`,类型与说明按项目规范书写。\n4) **关键字专用参数(keyword-only)**:当签名中出现 **裸 `*` 分隔符** 或 `*args` 之后的形参(直到 `**kwargs` 之前)。**这些形参必须归入“关键字参数”模块**。\n - 裸 `*` 仅是分隔符,**不出现在文档中**;其右侧的参数(如 `dtype=None`)统一移至“关键字参数”。\n5) **可变关键字参数**:`**kwargs`。根据项目约定决定是否在“关键字参数”模块列出;若列出需保留 `**` 前缀并简要说明用途,避免臆测具体键。\n\n* “参数/关键字参数”的**名称与顺序必须与函数定义完全一致**(先左后右、从签名原序列化得到)。\n* “可选/默认值”标注规则:\n* 形参**有默认值**(含 `=None`)或注解为可选类型时:在类型后补 `, 可选`,且在说明末尾追加“默认值:````,……”。\n* 形参**无默认值**:不写“可选”,不写默认值句。\n* **数据类型**需与定义一致;若原文缺失且上下文也无法确定,不要臆测,类型可暂缺省或保持原状。\n* 术语与标点:\n* 中文破折号统一使用 `–`(en dash),全文保持一致。\n* 默认值文字中的字面量使用行内代码围栏(如 ``None``、``True``、``-1``)。\n* **迁移示例(与你给的 case 对齐)**:签名 `(..., dim=None, *, dtype=None)` →\n `dim` 仍在“参数”;`dtype` 作为 **关键字专用参数** 移至“关键字参数”。\n\n### 4)“异常”模块格式\n\n* 目标格式:\n\n```\n异常:\n- **ErrorType** - 异常描述。\n```\n\n* 同类异常归并在一起,子类在前、父类在后;不得杜撰异常,无法确认时保留原状并在总结中标注需确认。\n\n### 5)“输入/输出”模块格式\n\n* 目标格式:\n\n```\n输入:\n- **输入名** (数据类型) – 描述。\n 输出:\n- **输出名** (数据类型) – 描述。\n```\n\n* 无序列表 `-`,加粗名称 + 圆括号数据类型 + `–` 描述。\n\n### 6)换行与缩进\n\n* 普通段落换行不缩进。\n* 有序/无序列表换行统一 2 个空格缩进,与上一行正文起始位置对齐。\n* 指令体(note/warning/seealso/include/math 等)内层统一缩进;子块(如代码)再按 RST 规范缩进一级。\n\n### 7)模块间空行\n\n* 各模块之间至少 1 个空行;指令与其前后段落之间保留空行,避免黏连。\n\n### 8)中文文本与排版\n\n* 修复错别字、冗余空格、英文标点混用(中文语句中使用中文标点:`,` `。` `:` `;` `( )`)。\n* 并列关系用顿号 `、` 分隔。\n* API 名、类名、参数名、代码标识符保留原文并用行内代码``包裹。\n* 句子更通顺简洁,但不得改变技术含义。\n\n## 额外一致性检查\n\n* 标题层级符号(`= - ~ ^ \" ' *` 等)长度需与标题文本长度一致;同级标题符号统一。\n* `:param:/:type:/:return:/:rtype:` 若与目标“列表式参数节”并存,应二选一统一为项目约定风格(通常统一为“参数/关键字参数”块)。\n* 交叉引用(`:class:`, `:func:`, `:mod:` 等)语法修复:角色名、目标、反引号与空格。\n* `include` 路径前后空格与相对路径的一致性。\n\n## 输出要求\n\n* 输出修复后的完整 RST 正文(不加多余说明或包裹)。\n* 如需 diff 模式,由外层系统控制;本指令默认仅产出修复后全文。\n\n## 审核自查清单(模型内部执行,无需输出)\n\n* [ ] 指令语法/缩进/空行正确\n* [ ] 行内标记无空格、边界空格正确\n* [ ] 文件名/标题/API 名一致(含 `func_` 例外)\n* [ ] 参数/关键字参数:顺序与分流严格按签名(含 `/`、裸 `*`、`*args`、`**kwargs`)\n* [ ] “可选/默认值”标注与默认值文字格式正确\n* [ ] 异常:格式统一、同类聚合、无臆测\n* [ ] 输入/输出:列表格式统一\n* [ ] 列表缩进 2 空格,段落不缩进\n* [ ] 模块间有空行(例如返回是一个模块,异常是一个模块,他们之间有空行,但是异常下面的具体异常和异常大标题之间无空行)\n* [ ] 中文标点与顿号、错别字修复\n* [ ] 代码/公式内容未被改写(仅围栏/空格修复)"),
+ maxTokens: config.get('generation.maxTokens', 4096),
+ temperature: config.get('generation.temperature', 0.3),
+ neverUploadIfWorkspaceTrusted: config.get('safety.neverUploadIfWorkspaceTrusted', false),
+ rewrapWidth: config.get('format.rewrapWidth', 0)
+ };
+ }
+
+ static validateConfig(config: OptimizationConfig): string[] {
+ const errors: string[] = [];
+
+ if (!config.baseUrl) {
+ errors.push('API 基础 URL 不能为空');
+ }
+
+ if (!config.model) {
+ errors.push('模型名称不能为空');
+ }
+
+ if (!config.apiKey) {
+ errors.push('API 密钥不能为空,请在设置中配置或设置环境变量 RST_OPTIMIZER_API_KEY');
+ }
+
+ if (config.maxTokens <= 0) {
+ errors.push('最大 token 数量必须大于 0');
+ }
+
+ if (config.temperature < 0 || config.temperature > 2) {
+ errors.push('温度值必须在 0-2 之间');
+ }
+
+ return errors;
+ }
+
+ static checkWorkspaceSafety(): boolean {
+ const workspaceTrust = vscode.workspace.isTrusted;
+ const config = this.getConfig();
+
+ if (config.neverUploadIfWorkspaceTrusted && workspaceTrust) {
+ vscode.window.showWarningMessage(
+ '安全设置阻止了在受信任工作区中上传内容到外部 API。请在设置中关闭 "neverUploadIfWorkspaceTrusted" 选项。'
+ );
+ return false;
+ }
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/tools/rst-optimizer/src/diff/diffActionCodeLensProvider.ts b/tools/rst-optimizer/src/diff/diffActionCodeLensProvider.ts
new file mode 100644
index 0000000000..0021d9b60e
--- /dev/null
+++ b/tools/rst-optimizer/src/diff/diffActionCodeLensProvider.ts
@@ -0,0 +1,37 @@
+import * as vscode from 'vscode';
+import { VirtualDocProvider } from './virtualDocProvider';
+
+
+export class DiffActionCodeLensProvider implements vscode.CodeLensProvider {
+ private onDidChangeCodeLensesEmitter = new vscode.EventEmitter();
+ public readonly onDidChangeCodeLenses: vscode.Event = this.onDidChangeCodeLensesEmitter.event;
+
+ provideCodeLenses(document: vscode.TextDocument): vscode.ProviderResult {
+ if (document.uri.scheme !== VirtualDocProvider.getScheme()) {
+ return [];
+ }
+ if (!document.uri.path.startsWith('/optimized/')) {
+ return [];
+ }
+
+ const lastLine = Math.max(0, document.lineCount - 1);
+ const range = new vscode.Range(lastLine, 0, lastLine, 0);
+
+ const applyLens = new vscode.CodeLens(range, {
+ title: '$(check) 应用优化',
+ command: 'rstOptimizer.applyResult'
+ });
+
+ const discardLens = new vscode.CodeLens(range, {
+ title: '$(x) 放弃优化',
+ command: 'rstOptimizer.discardResult'
+ });
+
+ return [applyLens, discardLens];
+ }
+
+ refresh(): void {
+ this.onDidChangeCodeLensesEmitter.fire();
+ }
+}
+
diff --git a/tools/rst-optimizer/src/diff/virtualDocProvider.ts b/tools/rst-optimizer/src/diff/virtualDocProvider.ts
new file mode 100644
index 0000000000..8fe3859564
--- /dev/null
+++ b/tools/rst-optimizer/src/diff/virtualDocProvider.ts
@@ -0,0 +1,42 @@
+import * as vscode from 'vscode';
+
+export class VirtualDocProvider implements vscode.TextDocumentContentProvider {
+ private static readonly scheme = 'rstopt';
+ private contentMap = new Map();
+ private _onDidChange = new vscode.EventEmitter();
+
+ readonly onDidChange = this._onDidChange.event;
+
+ static createUri(type: 'original' | 'optimized', filePath: string): vscode.Uri {
+ const timestamp = Date.now();
+ return vscode.Uri.parse(`${VirtualDocProvider.scheme}:${type}/${encodeURIComponent(filePath)}?t=${timestamp}`);
+ }
+
+ provideTextDocumentContent(uri: vscode.Uri): string | undefined {
+ const key = this.getKeyFromUri(uri);
+ return this.contentMap.get(key);
+ }
+
+ set(uri: vscode.Uri, content: string): void {
+ const key = this.getKeyFromUri(uri);
+ this.contentMap.set(key, content);
+ this._onDidChange.fire(uri);
+ }
+
+ clear(uri: vscode.Uri): void {
+ const key = this.getKeyFromUri(uri);
+ this.contentMap.delete(key);
+ }
+
+ clearAll(): void {
+ this.contentMap.clear();
+ }
+
+ private getKeyFromUri(uri: vscode.Uri): string {
+ return `${uri.path}${uri.query}`;
+ }
+
+ static getScheme(): string {
+ return VirtualDocProvider.scheme;
+ }
+}
\ No newline at end of file
diff --git a/tools/rst-optimizer/src/extension.ts b/tools/rst-optimizer/src/extension.ts
new file mode 100644
index 0000000000..1e71f29b7c
--- /dev/null
+++ b/tools/rst-optimizer/src/extension.ts
@@ -0,0 +1,452 @@
+import * as vscode from 'vscode';
+import { ConfigManager } from './config';
+import { LLMClient } from './llm/client';
+import { VirtualDocProvider } from './diff/virtualDocProvider';
+import { DiffActionCodeLensProvider } from './diff/diffActionCodeLensProvider';
+import { BatchResultsViewProvider } from './views/batchResultsView';
+import type { BatchResultItem } from './views/batchResultsHtml';
+import { FileUtils } from './utils/file';
+import { TextWrapper } from './utils/wrap';
+import { DiffContext } from './types';
+import { HistoryStore } from './history';
+
+let outputChannel: vscode.OutputChannel;
+let llmClient: LLMClient;
+let virtualDocProvider: VirtualDocProvider;
+let statusBarItem: vscode.StatusBarItem;
+let currentDiffContext: DiffContext | undefined;
+let batchResultsViewProvider: BatchResultsViewProvider;
+
+export function activate(context: vscode.ExtensionContext) {
+ outputChannel = vscode.window.createOutputChannel('RST Optimizer');
+ context.subscriptions.push(outputChannel);
+
+ llmClient = new LLMClient(outputChannel);
+
+ HistoryStore.init(context);
+
+ virtualDocProvider = new VirtualDocProvider();
+ context.subscriptions.push(
+ vscode.workspace.registerTextDocumentContentProvider(
+ VirtualDocProvider.getScheme(),
+ virtualDocProvider
+ )
+ );
+
+ const codeLensProvider = new DiffActionCodeLensProvider();
+ context.subscriptions.push(
+ vscode.languages.registerCodeLensProvider(
+ { scheme: VirtualDocProvider.getScheme() },
+ codeLensProvider
+ )
+ );
+
+ batchResultsViewProvider = new BatchResultsViewProvider({
+ onApplyAll: async (results) => applyAllBatchResults(results),
+ onApplySelected: async (results, selected) => applySelectedBatchResults(results, selected),
+ onViewDiffById: async (id) => {
+ const r = HistoryStore.getById(id);
+ if (r) {
+ await showDiffView(r.filePath, r.originalText, r.optimizedText);
+ }
+ },
+ onDiscard: async () => {/* no-op, close action handled by view */}
+ });
+ context.subscriptions.push(
+ vscode.window.registerWebviewViewProvider(BatchResultsViewProvider.ViewId, batchResultsViewProvider)
+ );
+ // 命令:确保可以显式聚焦视图
+ context.subscriptions.push(
+ vscode.commands.registerCommand('rstOptimizer.revealBatchResultsView', () => batchResultsViewProvider.reveal())
+ );
+
+ // 创建状态栏项
+ statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100);
+ statusBarItem.text = '$(edit) RST Optimizer';
+ statusBarItem.tooltip = '点击打开 RST Optimizer 命令';
+ statusBarItem.command = 'rstOptimizer.showQuickPick';
+ statusBarItem.show();
+ context.subscriptions.push(statusBarItem);
+
+ // 注册命令
+ const commands = [
+ vscode.commands.registerCommand('rstOptimizer.optimizeCurrent', optimizeCurrentFile),
+ vscode.commands.registerCommand('rstOptimizer.optimizePickFile', optimizePickedFile),
+ vscode.commands.registerCommand('rstOptimizer.applyResult', applyOptimizedResult),
+ vscode.commands.registerCommand('rstOptimizer.discardResult', discardOptimizedResult),
+ vscode.commands.registerCommand('rstOptimizer.openSettings', openSettings),
+ vscode.commands.registerCommand('rstOptimizer.showQuickPick', showQuickPick)
+ ];
+
+ context.subscriptions.push(...commands);
+
+ outputChannel.appendLine('RST Optimizer 扩展已激活');
+}
+
+export function deactivate() {
+ if (virtualDocProvider) {
+ virtualDocProvider.clearAll();
+ }
+ outputChannel?.appendLine('RST Optimizer 扩展已停用');
+}
+
+async function optimizeCurrentFile() {
+ const filePath = FileUtils.getCurrentRstFile();
+ if (!filePath) {
+ vscode.window.showWarningMessage('当前编辑器不是 RST 文件,请打开一个 .rst 文件');
+ return;
+ }
+
+ await optimizeFile(filePath);
+}
+
+async function optimizePickedFile() {
+ const filePaths = await FileUtils.pickRstFiles();
+ if (filePaths.length === 0) {
+ return; // 用户取消了选择
+ }
+
+ // 打开选中的文件
+ for (const filePath of filePaths) {
+ const document = await vscode.workspace.openTextDocument(filePath);
+ await vscode.window.showTextDocument(document, { preview: false });
+ }
+
+ if (filePaths.length === 1) {
+ // 单文件优化
+ await optimizeFile(filePaths[0]);
+ } else {
+ // 批量优化
+ await optimizeMultipleFiles(filePaths);
+ }
+}
+
+async function optimizeMultipleFiles(filePaths: string[]) {
+ try {
+ // 检查工作区安全设置
+ if (!ConfigManager.checkWorkspaceSafety()) {
+ return;
+ }
+
+ // 获取配置
+ const config = ConfigManager.getConfig();
+ const configErrors = ConfigManager.validateConfig(config);
+ if (configErrors.length > 0) {
+ vscode.window.showErrorMessage(`配置错误:${configErrors.join(', ')}`);
+ return;
+ }
+
+ const results: { filePath: string; originalText: string; optimizedText: string }[] = [];
+
+ // 显示总体进度
+ await vscode.window.withProgress(
+ {
+ location: vscode.ProgressLocation.Notification,
+ title: `正在批量优化 ${filePaths.length} 个 RST 文件...`,
+ cancellable: true
+ },
+ async (progress, token) => {
+ for (let i = 0; i < filePaths.length; i++) {
+ const filePath = filePaths[i];
+ const fileName = FileUtils.getFileName(filePath);
+
+ if (token.isCancellationRequested) {
+ break;
+ }
+
+ progress.report({
+ message: `正在优化 ${fileName} (${i + 1}/${filePaths.length})`,
+ increment: (100 / filePaths.length)
+ });
+
+ try {
+ const originalText = await FileUtils.readFile(filePath);
+ if (!originalText.trim()) {
+ outputChannel.appendLine(`跳过空文件: ${filePath}`);
+ continue;
+ }
+
+ const dotIdx = fileName.lastIndexOf('.');
+ const fileBaseName = dotIdx > 0 ? fileName.slice(0, dotIdx) : fileName;
+ const fileExt = dotIdx > -1 ? fileName.slice(dotIdx + 1) : '';
+ const optimizedText = await llmClient.optimizeRst(
+ {
+ text: originalText,
+ userPrompt: config.userPrompt,
+ config,
+ variables: {
+ filePath,
+ fileName,
+ relativePath: FileUtils.getRelativePath(filePath),
+ fileBaseName,
+ fileExt
+ }
+ },
+ token
+ );
+
+ const finalOptimizedText = config.rewrapWidth > 0
+ ? TextWrapper.wrapText(optimizedText, config.rewrapWidth)
+ : optimizedText;
+
+ results.push({
+ filePath,
+ originalText,
+ optimizedText: finalOptimizedText
+ });
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ outputChannel.appendLine(`优化失败 ${filePath}: ${errorMessage}`);
+ vscode.window.showWarningMessage(`优化 ${fileName} 失败: ${errorMessage}`);
+ }
+ }
+ }
+ );
+
+ if (results.length > 0) {
+ await showBatchOptimizationResults(results);
+ } else {
+ vscode.window.showWarningMessage('没有成功优化任何文件');
+ }
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ vscode.window.showErrorMessage(`批量优化失败: ${errorMessage}`);
+ outputChannel.appendLine(`批量优化失败: ${errorMessage}`);
+ }
+}
+
+async function optimizeFile(filePath: string) {
+ try {
+ // 检查工作区安全设置
+ if (!ConfigManager.checkWorkspaceSafety()) {
+ return;
+ }
+
+ // 获取配置
+ const config = ConfigManager.getConfig();
+ const configErrors = ConfigManager.validateConfig(config);
+ if (configErrors.length > 0) {
+ vscode.window.showErrorMessage(`配置错误:${configErrors.join(', ')}`);
+ return;
+ }
+
+ // 读取文件内容
+ const originalText = await FileUtils.readFile(filePath);
+ if (!originalText.trim()) {
+ vscode.window.showWarningMessage('文件内容为空');
+ return;
+ }
+
+ // 显示进度并执行优化
+ const optimizedText = await vscode.window.withProgress(
+ {
+ location: vscode.ProgressLocation.Notification,
+ title: `正在使用 ${config.provider}/${config.model} 优化 RST 文档...`,
+ cancellable: true
+ },
+ async (progress, token) => {
+ const fileName = FileUtils.getFileName(filePath);
+ const relativePath = FileUtils.getRelativePath(filePath);
+ const dotIdx = fileName.lastIndexOf('.');
+ const fileBaseName = dotIdx > 0 ? fileName.slice(0, dotIdx) : fileName;
+ const fileExt = dotIdx > -1 ? fileName.slice(dotIdx + 1) : '';
+ return await llmClient.optimizeRst(
+ {
+ text: originalText,
+ userPrompt: config.userPrompt,
+ config,
+ variables: { filePath, fileName, relativePath, fileBaseName, fileExt }
+ },
+ token
+ );
+ }
+ );
+
+ // 应用文本包装(如果配置了)
+ const finalOptimizedText = config.rewrapWidth > 0
+ ? TextWrapper.wrapText(optimizedText, config.rewrapWidth)
+ : optimizedText;
+
+ // 创建 diff 视图
+ await showDiffView(filePath, originalText, finalOptimizedText);
+
+ // 记录到历史并刷新侧边栏
+ await batchResultsViewProvider.showResults([{ filePath, originalText, optimizedText: finalOptimizedText }]);
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ vscode.window.showErrorMessage(`优化失败: ${errorMessage}`);
+ outputChannel.appendLine(`优化失败: ${errorMessage}`);
+ }
+}
+
+async function showDiffView(filePath: string, originalText: string, optimizedText: string) {
+ // 创建虚拟 URI
+ const originalUri = VirtualDocProvider.createUri('original', filePath);
+ const optimizedUri = VirtualDocProvider.createUri('optimized', filePath);
+
+ // 设置虚拟文档内容
+ virtualDocProvider.set(originalUri, originalText);
+ virtualDocProvider.set(optimizedUri, optimizedText);
+
+ // 保存当前 diff 上下文
+ currentDiffContext = {
+ originalUri,
+ optimizedUri,
+ filePath,
+ optimizedContent: optimizedText
+ };
+
+ // 打开 diff 视图
+ const fileName = FileUtils.getFileName(filePath);
+ const title = `RST Diff: ${fileName}`;
+
+ await vscode.commands.executeCommand(
+ 'vscode.diff',
+ originalUri,
+ optimizedUri,
+ title,
+ { preview: false }
+ );
+
+ // 设置上下文,确保菜单按钮在 diff 标题栏显示
+ await vscode.commands.executeCommand('setContext', 'rstOptimizer.hasActiveDiff', true);
+}
+
+async function showBatchOptimizationResults(results: { filePath: string; originalText: string; optimizedText: string }[]) {
+ await batchResultsViewProvider.showResults(results);
+}
+
+
+async function applyOptimizedResult() {
+ if (!currentDiffContext) {
+ vscode.window.showWarningMessage('没有可应用的优化结果');
+ return;
+ }
+
+ try {
+ await FileUtils.writeFile(currentDiffContext.filePath, currentDiffContext.optimizedContent);
+
+ const fileName = FileUtils.getFileName(currentDiffContext.filePath);
+ vscode.window.showInformationMessage(`已成功应用优化结果到 ${fileName}`);
+
+ outputChannel.appendLine(`[${new Date().toISOString()}] 已应用优化结果: ${currentDiffContext.filePath}`);
+
+ // 清理
+ await discardChanges();
+
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ vscode.window.showErrorMessage(`应用更改失败: ${errorMessage}`);
+ }
+}
+
+async function discardOptimizedResult() {
+ if (!currentDiffContext) {
+ vscode.window.showWarningMessage('没有可放弃的优化结果');
+ return;
+ }
+
+ const fileName = FileUtils.getFileName(currentDiffContext.filePath);
+ vscode.window.showInformationMessage(`已放弃对 ${fileName} 的优化结果`);
+
+ outputChannel.appendLine(`[${new Date().toISOString()}] 已放弃优化结果: ${currentDiffContext.filePath}`);
+
+ // 清理
+ await discardChanges();
+}
+
+async function discardChanges() {
+ if (currentDiffContext) {
+ // 清理虚拟文档
+ virtualDocProvider.clear(currentDiffContext.originalUri);
+ virtualDocProvider.clear(currentDiffContext.optimizedUri);
+ currentDiffContext = undefined;
+ }
+ // 清理上下文
+ await vscode.commands.executeCommand('setContext', 'rstOptimizer.hasActiveDiff', false);
+}
+
+async function applyAllBatchResults(results: { filePath: string; originalText: string; optimizedText: string }[]) {
+ let successCount = 0;
+ let failCount = 0;
+
+ for (const result of results) {
+ try {
+ await FileUtils.writeFile(result.filePath, result.optimizedText);
+ successCount++;
+ outputChannel.appendLine(`[${new Date().toISOString()}] 已应用优化结果: ${result.filePath}`);
+ } catch (error) {
+ failCount++;
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ outputChannel.appendLine(`[${new Date().toISOString()}] 应用失败: ${result.filePath} - ${errorMessage}`);
+ }
+ }
+
+ vscode.window.showInformationMessage(
+ `批量应用完成!成功: ${successCount} 个,失败: ${failCount} 个`
+ );
+}
+
+async function applySelectedBatchResults(
+ results: { filePath: string; originalText: string; optimizedText: string }[],
+ selectedFiles: string[]
+) {
+ let successCount = 0;
+ let failCount = 0;
+
+ for (const filePath of selectedFiles) {
+ const result = results.find(r => r.filePath === filePath);
+ if (!result) continue;
+
+ try {
+ await FileUtils.writeFile(result.filePath, result.optimizedText);
+ successCount++;
+ outputChannel.appendLine(`[${new Date().toISOString()}] 已应用优化结果: ${result.filePath}`);
+ } catch (error) {
+ failCount++;
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ outputChannel.appendLine(`[${new Date().toISOString()}] 应用失败: ${result.filePath} - ${errorMessage}`);
+ }
+ }
+
+ vscode.window.showInformationMessage(
+ `选择性应用完成!成功: ${successCount} 个,失败: ${failCount} 个`
+ );
+}
+
+// batch 结果的 HTML 已移动到 src/views/batchResultsHtml.ts
+
+async function openSettings() {
+ await vscode.commands.executeCommand('workbench.action.openSettings', 'rstOptimizer');
+}
+
+async function showQuickPick() {
+ const items = [
+ {
+ label: '$(file-text) 优化当前 RST 文件',
+ description: '优化当前活动编辑器中的 RST 文件',
+ command: 'rstOptimizer.optimizeCurrent'
+ },
+ {
+ label: '$(folder-opened) 选择 RST 文件优化',
+ description: '通过文件选择器选择要优化的 RST 文件',
+ command: 'rstOptimizer.optimizePickFile'
+ },
+ {
+ label: '$(settings-gear) 打开设置',
+ description: '配置 RST Optimizer 设置',
+ command: 'rstOptimizer.openSettings'
+ }
+ ];
+
+ const selected = await vscode.window.showQuickPick(items, {
+ placeHolder: '选择要执行的操作'
+ });
+
+ if (selected) {
+ await vscode.commands.executeCommand(selected.command);
+ }
+}
diff --git a/tools/rst-optimizer/src/history.ts b/tools/rst-optimizer/src/history.ts
new file mode 100644
index 0000000000..5b007aae98
--- /dev/null
+++ b/tools/rst-optimizer/src/history.ts
@@ -0,0 +1,58 @@
+import * as vscode from 'vscode';
+import type { BatchResultItem } from './views/batchResultsHtml';
+
+
+export class HistoryStore {
+ private static context: vscode.ExtensionContext | undefined;
+ private static readonly KEY = 'rstOptimizer.history';
+
+ static init(context: vscode.ExtensionContext) {
+ this.context = context;
+ }
+
+ private static ensureReady() {
+ if (!this.context) {
+ throw new Error('HistoryStore not initialized');
+ }
+ }
+
+ static getAll(): BatchResultItem[] {
+ this.ensureReady();
+ const items = this.context!.globalState.get(this.KEY, []);
+ return Array.isArray(items) ? items.slice().sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0)) : [];
+ }
+
+ static addMany(items: Omit[]): BatchResultItem[] {
+ this.ensureReady();
+ const existing = this.getAll();
+ const now = Date.now();
+ const withIds: BatchResultItem[] = items.map((it, idx) => ({
+ ...it,
+ id: `${now}-${idx}-${Math.random().toString(36).slice(2, 8)}`,
+ timestamp: now + idx,
+ }));
+ const next = [...withIds, ...existing];
+ this.context!.globalState.update(this.KEY, next);
+ return withIds;
+ }
+
+ static addOne(item: Omit): BatchResultItem {
+ return this.addMany([item])[0];
+ }
+
+ static removeById(id: string): void {
+ this.ensureReady();
+ const next = this.getAll().filter(it => it.id !== id);
+ this.context!.globalState.update(this.KEY, next);
+ }
+
+ static getById(id: string): BatchResultItem | undefined {
+ return this.getAll().find(it => it.id === id);
+ }
+
+ static clearAll(): void {
+ this.ensureReady();
+ this.context!.globalState.update(this.KEY, []);
+ }
+}
+
diff --git a/tools/rst-optimizer/src/llm/client.ts b/tools/rst-optimizer/src/llm/client.ts
new file mode 100644
index 0000000000..7985b498aa
--- /dev/null
+++ b/tools/rst-optimizer/src/llm/client.ts
@@ -0,0 +1,117 @@
+import * as vscode from 'vscode';
+import { OptimizationRequest, LLMResponse } from '../types';
+
+const DEFAULT_SYSTEM_PROMPT = `你是 reStructuredText(RST)与 Sphinx 文档优化专家。目标:在不破坏语义与构建的前提下,让文档更清晰专业。
+严格遵循:
+1) 保留并尊重所有 Sphinx 角色/指令/域(如 :ref:、:class:、:func:、.. code-block::、.. note::、.. figure:: 等),不要更改其语法结构与缩进。
+2) 绝不删除或更改链接目标、交叉引用锚点(如 .. _anchor:)。
+3) 保留代码块、控制台示例与行内字面值(\`\`literal\`\`)原样;除非是修复明显拼写错误。
+4) 标题层级与下划线风格统一(如 # * = - ^ ~ 等),但不得改变层级关系。
+5) 修复语法/拼写/术语一致性,使段落更简洁、技术准确;尽量减少被动语态。
+6) 表格、列表、缩进必须合法;指令体的缩进四空格对齐。
+7) 输出**完整优化后的整篇 RST**,不要输出 diff 或注释。`;
+
+export class LLMClient {
+ private outputChannel: vscode.OutputChannel;
+
+ constructor(outputChannel: vscode.OutputChannel) {
+ this.outputChannel = outputChannel;
+ }
+
+ async optimizeRst(
+ request: OptimizationRequest,
+ cancellationToken?: vscode.CancellationToken
+ ): Promise {
+ const startTime = Date.now();
+ const { text, userPrompt, config } = request;
+
+ this.outputChannel.appendLine(`[${new Date().toISOString()}] 开始优化 RST 文档`);
+ this.outputChannel.appendLine(`提供商: ${config.provider}`);
+ this.outputChannel.appendLine(`模型: ${config.model}`);
+ this.outputChannel.appendLine(`文档长度: ${text.length} 字符`);
+ this.outputChannel.appendLine(`估算 token: ${Math.ceil(text.length / 4)}`);
+
+ try {
+ const userContent = this.composeUserContent(text, userPrompt, request.variables);
+
+ const requestBody = {
+ model: config.model,
+ temperature: config.temperature,
+ max_tokens: config.maxTokens,
+ messages: [
+ { role: "system", content: DEFAULT_SYSTEM_PROMPT },
+ { role: "user", content: userContent }
+ ]
+ };
+
+ const controller = new AbortController();
+
+ // 处理取消令牌
+ if (cancellationToken) {
+ cancellationToken.onCancellationRequested(() => {
+ controller.abort();
+ this.outputChannel.appendLine(`[${new Date().toISOString()}] 请求被用户取消`);
+ });
+ }
+
+ const response = await fetch(`${config.baseUrl}/chat/completions`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${config.apiKey}`
+ },
+ body: JSON.stringify(requestBody),
+ signal: controller.signal
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
+ }
+
+ const result = await response.json() as LLMResponse;
+
+ if (!result.choices || result.choices.length === 0) {
+ throw new Error('API 返回了空的选择列表');
+ }
+
+ const optimizedText = result.choices[0].message.content;
+ const endTime = Date.now();
+ const duration = endTime - startTime;
+
+ this.outputChannel.appendLine(`[${new Date().toISOString()}] 优化完成`);
+ this.outputChannel.appendLine(`耗时: ${duration}ms`);
+
+ if (result.usage) {
+ this.outputChannel.appendLine(`Token 使用: ${result.usage.prompt_tokens} + ${result.usage.completion_tokens} = ${result.usage.total_tokens}`);
+ }
+
+ return optimizedText;
+
+ } catch (error) {
+ const endTime = Date.now();
+ const duration = endTime - startTime;
+
+ this.outputChannel.appendLine(`[${new Date().toISOString()}] 优化失败 (${duration}ms)`);
+
+ if (error instanceof Error) {
+ this.outputChannel.appendLine(`错误: ${error.message}`);
+ if (error.stack) {
+ this.outputChannel.appendLine(`堆栈: ${error.stack}`);
+ }
+ } else {
+ this.outputChannel.appendLine(`未知错误: ${String(error)}`);
+ }
+
+ throw error;
+ }
+ }
+
+ private composeUserContent(text: string, userPrompt: string, variables?: Record): string {
+ const replaced = variables
+ ? userPrompt.replace(/\$\{(\w+)\}/g, (_m, k) => (variables[k] ?? `
+${'${'}${k}}`))
+ : userPrompt;
+ return `${replaced}\n\n以下是需要优化的 RST 文档内容:\n\n${text}`;
+ }
+}
diff --git a/tools/rst-optimizer/src/test/extension.test.ts b/tools/rst-optimizer/src/test/extension.test.ts
new file mode 100644
index 0000000000..4ca0ab4198
--- /dev/null
+++ b/tools/rst-optimizer/src/test/extension.test.ts
@@ -0,0 +1,15 @@
+import * as assert from 'assert';
+
+// You can import and use all API from the 'vscode' module
+// as well as import your extension to test it
+import * as vscode from 'vscode';
+// import * as myExtension from '../../extension';
+
+suite('Extension Test Suite', () => {
+ vscode.window.showInformationMessage('Start all tests.');
+
+ test('Sample test', () => {
+ assert.strictEqual(-1, [1, 2, 3].indexOf(5));
+ assert.strictEqual(-1, [1, 2, 3].indexOf(0));
+ });
+});
diff --git a/tools/rst-optimizer/src/types.ts b/tools/rst-optimizer/src/types.ts
new file mode 100644
index 0000000000..8308b4a577
--- /dev/null
+++ b/tools/rst-optimizer/src/types.ts
@@ -0,0 +1,46 @@
+import * as vscode from 'vscode';
+
+export interface OptimizationConfig {
+ provider: string;
+ baseUrl: string;
+ model: string;
+ apiKey: string;
+ userPrompt: string;
+ maxTokens: number;
+ temperature: number;
+ neverUploadIfWorkspaceTrusted: boolean;
+ rewrapWidth: number;
+}
+
+export interface OptimizationRequest {
+ text: string;
+ userPrompt: string;
+ config: OptimizationConfig;
+ variables?: Record; // e.g. { fileName, filePath, relativePath, fileBaseName, fileExt }
+}
+
+export interface OptimizationResult {
+ optimizedText: string;
+ originalText: string;
+ filePath: string;
+}
+
+export interface LLMResponse {
+ choices: Array<{
+ message: {
+ content: string;
+ };
+ }>;
+ usage?: {
+ prompt_tokens: number;
+ completion_tokens: number;
+ total_tokens: number;
+ };
+}
+
+export interface DiffContext {
+ originalUri: vscode.Uri;
+ optimizedUri: vscode.Uri;
+ filePath: string;
+ optimizedContent: string;
+}
diff --git a/tools/rst-optimizer/src/utils/file.ts b/tools/rst-optimizer/src/utils/file.ts
new file mode 100644
index 0000000000..b487da6216
--- /dev/null
+++ b/tools/rst-optimizer/src/utils/file.ts
@@ -0,0 +1,117 @@
+import * as vscode from 'vscode';
+import * as fs from 'fs';
+import * as path from 'path';
+
+export class FileUtils {
+ /**
+ * 读取文件内容
+ */
+ static async readFile(filePath: string): Promise {
+ try {
+ const content = await fs.promises.readFile(filePath, 'utf8');
+ return content;
+ } catch (error) {
+ throw new Error(`读取文件失败: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ /**
+ * 写入文件内容
+ */
+ static async writeFile(filePath: string, content: string): Promise {
+ try {
+ await fs.promises.writeFile(filePath, content, 'utf8');
+ } catch (error) {
+ throw new Error(`写入文件失败: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ /**
+ * 获取当前活动编辑器的文件路径
+ */
+ static getCurrentRstFile(): string | undefined {
+ const activeEditor = vscode.window.activeTextEditor;
+ if (!activeEditor) {
+ return undefined;
+ }
+
+ const document = activeEditor.document;
+ if (document.languageId !== 'restructuredtext' && !document.fileName.endsWith('.rst')) {
+ return undefined;
+ }
+
+ return document.fileName;
+ }
+
+ /**
+ * 显示文件选择器,只选择 .rst 文件
+ */
+ static async pickRstFile(): Promise {
+ const options: vscode.OpenDialogOptions = {
+ canSelectMany: false,
+ openLabel: '选择 RST 文件',
+ filters: {
+ 'reStructuredText 文件': ['rst'],
+ '所有文件': ['*']
+ }
+ };
+
+ const fileUri = await vscode.window.showOpenDialog(options);
+ if (fileUri && fileUri[0]) {
+ return fileUri[0].fsPath;
+ }
+
+ return undefined;
+ }
+
+ /**
+ * 显示文件选择器,支持多选 .rst 文件
+ */
+ static async pickRstFiles(): Promise {
+ const options: vscode.OpenDialogOptions = {
+ canSelectMany: true,
+ openLabel: '选择 RST 文件(支持多选)',
+ filters: {
+ 'reStructuredText 文件': ['rst'],
+ '所有文件': ['*']
+ }
+ };
+
+ const fileUris = await vscode.window.showOpenDialog(options);
+ if (fileUris && fileUris.length > 0) {
+ return fileUris.map(uri => uri.fsPath);
+ }
+
+ return [];
+ }
+
+ /**
+ * 检查文件是否存在
+ */
+ static async fileExists(filePath: string): Promise {
+ try {
+ await fs.promises.access(filePath, fs.constants.F_OK);
+ return true;
+ } catch {
+ return false;
+ }
+ }
+
+ /**
+ * 获取文件名(不含路径)
+ */
+ static getFileName(filePath: string): string {
+ return path.basename(filePath);
+ }
+
+ /**
+ * 获取相对路径(相对于工作区)
+ */
+ static getRelativePath(filePath: string): string {
+ const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath));
+ if (workspaceFolder) {
+ return path.relative(workspaceFolder.uri.fsPath, filePath);
+ }
+ return path.basename(filePath);
+ }
+}
\ No newline at end of file
diff --git a/tools/rst-optimizer/src/utils/wrap.ts b/tools/rst-optimizer/src/utils/wrap.ts
new file mode 100644
index 0000000000..5a3cd07535
--- /dev/null
+++ b/tools/rst-optimizer/src/utils/wrap.ts
@@ -0,0 +1,112 @@
+export class TextWrapper {
+ /**
+ * 对文本进行硬换行包裹
+ * @param text 原始文本
+ * @param width 行宽限制
+ * @returns 包裹后的文本
+ */
+ static wrapText(text: string, width: number): string {
+ if (width <= 0) {
+ return text;
+ }
+
+ const lines = text.split('\n');
+ const wrappedLines: string[] = [];
+
+ for (const line of lines) {
+ // 保留空行
+ if (line.trim() === '') {
+ wrappedLines.push(line);
+ continue;
+ }
+
+ // 检查是否是 RST 特殊行(不应该被包裹)
+ if (this.shouldPreserveLine(line)) {
+ wrappedLines.push(line);
+ continue;
+ }
+
+ // 对普通文本行进行包裹
+ const wrapped = this.wrapLine(line, width);
+ wrappedLines.push(...wrapped);
+ }
+
+ return wrappedLines.join('\n');
+ }
+
+ /**
+ * 检查行是否应该保持原样(不进行包裹)
+ */
+ private static shouldPreserveLine(line: string): boolean {
+ const trimmed = line.trim();
+
+ // RST 指令行
+ if (trimmed.startsWith('.. ')) {
+ return true;
+ }
+
+ // 标题下划线
+ if (/^[\s]*[=\-`:'~^_*+#<>"]{3,}[\s]*$/.test(trimmed)) {
+ return true;
+ }
+
+ // 代码块内容(通过缩进判断)
+ if (line.startsWith(' ') || line.startsWith('\t')) {
+ return true;
+ }
+
+ // 列表项
+ if (/^[\s]*[-*+]\s/.test(line) || /^[\s]*\d+\.\s/.test(line)) {
+ return true;
+ }
+
+ // 表格行
+ if (trimmed.includes('|') && trimmed.length > 10) {
+ return true;
+ }
+
+ // 链接定义
+ if (/^[\s]*\.\. _[^:]+:/.test(line)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 包裹单行文本
+ */
+ private static wrapLine(line: string, width: number): string[] {
+ const leadingWhitespace = line.match(/^(\s*)/)?.[1] || '';
+ const content = line.trim();
+
+ if (content.length <= width - leadingWhitespace.length) {
+ return [line];
+ }
+
+ const words = content.split(/\s+/);
+ const wrappedLines: string[] = [];
+ let currentLine = leadingWhitespace;
+
+ for (const word of words) {
+ const testLine = currentLine === leadingWhitespace
+ ? currentLine + word
+ : currentLine + ' ' + word;
+
+ if (testLine.length <= width) {
+ currentLine = testLine;
+ } else {
+ if (currentLine.trim()) {
+ wrappedLines.push(currentLine);
+ }
+ currentLine = leadingWhitespace + word;
+ }
+ }
+
+ if (currentLine.trim()) {
+ wrappedLines.push(currentLine);
+ }
+
+ return wrappedLines.length > 0 ? wrappedLines : [line];
+ }
+}
\ No newline at end of file
diff --git a/tools/rst-optimizer/src/views/batchResultsHtml.ts b/tools/rst-optimizer/src/views/batchResultsHtml.ts
new file mode 100644
index 0000000000..bcbe9b63e6
--- /dev/null
+++ b/tools/rst-optimizer/src/views/batchResultsHtml.ts
@@ -0,0 +1,122 @@
+import { FileUtils } from '../utils/file';
+
+export interface BatchResultItem {
+ id: string;
+ timestamp: number;
+ filePath: string;
+ originalText: string;
+ optimizedText: string;
+}
+
+export function getBatchResultsHtml(results: BatchResultItem[]): string {
+ const fileItems = results.map((result, index) => {
+ const fileName = FileUtils.getFileName(result.filePath);
+ const relativePath = FileUtils.getRelativePath(result.filePath);
+ const originalLength = result.originalText.length;
+ const optimizedLength = result.optimizedText.length;
+ const changePercent = Math.round(((optimizedLength - originalLength) / Math.max(1, originalLength)) * 100);
+ const date = new Date(result.timestamp || Date.now());
+ const timeStr = `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
+
+ return `
+
+
+
+ ${relativePath}
+ ${timeStr}
+
+
+ 原文: ${originalLength} 字符
+ 优化后: ${optimizedLength} 字符
+
+ 变化: ${changePercent >= 0 ? '+' : ''}${changePercent}%
+
+
+
+ `;
+ }).join('');
+
+ return `
+
+
+
+
+
+ RST 批量优化结果
+
+
+
+
+
+
+
+
+ ${fileItems}
+
+
+
+
+
+
+
+
+
+ `;
+}
diff --git a/tools/rst-optimizer/src/views/batchResultsView.ts b/tools/rst-optimizer/src/views/batchResultsView.ts
new file mode 100644
index 0000000000..bee5b38086
--- /dev/null
+++ b/tools/rst-optimizer/src/views/batchResultsView.ts
@@ -0,0 +1,92 @@
+import * as vscode from 'vscode';
+import { getBatchResultsHtml, BatchResultItem } from './batchResultsHtml';
+import { HistoryStore } from '../history';
+
+export type BatchResultsHandlers = {
+ onApplyAll: (results: BatchResultItem[]) => Promise;
+ onApplySelected: (results: BatchResultItem[], selectedFiles: string[]) => Promise;
+ onViewDiffById: (id: string) => Promise;
+ onDiscard: () => Promise;
+};
+
+export class BatchResultsViewProvider implements vscode.WebviewViewProvider {
+ public static readonly ViewId = 'rstOptimizer.batchResults';
+
+ private _view?: vscode.WebviewView;
+ private _results: BatchResultItem[] = [];
+ private _handlers: BatchResultsHandlers;
+
+ constructor(handlers: BatchResultsHandlers) {
+ this._handlers = handlers;
+ }
+
+ resolveWebviewView(webviewView: vscode.WebviewView): void | Thenable {
+ this._view = webviewView;
+ webviewView.webview.options = { enableScripts: true };
+ this._results = HistoryStore.getAll();
+ this.updateHtml();
+
+ webviewView.webview.onDidReceiveMessage(async message => {
+ switch (message.command) {
+ case 'applyAll':
+ await this._handlers.onApplyAll(this._results);
+ break;
+ case 'applySelected':
+ await this._handlers.onApplySelected(this._results, message.selectedFiles || []);
+ break;
+ case 'viewDiff':
+ if (message.id) {
+ await this._handlers.onViewDiffById(message.id);
+ }
+ break;
+ case 'deleteItem':
+ if (message.id) {
+ HistoryStore.removeById(message.id);
+ this._results = HistoryStore.getAll();
+ this.updateHtml();
+ }
+ break;
+ case 'deleteSelected':
+ if (Array.isArray(message.ids) && message.ids.length) {
+ for (const id of message.ids) {
+ HistoryStore.removeById(id);
+ }
+ this._results = HistoryStore.getAll();
+ this.updateHtml();
+ }
+ break;
+ case 'discard':
+ await this._handlers.onDiscard();
+ break;
+ }
+ });
+ }
+
+ async showResults(results: Array>) {
+ // Append into history store and refresh from it
+ if (results && results.length) {
+ // results may not carry id/timestamp when passed in; ensure persistence creates them
+ const plain = results.map(r => ({ filePath: r.filePath, originalText: r.originalText, optimizedText: r.optimizedText }));
+ HistoryStore.addMany(plain);
+ }
+ this._results = HistoryStore.getAll();
+ this.updateHtml();
+ await vscode.commands.executeCommand('workbench.view.explorer');
+ await vscode.commands.executeCommand('workbench.view.extension.rstOptimizer'); // in case moved later
+ await vscode.commands.executeCommand('rstOptimizer.revealBatchResultsView');
+ }
+
+ private updateHtml() {
+ if (!this._view) return;
+ this._view.webview.html = getBatchResultsHtml(this._results);
+ }
+
+ reveal() {
+ this._view?.show?.(true);
+ }
+
+ refreshFromStore() {
+ this._results = HistoryStore.getAll();
+ this.updateHtml();
+ }
+}
--
Gitee