zW(hWcT9IPz^$F@_VR%rFL>2=wZ9uAZUIiBgbaW#%fjz3HtW3gHf(wP{0Om?xC97PbOAKhmAfMaxbTMmy-ozGGw6+HOS11~P{N97#58(IR_`M0g@5ArS_`MOo<$qHTJoQI;(*=JPdNAMo-GcCA
z)6W-#uZ41Rr*m?r0j9=s_jmk4K><2xn4@S2iDXAzgJ5X9#Szfds|5sGH@943j
zLlq_ai}oL?Ja~L>MMYU@@gZ~T`S1hgzTXIK`bDd;JkiP|6ypwPFLH6K+U)<}6PH
z`lsiTn@Ev(<2PXZ+c)YdKtTA(m^FJEN;zX*a;l-UgE!R-Fh;aZ12mQhx`mwEDXE8_s5f2DU;Ej#C8z6|sB!
zf$^M+iyg0EuKaWkMwzF^V0X!1WH0m$~~>p-aMF(@LwE4Put2{)nV5`;cJwZ@>Y02TT@n+cY<0sz&3^W&h`G7}1Sc%(Bu
z-;4o0?YUaC64Zz}_+n_QfkA1XRn61;lzgfpI(RdF}s6@@K&S|=BX9`LeV<&$G;RRG@S8>
z)qMIU3tjDT1v0wRG}L`ZzuiNHj@5bRA^seU3W=!?Q}GC8)ZJnYjk?v8%b9c4F5C`_
zgI0{|)q{YZcMVVg4A?O1COnUg)zA#DlNIf8h#?vr`na0x#zxS`iVeHflUD3Ju|yrc
zOYXJv)OhorE5x%sBclq`u`=Z#J`n5T^FH1r6Hov><@wyejdt%~I(38RF;R_uU_-PR
znT#`6QaJukYbz@flE>N%FV_e{Ds6l84c(HgL
zjGCzh1m|5KX~j|q=2Ih?SU1^0odma@IK|v(lQTMDpMmSC4sY=Uk
z<|yo`OuImHVS7Nz=i$=zbYJ%jj77|04dePIu@2<`oGtt01rdWc*ts;5|uGVPjt;NfLi#EuNO=f<#0
z;6A&7xKTM7w+oEGrFSG?VDLDzw7Qi@S>1hZ;$C{tIny}M9Nfrfp_9zXGvWJIHQ3c=
z=tnRYi$567JCv;&vNw!ZRXcE1z&3)2vh|MY8h)DPR8rZ$B)mm(d&7#rR%y+}i?3Q;
z*TMau8&yPG?&ETY;j4NkK}8aL5}0
zWQ;asvnEM7UWIr77zZ@fBc-b~>mXqU1qYda9A5IQ10>CaD=@d+c->kU%s$NcW#d8f
z2lL@AF9pjC{9UkU!Bsn7S_u|ej+%-~50~uSP}rIi3jIk==uZk;5n=m-kkMUMPyip(
z*rw=aIGOeyDJd%}t*AIqTwGLAzVBdBX-P$KQQ5I$6-E0i4iy)d7ndD73h8O6zm!YAdZYWyK&fx0tXopxDKq^t5!X)`Ax`&
zr+<3FZXSol><(fe&zOWnwA#89%tG+0KMD^!J#)@*UphJDq(r=lD;a)B_(IY%r|^JP
zI|WOZF^MxY`EZ91s9~mJ8gj%sFYs8dG8RPB5lRLD5x{5(RH+GO-u-N7lexGiyv7Jv
z-9ylGv(Px6I1;~(-L+sXnDFj^5NdOJvOwJJo{oj(0=YgmVgvFlG6LYC0TJ++Uu`Pb
zk1(495o6U4*+%CW9TC`D@-#eHxaxL09S-nP_{bk_du!wjyjPUbeULk~p6bT1Es#Oo
zN*oUk2x7+o5IeLFn(81%upZh|vKTe_)Y=8|GTixj(1cG2PJm$p@d?JI?*&sXc0+7s
zx~_u9oyHq5vhZYjq1V0MX`APOba=*Mb=u9-@LOYIteY1{nMkl1HjLv3;^bun*h`M+
z2>iRT5D8!(UV|42jtnRlyAN)hF}S1PozOQQKKk0L$4~s)SIb{N@!G2oSDYyShIbCJ
zOOS;SoTOb-lvrzek`iTn!AZp-@UcWU5d|BPDt^REq05^Qqf6=LR-d&SP9ZXeqYn;|
ztgk4-5Bn)7Tjojdf)-~pk+a+o0&Y+|;#eR$#KFdzK_NwLBr{G;!fDEh#rD~gz*$^1
zFhmo~SjHzvmI}^$!VRnE61iZ%KRKt9@=1M>3ViKA`^#<3Jb!tj`L=5fTMN7PBs>gM!
zT(r;t`~0i|8ruJA?T@zM@-IPPaiu8Yz-t7Qf}o+;ASTF?;?<&esfU*Ws2bDNI#^*#mXX*a8@jCp%HA*H+!Rs++9~@KQ!Gl{wiOzSjV5H4#IP*;o5PI$oBJ
zS*lrasS{4Y&Y_U-u7elxQWu|Z65z1G3NDNd2tfSsOg-G^m|q!?>l*5Z|JA+*ghS_}
zr!TPNaf2qP6TObl@FrHJ2cfnFua+NmCGWINA(Tyq5Zs9;X|V}2LU5^df()e<|gls5
zx~=YOSpC|yWFcSB<8!qR+`eYqdbQr+(!s$hb*wq)+tE{mK`?YQi9#sEH#Gx81TGed
zBf|I^QO2isXPrHG-b%*3FoQVBJ(uUXIJgiK(46Q`F7rBwGT|ZnnFNM6jmS4{vi=Nw
zS&ZT@_Sr4N^oq#ADZ8n1_IN6S7&440Mt|LLgn&-tpLU<@4A)*@CzX^*(^jvl`cw%U
zqIK9Ud;_kLh(gFZ7fuC74^CLxJ*;EZPymEN7&Y^7plU;5iHLM6Ec66yDv8ZuCwe8p
zKh!qxt}8jQ6u5eOcthdhc`IV@$XbyA(+EwH8KkDCyR#{po~_}!C5#%_7WT|VIQB``
z-m{4H-~k0HR{Sa!hD!pDb^5NrZIqs=^NeG$3bMs=U}`d8^faULEOlovFB}p87#DTD?;Iyp4mmi
z-fB(2P6yj5%2YwGOTolekGbaA(9ak_)KJ9(4(-m9f;T-c@%#WBJxK0QEuk<9n}yv(
zBSuumGT?DrEMUxatDe*isIJE-P#nIio|!JH@Kn9QR)UK}bsJp`St2`Iu&OlDtE4wm
zS(kYrOXjlS1^-vh5*X3AFnK`C`R&KNm+4e$v))aRTl3nD)tt!
zl83X`aNuz1zQZL4U){KDKI7dd-22l_*j{!Ia~--j7`0DCLq|$V_U_$RTvC3t=vc|o
zgGEN^@sfi@Wd{!)Eh;ZPP+3x3QF5#duJ_X0q%8#bgkYsQb!C+=RsgpK6-(vOA
z@d2yRn(1Nywd%Svcm+SPdRr0RqWlr3I4m9`bGir4W4mhrAr3%)^;A$|^h7d)^T-+F
zK$2ktiQ&l~NnKPei%;U}a+6@utI`fCmhqjr;RpERCrYx|c
zWQAednX{5hrU6u6{by2L_yxm@2pwsI5+#Kh&izY+0e2(q}*{9u%S=
zn$R%@2eS!pQ95a~02soZuCxpiGnas@OrRh&(C+jHL%NHRqqGr6JxwW%GviYP^a20@
z-3MMWgULc(}BR!hRg@1A+@j!rl0SNFpS6stm
z=P)ENp50RKjCH`s(k;V9qj6$oMm0%Qj|W7sjKFaX_k)}#rU55-fqbZQFE@A0n&?H=
z16GE;t7U-2>AhGwjG^WGd&Ty(FF47M;MvM
zG}eY$Fb*!X7s45DuxgkO>pbK8j_@(?gO{x3=W6L~Q@W|2R?j{gepu(DR$sgMp=Uxn
z%s<={T5Ep$bBNq`FCrd|Yo@waNljT7q)iH`alzd23t&g<{Dh9lRnEut;yGgt)P?9Ac-#cq1bb_W-4cii~%EqNsUM6LPq
z?YvD8?$O@t*gfxMDSdhGY#}!gInq^p+10TC7DVJGTnItMmjZi
zAluZq1Oo*!A9{~@Hln&|j%xfV6>Q_D#_@EW=Q&}YK1Bo7pR1i@Fw#!D+2AYY@l!?
zxD-F}*48z2REh&Q{^18V8}QyBW9CXG(rVANdJ#l~@lQ`sH6xG=g?L-D^S4T96&eGuQG1%?6Z^gXn)$Ag};|fTagvQOWXvx7UsO
z`3a%}acnAd;owiFhAsGvkwIIq4?BAJeRudT0s9QnOC%Hfzd%S$lCiCq^L}INND>>BpqzsPpLg4^(v*&gO
zQ*rsDbaYF~C>D!n)Qy2Vo=#2kf{Wu*okYqeo-*^ZfW=^5V(8`09K{Fj_*E;0bv7dD2br&UTOzoU^rJ~UlonnAka{~X
z5}>bvy=mApBQgXE9nf2kfRvNFpxMLT1raSk6GXylTqnThwewuO)XU%?c!@OZK3?U$
z1^6T32rnbBy~(rr4Z^C6dR^?(gofGL-(8qR+A@Z~vJis?QlV3-J4y2a2&Q8J)9Y2R
z#xL9B@O7P3oviD|i?{&kWEQ$!l0DbL*`cNFsQqd>$=RH4#A;|uh`
zfM`KxaQ5B7^w{@JcbEatUPif{YJ`0nta*rP$xW98g=O3z%)~)7>nMvM-hRM8V?Ecb
zLp7UT1LDIeshb=w76l@D;l$&@av#0jJ6~FfqR!U;D0?V7y#EmWAFB%Hatb>N-(N6S
z@cagQ!++nfOS1O($JB#Q{b0TE`r7xYx
z!)$A|he}p!8sPSK%LDw2g3z{4u(YJ?uteUoN7WyF=|pZY=1M4KW(_HHT0W%w=)_&Y
zkSocnlp!C}RZ7LlO+&H>U
z?varHNQpW5VUAT0+Oz#O52_2SE+?;UbzS_PyMNz^m26!1;?_{l$B(e|W+hIoqP?H}
z)W<%9;+uVn|1~2o6R&~=f>Kc=@rfwt)@nsHS3I0wkGsy&PYTh+X}9UJW5Aq{yM=eC
zjb<(>P=PfGen)zoeSI2W&*|>6%~OF4f)dQGZpFKJSe2tHB#1`(kQO6Y4aMh;etYf`
zoWyYR=wm((G|OHIZ(ybpd{59F*%&YzQ1Cu0ZV^XlP-)`j1}yizFt3JtoJR*4$Xu25vcixp9}3;^;V=g{#4}_LuENPwmgT&gF9MsdTPXu
z&Oz5GMfk9wVE`km5k5F%Ruic~kOLpv_0v!~2s^1#E1EE8!*iVO(BywVi%kdn$poB`FhC$HjD90uf%(uj8YYXJkU8Y|!OIz>
zs7S4NwyzKa;xLG-YNnVqre-B>wBc7@a{@o^Xt)2yMtaU68f)*NdAu^=xMU!&Ts#`@
zC?EfG#N$1;$VapImGGwb401~E06FQE(IyM%h*HB$D^wHeA*j0@
zuQOESdE9^gDmE>-tqhCa1f02DaVhEkFiL=$_2&wlX}_b0VTcIXJ8p@~&;=s!1FOFk
z33FwV)VVtwr|l-urP2Z(saqkI5=V{S02B}rJS33dCOi;;0kiE@)B!4lV!+u4JPMvj
z5h>5qZL0HSIT|MK=}82~pe%L={=x?RWD0RR-W^lBJ#eV_!Ar(I;=z`yq
zHbrY2-N#~PtL5lN1w3KL3;kH@h!@j_e+~&~0+RLCyNj*Z0aoLVR$B|Fh!G$71bpi=
z5lAXcw21%ar(QCUcn6;X
z=El9jFhQdwEG4K`@o>LbHIV;|yNWv-V`Ewb+*NjJTPbTAY5q)C&}m^p!Rnz<$m$%U
z84}#>K4cV%n;3(7dgas=G%3RS*}_i?($&0(|LFd$<|nR(x1wmF7Xxc`gYKVMh*E%r
z_ccb14Xbj5T7(XPAq4{m2dsyo1lKGBzp~QvI(sDz(ntz~fr!?$K^PiVLo=qc9z&sn
zq?q2cSTGc3#dCBFBMTr|+xd&35(9Ks&nsR6!SIq6lWSOm8}Xw+s+T=mRPzs#?FfZn
zRB8$sUylpvq(fopg>PU
zQ($xvq=e*c=nX=OjcZCGgy?nOPu-2M4Ju+Zexp}aBNeFCKsy19Y>hWSF2MSwYg{y~
zr(~oyw2-Hy_X|b5iC(kdh0qqHG;Bh^LgEX}P}q#7=yudFUZSZMiq}lyPHTXN!-@M@
z6|SB**#b|t)WLzGnh&goA5#5}q1unxEa@an;wscVqe6lUggwl|$D3#w($EZlg^C0s
zK2}~xLl!@E)f%Jw2Leau(~JNvMataKJcE3ZI4L!7WT?PlqN~I#3dMYX4@+=wAot)_
znWB&4keS@DIFAe?(x#^$OiR4xIoX)zg$vBCWO8rc>C$4kdhm!6EWdgPK6JX9{jO+~
zIf9eC9No!Z2*EN|;V9Hrv8XapBX^|SOub$Vbw)Vvb|2oIvBfW;obdJ&f+4*F(rlG1
zZU`J{!O<{x;8BS$NJ9(Y6y-xbf}Jm|MCaY4JWCFj7Uu$&@(>ACH7K
zuoy4z%Q<;pE^N*HSIqeXJerSIWz2_<#7-FBCbFU@nx6-n1#KPt$c|j3wM;h16j9NYBvrmV1J}!BX>ON;1
z9CTOmHMnAXa3$Ndh{xw9FAw6?2<@@>pob;0+0JmiAmnJVkdzH?lj*3}`?kJc=@^H_
zXc8XA_x!+*_VznW246w}2fzi#VhZ(3&
zf43w37S?(`cXg6N1eo_c7T&Q6W79*r&FuR^i2a*bx{VLr`gY!awxl`}$PJZcX?kB_
zo+Z784x$`2XG2vnvL>)f5uNm*VnA2o>1sam7=gN4M~E3TYM|7^`5SVlNhwu3UZA+2
z`d$uIi}#o|E3Kp?tkR!p7YLFSr)C(wvD?Rbo=VnmG$;hsgekJ6q$bZ=<3+Ph5=COt
zPg4LJ%0;z&1?enVT^qSV^qcv;*6^V$NFmN%Rh85XD9VJVyebC3k!=dAsj6rZo3c9q
zL@shbOq=q$S%T*@3Y?t^qx>Hv40`>6J|uKe$2)f=DL5JfzRBONmxH57Wj9t}{`iaV
zd4dqMPXV?UdbKps3us7+&r+KoE%E{B5lX%?g3=R}sHz4qwdz61cVp%UW6I*;R;>Vf
zL5ZnWz{4CtdOx`368a%K$xgCsVMrCJ;Q%=$gz-*|mN1v9O4gbBsTqMs!~E_mKp9_s
zc}@I_?k)qoWDdN-1n;c&uG=~*;s^FHFDajAseS*$kw3i?3df@_~SfhK-e2E2oQh2jP%wtb;$KDc(7vmKePlV01#
zHD>Wpc&$;xhGV?;5*Swle;F_r@vV2u^C*B>g{64vqv-L#&RTj~H3LV_I&gJ*#_CK<
zV9#X5$qjY~(*-K;Ysx3L6=RnaH9T=inf!&6_dzkF{Cs*Wno<%;M$r!R!$WRcV3Mn+
z8cLtC5cz%*Acx-1@>kQucph_jIuznW8bD`vI}S=DJpL7jsV<8pwxW^sp7IV#7LytJ
zUg&;FD^&hh-RYH_ifG3BG%BTh7A=%u(XpaHT9@3Em9A@dBsHBA_=rKZ52_@z7dVL&
zZMCadov24(XpRw+v>Td(-rg=vOm;uW?mYvUs||xyu`!_v(sCXds?4NT`guh!|(2;OGDPAL=x}eyQ^BOg%bD|zs7R8Y(
z3{XwGge-!(Q6vPPh6AgGExiX|hfDGTdLn?rzS~O+Q1pO3AHDW!O4XxM%42r95zg6s
zI3wsuFjIdM+HDYjD1=+yBwNLy3KPH7l02cok(-%LEBU`h@b?`_+eQjry&{%i8iBI>IBezVayiucN1&
zR6WHfd#0eEfVPM-dC(7EtkmKlN9(p$s@%F|eS#%pDoEN#E4N{E+=%IcvG!XH8XxRy
zLjfMMkFu0y0++YOw=~b+<7B_9d$!`?0gv~=!|ZJo=u36aI*3MJ`IvuQ7QWvQqz&6N
zs0UCbf}I4?T640XifHNkiS^bn`-!M(SKSMX9ldP;#ijWrU3BnU;}u0#JV~VYUx<)9
zm%6N&bFJakk7l{@IH+W+LZ>r(7j9yd??mIgj0t8v;krT4d4&a8@idh>e(?dlicp(8
z#cZh96e+4x-;MAqGoe=@`cH_htCtPOsm3}xGs*W{1RB(1OfNH>Ep2g+RPFKXr8-qxHl%+U
zic}a+!Cyd0m5{fU1mplsH)uHLTw%!wH>svuVp#b=4Nk`KH~4*Z<>rf!ly;kb=`Dlq
zS(0+e+}B>WVia=qzNGN>raUJ*R56EDWtUGY7iEJ~+;i=C;Qm(;#Y>VvJS$2Cy%&1*
zZoLiMC)!N|MM!oeq)z;`f7_a;2Vge87TU5JAwy|ABb{-XJ&+vz36048#s6U=vUIr%
zx%iHa1#Y9`JHp$xZ}>6(8s@Qz6td%X(H4SaGX
zMGEVK{>+KcjfwD!1!kfk{K)ig7la${x$Bvn{AZ>gdL#5?{`51?gcnxl=RcB@|43*L
zni~dRXSIu8XZ6;q=|>KR|9bW6aPGHq@~3jYWkx<4dSv>EuZ25ur@vK+@68D9o}PLo
z{PO+ga~}?Gn=bxh`2XTd67s&CllSdg9?lI-fBS`S*_s=z;k)i$8_NG)4t~9!(*zgF
zF{9;JOU34OFFvrkyGV)^R>r_udjdhKL14y*fw@}oWgCpkYfKn|L~*cl$wbK;^UJOZ
z5Is9C7M+AG4aL7zQwX1=Q;kfG_X_dF9fJZ=@wvFkt86a~xh`#yRpSFHpx+ME4yLr)
zE=XB?1Iyx5o$OTrH7R}ffzu$wph2BUe6JQ8$7>!3GdqU9(99*{Z|qFgXwONVv{);6
z2g%*gkqeGk$^Ibc}4#iQy7c>Cz2cAo&qPkd{YuY
zJ6=>%1VB-VuFosfG8};ioJ>0FDPrY2(iaWm0+F!GgM8@(-oykx5^4bhu|NiiKG8T@
zkvW4Ae$$J%$_W6_x)>;Wk!8#1q7JyJmZW~wg?IuQ?C|M&^Lw9%O&|CW05!Dh2IdBD
zz-)2)F9T%~ox_13UkSjE6dp*sHmn}Q(c6>t1ruu2a3_Lxi?DY%BZ|~7rX*lN!D6>y
zDnlC))47f31IAX=qIeXufI(1h%5BGn*rF{+8o_BG*(EiI^eg%=k-L>>W}}i>y6DyC
zwXDs9Z!TLMsF6kivT5MlNo=o<9bbGw6DU}*^q
z3gL0z0i`*pgk*3I09oj1<6a*^LkTI{VY-aRzzgUm(ZZUI4=>ib*ZS>KH9$jFGsGZD
zh1vVqxi)}S8Q40mINdIa<*-mmXvIK^|IH_U5ZYpv{~bJ)(!&RB+?T>S2{im+mkdxf
ztBE!t#{kdJV2Jc3HOj;Cbc<_i(?#^#+GmuWk
zi=g~LMyYOq$-+?t{L&uQ)53_FNt2y~A-=-&%ux1Tkw}96*br5A3Za=4Ccc9^hv^jb
z1D>l|i6Y8|cV6*h>#qCh!a+!&rX;#yg$V*nsfPx?`SGWX3;Yyefi*n1_u>NY#RcAr
z3ureeO5pe60)9Wq_u>K!E&RlB0mybWFRYpx!VL-aGU7ll>Id7P!Slo`J=$rOHpvz+8P*ro=Dn4)tO;2cmU_~u7#;!kI~
ze1nCWp#kzj*p1F<@Mq(cb4^Z++_1=HH^anZOLlSTm9}vIXlN_H@ut^44T~E?hF^G?xH`
zva1*E4tAr#M_w$Ot7V{y{OEs`5Z0iEm8RI!7BP4I8om$mcU}ywIdtc*o@7PDLXCc_
ziIeJO{xwFEZ`Lsjud~j)t(wHv%%(Mt&0PtnFv
zB2j>0qS$nRrqz5y<7FiI!F)sg9E~2|rNNaL60yO=Fy}x5xG9LwVT*UP!U=&dfaF&Z
z2nNT~#MP)XA|Cm37~1kJj%qB!>A=#w;A<-py#kq~
zbG5`Y*btf>2gC=1*F94eQ6~9DV()7E-#qMfB(9(EB)0#}%2)>MFnAdtwE4ZUI83bw
z@(H1RDAjpRCIK^O%Zmr)8&YY$p)+311+W44AVDYnx{mVigf?wNt^$6HtQ2yZwD~l@
z{zho)M(8Z%5eO*l4HAFu@vXZxAVAO85c<`#`86D2I2sNu(F~RN=H7n`Z7bIhKeA6z
z(?q8YGB&&MEnm_+wD%`9zeJMfXL9iCpUm|W;f#LEZ)gX<1o*Dp>CEo%=GFfni!~zj
delta 10900
zcmeHNdvIK5b>Dj>t=?KWjx5=VUy2pSj@RDRbLEuUX#m5dA+fQV*shc4Wg4Dgn!(VJ
z?5-ZG)qB@h($&+FY{`~nYge)q?@JF-hAC+W2*V7a!!RNCe)mymLuY^~LxBFy{Z_&W
zDY*T!gROnt`#sKgzH@%xIlpsVu1uFJ)3@y&VZX^_I)K-`cxB^N;6G6E7r1(`>sQ&y
z=S^8+*p&6CC7AJ(%%7+EGRDo}G`kqics4m;IcYg!If5R>zvkln)ciANgx1~CFNM9*
z8yP9muZ1Gxb6c`ycA)6Drb598j0#x%=W+ZJs(ZwQ4Zlkmv-<4v104MAGA
zzu0KC7_v5FG$UI3ugq=^PjJ8`10R>@w3mr*zoMB0(1W7j%~Cclv;q!yX*cgF93!g)F={ZwHZsk+)>chr~HRGhQcIw~ryWe!`t
zwZ_pK+d3gCW*3g(a%)In{j)!eD(Y+KX{8AiL5SkF_(`nBKO8Hnhe^#-{Yv
zE-Zlb;PbGxS23a?%BU9X(*mnnV1hK()S|{_6?sYv%%i2U6oZKGEDu3$`L*YT!aVe1
z%Y7`k9FO(KW7oByhk38b-}Q@mPMUZ{N(epQqYrx7@Hm_AQDe=_=Z&w7u*n7Hol%-6
z71sqtCM`n%48mF#n6Hzy^(z;B@u&wQLlU;uqEE!QLiygY;ixjw7GIm-!s{-dGB<_&
zP_K1puZC_t3F(a;Q-Zy`n=-tFHEUPgyg&Ah?=}R*Ua42R;<0XJJfcK>(52GWK`J-a
zW+;0^di$R@XJU_f+Syu@>V8!^^^JW_bup&K#uVQiCQb|sH8m|VUl5xH>9HiTqrO2X
zLg|g@qcfP!P_#!2&MEz^tgDZ;G;xhXV5R$#x^yWXgQ6#B-d8)~Q7>1b?rPI74QQ>i
z@o-ETZ6f_T?@+MnTSN<`V@4hCRcCJAcD1X~OUl%;a_P!#7cE8^>C&g>Y0A!iu5UIK
z!J4&8*RZS1cU=qI&{nRqo&i`R=DGZ+s-e8fYR8?jh8lZGouf+L6&8cij(hhfK_~KQhxqwi|Cuy$=i#&Nl;$dT
zc6DgyUh&&${a+Kz=HHl7e`7lD_kX6Wt0pP)RG~1q-JNpRkRpBKwI1?vGYd4uV^bTm
zz8e-#N-fnE`NyZkJ+dnzrc2L!d3OP98UDfo7q!r<`n3TTy~3vaut#|Jhl-?jEeWgrL}PzIyw^dbukk)e!5)v-mamRm8KCpUrvs=d>&
zt$5g_U2RkwBXAw&ZpCJ^k!f61u1_d^^VnTDJ3Rdz`I*C_W!Lby(s@m39{}d4%~!N2
zIXf&y8JSgFQ}JlG{8m&faB^4U4vGeth``Lh%zR;Xp;MW=%zSnefI}JK$|t
z5}{J-tUj~A+K1U#gtf%DGr^72$td+2nO1w|)aX?;Faf-ZhplroCA8wdz?Pc`CYHzH
ziL@0{{Yuk}G7@E81G!hOYc4r=S^Usb7zfKE)c_EU-BsFL+O%t9b}kI_v|u{2{*fq+
z*E^|uE^KVAI?@59@C5>_;6L)FXN3YM^A78-aR|kc5FOQ#Wp(PJ-rA|lvucxD9d#)S
zjW}Is%$7DTOyU6O=~B4q7>Y&%?dk=s?FI}8<3l{v-KnignyUq3kc;~Kbg?m_wl?vL
z?>Lq_b=Mzv*8j7w$~*T8|B~x7rIQEu<3RT81QN)>L&5_gc>1?e3$k-&Oi4K>OgSek
zGnNxM?`F^7|0lEFUEFjuo|SeYC$+j*C`^-{yj?8+4j;fw))z-`uf@$YiWc1oVBc?vdmiLa5$`va)-U#UQ=IVw^u4{
z5?gk0Z+zdVb{maaT7I~!1mYdPCo!tM)ZwhFs%@}aYsyP2tYx;!Dts%iwAR&DlvUL@
zO6yDO&i(0OD-PQ#G3?(Ir|%U0x01>ouLzqr-yvUh3TbGkAYXI}Pn05_nklY6nN9*@
z@UluQ*HPPGEv>hgTg$2{YOIcmiZW|`X@$M6qOPXGR$n8#UJ-U2vQ?inR*6nn<)2)n
z(P>=_T2HmHQ9Fh8joP1HZRUcN$_Eb#d*r8_!v2DcoCr4CXUg$e-n5kQ|2dJI2sZo6
zIY|#DRm<*!La{T)a@O=xO>LFEz8<^RP+JMvZDls-y1`~GYd8nlt7_|Q4Ypi#5$;Cb
zdobd@yU|8EjVO@W?s4KIG5y1PPpuDj1nN^&=cp*HtFT+`jtYmhtggmJ@vOsYD{m-=
zt(H}m+et#o%PJ&31i4&0>#Cb*0|
zNjh$|%0GKq%$9$BX9co-bfu+_!_jUZ3vW$&fX2XV=|d1k?9S5K&F@fKd?vA*I)aL^rOvb(pyE3
z-Wzcvr6C*OCS_Q(T=g;Wt0_pVkRr7|DBPc&U}0bzP^WA^EfzYVXKlU%8b!)ycmQ*Ht#zObm)a&ggOgDLMw;@XrK;
z(&J-Yty<^=5kM5fbKWNr_75q;t3+k2bR|aVTT@`tH`?^r0Ctbqg6=WowuYDYtIN@N
zq=&UEX*Vywvpg)l@j8-^?lH)aXlo>xl;8CP;ecS1;^8GqBnSQa(1?1e3(cS+m=uy(
zrEe)7y+FB6e}sE3FbSrnk6uJ#1cpLimRS1&GD_z4fGXP{CX^AE8uN0yj^A&vG}sQ-
z1IDUbZGviApasAS>zdbsZscduE@e06*YQ}0xt6)^jZ}~)n#$-D>}n$uLmtT4H7E;h
zxj|}f0Ns!>Ac07NAHo7i{h=vvNl5Y7{0hXu#I(K@Pa?tVY-O!co*pREPNM0UhjMm+
zCRUwbISewzAZ3iSecl7GA);Wi6}G`W9=eBcY&F0xBLAaQPoIdW!vSs#N_#t6g8Ea&
zF`q}fiVQWH;B?k0M2xu`+yUBY7rVeP@hljG7={CqBAdfP<>P5WUSYx(LCq|UDIGX3
zh>_vUkR-gcS$1U$DTlT05L=a@S;$4mFwm_G&eI+kP)Z!Y@X~vsl!z}?a;P9Xsa)*&=Zp<7AaC~9INP))5M0BX(
zi(Ij~yp*8g+6*6AomIPov@xq|ut^TsIEU13(2B&%LNXW*X?X6+E!We;&9WXiDYzJ7
zT{jq2_-{Oh10%?70nOAKtJ>NynLgd`NQ-u7@4MTi;Xe75gTg+sY%BjPE#RNFEym|V_Pm@QnD*UGvb0@x9TN5n
z8T7GFw51!LPCG5*t9O%?6(*e$Na(xCw@4RzZH~$sy93#LX^FkmT2^_k9OVDG8W4q*
zj=I`&4Rwxl`(-Bwu=8K1fKOiGeO;e;vaq;NtU@%u04jOXPOA-HnJ
zeF%i0bo_R4ddX8KK7JB4gc9j9Z+|-X^7(usC8&X?+ASDO7N2|jb%82JI|uh|p}W(^
z-I<;mP!@~FZ;xc<&@@77r75-2>@+(U{6)gy&g9I@>O!IKQwH&9FKMW+tE(%umN(Q>
z(ppoAEU@mJwW_SVzNDzc4ymyv
zpcaQ{ijfTR8zb|hn-?HB;jdo2uDB6jwE{M%fl)_*BXdAxEsLCQGaO6a5)tn+NZKDK
z3%hs3qn)hF%_2+WHpY2~h*7y2L~r1|^g?xMAqoM6I$@&qa7gLyR2@S@l1y?;cskE(<5un&%kl6`AV0!J1Y|6+65Y3ESL(KtufWE+M
zlxipjYI9NC6N|w}$=X~eWg!L4+j)ix&deF5W8FiTVC1^ZR
zgeR#B1mqBiW3x3ekP!kwBEqvkl;&*l@`M_&gf>d|lp0-xN)2Rk%GAOG!2Er6708BB
zd5*!OQVa)BK(Tr
ztPWoR4)pVqD#w!FA3^PFBpKH@Fxrhh=z{x`C@a@tSF{rbh+~I&hJjn?f^?hmF6_3E
z-_zlS7(N!|aADMOsiuiD2s;{g#->PvMyh#dGL5mgDU!*daAs7hsoNdZx;v>8Uee)_
zDzXIthSr4QR}4z#%T>ApfUxycC;{mDgp-1M$`%Y%U4IT>=#SK6Nh^_?5J+7K=mR&E
zOF?w!fQwr|F=&*B(O4L;7;>mrgX$7a*DfdnPJ#@RNDP=;^Voi@2_Vg@(&)e|KxoRm
zV|96iC^z%mn>?AoC<=>0zp55*3VANT+kL)@Y`Q=26_i>}5rz@Yh4iPyX$yv
z9ta(JPJAO9;3$eYG?8eU6U9-}oMqDQAG3__-;+mG@>$kTh3E#_d;_m4LWZzBTo51tCXOiXHM|d|qbO$egi(jJ4rp0YX
ze_{^h>=nP0b{IeRQ8utQ6>kqv=i+3%?McGh{UY9q1iYD=^;A@1`l3mS3WPFFjGyvKPki!J)tN
zy%m{9pLqPqzy8R{k9@eEyeu$@8Yb4l;T`cUg-fm&dFcufcf_d#=Ky)|6~v2Z2^0aP
z0W?yY@C-yZm0MT(==qkOc8bF0-MVbFN1=6%eEzmnZQUgAcvMK2zY_*a!_^04r}HLJ
zIPYXlL;xzIV`YvaGv$5OEO0umDF1>BFnsV>q7)PXvXb23&Z&0)ucs0_?JJk2+3^
z;iQ^Dn)cBnKn8(AR6&pbou{z^6q_eO!_xB`NKyYCFTA&|N}L|fhCCHB>5
z$q|c<%oD^-_+bSQE+a#7R_D?42sY`!nLNqz>vZYR*Uj=SyO`xfN=*4ML|uRWVB<)_
z^BJ7JQLitnD>M8oBGBc8p|M6x#R6?g`w%~DHWtW(XB;XCk&VY&NZ31$6-v~d*E)!U
z!1Tr;$}Ez9?P)y42PnRxO7EV0t<<%}^Yv1^H~
zt*Cu?D8nNQKNYrsItFWnc{ssAvKX8QwKceT5-l8H?TM(JKR3j;Mr$6za}v==eclNeHFv5{2Ak-IS^W@!m=e=^-&ct^cT)fuCDbL-!vO-%c|NsmDyI$8hCC
zsi9+);%Zuo`I{#5H*sOr9Qx*$L`Qo6lVTQrhEhUbuNL1Cga<>5Pl?ZN>3>d4Ud+q=
zHzA9Y2>VS*M}_?h{vASS|080aDC}Q6di!fxH(g?KZklNNg-K+lU-= 16'}
+
+ '@intlify/message-compiler@12.0.0-alpha.3':
+ resolution: {integrity: sha512-mDDTN3gfYOHhBnpnlby19UHyvMaOnzdlpsIrxUfs44R/vCATfn8pMOkE8PXD2t410xkocEj3FpDcC9XC/0v4Dg==}
+ engines: {node: '>= 16'}
+
+ '@intlify/shared@12.0.0-alpha.3':
+ resolution: {integrity: sha512-ryaNYBvxQjyJUmVuBBg+HHUsmGnfxcEUPR0NCeG4/K9N2qtyFE35C80S15IN6iYFE2MGWLN7HfOSyg0MXZIc9w==}
+ engines: {node: '>= 16'}
+
+ '@intlify/vue-i18n-core@12.0.0-alpha.3':
+ resolution: {integrity: sha512-YwAfTQILHN+VoK0P/Yv47GbKnEf1lhfbliyVyW3knAL1EmT8m0m3rwffXJnwyQhYw8Jpx85CpL49WkSgyi6d/g==}
+ engines: {node: '>= 16'}
+ peerDependencies:
+ vue: ^3.0.0
+
'@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22}
@@ -3957,6 +3978,13 @@ packages:
vue-flow-layout@0.2.0:
resolution: {integrity: sha512-zKgsWWkXq0xrus7H4Mc+uFs1ESrmdTXlO0YNbR6wMdPaFvosL3fMB8N7uTV308UhGy9UvTrGhIY7mVz9eN+L0Q==}
+ vue-i18n@12.0.0-alpha.3:
+ resolution: {integrity: sha512-+KQgD9LJoHfGCdJh3gaLdVS/Sps1n860+6wsjyeNLWJeEofjdVH7KPjz4rAeBlTAUaIDlIjHoXQY0Lk+8B6S9w==}
+ engines: {node: '>= 16'}
+ deprecated: This version is NOT deprecated. Previous deprecation was a mistake.
+ peerDependencies:
+ vue: ^3.0.0
+
vue-router@4.6.4:
resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
peerDependencies:
@@ -4690,6 +4718,25 @@ snapshots:
'@iconify/types': 2.0.0
mlly: 1.8.0
+ '@intlify/core-base@12.0.0-alpha.3':
+ dependencies:
+ '@intlify/message-compiler': 12.0.0-alpha.3
+ '@intlify/shared': 12.0.0-alpha.3
+
+ '@intlify/message-compiler@12.0.0-alpha.3':
+ dependencies:
+ '@intlify/shared': 12.0.0-alpha.3
+ source-map-js: 1.2.1
+
+ '@intlify/shared@12.0.0-alpha.3': {}
+
+ '@intlify/vue-i18n-core@12.0.0-alpha.3(vue@3.5.26(typescript@5.9.3))':
+ dependencies:
+ '@intlify/core-base': 12.0.0-alpha.3
+ '@intlify/shared': 12.0.0-alpha.3
+ '@vue/devtools-api': 6.6.4
+ vue: 3.5.26(typescript@5.9.3)
+
'@isaacs/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0':
@@ -8104,6 +8151,14 @@ snapshots:
vue-flow-layout@0.2.0: {}
+ vue-i18n@12.0.0-alpha.3(vue@3.5.26(typescript@5.9.3)):
+ dependencies:
+ '@intlify/core-base': 12.0.0-alpha.3
+ '@intlify/shared': 12.0.0-alpha.3
+ '@intlify/vue-i18n-core': 12.0.0-alpha.3(vue@3.5.26(typescript@5.9.3))
+ '@vue/devtools-api': 6.6.4
+ vue: 3.5.26(typescript@5.9.3)
+
vue-router@4.6.4(vue@3.5.26(typescript@5.9.3)):
dependencies:
'@vue/devtools-api': 6.6.4
diff --git a/src/main/db/data-source.ts b/src/main/db/data-source.ts
index ea260c6..2f216e7 100644
--- a/src/main/db/data-source.ts
+++ b/src/main/db/data-source.ts
@@ -5,6 +5,9 @@ import path from 'path'
import { ReadingReflectionTaskBatch } from './entities/ReadingReflectionTaskBatch'
import { ReadingReflectionTaskItem } from './entities/ReadingReflectionTaskItem'
import { ReadingPersona } from './entities/ReadingPersona' // 必须导入
+import { DatabaseConnectionError } from '@main/utils/errors/databaseError'
+import { ErrorHandler } from '@main/utils/errorHandler'
+import logger from '@shared/utils/logger'
const dbPath = app.isPackaged
? path.join(app.getPath('userData'), 'reflections.db')
@@ -26,9 +29,20 @@ export const initDB = async () => {
if (!AppDataSource.isInitialized) {
try {
await AppDataSource.initialize()
- console.log('Database initialized successfully at:', dbPath)
- } catch (err) {
- console.error('Error during Data Source initialization', err)
+ logger.info('DATABASE_INITIALIZED', {
+ message: 'Database initialized successfully',
+ path: dbPath
+ })
+ } catch (err: any) {
+ const error = new DatabaseConnectionError('数据库初始化失败', {
+ originalError: err.message,
+ stack: err.stack,
+ path: dbPath
+ }, {
+ cause: err
+ })
+ ErrorHandler.handleError(error)
+ throw error
}
}
return AppDataSource
diff --git a/src/main/db/entities/ReadingReflectionTaskBatch.ts b/src/main/db/entities/ReadingReflectionTaskBatch.ts
index edf1862..03b2616 100644
--- a/src/main/db/entities/ReadingReflectionTaskBatch.ts
+++ b/src/main/db/entities/ReadingReflectionTaskBatch.ts
@@ -30,6 +30,18 @@ export class ReadingReflectionTaskBatch implements IReadingReflectionTaskBatch {
@CreateDateColumn({ type: 'datetime' })
createdAt!: Date
+ /**
+ * 是否暂停
+ */
+ @Column({ type: 'boolean', default: false })
+ isPaused!: boolean
+
+ /**
+ * 暂停时间
+ */
+ @CreateDateColumn({ type: 'datetime', nullable: true })
+ pausedAt!: Date | null
+
@OneToMany(() => ReadingReflectionTaskItem, (item) => item.batch, {
cascade: true, // 级联操作:删除 Batch 时自动删除所有 Item
onDelete: 'CASCADE'
diff --git a/src/main/db/entities/ReadingReflectionTaskItem.ts b/src/main/db/entities/ReadingReflectionTaskItem.ts
index ed24ed5..dc74265 100644
--- a/src/main/db/entities/ReadingReflectionTaskItem.ts
+++ b/src/main/db/entities/ReadingReflectionTaskItem.ts
@@ -1,4 +1,4 @@
-import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryColumn } from 'typeorm'
+import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'
import { ReadingReflectionTaskBatch } from './ReadingReflectionTaskBatch'
import { IReadingReflectionTaskItem } from '@shared/types/IReadingReflectionTask'
@@ -8,7 +8,7 @@ export class ReadingReflectionTaskItem implements IReadingReflectionTaskItem {
id: string
@Column({ type: 'varchar' })
- status: 'PENDING' | 'WRITING' | 'COMPLETED' | 'FAILED'
+ status: 'PENDING' | 'WRITING' | 'COMPLETED' | 'FAILED' | 'PAUSED'
@Column({ type: 'int', default: 0 })
progress: number
@@ -28,8 +28,12 @@ export class ReadingReflectionTaskItem implements IReadingReflectionTaskItem {
@CreateDateColumn() // 增加这一行,TypeORM 会自动处理时间
createdAt: Date
+ @Column({ type: 'varchar' })
+ batchId: string
+
// 多对一关联
@ManyToOne(() => ReadingReflectionTaskBatch, (batch) => batch.items)
+ @JoinColumn({ name: 'batchId' })
batch!: ReadingReflectionTaskBatch
resultData: any
}
diff --git a/src/main/index.ts b/src/main/index.ts
index 856380e..b8c3caa 100644
--- a/src/main/index.ts
+++ b/src/main/index.ts
@@ -7,7 +7,13 @@ import { appRouter } from '@rpc/router'
import { createIPCHandler } from 'electron-trpc/main'
import { initDB } from '@main/db/data-source'
+// 声明全局变量类型
+declare global {
+ var mainWindow: BrowserWindow | null
+}
+
function createWindow(): void {
+ // 创建窗口
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
@@ -22,6 +28,9 @@ function createWindow(): void {
}
})
+ // 设置为全局变量,以便其他模块访问
+ global.mainWindow = mainWindow
+
// 核心绑定:使用 exposeElectronTRPC 适配当前窗口
createIPCHandler({
router: appRouter,
diff --git a/src/main/manager/readingReflectionsTaskManager.ts b/src/main/manager/readingReflectionsTaskManager.ts
index f5b7611..ff01062 100644
--- a/src/main/manager/readingReflectionsTaskManager.ts
+++ b/src/main/manager/readingReflectionsTaskManager.ts
@@ -1,52 +1,25 @@
-import { EventEmitter } from 'events'
-import pLimit from 'p-limit' // 建议使用 v2.2.0 以兼容 CJS
-import { readingReflectionGraph } from '@main/services/ai/graph/readingReflectionGraph'
+import { TaskStatusManager, readingReflectionTaskEvent } from './taskStatusManager'
+import { TaskExecutor } from './taskExecutor'
import { AppDataSource } from '@main/db/data-source'
import { ReadingReflectionTaskBatch } from '@main/db/entities/ReadingReflectionTaskBatch'
-import { ReadingReflectionTaskItem } from '@main/db/entities/ReadingReflectionTaskItem'
-import Store from 'electron-store'
-import { CONFIG_STORE_KEY } from '@rpc/constants/store_key'
-import { Notification } from 'electron'
-export const readingReflectionTaskEvent = new EventEmitter()
-
-// 兼容性处理获取 Store 构造函数
-const StoreClass = (Store as any).default || Store
-const store = new StoreClass({ encryptionKey: CONFIG_STORE_KEY })
-
-class TaskManager {
- private limit = pLimit(2)
+export class TaskManager {
+ private taskStatusManager: TaskStatusManager
+ private taskExecutor: TaskExecutor
private batchRepo = AppDataSource.getRepository(ReadingReflectionTaskBatch)
- private itemRepo = AppDataSource.getRepository(ReadingReflectionTaskItem)
- /**
- * 更新主任务汇总进度
- */
- private async updateBatchStatus(batchId: string) {
- const items = await this.itemRepo.find({ where: { batch: { id: batchId } } })
- if (items.length === 0) return
-
- const avgProgress = Math.round(items.reduce((acc, i) => acc + i.progress, 0) / items.length)
- let status = 'PROCESSING'
- if (avgProgress === 100) status = 'COMPLETED'
- if (items.every((i) => i.status === 'FAILED')) status = 'FAILED'
-
- await this.batchRepo.update(batchId, { progress: avgProgress, status })
-
- // 发送给左侧列表订阅者
- readingReflectionTaskEvent.emit('batchProgressUpdate', {
- batchId,
- progress: avgProgress,
- status
- })
+ constructor() {
+ this.taskStatusManager = new TaskStatusManager()
+ this.taskExecutor = new TaskExecutor(this.taskStatusManager)
}
- async startBatchTask(taskId: string, task: any) {
+ async startBatchTask(taskId: string, task: any): Promise {
const total = task.quantity || 1
// 1. 初始化主任务
const batch = this.batchRepo.create({ id: taskId, bookName: task.bookName, totalCount: total })
await this.batchRepo.save(batch)
+
// 发送给左侧列表订阅者
readingReflectionTaskEvent.emit('batchProgressUpdate', {
batchId: taskId,
@@ -54,119 +27,10 @@ class TaskManager {
status: 'PROCESSING'
})
- const promises = Array.from({ length: total }).map((_, index) => {
- const subTaskId = total === 1 ? taskId : `${taskId}-${index}`
-
- return this.limit(async () => {
- try {
- const item = this.itemRepo.create({ id: subTaskId, batch: batch, status: 'PENDING' })
- await this.itemRepo.save(item)
-
- const stream = await readingReflectionGraph.stream(
- { ...task },
- { configurable: { thread_id: subTaskId } }
- )
-
- let finalResult: any = {}
-
- for await (const chunk of stream) {
- // 处理生成正文节点
- if (chunk.generateReadingReflectionContent) {
- const contentData = chunk.generateReadingReflectionContent
- await this.itemRepo.update(subTaskId, {
- status: 'WRITING',
- progress: 50,
- content: contentData.content,
- title: contentData.title
- })
- finalResult = { ...finalResult, ...contentData }
- await this.updateBatchStatus(taskId)
- this.emitProgress(taskId, index, total, 60, '正文已生成...')
- }
-
- // 处理生成摘要节点
- if (chunk.generateReadingReflectionSummary) {
- const summaryData = chunk.generateReadingReflectionSummary
- finalResult = { ...finalResult, ...summaryData }
- await this.itemRepo.update(subTaskId, {
- status: 'COMPLETED',
- progress: 100,
- summary: summaryData.summary,
- title: finalResult.title,
- keywords: summaryData.keywords
- })
- }
- }
-
- await this.updateBatchStatus(taskId)
- this.emitProgress(taskId, index, total, 100, '生成成功', finalResult)
- } catch (error) {
- await this.itemRepo.update(subTaskId, { status: 'FAILED', progress: 0 })
- await this.updateBatchStatus(taskId)
- this.emitProgress(taskId, index, total, 0, '生成失败')
- }
- })
- })
-
- await Promise.all(promises)
- }
-
- private emitProgress(
- taskId: string,
- index: number,
- total: number,
- progress: number,
- status: string,
- result?: any
- ) {
- const displayId = total === 1 ? taskId : `${taskId}-${index}`
- //发送 tRPC 实时事件(驱动前端 UI 进度条)
- readingReflectionTaskEvent.emit('readingReflectionTaskProgress', {
- taskId: displayId,
- progress,
- status: status, // 传枚举 Key
- statusText: `[任务${index + 1}/${total}] ${status}`, // 传描述文字
- result
- })
- // 2. 添加任务状态通知判断
- this.handleNotification(status, progress, total, index)
- }
- /**
- * 内部私有方法:处理通知逻辑
- */
- private handleNotification(status: string, progress: number, total: number, index: number) {
- // 从 electron-store 获取用户偏好
- const config = store.get('notification') || {
- masterSwitch: true,
- taskCompleted: true,
- taskFailed: true
- }
-
- // 如果总开关关闭,直接拦截
- if (!config.masterSwitch) return
-
- // 场景 A: 任务全部完成 (100%)
- if (progress === 100 && config.taskCompleted) {
- // 只有当所有子任务都完成,或者当前是单任务时才弹出
- // 如果是批量任务,你可以选择在最后一个子任务完成时通知
- if (index + 1 === total) {
- new Notification({
- title: '🎉 读书心得已生成',
- body: total > 1 ? `共 ${total} 篇心得已全部处理完成。` : '您的书籍心得已准备就绪。',
- silent: config.silentMode
- }).show()
- }
- }
-
- // 场景 B: 任务失败 (假设你传入的 status 是 'FAILED')
- if (status === 'FAILED' && config.taskFailed) {
- new Notification({
- title: '❌ 任务生成失败',
- body: `第 ${index + 1} 项任务执行异常,请检查网络或 API 余额。`,
- silent: config.silentMode
- }).show()
- }
+ // 2. 执行子任务
+ await this.taskExecutor.startBatchTask(taskId, task)
}
}
-export const readingReflectionsTaskManager = new TaskManager()
+export { readingReflectionTaskEvent }
+export const readingReflectionsTaskManager = new TaskManager()
\ No newline at end of file
diff --git a/src/main/manager/taskExecutor.ts b/src/main/manager/taskExecutor.ts
new file mode 100644
index 0000000..eb86f47
--- /dev/null
+++ b/src/main/manager/taskExecutor.ts
@@ -0,0 +1,187 @@
+import { readingReflectionGraph } from '@main/services/ai/graph/readingReflectionGraph'
+import { AppDataSource } from '@main/db/data-source'
+import { ReadingReflectionTaskItem } from '@main/db/entities/ReadingReflectionTaskItem'
+import { ReadingReflectionTaskBatch } from '@main/db/entities/ReadingReflectionTaskBatch'
+import { TaskStatusManager } from './taskStatusManager'
+import { concurrencyManager } from '@main/utils/concurrencyManager'
+import { z } from 'zod'
+
+// 定义子任务执行的输入验证模式
+const ExecuteSubTaskSchema = z.object({
+ taskId: z.string(),
+ subTaskId: z.string(),
+ task: z.object({
+ bookName: z.string().min(1),
+ author: z.string().optional(),
+ description: z.string(),
+ occupation: z.enum(['student', 'teacher', 'professional', 'researcher', 'other']),
+ prompt: z.string(),
+ wordCount: z.number().default(1000),
+ quantity: z.number().min(1).max(5).default(1),
+ language: z.enum(['zh', 'en']).optional(),
+ tone: z.string().optional()
+ }),
+ index: z.number().min(0),
+ total: z.number().min(1)
+})
+
+export class TaskExecutor {
+ private itemRepo = AppDataSource.getRepository(ReadingReflectionTaskItem)
+ private batchRepo = AppDataSource.getRepository(ReadingReflectionTaskBatch)
+ private taskStatusManager: TaskStatusManager
+
+ constructor(taskStatusManager: TaskStatusManager) {
+ this.taskStatusManager = taskStatusManager
+ }
+
+ /**
+ * 检查批次是否被暂停
+ */
+ private async checkPauseStatus(batchId: string): Promise {
+ const batch = await this.batchRepo.findOneBy({ id: batchId })
+ return batch?.isPaused || false
+ }
+
+ /**
+ * 执行单个子任务
+ */
+ private async executeSubTask(
+ taskId: string,
+ subTaskId: string,
+ task: any,
+ index: number,
+ total: number
+ ): Promise {
+ // 验证输入参数
+ ExecuteSubTaskSchema.parse({
+ taskId,
+ subTaskId,
+ task,
+ index,
+ total
+ })
+ try {
+ // 检查任务是否已被暂停
+ if (await this.checkPauseStatus(taskId)) {
+ return
+ }
+
+ const item = this.itemRepo.create({ id: subTaskId, batchId: taskId, batch: { id: taskId }, status: 'PENDING' })
+ await this.itemRepo.save(item)
+
+ const stream = await readingReflectionGraph.stream(
+ { ...task },
+ { configurable: { thread_id: subTaskId } }
+ )
+
+ let finalResult: any = {}
+ let chunkCount = 0
+
+ for await (const chunk of stream) {
+ // 每处理10个chunk检查一次暂停状态
+ if (chunkCount % 10 === 0 && await this.checkPauseStatus(taskId)) {
+ await this.itemRepo.update(subTaskId, {
+ status: 'PAUSED',
+ progress: 0
+ })
+ await this.taskStatusManager.updateBatchStatus(taskId)
+ this.taskStatusManager.emitProgress(taskId, index, total, 0, '已暂停')
+ return
+ }
+ chunkCount++
+
+ // 处理生成正文节点
+ if (chunk.generateReadingReflectionContent) {
+ const contentData = chunk.generateReadingReflectionContent
+ await this.itemRepo.update(subTaskId, {
+ status: 'WRITING',
+ progress: 50,
+ content: contentData.content,
+ title: contentData.title
+ })
+ finalResult = { ...finalResult, ...contentData }
+ await this.taskStatusManager.updateBatchStatus(taskId)
+ this.taskStatusManager.emitProgress(taskId, index, total, 60, '正文已生成...')
+ }
+
+ // 处理生成摘要节点
+ if (chunk.generateReadingReflectionSummary) {
+ const summaryData = chunk.generateReadingReflectionSummary
+ finalResult = { ...finalResult, ...summaryData }
+ await this.itemRepo.update(subTaskId, {
+ status: 'COMPLETED',
+ progress: 100,
+ summary: summaryData.summary,
+ title: finalResult.title,
+ keywords: summaryData.keywords
+ })
+ }
+ }
+
+ await this.taskStatusManager.updateBatchStatus(taskId)
+ this.taskStatusManager.emitProgress(taskId, index, total, 100, '生成成功', finalResult)
+ } catch (error) {
+ // 检查是否是暂停导致的中断
+ if (await this.checkPauseStatus(taskId)) {
+ await this.itemRepo.update(subTaskId, {
+ status: 'PAUSED',
+ progress: 0
+ })
+ await this.taskStatusManager.updateBatchStatus(taskId)
+ this.taskStatusManager.emitProgress(taskId, index, total, 0, '已暂停')
+ } else {
+ await this.itemRepo.update(subTaskId, { status: 'FAILED', progress: 0 })
+ await this.taskStatusManager.updateBatchStatus(taskId)
+ this.taskStatusManager.emitProgress(taskId, index, total, 0, '生成失败')
+ }
+ }
+ }
+
+ /**
+ * 启动批量任务
+ */
+ async startBatchTask(taskId: string, task: any): Promise {
+ const total = task.quantity || 1
+
+ // 检查任务是否已被暂停
+ if (await this.checkPauseStatus(taskId)) {
+ return
+ }
+
+ // 根据任务数量动态调整并发数
+ if (total > 5) {
+ concurrencyManager.increaseConcurrency()
+ }
+
+ const limit = concurrencyManager.getLimit()
+
+ // 获取已存在的子任务,检查它们的状态
+ const existingItems = await this.itemRepo.find({ where: { batchId: taskId } })
+ const existingItemsMap = new Map(existingItems.map(item => [item.id, item]))
+
+ const promises = Array.from({ length: total }).map((_, index) => {
+ const subTaskId = total === 1 ? taskId : `${taskId}-${index}`
+
+ // 检查子任务是否已完成,如果已完成则跳过
+ const existingItem = existingItemsMap.get(subTaskId)
+ if (existingItem && existingItem.status === 'COMPLETED') {
+ return Promise.resolve()
+ }
+
+ return limit(async () => {
+ // 检查任务是否已被暂停,再执行子任务
+ if (await this.checkPauseStatus(taskId)) {
+ return
+ }
+ await this.executeSubTask(taskId, subTaskId, task, index, total)
+ })
+ })
+
+ try {
+ await Promise.all(promises)
+ } finally {
+ // 任务完成后重置并发数
+ concurrencyManager.resetConcurrency()
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/manager/taskStatusManager.ts b/src/main/manager/taskStatusManager.ts
new file mode 100644
index 0000000..36fb2b1
--- /dev/null
+++ b/src/main/manager/taskStatusManager.ts
@@ -0,0 +1,88 @@
+import { EventEmitter } from 'events'
+import { AppDataSource } from '@main/db/data-source'
+import { ReadingReflectionTaskBatch } from '@main/db/entities/ReadingReflectionTaskBatch'
+import { ReadingReflectionTaskItem } from '@main/db/entities/ReadingReflectionTaskItem'
+import { NotificationService } from '@main/services/notificationService'
+
+export const readingReflectionTaskEvent = new EventEmitter()
+
+export class TaskStatusManager {
+ private batchRepo = AppDataSource.getRepository(ReadingReflectionTaskBatch)
+ private itemRepo = AppDataSource.getRepository(ReadingReflectionTaskItem)
+ private notificationService: NotificationService
+
+ constructor() {
+ this.notificationService = new NotificationService()
+ }
+
+ /**
+ * 更新主任务汇总进度
+ */
+ async updateBatchStatus(batchId: string): Promise {
+ // 获取批次信息,包括暂停状态
+ const batch = await this.batchRepo.findOneBy({ id: batchId })
+ if (!batch) return
+
+ // 使用显式的batchId列进行查询,提高性能
+ const [itemsStats, failedCount] = await Promise.all([
+ this.itemRepo
+ .createQueryBuilder('item')
+ .select('AVG(item.progress)', 'avgProgress')
+ .addSelect('COUNT(*)', 'totalCount')
+ .where('item.batchId = :batchId', { batchId })
+ .getRawOne(),
+ this.itemRepo
+ .createQueryBuilder('item')
+ .select('COUNT(*)', 'failedCount')
+ .where('item.batchId = :batchId AND item.status = :status', { batchId, status: 'FAILED' })
+ .getRawOne()
+ ])
+
+ const totalCount = parseInt(itemsStats.totalCount, 10)
+ if (totalCount === 0) return
+
+ const avgProgress = Math.round(parseFloat(itemsStats.avgProgress || '0'))
+ let status = batch.status
+ if (!batch.isPaused) {
+ if (avgProgress === 100) status = 'COMPLETED'
+ if (parseInt(failedCount.failedCount, 10) === totalCount) status = 'FAILED'
+ if (avgProgress > 0 && avgProgress < 100) status = 'PROCESSING'
+ } else {
+ status = 'PAUSED'
+ }
+
+ await this.batchRepo.update(batchId, { progress: avgProgress, status })
+
+ // 发送给左侧列表订阅者
+ readingReflectionTaskEvent.emit('batchProgressUpdate', {
+ batchId,
+ progress: avgProgress,
+ status,
+ isPaused: batch.isPaused
+ })
+ }
+
+ /**
+ * 发送任务进度事件
+ */
+ emitProgress(
+ taskId: string,
+ index: number,
+ total: number,
+ progress: number,
+ status: string,
+ result?: any
+ ): void {
+ const displayId = total === 1 ? taskId : `${taskId}-${index}`
+ //发送 tRPC 实时事件(驱动前端 UI 进度条)
+ readingReflectionTaskEvent.emit('readingReflectionTaskProgress', {
+ taskId: displayId,
+ progress,
+ status: status, // 传枚举 Key
+ statusText: `[任务${index + 1}/${total}] ${status}`, // 传描述文字
+ result
+ })
+ // 添加任务状态通知判断
+ this.notificationService.handleNotification(status, progress, total, index, taskId)
+ }
+}
\ No newline at end of file
diff --git a/src/main/services/ai/llmService.ts b/src/main/services/ai/core/llmService.ts
similarity index 67%
rename from src/main/services/ai/llmService.ts
rename to src/main/services/ai/core/llmService.ts
index a7132f1..d6b1df0 100644
--- a/src/main/services/ai/llmService.ts
+++ b/src/main/services/ai/core/llmService.ts
@@ -1,12 +1,9 @@
import { ChatOpenAI } from '@langchain/openai'
-import Store from 'electron-store'
-import { CONFIG_STORE_KEY } from '@rpc/constants/store_key'
-
-const StoreClass = (Store as any).default || Store
-const store = new StoreClass({ encryptionKey: CONFIG_STORE_KEY })
+import { configService } from '@main/services/configService'
export const createChatModel = (type: 'reading' | 'summary', schema: any) => {
- const config = store.get(`chatModels.${type}`) as any
+ const chatModelsConfig = configService.getChatModelsConfig()
+ const config = chatModelsConfig?.[type] as any
console.log('chatModels', config)
if (!config || !config.apiKey) {
diff --git a/src/main/services/ai/nodes/readingReflectionContent.ts b/src/main/services/ai/nodes/readingReflectionContent.ts
index cf003f4..6eab9a3 100644
--- a/src/main/services/ai/nodes/readingReflectionContent.ts
+++ b/src/main/services/ai/nodes/readingReflectionContent.ts
@@ -4,7 +4,7 @@ import { REFLECTION_CONTENT_PROMPT } from '@main/services/ai/prompts/readingRefl
import { z } from 'zod'
import { AppDataSource } from '@main/db/data-source'
import { ReadingReflectionTaskItem } from '@main/db/entities/ReadingReflectionTaskItem'
-import { createChatModel } from '@main/services/ai/llmService'
+import { createChatModel } from '@main/services/ai/core/llmService'
export const generateReadingReflectionContentNode = async (
state: typeof ReadingReflectionState.State,
diff --git a/src/main/services/ai/nodes/readingReflectionSummary.ts b/src/main/services/ai/nodes/readingReflectionSummary.ts
index 71ab6a5..de1a65b 100644
--- a/src/main/services/ai/nodes/readingReflectionSummary.ts
+++ b/src/main/services/ai/nodes/readingReflectionSummary.ts
@@ -2,7 +2,7 @@ import { PromptTemplate } from '@langchain/core/prompts'
import { ReadingReflectionState } from '../state/readingReflectionState'
import { REFLECTION_SUMMARY_PROMPT } from '@main/services/ai/prompts/readingReflactionPrompts'
import { z } from 'zod'
-import { createChatModel } from '@main/services/ai/llmService'
+import { createChatModel } from '@main/services/ai/core/llmService'
/**
* 步骤 3:生成摘要和关键词
diff --git a/src/main/services/configService.ts b/src/main/services/configService.ts
new file mode 100644
index 0000000..3e1badf
--- /dev/null
+++ b/src/main/services/configService.ts
@@ -0,0 +1,173 @@
+import Store from 'electron-store'
+import { app } from 'electron'
+import path from 'path'
+import crypto from 'crypto'
+
+// 定义配置键名
+const NOTIFICATION_KEY = 'notification'
+const CHAT_MODELS_KEY = 'chatModels'
+const LANGUAGE_KEY = 'language'
+
+/**
+ * 配置服务,负责统一管理应用配置
+ */
+export class ConfigService {
+ private store: Store
+ private encryptionKey: string
+
+ constructor() {
+ // 生成或获取加密密钥
+ this.encryptionKey = this.getOrGenerateEncryptionKey()
+
+ // 兼容性处理获取 Store 构造函数
+ const StoreClass = (Store as any).default || Store
+
+ // 创建 Store 实例
+ this.store = new StoreClass({
+ encryptionKey: this.encryptionKey,
+ // 使用更安全的加密算法
+ schema: {
+ [NOTIFICATION_KEY]: {
+ type: 'object',
+ properties: {
+ masterSwitch: { type: 'boolean' },
+ taskCompleted: { type: 'boolean' },
+ taskFailed: { type: 'boolean' },
+ silentMode: { type: 'boolean' }
+ }
+ },
+ [CHAT_MODELS_KEY]: {
+ type: 'object',
+ properties: {
+ reading: { type: 'object' },
+ summary: { type: 'object' }
+ }
+ },
+ [LANGUAGE_KEY]: {
+ type: 'object',
+ properties: {
+ language: { type: 'string', enum: ['zh', 'en', 'ja', 'ko', 'es'] }
+ }
+ }
+ },
+ // 提高加密强度
+ encryptionAlgorithm: 'aes-256-cbc' as any
+ })
+ }
+
+ /**
+ * 生成或获取加密密钥
+ */
+ private getOrGenerateEncryptionKey(): string {
+ // 从安全位置获取或生成密钥
+ // 这里实现一个简单的密钥生成和存储机制
+ const keyPath = path.join(app.getPath('userData'), 'encryption.key')
+
+ try {
+ // 尝试从文件读取密钥
+ const fs = require('fs')
+ if (fs.existsSync(keyPath)) {
+ return fs.readFileSync(keyPath, 'utf8')
+ }
+
+ // 如果密钥不存在,生成一个新的随机密钥
+ const newKey = crypto.randomBytes(32).toString('hex')
+ fs.writeFileSync(keyPath, newKey, { mode: 0o600 }) // 设置文件权限,只有所有者可以读写
+ return newKey
+ } catch (error) {
+ // 如果出现错误,使用一个基于应用信息的密钥作为后备
+ console.error('Failed to generate or read encryption key:', error)
+ return this.generateFallbackKey()
+ }
+ }
+
+ /**
+ * 生成后备密钥
+ */
+ private generateFallbackKey(): string {
+ // 基于应用信息生成一个后备密钥
+ const appInfo = `${app.name}-${app.getVersion()}-${app.getPath('userData')}`
+ return crypto.createHash('sha256').update(appInfo).digest('hex')
+ }
+
+ /**
+ * 获取通知配置
+ */
+ getNotificationConfig(): any {
+ return this.store.get(NOTIFICATION_KEY) || {
+ masterSwitch: true,
+ taskCompleted: true,
+ taskFailed: true
+ }
+ }
+
+ /**
+ * 保存通知配置
+ */
+ saveNotificationConfig(config: any): void {
+ this.store.set(NOTIFICATION_KEY, config)
+ }
+
+ /**
+ * 获取聊天模型配置
+ */
+ getChatModelsConfig(): any {
+ const data = this.store.get(CHAT_MODELS_KEY) as { reading?: any; summary?: any } | null
+
+ // 检查是否包含必要的嵌套 Key,如果没有,说明是旧版本数据
+ if (data && typeof data === 'object' && !data.reading && !data.summary) {
+ console.log('检测到旧版本配置,正在重置...')
+ this.store.delete(CHAT_MODELS_KEY) // 删除旧的根键
+ return null
+ }
+
+ return data || null
+ }
+
+ /**
+ * 保存聊天模型配置
+ */
+ saveChatModelConfig(type: string, config: any): void {
+ this.store.set(`${CHAT_MODELS_KEY}.${type}`, config)
+ }
+
+ /**
+ * 获取指定键的配置
+ */
+ get(key: string): any {
+ return this.store.get(key)
+ }
+
+ /**
+ * 设置配置
+ */
+ set(key: string, value: any): void {
+ this.store.set(key, value)
+ }
+
+ /**
+ * 删除配置
+ */
+ delete(key: string): void {
+ this.store.delete(key)
+ }
+
+ /**
+ * 获取语言配置
+ */
+ getLanguageConfig(): any {
+ return this.store.get(LANGUAGE_KEY) || {
+ language: 'zh'
+ }
+ }
+
+ /**
+ * 保存语言配置
+ */
+ saveLanguageConfig(config: any): void {
+ this.store.set(LANGUAGE_KEY, config)
+ }
+}
+
+// 导出单例实例
+export const configService = new ConfigService()
\ No newline at end of file
diff --git a/src/main/services/notificationService.ts b/src/main/services/notificationService.ts
new file mode 100644
index 0000000..fd564ab
--- /dev/null
+++ b/src/main/services/notificationService.ts
@@ -0,0 +1,56 @@
+import { Notification } from 'electron'
+import { configService } from '@main/services/configService'
+import { notificationEventEmitter } from '@rpc/router/notice.router'
+
+export class NotificationService {
+ /**
+ * 处理通知逻辑
+ */
+ handleNotification(status: string, progress: number, total: number, index: number, batchId: string): void {
+ // 从配置服务获取用户偏好
+ const config = configService.getNotificationConfig()
+
+ // 如果总开关关闭,直接拦截
+ if (!config.masterSwitch) return
+
+ // 场景 A: 任务全部完成 (100%)
+ if (progress === 100 && config.taskCompleted) {
+ // 只有当所有子任务都完成,或者当前是单任务时才弹出
+ // 如果是批量任务,你可以选择在最后一个子任务完成时通知
+ if (index + 1 === total) {
+ const notification = new Notification({
+ title: '🎉 读书心得已生成',
+ body: total > 1 ? `共 ${total} 篇心得已全部处理完成。` : '您的书籍心得已准备就绪。',
+ silent: config.silentMode
+ })
+
+ notification.show()
+
+ // 监听点击事件
+ notification.on('click', () => {
+ notificationEventEmitter.emit('notification-click', {
+ batchId
+ })
+ })
+ }
+ }
+
+ // 场景 B: 任务失败 (假设你传入的 status 是 'FAILED')
+ if (status === 'FAILED' && config.taskFailed) {
+ const notification = new Notification({
+ title: '❌ 任务生成失败',
+ body: `第 ${index + 1} 项任务执行异常,请检查网络或 API 余额。`,
+ silent: config.silentMode
+ })
+
+ notification.show()
+
+ // 监听点击事件
+ notification.on('click', () => {
+ notificationEventEmitter.emit('notification-click', {
+ batchId
+ })
+ })
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/utils/concurrencyManager.ts b/src/main/utils/concurrencyManager.ts
new file mode 100644
index 0000000..58c06c2
--- /dev/null
+++ b/src/main/utils/concurrencyManager.ts
@@ -0,0 +1,61 @@
+import pLimit from 'p-limit' // 建议使用 v2.2.0 以兼容 CJS
+
+/**
+ * 并发管理器,负责动态调整并发数
+ */
+export class ConcurrencyManager {
+ private baseConcurrency: number
+ private currentConcurrency: number
+ private limit: ReturnType
+
+ constructor(baseConcurrency: number = 2) {
+ this.baseConcurrency = baseConcurrency
+ this.currentConcurrency = baseConcurrency
+ this.limit = pLimit(this.currentConcurrency)
+ }
+
+ /**
+ * 获取当前的并发限制器
+ */
+ getLimit() {
+ return this.limit
+ }
+
+ /**
+ * 增加并发数
+ */
+ increaseConcurrency(): void {
+ this.currentConcurrency++
+ this.limit = pLimit(this.currentConcurrency)
+ }
+
+ /**
+ * 减少并发数
+ */
+ decreaseConcurrency(): void {
+ if (this.currentConcurrency > 1) {
+ this.currentConcurrency--
+ this.limit = pLimit(this.currentConcurrency)
+ }
+ }
+
+ /**
+ * 重置并发数为基准值
+ */
+ resetConcurrency(): void {
+ this.currentConcurrency = this.baseConcurrency
+ this.limit = pLimit(this.currentConcurrency)
+ }
+
+ /**
+ * 根据系统资源动态调整并发数
+ */
+ adjustConcurrency(): void {
+ // 这里可以添加根据系统资源(如CPU、内存使用率)动态调整并发数的逻辑
+ // 目前实现一个简单的基于任务类型的调整策略
+ this.resetConcurrency()
+ }
+}
+
+// 导出一个单例实例
+export const concurrencyManager = new ConcurrencyManager()
\ No newline at end of file
diff --git a/src/main/utils/errorHandler.ts b/src/main/utils/errorHandler.ts
new file mode 100644
index 0000000..c884f58
--- /dev/null
+++ b/src/main/utils/errorHandler.ts
@@ -0,0 +1,76 @@
+import { BaseError } from './errors/baseError'
+import logger from '@shared/utils/logger'
+
+/**
+ * 错误处理工具类
+ */
+export class ErrorHandler {
+ /**
+ * 处理错误
+ */
+ static handleError(error: any): void {
+ if (error instanceof BaseError) {
+ // 处理自定义错误
+ this.handleCustomError(error)
+ } else {
+ // 处理原生错误
+ this.handleNativeError(error)
+ }
+ }
+
+ /**
+ * 处理自定义错误
+ */
+ private static handleCustomError(error: BaseError): void {
+ logger.error(error.code, {
+ message: error.message,
+ details: error.details,
+ stack: error.stack
+ })
+ }
+
+ /**
+ * 处理原生错误
+ */
+ private static handleNativeError(error: Error): void {
+ logger.error('UNHANDLED_ERROR', {
+ message: error.message,
+ stack: error.stack
+ })
+ }
+
+ /**
+ * 重试函数
+ */
+ static async retry(
+ fn: () => Promise,
+ maxAttempts: number = 3,
+ delay: number = 1000,
+ retryableErrors: Array Error> = []
+ ): Promise {
+ let attempts = 0
+
+ while (attempts < maxAttempts) {
+ try {
+ return await fn()
+ } catch (error: any) {
+ attempts++
+
+ // 检查是否可以重试
+ const isRetryable = retryableErrors.some(
+ (ErrorClass) => error instanceof ErrorClass
+ )
+
+ if (attempts >= maxAttempts || !isRetryable) {
+ throw error
+ }
+
+ // 等待一段时间后重试
+ await new Promise((resolve) => setTimeout(resolve, delay * Math.pow(2, attempts - 1)))
+ }
+ }
+
+ // 理论上不会执行到这里,因为上面的循环会抛出错误
+ throw new Error('重试次数已达上限')
+ }
+}
\ No newline at end of file
diff --git a/src/main/utils/errors/aiError.ts b/src/main/utils/errors/aiError.ts
new file mode 100644
index 0000000..33833d3
--- /dev/null
+++ b/src/main/utils/errors/aiError.ts
@@ -0,0 +1,46 @@
+import { BaseError } from './baseError'
+
+/**
+ * AI服务相关错误
+ */
+export class AIError extends BaseError {
+ constructor(message: string, details?: any, options?: ErrorOptions) {
+ super(message, 'AI_ERROR', details, options)
+ }
+}
+
+/**
+ * AI模型调用错误
+ */
+export class AIModelCallError extends AIError {
+ constructor(message: string = 'AI模型调用失败', details?: any, options?: ErrorOptions) {
+ super(message, details, options)
+ }
+}
+
+/**
+ * AI生成内容错误
+ */
+export class AIGenerationError extends AIError {
+ constructor(message: string = 'AI生成内容失败', details?: any, options?: ErrorOptions) {
+ super(message, details, options)
+ }
+}
+
+/**
+ * AI图执行错误
+ */
+export class AIGraphError extends AIError {
+ constructor(message: string = 'AI图执行失败', details?: any, options?: ErrorOptions) {
+ super(message, details, options)
+ }
+}
+
+/**
+ * AI提示词错误
+ */
+export class AIPromptError extends AIError {
+ constructor(message: string = 'AI提示词错误', details?: any, options?: ErrorOptions) {
+ super(message, details, options)
+ }
+}
\ No newline at end of file
diff --git a/src/main/utils/errors/baseError.ts b/src/main/utils/errors/baseError.ts
new file mode 100644
index 0000000..4cc6358
--- /dev/null
+++ b/src/main/utils/errors/baseError.ts
@@ -0,0 +1,25 @@
+/**
+ * 基础错误类,所有自定义错误的基类
+ */
+export class BaseError extends Error {
+ public readonly name: string
+ public readonly code: string
+ public readonly details?: any
+ public readonly timestamp: Date
+
+ constructor(
+ message: string,
+ code: string,
+ details?: any,
+ options?: ErrorOptions
+ ) {
+ super(message, options)
+ this.name = this.constructor.name
+ this.code = code
+ this.details = details
+ this.timestamp = new Date()
+
+ // 设置原型链,确保 instanceof 正常工作
+ Object.setPrototypeOf(this, new.target.prototype)
+ }
+}
\ No newline at end of file
diff --git a/src/main/utils/errors/databaseError.ts b/src/main/utils/errors/databaseError.ts
new file mode 100644
index 0000000..20029cd
--- /dev/null
+++ b/src/main/utils/errors/databaseError.ts
@@ -0,0 +1,46 @@
+import { BaseError } from './baseError'
+
+/**
+ * 数据库操作相关错误
+ */
+export class DatabaseError extends BaseError {
+ constructor(message: string, details?: any, options?: ErrorOptions) {
+ super(message, 'DATABASE_ERROR', details, options)
+ }
+}
+
+/**
+ * 数据库连接错误
+ */
+export class DatabaseConnectionError extends DatabaseError {
+ constructor(message: string = '数据库连接失败', details?: any, options?: ErrorOptions) {
+ super(message, details, options)
+ }
+}
+
+/**
+ * 数据库查询错误
+ */
+export class DatabaseQueryError extends DatabaseError {
+ constructor(message: string = '数据库查询失败', details?: any, options?: ErrorOptions) {
+ super(message, details, options)
+ }
+}
+
+/**
+ * 数据库更新错误
+ */
+export class DatabaseUpdateError extends DatabaseError {
+ constructor(message: string = '数据库更新失败', details?: any, options?: ErrorOptions) {
+ super(message, details, options)
+ }
+}
+
+/**
+ * 数据库插入错误
+ */
+export class DatabaseInsertError extends DatabaseError {
+ constructor(message: string = '数据库插入失败', details?: any, options?: ErrorOptions) {
+ super(message, details, options)
+ }
+}
\ No newline at end of file
diff --git a/src/renderer/components.d.ts b/src/renderer/components.d.ts
index 58a1ec5..9f610b1 100644
--- a/src/renderer/components.d.ts
+++ b/src/renderer/components.d.ts
@@ -23,6 +23,7 @@ declare module 'vue' {
AInputPassword: typeof import('@arco-design/web-vue')['InputPassword']
AModal: typeof import('@arco-design/web-vue')['Modal']
AOption: typeof import('@arco-design/web-vue')['Option']
+ ARadio: typeof import('@arco-design/web-vue')['Radio']
ASelect: typeof import('@arco-design/web-vue')['Select']
ASlider: typeof import('@arco-design/web-vue')['Slider']
ASwitch: typeof import('@arco-design/web-vue')['Switch']
diff --git a/src/renderer/src/App.vue b/src/renderer/src/App.vue
index 36a3753..dac356f 100644
--- a/src/renderer/src/App.vue
+++ b/src/renderer/src/App.vue
@@ -1,14 +1,29 @@
@@ -22,7 +37,7 @@ const goTaskCreatePage = () => {
Z
- 读书心得助手
+ {{ t('common.title') }}
diff --git a/src/renderer/src/common/taskStatus.ts b/src/renderer/src/common/taskStatus.ts
index e5001be..52863ae 100644
--- a/src/renderer/src/common/taskStatus.ts
+++ b/src/renderer/src/common/taskStatus.ts
@@ -1,6 +1,27 @@
-export const TASK_STATUS: Record = {
- PENDING: { color: '#7816ff', text: '任务排队中', desc: '任务排队中,等待系统调度算力...' },
- WRITING: { color: '#ff5722', text: '运行中', desc: '深度学习模型正在分析文本,请稍后...' },
- COMPLETED: { color: '#00b42a', text: '任务完成', desc: '任务已完成...' },
- FAILED: { color: '#86909c', text: '任务失败', desc: '任务失败,正在重试...' }
+export const TASK_STATUS = {
+ PENDING: {
+ color: '#7816ff',
+ text: 'task.list.status.pending',
+ desc: 'task.list.status.pendingDesc'
+ },
+ WRITING: {
+ color: '#ff5722',
+ text: 'task.list.status.processing',
+ desc: 'task.list.status.processingDesc'
+ },
+ COMPLETED: {
+ color: '#00b42a',
+ text: 'task.list.status.completed',
+ desc: 'task.list.status.completedDesc'
+ },
+ FAILED: {
+ color: '#86909c',
+ text: 'task.list.status.failed',
+ desc: 'task.list.status.failedDesc'
+ },
+ PAUSED: {
+ color: '#ff9800',
+ text: 'task.list.status.paused',
+ desc: 'task.list.status.pausedDesc'
+ }
}
diff --git a/src/renderer/src/locales/en.json b/src/renderer/src/locales/en.json
new file mode 100644
index 0000000..e8e374c
--- /dev/null
+++ b/src/renderer/src/locales/en.json
@@ -0,0 +1,282 @@
+{
+ "common": {
+ "title": "读书心得助手",
+ "loading": "Loading...",
+ "success": "Operation successful",
+ "error": "Operation failed",
+ "confirm": "Confirm",
+ "cancel": "Cancel",
+ "delete": "Delete",
+ "save": "Save",
+ "edit": "Edit",
+ "preview": "Preview",
+ "back": "Back",
+ "next": "Next",
+ "previous": "Previous"
+ },
+ "task": {
+ "list": {
+ "title": "Task List",
+ "create": "Create Task",
+ "total": "reflections",
+ "status": {
+ "pending": "Pending",
+ "pendingDesc": "Task queued, waiting for system resource scheduling...",
+ "processing": "Processing",
+ "processingDesc": "Deep learning model is analyzing text, please wait...",
+ "completed": "Completed",
+ "completedDesc": "Task has been completed...",
+ "failed": "Failed",
+ "failedDesc": "Task failed, retrying...",
+ "paused": "Paused",
+ "pausedDesc": "Task has been paused, click resume to continue..."
+ }
+ },
+ "index": {
+ "totalProgress": "Total Progress",
+ "pauseQueue": "Pause Queue",
+ "resumeQueue": "Resume Queue",
+ "packageResults": "Package Results",
+ "deleteTask": "Delete Task",
+ "confirmDelete": "Confirm Delete Task",
+ "deleteContent": "This operation will permanently delete all reflection records and generation progress for this book.",
+ "deleting": "Cleaning from disk...",
+ "deleteSuccess": "Task has been successfully deleted",
+ "deleteFailed": "Delete failed, please check database connection",
+ "previewResults": "Preview Results",
+ "pauseSuccess": "Task has been paused",
+ "pauseFailed": "Pause failed",
+ "resumeSuccess": "Task has been resumed",
+ "resumeFailed": "Resume failed"
+ },
+ "create": {
+ "title": "Create Reading Reflection",
+ "bookName": "Book Name",
+ "bookAuthor": "Book Author",
+ "bookDescription": "Book Description",
+ "occupation": "Target Audience Occupation",
+ "occupationList": {
+ "student": "Student",
+ "professional": "Professional",
+ "scholar": "Scholar/Researcher",
+ "freelancer": "Freelancer",
+ "teacher": "Teacher"
+ },
+ "prompt": "Custom Prompt",
+ "wordCount": "Word Count per Reflection",
+ "wordUnit": "words",
+ "quantity": "Number of Reflections",
+ "language": "Generation Language",
+ "tone": "Generation Tone",
+ "submit": "Start Task",
+ "success": "Task has been added to queue",
+ "error": {
+ "bookName": "Please enter book name"
+ },
+ "placeholder": {
+ "bookName": "Please enter complete book name...",
+ "description": "Briefly describe the book content to help AI extract more accurate key points...",
+ "prompt": "For example: Use Lu Xun's writing style, add 3 practical cases, target beginners..."
+ }
+ }
+ },
+ "reflection": {
+ "index": {
+ "copyContent": "Copy Content",
+ "exportDocument": "Export Document",
+ "sharePoster": "Share Poster",
+ "copySuccess": "Content has been successfully copied",
+ "summary": "Article Summary"
+ },
+ "export": {
+ "title": "Export Markdown",
+ "format": "Export Format",
+ "word": "Word Document",
+ "pdf": "PDF Document",
+ "export": "Confirm Export Report",
+ "step1": "Step 1: Select Export Style Template",
+ "step2": "Step 2: Complete Additional Information",
+ "step3": "Step 3: MD Source Preview",
+ "building": "Building...",
+ "selectTemplate": "Please select a template to start preview",
+ "noExtraInfo": "No additional information needed",
+ "success": "Markdown export successful! The directory has been opened",
+ "error": {
+ "missingParams": "Missing export parameters",
+ "initFailed": "Failed to initialize data",
+ "parseTemplateFailed": "Failed to parse template tags",
+ "exportFailed": "Export failed"
+ }
+ }
+ },
+ "about": {
+ "hero": {
+ "title1": "For every reader",
+ "title2": "enjoy the pleasure of reading and thinking",
+ "description": "We are committed to helping deep readers efficiently digest knowledge through cutting-edge AI technology. From massive text to structured insights, just one click.",
+ "startExperience": "Start Experience"
+ },
+ "coreValues": {
+ "dataDriven": "Data-Driven",
+ "dataDrivenDesc": "Based on large models, precisely extract the essence of each book.",
+ "extremeExperience": "Extreme Experience",
+ "extremeExperienceDesc": "Simplify complexity, make AI creation as natural and smooth as breathing.",
+ "connectFuture": "Connect Future",
+ "connectFutureDesc": "Explore new paradigms of human-computer collaboration, redefine reading and writing."
+ },
+ "middle": {
+ "title": "For those who truly love words",
+ "description": "In an era of information fragmentation, deep reading is becoming more luxurious than ever. We don't want AI to replace reading, but rather to serve as your \"digital pen pal\", helping you organize logic, capture inspiration, freeing you from tedious summarization work, and returning to thinking itself.",
+ "users": "Active Users",
+ "readings": "Reading Volume"
+ },
+ "footer": {
+ "copyright": "© 2026 AI Reader Studio. All Rights Reserved. Crafted with ❤️ for readers worldwide."
+ }
+ },
+ "faq": {
+ "hero": {
+ "title": "How can we help you?",
+ "searchPlaceholder": "Search for your question..."
+ },
+ "categories": {
+ "general": "General Questions",
+ "usage": "Usage Tips",
+ "billing": "Subscription & Payment",
+ "privacy": "Privacy & Security"
+ },
+ "emptyState": "No related questions found, please try other keywords",
+ "support": {
+ "title": "Still have questions?",
+ "responseTime": "Our team typically responds to your email within 2 hours",
+ "contact": "Contact Support"
+ },
+ "items": {
+ "general1": {
+ "q": "How does this AI reading tool work?",
+ "a": "We use deep learning models to perform semantic analysis on book texts. It not only summarizes the entire text but also extracts specific knowledge points based on your selected \"professional background\"."
+ },
+ "usage1": {
+ "q": "Can the generated content exceed 5000 words?",
+ "a": "Currently, the single generation limit is 5000 words to ensure logical coherence. If you need longer content, it is recommended to create tasks by chapter."
+ },
+ "billing1": {
+ "q": "Is there a subscription plan currently?",
+ "a": "The tool is currently free to use, no cost required, but you need to use your own large model API key"
+ },
+ "privacy1": {
+ "q": "How can I view my data?",
+ "a": "You can view your data in the settings page."
+ },
+ "usage2": {
+ "q": "How to set up the large model?",
+ "a": "You can set up reading notes and summary models separately in 『Settings Center』-『AI Model Configuration』.
It is recommended to use the free API provided by [Alibaba Cloud - iFlow Platform (iflow.cn)], which supports long text processing for single generation. If the generated content is close to the 5000-word limit, it is recommended to create tasks by chapter for optimal logical results.
You can refer to the following operation guide:
- Get API Key: Visit Alibaba Cloud iFlow Platform (iflow.cn).
After registering and logging in, create a new API Key in the backend. iFlow Platform currently provides stable free quota, which is very suitable for personal reading assistant use.- Configure interface address:
Enter in the Base URL field of application settings: https://apis.iflow.cn/v1.- Model selection:
The latest model ID provided by the platform.
"
+ }
+ }
+ },
+ "menus": {
+ "groups": {
+ "dataExploration": "Data Exploration",
+ "automation": "Automation & Export",
+ "lab": "Lab Features"
+ },
+ "items": {
+ "search": {
+ "title": "Global Search",
+ "desc": "Search all local notes in seconds"
+ },
+ "userPersona": {
+ "title": "Reading Profile",
+ "desc": "Visualize your knowledge boundaries and preferences"
+ },
+ "monitor": {
+ "title": "Book Library Monitoring",
+ "desc": "Automatically scan local folders for new books"
+ },
+ "tts": {
+ "title": "Audiobook Mode",
+ "desc": "Use system engine to read summaries aloud"
+ },
+ "model": {
+ "title": "Model Lab",
+ "desc": "Compare generation effects of different prompts"
+ }
+ }
+ },
+ "search": {
+ "title": "Global Search",
+ "placeholder": "Search book titles, notes, keywords...",
+ "loading": "Searching...",
+ "noResults": "No related notes found",
+ "enterKeyword": "Please enter keywords to start searching"
+ },
+ "userPersona": {
+ "backTitle": "Back to App Menu",
+ "syncing": "Syncing...",
+ "syncLatest": "Sync Latest Profile",
+ "stats": {
+ "totalNotes": "Total Notes Produced",
+ "tenThousandWords": "10k words",
+ "words": "words",
+ "keepGrowing": "Keep growing",
+ "deepBooks": "Deep Books Read",
+ "books": "books",
+ "deepReading": "Deep reading",
+ "focusTime": "Focus Growth Time",
+ "hours": "hours",
+ "focusTimeLabel": "Focus time"
+ },
+ "radar": {
+ "cognitiveDepth": "Cognitive Depth",
+ "productionEfficiency": "Production Efficiency",
+ "maturity": "Maturity",
+ "knowledgeBreadth": "Knowledge Breadth",
+ "languageAbility": "Language Ability"
+ },
+ "charts": {
+ "multidimensionalProfile": "Multidimensional Ability Profile",
+ "productionDensity": "Production Density (Last 7 Days)"
+ },
+ "contribution": {
+ "title": "Reading Contribution Footprint"
+ },
+ "report": {
+ "title": "Reading Dimension Report",
+ "analysis": "Based on your cumulative {totalWords} words of notes analysis: You have initially established a knowledge framework in the {domain} field.",
+ "exploration": "Exploration"
+ }
+ },
+
+
+
+ "setting": {
+ "title": "Settings Center",
+ "menu": {
+ "model": "AI Model Settings",
+ "account": "Account Security",
+ "billing": "Subscription & Billing",
+ "notification": "Notification Settings",
+ "language": "Language Settings"
+ },
+ "notification": {
+ "title": "Notification Management Center",
+ "description": "Stay updated with reading reflection generation progress to ensure every inspiration is delivered promptly.",
+ "taskStatus": "Task Status Notifications",
+ "realTime": "Real-time Push",
+ "taskCompleted": "Generation Success Alert",
+ "taskCompletedDesc": "Send desktop notifications when reading reflections and summaries are generated.",
+ "taskFailed": "Abnormal Interruption Alert",
+ "taskFailedDesc": "Notify when generation fails due to network fluctuations or insufficient API balance.",
+ "silentMode": "Silent Mode",
+ "silentModeDesc": "No system sound will play when notifications pop up.",
+ "tip": "Tip: Notification effects are affected by the operating system's 'Focus Mode' or 'Do Not Disturb Mode'. If you still don't receive notifications after enabling settings, please check your system's notification management permissions."
+ },
+ "masterSwitch": "Master Switch",
+ "taskCompleted": "Notify when task completed",
+ "taskFailed": "Notify when task failed",
+ "silentMode": "Silent Mode",
+ "language": "Language Settings",
+ "zh": "Chinese",
+ "en": "English"
+ }
+}
\ No newline at end of file
diff --git a/src/renderer/src/locales/es.json b/src/renderer/src/locales/es.json
new file mode 100644
index 0000000..33de62b
--- /dev/null
+++ b/src/renderer/src/locales/es.json
@@ -0,0 +1,282 @@
+{
+ "common": {
+ "title": "Asistente de Reflexiones de Lectura",
+ "loading": "Cargando...",
+ "success": "Operación exitosa",
+ "error": "Operación fallida",
+ "confirm": "Confirmar",
+ "cancel": "Cancelar",
+ "delete": "Eliminar",
+ "save": "Guardar",
+ "edit": "Editar",
+ "preview": "Vista previa",
+ "back": "Atrás",
+ "next": "Siguiente",
+ "previous": "Anterior"
+ },
+ "task": {
+ "list": {
+ "title": "Lista de Tareas",
+ "create": "Crear Tarea",
+ "total": "reflexiones",
+ "status": {
+ "pending": "En espera",
+ "pendingDesc": "Tarea en cola, esperando programación de recursos del sistema...",
+ "processing": "Procesando",
+ "processingDesc": "El modelo de aprendizaje profundo está analizando el texto, por favor espere...",
+ "completed": "Completada",
+ "completedDesc": "La tarea ha sido completada...",
+ "failed": "Fallida",
+ "failedDesc": "Tarea fallida, reintentando...",
+ "paused": "Pausada",
+ "pausedDesc": "La tarea ha sido pausada, haga clic en reanudar para continuar..."
+ }
+ },
+ "index": {
+ "totalProgress": "Progreso Total",
+ "pauseQueue": "Pausar Cola",
+ "resumeQueue": "Reanudar Cola",
+ "packageResults": "Empaquetar Resultados",
+ "deleteTask": "Eliminar Tarea",
+ "confirmDelete": "Confirmar Eliminación de Tarea",
+ "deleteContent": "Esta operación eliminará permanentemente todos los registros de reflexiones y progreso de generación para este libro.",
+ "deleting": "Limpiando del disco...",
+ "deleteSuccess": "La tarea ha sido eliminada exitosamente",
+ "deleteFailed": "Error al eliminar, por favor verifique la conexión a la base de datos",
+ "previewResults": "Vista Previa de Resultados",
+ "pauseSuccess": "La tarea ha sido pausada",
+ "pauseFailed": "Error al pausar",
+ "resumeSuccess": "La tarea ha sido reanudada",
+ "resumeFailed": "Error al reanudar"
+ },
+ "create": {
+ "title": "Crear Reflexión de Lectura",
+ "bookName": "Nombre del Libro",
+ "bookAuthor": "Autor del Libro",
+ "bookDescription": "Descripción del Libro",
+ "occupation": "Ocupación del Público Objetivo",
+ "occupationList": {
+ "student": "Estudiante",
+ "professional": "Profesional",
+ "scholar": "Investigador/Escolar",
+ "freelancer": "Freelance",
+ "teacher": "Profesor"
+ },
+ "prompt": "Prompt Personalizado",
+ "wordCount": "Número de Palabras por Reflexión",
+ "wordUnit": "palabras",
+ "quantity": "Número de Reflexiones",
+ "language": "Idioma de Generación",
+ "tone": "Tono de Generación",
+ "submit": "Iniciar Tarea",
+ "success": "La tarea ha sido agregada a la cola",
+ "error": {
+ "bookName": "Por favor ingrese el nombre del libro"
+ },
+ "placeholder": {
+ "bookName": "Por favor ingrese el nombre completo del libro...",
+ "description": "Describa brevemente el contenido del libro para ayudar a la IA a extraer puntos más precisos...",
+ "prompt": "Por ejemplo: Usar el estilo de Lu Xun, agregar 3 casos prácticos, dirigido a principiantes..."
+ }
+ }
+ },
+ "reflection": {
+ "index": {
+ "copyContent": "Copiar Contenido",
+ "exportDocument": "Exportar Documento",
+ "sharePoster": "Compartir Poster",
+ "copySuccess": "El contenido ha sido copiado exitosamente",
+ "summary": "Resumen del Artículo"
+ },
+ "export": {
+ "title": "Exportar Markdown",
+ "format": "Formato de Exportación",
+ "word": "Documento Word",
+ "pdf": "Documento PDF",
+ "export": "Confirmar Exportación del Informe",
+ "step1": "Paso 1: Seleccionar Plantilla de Estilo de Exportación",
+ "step2": "Paso 2: Completar Información Adicional",
+ "step3": "Paso 3: Vista Previa del Código Fuente MD",
+ "building": "Construyendo...",
+ "selectTemplate": "Por favor seleccione una plantilla para iniciar la vista previa",
+ "noExtraInfo": "No se requiere información adicional",
+ "success": "¡Exportación de Markdown exitosa! Se ha abierto el directorio",
+ "error": {
+ "missingParams": "Parámetros de exportación faltantes",
+ "initFailed": "Error al inicializar datos",
+ "parseTemplateFailed": "Error al analizar etiquetas de plantilla",
+ "exportFailed": "Error al exportar"
+ }
+ }
+ },
+ "about": {
+ "hero": {
+ "title1": "Para todos los lectores",
+ "title2": "disfrutar del placer de leer y pensar",
+ "description": "Nos dedicamos a ayudar a los lectores profundos a digerir conocimientos de manera eficiente a través de tecnología de IA de vanguardia. Desde texto masivo hasta insights estructurados, solo con un clic.",
+ "startExperience": "Iniciar Experiencia"
+ },
+ "coreValues": {
+ "dataDriven": "Impulsado por Datos",
+ "dataDrivenDesc": "Basado en modelos grandes, extrae con precisión la esencia de cada libro.",
+ "extremeExperience": "Experiencia Extrema",
+ "extremeExperienceDesc": "Simplifica la complejidad, hace que la creación con IA sea tan natural y fluida como respirar.",
+ "connectFuture": "Conectar el Futuro",
+ "connectFutureDesc": "Explora nuevos paradigmas de colaboración humano-computadora, redefine la lectura y la escritura."
+ },
+ "middle": {
+ "title": "Para aquellos que realmente aman las palabras",
+ "description": "En una era de fragmentación de la información, la lectura profunda se está volviendo más lujosa que nunca. No queremos que la IA reemplace la lectura, sino que sea su 'amigo digital', ayudándote a organizar la lógica, capturar inspiraciones y liberarte del tedioso trabajo de resumen para regresar al pensamiento mismo.",
+ "users": "Usuarios Activos",
+ "readings": "Volumen de Lectura"
+ },
+ "footer": {
+ "copyright": "© 2026 AI Reader Studio. Todos los derechos reservados. Creado con ❤️ para lectores de todo el mundo."
+ }
+ },
+ "faq": {
+ "hero": {
+ "title": "¿Cómo podemos ayudarte?",
+ "searchPlaceholder": "Busca tu pregunta..."
+ },
+ "categories": {
+ "general": "Preguntas Generales",
+ "usage": "Consejos de Uso",
+ "billing": "Suscripción y Pago",
+ "privacy": "Privacidad y Seguridad"
+ },
+ "emptyState": "No se encontraron preguntas relacionadas, por favor intenta con otras palabras clave",
+ "support": {
+ "title": "¿Todavía tienes preguntas?",
+ "responseTime": "Nuestro equipo generalmente responde a tu correo electrónico en menos de 2 horas",
+ "contact": "Contactar Soporte"
+ },
+ "items": {
+ "general1": {
+ "q": "¿Cómo funciona esta herramienta de lectura con IA?",
+ "a": "Utilizamos modelos de aprendizaje profundo para realizar análisis semántico en textos de libros. No solo resume todo el texto, sino que también extrae puntos de conocimiento específicos basados en tu 'contexto profesional' seleccionado."
+ },
+ "usage1": {
+ "q": "¿Puede el contenido generado exceder las 5000 palabras?",
+ "a": "Actualmente, el límite de generación individual es de 5000 palabras para garantizar coherencia lógica. Si necesitas contenido más largo, te recomendamos crear tareas por capítulos."
+ },
+ "billing1": {
+ "q": "¿Hay planes de suscripción actualmente?",
+ "a": "Esta herramienta está disponible gratuitamente en la actualidad. No tiene costo, pero necesitas usar tu propia clave de API de modelo grande"
+ },
+ "privacy1": {
+ "q": "¿Cómo puedo ver mis datos?",
+ "a": "Puedes ver tus datos en la página de configuración."
+ },
+ "usage2": {
+ "q": "¿Cómo configurar el modelo grande?",
+ "a": "Puedes configurar modelos de reflexiones de lectura y resumen por separado en 『Centro de Configuración』-『Configuración de Modelo Grande』.
Se recomienda usar la API gratuita proporcionada por [Alibaba Cloud - Plataforma iFlow (iflow.cn)], que admite procesamiento de texto largo en generación individual. Si el contenido generado está cerca del límite de 5000 palabras, se recomienda crear tareas por capítulos para obtener el mejor efecto lógico.
Puedes consultar la siguiente guía de操作:
- Obtener API Key: Visita Alibaba Cloud iFlow Platform (iflow.cn).
Regístrate e inicia sesión, luego crea una nueva API Key en el backend. La plataforma iFlow actualmente ofrece una cuota gratuita estable, muy adecuada para uso personal de asistente de lectura.- Configurar dirección de interfaz:
En el campo Base URL de la configuración de la aplicación, ingresa: https://apis.iflow.cn/v1.- Selección de modelo:
ID del modelo más reciente proporcionado por la plataforma.
"
+ }
+ }
+ },
+ "menus": {
+ "groups": {
+ "dataExploration": "Exploración de Datos",
+ "automation": "Automatización y Exportación",
+ "lab": "Funciones de Laboratorio"
+ },
+ "items": {
+ "search": {
+ "title": "Búsqueda Global",
+ "desc": "Busca todo el contenido de reflexiones locales en segundos"
+ },
+ "userPersona": {
+ "title": "Perfil de Lectura",
+ "desc": "Visualiza tus límites de conocimiento y preferencias"
+ },
+ "monitor": {
+ "title": "Monitoreo de Biblioteca",
+ "desc": "Escanea automáticamente carpetas locales en busca de nuevos libros"
+ },
+ "tts": {
+ "title": "Modo Audiolibro",
+ "desc": "Usa el motor del sistema para leer resúmenes en voz alta"
+ },
+ "model": {
+ "title": "Laboratorio de Modelos",
+ "desc": "Compara efectos de generación de diferentes prompts"
+ }
+ }
+ },
+ "search": {
+ "title": "Búsqueda Global",
+ "placeholder": "Busca títulos de libros, notas, palabras clave...",
+ "loading": "Buscando...",
+ "noResults": "No se encontraron notas relacionadas",
+ "enterKeyword": "Por favor ingrese palabras clave para comenzar a buscar"
+ },
+ "userPersona": {
+ "backTitle": "Volver al Menú de la App",
+ "syncing": "Sincronizando...",
+ "syncLatest": "Sincronizar Perfil Más Reciente",
+ "stats": {
+ "totalNotes": "Total de Notas Generadas",
+ "tenThousandWords": "10k palabras",
+ "words": "palabras",
+ "keepGrowing": "Seguir creciendo",
+ "deepBooks": "Libros Leídos Profundamente",
+ "books": "libros",
+ "deepReading": "Lectura profunda",
+ "focusTime": "Tiempo de Crecimiento Concentrado",
+ "hours": "horas",
+ "focusTimeLabel": "Tiempo de concentración"
+ },
+ "radar": {
+ "cognitiveDepth": "Profundidad Cognitiva",
+ "productionEfficiency": "Eficiencia de Producción",
+ "maturity": "Madurez",
+ "knowledgeBreadth": "Amplitud de Conocimiento",
+ "languageAbility": "Habilidad Lingüística"
+ },
+ "charts": {
+ "multidimensionalProfile": "Perfil de Habilidades Multidimensional",
+ "productionDensity": "Densidad de Producción (Últimos 7 Días)"
+ },
+ "contribution": {
+ "title": "Huella de Contribución de Lectura"
+ },
+ "report": {
+ "title": "Informe de Dimensión de Lectura",
+ "analysis": "Basado en el análisis de tus {totalWords} palabras de notas acumuladas: Has establecido inicialmente un marco de conocimiento en el campo de {domain}.",
+ "exploration": "Exploración"
+ }
+ },
+ "setting": {
+ "title": "Centro de Configuración",
+ "menu": {
+ "model": "Configuración de Modelo de IA",
+ "account": "Seguridad de Cuenta",
+ "billing": "Suscripción y Facturación",
+ "notification": "Configuración de Notificaciones",
+ "language": "Configuración de Idioma"
+ },
+ "notification": {
+ "title": "Centro de Gestión de Notificaciones",
+ "description": "Mantente actualizado con el progreso de generación de reflexiones de lectura para garantizar que cada inspiración se entregue a tiempo.",
+ "taskStatus": "Notificaciones de Estado de Tarea",
+ "realTime": "Push en Tiempo Real",
+ "taskCompleted": "Alerta de Éxito en Generación",
+ "taskCompletedDesc": "Envía notificaciones de escritorio cuando se generen reflexiones y resúmenes de lectura.",
+ "taskFailed": "Alerta de Interrupción Anormal",
+ "taskFailedDesc": "Notifica cuando la generación falle debido a fluctuaciones de red o saldo insuficiente de API.",
+ "silentMode": "Modo Silencioso",
+ "silentModeDesc": "No se reproducirá sonido del sistema cuando aparezcan notificaciones.",
+ "tip": "Consejo: Los efectos de notificación se ven afectados por el 'Modo de Enfoque' o 'Modo No Molestar' del sistema operativo. Si aún no recibes notificaciones después de habilitar la configuración, verifica los permisos de gestión de notificaciones de tu sistema."
+ },
+ "masterSwitch": "Interruptor Principal",
+ "taskCompleted": "Notificar cuando la tarea se complete",
+ "taskFailed": "Notificar cuando la tarea falle",
+ "silentMode": "Modo Silencioso",
+ "language": "Configuración de Idioma",
+ "zh": "Chino",
+ "en": "Inglés",
+ "ja": "Japonés",
+ "ko": "Coreano",
+ "es": "Español"
+ }
+}
\ No newline at end of file
diff --git a/src/renderer/src/locales/ja.json b/src/renderer/src/locales/ja.json
new file mode 100644
index 0000000..e009391
--- /dev/null
+++ b/src/renderer/src/locales/ja.json
@@ -0,0 +1,277 @@
+{
+ "common": {
+ "title": "読書レフレクションアシスタント",
+ "loading": "読み込み中...",
+ "success": "操作が成功しました",
+ "error": "操作に失敗しました",
+ "confirm": "確認",
+ "cancel": "キャンセル",
+ "delete": "削除",
+ "save": "保存",
+ "edit": "編集",
+ "preview": "プレビュー",
+ "back": "戻る",
+ "next": "次へ",
+ "previous": "前へ"
+ },
+ "task": {
+ "list": {
+ "title": "タスクリスト",
+ "create": "タスクを作成",
+ "total": "レフレクション",
+ "status": {
+ "pending": "キュー待ち",
+ "processing": "処理中",
+ "completed": "完了",
+ "failed": "失敗",
+ "paused": "一時停止"
+ }
+ },
+ "index": {
+ "totalProgress": "総進捗",
+ "pauseQueue": "キューを一時停止",
+ "resumeQueue": "キューを再開",
+ "packageResults": "成果をパッケージ化",
+ "deleteTask": "タスクを削除",
+ "confirmDelete": "タスクの削除を確認",
+ "deleteContent": "この操作により、この本のすべてのレフレクション記録と生成進捗が永久に削除されます。",
+ "deleting": "ディスクからクリーニング中...",
+ "deleteSuccess": "タスクが正常に削除されました",
+ "deleteFailed": "削除に失敗しました。データベース接続を確認してください",
+ "previewResults": "成果をプレビュー",
+ "pauseSuccess": "タスクが一時停止されました",
+ "pauseFailed": "一時停止に失敗しました",
+ "resumeSuccess": "タスクが再開されました",
+ "resumeFailed": "再開に失敗しました"
+ },
+ "create": {
+ "title": "読書レフレクションを作成",
+ "bookName": "本の名前",
+ "bookAuthor": "著者",
+ "bookDescription": "本の説明",
+ "occupation": "対象読者の職業",
+ "occupationList": {
+ "student": "学生",
+ "professional": "プロフェッショナル",
+ "scholar": "学者/研究者",
+ "freelancer": "フリーランサー",
+ "teacher": "教師"
+ },
+ "prompt": "カスタムプロンプト",
+ "wordCount": "レフレクションの単語数",
+ "wordUnit": "文字",
+ "quantity": "レフレクションの数",
+ "language": "生成言語",
+ "tone": "生成トーン",
+ "submit": "タスクを開始",
+ "success": "タスクがキューに追加されました",
+ "error": {
+ "bookName": "本の名前を入力してください"
+ },
+ "placeholder": {
+ "bookName": "本の完全な名前を入力してください...",
+ "description": "AIがより正確なポイントを抽出できるように、本の内容を簡潔に説明してください...",
+ "prompt": "例:魯迅の文体を使用、3つの実践例を追加、初心者向け..."
+ }
+ }
+ },
+ "reflection": {
+ "index": {
+ "copyContent": "内容をコピー",
+ "exportDocument": "ドキュメントをエクスポート",
+ "sharePoster": "ポスターを共有",
+ "copySuccess": "内容が正常にコピーされました",
+ "summary": "記事の要約"
+ },
+ "export": {
+ "title": "Markdownをエクスポート",
+ "format": "エクスポート形式",
+ "word": "Word文書",
+ "pdf": "PDF文書",
+ "export": "レポートのエクスポートを確認",
+ "step1": "ステップ1:エクスポートスタイルテンプレートを選択",
+ "step2": "ステップ2:追加情報を入力",
+ "step3": "ステップ3:MDソースのプレビュー",
+ "building": "構築中...",
+ "selectTemplate": "プレビューを開始するにはテンプレートを選択してください",
+ "noExtraInfo": "追加情報は不要です",
+ "success": "Markdownのエクスポートに成功しました!保存先フォルダが開かれました",
+ "error": {
+ "missingParams": "エクスポートパラメータが不足しています",
+ "initFailed": "データの初期化に失敗しました",
+ "parseTemplateFailed": "テンプレートタグの解析に失敗しました",
+ "exportFailed": "エクスポートに失敗しました"
+ }
+ }
+ },
+ "about": {
+ "hero": {
+ "title1": "すべての読者に",
+ "title2": "読書と思考の喜びを",
+ "description": "私たちは最先端のAI技術を通じて、深読みする人々が知識を効率的に消化するのを支援することに専念しています。大量のテキストから構造化された洞察まで、たった一つのクリックで。",
+ "startExperience": "体験を開始"
+ },
+ "coreValues": {
+ "dataDriven": "データ主導",
+ "dataDrivenDesc": "大規模モデルに基づいて、各本の本質を正確に抽出します。",
+ "extremeExperience": "極限の体験",
+ "extremeExperienceDesc": "複雑さを単純化し、AIによる創作を呼吸のように自然で滑らかなものにします。",
+ "connectFuture": "未来との接続",
+ "connectFutureDesc": "人間とコンピューターの協力の新しいパラダイムを探索し、読書と執筆を再定義します。"
+ },
+ "middle": {
+ "title": "本当に言葉を愛する人々のために",
+ "description": "情報の断片化が進む時代において、深読みはかつてないほど贅沢なものとなっています。私たちはAIに読書を置き換えてほしいのではなく、あなたの「デジタルペンフレンド」として、論理を整理し、インスピレーションを捉え、退屈な要約作業から解放し、思考そのものに戻るお手伝いをしたいのです。",
+ "users": "アクティブユーザー",
+ "readings": "読書量"
+ },
+ "footer": {
+ "copyright": "© 2026 AI Reader Studio. All Rights Reserved. Crafted with ❤️ for readers worldwide."
+ }
+ },
+ "faq": {
+ "hero": {
+ "title": "どのようにお手伝いできますか?",
+ "searchPlaceholder": "質問を検索..."
+ },
+ "categories": {
+ "general": "一般的な質問",
+ "usage": "使用のヒント",
+ "billing": "サブスクリプションと支払い",
+ "privacy": "プライバシーとセキュリティ"
+ },
+ "emptyState": "関連する質問が見つかりませんでした。他のキーワードを試してください",
+ "support": {
+ "title": "まだ質問がありますか?",
+ "responseTime": "私たちのチームは通常、2時間以内にメールに返信します",
+ "contact": "サポートに問い合わせ"
+ },
+ "items": {
+ "general1": {
+ "q": "このAI読書ツールはどのように機能しますか?",
+ "a": "私たちはディープラーニングモデルを使用して、書籍のテキストに対して意味的分析を実行します。全文を要約するだけでなく、選択した「専門的な背景」に基づいて特定の知識ポイントを抽出することもできます。"
+ },
+ "usage1": {
+ "q": "生成されたコンテンツは5000語を超えることができますか?",
+ "a": "現在、論理的な一貫性を確保するために、単回生成の制限は5000語です。より長いコンテンツが必要な場合は、章ごとにタスクを作成することをお勧めします。"
+ },
+ "billing1": {
+ "q": "現在サブスクリプションプランはありますか?",
+ "a": "このツールは現在無料で使用できます。費用はかかりませんが、独自の大規模モデルAPIキーを使用する必要があります"
+ },
+ "privacy1": {
+ "q": "データを表示するにはどうすればよいですか?",
+ "a": "設定ページでデータを表示できます。"
+ },
+ "usage2": {
+ "q": "大規模モデルを設定するにはどうすればよいですか?",
+ "a": "『設定センター』-『大規模モデル設定』で、読書レフレクションと要約モデルをそれぞれ設定できます。
無料APIを提供する【アリババクラウド-イフロープラットフォーム (iflow.cn)】の使用を推奨します。単回生成で長いテキスト処理をサポートしています。生成コンテンツが5000語の上限に近い場合は、最適な論理効果を得るために章ごとにタスクを作成することをお勧めします。
以下の操作ガイドを参照してください:
- APIキーの取得:アリババクラウドイフロープラットフォーム (iflow.cn)にアクセスしてください。
登録してログインした後、バックエンドで新しいAPIキーを作成します。イフロープラットフォームは現在、個人の読書アシスタントに非常に適した安定した無料枠を提供しています。- インターフェースアドレスの設定:
アプリケーション設定のBase URLに次のURLを入力します:https://apis.iflow.cn/v1。- モデルの選択:
プラットフォームが提供する最新のモデルID。
"
+ }
+ }
+ },
+ "menus": {
+ "groups": {
+ "dataExploration": "データ探索",
+ "automation": "自動化とエクスポート",
+ "lab": "ラボ機能"
+ },
+ "items": {
+ "search": {
+ "title": "グローバル検索",
+ "desc": "すべてのローカルレフレクションコンテンツを秒単位で検索"
+ },
+ "userPersona": {
+ "title": "読書プロファイル",
+ "desc": "知識の境界と嗜好を可視化"
+ },
+ "monitor": {
+ "title": "ブックライブラリモニタリング",
+ "desc": "ローカルフォルダの新しい本を自動的にスキャン"
+ },
+ "tts": {
+ "title": "オーディオブックモード",
+ "desc": "システムエンジンを使用して要約を音声で読み上げ"
+ },
+ "model": {
+ "title": "モデルラボ",
+ "desc": "異なるプロンプトの生成効果を比較"
+ }
+ }
+ },
+ "search": {
+ "title": "グローバル検索",
+ "placeholder": "本のタイトル、メモ、キーワードを検索...",
+ "loading": "検索中...",
+ "noResults": "関連するメモが見つかりません",
+ "enterKeyword": "検索を開始するにはキーワードを入力してください"
+ },
+ "userPersona": {
+ "backTitle": "アプリメニューに戻る",
+ "syncing": "同期中...",
+ "syncLatest": "最新のプロファイルを同期",
+ "stats": {
+ "totalNotes": "生成されたレフレクションの合計",
+ "tenThousandWords": "万字",
+ "words": "文字",
+ "keepGrowing": "成長し続ける",
+ "deepBooks": "深く読んだ本",
+ "books": "冊",
+ "deepReading": "深読み",
+ "focusTime": "集中成長時間",
+ "hours": "時間",
+ "focusTimeLabel": "集中時間"
+ },
+ "radar": {
+ "cognitiveDepth": "認知の深さ",
+ "productionEfficiency": "生産効率",
+ "maturity": "成熟度",
+ "knowledgeBreadth": "知識の幅",
+ "languageAbility": "言語能力"
+ },
+ "charts": {
+ "multidimensionalProfile": "多次元能力プロファイル",
+ "productionDensity": "生産密度(過去7日間)"
+ },
+ "contribution": {
+ "title": "読書貢献の足跡"
+ },
+ "report": {
+ "title": "読書次元レポート",
+ "analysis": "累計 {totalWords} 文字のレフレクション分析に基づく:{domain} 分野で知識の枠組みが初期的に確立されています。",
+ "exploration": "探索"
+ }
+ },
+ "setting": {
+ "title": "設定センター",
+ "menu": {
+ "model": "大規模モデル設定",
+ "account": "アカウントセキュリティ",
+ "billing": "サブスクリプションと請求",
+ "notification": "通知設定",
+ "language": "言語設定"
+ },
+ "notification": {
+ "title": "通知管理センター",
+ "description": "読書レフレクションの生成進捗をリアルタイムで把握し、あらゆるインスピレーションがタイムリーに届くようにします。",
+ "taskStatus": "タスクステータス通知",
+ "realTime": "リアルタイムプッシュ",
+ "taskCompleted": "生成成功アラート",
+ "taskCompletedDesc": "読書レフレクションと要約が生成されたときにデスクトップ通知を送信します。",
+ "taskFailed": "異常中断アラート",
+ "taskFailedDesc": "ネットワークの変動やAPI残高不足による生成失敗時に通知します。",
+ "silentMode": "サイレントモード",
+ "silentModeDesc": "通知がポップアップするときにシステム音を再生しません。",
+ "tip": "ヒント:通知の効果は、オペレーティングシステムの「フォーカスモード」または「通知停止モード」の影響を受けます。設定を有効にした後も通知を受け取らない場合は、システムの通知管理権限を確認してください。"
+ },
+ "masterSwitch": "マスタースイッチ",
+ "taskCompleted": "タスク完了時に通知",
+ "taskFailed": "タスク失敗時に通知",
+ "silentMode": "サイレントモード",
+ "language": "言語設定",
+ "zh": "中国語",
+ "en": "英語",
+ "ja": "日本語",
+ "ko": "韓国語",
+ "es": "スペイン語"
+ }
+}
\ No newline at end of file
diff --git a/src/renderer/src/locales/ko.json b/src/renderer/src/locales/ko.json
new file mode 100644
index 0000000..f8af61c
--- /dev/null
+++ b/src/renderer/src/locales/ko.json
@@ -0,0 +1,282 @@
+{
+ "common": {
+ "title": "독서 반성 어시스턴트",
+ "loading": "로딩 중...",
+ "success": "작업이 성공했습니다",
+ "error": "작업이 실패했습니다",
+ "confirm": "확인",
+ "cancel": "취소",
+ "delete": "삭제",
+ "save": "저장",
+ "edit": "편집",
+ "preview": "미리보기",
+ "back": "뒤로",
+ "next": "다음",
+ "previous": "이전"
+ },
+ "task": {
+ "list": {
+ "title": "작업 목록",
+ "create": "작업 생성",
+ "total": "반성",
+ "status": {
+ "pending": "대기 중",
+ "pendingDesc": "작업이 대기 중이며, 시스템 자원 스케줄링을 기다리는 중...",
+ "processing": "처리 중",
+ "processingDesc": "딥러닝 모델이 텍스트를 분석 중입니다. 잠시만 기다려 주세요...",
+ "completed": "완료",
+ "completedDesc": "작업이 완료되었습니다...",
+ "failed": "실패",
+ "failedDesc": "작업이 실패했습니다. 재시도 중...",
+ "paused": "일시 중지",
+ "pausedDesc": "작업이 일시 중지되었습니다. 계속하려면 재개를 클릭하세요..."
+ }
+ },
+ "index": {
+ "totalProgress": "총 진행 상황",
+ "pauseQueue": "큐 일시 중지",
+ "resumeQueue": "큐 재개",
+ "packageResults": "결과 패키징",
+ "deleteTask": "작업 삭제",
+ "confirmDelete": "작업 삭제 확인",
+ "deleteContent": "이 작업을 수행하면 이 책에 대한 모든 반성 기록과 생성 진행 상황이 영구적으로 삭제됩니다.",
+ "deleting": "디스크에서 정리 중...",
+ "deleteSuccess": "작업이 성공적으로 삭제되었습니다",
+ "deleteFailed": "삭제에 실패했습니다. 데이터베이스 연결을 확인하세요",
+ "previewResults": "결과 미리보기",
+ "pauseSuccess": "작업이 일시 중지되었습니다",
+ "pauseFailed": "일시 중지에 실패했습니다",
+ "resumeSuccess": "작업이 재개되었습니다",
+ "resumeFailed": "재개에 실패했습니다"
+ },
+ "create": {
+ "title": "독서 반성 만들기",
+ "bookName": "책 이름",
+ "bookAuthor": "저자",
+ "bookDescription": "책 설명",
+ "occupation": "대상 독자 직업",
+ "occupationList": {
+ "student": "학생",
+ "professional": "전문가",
+ "scholar": "학자/연구원",
+ "freelancer": "프리랜서",
+ "teacher": "교사"
+ },
+ "prompt": "사용자 지정 프롬프트",
+ "wordCount": "반성 당 단어 수",
+ "wordUnit": "글자",
+ "quantity": "반성 수",
+ "language": "생성 언어",
+ "tone": "생성 톤",
+ "submit": "작업 시작",
+ "success": "작업이 큐에 추가되었습니다",
+ "error": {
+ "bookName": "책 이름을 입력하세요"
+ },
+ "placeholder": {
+ "bookName": "책의 전체 이름을 입력하세요...",
+ "description": "AI가 보다 정확한 포인트를 추출할 수 있도록 책 내용을 간략히 설명하세요...",
+ "prompt": "예: 루쉰의 문체 사용, 3개의 실践 예제 추가, 초보자를 대상으로..."
+ }
+ }
+ },
+ "reflection": {
+ "index": {
+ "copyContent": "내용 복사",
+ "exportDocument": "문서 내보내기",
+ "sharePoster": "포스터 공유",
+ "copySuccess": "내용이 성공적으로 복사되었습니다",
+ "summary": "기사 요약"
+ },
+ "export": {
+ "title": "Markdown 내보내기",
+ "format": "내보내기 형식",
+ "word": "Word 문서",
+ "pdf": "PDF 문서",
+ "export": "보고서 내보내기 확인",
+ "step1": "단계 1: 내보내기 스타일 템플릿 선택",
+ "step2": "단계 2: 추가 정보 입력",
+ "step3": "단계 3: MD 소스 미리보기",
+ "building": "구축 중...",
+ "selectTemplate": "미리보기를 시작하려면 템플릿을 선택하세요",
+ "noExtraInfo": "추가 정보가 필요하지 않습니다",
+ "success": "Markdown 내보내기 성공! 저장된 디렉토리가 열렸습니다",
+ "error": {
+ "missingParams": "내보내기 매개변수가 누락되었습니다",
+ "initFailed": "데이터 초기화에 실패했습니다",
+ "parseTemplateFailed": "템플릿 태그 구문 분석에 실패했습니다",
+ "exportFailed": "내보내기에 실패했습니다"
+ }
+ }
+ },
+ "about": {
+ "hero": {
+ "title1": "모든 독자에게",
+ "title2": "독서와 사고의 즐거움을",
+ "description": "우리는 최첨단 AI 기술을 통해 심층 독자들이 지식을 효율적으로 소화하도록 돕기 위해 전념하고 있습니다. 방대한 텍스트에서 구조화된 통찰까지, 단 한 번의 클릭으로 이루어집니다.",
+ "startExperience": "경험 시작"
+ },
+ "coreValues": {
+ "dataDriven": "데이터 기반",
+ "dataDrivenDesc": "대규모 모델을 기반으로 각 책의 본질을 정확히 추출합니다.",
+ "extremeExperience": "극한 경험",
+ "extremeExperienceDesc": "복잡성을 단순화하여 AI 생성을 호흡처럼 자연스럽고 원활하게 만듭니다.",
+ "connectFuture": "미래 연결",
+ "connectFutureDesc": "인간-컴퓨터 협력의 새로운 패러다임을 탐구하여 독서와 쓰기를 재정의합니다."
+ },
+ "middle": {
+ "title": "진정으로 단어를 사랑하는 사람들을 위해",
+ "description": "정보 단편화 시대에 심층 독서는 그 어느 때보다 사치스러운 일이 되고 있습니다. 우리는 AI가 독서를 대체하기를 원하는 것이 아니라, 논리를 정리하고 영감을 포착하여 지루한 요약 작업에서 자유롭게 해주는 '디지털 펜팔'로서 여러분을 돕고자 합니다.",
+ "users": "활성 사용자",
+ "readings": "독서량"
+ },
+ "footer": {
+ "copyright": "© 2026 AI Reader Studio. All Rights Reserved. Crafted with ❤️ for readers worldwide."
+ }
+ },
+ "faq": {
+ "hero": {
+ "title": "어떻게 도와드릴까요?",
+ "searchPlaceholder": "질문을 검색하세요..."
+ },
+ "categories": {
+ "general": "일반 질문",
+ "usage": "사용 팁",
+ "billing": "구독 및 결제",
+ "privacy": "개인정보 보호 및 보안"
+ },
+ "emptyState": "관련 질문을 찾을 수 없습니다. 다른 키워드를 시도하세요",
+ "support": {
+ "title": "아직 질문이 있으신가요?",
+ "responseTime": "저희 팀은 일반적으로 2시간 이내에 이메일에 응답합니다",
+ "contact": "지원 문의"
+ },
+ "items": {
+ "general1": {
+ "q": "이 AI 독서 도구는 어떻게 작동하나요?",
+ "a": "우리는 딥러닝 모델을 사용하여 책 텍스트에 대한 의미 분석을 수행합니다. 전문적인 '배경'을 기반으로 특정 지식 포인트를 추출할 뿐만 아니라 전체 텍스트를 요약할 수도 있습니다."
+ },
+ "usage1": {
+ "q": "생성된 콘텐츠는 5000자 이상이 될 수 있나요?",
+ "a": "현재 논리적 일관성을 보장하기 위해 단일 생성 제한은 5000자입니다. 더 긴 콘텐츠가 필요한 경우 장별로 작업을 생성하는 것이 좋습니다."
+ },
+ "billing1": {
+ "q": "현재 구독 플랜이 있나요?",
+ "a": "이 도구는 현재 무료로 사용할 수 있습니다. 비용이 들지 않지만 자체 대규모 모델 API 키를 사용해야 합니다"
+ },
+ "privacy1": {
+ "q": "데이터를 보려면 어떻게 해야 하나요?",
+ "a": "설정 페이지에서 데이터를 볼 수 있습니다."
+ },
+ "usage2": {
+ "q": "대규모 모델을 설정하려면 어떻게 해야 하나요?",
+ "a": "『설정 센터』-『대규모 모델 설정』에서 독서 반성 및 요약 모델을 각각 설정할 수 있습니다.
무료 API를 제공하는 【알리바바 클라우드-이플로우 플랫폼 (iflow.cn)】 사용을 권장합니다. 단일 생성으로 긴 텍스트 처리를 지원합니다. 생성된 내용이 5000자 제한에 가까운 경우 최적의 논리 효과를 얻기 위해 장별로 작업을 생성하는 것이 좋습니다.
다음操作 가이드를 참조하세요:
- API 키 가져오기: 알리바바 클라우드 이플로우 플랫폼 (iflow.cn)에 방문하세요.
등록 및 로그인 후 백엔드에서 새로운 API 키를 생성하세요. 이플로우 플랫폼은 현재 개인 독서 어시스턴트 사용에 매우 적합한 안정적인 무료 할당량을 제공합니다.- 인터페이스 주소 설정:
응용 프로그램 설정의 Base URL에 다음을 입력하세요: https://apis.iflow.cn/v1.- 모델 선택:
플랫폼이 제공하는 최신 모델 ID.
"
+ }
+ }
+ },
+ "menus": {
+ "groups": {
+ "dataExploration": "데이터 탐색",
+ "automation": "자동화 및 내보내기",
+ "lab": "실험실 기능"
+ },
+ "items": {
+ "search": {
+ "title": "전역 검색",
+ "desc": "모든 로컬 노트를 초 단위로 검색"
+ },
+ "userPersona": {
+ "title": "독서 프로필",
+ "desc": "지식의 경계와 선호도 시각화"
+ },
+ "monitor": {
+ "title": "서재 모니터링",
+ "desc": "로컬 폴더에서 새 책 자동 스캔"
+ },
+ "tts": {
+ "title": "오디오북 모드",
+ "desc": "시스템 엔진을 사용하여 요약 읽기"
+ },
+ "model": {
+ "title": "모델 실험실",
+ "desc": "다른 프롬프트의 생성 효과 비교"
+ }
+ }
+ },
+ "search": {
+ "title": "전역 검색",
+ "placeholder": "책 제목, 노트, 키워드를 검색하세요...",
+ "loading": "검색 중...",
+ "noResults": "관련 노트를 찾을 수 없습니다",
+ "enterKeyword": "검색을 시작하려면 키워드를 입력하세요"
+ },
+ "userPersona": {
+ "backTitle": "앱 메뉴로 돌아가기",
+ "syncing": "동기화 중...",
+ "syncLatest": "최신 프로필 동기화",
+ "stats": {
+ "totalNotes": "생성된 노트 총합",
+ "tenThousandWords": "만자",
+ "words": "글자",
+ "keepGrowing": "성장 지속",
+ "deepBooks": "심층 읽은 책",
+ "books": "권",
+ "deepReading": "심층 독서",
+ "focusTime": "집중 성장 시간",
+ "hours": "시간",
+ "focusTimeLabel": "집중 시간"
+ },
+ "radar": {
+ "cognitiveDepth": "인지 깊이",
+ "productionEfficiency": "생산 효율성",
+ "maturity": "성숙도",
+ "knowledgeBreadth": "지식 폭",
+ "languageAbility": "언어 능력"
+ },
+ "charts": {
+ "multidimensionalProfile": "다차원 능력 프로필",
+ "productionDensity": "생산 밀도 (최근 7일)"
+ },
+ "contribution": {
+ "title": "독서 기여 발자취"
+ },
+ "report": {
+ "title": "독서 차원 보고서",
+ "analysis": "누적 {totalWords} 자 노트 분석에 기초: {domain} 분야에서 지식 프레임워크를 초기적으로 구축했습니다.",
+ "exploration": "탐험"
+ }
+ },
+ "setting": {
+ "title": "설정 센터",
+ "menu": {
+ "model": "대규모 모델 설정",
+ "account": "계정 보안",
+ "billing": "구독 및 청구",
+ "notification": "알림 설정",
+ "language": "언어 설정"
+ },
+ "notification": {
+ "title": "알림 관리 센터",
+ "description": "독서 반성 생성 진행 상황을 실시간으로 파악하여 모든 영감이 적시에 전달되도록 합니다.",
+ "taskStatus": "작업 상태 알림",
+ "realTime": "실시간 푸시",
+ "taskCompleted": "생성 성공 경고",
+ "taskCompletedDesc": "독서 반성 및 요약이 생성되면 데스크톱 알림을 보냅니다.",
+ "taskFailed": "비정상 중단 경고",
+ "taskFailedDesc": "네트워크 변동 또는 API 잔액 부족으로 인한 생성 실패 시 알림합니다.",
+ "silentMode": "사일런트 모드",
+ "silentModeDesc": "알림이 팝업될 때 시스템 소리를 재생하지 않습니다.",
+ "tip": "팁: 알림 효과는 운영 체제의 '집중 모드' 또는 '방해 금지 모드'의 영향을 받습니다. 설정을 활성화한 후에도 알림을 받지 못하면 시스템의 알림 관리 권한을 확인하세요."
+ },
+ "masterSwitch": "마스터 스위치",
+ "taskCompleted": "작업 완료 시 알림",
+ "taskFailed": "작업 실패 시 알림",
+ "silentMode": "사일런트 모드",
+ "language": "언어 설정",
+ "zh": "중국어",
+ "en": "영어",
+ "ja": "일본어",
+ "ko": "한국어",
+ "es": "스페인어"
+ }
+}
\ No newline at end of file
diff --git a/src/renderer/src/locales/zh.json b/src/renderer/src/locales/zh.json
new file mode 100644
index 0000000..a8fd438
--- /dev/null
+++ b/src/renderer/src/locales/zh.json
@@ -0,0 +1,282 @@
+{
+ "common": {
+ "title": "读书心得助手",
+ "loading": "加载中...",
+ "success": "操作成功",
+ "error": "操作失败",
+ "confirm": "确认",
+ "cancel": "取消",
+ "delete": "删除",
+ "save": "保存",
+ "edit": "编辑",
+ "preview": "预览",
+ "back": "返回",
+ "next": "下一步",
+ "previous": "上一步"
+ },
+ "task": {
+ "list": {
+ "title": "任务列表",
+ "create": "创建任务",
+ "total": "篇心得",
+ "status": {
+ "pending": "任务排队中",
+ "pendingDesc": "任务排队中,等待系统调度算力...",
+ "processing": "进行中",
+ "processingDesc": "深度学习模型正在分析文本,请稍后...",
+ "completed": "任务完成",
+ "completedDesc": "任务已完成...",
+ "failed": "任务失败",
+ "failedDesc": "任务失败,正在重试...",
+ "paused": "任务暂停",
+ "pausedDesc": "任务已暂停,点击恢复继续..."
+ }
+ },
+ "index": {
+ "totalProgress": "总进度",
+ "pauseQueue": "暂停队列",
+ "resumeQueue": "恢复队列",
+ "packageResults": "打包成果",
+ "deleteTask": "删除任务",
+ "confirmDelete": "确认删除任务",
+ "deleteContent": "此操作将永久删除该书籍的所有心得记录及生成进度。",
+ "deleting": "正在从磁盘清理...",
+ "deleteSuccess": "任务已成功删除",
+ "deleteFailed": "删除失败,请检查数据库连接",
+ "previewResults": "预览成果",
+ "pauseSuccess": "任务已暂停",
+ "pauseFailed": "暂停失败",
+ "resumeSuccess": "任务已恢复",
+ "resumeFailed": "恢复失败"
+ },
+ "create": {
+ "title": "创建读书心得",
+ "bookName": "书籍名称",
+ "bookAuthor": "书籍作者",
+ "bookDescription": "书籍简介",
+ "occupation": "目标受众职业",
+ "occupationList": {
+ "student": "学生",
+ "professional": "职场白领",
+ "scholar": "学者/研究员",
+ "freelancer": "自由职业者",
+ "teacher": "教师"
+ },
+ "prompt": "个性化提示词",
+ "wordCount": "单篇心得字数",
+ "wordUnit": "字",
+ "quantity": "生成心得篇数",
+ "language": "生成语言",
+ "tone": "生成语气",
+ "submit": "立即开启任务",
+ "success": "任务已加入队列",
+ "error": {
+ "bookName": "请输入书籍名称"
+ },
+ "placeholder": {
+ "bookName": "请输入书籍完整名称...",
+ "description": "简要描述书籍内容,帮助 AI 提取更准确的关键点...",
+ "prompt": "例如:使用鲁迅的文风、增加 3 个实战案例、针对小白用户..."
+ }
+ }
+ },
+ "reflection": {
+ "index": {
+ "copyContent": "复制原文",
+ "exportDocument": "导出文档",
+ "sharePoster": "分享海报",
+ "copySuccess": "正文已成功复制",
+ "summary": "文章摘要"
+ },
+ "export": {
+ "title": "导出 Markdown",
+ "format": "导出格式",
+ "word": "Word文档",
+ "pdf": "PDF文档",
+ "export": "确认导出报告",
+ "step1": "第一步:选择导出样式模板",
+ "step2": "第二步:完善补充信息",
+ "step3": "第三步:MD 源码预览",
+ "building": "构建中...",
+ "selectTemplate": "请选择一个模板以开启预览",
+ "noExtraInfo": "无须额外信息",
+ "success": "Markdown 导出成功!已打开所在目录",
+ "error": {
+ "missingParams": "缺少导出参数",
+ "initFailed": "初始化数据失败",
+ "parseTemplateFailed": "解析模板标签失败",
+ "exportFailed": "导出失败"
+ }
+ }
+ },
+ "about": {
+ "hero": {
+ "title1": "让每一篇阅读者",
+ "title2": "享受阅读与思考的乐趣",
+ "description": "我们致力于通过最前沿的 AI 技术,帮助深度阅读者高效消化知识。从海量文本到结构化心得,只需一键。",
+ "startExperience": "开始体验"
+ },
+ "coreValues": {
+ "dataDriven": "数据驱动",
+ "dataDrivenDesc": "基于大模型,精准提取每一本书籍的灵魂。",
+ "extremeExperience": "极致体验",
+ "extremeExperienceDesc": "化繁为简,让 AI 创作如同呼吸般自然、流畅。",
+ "connectFuture": "连接未来",
+ "connectFutureDesc": "探索人机协作的新范式,重新定义阅读与写作。"
+ },
+ "middle": {
+ "title": "让那些真正热爱文字的人",
+ "description": "在一个信息碎片化的时代,深度阅读正变得前所未有的奢侈。我们不希望 AI 替代阅读,而是希望它能作为你的\"数字笔友\",帮你梳理逻辑、捕捉灵感,让你从繁琐的摘要工作中解放,回归思考本身。",
+ "users": "使用用户",
+ "readings": "阅读量"
+ },
+ "footer": {
+ "copyright": "© 2026 AI Reader Studio. All Rights Reserved. Crafted with ❤️ for readers worldwide."
+ }
+ },
+ "faq": {
+ "hero": {
+ "title": "有什么可以帮到你?",
+ "searchPlaceholder": "搜索您遇到的问题..."
+ },
+ "categories": {
+ "general": "常规问题",
+ "usage": "使用技巧",
+ "billing": "订阅支付",
+ "privacy": "隐私安全"
+ },
+ "emptyState": "未找到相关问题,请尝试其他关键词",
+ "support": {
+ "title": "仍有疑问?",
+ "responseTime": "我们的团队通常在 2 小时内回复您的邮件",
+ "contact": "联系支持"
+ },
+ "items": {
+ "general1": {
+ "q": "这个 AI 阅读工具是如何工作的?",
+ "a": "我们利用深度学习模型对书籍文本进行语义分析。它不仅能总结全文,还能根据你选择的\"职业背景\"提取特定的知识点。"
+ },
+ "usage1": {
+ "q": "生成的字数可以超过 5000 字吗?",
+ "a": "目前单次生成上限为 5000 字,以确保内容的逻辑连贯性。如果需要更长篇幅,建议分章节创建任务。"
+ },
+ "billing1": {
+ "q": "当前有订阅计划吗?",
+ "a": "当前工具为免费使用,不需要任何费用,但是需要自己使用大模型KEY"
+ },
+ "privacy1": {
+ "q": "如何查看我的数据?",
+ "a": "你可以在设置页面查看你的数据。"
+ },
+ "usage2": {
+ "q": "如何设置大模型?",
+ "a": "您可以在『设置中心』-『大模型配置』中分别设置读书心得与摘要模型。
推荐使用【阿里云-心流平台 (iflow.cn)】提供的免费 API,其单次生成支持长文本处理。若生成内容接近 5000 字上限,建议分章节创建任务以获得最佳逻辑效果。
可以参考以下操作指南:
- 获取 API Key:访问 阿里云心流平台 (iflow.cn)。
注册并登录后,在后台创建一个新的 API Key。心流平台目前提供稳定的免费额度,非常适合个人阅读助手使用。- 配置接口地址:
在应用设置的 Base URL 处填入:https://apis.iflow.cn/v1。- 模型选择:
平台提供的最新模型 ID。
"
+ }
+ }
+ },
+ "menus": {
+ "groups": {
+ "dataExploration": "数据探索",
+ "automation": "自动化与导出",
+ "lab": "实验室功能"
+ },
+ "items": {
+ "search": {
+ "title": "全局检索",
+ "desc": "秒级搜索所有本地心得内容"
+ },
+ "userPersona": {
+ "title": "阅读画像",
+ "desc": "可视化你的知识边界与偏好"
+ },
+ "monitor": {
+ "title": "书库监控",
+ "desc": "自动扫描本地文件夹新书"
+ },
+ "tts": {
+ "title": "听书模式",
+ "desc": "调用系统引擎朗读心得摘要"
+ },
+ "model": {
+ "title": "模型实验室",
+ "desc": "对比不同 Prompt 的生成效果"
+ }
+ }
+ },
+ "search": {
+ "title": "全局检索",
+ "placeholder": "搜索书名、心得、关键词...",
+ "loading": "正在搜索...",
+ "noResults": "没有找到相关的心得",
+ "enterKeyword": "请输入关键词开始搜索"
+ },
+ "userPersona": {
+ "backTitle": "返回应用菜单",
+ "syncing": "同步中...",
+ "syncLatest": "同步最新画像",
+ "stats": {
+ "totalNotes": "累计产出心得",
+ "tenThousandWords": "万字",
+ "words": "字",
+ "keepGrowing": "Keep growing",
+ "deepBooks": "已读深度书籍",
+ "books": "本",
+ "deepReading": "Deep reading",
+ "focusTime": "专注成长时长",
+ "hours": "小时",
+ "focusTimeLabel": "Focus time"
+ },
+ "radar": {
+ "cognitiveDepth": "认知深度",
+ "productionEfficiency": "产出效率",
+ "maturity": "成熟度",
+ "knowledgeBreadth": "知识广度",
+ "languageAbility": "语言能力"
+ },
+ "charts": {
+ "multidimensionalProfile": "多维能力画像",
+ "productionDensity": "产出密度 (近7日)"
+ },
+ "contribution": {
+ "title": "阅读贡献足迹"
+ },
+ "report": {
+ "title": "阅读维度报告",
+ "analysis": "基于你累计生成的 {totalWords} 字心得分析: 你在 {domain} 领域已初步建立知识框架。",
+ "exploration": "探索"
+ }
+ },
+
+
+
+ "setting": {
+ "title": "设置中心",
+ "menu": {
+ "model": "大模型配置",
+ "account": "账号安全",
+ "billing": "订阅与账单",
+ "notification": "通知设置",
+ "language": "语言设置"
+ },
+ "notification": {
+ "title": "通知管理中心",
+ "description": "实时掌握读书心得生成进度,确保每一份灵感都能及时送达。",
+ "taskStatus": "任务状态通知",
+ "realTime": "实时推送",
+ "taskCompleted": "生成成功提醒",
+ "taskCompletedDesc": "当读书心得、摘要生成完毕时,发送桌面通知。",
+ "taskFailed": "异常中断提醒",
+ "taskFailedDesc": "若因网络波动或 API 余额不足导致生成失败时提醒。",
+ "silentMode": "静默模式",
+ "silentModeDesc": "通知弹出时不再播放系统提示音。",
+ "tip": "提示:通知效果受操作系统\"专注模式\"或\"勿扰模式\"影响。如果在设置开启后仍未收到通知,请检查系统的通知管理权限。"
+ },
+ "masterSwitch": "总开关",
+ "taskCompleted": "任务完成时提醒",
+ "taskFailed": "任务失败时提醒",
+ "silentMode": "静默模式",
+ "language": "语言设置",
+ "zh": "中文",
+ "en": "English"
+ }
+}
\ No newline at end of file
diff --git a/src/renderer/src/main.ts b/src/renderer/src/main.ts
index 94a1c95..3b40a40 100644
--- a/src/renderer/src/main.ts
+++ b/src/renderer/src/main.ts
@@ -1,10 +1,11 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from '@renderer/router'
+import i18n from '@renderer/plugins/i18n'
import '@arco-design/web-vue/dist/arco.css'
import 'virtual:uno.css'
import './style.css'
import './assets/global.less'
import '@icon-park/vue-next/styles/index.css'
-createApp(App).use(router).mount('#app')
+createApp(App).use(router).use(i18n).mount('#app')
diff --git a/src/renderer/src/pages/about/index.vue b/src/renderer/src/pages/about/index.vue
index ad6cb2a..21595b8 100644
--- a/src/renderer/src/pages/about/index.vue
+++ b/src/renderer/src/pages/about/index.vue
@@ -1,25 +1,27 @@
@@ -28,12 +30,11 @@ const coreValues = [
- © 2026 AI Reader Studio. All Rights Reserved. Crafted with ❤️ for readers worldwide.
+ {{ t('about.footer.copyright') }}
diff --git a/src/renderer/src/pages/faq/data/faqData.ts b/src/renderer/src/pages/faq/data/faqData.ts
index e2045fb..3ab2231 100644
--- a/src/renderer/src/pages/faq/data/faqData.ts
+++ b/src/renderer/src/pages/faq/data/faqData.ts
@@ -1,45 +1,38 @@
-export const faqList = [
- {
- id: 1,
- category: 'general',
- q: '这个 AI 阅读工具是如何工作的?',
- a: '我们利用深度学习模型对书籍文本进行语义分析。它不仅能总结全文,还能根据你选择的“职业背景”提取特定的知识点。'
- },
- {
- id: 2,
- category: 'usage',
- q: '生成的字数可以超过 5000 字吗?',
- a: '目前单次生成上限为 5000 字,以确保内容的逻辑连贯性。如果需要更长篇幅,建议分章节创建任务。'
- },
- {
- id: 4,
- category: 'billing',
- q: '当前有订阅计划吗?',
- a: '当前工具为免费使用,不需要任何费用,但是需要自己使用大模型KEY'
- },
- {
- id: 5,
- category: 'privacy',
- q: '如何查看我的数据?',
- a: '你可以在设置页面查看你的数据。'
- },
- {
- id: 6,
- category: 'usage',
- q: '如何设置大模型?',
- a: `
- 您可以在『设置中心』-『大模型配置』中分别设置读书心得与摘要模型。
-
推荐使用【阿里云-心流平台 (iflow.cn)】提供的免费 API,其单次生成支持长文本处理。若生成内容接近 5000 字上限,建议分章节创建任务以获得最佳逻辑效果。
-
可以参考以下操作指南:
-
-
- - 获取 API Key:访问 阿里云心流平台 (iflow.cn)。
- 注册并登录后,在后台创建一个新的 API Key。心流平台目前提供稳定的免费额度,非常适合个人阅读助手使用。
- - 配置接口地址:
- 在应用设置的 Base URL 处填入:https://apis.iflow.cn/v1。
- - 模型选择:
- 平台提供的最新模型 ID。
-
-`
- }
-]
+import { useI18n } from 'vue-i18n'
+
+export const useFaqList = () => {
+ const { t } = useI18n()
+
+ return [
+ {
+ id: 1,
+ category: 'general',
+ q: t('faq.items.general1.q'),
+ a: t('faq.items.general1.a')
+ },
+ {
+ id: 2,
+ category: 'usage',
+ q: t('faq.items.usage1.q'),
+ a: t('faq.items.usage1.a')
+ },
+ {
+ id: 4,
+ category: 'billing',
+ q: t('faq.items.billing1.q'),
+ a: t('faq.items.billing1.a')
+ },
+ {
+ id: 5,
+ category: 'privacy',
+ q: t('faq.items.privacy1.q'),
+ a: t('faq.items.privacy1.a')
+ },
+ {
+ id: 6,
+ category: 'usage',
+ q: t('faq.items.usage2.q'),
+ a: t('faq.items.usage2.a')
+ }
+ ]
+}
diff --git a/src/renderer/src/pages/faq/index.vue b/src/renderer/src/pages/faq/index.vue
index b6510a0..45c2f8c 100644
--- a/src/renderer/src/pages/faq/index.vue
+++ b/src/renderer/src/pages/faq/index.vue
@@ -1,5 +1,6 @@
diff --git a/src/renderer/src/pages/menus/views/search/index.vue b/src/renderer/src/pages/menus/views/search/index.vue
index 410904e..1ce0400 100644
--- a/src/renderer/src/pages/menus/views/search/index.vue
+++ b/src/renderer/src/pages/menus/views/search/index.vue
@@ -1,13 +1,15 @@
+
+
+
+
{{ $t('setting.language') }}
+
+
+
+
+ {{ $t(item.label) }}
+
+
+
+
+
+
+
+
diff --git a/src/renderer/src/pages/setting/components/NotificationSetting.vue b/src/renderer/src/pages/setting/components/NotificationSetting.vue
index d28dd68..eea34d8 100644
--- a/src/renderer/src/pages/setting/components/NotificationSetting.vue
+++ b/src/renderer/src/pages/setting/components/NotificationSetting.vue
@@ -1,10 +1,13 @@
@@ -26,7 +31,7 @@ onMounted(() => {})