From b992ee0b211922f08e5a536e062aba545656761e Mon Sep 17 00:00:00 2001 From: zhaoyingbo Date: Wed, 25 Sep 2024 09:14:10 +0000 Subject: [PATCH] =?UTF-8?q?feat(group-agent):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=BE=A4=E7=BB=84=E9=97=AE=E7=AD=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 3 + Dockerfile | 9 +- bun.lockb | Bin 120836 -> 135792 bytes db/appConfig/index.ts | 40 +++++++++ db/groupAgentConfig/index.ts | 27 ++++++ db/index.ts | 4 + package.json | 5 +- routes/bot/actionMsg.ts | 15 ++-- routes/bot/eventMsg.ts | 83 +++++++++--------- routes/bot/groupAgent/groupManager.ts | 74 ++++++++++++++++ routes/bot/groupAgent/index.ts | 118 ++++++++++++++++++++++++++ routes/sheet/createKVTemp.ts | 10 ++- services/lark/auth.ts | 6 +- services/lark/base.ts | 2 +- services/lark/chat.ts | 33 +++++++ services/lark/drive.ts | 16 ++-- services/lark/index.ts | 3 + services/lark/message.ts | 108 +++++++++++++++++++---- services/lark/sheet.ts | 45 +++++----- services/lark/user.ts | 26 +++--- test/getChatHistory.ts | 14 +++ test/getInnerList.ts | 7 ++ types/db.ts | 7 ++ types/larkServer.ts | 33 +++++++ utils/genMsg.ts | 21 ++++- utils/llm.ts | 75 ++++++++++++++++ 26 files changed, 660 insertions(+), 124 deletions(-) create mode 100644 db/appConfig/index.ts create mode 100644 db/groupAgentConfig/index.ts create mode 100644 routes/bot/groupAgent/groupManager.ts create mode 100644 routes/bot/groupAgent/index.ts create mode 100644 services/lark/chat.ts create mode 100644 test/getChatHistory.ts create mode 100644 test/getInnerList.ts create mode 100644 utils/llm.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 056a9f7..8389a5d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,14 +5,17 @@ "Chakroun", "commitlint", "dbaeumer", + "deepseek", "devcontainer", "devcontainers", "eamodio", "esbenp", "Gruntfuggly", + "langchain", "metas", "mina", "mindnote", + "openai", "openchat", "tseslint", "userid", diff --git a/Dockerfile b/Dockerfile index c624fe2..2d17ea3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,13 +2,14 @@ FROM micr.cloud.mioffice.cn/zhaoyingbo/bun:alpine-cn WORKDIR /app -COPY package*.json ./ +# COPY package*.json ./ -COPY bun.lockb ./ +# COPY bun.lockb ./ -COPY .npmrc ./ +# COPY .npmrc ./ -RUN bun install + +# RUN bun install COPY . . diff --git a/bun.lockb b/bun.lockb index 00e541ac88fc2d0381c563df567cd10ae756e308..060c37bb9870a261cb37ba13e8d8259a5069b005 100755 GIT binary patch delta 30721 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�wbKo!3dgNvw&)l0I`Q8z|ZBvDFMlKAqJvMNjYM9EH7CB8hR z{!Ec?4@#e9Q$0_qUU_PWd_NBNYHAQ5RUk^qK#3|V#L)-$T`ejo7&nPMM zt5i;uk`E;Q0r?iCA4(bJDLMUfDgS4bie5cDgS>_s`r6>ixQuu2IVOU zz)`RNkCZgkLw)Mc5R|%9N$rVJvXaEhR7VM_SXru2MWV=}pRx#`sj&h6e{U_(hZ>+u zqU=tb@qe35*dsx(%FijS@l>h29HpDBSU?l_zc!gvkILyj^xtOlIGF<%QL-FuGU*~p zYwo|z<{empEOQs2>n=diA^|7|w^+id=~+5B&_Ns$s4Z9XYT{#!A`P=$ zd`-2D|)(}Hr&O03oLX!6&-I*sd>?{PHBV1pI13d2|JY*-@4n= zCch~6Ey-Vfa75!VwW_=e>AiNWSz-H@(}G_Qd31E!uNgBI4Dua0CeAKmKso8W%iHc? z>|~=Z7Ydr(OsMh5cXG3W)&YkSqv|Yd^w@jEA3tb*k|k7?=2hIgsZ1)%@2*6 z^>KDPJ%{0~B5nIr_kFC`%U(dbwp?=hzIk6PIyPy2B5~UG!FTQQFD2MLJ~qS4-r#~; zV%vQ;o1E~ay<9l4nyaO1^c2u-0W=mvCfW3^8z}|ZXLgUdqMWD zu@5_4?N|EY&e4i&{saqMll-~Y7X}$@?)15B;pKvv^Kz5VMaNle4~jDN?b{@~bAx@| z4_yfOQaNmNn`r%UhcEUFvb}r%(dwQRMi!4zZL{yTw4CkaPs|1B-c}l`KXp^1{xLD7 z&u%n!ZXMIOgJF_d`=v)v>CCMeD-v(VCEefEGS_n3vdWu-O2WH&G|qpO(0l6&gSSf? z8yx$%u{sNzXrX%*wR(AzO>1Xbz53iW&Tj5h3)|EV2De{^pIxxfX1#+&RO7d0uaBLb z({AOOIo-!ay1CB{=3bdOG<06$`yo3$A!&6v>HN#jY)*R7(_aR@YnDG_!1ty1lZj&EOa`iqg{-7&jI75cwkYI?I73v6GjtJT`o zA-gAz)Vf7aOKF=uH~G=Hop9s%r&!Z1HvG}s+ts=jjjhyl?xTAJBYw->zo)|6_pjFPoLYab&yYnIrg>M` z=)817$i@22wldE%xo=;et+jVTqon)Y7DSb^Wb5+6osHk${nF>#CT5-5?N`5w?w1@m z^U0SFUW#mjdlj9zVSnjc@w%%Rxue&vQWkJWVVve=HDc8lB0Dov@S ztCh05X3cu4$uGTvqr>aJS*lf5);Sgj`+iteH*I%He!qToi_{Hv9y`{%UDnoShy3jK zmU(jt5v`8TGcFxGX7N(9aPz!MBjHVu1hBP*ccoyNclcJ}tXRn@VHEF5qi`Oi5s)cUzV(*uOS08J? z@#X;aqsQ+ID%Lu`W!kCMrLU)Nj{jooy#H-c5ciAe#&O2Fu)y)o@iW$(ZJ2(p!QOnn z+WoQ4bFOwMC!MCe?XFI3aK~TKcFq09M^?NoD6P~^mwHlHWy$Al)o)oYFudq^%e<^s zvt6?+kC?vgb#KDEDZO?bK04-g(ved(Gv3X88`>?8)h@KqsdFZ7c{=0S>dtop*7mr3 zWJ;Zi)m2G{2Y=eIzrO0!rxm;I95UN_d6(^p$ha2gq6S`m_I!6_WbaXPYI_XHdwMmo z&&G1ng_W1?yyM!0`7gV8U8;2_`Tk|rch#@&Mm-x5ZfxSSX6Uu(mfap#IyZCr$L5yG z4_{1zuXz+V@*DqpYU;=1<3Aj(`g`aAMIJi?=>pU9LhD|y)6Zf^#@m#*W6f$kLR9CnLpCE`JNiu7_O{X9##0ViJ$-LJIK;Zo^@nFByC^CQ+FedM`~>3r zrT5i!_M(!EbG<9JRptJ9uhi?cy8j3Bt(zM9w{mmVR{t`1ifYCdgDn=8zt1_HyUx?+ z<5Byi=M1tgxm}($H*nyUKIX=3=rjx6>QC!qR)5%7>yc4d*O*0KHusx+Fi-YddrG@u z^Q;s4`>XG`QRmyug-4Dax%G5Se>ctXtBIiwPm=~&4^Q~C(8VX|bvfz6zsm-{`!(jk zuVc;yP2E>>wrQ;+8{;Q;jQMrMFP+*BnA&l{xplkTHji5LW#*E_mK!1$T^qNjNM7f{%4KnNoXblZ?2XE{PZOyOxs<-_(D<^v0g-`1bNV z?fBB$?>e?OoIB-N`^uT#-=1VYuC(2^=vY(@F$;^UG(*NxT^hrk4wS%%SVR(;`!=+qnKSLr+wdF^cfK_ zqfzsFA-2YbZs{{`SVh%7KY8|o+HF@JId(iZIP+_CIqB$oSozYsF|qZOi+P`RoU6Mv z(`~_;vCkWHt#5ZSWBc|HZvKs^(heJU?VhxG^fAlHj}BhX`_gii)4AQnZIz!oT7|0W z1s&?eU1pP}TQKcm4HwBaAC6@0W*F;CUFy!TTk`OhiObw2z4HBAuD!X?yi&l|^Z}2n zt+?wtVcCpYPV=U}d-B;rZ_NE#)=i6QsXkm>IpEHW9S^=9FA1(iA5Y8iC0Z5~R4?As zFXiK|Gww$}76h%+_no)iEI)siudm5A-!N@V`n|LJL$6s6k6@PPtL!hUy}@CmLr{yi zn=*3FTFq=a-|oyxegb;L;!@->_&Gbf&zLxdDZPsCnA=A}~`{Sw% z{{!9y9V%?F?ul#vw$-z=<&E3Uo9Wx^N&9#d<7SC$iV{yc$UYUnq*p@B3 z>a=r&XOhbH#fEd5NpVMhn|)<>Zr9S4&GOVP<)rIbUOMl``@DuZwW)2fI_%i78lQi^ z;6GQt&%W)!$qwDxbeOxv(xKG3$B==xmNBav=UP0oskpw!ig$k$80L(4vu@Rjt0yj( zvM}Bv^w{{4KQ8ubW|lg2Urd(|FUMuo?yrtsvixN-yJ&ql`v$oMU51VmSZdx1M-xv91 zYJmH5|6yC-C%1k4aKcpM_xIN9=GX0g={kPpfx`ROYV7=ym^U@*;K7&G-(F8@db3=o zG38aBymw-S=pJF`48mCW{sfy@E}^Smt!!w!C$z!4O7X>&3Qo4SJGtrfVavD>yMW6# zf3LMLY}~_&AHMc#;k_*7wCi>QC0jqsf-N7f;o{hv@sTWPwlV8kpy9M^aX}=rm}AWJ zCuq1t7Cs@8odCBDTr%S(MzY+w#!Nd=gFg`249;<$F{?32!=-@z_W;~n<}x4pt%H8^HQanw1}%ScOfx855 z8?##h{kB2B6&h{_D+SkXJM>$r;dZg%E1@5_2jKQFmsQYj2lQK|;r6jIa8Wy<-)aqa zfEBKWe!HL_xPvTk4fF%Ie2s=X%-(=Y+712IYPh3p@mlD&2l}nk;IFvC*FisU+rXV< z+Rt51#gFbM~&I~?HcX@`v}hB zm@(_QL&H5{t9L*@aK<||+!GeP6Z#!DX8XWBW2#-y?}RZ+-KF7PuwCG;f~&n-!@Xk3 zyP@AnV|E7I8)mr&`jud#vPZ+cV<*8q2IsU_!~M?k_Cmi?#_Sfj53J!n=yw|W?bC1{ z*)?!~fb-q2;XbqR`=Q?%=m+jk=6wMA{Q~_CXt;0e2{^;E#w@H@qvSX?r#OP`1p6o0 z3LFbL7{QXxLBoR@r9Q_#fwd@wiib2x1CFgZ6v0k_H9o9SsyJqJB!cChugp@8geog> ztlH5C=6C@!byTA?=GY;ySHadhrcqYqSmv<^Htr(k3alx|?2ktFtPDQY&YjDX^8l?@#-hwr}4xc=&QQC2A$!X{dR{xAf>A-R zeQ&}of6*u#aBK_M6JTqc)o=}2%vsFoE%+t4#?15_=JYnSJ*VNE*gSJoR&dRaBAjq5p#MEdS29U-mDDV zAK+SF(r`Yk@KPk#f<3{jFAKaJ$@#I_c=cy*@EX8^uS9ZzY%yMg*hjp!WZ_pMxmIj7 zURyKnS|rzoMdP(C+l<$COm#hy3uZBR4Pm?Rs$r%#BDqkOjMp%B5U=f-<;_U^6=Q$A zc4Q~<8qVx)MRJ{39$q`MQoMFy4R1%{Z!?GEHG*BkYb0~I6Ujxf@pz49Wq9qzyzfTh zr>uo|?ZKYlwI>TKi{yH-*?8^E-r%(l3%(c0#jwSAjb$J48dns4zi|cSMx&x$k3Vtv zqd7fe`Sg(ZYV@an!yo!n?zA_C%HGx4u!nt#t12G|5}TaX6o2bo7^w%_@U;hk$m`Ei?Ue4kl2?seB)p;JF)Q|C`zu_DH@Ljof7-Jf%{Q5Pz z>qyykaOy;McCt1zAwc}gWcm($I)LxP^EU*r5pS9+M+6q_e^XPBsw)?DEUNjw5obKU zi};7h5YIWa$o9Q8R}}EPv!Eg?{%EN0(hFS_4S#rnGnW6RS*Xfv-_sut>L~*fif(^? z*#fN`m4A6@#u?4(gKY?|sCfRe7Z+kmKR2Qr`T~QzPFc=@M8 zt)xs7Wa0-+!qr;JGX<9<<+TB&$%Wr3`bv52K*@U5fk-J&14>CVU_L-ssFYU&>2x6n zKhC0`cTmNefc(~izBHpeb3oqEiytBge>ksri9)gl{ep|?(btUhOB}LBI4B87zs!(+ za;WGm)w4n#jYk*#>Wk{>tnrc$hBS|mGHWB9jWlUapGr~1IzWz;*9{c^g`a$+AVJpX zF6G%Gohs$gT;spO4j})bt0yS+XAi)Cgu_P^y^%@f4uJdtY#%AFF7oyvOf~)@`{lr0TQLW z#z+SMbdgWszXIVy_?;2;k&F!T1t*{kX(}TNQ9T3}#XTvHEJS%t$$u2?hQeXx^KNXt-EJpDXU@5Q+pryAGSOu&G)&OgP zb-;RH1F#X;1Z)Pj07bx7U>mR<*a7SWb^*JAJ-}Z2t?)i1_5%liV&EWf2sjKJ0geLX zBQ*as-@O2u;ywUPAx+sN)R_!S0VV<+0SW`10Q$6omVYo10%(9xAPi_tuHOa;`WUhm zukzy>>&Wut3gqDQgR0+v-+{xx5#T6r0yqhj0H*;88{2^$z#L#1upC$ctOQm8^x5_r zU?xDn&8rM>Km|Y#s0jQ?2Kj)2{{Y?szX87kFMyZ8W8ew!6nF&u3OoSr0%gDr;4*Lp zxCoR2CxDYc32+KH0~`lNV!+YBSYRA59w^}S6p9HP|EUF6%PtNX!X*E+knvaHs zz%*bc@9oQ3=~9u503v~Cpc~K~=mE3?yZ~>YIp7Mo0cyYrAUAFbv;>TmSj<2L-~)#G z1NaC$2A%-)rPMQke!h1RI0NjWMZX(~J-|9(J-`FA07Jk4_=L_s10R9+z-!sV00R{nsDQ5r@@jwg^3-|ynfMx*wL{1<0 zg2sOW^o8FyfZ`T?`;L$?<%n*bFs5y%I!0SfT_fE0j2RCB-&$VbDzKpgM}JjLVvz%}s0K}P`T z0L9;dKrTShjPi)K2HFCFfG@dDF%s9&mMJqQFMT?Tp&xDPx4eg&vbMqeaw zZOWw3Xy2L^hZ(Xe0~Ee0fd+t*A0>g3FVQL`f1!m&7A7TPff%3<(3`gl;y&vG46PmL_}!zZ4v8+e2}C*o_=c+$XfFU0 zqZ6VYML>##VE{!xszecxC`CXG&;e)uo74T%m;XkFLXKslmSx#x;IP$CIS-x^8I{(>XNWSfx*BaAP=B*lns#Z zDFC@_GCfYtFEIF6KJaD0ds&^z-(YHun1T{IxR$EF;E9s4v@ynfTh3+U>&dqSPN_f z)&m=WG=R*w4KivabhlRKh; zsN-t@c{SyeP*)_Lyj-6XHvm-k6Yvoz1MUKMfZM<=KpyNp(!T@00Z)M^z&+qG@Bp|^ zM*9_sN5DfdL)c8bK>C@)%MG4ObzXwL23`SgfOo(LfI6g({{WN_lmiq1O%2uk2K)(p z0X_p?feN${rn#p(LIfz?!y+YG4QXSb3SbJ*ZN&tr4v;0NZXD1Na0cuEYGVu30BnHT zfHhzVSODfgO`tEvumY_`_e3hBj5?r;KxMuGc1{whkfhK?x z&;WgefOY^{0n`Tx>Z)Hrl7i(cp*)wkenz8R9-@ENK+8;0`&wu0CzwQ_yFF3 zFVI5b>Ao2V_yctR3;=8-VMvH}0NoGU0Ih*w+7nR$Z4YRvvk3eQ4A33u1@r)9i}ggh56~N+X|2Xth|7x_k%{hR;ssa!Dn2Bb zo5;EFSAw}j5dRQv7^mj*aVtH*~G7bBKXbG5UJo3mR5j_;%4T$LsqkM9FQxmtV~ z^*D+D3}Uqi($TO5nSJ;540&=4?cAK3IX6ReJv0p$m8FLO%ZY)AO%s@7EqYi4s?5H)m)Y!iS+( zZY_V7h%knm)lWO}bao|KP|wXuK3jOt`G>KqvbK&TC7tmPPRQ48&slmLuOMn;ccW>q z2EFVaDY;=D&aNbie9|zx@T|iAgWuLGITvS7Do_mL`?cr7to~hAQAqxIdsv_jZ_t4o z?jfH>8r#3FZ$PE0UV4Hwz`gKvVdA9bjucA7y;- zS2z7R+a4yM224jb5_&SLST3y-((>8G@;QEJ;pXh>>g=wNKQojM)DuSYP{TsJm9q~&831y2zamJgej4_*|Na_fjC zH3f0T7zUQlS`@TuhN=C26199PBT6vqV zQVrbAF@Stt^#tkcMznFoY{DY)(bn=Qk0_CXsC)>vd?2J!CprfPlF!MO&x|B5RFiLf zKUrHoWfI-tMnFaKncVXEl#&Q)Yz?Jj#O0-ECe6N=LO#D+Jj+t&bs3GMC8ABkNy*7+ ziK8aX=yG>-;Vv{HpTwzV{Pq;Cnod4yTt0SFyid?v%7==}2W+ATI6d_cP)m$B@==^9A^p6t zD?*784}?tt#(Q_STPJt2Z*u74Mfw(39KBTNqGJaJ<` z8qT>XTh`$>b>b|ng4r6$$7!QFtQ@M4)5HFbG3d_M|A8X(ak;>P24_3 z&`8krZ5@6H`cQtY!!L*dmHPCQ&!8?FcO!dq`w~O6aCfG4sF2TiF5GeWaI(G2xF4gn zwBf%}U$Hj4MQ5&&^$6_W5L><<(k{zq!Z91ZS7**v`PPQd?@XhLEv;kG!WJ#k>byH} z>1ti?AAK9y^1q{x*_OAB<;*I2LFOg4 ze8(=x+hEK0?SdWBep`M4c+YcEFU?&vU1|-#vre!Jg&M_6l$fFa->y5JVaLzw##!($yK?Q7>2|zd1Tu%&@y{_Ud`1LUOF7w&pBMq* zXWQ|`pw`RKXBG5mHsSicuBVSKN1qgnuyVK9@l_(x{E8jl5zVddqoxqol=pgtzkHb? z)WqV$8yRuht)HU?IkxS9kWHCpA;qZC%vspxyob8fm%^U^r4K|o6bVsA*z@?d(n6`{ z!2b~mk*YZGO`^b?JMe9yI9E?Q2k}e2++Dw`^_vH;MR%ARXBb8gB{WdcN!_%d#C7#h zB1HkT-GDwk|GkEE_wBvq{}8OTe4c)4!#c5R zrpI#W%y%OC#hG8!9W8#c`N47|v9~qlFLvi#t$wmmC6hzY zDZE_|&doXB1NEz-<2J%J1)i!MC~bm3w0F zqj0IpXgSuP;exQGTUMeaZN|N^y50GJp4?XDZyx;no-oC~I~u+_ZPg2&A)krA)-Gzv zr_tZ)3VjLomk-T;{zvSs-qj7=Q6fe3ooap^+A1G-@pnQ0$0#tkb8oJZ@{ae8ZoN4p zPI=#sPw0at|Ihv{tyqK)VYOLEtIa|wuTyzV{rl>Z7q7gwHXxLkzCdam&wXm=HgG^WN(xDcVvEL@_ZW3v;$69**Me%ikud^X1PaLbUh3d=o7k zS$>XzUX*K>GZohyMGv$;!Wxi1$iDpIw9a6ygm`c>J={LOCmlUj*Lsy%hgz+m|^M ztL%572YjYLK03l)4A|Ycc{kd8SV;E{3Yf52kCyyPYMa{164n?grt+vR<`BO zVzqROF=@@;#Ax_9rD{LU($h9rRNnE;?N?dmuXmtJY*xs$E|?D5{&gIdviIiZBHEW= zLq}^y7F2$a#3{ok^+Vd1oJI{=445$SnG=fq0gySW&>aR=Ta6>e?Wkxxj4>E$sVbi{@VI=A|*>bA8f4-7*~7jI{HgW^L+ z{+CotjzPE>NIM>HF@43`IY~ci$Pb>l^ZP0rRTYcfKT0IIc=t4Jgs1#`i$7DiO&RvB zuA>^ge}`G6M_}apCZ3BQGPW<>mR#u`=b}gm=bxuRG5MhxzupQzp8TSAFRCF_P)Iu@ zPx)yX&*Qswz15<)6>89Q!tg)wOzB-C)?ZpWj3V`6Ek9YK>xJia2c?@tqYs*R^nn$- z7d5Sas)I1S=;JljkRP`Z=e|9+`Ps;d=tDB3WDMo_F1%j`xAkY6A-pf8(Xbh+(jRg) zir^jlV;9s1-bfDFbfsVM!_l!rsZUX^MUnhe)bx}eQ8E8${gsWzpMLOTRQahGib?Me zuMGNp^+$;)k0t-1KR&jSpXPBuH|tATV7K0=<}D8KDUxrJ3B}~+e=I89ol#mPZ8B<5 z0K~3Rx+yCgNAZI)MW5P$7RpzV{0ZXYqxmnyW1mtp3w&xcA40S!njZ#geI#0ZVu(+{ zlAEq}9}l8u`uGxtD2wJ#XK@ys@R6OFf$)Acs{71rbQas4ua%8Xwsz;+6E9|4%MTTK z*l|#Wu&%RdRl}(6*rs9%=b@st{5X-;hvVYTG z?(gfZxRkvqern0Ct&dWw9{DupM-BP8CRco0bgy~F@6eBu z^Rawo#2Z<5nR?0(HaUIx-r`?Rt-FC1^oa^ACqMe6YS#E>(3njw-_iP+L8GU&Q zPfLN5dv;xWc4L`&TcL(p4Hxu*k7Yv=_*S`yh%*!TX}Nfh+aOWAFUSvF$&cNk z2{BkBL`3Q1Hz}6NGl$RHYDp##SCi4XRh)t@lhu6RJZ^xMx`nuhALZq7n#-;7HDI+^ ze$`aD<1;?3<4glmV$u@v(6|R?h#F0R8*8X$zAxs%bY#maS~UNk6Xf-DqURpzoADJgk8cZ{oDLt=!wQrgLdj~UL@ zG`6MJ}$xJM4^8oSviPEa<76>p0PE#l>Ba3lr?= z?$nnLEaj{%Vi(3Bh1eE_^pfSm9`Q2ks`aKP!y9x8bznYJ4d_07z zI??%2PfXB!)6E*n^>XoYc0t7Ol4d>)51%WSbV|@>WBtUYXJ$LarKe?Qrl;U(mNAK# z&whBEU`$H#0Bu5gW~x(sOm>Wu@QEyb{)`NPr_Kl!#cl+LYD^g-4|#qtPOkL%9*HzC zDJFYhA|4TyA~bOQ!K1+s#!1*z3k|ff$?+*9AD$JNm7Ja?ShwN8zh{$~|C#CJ#>X7u z9E|BhV^kpG)Qmqji*tzjiy|od3lAgG#}yD*P$E7(Rht{9&B(_1!dn<8@yRI|5Gy=W z8gDyb5>di`G{9K!G^s~nWjFZ8plJG!JaKV2H2lXDNy_|X+TeI< z-sKQy=l0k6McuzaXM3}3pN zThISFj;rqZJr9k(r^P9tCWr?JFWPu%mV{5q5jOdNQm%F#nL`joq@ibj3K13FjT@r} zzjz~OXC^!lR4$H>r3Ej>Q18EWjXHnJ5kiRyYqGhT$>$73%Ehmc7)95CRgRvhd3*mlSdN dIQ-q2dJ{Lri0?m^^X511;no~EaRLPg%sD|p1)M<4A-&Z!6HU!TKt&Od!J&KtOUt=zr^l=` zHK!cXoJ(`cQq+np%d*VMGN-b#Y_j*W&X7F4KE2N$zw7$_)4BZKYk&7%dsus~z4p1! zjpy6v1Ey{)`*>87;*oP2zVl}Aur2po&+^q9zW&NjW4skVXi&I+hT7Vt^WvWQ?<+&D z_VWhR@g0q~iIrmn8b*-rf+1!QGMZkx9+ zq)B}WtC2Z2&u(x;dg`bYl4HvlMrHD^(}+}Rqm>G2QXXwDEG2JJRw`PToUXv<}q@DJJ(t(?fr zjIkN{#+_7^+TW2cwJOqdb)+9sdb#G5XBJFk5)5N>b_Sn@;ZN5x9apJT33=9$B}i%i zBS(ILu8i`1Bw6}Vzb1vBP_Zs~2az(x>3NwMS^2chlu6xTj=bo|WJk_oVA5A+`gj?N zF$FIAS&p2DlzB``PtS?W$TLPU)v`prs4w{sgj&3?=rjqbSb~(nC7~e3h9eswDs0VIG&W6ohwuHGWlZI<8|%)mGG*CWTd4JFBpl!(_~186GmmEj)G_8 zrB9?iLr2&hkEmz6MNUfos7OYM8)uH8yy>m>42??3i)64P>%gTQ*ZERLTlN?e>Kg`= zSCopRtD=J3jL5>#?24R%tVyZaY3Vb_l^rn|iGLJjB4uVqq~zs~qodsPg1n3olZ=dp zwuNmQ*%m#5l$mLY6#aqJ<6lu*KA4=M#z<+m6K%*+hr&g#povjvP1WeU$Vn+3dFdHV?1q;#wdE5?8BpYH_SA_Zq_70;%$27>YlATFB#Q<$v$y;NdKIVskbD`4 z>)h8Iy-xIRjmTZId$}^!(B0Q*6o`c_1r#~M9OZefRur_ zE91_zyE5r{sVSK$Y3Zq%83h?M<}U4|=y(Y?Mt(-tB$=!uUF_X9HX{p@^YS}%x7*Ll z$;`;dCB~*_Paw!_@CFt(riidwCItvpf|a}-i0)eV7GEQSB4P#8*k zqCYw#KSSnbIs@WdC~^ag*-!f6FqlfB3I+LA20=W_(l_p=LOA(<#?t<_o*PRgL`D)& zbI=P#Z>=*U!2@hdA3@5DP)scqN&;x_)kzYra zN3X|Vdo_Q6%gmgGi>3LaawdMU#n8d z&s$F!25lGl>)={(YMbsJlBhcC8GO#whxt6Jy`hOJN_XcoSI^*cpFSL#==sA>2i8bX zak_hrL{+F~@L8e{*GTmIUPcGjOi;~r_nL_+U(eukqdr_S(ff_RVZ_su(tZ2I>+WHR z-g|E`jJD)>^gLg@J{*>)4r_0CqBo?hVc^l^GbV3$7(vBmbagm9QN5zQwGzGGJ9>V) zBs5MX=oz&V)dGE(&$HTFJJEBiR~LsTsQdJc+KFnZK3qG|`!SQ6Kx2MZ(_u_CE>&dc zzBS@hhMrL;(YuyB!h2DmH6f?8w{D`h2D?FqaEtC67N-X38RRV_Zy0$DzHXd<0~Rq? z&k1ekHAxM%+9cm|wY;7gk)ZnO!x1tBZ@ols^$LcOWK|UF2EYUp7Y-f~0{VgAy&e-bRs>SzV2;p8))jpDpz@iysJ>EID@-gH=|#h4jy32cbP<~4}( zLMi;Ua* zqr?1#d19*T;^+j=)75ogOoI3G>W0z6s#g*c=U*j+R&-EQJMW#6B45esd1t}I7htq^ z5GFPox+FZ#`zwqnDz-(%d6PnIO;)`|oM&dJ4s4a+{hCZ?a15Y94c#L&!JENNLr3bB z(ZTev29|5JA|no~X&8iXaRhp}4<`Mg30ne_#;m3FegqSjvBu)55vF^@CU|=hb|e#z zkio<3C6Xn5-zlRpSha9hQ&>NkYyxY$d1t|-o}VnOcb}t)H^Tma-9Z`aL%qJl*Y>b7 z)&$R~rHkVeyobn?L0G=yy`}b_jm5@!+rwnen5Eir{tro+4hnDQzr#w+soBnZjg&OW znqfjK?vq->0K^S9ynX80lN(^oz*8_$VSF@p9M;FJ z?zuHW_h_Hs?H^%}wyZVByI}SnmkGT5Clh1qaz@D5?fstz>jbm6>{ggeAU4;H^In2U zD`mtNyzw07vZiIlL;WYi;`On++Xs+Hu#^~^|97xXdQMn7PpkU6M`D6^UVVFacr42= z!DQ&Hc1WDJLj%KTWw#F-2a~?hjE(cGX`p9zOz?g~W;-%*&X_n)(}p@QDZw+jq3)5C z;C->7y(E+|ytA+_R`rs4ao(-j(7RjY$}t(EO_rCC7T2-I$7&5 zd*Ls@Wa=3sUB=$#ZaneN99SG>6)gW;1+y2TZ_POGNtjF&gTtEOW_o7V1aEdTJK%U^ zPkVMY(}CR*yq-wIxC>4CV{2!_WV*dpOj-`>Zne@kDb9QG-^%Jn{b?IBn+3ZUO*@vo z3*+2(Sfl3ltXpONnXryl_}oFtcK?$2IPVWI@n1r2r#Sy8;#F5`cJfI{KF-HB+5sbY zHEieo-O0CH!5hbcA>+1#NdZh+#tm`NJuq1)D>Qn`GfC1Oo1j*le^*#5eJrG%_kL31 zqyAPYg>{*yNlU?>1Oq#VLWHZKM?~#&Q<2>VA>Ed1q z-s5C4XxUV^#d-W%>%iIx-aFay;uO|Gd8fggAQKtq*@w!V3ErlZ+cP6uGY4kdCO)vv zVfG<#4kn&tZ9i`}oTHC5Bsnfjm>hF~mY1D?iM7;Ai!Z}aTUe9OnUpkc9kbr&99Gua zm*2yZDYFA-cfOX0rp+|Wo?;o)XE1xa3#-f68J9ib-ZYrI81VrRd%Ut1-@_7gFLp#U z+gZHcKFspk>A<@ayc@_AWqV^>hTZ7DDM=ZdT~-96Eu2AO(#tTGk#(BKXrel}ZYz^H z8D@7R>{XcbV8`R@FidvZjKv@+vm@eonDlP9`4Y_TPg=PM6MJv5wq-r${6;H-U=pgz z>)`nK0Ei6C>dx~Y!TfymLu0FsHq5VChyJv3Dq&Ky0%6`Uu8WC<@jkP2;?6iKsnb_AS~}7@N9N zJ8vd8d++@NauiF-+!x!<4Oa9Yzdc)g+uW z#~{0B@$M8D19Plj4ZGp%7ho&|J&R_f&$h(3)TCeAw%Hk>+k>mR~o<_P1=+-;me;;qaF)~tjvFo{%m#(BSmNj*D^ z-bHwmaZpw}&N~w(t#A^D#(56isf*JR{2LNdI_YC+?ff;V5t8!$OiDViPpQ^@?ZvQ$ z@5%0~14krymy(H=sC0_+UV-66GFtqz^<8$atZZz207N>4$Y}_Zh2_X%2;bhNXO2wp zM)WhRa*(9B2ak0QVx+B~m!^ZCxT2XkpWTledt^#;~scLI(b+sC2EII^Crk z%&@M~(nAjJYh5CROCfeOQh({7s4e{ z`ZIy(EtDr_a*?pEn@Lf83P{DLEv^4fO1)sBBkItM=o{bGNfE0 zrF=P%dMg~h5-Hc;NvXHW(z2z@=nJ;}Bp{-&)=>~C9jphEzX8Z4QuvEN8hFX!n~`#f zl=3Y?xJpZDZyS*E?Le+VNl3;HNA5(H0lR@*rKRY-YUSQsN(Xy^=0rJWF@tVS48+OO-BBW=%-3<)(&or0T5X{Z_HvK3>H;y104QYBVC94Yy29R7Dw z29luYUmEV{G?e7XPDsh`>@*}&l3g4wQj%RAelscc@1R^{4@a-G6nE%n&A%iZ1(A}x z+u?sFMWw%^_jgiMlKGH<3`7dQM?Sa;B_ZiSjvVY1h?L|IKBVGMhu`P$6o(H-%2is5 zewtIBjw}n$bYz~R@5n-i;?{JYy&zTrfD0^g!lY28MMO&S6k<$Biqzv~JC;#uH=+&wZ2J zPl>1gbKmqDvmlov|G96nW}_<3A^uTKJ{&0#@1Ogo()UX;Gs6G5Z(<_;xo`UCzKOl? z|MtFVv*(eggZ1u}K7Gy%Dy;m$&aH)gPEPyrzJ8}i-+5vAj>-3x?GU@*^<(1;roHLc zWx-FCmlxFg^qm!LGU}Z;^yHJfKDe;Sb8JqB=DXin*CeRsZ@n3wGOkyYziDkPd)K+j z6ir$G?wTfZMi-2^e7*CodYdDMEzb-2qhal#Z!drDY-*+c4@7k?)9R_^HR?oEY2*nw zH}&h+dn|3cbi;@*XM8y3_I2ydpVxC1hU$Z>eEi`?@ah5jj;Dfj!D^psqxZqC!5Xab zsdjqYngM$Cq9FYqtb>ktVSpa^bda9*f=?yt5?H`9LAv!?pGwkG)(+4+U}s^s>*#d@ z^r&Zp^!#-`)kU9yg)R=#UDo?lH$89t0DTB{71muRZ5W^@E(y{rHu#iJUx77#E=c!( z(WiRpr7sT9AHe)K`qZ7e@5TXo`qCi171md)O#^iNvLKzl$*211O|WyYYA^Xzf1UEu z0KIT|kUj_-po2GK-}BhF*{AN&`(W2#4Yv5yU_EXN_N~A^*u6SpEB39#zO6p~Sfm6N zunPOO`BaLYvJLxSXJM&2dOP;5#=h-7m9EdgLf2s54xbvS=k35g*i~4DPTGlmFJRwJ zpBkgDz#6Z`zL$M!tX}#u_QCvL@u_Ux_Z940hkdYdTJ6HV_1L$|r}FeB*g06W-9A;I zQ+8wD2JC}P(7~@_-;3Dys!vVQ`(W2#4fgod1A5#Z?AwTauz%@@z1X)2`}X?y`*44w2E_HD(! z*L-T0z5;8!4f_uI)EvF^AojuhU-zlUbl=ypZ#(wE=4tf?_U*vFH+)L#O|WyYYKMGk zzD_xWeLJxaX6oR>*!ME_9rmfG^gh@%Sc77pdRmVw#=cju5B98%ID&ng zfZf=4)W=`kOgV~uu(Pn`I{Fy)y^4Lud}@V00}I`QeQ)~IDn0K_?1No}tYdME-@YLIJ8Zq~{xN*x z_Q7_;wrcNj?0YRpk3H^F+x1S^9S4JSt#^HDr_Oj6`(Vdlujm@@Vc+XP`d{z))NXwk zHt>xg9eKj1_UK6`uMi{XQna)FMOU0 zhJNCUWZn3EmKl1<(4EdE>kptS&iXvx82UTt^bc_4FMXaXhFHUF^O$=IcRug) z{BG#|(19Q0&lh~2KMb9FAz24}fMg0s*zws&lpToB-Ds08KK2=d)fldD$ zJFoauuwHtFMg0OZzw@aoy6<-^Dr_sPnpWTAv}ZB$d!Gu?n_zc*iH$$_R1KZ-15OJ& z2n*A}S8>{}u<)u+)zbT51J7aLk3LmLkNc7J{2KdU5jx^0?1N4F$)|4BC9qNFvG1Bs zHPBP8Vc!MpgEi98KVu(k{?9(uM4y38yvS1i;#0TjdB0%aCG3Mm>ZD(>54Pf0AAfv! z1vdRM_WkBl(R%4`*!KH8y54IK7TC3~W_bv8amqT$A?2aqg_lF#cDSu!e z>>#Y&)?js;pIW@VgP%ICo(L&pK37o%cotUKJ~u#h_4BVEO1$b{db(} zBDVJ~uaZ6fC-~*5f)syWs;Zgbr=Bp|S5n_f=4^l3+Yuv~$-;|*RY3LdR`wr~%6FP4 z0Y73DrL{5_2dlRJN!%M$*gh{n{pvw~`>Cp`sxtRhQ0-x!ySr@vv6^bCg4{n8<`0ac z#%`;w!qxVUA*!EkLJbw*f0kc4%WY?9Z%JN|`=_av{(|c2HfimLLEB#qQEsxFQa=DDx{srvDGjF-$l4qK7x!>F5ao9*FPrk!RQl0>2IC<5b zJZWEkZIVmA+etCcMhoT79IY$V$&^R*w@N8j4JS{&_Q<10xoSFjGC247XPA>GzYvaf z^unDyc_38`G2G_wv$c@-bPvsY9x8`8$eYcPiV!W#z^U;8dw74YATAz>cD;2ew(8g zLS8r0GWbX*FO+mU((d4;J$WYB-pP}}^H2U0ic#2-lvpG`D@bM-XyxQdndF57E8&z$ zgS8}!La{v7(UTu(-CsuIoV+^ZO(QKv%6};1pHUY)2QaeG`n^IjBY-G~MQxFiR}Umf zf=eut#f;^B93cz?U0Zasw!2RF>Fd6&{JO~Ow5tsrV0uO_! zl0A*YBj8am9n1hT!7MNvL@+4Xd-Z|1vAC_csd!}*a5p;rK{DtE?f~6E58wkmL1)kf zbOpBqStnV8x`tVOgsNx#sxO}R3hnIzyMgTO*tfL&k<*a}_*8^I>90jveUI-ookAr#O3GgJC3*=c%Pat0e?*tNT`hvS;!%L8ny&451xU>MWCuN_@ zz77KtsU)DJ(bzz654aa}192c8v;ol|2FT;~W*`zofllB(>KB6}Kz^~<3--x0ZUmda zOJFmQUu~WN^T8DG5O^5mg930L7zW;=(Ko?9upjINyTNv_4QvK4f#u+Numn5>7J<8| zFTpEG&L7#|!N8C-q8(@sj>3?EV-a!BYaM0{&npm?h^PeUyWeb4R(LlDD#4Z^!vCIx?Qz8EXlfeUEJQxSY0BKaB z>_{NnDHWuEVc$adLTg>7LuCDBEoGV(f~i2(T^wftmn#9wt&sxCE)gTgmkg2+hMQ=yb891-QZ=g1MCF5z$*@a1GyjUb-1g)4=y?fz(MdD zcpV%9M}f2_?H&W~gA?FAP$+97%I|`AKnakTcpQ8HWbHo%pMcZgOeuMu^jYvF_!@i# z&VdU+W0hd7n_#S)*q)hmaAiosE*bRs9zDt(Ils2HBi;za-_+(q7YCWgvxndhlXS{G6*@h>HxWU zkei4IAo;f!x9N3UygA(;@Zkt zCFh(wWpZxGc_s(3tg9%mb)?KtPIn5phD|bG&Q-Hj3$t6E8i{x@Pd%t&&8U3U+S9@? zUo29gD#m;;U)4}8&4u|Y+rr<1 zwlA75eZ-@Nwup+2iY6$TOHfdY%q!zn)mvIg)(W%S1XaVlSfFZ#U!r0#6>r^r;#l~W z)xS_NCaOhL43418dgE1$DsT25uX=^KZ?`yd$$wx{!#%Mmwv1{e|J9_vxqCcAXydWJ z-lw-uZB$s{_c|Wcf`%qytJQZ6Wp&$9RWlRm-5yX4Gk1cjsVbXu$O^0LXMK|^TGsMJ z?IBtG*&Vwrjh{$MLFT-PYD$><(t+XQ>UZc^ zu425^Qk$rjEVvmtiLTsNW0VYh;qC4}EII3Rg*DU76cp5l<^v)gH0MuJEnBgTvk-;W?%Ol+t6V6ExiGK-3N7t9@ULL6)V?oT`Y+o0gJn|GJqbB5(@#zeK|PO7?D^e7FvuV|UL^J<)b+wN=85VwuxE3Dm6_>}7Q!^Zdp zkxxb(sLY@rY+*+hN5k_#s2M&T^W3+y4CwoAuY12ce8w@4B{>~x_Cdolzot2QIuo)l z%v>z|XqdTUy6PEzlMPH@k;Uv*%d9(trPy7|%$h-s%eBqvGiV{Bj=4*?y%eG5vE6{YNtIM{7rjjIf#RTTS_qqnx1Fw<@Z+0&yV%-P6T_q8d3Uk|92 z_}pxXv$C(L;lA6Vck}(rY7`%+YPDqzt?8|1g;_-424>PMD;5o&rCK^Z5gMLa-+HW6 zbaHyb>j{IWd#NYWMogMe-+YaFo{Wa(wOJVNz7b_s+LN1heR%k^N68AXMn>C4=CIj} z_K`;B{j=2;Pev27#T;t7UQ*D+d=MV)zWQWs#K2|0O#7ogeTiQ&`Xx=xqjP8>@;38J zsX3^bS?P&VEiiqf=BlOvcSl+e*NV(_vsF!V%UpHd=_KZ_E*R^+^r!#HGq+W)Gt8R2uD|zvay|vtp;B1-F zTg=%L;qIGZ(jWYKLhe0hR?$dH+gY4_9PYlYCnj_A_r-6G8i_`{Js*y{cg_oaWv(M?hr2I^spI)^K|VVZcuMx0FM-)zLDjm(Xr;l8zJPqU{#Y}Y@u2SbSFTqKaOR9|R3 zWJ3!x^hx9oEzF)0(A~GwbohSnrs7JiM^Ph=S;wC@MVm!vc>H3_MN-2Vl4pHo^PI)) z!|HF2Ip-*FYlge8;>p|9f9(cOMG0H2?2zLeN&j|ucrI2pznHJ;dbYJPZ&|>m-qXsw zdx34jbdiTznY$$4HN|~<%*Fi1_qUnVpqJIWwOzQwOE59kec#Ng8JE%x zV$EEWHX~xqPZnZyom?Zmu;6JmBKFyi_UYoj`h){9K8oAK7RE|S%5na7X^*o={s{S|HKn??u1Z%`-0U}Jbk+v z{|vr-th4#ZGx(v?VE9vAtlxBsI{#2{>)OM69;9M)8%EW_Slh+C{0wvAzJI6B8||N5 z`Q0Mfz>bFd@}JXJSJ&}Y2z~iRi8Czo?q}6Qu{T*dxtAtpxNjlqF!JrR$&Zef^CyPT zz>3uDYL@4|CER^)(H9^0eIw&it)Ze}M-S)DCiW)V!im{8aaqR^(qhfIa#!TE5$?X- zsQ>Yc_3zKFbPv7A?6J5w*C`ptO>J;DOr<4gxUV-#ZMAJ;+qVYX^1mj-)1kXLbcxy$ zzAwyP?c}~kdZ$#{@`lx$br5n}6ZIV9`lN?>$8$Hoy>ZUxyL*}&(TsIp3H0p2hO3&+ zIBI?2v6s(%3z0GB#{;W6{rbWG8jo3hDR+AA+mrSbF8u9G(xCfLlQRX=H}o`na%&mx zzHjNdl9zKzDrC(?gYSoOr{|uzr+b=~(8=Bh9Ty`oZ;^%#|j9WE#Jqi zyPUSG_c7Zpr_E7)%q-zfez^Nmr!V{5@7JsUQ?l3NqGDP_#mSQHLNnZbol}a)YrA(2^lxc1{6Bo%WPHx_d))2kzR@Z9)7|f!FX8O5=ZJAm?Ptz@p5E-2Kp90Z zPG1t$ZO50#ER9&X$Kh6M@AFvT?wRr{IFX%i2xf;Bs+K3UzByzCZA|EIKMfch_Qtk} zt*^^nki;O`IG=1jjz+BeBBv>zTu8t3c$;uEoW1J4D{ArFpC=W!*!$y+5~tr-_pMPo zw|tdZ@wH#%S%Xv8eXrE}9oi4Bek@`Cjgm(On9T@3?$})!n>x_``+%bdK3n?5k#!$Y z!?}@mUp-a){_L^~TR!lrRbt%^JUr0cKwGia+o}kLvHjP+_2QWjIYXU8d^xQKQ}XbX zdpb6|bfK}+DzogagZo0NM~j=b+kgEbdDP%+ z+%;bxWTvemLiQMJzO+g;t^d(r`}>Rg(yOB3+kHLCzF5p>HRdGgqteVe6XP2lLeU)<@OE3fJr2ZIdfxV{AE>z%%_Y0k5c8WiRF& { + const config = await managePb404(() => + pbClient.collection("config").getFirstListItem(`key='${key}'`) + ) + if (!config) return "" + return config.value +} + +/** + * 获取Deepseek的apiKey + * @returns {string} ak + */ +const getDeepseekApiKey = async () => get("deepseek_api_key") + +/** + * 获取OpenAI的key + * @returns {string} ak + */ +const getOpenAIApiKey = async () => get("openai_api_key") + +const appConfig = { + getOpenAIApiKey, + getDeepseekApiKey, +} + +export default appConfig diff --git a/db/groupAgentConfig/index.ts b/db/groupAgentConfig/index.ts new file mode 100644 index 0000000..8cd6838 --- /dev/null +++ b/db/groupAgentConfig/index.ts @@ -0,0 +1,27 @@ +import { DB } from "../../types" +import { managePb404 } from "../../utils/pbTools" +import pbClient from "../pbClient" + +const get = async (userId: string) => + managePb404(() => + pbClient + .collection("group_agent_config") + .getFirstListItem(`user_id='${userId}'`) + ) + +const upsert = async (data: Partial) => { + const { user_id } = data + const old = await get(user_id!) + if (old) { + await pbClient.collection("group_agent_config").update(old.id, data) + return old.id + } + return pbClient.collection("group_agent_config").create(data) +} + +const groupAgentConfig = { + get, + upsert, +} + +export default groupAgentConfig diff --git a/db/index.ts b/db/index.ts index 9e4d405..4c11aba 100644 --- a/db/index.ts +++ b/db/index.ts @@ -1,5 +1,7 @@ import apiKey from "./apiKey" +import appConfig from "./appConfig" import appInfo from "./appInfo" +import groupAgentConfig from "./groupAgentConfig" import log from "./log" import messageGroup from "./messageGroup" import tenantAccessToken from "./tenantAccessToken" @@ -10,6 +12,8 @@ const db = { messageGroup, log, tenantAccessToken, + groupAgentConfig, + appConfig, } export default db diff --git a/package.json b/package.json index 05e3f7b..b59d7c6 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,10 @@ "dependencies": { "@egg/hooks": "^1.2.0", "@egg/lark-msg-tool": "^1.2.1", - "@egg/logger": "^1.4.2", - "@egg/net-tool": "^1.6.3", + "@egg/logger": "^1.4.3", + "@egg/net-tool": "^1.6.5", "@egg/path-tool": "^1.3.0", + "@langchain/openai": "^0.3.0", "joi": "^17.13.3", "node-schedule": "^2.1.1", "p-limit": "^6.1.0", diff --git a/routes/bot/actionMsg.ts b/routes/bot/actionMsg.ts index 14274d2..bd2d174 100644 --- a/routes/bot/actionMsg.ts +++ b/routes/bot/actionMsg.ts @@ -1,15 +1,15 @@ -import type { LarkAction } from "@egg/lark-msg-tool" import { getActionType, getIsActionMsg } from "@egg/lark-msg-tool" import { sleep } from "bun" import { Context } from "../../types" +import groupAgent from "./groupAgent" /** * 返回ChatId卡片 * @param {LarkAction.Data} body * @returns {Promise} 返回包含ChatId卡片的JSON字符串 */ -const makeChatIdCard = async (body: LarkAction.Data): Promise => { +const makeChatIdCard = async ({ body }: Context.Data): Promise => { await sleep(500) return JSON.stringify({ type: "template", @@ -27,6 +27,7 @@ const makeChatIdCard = async (body: LarkAction.Data): Promise => { const ACTION_MAP = { chat_id: makeChatIdCard, + group_selector: groupAgent.setChatGroupContext, } /** @@ -34,11 +35,8 @@ const ACTION_MAP = { * @param {Context.Data} ctx - 上下文数据,包含body, larkService和logger * @returns {Promise} 无返回值 */ -const manageBtnClick = async ({ - body, - larkService, - logger, -}: Context.Data): Promise => { +const manageBtnClick = async (ctx: Context.Data): Promise => { + const { body, larkService, logger } = ctx const { action } = body?.action?.value as { action: keyof typeof ACTION_MAP } @@ -46,7 +44,7 @@ const manageBtnClick = async ({ if (!action) return const func = ACTION_MAP[action] if (!func) return - const card = await func(body) + const card = await func(ctx) if (!card) return // 更新飞书的卡片 await larkService.message.update(body.open_message_id, card) @@ -64,5 +62,6 @@ export const manageActionMsg = (ctx: Context.Data): boolean => { } const actionType = getActionType(ctx.body) if (actionType === "button") manageBtnClick(ctx) + if (actionType === "select_static") manageBtnClick(ctx) return true } diff --git a/routes/bot/eventMsg.ts b/routes/bot/eventMsg.ts index 0a1f5dd..a72d9a9 100644 --- a/routes/bot/eventMsg.ts +++ b/routes/bot/eventMsg.ts @@ -11,6 +11,7 @@ import { import { LarkService } from "../../services" import { Context } from "../../types" import createKVTemp from "../sheet/createKVTemp" +import groupAgent from "./groupAgent" /** * 是否为P2P或者群聊并且艾特了小煎蛋 @@ -37,12 +38,17 @@ const filterIllegalMsg = ({ }: Context.Data): boolean => { // 没有chatId的消息不处理 const chatId = getChatId(body) - logger.debug(`bot req chatId: ${chatId}`) + logger.info(`bot req chatId: ${chatId}`) if (!chatId) return true + // 非私聊和群聊中艾特小煎蛋的消息不处理 + if (!getIsP2pOrGroupAtBot(body)) { + return true + } + // 获取msgType const msgType = getMsgType(body) - logger.debug(`bot req msgType: ${msgType}`) + logger.info(`bot req msgType: ${msgType}`) // 放行纯文本消息 if (msgType === "text") { // 过滤艾特全体成员的消息 @@ -79,19 +85,19 @@ const filterIllegalMsg = ({ * @param {LarkService} service - Lark服务实例 */ const manageIdMsg = (chatId: string, service: LarkService): void => { - const content = JSON.stringify({ - type: "template", - data: { - config: { - update_multi: true, - }, - template_id: "ctp_AAi3NnHb6zgK", - template_variable: { - chat_id: chatId, - }, - }, + service.message.sendTemp("chat_id", chatId, "ctp_AAi3NnHb6zgK", { + chat_id: chatId, + }) +} + +/** + * 回复引导消息 + * @param {Context.Data} ctx - 上下文数据,包含body, larkService和logger + */ +const manageHelpMsg = (chatId: string, service: LarkService): void => { + service.message.sendTemp("chat_id", chatId, "ctp_AAyVx5R39xU9", { + chat_id: chatId, }) - service.message.send("chat_id", chatId, "interactive", content) } /** @@ -102,15 +108,20 @@ const manageIdMsg = (chatId: string, service: LarkService): void => { const manageCMDMsg = (ctx: Context.Data): boolean => { const { body, logger, larkService, attachService } = ctx const text = getMsgText(body) - logger.debug(`bot req text: ${text}`) + logger.info(`bot req text: ${text}`) const chatId = getChatId(body) - if (!chatId) return false // 处理命令消息 if (text.trim() === "/id") { logger.info(`bot command is /id, chatId: ${chatId}`) manageIdMsg(chatId, larkService) return true } + // 帮助 + if (text.trim() === "/help") { + logger.info(`bot command is /help, chatId: ${chatId}`) + manageHelpMsg(chatId, larkService) + return true + } // CI监控 if (text.trim() === "/ci") { logger.info(`bot command is /ci, chatId: ${chatId}`) @@ -136,33 +147,21 @@ const manageCMDMsg = (ctx: Context.Data): boolean => { createKVTemp.createFromEvent(ctx) return true } + // 选择群组信息 + if (text.trim() === "/sg") { + logger.info(`bot command is /sg, chatId: ${chatId}`) + groupAgent.sendGroupSelector(ctx) + return true + } + // 获取当前群组信息 + if (text.trim() === "/cg") { + logger.info(`bot command is /cg, chatId: ${chatId}`) + groupAgent.getCurrentGroup(ctx) + return true + } return false } -/** - * 回复引导消息 - * @param {Context.Data} ctx - 上下文数据,包含body, larkService和logger - */ -const replyGuideMsg = ({ body, larkService, logger }: Context.Data): void => { - const chatId = getChatId(body) - logger.info(`reply guide message, chatId: ${chatId}`) - if (!chatId) return - const content = JSON.stringify({ - type: "template", - data: { - config: { - enable_forward: false, - update_multi: true, - }, - template_id: "ctp_AAyVx5R39xU9", - template_variable: { - chat_id: chatId, - }, - }, - }) - larkService.message.send("chat_id", chatId, "interactive", content) -} - /** * 处理Event消息 * @param {Context.Data} ctx - 上下文数据 @@ -181,7 +180,7 @@ export const manageEventMsg = (ctx: Context.Data): boolean => { if (manageCMDMsg(ctx)) { return true } - // 返回引导消息 - replyGuideMsg(ctx) + // 群组消息处理 + groupAgent.manageGroupMsg(ctx) return true } diff --git a/routes/bot/groupAgent/groupManager.ts b/routes/bot/groupAgent/groupManager.ts new file mode 100644 index 0000000..854e09f --- /dev/null +++ b/routes/bot/groupAgent/groupManager.ts @@ -0,0 +1,74 @@ +import { getChatId, LarkAction, LarkEvent } from "@egg/lark-msg-tool" + +import db from "../../../db" +import { Context } from "../../../types" +import { genGroupAgentSuccessMsg } from "../../../utils/genMsg" + +/** + * 发送群组选择器 + * @param ctx - 上下文数据,包含body和larkService + */ +const sendGroupSelector = async ({ larkService, body }: Context.Data) => { + const chatId = getChatId(body)! + const { data: innerList } = await larkService.chat.getInnerList() + // 组织群组数据 + const groups = innerList.map((v) => ({ + text: v.name, + value: `${v.chat_id}|${v.name}`, + })) + larkService.message.sendTemp("chat_id", chatId, "ctp_AA00oqPWPTdc", { + groups, + }) +} + +/** + * 获取当前群组 + * @param ctx - 上下文数据,包含body和larkService + */ +const getCurrentGroup = async (ctx: Context.Data, needSendMsg = true) => { + const body = ctx.body as LarkEvent.Data + const chatId = getChatId(body)! + const group = await db.groupAgentConfig.get( + body.event.sender.sender_id.user_id + ) + if (!needSendMsg) return group + if (!group) { + await sendGroupSelector(ctx) + return + } + const msg = genGroupAgentSuccessMsg(`当前群组:${group.chat_name}`) + ctx.larkService.message.send("chat_id", chatId, "interactive", msg) +} + +/** + * 设置群组上下文 + * @param ctx - 上下文数据,包含body, larkService和logger + */ +const setChatGroupContext = async (ctx: Context.Data) => { + const { larkService, logger } = ctx + const body = ctx.body as LarkAction.Data + const targetId = body?.action?.option?.split?.("|")[0] + const targetName = body?.action?.option?.split?.("|")[1] + if (!targetId || !targetName) { + logger.error( + `invalid targetId or targetName: ${JSON.stringify(body?.action)}` + ) + } + // 更新群组数据 + await db.groupAgentConfig.upsert({ + user_id: body.user_id, + chat_id: targetId, + chat_name: targetName, + }) + // 更新成功消息 + const successMsg = genGroupAgentSuccessMsg(`已将群组切换至 ${targetName}`) + larkService.message.update(body.open_message_id, successMsg) +} + +const groupManager = { + sendGroupSelector, + setChatGroupContext, + getCurrentGroup, +} + +export default groupManager diff --git a/routes/bot/groupAgent/index.ts b/routes/bot/groupAgent/index.ts new file mode 100644 index 0000000..7a06954 --- /dev/null +++ b/routes/bot/groupAgent/index.ts @@ -0,0 +1,118 @@ +import { getChatId, getMsgText, LarkEvent } from "@egg/lark-msg-tool" + +import db from "../../../db" +import { Context } from "../../../types" +import { + genGroupAgentErrorMsg, + genGroupAgentSuccessMsg, +} from "../../../utils/genMsg" +import llm from "../../../utils/llm" +import groupManager from "./groupManager" + +/** + * 根据聊天历史回答用户的问题 + * @param ctx - 上下文数据 + * @param userInput - 用户输入 + * @param targetChatId - 目标群组ID + * @param loadingMsgId - loading消息ID + */ +const chat2llm = async ( + ctx: Context.Data, + userInput: string, + targetChatId: string, + loadingMsgId?: string +) => { + const { logger, body } = ctx + // 发送消息给Deepseek模型解析时间,返回格式为 YYYY-MM-DD HH:mm:ss + const { startTime, endTime } = await llm.parseTime(userInput) + logger.info(`Parsed result: startTime = ${startTime}, endTime = ${endTime}`) + // 获取服务器的时区偏移量(以分钟为单位) + const serverTimezoneOffset = new Date().getTimezoneOffset() + // 上海时区的偏移量(UTC+8,以分钟为单位) + const shanghaiTimezoneOffset = -8 * 60 + // 计算时间戳,调整为上海时区 + const startTimeTimestamp = + Math.round(new Date(startTime).getTime() / 1000) + + (shanghaiTimezoneOffset - serverTimezoneOffset) * 60 + const endTimeTimestamp = + Math.round(new Date(endTime).getTime() / 1000) + + (shanghaiTimezoneOffset - serverTimezoneOffset) * 60 + + // 获取群聊中的历史记录 + const { data: chatHistory } = await ctx.larkService.message.getHistory( + targetChatId, + String(startTimeTimestamp), + String(endTimeTimestamp) + ) + // 如果没有历史记录则返回错误消息 + if (chatHistory.length === 0) { + logger.error("Chat history is empty") + const content = genGroupAgentErrorMsg("未找到聊天记录") + if (loadingMsgId) { + await ctx.larkService.message.update(loadingMsgId, content) + } else { + await ctx.larkService.message.sendInteractive2Chat( + getChatId(body), + content + ) + } + return + } + logger.debug(`Chat history: ${JSON.stringify(chatHistory)}`) + const llmRes = await llm.queryWithChatHistory( + userInput, + JSON.stringify(chatHistory) + ) + logger.info(`LLM result: ${llmRes.content}`) + const successMsg = genGroupAgentSuccessMsg(llmRes.content as string) + // 发送LLM结果 + if (loadingMsgId) { + await ctx.larkService.message.update(loadingMsgId, successMsg) + } else { + await ctx.larkService.message.sendInteractive2Chat( + getChatId(body), + successMsg + ) + } +} + +/** + * 群组代理处理消息 + * @param ctx - 上下文数据 + */ +const manageGroupMsg = async (ctx: Context.Data) => { + const { logger } = ctx + logger.info("Start to manage group message") + const body = ctx.body as LarkEvent.Data + // 获取用户输入 + const userInput = getMsgText(body) + // 先获取当前对话的目标群组ID + const group = await groupManager.getCurrentGroup(ctx, false) + // 没有目标群组ID则发送群组选择器,并保存当前问题,在选择后立即处理 + if (!group || !group.chat_id) { + logger.info("No group found, send group selector") + groupManager.sendGroupSelector(ctx) + db.groupAgentConfig.upsert({ + user_id: body.event.sender.sender_id.user_id, + pre_query: userInput, + }) + return + } + // 发送一个loading的消息 + const loadingRes = await ctx.larkService.message.sendInteractive2Chat( + getChatId(body), + genGroupAgentSuccessMsg("小煎蛋正在爬楼中,请稍等...") + ) + if (loadingRes.code !== 0) { + logger.error("Failed to send loading message") + } + const { message_id } = loadingRes.data + await chat2llm(ctx, userInput, group.chat_id, message_id) +} + +const groupAgent = { + ...groupManager, + manageGroupMsg, +} + +export default groupAgent diff --git a/routes/sheet/createKVTemp.ts b/routes/sheet/createKVTemp.ts index 472db8a..5a3f107 100644 --- a/routes/sheet/createKVTemp.ts +++ b/routes/sheet/createKVTemp.ts @@ -1,7 +1,7 @@ import { getChatId, getChatType, getUserId } from "@egg/lark-msg-tool" import { Context, LarkServer } from "../../types" -import { genSheetDbErrorMsg, genTempMsg } from "../../utils/genMsg" +import { genSheetDbErrorMsg } from "../../utils/genMsg" /** * 创建键值多维表格 @@ -93,8 +93,12 @@ const createFromEvent = async (ctx: Context.Data) => { if (addRes.code !== 0) throw new Error(addRes.message) } // 全部成功,发送成功消息 - const successMsg = genTempMsg("ctp_AA00oqPWPXtG", createRes.data) - ctx.larkService.message.send("chat_id", chatId, "interactive", successMsg) + ctx.larkService.message.sendTemp( + "chat_id", + chatId, + "ctp_AA00oqPWPXtG", + createRes.data + ) } catch (e: any) { ctx.logger.error(`create KV bitable failed: ${e.message}`) const errorMsg = genSheetDbErrorMsg(e.message) diff --git a/services/lark/auth.ts b/services/lark/auth.ts index 7cee1be..35ad008 100644 --- a/services/lark/auth.ts +++ b/services/lark/auth.ts @@ -1,12 +1,12 @@ import LarkBaseService from "./base" class LarkAuthService extends LarkBaseService { - getAk(app_id: string, app_secret: string) { + getAk(appId: string, appSecret: string) { return this.post<{ tenant_access_token: string; code: number }>( "/auth/v3/tenant_access_token/internal", { - app_id, - app_secret, + app_id: appId, + app_secret: appSecret, } ) } diff --git a/services/lark/base.ts b/services/lark/base.ts index 2728e2a..cd17d15 100644 --- a/services/lark/base.ts +++ b/services/lark/base.ts @@ -21,7 +21,7 @@ class LarkBaseService extends NetToolBase { data: error.data, message: error.message, } as T - this.logger.error("larkNetTool catch error: ", JSON.stringify(res)) + this.logger.error(`larkNetTool catch error: ${JSON.stringify(res)}`) return res }) } diff --git a/services/lark/chat.ts b/services/lark/chat.ts new file mode 100644 index 0000000..c92a78e --- /dev/null +++ b/services/lark/chat.ts @@ -0,0 +1,33 @@ +import { LarkServer } from "../../types" +import LarkBaseService from "./base" + +class LarkChatService extends LarkBaseService { + /** + * 获取机器人所在群列表 + */ + async getInnerList() { + const path = "/im/v1/chats" + const chatList = [] + let hasMore = true + let pageToken = "" + while (hasMore) { + const { data, code } = await this.get< + LarkServer.BaseListRes + >(path, { + page_size: 100, + page_token: pageToken, + }) + if (code !== 0) break + chatList.push(...data.items) + hasMore = data.has_more + pageToken = data.page_token + } + return { + code: 0, + data: chatList, + message: "ok", + } + } +} + +export default LarkChatService diff --git a/services/lark/drive.ts b/services/lark/drive.ts index 411a58a..ddd2b94 100644 --- a/services/lark/drive.ts +++ b/services/lark/drive.ts @@ -6,14 +6,14 @@ class LarkDriveService extends LarkBaseService { * 批量获取文档元数据。 * * @param docTokens - 文档令牌数组。 - * @param doc_type - 文档类型,默认为 "doc"。 - * @param user_id_type - 用户ID类型,默认为 "user_id"。 + * @param docType - 文档类型,默认为 "doc"。 + * @param userIdType - 用户ID类型,默认为 "user_id"。 * @returns 包含元数据和失败列表的响应对象。 */ async batchGetMeta( docTokens: string[], - doc_type = "doc", - user_id_type = "user_id" + docType = "doc", + userIdType = "user_id" ) { const path = "/drive/v1/metas/batch_query" // 如果docTokens长度超出150,需要分批请求 @@ -27,11 +27,11 @@ class LarkDriveService extends LarkBaseService { const data = { request_docs: docTokensSlice.map((id) => ({ doc_token: id, - doc_type, + doc_type: docType, })), } return this.post(path, data, { - user_id_type, + user_id_type: userIdType, }) } ) @@ -40,7 +40,7 @@ class LarkDriveService extends LarkBaseService { return res.data?.metas || [] }) - const failed_list = responses.flatMap((res) => { + const failedList = responses.flatMap((res) => { return res.data?.failed_list || [] }) @@ -48,7 +48,7 @@ class LarkDriveService extends LarkBaseService { code: 0, data: { metas, - failed_list, + failedList, }, message: "success", } diff --git a/services/lark/index.ts b/services/lark/index.ts index 39143c1..5a2b056 100644 --- a/services/lark/index.ts +++ b/services/lark/index.ts @@ -1,4 +1,5 @@ import LarkAuthService from "./auth" +import LarkChatService from "./chat" import LarkDriveService from "./drive" import LarkMessageService from "./message" import LarkSheetService from "./sheet" @@ -10,6 +11,7 @@ class LarkService { user: LarkUserService sheet: LarkSheetService auth: LarkAuthService + chat: LarkChatService requestId: string constructor(appName: string, requestId: string) { @@ -18,6 +20,7 @@ class LarkService { this.user = new LarkUserService(appName, requestId) this.sheet = new LarkSheetService(appName, requestId) this.auth = new LarkAuthService(appName, requestId) + this.chat = new LarkChatService(appName, requestId) this.requestId = requestId } diff --git a/services/lark/message.ts b/services/lark/message.ts index 8fb0802..a3a0992 100644 --- a/services/lark/message.ts +++ b/services/lark/message.ts @@ -1,40 +1,114 @@ import { LarkServer } from "../../types/larkServer" +import { genTempMsg } from "../../utils/genMsg" import LarkBaseService from "./base" 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对应不同内容 + * @param receiveIdType 消息接收者id类型 open_id/user_id/union_id/email/chat_id + * @param receiveId 消息接收者的ID,ID类型应与查询参数receiveIdType 对应 + * @param msgType 消息类型 包括:text、post、image、file、audio、media、sticker、interactive、share_chat、share_user + * @param content 消息内容,JSON结构序列化后的字符串。不同msgType对应不同内容 */ async send( - receive_id_type: LarkServer.ReceiveIDType, - receive_id: string, - msg_type: LarkServer.MsgType, + receiveIdType: LarkServer.ReceiveIDType, + receiveId: string, + msgType: LarkServer.MsgType, content: string ) { - const path = `/im/v1/messages?receive_id_type=${receive_id_type}` - if (msg_type === "text" && !content.includes('"text"')) { + const path = `/im/v1/messages?receive_id_type=${receiveIdType}` + if (msgType === "text" && !content.includes('"text"')) { content = JSON.stringify({ text: content }) } - return this.post(path, { - receive_id, - msg_type, + return this.post>(path, { + receive_id: receiveId, + msg_type: msgType, content, }) } /** - * 更新卡片 - * @param {string} message_id 消息id - * @param {string} content 消息内容,JSON结构序列化后的字符串。不同msg_type对应不同内容 + * 发送模板消息 + * @param receiveIdType 消息接收者id类型 open_id/user_id/union_id/email/chat_id + * @param receiveId 消息接收者的ID,ID类型应与查询参数receiveIdType 对应 + * @param templateId 模板ID + * @param variable 模板变量 */ - async update(message_id: string, content: string) { - const path = `/im/v1/messages/${message_id}` + async sendTemp( + receiveIdType: LarkServer.ReceiveIDType, + receiveId: string, + templateId: string, + variable: any + ) { + return this.send( + receiveIdType, + receiveId, + "interactive", + genTempMsg(templateId, variable) + ) + } + + /** + * 发送富文本信息 + * @param receiveId 消息接收者的ID,ID类型应与查询参数receiveIdType 对应 + * @param content 消息内容 + */ + async sendInteractive2Chat(receiveId: string, content: string) { + return this.send("chat_id", receiveId, "interactive", content) + } + + /** + * 发送文本信息 + * @param receiveId 消息接收者的ID,ID类型应与查询参数receiveIdType 对应 + * @param content 消息内容 + */ + async sendText2Chat(receiveId: string, content: string) { + return this.send("chat_id", receiveId, "text", content) + } + + /** + * 更新卡片 + * @param messageId 消息id + * @param content 消息内容,JSON结构序列化后的字符串。不同msgType对应不同内容 + */ + async update(messageId: string, content: string) { + const path = `/im/v1/messages/${messageId}` return this.patch(path, { content }) } + + /** + * 获取消息历史记录 + * @param chatId 会话ID + * @param startTime 开始时间 秒级时间戳 + * @param endTime 结束时间 秒级时间戳 + */ + async getHistory(chatId: string, startTime: string, endTime: string) { + const path = `/im/v1/messages` + const messageList = [] as LarkServer.MessageData[] + let hasMore = true + let pageToken = "" + while (hasMore) { + const { code, data } = await this.get< + LarkServer.BaseListRes + >(path, { + container_id_type: "chat", + container_id: chatId, + start_time: startTime, + end_time: endTime, + page_size: 50, + page_token: pageToken, + }) + if (code !== 0) break + messageList.push(...data.items) + hasMore = data.has_more + pageToken = data.page_token + } + return { + code: 0, + data: messageList, + message: "ok", + } + } } export default LarkMessageService diff --git a/services/lark/sheet.ts b/services/lark/sheet.ts index 88ce7f1..4601ddb 100644 --- a/services/lark/sheet.ts +++ b/services/lark/sheet.ts @@ -4,10 +4,10 @@ import LarkBaseService from "./base" class LarkSheetService extends LarkBaseService { /** * 向电子表格中插入行。 - * @param {string} sheetToken - 表格令牌。 - * @param {string} range - 插入数据的范围。 - * @param {string[][]} values - 要插入的值。 - * @returns {Promise} 返回一个包含响应数据的Promise。 + * @param sheetToken 表格令牌。 + * @param range 插入数据的范围。 + * @param values 要插入的值。 + * @returns 返回一个包含响应数据的Promise。 */ async insertRows(sheetToken: string, range: string, values: string[][]) { const path = `/sheets/v2/spreadsheets/${sheetToken}/values_append?insertDataOption=INSERT_ROWS` @@ -21,9 +21,9 @@ class LarkSheetService extends LarkBaseService { /** * 获取指定范围内的电子表格数据。 - * @param {string} sheetToken - 表格令牌。 - * @param {string} range - 要获取数据的范围。 - * @returns {Promise} 返回一个包含响应数据的Promise。 + * @param sheetToken 表格令牌。 + * @param range 要获取数据的范围。 + * @returns 返回一个包含响应数据的Promise。 */ async getRange(sheetToken: string, range: string) { const path = `/sheets/v2/spreadsheets/${sheetToken}/values/${range}?valueRenderOption=ToString` @@ -32,35 +32,38 @@ class LarkSheetService extends LarkBaseService { /** * 获取指定表格的所有数据表(多维表格专用) - * @param {string} appToken - 表格令牌。 - * @returns {Promise >(path, { page_size: 100, + page_token: pageToken, }) if (code !== 0) break - res.push(...data.items) - has_more = data.has_more + tableList.push(...data.items) + hasMore = data.has_more + pageToken = data.page_token } return { code: 0, - data: res, + data: tableList, message: "ok", } } /** * 获取指定数据表的所有视图(多维表格专用) - * @param {string} appToken - 表格令牌。 - * @param {string} tableId - 表格ID。 - * @returns {Promise} 返回一个包含响应数据的Promise。 + * @param appToken 表格令牌。 + * @param tableId 表格ID。 + * @returns 返回一个包含响应数据的Promise。 */ async getRecords(appToken: string, tableId: string) { const path = `/bitable/v1/apps/${appToken}/tables/${tableId}/records` diff --git a/services/lark/user.ts b/services/lark/user.ts index 0a0e332..5758eb9 100644 --- a/services/lark/user.ts +++ b/services/lark/user.ts @@ -4,7 +4,7 @@ import LarkBaseService from "./base" class LarkUserService extends LarkBaseService { /** * 登录凭证校验 - * @param {string} code 登录凭证 + * @param code 登录凭证 * @returns */ async code2Login(code: string) { @@ -14,38 +14,38 @@ class LarkUserService extends LarkBaseService { /** * 获取用户信息 - * @param {string} user_id 用户ID - * @param {"open_id" | "user_id"} user_id_type 用户ID类型 + * @param userId 用户ID + * @param userIdType 用户ID类型 * @returns */ - async getOne(user_id: string, user_id_type: "open_id" | "user_id") { - const path = `/contact/v3/users/${user_id}` + async getOne(userId: string, userIdType: "open_id" | "user_id") { + const path = `/contact/v3/users/${userId}` return this.get(path, { - user_id_type, + user_id_type: userIdType, }) } /** * 批量获取用户信息 - * @param {string[]} user_ids 用户ID数组 - * @param {"open_id" | "user_id"} user_id_type 用户ID类型 + * @param userIds 用户ID数组 + * @param userIdType 用户ID类型 * @returns */ - async batchGet(user_ids: string[], user_id_type: "open_id" | "user_id") { + async batchGet(userIds: string[], userIdType: "open_id" | "user_id") { const path = `/contact/v3/users/batch` // 如果user_id长度超出50,需要分批请求, - const userCount = user_ids.length + const userCount = userIds.length const maxLen = 50 const requestMap = Array.from( { length: Math.ceil(userCount / maxLen) }, (_, index) => { const start = index * maxLen - const user_idsSlice = user_ids.slice(start, start + maxLen) - const getParams = `${user_idsSlice + const getParams = `${userIds + .slice(start, start + maxLen) .map((id) => `user_ids=${id}`) - .join("&")}&user_id_type=${user_id_type}` + .join("&")}&user_id_type=${userIdType}` return this.get(path, getParams) } ) diff --git a/test/getChatHistory.ts b/test/getChatHistory.ts new file mode 100644 index 0000000..ffbcee4 --- /dev/null +++ b/test/getChatHistory.ts @@ -0,0 +1,14 @@ +import { LarkService } from "../services" + +const service = new LarkService("egg", "") + +const currentTime = Math.floor(new Date().getTime() / 1000) +const yesterdayTime = currentTime - 24 * 60 * 60 + +const res = await service.message.getHistory( + "oc_c83f627bde3da39b01bbbfb026a00111", + yesterdayTime.toString(), + currentTime.toString() +) + +console.log(JSON.stringify(res, null, 2)) diff --git a/test/getInnerList.ts b/test/getInnerList.ts new file mode 100644 index 0000000..c541c5b --- /dev/null +++ b/test/getInnerList.ts @@ -0,0 +1,7 @@ +import { LarkService } from "../services" + +const service = new LarkService("egg", "") + +const res = await service.chat.getInnerList() + +console.log(JSON.stringify(res, null, 2)) diff --git a/types/db.ts b/types/db.ts index 79e047c..6e4f11c 100644 --- a/types/db.ts +++ b/types/db.ts @@ -1,6 +1,13 @@ import { RecordModel } from "pocketbase" export namespace DB { + export interface GroupAgentConfig extends RecordModel { + user_id: string + chat_id: string + chat_name: string + pre_query?: string + } + export interface AppInfo extends RecordModel { name: string app_id: string diff --git a/types/larkServer.ts b/types/larkServer.ts index 1e6c3a8..6ec3ad3 100644 --- a/types/larkServer.ts +++ b/types/larkServer.ts @@ -95,6 +95,39 @@ export namespace LarkServer { view_private_owner_id?: string } + export interface MessageData { + message_id: string + root_id: string + parent_id: string + msg_type: MsgType + create_time: string + update_time: string + deleted: boolean + updated: boolean + chat_id: string + sender: { + id: string + id_type: "open_id" | "app_id" + sender_type: "user" | "app" + } + body: { + content: string + } + mentions: any[] + upper_message_id: string + } + + export interface ChatGroupData { + avatar: string + chat_id: string + description: string + external: boolean + name: string + owner_id: string + owner_id_type: "open_id" | "user_id" + tenant_key: string + } + export interface BaseRes { code: number data: T diff --git a/utils/genMsg.ts b/utils/genMsg.ts index 688852f..2e062b6 100644 --- a/utils/genMsg.ts +++ b/utils/genMsg.ts @@ -56,6 +56,7 @@ export const genTempMsg = (id: string, variable: any) => data: { config: { update_multi: true, + enable_forward: false, }, template_id: id, template_variable: variable, @@ -68,7 +69,7 @@ export const genTempMsg = (id: string, variable: any) => * @returns {string} JSON 字符串 */ export const genSheetDbErrorMsg = (content: string) => - genErrorMsg("🍳 小煎蛋 Sheet DB 错误提醒", content) + genErrorMsg("🍪 小煎蛋 Sheet DB 错误提醒", content) /** * 生成 Sheet DB 成功消息的 JSON 字符串 @@ -76,4 +77,20 @@ export const genSheetDbErrorMsg = (content: string) => * @returns {string} JSON 字符串 */ export const genSheetDbSuccessMsg = (content: string) => - genSuccessMsg("🍳 感谢使用小煎蛋 Sheet DB", content) + genSuccessMsg("🍪 感谢使用小煎蛋 Sheet DB", content) + +/** + * 生成 Group Agent 错误消息的 JSON 字符串 + * @param {string} content - 消息内容 + * @returns {string} JSON 字符串 + */ +export const genGroupAgentErrorMsg = (content: string) => + genErrorMsg("🧑‍💻 小煎蛋 Group Agent 错误提醒", content) + +/** + * 生成 Group Agent 成功消息的 JSON 字符串 + * @param {string} content - 消息内容 + * @returns {string} JSON 字符串 + */ +export const genGroupAgentSuccessMsg = (content: string) => + genSuccessMsg("🧑‍💻 感谢使用小煎蛋 Group Agent", content) diff --git a/utils/llm.ts b/utils/llm.ts new file mode 100644 index 0000000..466983a --- /dev/null +++ b/utils/llm.ts @@ -0,0 +1,75 @@ +import { ChatOpenAI } from "@langchain/openai" +import { z } from "zod" + +import db from "../db" + +/** + * 获取Deepseek模型 + * @param temperature 温度 + */ +const getDeepseekModel = async (temperature = 0) => { + const model = "deepseek-chat" + const apiKey = await db.appConfig.getDeepseekApiKey() + const baseURL = "https://api.deepseek.com" + return new ChatOpenAI({ apiKey, temperature, model }, { baseURL }) +} + +const timeConfig = z.object({ + startTime: z.string().describe("开始时间,格式为 YYYY-MM-DD HH:mm:ss"), + endTime: z.string().describe("结束时间,格式为 YYYY-MM-DD HH:mm:ss"), +}) + +/** + * 解析时间 + * @param userInput 用户输入 + * @returns + */ +const parseTime = async (userInput: string) => { + const model = await getDeepseekModel() + const structuredLlm = model.withStructuredOutput(timeConfig, { name: "time" }) + return await structuredLlm.invoke( + ` + 当前时间为 ${new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })} + 你是一个专业的语义解析工程师,给定以下用户输入,帮我解析出开始时间和结束时间 + 如果不包含时间信息,请返回当天的起始时间到当前时间 + 用户输入: + \`\`\` + ${userInput.replaceAll("`", " ")} + \`\`\` + ` + ) +} + +/** + * 根据聊天历史回答用户的问题 + * @param userInput 用户输入 + * @param chatHistory 聊天历史 + * @returns + */ +const queryWithChatHistory = async (userInput: string, chatHistory: string) => { + const model = await getDeepseekModel(0.5) + return await model.invoke( + ` + 当前时间为 ${new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })} + 你是一个专业的聊天记录分析工程师,给定以下用户输入和聊天历史,帮我回答用户的问题 + 如果无法回答或者用户的问题不清晰,请引导用户问出更具体的问题 + + 用户输入: + \`\`\` + ${userInput.replaceAll("`", " ")} + \`\`\` + 聊天历史: + \`\`\` + ${chatHistory.replaceAll("`", " ")} + \`\`\` + ` + ) +} + +const llm = { + getDeepseekModel, + parseTime, + queryWithChatHistory, +} + +export default llm