From 09e352a9c1672016ca3db3185f65029f3a414978 Mon Sep 17 00:00:00 2001 From: zhaoyingbo Date: Fri, 16 Aug 2024 09:12:11 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8A=BD=E8=B1=A1=E7=BD=91=E7=BB=9C?= =?UTF-8?q?=E8=AF=B7=E6=B1=82=E7=B1=BB=20&=20=E5=86=85=E5=AE=B9=E8=BD=AC?= =?UTF-8?q?=E4=B8=BActx=E5=90=91=E5=86=85=E4=BC=A0=E9=80=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 2 + bun.lockb | Bin 100473 -> 113100 bytes db/tenantAccessToken/index.ts | 3 +- index.ts | 28 ++- log/index.ts | 55 +++++ package.json | 9 +- routes/bot/actionMsg.ts | 30 +-- routes/bot/eventMsg.ts | 86 ++++--- routes/bot/index.ts | 36 ++- routes/message/index.ts | 73 +++--- routes/microApp/index.ts | 71 +++--- routes/sheet/index.ts | 59 +++-- schedule/accessToken.ts | 36 +-- services/attach/index.ts | 53 ++--- services/index.ts | 11 +- services/lark/auth.ts | 15 ++ services/lark/base.ts | 28 +++ services/lark/drive.ts | 39 ++-- services/lark/index.ts | 37 ++- services/lark/larkNetTool.ts | 125 ---------- services/lark/message.ts | 50 ++-- services/lark/sheet.ts | 37 +-- services/lark/user.ts | 71 +++--- services/netTool.ts | 232 ------------------- types/context.ts | 17 ++ types/index.ts | 3 +- types/larkServer.ts | 20 +- utils/genContext.ts | 42 ++++ utils/netTool.ts | 424 ++++++++++++++++++++++++++++++++++ utils/pathTools.ts | 71 ++++-- utils/pbTools.ts | 1 - 31 files changed, 1064 insertions(+), 700 deletions(-) create mode 100644 log/index.ts create mode 100644 services/lark/auth.ts create mode 100644 services/lark/base.ts delete mode 100644 services/lark/larkNetTool.ts delete mode 100644 services/netTool.ts create mode 100644 types/context.ts create mode 100644 utils/genContext.ts create mode 100644 utils/netTool.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 0cec1de..4339465 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,8 @@ "eamodio", "esbenp", "Gruntfuggly", + "metas", + "mina", "tseslint", "wlpbbgiky", "Yoav" diff --git a/bun.lockb b/bun.lockb index 94d0484e580d4a552136b08292996554e909e4c2..f9abe845fd4960ce87f53c54b3b8fa8f89c0e972 100755 GIT binary patch delta 26094 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≶qiZEm_%n0)ej^_}KTV&tF&6E~eK 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 E57lU_Jpcdz delta 18818 zcmeI4i+_*h|Htq9V`CpSCTGUxFo(t1%nlA8Bj4>OQ---@?&-Z;O-*5W8nYs^%B3+uhlfV?4nGwdF!=0+8M-D{(ER@4 zMVWy%v2wKXnpXS*Ns-SWLy(VBp*nIZG8lO;vH~(^{KTcYV|;#Ai)@U~8$V`J#@G@5$xi-Yq&TP_QpWA-$ahfEav)O1X-mF1qNOd1in9t@ zpv!Qr9B!0~jhqViW@cq%qEJ?We8P+odrW((X~oBGl&)RBX0*u1R6 ze}#+Va{Lp-KJ7cF{H!GdwBidjR8&B{2Iq(iQS6i>^IGJL%}6iwBeQa|3bj+URdztV zqJwp8J2TS@S}>WTcEY9Eyf9m>vq({ADS;G$dS{cVhm9{56Qpc(HK&b@r&<+Nk@74{=&j6 zfBq~gOGj21lD-yc^SrDPh53HJmXlRbn9qPUn%M3BbD{>3FCJ*t)Q+@zNa@e@Z2rg* z8FZ_KP_MkK**VQ@%T6Gf#Nw$Azc0$R^axTq7(2#an3e0-a+$x379+<@pq>n*(UC;Z zPmOJbzCud=fA)I?L%ie@e;Mg0u&o7amM|_ZAt4zq@}Q4QkTEPPndj=gMD@k)D%2!k>|oHD0uL=Xz31qP6Jy(Sa<&+^jK7MnU21Bz3r4 zy`l+8_U<_{Ys`oi&>#Old*2j5ds9J3KF`UZ8f9kdEAGkF~;4s>VCx{tvB*Ii*X{Y(4o ztU}yGFjqxGc91`|>kVD(ffpks)MhyxziD!P++NezVT#>tdQLaHp1V!U?W6^J^sm&D z?Ip(XP-7-e40{qO9-4`)B8zDPiCd7{d)gJ&BE|AWj-2VpiH@x1bTrSf6|X`JN@xs4 zN~m~|;;Cy$FS0N*f9&{CnOZ*e+4737cen58;V4#B2CnjGt!cG>j#4iyYL%3VLJv>S@c|{rdj9a#=UaXb8RpqCEHqRPYPrU@`-|zrA5eLf zSEz48g8r-%ddmRa^Qxw@!+hQvx~2_d43D}JmJqlamai^XZRf4-(X=7tYvd>D9(A#< z&vVS9qU!lP5oJ_1;!qh?QqSj|8b}i=>gr031n&>9RF`>rmQ~r|KJQCqHLVYsWz>~w z37&v*>LQuF%V}C)G6PghSb}#Mj6jm%swQ~Olv5=UK5zRVO>1kli>aRAnI5E~>ifK( zk;%+S%^C@w2(K!s>hq4kk1TjP+f2LO(qC}V@b=*v ziPXe~0f|8nyBSXMdWj`rwn05%ovm>;cTNm~*p8Dv-h#zZ6sR^gOz?UL-2_-g%km@` zJ6v&jYqWb|(k@VqtDWF^J5-f4^?AxwRu`M%V%EkzlzFV0yJ0eWnutA2FT!hor-uPJi!~p^6g?Z8&@;In*y`#8Aq3M92TTD!_LBFf&wKx zymeTfq9)S?ONGfip$gNi99Gt9cN``&6QE>-$~+%QFIKF3d&8t%Ijd|dEY>QM@xJ(j zNpRMzZkxv>z(&Kw(-;fe2ow8=64>W38G+zzl;Ew;tyfrCiJ-ty!qmpZ_Bu~fEq$Kg znku`c&wCepP%EoR3Y9)AbYFTS>g_9X* zsdfn_2HF7#>e%+`Ua3S0*3tS;AR=eZE3qH6g9Be}(-soZ+)0xyy3 zVWslwvGZ7|w@BS(r5c1Y16C@NRI;^0Z6zhH!UQ7gTUc9|SB;BI@J2oQj0-(O#qB>230oJ^`Pz%Q^k8c+rICpEAK$7ZJe zA((i8@MOImgNZ8T#M6Q0dGd%;E!a|hq-01G#%uS(WCpD5#k&(GZ7i<^{sfCvxkmed zMy`K~NcFWmaFmqg3vV?Zj--*bnmj`zRdzd{XI-Q!K?F8d7ZGh5tL)l7@07-Fv|M56 zZ7?wI4ztI(0869H4#$pBc1=5kX2Ha{HhT{yy%VYl3Et|r z+IHD4O@WC^?KzzW6KyR$ysyJL!>lRtlx?oEJNdkwn!8($m_7yO1Qcv1OjNRmt-|xE zIEh7%%X-5k{)4RfJ`A(>Z|S9Gv^$WnK`>Fm-nf>)L_xdH6Hb}Et=!VmHq74l`@%#Q zdxSYKXLS1950lkuJMvf9pMA!$m9(QQ*qXkvFwxT<{b`4Jt(ZLt%d%=-2}|&_k5Sp( z2%Z>Kg7_syT|^9xRZ-o2o?Wr3q`S{suC=D!O@k^brZzWh*!?i(k8LZGa1t|ZQ^vqV zC41OsVG?b2*=dq?%!+%evLGdLs#uF^Jj`#kld{iXVoDY3ksv)@MfLJ|cVl=jt249? zEYFRMr4ZT9+n1C)KU5Rj0_Ve$RW3W#aZ)mi_GaA#tt5EtyS^VLYT9$UjO3qldK4yV zT5H(jO;RN(K5r7w;yjFs2Z-z?utasaVLR^$QgZvVr?n>S#9r=B+yhczF|c6E=?}o{ zLFC5#9IU(DANKwTlWDbv^d@1t#H+Old+&oeTL(3FILzK~e}ah_b~v;oYNQuS7mvTa zy2wM`Dl%=wQOPr*gUasf^KK=luT^tg%LH!#-4BP^D{v%C#`9V?i)|z&7@1F+$97aD z{e0g0nKx-*?~Z3+q6AwJ!BL-BkmlTyc_12^tV-_i1@0uXv)Xt^yTB-BYNVvR^GHe0 z_QPW-3@13cm+h=B-s$ru;ssHYp4r%E!elex38im>w-hF$+Or#Qn>{LwQ9IwJF4p#W zrroBZ>JT@SN!J7+Wr1B(cB;=CPfvG}$$WN941(AzRf6wVm^>fy;6QkH>FPc^%CtQW zv&&?+KMi9c%XHH&h&V{Kx|F-bU>M7jc$3G(r(k_xc#J+T!)&``ybi>HZHe?b9@g9H zMauTVoUp^U2@L(`l6ek>0%8I+e}G}A!xDPhYr9Y0EN59zd<@MyZo?i<2)h08|GJ z0N%B(8>NRF^s%o0B4affkSWk)00LW+hRHy#Wb4zGQce(Sn)-Z5c#+g1j;vNpl6BoE zrJT97%9#S|5-It3-pa?z)^(%wl5>}nFH%M&=&VbmwC`_yceWE&g#lKvrrk`6d3OWp z=pLt@Nb%n=hu=&}{dA}Nud>K4kpj^u!>K4z8ZzhBB~rNGkt3~Kb_k>>mgVGUJNY7| z{%D8aOiFo9Ri{D@vMk6Kt`-h$qBabzX(@Lv8B%qkBPTg>GE%M^rPR9*h$#;M;SU12 zL<*NqMHU0Oia`Z18;Az;K@eCh-FQ@BdiX%8z6^+54&)Lk)mIDQ5*Y~A0U3ThkV~Yr z+u+EJj@*RQ)W-C1%gE=2afy`HF97LnnZwW+(WxGX5e(k@tWob{a@WA2{+uq(==NUZWEU?W}xo-AqdBb3o*$K&~65S1lV} zqe%L`Y?b|QQtE&0)cU{%b^eaI4caHoXDHpQHD)<2+6@LU$@h690b@(-;TsKOo z|0|ICzkzZf03x!2Wd4nmS*YlgSJZE_>fMlGPg7+oNX4p7MUgTqwHz)|%EN@Kg&E(skZN>)23U!)k{0V(x6Ir%q}qE#2& zT7p)>@>W-;fk;Vqb7XfX|7KERySGz*Gbu%VoN|$(U|*ytc$aQ1IZ42*jK4~$IDm5T z#9*i1jZzfIaPt2bDVC3gT**RrSFXy^KnKu)kV6E?B(|3sjYh`l|u?!K$z*MK7oJz=8@wRAjNC^U}Jw zzuFFa2UbDVpVD7t7KW(lQw%*s9f#E%AEMe!HS|hq%GCbqAnbEkWfk*Ke>HJJh+6oN zp;uAoU`_80QC+7QdNnnFT7UH(Yq%$&72sbdQUg>T59q1{wi@&i24myNA-NT zzd8@w_^_eZRaaq)CWolOGYmakt((zb^|&uYh0HYc`fA|J{_1DgPFO?borQh(hp60H zh90SQzy>@JqUt_k=uK4CBiQ#~uqu5dMQ^5R&Bi|1{j*c_Th$?0W>K(eJ|{(Qp(f42 zzTyz|(Hui>shZ8jKG@v3h90BdhfSOkqB=Zk=&jYPN3m~eh`J1GqmmxOKG?Fy3_U?z zfX#de`{o&Xl3F|u`=((ZtexsPANyb%=NozlbrrT~I`%zo=pEI%$Fc8W>{Eu`Nexuk z2ipm|O?ek!-wf!wVuE}*aJ@(x}griG9L+1 zElfl2ttOe+Hyit4x2tB0un#tOk)iif@53g}!M??Yey5tX82jd8A1qZREx|t6vL%Mz zUtNIBd=&ehH1vUL@srs181}*LRy~(uA8g}NLm#ZJ!WPZLzGa3!RIOWvee*-qGm{NH zUFplQ59VKP=oxA=Z0+MAs@hYA?pNtgVIO{Y12#%kUV(iJh@KUOo~8D{w!F>uN(Eug<~V zgLQqz(C=0ApTWMxA?jbSNvhMc*tZ0&pEdOR)D_rySn3)>e?UFG2K$~2QGsg>y-4+2 zi+xK&)QhkwN?(V4F#kG3e@JbHtzCwFC5Ap-rI%pea_oc6P?gtX-&5GP-q2^MJ+SSt z$PI=*TaDX*eJijJHdoc(h<#6E-$p}!OdW?EgtggZ==0T-P1v^*`(R4NJcoU&uA^#wy;rk;KQ`<}(VEr$M- z>bC{^)?gp(X{B$)KA3;2p|4V#VQbf7-;0L+j7onI`_^F}Y>le?684o~-%EzRPVIqh zF9}wSwx#In)wpfgw?0_C4cn+DZO6V1#OHQHe@->qfqk&KI}ClZdLK4%Bk}pNp>I*M zUdFyn#3$@UmGlbs!Ir&Z=-bo<*v#jM&sPn7hg$q9_B~I0!d_85cVZuG<4!~0sjk8n zZ6-c<8TxB#-7f5Vf%W&Aq3=-xU&B7wPS`%>-Hm-)Sbw_>{SCDPHef63Z;zoLP+5Dh z??u)h?2xLp7yDoj>^1Z^)gf5sORT?rhJHj%+J}AHg4KunQuL#0^8?rii+epqKdz>{ zj(yvizt;`@q>6b1`*tvYZy5SJ>KyDnSl9iA{;rz8ANyWr{$TH`P6x2>73S}Np?{#R zz|O-`4;r42G_~?znp*TK6L`q*oYmAFhtgD!oy;KgoTfa7)6~yU|6#-Psit0luHA*F zSf8J(p7&wnYs~1IhUZI74SzFD1?^@^p%*lDOKF3vdX)^;Eb!NHhs81@Xb(uI74ZF-6~yD&Hg>j>S|lgoY~}2t*TU2 zbI$b$%!{x)8EsXo=l&S)_DK8ZJM!IW7T^V4u^FjGe%dy$pfTTv>3F?I9s97obiQTY z7fr3^6;8LYN8$HdYX0RJRG2CiJhPLwTs%Mdj|L(3@0NIha~oaL*HNpvXP%*YU2f~~ zrfhlm^0nCV&pW^CT@YjjH_?;%O85J9`$8k_b0A;cyh6BKE-K3lAi2H(Qb(Rrt^>Kg z1d=D;&vi*~od=SXFFKfDT^IP2yt1I7B;+gQMIdQ;tK_~6`pU_ZmzL5t8?` zR|0c^T<&W$B0{^JLa|6*sYyL~b0Kf7#Gs#ncu(G5xZiZ-6`SN$5&J(TA)3m2Hp!Hi z$L|2q^mib6)xaqrc`||wBCkP?08w6CE_pS;TTY(G$*T!p2*jW=PF^k2bxBti|H}(K z{>d+jwG#lXEcZ%Y9UyN-e{yHF@vW z0C|g(w_7yj3J$Sl`S#;m$ND1T%(YQ^RdaWg9@j$RTO58}>Pa&oP86p}1jrjz2^n#G z4fDdSdXpjM2Q5G}XbD<@7!V6ugE-Iz#DfH|6YK_i zz+SKqybj&~5=b&d#7U7gsS=eEh4SVv6{G=KTa7>@XbPHvD9{|p5|ba_)dUiWpMuYT zto1K}tXo-fvMj#>5+AbMWd6$m@ubXgd6~Q)sqh`R2Mhs2K|Uw|_ksICG8g~`g2`Y! zm;n5s1IPtqz&J1(j0B@VCP)W;Kwr=g+zGmZZlDw6TQB+cBM*RsKo<95@FrLXO29U- z1H268f+=7sco@i99g8(1fFH>FQTe}-L7*q-1q_e^XaQP*B6&q!8`J@p=}4ki z0!%gnS@^@igWv&hFYtkmARA0F3XG<04#)=b zUR>6>yf>F0ylWe&@Eq`wkqllXJr3Cf{6abr$o44P;U&_tZqtw-gLl9wPzsKKx4=TM z7~DZU*`exzbMQ~VF>nIB2i^xq#eI@_6f6Z1RF?FI;3M!Xkc~w)k~5@b^N`KrHqZcc z1zkWlkmzg-S`~9il-{67?ujqaW)rvrh@lcsL0~Od1Kg=CN7@7E#=6@DAj^QVKo-Gp zkPf76Ea(mH2Gu}I&;kqs1Azp1vP_f&rVMNVi4BR7&OqWv;;0>H3z9$^AfAo{^*}99 z6Uequ2~-3jpaLilydW5eLN$OWBIDfx*jLzJnA++jYJ<9<4hRGFK{$v2jX(p?5QtT- zHPNKSqGq58kd7OJrr=f(1)75x&D=rPlA{YRq{~_QWFc=I4MZ@?IEA9s)fw))ZQrtKh zi~y5>xKQR-toH*qKZCTCN&aY%4U$17kpA4dG7XX*1+sv+f1=F43|6g&x*gVjJ>zaEr0a)TqcBA)}#gDqe)cmcc!#CmBXH@Tu8NE`!u zfix0tYy*41YhV|61-uM)fbF0s*a=<*QYL&ikl=FpLDFu2uan*f4uJjO4R8pQij&_Y zaTpu{Z-JxWYw$fd52O>(_-$|!oB(aXaquM&OQdWR_yRNoO~5-qHn-Er_rSa06nGze z1U>*~Ws=W086P4)1?RxW;1lo}_#B8rqR1ui6}Sj4fXhJgzX5H)ci;;67RYnN@5p}v zsq-`V3H<1A@$^<%hrfYe!7tz%xC*2}ARSysdXTc`x~|hn2Y@m_=2z-wJ5oH=stN^M zuP2&m@p@ykI9{Jr-eYC)G0d!+ptshu%zh$fnbQ${7f?`{u-{v-V*9%tQ%(lyLlUB6 z5~5pK%Or%7ZUd5|g4?&6>d}Y9M#n_Q*x^*368F#qC9R@kW1?GY_v+?b?e&^rQ_&-s z8lR`cKC<$~3+Fs~E4!)osBU^X=&@n;fi8ZU)aSl?^O}9#*`qtdY7dn$`*zT4dX|>i zmfb;**FC?LF<1I%;-0CCn9%F_{HncnTf-(Xsumhxe(Td~#=EESQr|y!>n-8NWLq(8 z(cE)=eILwu^~23uHc*0%(Xnz8&GdDo?H@-2D+LV?pTFgz)ix$Nj-5EbHs;C2jdt}` zYkr_4(Qex>z}!6$bKT>IRcD|6B(?O7XQ zF`8Dz?LVAuSncU`pLV1MT1br_0?c{RyL$xn+{_OOH}^RlWc3jv3b=<=3tmlKzsXbS zid7OTZ94{>G4Y&k>?8cIOy2rzHyq(_y1vaEF@Ep8HIcI%|a#=FM} z!#$r2>G$>5d9-Lv9N=~LgzLc9z7AY^=5kZ3ubAirYzZmv`-LtJ#M_`r^hYJu1gU)lUx8uzMf26d&g z(kf>AuJCuNn0bH7SWmfX=4vUQUCn&2D<-?gQOjTGU$^6mIoB))TOl#HhMC-r9?sS< z2X&*xg&O9g!oRCwt`+`k4f8{In0*jcE8cRb=<&{@Hbhzmvd-DjYM7zj37NmI;T~2^ z>-T1#^w6ybXd7<_T3vHqZ=|!3;{SgCxOs?+Q|8q*f0w>C)-~^u zzDnzwZ}rq`n!S4JwLNS*BYQIA?$Ox!AN15;I6iE~pBvMEH1nj?Gtc+bm)tChl5hh% zhokiW&?ixIsTV56)icW)XmOMEA;HYFIP1gAG4#x^FCwf*-Qt*zt0V7!sB&#<{$rz) zxGzPR-x|!`Ul#?txV7=gA#h@}__gLwo^F>~vzIgTY+AAjfd)JY&C2v^sYpHvnf%yY{#JdN4H+}fZrTPWU zMN?<<`u`jr>(2duxo6l@5gY&aecg0>LjxRYM}ae)vF5}+M8o+=b3q>h_)4VtNgvx4 z)o({$i!^(Q{Hn3};O)5AJ;m$2KL4p7z!IC;HNbd(ya1%e^aW9o|=!it%g^c-K9f{9BIxd|rbtrzw$}7JYNW>mzO5Ur zJhA*zV8pOa)Npj)*1}A_Qy&@6(P{Z6Qar!qpt+4-?2zQqAB<&qo|slp;-zHOn~Nqr zm3<F!97F~X*^y2zMuRTc17#^%J zGA7=PPQ^c+s1Ztyz6aVbc=o#`nRX2vp}B{v&;Dy|xVK`>m)(*W&C$zTK18o=Zc5cN z<8LyTaxY}danE>n7U1L~7X2leWiwbt~?{|rHO5i_Ag5xcvBR)(IfothPk~zw~w3LMH6reYI7GIGXUk9 zw=-uB;BJ4awjD<9@nh-OHt^&2=C{<0cMo1Kd!zB1s9B|-|2edK)LWbTaP_o`t@9=K|`rwYpu2IV9t>Ma}T($IR0|p@rq*}rH1Sfgog7V>>1s` zJU!43tcrusyk!To=^*%OpLvgP9=s+9-{><-M1Jox&mhB`Bk{%U9$EQeZ2hZy=vnSC z_@I7Av-#bOT|L>h{l_s+Os!h}*@o0$o0TQJD%l)H4bR48bMoEvc|FqrYci$3O5g7J`L6piGLAI)KA#$G{&;L|br1Tve_vzxxJDh4 zD^yCPMocu`;frLLS*EJpX}eA>r5Ph33C6SeIt^j+M=EWL+O-NW|J&3r1l z`}Qx6ST)%B<uI^1fv+|->CXWV%AaQ=?1U*=SL{hBWc$sT6T5ZvMFE^FT5g?dO?2 z&97+|@1FZ#l~?t^gc-AAts2&MPWLYZ#HX%5`rNr1ayyNSw(j5Cdz$Ts{yC&4-!Svw z@$TOU_$Ksy^X-YnyJ+ET%kEzZ$a|vF`QV&sC6urg%QuB8Ddw)BY>-1!%-~^sq4_q& zz9(KwF_VWehP#ov=FJ)Qhk3E(L*+d*SKlP_vtfE?vuV2Cs`lD&`^)3B#5Se+#EQQc zJC)AOF!R&(hrPqck7-djDbHVE_Fbw+n9Yalt<62l^dxh1jvi$m8KcK+>zb>l2brBm z>dno*)AgWjo5t%`Ld|_Sdh)inAJ!-9AsJ(H_&X9AW5@8fS_;i^CjE?=sW&KZ=Z{Ix z_1l??v-JdX#YnxZx# { } tokenCache[appName] = value - console.log(`reset ${appName} access token success`, value) + loggerIns.info(`reset ${appName} access token success: ${value}`) } /** diff --git a/index.ts b/index.ts index 2ae0048..f68ae1d 100644 --- a/index.ts +++ b/index.ts @@ -1,37 +1,41 @@ +import loggerIns from "./log" import { manageBotReq } from "./routes/bot" import { manageMessageReq } from "./routes/message" import { manageMicroAppReq } from "./routes/microApp" import { manageSheetReq } from "./routes/sheet" import { initSchedule } from "./schedule" -import netTool from "./services/netTool" +import genContext from "./utils/genContext" import { makeCheckPathTool } from "./utils/pathTools" initSchedule() const server = Bun.serve({ async fetch(req) { + // 生成上下文 + const ctx = await genContext(req) try { - // 打印当前路由 - console.log("🚀 ~ serve ~ req.url", req.url) // 路由处理 - const { exactCheck, startsWithCheck } = makeCheckPathTool(req.url) + const { exactCheck, startsWithCheck, fullCheck } = makeCheckPathTool( + req.url + ) + // 非根路由打印 + if (!fullCheck("/")) ctx.logger.info(`${req.method} ${req.url}`) // 机器人 - if (exactCheck("/bot")) return await manageBotReq(req) + if (exactCheck("/bot")) return await manageBotReq(ctx) // 消息代理发送 - if (exactCheck("/message")) return await manageMessageReq(req) + if (exactCheck("/message")) return await manageMessageReq(ctx) // 表格代理操作 - if (exactCheck("/sheet")) return await manageSheetReq(req) + if (exactCheck("/sheet")) return await manageSheetReq(ctx) // 小程序 - if (startsWithCheck("/micro_app")) return await manageMicroAppReq(req) + if (startsWithCheck("/micro_app")) return await manageMicroAppReq(ctx) // 其他 - return netTool.ok("hello, there is egg, glade to serve you!") + return ctx.genResp.ok("hello, there is egg, glade to serve you!") } catch (error: any) { // 错误处理 - console.error("🚀 ~ serve ~ error", error) - return netTool.serverError(error.message || "server error") + return ctx.genResp.serverError(error.message || "server error") } }, port: 3000, }) -console.log(`Listening on ${server.hostname}:${server.port}`) +loggerIns.info(`Listening on ${server.hostname}:${server.port}`) diff --git a/log/index.ts b/log/index.ts new file mode 100644 index 0000000..dc532d4 --- /dev/null +++ b/log/index.ts @@ -0,0 +1,55 @@ +import "winston-daily-rotate-file" + +import winston, { format } from "winston" + +const isProd = process.env.NODE_ENV === "production" + +const transports: any[] = [ + new winston.transports.Console({ + level: "info", + }), +] + +if (isProd) { + const config = { + datePattern: "YYYY-MM-DD", + zippedArchive: true, + maxSize: "20m", + maxFiles: "14d", + } + + transports.push( + new winston.transports.DailyRotateFile({ + level: "info", + filename: "/home/work/log/egg-info-%DATE%.log", + ...config, + }) + ) + transports.push( + new winston.transports.DailyRotateFile({ + level: "debug", + filename: "/home/work/log/egg-debug-%DATE%.log", + ...config, + }) + ) +} + +const loggerIns = winston.createLogger({ + level: "silly", + format: format.combine( + format.colorize({ + level: !isProd, + }), // 开发环境下输出彩色日志 + format.simple(), // 简单文本格式化 + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + format.printf(({ level, message, timestamp, requestId }) => { + const singleLineMessage = isProd + ? message.replace(/\n/g, " ") // 将换行符替换为空格 + : message + return `${timestamp} [${level}]${requestId ? ` [RequestId: ${requestId}]` : ""}: ${singleLineMessage}` + }) + ), + transports, +}) + +export default loggerIns diff --git a/package.json b/package.json index 5e0d93b..c1733fa 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@commitlint/config-conventional": "^19.2.2", "@eslint/js": "^9.7.0", "@types/node-schedule": "^2.1.7", + "@types/uuid": "^10.0.0", "bun-types": "latest", "eslint": "^9.7.0", "eslint-plugin-simple-import-sort": "^12.1.1", @@ -33,6 +34,10 @@ }, "dependencies": { "node-schedule": "^2.1.1", - "pocketbase": "^0.21.3" + "p-limit": "^6.1.0", + "pocketbase": "^0.21.3", + "uuid": "^10.0.0", + "winston": "^3.14.2", + "winston-daily-rotate-file": "^5.0.0" } -} +} \ No newline at end of file diff --git a/routes/bot/actionMsg.ts b/routes/bot/actionMsg.ts index 745eae9..20a6334 100644 --- a/routes/bot/actionMsg.ts +++ b/routes/bot/actionMsg.ts @@ -1,14 +1,14 @@ import { sleep } from "bun" -import service from "../../services" -import { LarkAction } from "../../types" +import { Context, LarkAction } from "../../types" import { getActionType, getIsActionMsg } from "../../utils/msgTools" /** * 返回ChatId卡片 * @param {LarkAction.Data} body + * @returns {Promise} 返回包含ChatId卡片的JSON字符串 */ -const makeChatIdCard = async (body: LarkAction.Data) => { +const makeChatIdCard = async (body: LarkAction.Data): Promise => { await sleep(500) return JSON.stringify({ type: "template", @@ -30,34 +30,38 @@ const ACTION_MAP = { /** * 处理按钮点击事件 - * @param {LarkAction.Data} body + * @param {Context.Data} ctx - 上下文数据,包含body, larkService和logger + * @returns {Promise} 无返回值 */ -const manageBtnClick = async (body: LarkAction.Data) => { +const manageBtnClick = async ({ + body, + larkService, + logger, +}: Context.Data): Promise => { const { action } = body?.action?.value as { action: keyof typeof ACTION_MAP } + logger.info(`got button click action: ${action}`) if (!action) return const func = ACTION_MAP[action] if (!func) return const card = await func(body) if (!card) return // 更新飞书的卡片 - await service.lark.message.update()(body.open_message_id, card) + await larkService.message.update(body.open_message_id, card) } /** * 处理Action消息 - * @param {LarkAction.Data} body + * @param {Context.Data} ctx - 上下文数据 * @returns {boolean} 是否在本函数中处理了消息 */ -export const manageActionMsg = (body: LarkAction.Data) => { +export const manageActionMsg = (ctx: Context.Data): boolean => { // 过滤非Action消息 - if (!getIsActionMsg(body)) { + if (!getIsActionMsg(ctx.body)) { return false } - const actionType = getActionType(body) - if (actionType === "button") { - manageBtnClick(body) - } + const actionType = getActionType(ctx.body) + if (actionType === "button") manageBtnClick(ctx) return true } diff --git a/routes/bot/eventMsg.ts b/routes/bot/eventMsg.ts index 56e386b..4158556 100644 --- a/routes/bot/eventMsg.ts +++ b/routes/bot/eventMsg.ts @@ -1,5 +1,5 @@ -import service from "../../services" -import { LarkEvent } from "../../types" +import { LarkService } from "../../services" +import { Context, LarkEvent } from "../../types" import { getChatId, getChatType, @@ -14,27 +14,32 @@ import { * @param {LarkEvent.Data} body * @returns {boolean} 是否为P2P或者群聊并且艾特了小煎蛋 */ -const getIsP2pOrGroupAtBot = (body: LarkEvent.Data) => { +const getIsP2pOrGroupAtBot = (body: LarkEvent.Data): boolean => { const isP2p = getChatType(body) === "p2p" const isAtBot = getMentions(body)?.some?.( (mention) => mention.name === "小煎蛋" ) - return isP2p || isAtBot + return Boolean(isP2p || isAtBot) } /** * 过滤出非法消息,如果发表情包就直接发回去 - * @param {LarkEvent.Data} body + * @param {Context.Data} ctx - 上下文数据,包含body, logger和larkService * @returns {boolean} 是否为非法消息 */ -const filterIllegalMsg = (body: LarkEvent.Data) => { +const filterIllegalMsg = ({ + body, + logger, + larkService, +}: Context.Data): boolean => { // 没有chatId的消息不处理 const chatId = getChatId(body) + logger.debug(`bot req chatId: ${chatId}`) if (!chatId) return true // 获取msgType const msgType = getMsgType(body) - + logger.debug(`bot req msgType: ${msgType}`) // 放行纯文本消息 if (msgType === "text") { // 过滤艾特全体成员的消息 @@ -47,16 +52,18 @@ const filterIllegalMsg = (body: LarkEvent.Data) => { // 发表情包就直接发回去 if (msgType === "sticker") { + logger.info(`got a sticker message, chatId: ${chatId}`) const content = body?.event?.message?.content - service.lark.message.send()("chat_id", chatId, "sticker", content) + larkService.message.send("chat_id", chatId, "sticker", content) } // 非表情包只在私聊或者群聊中艾特小煎蛋时才回复 else if (getIsP2pOrGroupAtBot(body)) { + logger.info(`got a illegal message, chatId: ${chatId}`) const content = JSON.stringify({ text: "哇!这是什么东东?我只懂普通文本啦![可爱]", }) - service.lark.message.send()("chat_id", chatId, "text", content) + larkService.message.send("chat_id", chatId, "text", content) } // 非纯文本,全不放行 @@ -65,9 +72,10 @@ const filterIllegalMsg = (body: LarkEvent.Data) => { /** * 发送ID消息 - * @param chatId - 发送消息的chatId + * @param {string} chatId - 发送消息的chatId + * @param {LarkService} service - Lark服务实例 */ -const manageIdMsg = async (chatId: string) => { +const manageIdMsg = (chatId: string, service: LarkService): void => { const content = JSON.stringify({ type: "template", data: { @@ -80,33 +88,46 @@ const manageIdMsg = async (chatId: string) => { }, }, }) - service.lark.message.send()("chat_id", chatId, "interactive", content) + service.message.send("chat_id", chatId, "interactive", content) } /** * 处理命令消息 - * @param body - 消息体 - * @returns + * @param {Context.Data} ctx - 上下文数据,包含body, logger, larkService和attachService + * @returns {boolean} 是否处理了命令消息 */ -const manageCMDMsg = (body: LarkEvent.Data) => { +const manageCMDMsg = ({ + body, + logger, + larkService, + attachService, +}: Context.Data): boolean => { const text = getMsgText(body) - console.log("🚀 ~ manageCMDMsg ~ text:", text) + logger.debug(`bot req text: ${text}`) const chatId = getChatId(body) + // 处理命令消息 if (text.trim() === "/id") { - manageIdMsg(chatId) + logger.info(`bot command is /id, chatId: ${chatId}`) + manageIdMsg(chatId, larkService) return true } + // CI监控 if (text.trim() === "/ci") { - service.attach.ciMonitor(chatId) + logger.info(`bot command is /ci, chatId: ${chatId}`) + attachService.ciMonitor(chatId) return true } + // 简报 if (text.includes("share") && text.includes("简报")) { - service.attach.reportCollector(body) + logger.info(`bot command is share report, chatId: ${chatId}`) // 这个用时比较久,先发一条提醒用户收到了请求 - const content = JSON.stringify({ - text: "正在为您收集简报,请稍等片刻~", - }) - service.lark.message.send()("chat_id", chatId, "text", content) + larkService.message.send( + "chat_id", + chatId, + "text", + "正在为您收集简报,请稍等片刻~" + ) + attachService.reportCollector(body) return true } return false @@ -114,10 +135,11 @@ const manageCMDMsg = (body: LarkEvent.Data) => { /** * 回复引导消息 - * @param {LarkEvent.Data} body + * @param {Context.Data} ctx - 上下文数据,包含body, larkService和logger */ -const replyGuideMsg = async (body: LarkEvent.Data) => { +const replyGuideMsg = ({ body, larkService, logger }: Context.Data): void => { const chatId = getChatId(body) + logger.info(`reply guide message, chatId: ${chatId}`) const content = JSON.stringify({ type: "template", data: { @@ -131,28 +153,28 @@ const replyGuideMsg = async (body: LarkEvent.Data) => { }, }, }) - await service.lark.message.send()("chat_id", chatId, "interactive", content) + larkService.message.send("chat_id", chatId, "interactive", content) } /** * 处理Event消息 - * @param {LarkUserAction} body + * @param {Context.Data} ctx - 上下文数据 * @returns {boolean} 是否在本函数中处理了消息 */ -export const manageEventMsg = (body: LarkEvent.Data) => { +export const manageEventMsg = (ctx: Context.Data): boolean => { // 过滤非Event消息 - if (!getIsEventMsg(body)) { + if (!getIsEventMsg(ctx.body)) { return false } // 过滤非法消息 - if (filterIllegalMsg(body)) { + if (filterIllegalMsg(ctx)) { return true } // 处理命令消息 - if (manageCMDMsg(body)) { + if (manageCMDMsg(ctx)) { return true } // 返回引导消息 - replyGuideMsg(body) + replyGuideMsg(ctx) return true } diff --git a/routes/bot/index.ts b/routes/bot/index.ts index 664a861..86147df 100644 --- a/routes/bot/index.ts +++ b/routes/bot/index.ts @@ -1,18 +1,32 @@ -import netTool from "../../services/netTool" +import { Context } from "../../types" import { manageActionMsg } from "./actionMsg" import { manageEventMsg } from "./eventMsg" -export const manageBotReq = async (req: Request) => { - const body = (await req.json()) as any - console.log("🚀 ~ manageBotReq ~ body:", body) - // 验证机器人 - if (body?.type === "url_verification") { - return Response.json({ challenge: body?.challenge }) +/** + * 处理机器人请求 + * @param {Context.Data} ctx - 上下文数据,包含请求体、日志记录器和响应生成器 + * @returns {Promise} 返回响应对象 + */ +export const manageBotReq = async (ctx: Context.Data): Promise => { + const { body } = ctx + + // 检查请求体是否为空 + if (!body) { + return ctx.genResp.badRequest("bot req body is empty") } + + // 验证机器人 + if (body.type === "url_verification") { + ctx.logger.info(`bot challenge: ${body.challenge}`) + return Response.json({ challenge: body.challenge }) + } + // 处理Event消息 - if (manageEventMsg(body)) return netTool.ok() + if (manageEventMsg(ctx)) return ctx.genResp.ok() + // 处理Action消息 - if (manageActionMsg(body)) return netTool.ok() - // 其他 - return netTool.ok() + if (manageActionMsg(ctx)) return ctx.genResp.ok() + + // 其他情况,返回成功响应 + return ctx.genResp.ok() } diff --git a/routes/message/index.ts b/routes/message/index.ts index e65b727..ce8281a 100644 --- a/routes/message/index.ts +++ b/routes/message/index.ts @@ -1,34 +1,49 @@ import db from "../../db" -import service from "../../services" -import netTool from "../../services/netTool" -import { DB, LarkServer, MsgProxy } from "../../types" +import { Context, DB, LarkServer, MsgProxy } from "../../types" import { safeJsonStringify } from "../../utils/pathTools" const LOG_COLLECTION = "message_log" -const validateMessageReq = (body: MsgProxy.Body) => { +/** + * 校验消息请求的参数 + * @param {Context.Data} ctx - 上下文数据,包含请求体和响应生成器 + * @returns {false | Response} 如果校验失败,返回响应对象;否则返回 false + */ +const validateMessageReq = ({ + body, + genResp, +}: Context.Data): false | Response => { if (!body.api_key) { - return netTool.badRequest("api_key is required") + return genResp.badRequest("api_key is required") } if (!body.group_id && !body.receive_id) { - return netTool.badRequest("group_id or receive_id is required") + return genResp.badRequest("group_id or receive_id is required") } if (body.receive_id && !body.receive_id_type) { - return netTool.badRequest("receive_id_type is required") + return genResp.badRequest("receive_id_type is required") } if (!body.msg_type) { - return netTool.badRequest("msg_type is required") + return genResp.badRequest("msg_type is required") } if (!body.content) { - return netTool.badRequest("content is required") + return genResp.badRequest("content is required") } return false } -export const manageMessageReq = async (req: Request) => { - const body = (await req.json()) as MsgProxy.Body +/** + * 处理消息请求 + * @param {Context.Data} ctx - 上下文数据,包含请求体、日志记录器、响应生成器和 Lark 服务 + * @returns {Promise} 返回响应对象 + */ +export const manageMessageReq = async ( + ctx: Context.Data +): Promise => { + const { body: rawBody, genResp, larkService } = ctx + const body = rawBody as MsgProxy.Body + // 校验参数 - const validateRes = validateMessageReq(body) + const validateRes = validateMessageReq(ctx) if (validateRes) return validateRes // 处理消息内容 @@ -37,7 +52,7 @@ export const manageMessageReq = async (req: Request) => { ? safeJsonStringify(body.content) : body.content - // 遍历所有id发送消息,保存所有对应的messageId + // 初始化发送结果对象 const sendRes = { chat_id: {} as Record, open_id: {} as Record, @@ -55,30 +70,30 @@ export const manageMessageReq = async (req: Request) => { final_content: finalContent, } - // 校验api_key + // 校验 api_key const apiKeyInfo = await db.apiKey.getOne(body.api_key) if (!apiKeyInfo) { const error = "api key not found" db.log.create(LOG_COLLECTION, { ...baseLog, error }) - return netTool.notFound(error) + return genResp.notFound(error) } - // 获取app name + // 获取 app name const appName = apiKeyInfo.expand?.app?.name if (!appName) { const error = "app name not found" db.log.create(LOG_COLLECTION, { ...baseLog, error }) - return netTool.notFound(error) + return genResp.notFound(error) } - // 如果有group_id,则发送给所有group_id中的人 + // 如果有 group_id,则发送给所有 group_id 中的人 if (body.group_id) { // 获取所有接收者 const group = await db.messageGroup.getOne(body.group_id!) if (!group) { const error = "message group not found" db.log.create(LOG_COLLECTION, { ...baseLog, error }) - return netTool.notFound(error) + return genResp.notFound(error) } const { chat_id, open_id, union_id, user_id, email } = group @@ -87,8 +102,9 @@ export const manageMessageReq = async (req: Request) => { const makeSendFunc = (receive_id_type: LarkServer.ReceiveIDType) => { return (receive_id: string) => { sendList.push( - service.lark.message - .send(appName)( + larkService + .child(appName) + .message.send( receive_id_type, receive_id, body.msg_type, @@ -109,12 +125,13 @@ export const manageMessageReq = async (req: Request) => { if (email) email.map(makeSendFunc("email")) } - // 如果有receive_id,则发送给所有receive_id中的人 + // 如果有 receive_id,则发送给所有 receive_id 中的人 if (body.receive_id && body.receive_id_type) { body.receive_id.split(",").forEach((receive_id) => { sendList.push( - service.lark.message - .send(appName)( + larkService + .child(appName) + .message.send( body.receive_id_type, receive_id, body.msg_type, @@ -128,14 +145,14 @@ export const manageMessageReq = async (req: Request) => { } try { - // 里边有错误处理,这里不用担心执行不完 - await Promise.all(sendList) + // 发送消息 + await Promise.allSettled(sendList) // 保存消息记录 db.log.create(LOG_COLLECTION, { ...baseLog, send_result: sendRes }) - return netTool.ok(sendRes) + return genResp.ok(sendRes) } catch { const error = "send msg failed" db.log.create(LOG_COLLECTION, { ...baseLog, error }) - return netTool.serverError(error, sendRes) + return genResp.serverError(error, sendRes) } } diff --git a/routes/microApp/index.ts b/routes/microApp/index.ts index 1fed703..9f6ef0e 100644 --- a/routes/microApp/index.ts +++ b/routes/microApp/index.ts @@ -1,5 +1,4 @@ -import service from "../../services" -import netTool from "../../services/netTool" +import { Context } from "../../types" import { makeCheckPathTool } from "../../utils/pathTools" /** @@ -7,34 +6,28 @@ import { makeCheckPathTool } from "../../utils/pathTools" * @param req * @returns */ -const manageLogin = async (req: Request) => { +const manageLogin = async (ctx: Context.Data) => { + const { req, larkService, genResp, logger } = ctx + logger.info("micro app login") const url = new URL(req.url) const code = url.searchParams.get("code") const appName = url.searchParams.get("app_name") || undefined if (!code) { - return netTool.badRequest("code not found") + return genResp.badRequest("code not found") } const { code: resCode, data, - msg, - } = await service.lark.user.code2Login(appName)(code) + message, + } = await larkService.child(appName).user.code2Login(code) - console.log("🚀 ~ manageLogin:", resCode, data, msg) + logger.debug(`get user session: ${JSON.stringify(data)}`) if (resCode !== 0) { - return Response.json({ - code: resCode, - message: msg, - data: null, - }) + return genResp.serverError(message) } - return Response.json({ - code: 0, - message: "success", - data, - }) + return genResp.ok(data) } /** @@ -42,35 +35,29 @@ const manageLogin = async (req: Request) => { * @param req * @returns */ -const manageBatchUser = async (req: Request) => { - const body = (await req.json()) as any - console.log("🚀 ~ manageBatchUser ~ body:", body) +const manageBatchUser = async (ctx: Context.Data) => { + const { body, genResp, larkService, logger } = ctx + logger.info("batch get user info") + if (!body) return genResp.badRequest("req body is empty") const { user_ids, user_id_type, app_name } = body if (!user_ids) { - return netTool.badRequest("user_ids not found") + return genResp.badRequest("user_ids not found") } if (!user_id_type) { - return netTool.badRequest("user_id_type not found") + return genResp.badRequest("user_id_type not found") } - const { code, data, msg } = await service.lark.user.batchGet(app_name)( - user_ids, - user_id_type - ) + const { code, data, message } = await larkService + .child(app_name) + .user.batchGet(user_ids, user_id_type) + + logger.debug(`batch get user info: ${JSON.stringify(data)}`) - console.log("🚀 ~ manageBatchUser:", code, data, msg) if (code !== 0) { - return Response.json({ - code, - message: msg, - data: null, - }) + return genResp.serverError(message) } - return Response.json({ - code, - message: "success", - data: data.items, - }) + + return genResp.ok(data) } /** @@ -78,15 +65,15 @@ const manageBatchUser = async (req: Request) => { * @param req * @returns */ -export const manageMicroAppReq = async (req: Request) => { - const { exactCheck } = makeCheckPathTool(req.url, "/micro_app") +export const manageMicroAppReq = async (ctx: Context.Data) => { + const { exactCheck } = makeCheckPathTool(ctx.req.url, "/micro_app") // 处理登录请求 if (exactCheck("/login")) { - return manageLogin(req) + return manageLogin(ctx) } // 处理批量获取用户信息请求 if (exactCheck("/batch_user")) { - return manageBatchUser(req) + return manageBatchUser(ctx) } - return netTool.ok() + return ctx.genResp.ok() } diff --git a/routes/sheet/index.ts b/routes/sheet/index.ts index fa8a45b..2a80c57 100644 --- a/routes/sheet/index.ts +++ b/routes/sheet/index.ts @@ -1,59 +1,72 @@ import db from "../../db" -import service from "../../services" -import netTool from "../../services/netTool" +import { Context } from "../../types" import { SheetProxy } from "../../types/sheetProxy" -const validateSheetReq = async (body: SheetProxy.Body) => { +/** + * 校验表格请求的参数 + * @param {Context.Data} ctx - 上下文数据,包含请求体和响应生成器 + * @returns {Promise} 如果校验失败,返回响应对象;否则返回 false + */ +const validateSheetReq = async ( + ctx: Context.Data +): Promise => { + const { body, genResp } = ctx if (!body.api_key) { - return netTool.badRequest("api_key is required") + return genResp.badRequest("api_key is required") } if (!body.sheet_token) { - return netTool.badRequest("sheet_token is required") + return genResp.badRequest("sheet_token is required") } if (!body.range) { - return netTool.badRequest("range is required") + return genResp.badRequest("range is required") } if (!body.values) { - return netTool.badRequest("values is required") + return genResp.badRequest("values is required") } if (!SheetProxy.isType(body.type)) { - return netTool.badRequest("type is invalid") + return genResp.badRequest("type is invalid") } return false } -export const manageSheetReq = async (req: Request) => { - const body = (await req.json()) as SheetProxy.Body +/** + * 处理表格请求 + * @param {Context.Data} ctx - 上下文数据,包含请求体、响应生成器和 Lark 服务 + * @returns {Promise} 返回响应对象 + */ +export const manageSheetReq = async (ctx: Context.Data): Promise => { + const { body: rawBody, genResp, larkService } = ctx + const body = rawBody as SheetProxy.Body + // 校验参数 - const validateRes = await validateSheetReq(body) + const validateRes = await validateSheetReq(ctx) if (validateRes) return validateRes - // 校验api_key + // 校验 api_key const apiKeyInfo = await db.apiKey.getOne(body.api_key) if (!apiKeyInfo) { - return netTool.notFound("api key not found") + return genResp.notFound("api key not found") } // 获取 app name const appName = apiKeyInfo.expand?.app?.name if (!appName) { - return netTool.notFound("app name not found") + return genResp.notFound("app name not found") } if (body.type === "insert") { // 插入行 - const insertRes = await service.lark.sheet.insertRows(appName)( - body.sheet_token, - body.range, - body.values - ) + const insertRes = await larkService + .child(appName) + .sheet.insertRows(body.sheet_token, body.range, body.values) if (insertRes?.code !== 0) { - return netTool.serverError(insertRes?.msg, insertRes?.data) + return genResp.serverError(insertRes?.message) } - // 返回 - return netTool.ok(insertRes?.data) + // 返回插入结果 + return genResp.ok(insertRes?.data) } - return netTool.ok() + // 默认返回成功响应 + return genResp.ok() } diff --git a/schedule/accessToken.ts b/schedule/accessToken.ts index 6ac783c..ec149e8 100644 --- a/schedule/accessToken.ts +++ b/schedule/accessToken.ts @@ -1,20 +1,30 @@ -import db from "../db" -import netTool from "../services/netTool" +import pLimit from "p-limit" -const URL = - "https://open.f.mioffice.cn/open-apis/auth/v3/tenant_access_token/internal" +import db from "../db" +import loggerIns from "../log" +import { LarkService } from "../services" export const resetAccessToken = async () => { try { const appList = await db.appInfo.getFullList() - for (const app of appList) { - const { tenant_access_token } = await netTool.post(URL, { - app_id: app.app_id, - app_secret: app.app_secret, - }) - await db.tenantAccessToken.update(app.id, app.name, tenant_access_token) - } - } catch (error) { - console.error("🚀 ~ resetAccessToken ~ error", error) + const limit = pLimit(3) + const service = new LarkService("", "schedule") + const promiseList = appList.map((app) => + limit(() => + service.auth.getAk(app.app_id, app.app_secret).then((res) => { + if (res.code !== 0) return + return db.tenantAccessToken.update( + app.id, + app.name, + res.tenant_access_token + ) + }) + ) + ) + await Promise.allSettled(promiseList) + } catch (error: any) { + loggerIns + .child({ requestId: "schedule" }) + .error(`resetAccessToken error: ${error.message}`) } } diff --git a/services/attach/index.ts b/services/attach/index.ts index c650c21..b230121 100644 --- a/services/attach/index.ts +++ b/services/attach/index.ts @@ -1,37 +1,26 @@ import { LarkEvent } from "../../types" -import netTool from "../netTool" +import { NetToolBase } from "../../utils/netTool" -/** - * 请求 CI 监控 - */ -const ciMonitor = async (chat_id: string) => { - const URL = `https://ci-monitor.xiaomiwh.cn/gitlab/ci?chat_id=${chat_id}` - try { - const res = await netTool.get(URL) - return (res as string) || "" - } catch { - return "" +class AttachService extends NetToolBase { + /** + * 监控CI状态 + * @param {string} chat_id - 聊天ID。 + * @returns {Promise} 返回CI监控结果。 + */ + async ciMonitor(chat_id: string) { + const URL = `https://ci-monitor.xiaomiwh.cn/gitlab/ci?chat_id=${chat_id}` + return this.get(URL).catch(() => "") + } + + /** + * 收集报告数据 + * @param {LarkEvent.Data} body - 报告数据。 + * @returns {Promise} 返回报告收集结果。 + */ + async reportCollector(body: LarkEvent.Data) { + const URL = "https://report.imoaix.cn/report" + return this.post(URL, body).catch(() => "") } } -/** - * 请求简报收集器 - * @param body - * @returns - */ -const reportCollector = async (body: LarkEvent.Data) => { - const URL = "https://report.imoaix.cn/report" - try { - const res = await netTool.post(URL, body) - return (res as string) || "" - } catch { - return "" - } -} - -const attach = { - ciMonitor, - reportCollector, -} - -export default attach +export default AttachService diff --git a/services/index.ts b/services/index.ts index c6eee96..0b646ab 100644 --- a/services/index.ts +++ b/services/index.ts @@ -1,9 +1,4 @@ -import attach from "./attach" -import lark from "./lark" +import AttachService from "./attach" +import LarkService from "./lark" -const service = { - attach, - lark, -} - -export default service +export { AttachService, LarkService } diff --git a/services/lark/auth.ts b/services/lark/auth.ts new file mode 100644 index 0000000..7cee1be --- /dev/null +++ b/services/lark/auth.ts @@ -0,0 +1,15 @@ +import LarkBaseService from "./base" + +class LarkAuthService extends LarkBaseService { + getAk(app_id: string, app_secret: string) { + return this.post<{ tenant_access_token: string; code: number }>( + "/auth/v3/tenant_access_token/internal", + { + app_id, + app_secret, + } + ) + } +} + +export default LarkAuthService diff --git a/services/lark/base.ts b/services/lark/base.ts new file mode 100644 index 0000000..1b5a224 --- /dev/null +++ b/services/lark/base.ts @@ -0,0 +1,28 @@ +import db from "../../db" +import { NetError, NetToolBase } from "../../utils/netTool" + +class LarkBaseService extends NetToolBase { + constructor(appName: string, requestId: string) { + super({ + prefix: "https://open.f.mioffice.cn/open-apis", + requestId, + getHeaders: async () => ({ + Authorization: `Bearer ${await db.tenantAccessToken.get(appName)}`, + }), + }) + } + + protected async request(params: any): Promise { + return super.request(params).catch((error: NetError) => { + const res = { + code: error.code, + data: null, + message: error.message, + } as T + this.logger.error("larkNetTool catch error: ", JSON.stringify(res)) + return res + }) + } +} + +export default LarkBaseService diff --git a/services/lark/drive.ts b/services/lark/drive.ts index 610c62c..30d2054 100644 --- a/services/lark/drive.ts +++ b/services/lark/drive.ts @@ -1,11 +1,21 @@ import { LarkServer } from "../../types" -import larkNetTool from "./larkNetTool" +import LarkBaseService from "./base" -const batchGetMeta = - (appName?: string) => - async (docTokens: string[], doc_type = "doc", user_id_type = "user_id") => { - const URL = - "https://open.f.mioffice.cn/open-apis/drive/v1/metas/batch_query" +class LarkDriveService extends LarkBaseService { + /** + * 批量获取文档元数据。 + * + * @param {string[]} docTokens - 文档令牌数组。 + * @param {string} [doc_type="doc"] - 文档类型,默认为 "doc"。 + * @param {string} [user_id_type="user_id"] - 用户ID类型,默认为 "user_id"。 + * @returns {Promise<{ code: number, data: { metas: any[], failed_list: any[] }, message: string }>} 包含元数据和失败列表的响应对象。 + */ + async batchGetMeta( + docTokens: string[], + doc_type = "doc", + user_id_type = "user_id" + ) { + const path = "/drive/v1/metas/batch_query" // 如果docTokens长度超出150,需要分批请求 const docTokensLen = docTokens.length const maxLen = 150 @@ -20,13 +30,9 @@ const batchGetMeta = doc_type, })), } - return larkNetTool.post(appName)( - URL, - data, - { - user_id_type, - } - ) + return this.post(path, data, { + user_id_type, + }) } ) const responses = await Promise.all(requestMap) @@ -44,12 +50,9 @@ const batchGetMeta = metas, failed_list, }, - msg: "success", + message: "success", } } - -const drive = { - batchGetMeta, } -export default drive +export default LarkDriveService diff --git a/services/lark/index.ts b/services/lark/index.ts index d04fa35..39143c1 100644 --- a/services/lark/index.ts +++ b/services/lark/index.ts @@ -1,13 +1,30 @@ -import drive from "./drive" -import message from "./message" -import sheet from "./sheet" -import user from "./user" +import LarkAuthService from "./auth" +import LarkDriveService from "./drive" +import LarkMessageService from "./message" +import LarkSheetService from "./sheet" +import LarkUserService from "./user" -const lark = { - message, - user, - drive, - sheet, +class LarkService { + drive: LarkDriveService + message: LarkMessageService + user: LarkUserService + sheet: LarkSheetService + auth: LarkAuthService + requestId: string + + constructor(appName: string, requestId: string) { + this.drive = new LarkDriveService(appName, requestId) + this.message = new LarkMessageService(appName, requestId) + this.user = new LarkUserService(appName, requestId) + this.sheet = new LarkSheetService(appName, requestId) + this.auth = new LarkAuthService(appName, requestId) + this.requestId = requestId + } + + child(appName?: string) { + if (!appName) return this + return new LarkService(appName, this.requestId) + } } -export default lark +export default LarkService diff --git a/services/lark/larkNetTool.ts b/services/lark/larkNetTool.ts deleted file mode 100644 index 66cf818..0000000 --- a/services/lark/larkNetTool.ts +++ /dev/null @@ -1,125 +0,0 @@ -import db from "../../db" -import { LarkServer } from "../../types" -import netTool from "../netTool" - -/** - * 发送网络请求并返回一个解析为响应数据的Promise。 - * @param url - 要发送请求的URL。 - * @param method - 请求使用的HTTP方法。 - * @param queryParams - 要包含在URL中的查询参数。 - * @param payload - 请求的有效负载数据。 - * @param additionalHeaders - 要包含在请求中的附加头。 - * @param appName - 应用名称,用于获取授权令牌。 - * @returns 一个解析为响应数据的Promise。 - * @throws 如果网络响应不成功或存在解析错误,则抛出错误。 - */ -const larkNetTool = async ({ - url, - method, - queryParams, - payload, - additionalHeaders, - appName = "egg", -}: { - url: string - method: string - queryParams?: any - payload?: any - additionalHeaders?: any - appName?: string -}): Promise => { - const headersWithAuth = { - Authorization: `Bearer ${await db.tenantAccessToken.get(appName)}`, - ...additionalHeaders, - } - return netTool({ - url, - method, - queryParams, - payload, - additionalHeaders: headersWithAuth, - }).catch((error) => { - console.error("larkNetTool catch error: ", error) - return { - code: 1, - data: null, - msg: error.message || "网络请求异常", - } as T - }) -} - -/** - * 发送GET请求并返回一个解析为响应数据的Promise。 - * - * @param appName - 应用名称,用于获取授权令牌。 - * @returns 一个函数,该函数接受URL、查询参数和附加头,并返回一个解析为响应数据的Promise。 - */ -larkNetTool.get = - (appName?: string) => - ( - url: string, - queryParams?: any, - additionalHeaders?: any - ): Promise => - larkNetTool({ - url, - method: "get", - queryParams, - additionalHeaders, - appName, - }) - -/** - * 发送POST请求并返回一个解析为响应数据的Promise。 - * - * @param appName - 应用名称,用于获取授权令牌。 - * @returns 一个函数,该函数接受URL、有效负载、查询参数和附加头,并返回一个解析为响应数据的Promise。 - */ -larkNetTool.post = - (appName?: string) => - ( - url: string, - payload?: any, - queryParams?: any, - additionalHeaders?: any - ): Promise => - larkNetTool({ - url, - method: "post", - payload, - queryParams, - additionalHeaders, - appName, - }) - -/** - * 发送DELETE请求并返回一个解析为响应数据的Promise。 - * - * @param appName - 应用名称,用于获取授权令牌。 - * @returns 一个函数,该函数接受URL、有效负载和附加头,并返回一个解析为响应数据的Promise。 - */ -larkNetTool.del = - (appName?: string) => - ( - url: string, - payload: any, - additionalHeaders?: any - ): Promise => - larkNetTool({ url, method: "delete", payload, additionalHeaders, appName }) - -/** - * 发送PATCH请求并返回一个解析为响应数据的Promise。 - * - * @param appName - 应用名称,用于获取授权令牌。 - * @returns 一个函数,该函数接受URL、有效负载和附加头,并返回一个解析为响应数据的Promise。 - */ -larkNetTool.patch = - (appName?: string) => - ( - url: string, - payload: any, - additionalHeaders?: any - ): Promise => - larkNetTool({ url, method: "patch", payload, additionalHeaders, appName }) - -export default larkNetTool diff --git a/services/lark/message.ts b/services/lark/message.ts index f9b8c52..8fb0802 100644 --- a/services/lark/message.ts +++ b/services/lark/message.ts @@ -1,46 +1,40 @@ import { LarkServer } from "../../types/larkServer" -import larkNetTool from "./larkNetTool" +import LarkBaseService from "./base" -/** - * 发送卡片 - * @param {LarkServer.ReceiveIDType} receive_id_type 消息接收者id类型 open_id/user_id/union_id/email/chat_id - * @param {string} receive_id 消息接收者的ID,ID类型应与查询参数receive_id_type 对应 - * @param {MsgType} msg_type 消息类型 包括:text、post、image、file、audio、media、sticker、interactive、share_chat、share_user - * @param {string} content 消息内容,JSON结构序列化后的字符串。不同msg_type对应不同内容 - */ -const send = - (appName?: string) => - async ( +class LarkMessageService extends LarkBaseService { + /** + * 发送卡片 + * @param {LarkServer.ReceiveIDType} receive_id_type 消息接收者id类型 open_id/user_id/union_id/email/chat_id + * @param {string} receive_id 消息接收者的ID,ID类型应与查询参数receive_id_type 对应 + * @param {MsgType} msg_type 消息类型 包括:text、post、image、file、audio、media、sticker、interactive、share_chat、share_user + * @param {string} content 消息内容,JSON结构序列化后的字符串。不同msg_type对应不同内容 + */ + async send( receive_id_type: LarkServer.ReceiveIDType, receive_id: string, msg_type: LarkServer.MsgType, content: string - ) => { - const URL = `https://open.f.mioffice.cn/open-apis/im/v1/messages?receive_id_type=${receive_id_type}` + ) { + const path = `/im/v1/messages?receive_id_type=${receive_id_type}` if (msg_type === "text" && !content.includes('"text"')) { content = JSON.stringify({ text: content }) } - return larkNetTool.post(appName)(URL, { + return this.post(path, { receive_id, msg_type, content, }) } -/** - * 更新卡片 - * @param {string} message_id 消息id - * @param {string} content 消息内容,JSON结构序列化后的字符串。不同msg_type对应不同内容 - */ -const update = - (appName?: string) => async (message_id: string, content: string) => { - const URL = `https://open.f.mioffice.cn/open-apis/im/v1/messages/${message_id}` - return larkNetTool.patch(appName)(URL, { content }) + /** + * 更新卡片 + * @param {string} message_id 消息id + * @param {string} content 消息内容,JSON结构序列化后的字符串。不同msg_type对应不同内容 + */ + async update(message_id: string, content: string) { + const path = `/im/v1/messages/${message_id}` + return this.patch(path, { content }) } - -const message = { - send, - update, } -export default message +export default LarkMessageService diff --git a/services/lark/sheet.ts b/services/lark/sheet.ts index 8ec5eab..de97dba 100644 --- a/services/lark/sheet.ts +++ b/services/lark/sheet.ts @@ -1,16 +1,17 @@ import { LarkServer } from "../../types/larkServer" -import larkNetTool from "./larkNetTool" +import LarkBaseService from "./base" -/** - * 向电子表格中插入行。 - * @param appName - 应用程序的名称(可选)。 - * @returns 一个函数,该函数接受表格令牌、范围和要插入的值。 - */ -const insertRows = - (appName?: string) => - async (sheetToken: string, range: string, values: string[][]) => { - const URL = `https://open.f.mioffice.cn/open-apis/sheets/v2/spreadsheets/${sheetToken}/values_append?insertDataOption=INSERT_ROWS` - return larkNetTool.post(appName)(URL, { +class LarkSheetService extends LarkBaseService { + /** + * 向电子表格中插入行。 + * @param {string} sheetToken - 表格令牌。 + * @param {string} range - 插入数据的范围。 + * @param {string[][]} values - 要插入的值。 + * @returns {Promise} 返回一个包含响应数据的Promise。 + */ + async insertRows(sheetToken: string, range: string, values: string[][]) { + const path = `/sheets/v2/spreadsheets/${sheetToken}/values_append?insertDataOption=INSERT_ROWS` + return this.post(path, { valueRange: { range, values, @@ -18,8 +19,16 @@ const insertRows = }) } -const sheet = { - insertRows, + /** + * 获取指定范围内的电子表格数据。 + * @param {string} sheetToken - 表格令牌。 + * @param {string} range - 要获取数据的范围。 + * @returns {Promise} 返回一个包含响应数据的Promise。 + */ + async getRange(sheetToken: string, range: string) { + const path = `/sheets/v2/spreadsheets/${sheetToken}/values/${range}?valueRenderOption=ToString` + return this.get(path) + } } -export default sheet +export default LarkSheetService diff --git a/services/lark/user.ts b/services/lark/user.ts index bca43a0..0a0e332 100644 --- a/services/lark/user.ts +++ b/services/lark/user.ts @@ -1,39 +1,38 @@ import { LarkServer } from "../../types/larkServer" -import larkNetTool from "./larkNetTool" +import LarkBaseService from "./base" -/** - * 登录凭证校验 - * @param code - * @returns - */ -const code2Login = (appName?: string) => async (code: string) => { - const URL = `https://open.f.mioffice.cn/open-apis/mina/v2/tokenLoginValidate` - return larkNetTool.post(appName)(URL, { code }) -} +class LarkUserService extends LarkBaseService { + /** + * 登录凭证校验 + * @param {string} code 登录凭证 + * @returns + */ + async code2Login(code: string) { + const path = `/mina/v2/tokenLoginValidate` + return this.post(path, { code }) + } -/** - * 获取用户信息 - * @param user_id - * @returns - */ -const get = - (appName?: string) => - async (user_id: string, user_id_type: "open_id" | "user_id") => { - const URL = `https://open.f.mioffice.cn/open-apis/contact/v3/users/${user_id}` - return larkNetTool.get(appName)(URL, { + /** + * 获取用户信息 + * @param {string} user_id 用户ID + * @param {"open_id" | "user_id"} user_id_type 用户ID类型 + * @returns + */ + async getOne(user_id: string, user_id_type: "open_id" | "user_id") { + const path = `/contact/v3/users/${user_id}` + return this.get(path, { user_id_type, }) } -/** - * 批量获取用户信息 - * @param user_ids - * @returns - */ -const batchGet = - (appName?: string) => - async (user_ids: string[], user_id_type: "open_id" | "user_id") => { - const URL = `https://open.f.mioffice.cn/open-apis/contact/v3/users/batch` + /** + * 批量获取用户信息 + * @param {string[]} user_ids 用户ID数组 + * @param {"open_id" | "user_id"} user_id_type 用户ID类型 + * @returns + */ + async batchGet(user_ids: string[], user_id_type: "open_id" | "user_id") { + const path = `/contact/v3/users/batch` // 如果user_id长度超出50,需要分批请求, const userCount = user_ids.length @@ -47,10 +46,7 @@ const batchGet = const getParams = `${user_idsSlice .map((id) => `user_ids=${id}`) .join("&")}&user_id_type=${user_id_type}` - return larkNetTool.get(appName)( - URL, - getParams - ) + return this.get(path, getParams) } ) @@ -65,14 +61,9 @@ const batchGet = data: { items, }, - msg: "success", + message: "success", } } - -const user = { - code2Login, - batchGet, - get, } -export default user +export default LarkUserService diff --git a/services/netTool.ts b/services/netTool.ts deleted file mode 100644 index 082a37f..0000000 --- a/services/netTool.ts +++ /dev/null @@ -1,232 +0,0 @@ -interface NetRequestParams { - url: string - method: string - queryParams?: any - payload?: any - additionalHeaders?: any -} - -/** - * 记录响应详情并返回响应日志对象。 - * @param response - 响应对象。 - * @param method - 请求使用的HTTP方法。 - * @param headers - 请求头。 - * @param requestBody - 请求体。 - * @param responseBody - 响应体。 - * @returns 响应日志对象。 - */ -const logResponse = ( - response: Response, - method: string, - headers: any, - requestBody: any, - responseBody: any -) => { - const responseLog = { - ok: response.ok, - status: response.status, - statusText: response.statusText, - url: response.url, - method: method, - requestHeaders: headers, - responseHeaders: response.headers, - requestBody, - responseBody, - } - console.log("🚀 ~ responseLog:", JSON.stringify(responseLog, null, 2)) - return responseLog -} - -/** - * 发送网络请求并返回一个解析为响应数据的Promise。 - * @param url - 要发送请求的URL。 - * @param method - 请求使用的HTTP方法。 - * @param queryParams - 要包含在URL中的查询参数。 - * @param payload - 请求的有效负载数据。 - * @param additionalHeaders - 要包含在请求中的附加头。 - * @returns 一个解析为响应数据的Promise。 - * @throws 如果网络响应不成功或存在解析错误,则抛出错误。 - */ -const netTool = async ({ - url, - method, - queryParams, - payload, - additionalHeaders, -}: NetRequestParams): Promise => { - // 拼接完整的URL - let fullUrl = url - if (queryParams) { - if (typeof queryParams === "string") { - fullUrl = `${url}?${queryParams}` - } else { - const queryString = new URLSearchParams(queryParams).toString() - if (queryString) fullUrl = `${url}?${queryString}` - } - } - - // 设置请求头 - const headers = { - "Content-Type": "application/json", - ...additionalHeaders, - } - - // 发送请求 - const res = await fetch(fullUrl, { - method, - body: JSON.stringify(payload), - headers, - }) - // 获取响应数据 - let resData: any = null - let resText: string = "" - - try { - resText = await res.text() - resData = JSON.parse(resText) - } catch { - /* empty */ - } - - // 记录响应 - logResponse(res, method, headers, payload, resData || resText) - if (!res.ok) { - if (resData?.msg) { - throw new Error(resData.msg) - } - if (resText) { - throw new Error(resText) - } - throw new Error("网络响应异常") - } - // http 错误码正常,但解析异常 - if (!resData) { - throw new Error("解析响应数据异常") - } - return resData as T -} - -/** - * 发送GET请求并返回一个解析为响应数据的Promise。 - * - * @param url - 要发送请求的URL。 - * @param queryParams - 要包含在URL中的查询参数。 - * @param additionalHeaders - 要包含在请求中的附加头。 - * @returns 一个解析为响应数据的Promise。 - */ -netTool.get = ( - url: string, - queryParams?: any, - additionalHeaders?: any -): Promise => netTool({ url, method: "get", queryParams, additionalHeaders }) - -/** - * 发送POST请求并返回一个解析为响应数据的Promise。 - * - * @param url - 要发送请求的URL。 - * @param payload - 请求的有效负载数据。 - * @param queryParams - 要包含在URL中的查询参数。 - * @param additionalHeaders - 要包含在请求中的附加头。 - * @returns 一个解析为响应数据的Promise。 - */ -netTool.post = ( - url: string, - payload?: any, - queryParams?: any, - additionalHeaders?: any -): Promise => - netTool({ url, method: "post", payload, queryParams, additionalHeaders }) - -/** - * 发送PUT请求并返回一个解析为响应数据的Promise。 - * - * @param url - 要发送请求的URL。 - * @param payload - 请求的有效负载数据。 - * @param queryParams - 要包含在URL中的查询参数。 - * @param additionalHeaders - 要包含在请求中的附加头。 - * @returns 一个解析为响应数据的Promise。 - */ -netTool.put = ( - url: string, - payload: any, - queryParams?: any, - additionalHeaders?: any -): Promise => - netTool({ url, method: "put", payload, queryParams, additionalHeaders }) - -/** - * 发送DELETE请求并返回一个解析为响应数据的Promise。 - * - * @param url - 要发送请求的URL。 - * @param payload - 请求的有效负载数据。 - * @param queryParams - 要包含在URL中的查询参数。 - * @param additionalHeaders - 要包含在请求中的附加头。 - * @returns 一个解析为响应数据的Promise。 - */ -netTool.del = ( - url: string, - payload: any, - queryParams?: any, - additionalHeaders?: any -): Promise => - netTool({ url, method: "delete", payload, queryParams, additionalHeaders }) - -/** - * 发送PATCH请求并返回一个解析为响应数据的Promise。 - * - * @param url - 要发送请求的URL。 - * @param payload - 请求的有效负载数据。 - * @param queryParams - 要包含在URL中的查询参数。 - * @param additionalHeaders - 要包含在请求中的附加头。 - * @returns 一个解析为响应数据的Promise。 - */ -netTool.patch = ( - url: string, - payload: any, - queryParams?: any, - additionalHeaders?: any -): Promise => - netTool({ url, method: "patch", payload, queryParams, additionalHeaders }) - -/** - * 创建一个表示400 Bad Request的响应对象。 - * - * @param msg - 错误消息。 - * @param requestId - 请求ID。 - * @returns 一个表示400 Bad Request的响应对象。 - */ -netTool.badRequest = (msg: string, requestId?: string) => - Response.json({ code: 400, msg, requestId }, { status: 400 }) - -/** - * 创建一个表示404 Not Found的响应对象。 - * - * @param msg - 错误消息。 - * @param requestId - 请求ID。 - * @returns 一个表示404 Not Found的响应对象。 - */ -netTool.notFound = (msg: string, requestId?: string) => - Response.json({ code: 404, msg, requestId }, { status: 404 }) - -/** - * 创建一个表示500 Internal Server Error的响应对象。 - * - * @param msg - 错误消息。 - * @param data - 错误数据。 - * @param requestId - 请求ID。 - * @returns 一个表示500 Internal Server Error的响应对象。 - */ -netTool.serverError = (msg: string, data?: any, requestId?: string) => - Response.json({ code: 500, msg, data, requestId }, { status: 500 }) - -/** - * 创建一个表示200 OK的响应对象。 - * - * @param data - 响应数据。 - * @param requestId - 请求ID。 - * @returns 一个表示200 OK的响应对象。 - */ -netTool.ok = (data?: any, requestId?: string) => - Response.json({ code: 0, msg: "success", data, requestId }) - -export default netTool diff --git a/types/context.ts b/types/context.ts new file mode 100644 index 0000000..258728e --- /dev/null +++ b/types/context.ts @@ -0,0 +1,17 @@ +import { Logger } from "winston" + +import { AttachService, LarkService } from "../services" +import NetTool from "../utils/netTool" + +export namespace Context { + export interface Data { + req: Request + requestId: string + logger: Logger + genResp: NetTool + body: any + text: string + larkService: LarkService + attachService: AttachService + } +} diff --git a/types/index.ts b/types/index.ts index 277c1b6..e9f3a20 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,7 +1,8 @@ +import type { Context } from "./context" import type { DB } from "./db" import type { LarkAction } from "./larkAction" import type { LarkEvent } from "./larkEvent" import type { LarkServer } from "./larkServer" import type { MsgProxy } from "./msgProxy" -export { DB, LarkAction, LarkEvent, LarkServer, MsgProxy } +export { Context, DB, LarkAction, LarkEvent, LarkServer, MsgProxy } diff --git a/types/larkServer.ts b/types/larkServer.ts index 5fa5a96..1781d03 100644 --- a/types/larkServer.ts +++ b/types/larkServer.ts @@ -58,10 +58,28 @@ export namespace LarkServer { code: number } + export interface ValueRange { + majorDimension: string // 插入维度 + range: string // 返回数据的范围,为空时表示查询范围没有数据 + revision: number // sheet 的版本号 + values: Array> // 查询得到的值 + } + + export interface SpreadsheetData { + revision: number // sheet 的版本号 + spreadsheetToken: string // spreadsheet 的 token + valueRange: ValueRange // 值与范围 + } + export interface BaseRes { code: number data: any - msg: string + // 在错误处理中msg会被赋值为message + message: string + } + + export interface SpreadsheetRes extends BaseRes { + data: SpreadsheetData } export interface UserSessionRes extends BaseRes { diff --git a/utils/genContext.ts b/utils/genContext.ts new file mode 100644 index 0000000..7997c53 --- /dev/null +++ b/utils/genContext.ts @@ -0,0 +1,42 @@ +import { v4 as uuid } from "uuid" + +import loggerIns from "../log" +import { AttachService, LarkService } from "../services" +import { Context } from "../types" +import NetTool from "./netTool" + +/** + * 生成请求上下文。 + * + * @param {Request} req - 请求对象。 + * @returns {Promise} 返回包含请求上下文的对象。 + */ +const genContext = async (req: Request) => { + const requestId = uuid() + const logger = loggerIns.child({ requestId }) + const genResp = new NetTool({ requestId }) + const larkService = new LarkService("egg", requestId) + const attachService = new AttachService({ requestId }) + + let body: any = null + let text: string = "" + try { + text = await req.text() + body = JSON.parse(text) + } catch { + /* empty */ + } + logger.debug(`req body: ${text}`) + return { + req, + requestId, + logger, + genResp, + body, + text, + larkService, + attachService, + } as Context.Data +} + +export default genContext diff --git a/utils/netTool.ts b/utils/netTool.ts new file mode 100644 index 0000000..cc044fc --- /dev/null +++ b/utils/netTool.ts @@ -0,0 +1,424 @@ +import { Logger } from "winston" + +import loggerIns from "../log" + +interface NetRequestParams { + url: string + method: string + queryParams?: any + payload?: any + additionalHeaders?: any +} + +interface NetErrorDetail { + httpStatus: number + code: number + message: string +} + +export class NetError extends Error { + public code: number + public message: string + public httpStatus: number + + constructor({ code, message, httpStatus }: NetErrorDetail) { + super(message) + this.code = code + this.message = message + this.httpStatus = httpStatus + } +} + +/** + * 网络工具类,提供发送HTTP请求的方法。 + */ +class NetToolBase { + protected prefix: string + protected headers: any + protected getHeaders: () => any + protected logger: Logger + protected requestId: string + + /** + * 创建一个网络工具类实例。 + * + * @param {Object} params - 构造函数参数。 + * @param {string} [params.prefix] - URL前缀。 + * @param {any} [params.headers] - 默认请求头。 + * @param {Function} [params.getHeaders] - 获取请求头的方法。 + * @param {string} [params.requestId] - 请求ID。 + */ + constructor({ + prefix, + headers, + getHeaders, + requestId, + }: { + prefix?: string + headers?: any + getHeaders?: () => any + requestId?: string + } = {}) { + this.prefix = prefix || "" + this.headers = headers || {} + this.getHeaders = getHeaders || (() => ({})) + this.requestId = requestId || "" + this.logger = loggerIns.child({ requestId }) + } + + /** + * 记录响应详情并返回响应日志对象。 + * @param response - 响应对象。 + * @param method - 请求使用的HTTP方法。 + * @param headers - 请求头。 + * @param requestBody - 请求体。 + * @param responseBody - 响应体。 + * @returns 响应日志对象。 + */ + private logResponse( + response: Response, + method: string, + headers: any, + requestBody: any, + responseBody: any + ) { + const responseLog = { + ok: response.ok, + status: response.status, + statusText: response.statusText, + url: response.url, + method: method, + requestHeaders: headers, + responseHeaders: response.headers, + requestBody, + responseBody, + } + this.logger.http(JSON.stringify(responseLog, null, 2)) + return responseLog + } + + /** + * 发送网络请求并返回一个解析为响应数据的Promise。 + * @param url - 要发送请求的URL。 + * @param method - 请求使用的HTTP方法。 + * @param queryParams - 要包含在URL中的查询参数。 + * @param payload - 请求的有效负载数据。 + * @param additionalHeaders - 要包含在请求中的附加头。 + * @returns 一个解析为响应数据的Promise。 + * @throws 如果网络响应不成功或存在解析错误,则抛出错误。 + */ + protected async request({ + url, + method, + queryParams, + payload, + additionalHeaders, + }: NetRequestParams): Promise { + // 拼接完整的URL + let fullUrl = `${this.prefix}${url}` + if (queryParams) { + if (typeof queryParams === "string") { + fullUrl = `${fullUrl}?${queryParams}` + } else { + const queryString = new URLSearchParams(queryParams).toString() + if (queryString) fullUrl = `${fullUrl}?${queryString}` + } + } + + // 设置请求头 + const headers = { + ...this.headers, + ...(await this.getHeaders()), + ...additionalHeaders, + } + // 设置请求Header + if (!(payload instanceof FormData)) { + headers["Content-Type"] = "application/json" + } + + // 处理请求数据 + const body = payload instanceof FormData ? payload : JSON.stringify(payload) + + // 发送请求 + const res = await fetch(fullUrl, { + method, + body, + headers, + }) + // 获取响应数据 + let resData: any = null + let resText: string = "" + + try { + resText = await res.text() + resData = JSON.parse(resText) + } catch { + /* empty */ + } + + // 记录响应 + this.logResponse(res, method, headers, payload, resData || resText) + if (!res.ok) { + if (resData?.message || resData?.msg) { + throw new NetError({ + httpStatus: res.status, + code: resData?.code, + message: resData?.message || resData?.msg, + }) + } + throw new NetError({ + httpStatus: res.status, + code: res.status, + message: resText || "网络响应异常", + }) + } + // http 错误码正常,但解析异常 + if (!resData) { + throw new NetError({ + httpStatus: res.status, + code: 1, + message: "解析响应数据异常", + }) + } + // 响应数据异常 + if ("code" in resData && resData.code !== 0) { + throw new NetError({ + httpStatus: res.status, + code: resData.code, + message: resData.message || resData.msg || "网络请求失败", + }) + } + return resData as T + } + + /** + * 发送GET请求并返回一个解析为响应数据的Promise。 + * + * @param url - 要发送请求的URL。 + * @param queryParams - 要包含在URL中的查询参数。 + * @param additionalHeaders - 要包含在请求中的附加头。 + * @returns 一个解析为响应数据的Promise。 + */ + protected get( + url: string, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return this.request({ url, method: "get", queryParams, additionalHeaders }) + } + + /** + * 发送POST请求并返回一个解析为响应数据的Promise。 + * + * @param url - 要发送请求的URL。 + * @param payload - 请求的有效负载数据。 + * @param queryParams - 要包含在URL中的查询参数。 + * @param additionalHeaders - 要包含在请求中的附加头。 + * @returns 一个解析为响应数据的Promise。 + */ + protected post( + url: string, + payload?: any, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return this.request({ + url, + method: "post", + payload, + queryParams, + additionalHeaders, + }) + } + + /** + * 发送PUT请求并返回一个解析为响应数据的Promise。 + * + * @param url - 要发送请求的URL。 + * @param payload - 请求的有效负载数据。 + * @param queryParams - 要包含在URL中的查询参数。 + * @param additionalHeaders - 要包含在请求中的附加头。 + * @returns 一个解析为响应数据的Promise。 + */ + protected put( + url: string, + payload: any, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return this.request({ + url, + method: "put", + payload, + queryParams, + additionalHeaders, + }) + } + + /** + * 发送DELETE请求并返回一个解析为响应数据的Promise。 + * + * @param url - 要发送请求的URL。 + * @param payload - 请求的有效负载数据。 + * @param queryParams - 要包含在URL中的查询参数。 + * @param additionalHeaders - 要包含在请求中的附加头。 + * @returns 一个解析为响应数据的Promise。 + */ + protected del( + url: string, + payload: any, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return this.request({ + url, + method: "delete", + payload, + queryParams, + additionalHeaders, + }) + } + + /** + * 发送PATCH请求并返回一个解析为响应数据的Promise。 + * + * @param url - 要发送请求的URL。 + * @param payload - 请求的有效负载数据。 + * @param queryParams - 要包含在URL中的查询参数。 + * @param additionalHeaders - 要包含在请求中的附加头。 + * @returns 一个解析为响应数据的Promise。 + */ + protected patch( + url: string, + payload: any, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return this.request({ + url, + method: "patch", + payload, + queryParams, + additionalHeaders, + }) + } +} + +class NetTool extends NetToolBase { + public request({ + url, + method, + queryParams, + payload, + additionalHeaders, + }: NetRequestParams): Promise { + return super.request({ + url, + method, + queryParams, + payload, + additionalHeaders, + }) + } + public get( + url: string, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return super.get(url, queryParams, additionalHeaders) + } + public post( + url: string, + payload?: any, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return super.post(url, payload, queryParams, additionalHeaders) + } + public put( + url: string, + payload: any, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return super.put(url, payload, queryParams, additionalHeaders) + } + public del( + url: string, + payload: any, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return super.del(url, payload, queryParams, additionalHeaders) + } + public patch( + url: string, + payload: any, + queryParams?: any, + additionalHeaders?: any + ): Promise { + return super.patch(url, payload, queryParams, additionalHeaders) + } + /** + * 创建一个表示400 Bad Request的响应对象。 + * + * @param message - 错误消息。 + * @returns 一个表示400 Bad Request的响应对象。 + */ + badRequest(message: string) { + this.logger.error(`return a bad request response: ${message}`) + return Response.json( + { code: 400, message, requestId: this.requestId }, + { status: 400 } + ) + } + + /** + * 创建一个表示404 Not Found的响应对象。 + * + * @param message - 错误消息。 + * @returns 一个表示404 Not Found的响应对象。 + */ + notFound(message: string) { + this.logger.error(`return a not found response: ${message}`) + return Response.json( + { code: 404, message, requestId: this.requestId }, + { status: 404 } + ) + } + + /** + * 创建一个表示500 Internal Server Error的响应对象。 + * + * @param message - 错误消息。 + * @param data - 错误数据。 + * @returns 一个表示500 Internal Server Error的响应对象。 + */ + serverError(message: string, data?: any) { + this.logger.error(`return a server error response: ${message}`) + return Response.json( + { code: 500, message, data, requestId: this.requestId }, + { status: 500 } + ) + } + + /** + * 创建一个表示200 OK的响应对象。 + * + * @param data - 响应数据。 + * @returns 一个表示200 OK的响应对象。 + */ + ok(data?: any) { + this.logger.info(`return a ok response: ${JSON.stringify(data)}`) + return Response.json({ + code: 0, + message: "success", + data, + requestId: this.requestId, + }) + } +} + +export { NetToolBase } + +export default NetTool diff --git a/utils/pathTools.ts b/utils/pathTools.ts index 683ff15..8861f8a 100644 --- a/utils/pathTools.ts +++ b/utils/pathTools.ts @@ -1,3 +1,61 @@ +/** + * 创建一个路径检查工具,用于精确匹配和前缀匹配路径。 + * @param {string} url - 要检查的基础 URL。 + * @param {string} [prefix] - 可选的路径前缀。 + * @returns {object} 包含路径检查方法的对象。 + */ +export const makeCheckPathTool = (url: string, prefix?: string) => { + const { pathname } = new URL(url) + const makePath = (path: string) => `${prefix || ""}${path}` + return { + /** + * 检查路径是否与基础 URL 的路径精确匹配。 + * @param {string} path - 要检查的路径。 + * @returns {boolean} 如果路径精确匹配则返回 true,否则返回 false。 + */ + exactCheck: (path: string) => { + return pathname === makePath(path) + }, + /** + * 检查路径是否以基础 URL 的路径为前缀。 + * @param {string} path - 要检查的路径。 + * @returns {boolean} 如果路径以基础 URL 的路径为前缀则返回 true,否则返回 false。 + */ + startsWithCheck: (path: string) => pathname.startsWith(makePath(path)), + /** + * 检查完整路径是否与基础 URL 的路径精确匹配。 + * @param {string} path - 要检查的路径。 + * @returns {boolean} 如果完整路径与基础 URL 的路径精确匹配则返回 true,否则返回 false。 + */ + fullCheck: (path: string) => pathname === path, + } +} + +/** + * 裁剪路径字符串,如果路径长度超过20个字符,则只保留最后两级目录。 + * + * @param {string} path - 要处理的路径字符串。 + * @returns {string} - 裁剪后的路径字符串,如果长度不超过20个字符则返回原路径。 + */ +export const shortenPath = (path: string): string => { + if (path.length <= 20) { + return path + } + + const parts = path.split("/") + if (parts.length <= 2) { + return path + } + + return `.../${parts[parts.length - 2]}/${parts[parts.length - 1]}` +} + +/** + * 安全地将对象转换为 JSON 字符串。 + * 如果转换失败,则返回对象的字符串表示。 + * @param {any} obj - 要转换的对象。 + * @returns {string} - JSON 字符串或对象的字符串表示。 + */ export const safeJsonStringify = (obj: any) => { try { return JSON.stringify(obj) @@ -5,16 +63,3 @@ export const safeJsonStringify = (obj: any) => { return String(obj) } } - -export const makeCheckPathTool = (url: string, prefix?: string) => { - const { pathname } = new URL(url) - const makePath = (path: string) => `${prefix || ""}${path}` - return { - // 精确匹配 - exactCheck: (path: string) => { - return pathname === makePath(path) - }, - // 前缀匹配 - startsWithCheck: (path: string) => pathname.startsWith(makePath(path)), - } -} diff --git a/utils/pbTools.ts b/utils/pbTools.ts index 58fd310..7f24592 100644 --- a/utils/pbTools.ts +++ b/utils/pbTools.ts @@ -16,7 +16,6 @@ export const managePbError = async ( try { return await dbFunc() } catch (err: any) { - console.log("🚀 ~ managePbError ~ err:", err) return null } }