From 634ef9b5a8b45e9f89fe1e7399b11621fcfe3b53 Mon Sep 17 00:00:00 2001 From: "dongdong.xiang" Date: Tue, 18 Jun 2024 22:16:53 +0800 Subject: [PATCH 001/107] =?UTF-8?q?bug=E4=BF=AE=E5=A4=8D=20=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E8=8E=B7=E5=8F=96=E5=95=86=E5=93=81=20SPU=20=E5=88=86?= =?UTF-8?q?=E9=A1=B5=E5=88=97=E8=A1=A8=E6=95=B0=E6=8D=AE=E6=97=B6=EF=BC=8C?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E4=BC=9A=E5=87=BA=E7=8E=B0=E9=94=99=E4=B9=B1?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yudao/module/product/dal/mysql/spu/ProductSpuMapper.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java index 68f2d210f..cf8ae6483 100755 --- a/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java +++ b/yudao-module-mall/yudao-module-product-biz/src/main/java/cn/iocoder/yudao/module/product/dal/mysql/spu/ProductSpuMapper.java @@ -30,7 +30,8 @@ public interface ProductSpuMapper extends BaseMapperX { .likeIfPresent(ProductSpuDO::getName, reqVO.getName()) .eqIfPresent(ProductSpuDO::getCategoryId, reqVO.getCategoryId()) .betweenIfPresent(ProductSpuDO::getCreateTime, reqVO.getCreateTime()) - .orderByDesc(ProductSpuDO::getSort); + .orderByDesc(ProductSpuDO::getSort) + .orderByDesc(ProductSpuDO::getId); appendTabQuery(tabType, queryWrapper); return selectPage(reqVO, queryWrapper); } From 42634c0e3f7b35e0a371ca59be16b6b8420e2525 Mon Sep 17 00:00:00 2001 From: fengjiajie Date: Fri, 21 Jun 2024 14:59:33 +0800 Subject: [PATCH 002/107] =?UTF-8?q?fix(bpm):=20=E5=A4=9A=E7=A7=9F=E6=88=B7?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=90=8C=E4=B8=80=E4=B8=AA=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E6=A0=87=E8=AF=86key=E6=97=B6=E6=9F=A5=E8=AF=A2=E6=8A=A5?= =?UTF-8?q?=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/bpm/service/definition/BpmModelServiceImpl.java | 4 +++- .../service/definition/BpmProcessDefinitionServiceImpl.java | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java index abfa0d568..7c4dae618 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java @@ -262,7 +262,9 @@ public class BpmModelServiceImpl implements BpmModelService { } private Model getModelByKey(String key) { - return repositoryService.createModelQuery().modelKey(key).singleResult(); + return repositoryService.createModelQuery() + .modelTenantId(FlowableUtils.getTenantId()) + .modelKey(key).singleResult(); } @Override diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java index a11fdbac8..99470f6a5 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java @@ -79,7 +79,9 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ @Override public ProcessDefinition getActiveProcessDefinition(String key) { - return repositoryService.createProcessDefinitionQuery().processDefinitionKey(key).active().singleResult(); + return repositoryService.createProcessDefinitionQuery() + .processDefinitionTenantId(FlowableUtils.getTenantId()) + .processDefinitionKey(key).active().singleResult(); } @Override @@ -172,6 +174,7 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ @Override public PageResult getProcessDefinitionPage(BpmProcessDefinitionPageReqVO pageVO) { ProcessDefinitionQuery query = repositoryService.createProcessDefinitionQuery(); + query.processDefinitionTenantId(FlowableUtils.getTenantId()); if (StrUtil.isNotBlank(pageVO.getKey())) { query.processDefinitionKey(pageVO.getKey()); } From ced838f29fdbb94d836eacc1294da6797b150684 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BB=8A=E6=99=9A=E6=89=93=E8=80=81=E8=99=8E?= Date: Tue, 25 Jun 2024 04:42:14 +0000 Subject: [PATCH 003/107] =?UTF-8?q?fix(bpm):=20=E5=8F=91=E8=B5=B7=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E5=8F=AF=E8=83=BD=E6=B2=A1=E6=9C=89=E9=83=A8=E9=97=A8?= =?UTF-8?q?=20=E5=8F=91=E8=B5=B7=E7=94=A8=E6=88=B7=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E6=B2=A1=E6=9C=89=E9=83=A8=E9=97=A8=EF=BC=8C=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E9=83=A8=E9=97=A8=E4=BF=A1=E6=81=AF=E6=97=B6?= =?UTF-8?q?=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 今晚打老虎 --- .../bpm/controller/admin/task/BpmProcessInstanceController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java index 819ce366f..9b1b9cf24 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java @@ -135,7 +135,7 @@ public class BpmProcessInstanceController { processDefinitionService.getProcessDefinitionBpmnModel(processInstance.getProcessDefinitionId())); AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId())).getCheckedData(); DeptRespDTO dept = null; - if (startUser != null) { + if (startUser != null && startUser.getDeptId() != null) { dept = deptApi.getDept(startUser.getDeptId()).getCheckedData(); } return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstance(processInstance, From 2414b00083f2779de2d36d88143e9cd83f7657d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E6=A5=A0=E5=8D=9A?= Date: Tue, 25 Jun 2024 13:54:21 +0800 Subject: [PATCH 004/107] =?UTF-8?q?=E3=80=90=E4=BF=AE=E5=A4=8D=E3=80=91?= =?UTF-8?q?=E5=88=86=E9=94=80=E7=94=A8=E6=88=B7=E7=BB=91=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E5=88=A4=E6=96=AD=E6=98=AF=E5=90=A6=E4=B8=BA=E6=96=B0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E9=80=BB=E8=BE=91=E9=94=99=E8=AF=AF=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trade/service/brokerage/BrokerageUserServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java index 68aba3166..1b3cf674a 100644 --- a/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageUserServiceImpl.java @@ -290,7 +290,7 @@ public class BrokerageUserServiceImpl implements BrokerageUserService { */ private boolean isNewRegisterUser(Long userId) { MemberUserRespDTO user = memberUserApi.getUser(userId).getCheckedData(); - return user != null && LocalDateTimeUtils.beforeNow(user.getCreateTime().plusSeconds(30)); + return user != null && LocalDateTimeUtils.afterNow(user.getCreateTime().plusSeconds(30)); } private void validateCanBindUser(BrokerageUserDO user, Long bindUserId) { From 67696f9d516147f698ea885708855b7a79d0d1c5 Mon Sep 17 00:00:00 2001 From: zk Date: Tue, 9 Jul 2024 15:47:05 +0800 Subject: [PATCH 005/107] =?UTF-8?q?bugfix=EF=BC=9A=E4=BF=AE=E5=A4=8Dvue2?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=AF=BC=E5=87=BAundefined=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/codegen/vue/views/index.vue.vm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm index 2328007a0..9c1e124dc 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm @@ -306,8 +306,8 @@ export default { await this.#[[$modal]]#.confirm('是否确认导出所有${table.classComment}数据项?'); try { this.exportLoading = true; - const res = await ${simpleClassName}Api.export${simpleClassName}Excel(this.queryParams); - this.#[[$]]#download.excel(res.data, '${table.classComment}.xls'); + const data = await ${simpleClassName}Api.export${simpleClassName}Excel(this.queryParams); + this.#[[$]]#download.excel(data, '${table.classComment}.xls'); } catch { } finally { this.exportLoading = false; From b9fbf762bd5c9ae5bbdc54960b92eb08893ea9cb Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 13 Jul 2024 10:17:42 +0800 Subject: [PATCH 006/107] =?UTF-8?q?README=EF=BC=9A=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=85=B3=E4=BA=8E=20AI=20=E5=A4=A7=E6=A8=A1=E5=9E=8B=E7=9A=84?= =?UTF-8?q?=E4=BB=8B=E7=BB=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .image/common/ai-feature.png | Bin 0 -> 21239 bytes .image/common/ai-preview.gif | Bin 0 -> 356191 bytes README.md | 20 ++++++++++++++------ 3 files changed, 14 insertions(+), 6 deletions(-) create mode 100644 .image/common/ai-feature.png create mode 100644 .image/common/ai-preview.gif diff --git a/.image/common/ai-feature.png b/.image/common/ai-feature.png new file mode 100644 index 0000000000000000000000000000000000000000..b4a55f547c85650d1b34e0bf0852a40634ecc99f GIT binary patch literal 21239 zcmce;1za3Sw>LbvyK8WF4Z+>r26wlhfdIjRySux)1`qCTAwX~l5Zn^r9g^LB^4z<7 z?|tR_iurY)u2WUV{&l*hx`(HwrwstAjD)lV01ONO00aF2o<0FY0q`&|a4^vDaIkO) z2=Is~c&I4I$SB0vIOup3Bvh2-B;;f?j65tfbX@dg_^K{>A@)NI}SQ=KcF{5FXA< zLjPU`x`O}S{aYO-DgaE>cDX+1x4K^>m_gBBb>OT(y1``qyMVMLjS<%p0Q7R*p4MO7 zzXnFj#i6PNB+lE2u05>W3_{v-k5^rhxP&sTQoOs+A1 zRsTtVcgg)p0>H55>PeojxW4Yve}X|7n*zKOR!k zJ%$5o-~U!0jt~Usxo0^)l_oGB%{2W}lYgD7XaAs)bvx~uG__vML29aueML=2#|o#K z`)`egU39{u$Cj6cw&rRqtedbTZzO^fwAP0xzG^QurK(?E3TZ4g zHwlp*34$Aop8_D&n@!aHOUXYe%>&<`BpA}%^0+f}5R%4&|8sP}-~s<_@PiI*cZ9)& z#9bN2l!3#fxu#Pq1po-!W#@`tWBw`Q`HXdM`0G;CmCGsTCjzWww-IWm$_i)W-b`fr zf;HK;E(rZ<^V<>r;g3l~+Lq3v^b-y*!}@X(z=Zxeyr@6>L;aEj8?K%7Gf4jg>>rc>&yb|k>bwBCZ^Rcst|Ki& zQ_aRz4hiKiI<@oB(o5 z001KXSQU0cfWIFGk+AB)eJ$(rIyHjUmn!Z(Z$Zg`{hND1?#b4@yUX#RC_kl?3$p-| zD%V*BKYEg%HvU6QAz+ej?ms*V@>iH6ZRy&(7Irq*4pIq0n)gX`55qOfiMnz;+v!gm zp91B2xbzPYX7MoaDWAS=pWPLnMKwRBQNg6!D=K$7R{~haAN~=HphK}rO7U%M1c~6c z-T#0UzT&R={U7>P7bI(#0D$A&y@^fhEJ!}AebIy(01VAWW$Zl^NS(h&^m*13Kr{c$ zqj)R;l-9tI-=Ke51VQLuuoHUr#NQ?!n1q}Aju+wM0;WWu9)nbRyq+q16`v{tH!Tda~@=bd@S!z};y6A);VA z{EyX=n-_|yFrWko&Tz<8@fDOb|Al;#XVw2U5kic3KIljH?`E?hgL**n%-L!GVYTxZ z-_iW&tAm-G=X@aaJi`CPc;}Lp#wtSqAh1-WB4d8PV7q}#SBGO9xXG9or>!Hcwp;zW z%3jLV_HUCt3hlS6w}LonUPG@yL!enGGggAm08v8rlzNP;ydI>vS=N3GXt?o6xtMhP zB)aMU4R-?I&wzoiEi>Cd4rK$aC;rLDum1STReI;?tTd9d*%FO|2kaj2~2wGPw#mD9vi|F%I0VHfC@;DjPpsp z4wYOUR=irC<(faifk=#!{BdWJW`A!{R^M_N(8>}((hUG0R^c&;!~+1H#Mu^I)t2X- zf8o_#iP$7~`l9_^D3}!BIx9PR{$Tf6!n2wS931pXNYe7KqX~cBV4+c9M!#H8r{`SW zJOOy`qYUJC=e0s=f6Chq!th~nLWC>I-Xgl1ch!ACP|v)z!zrt@y((8?Ufyth4cxlg{J=7 z`eH$%$AIHo{ZF%Jz97wh-pFBpyH&8b3m72tyj;(EXl}l1nYd8A6hxWud_)nsfBWik zE~8XY5JV-<28>(m3iI4FUpl3YonCCE9bSD~UHlN7jaYffxxc`!UQC;o|MF-NM*zia z_UJNOayLtG_N}19$aV4#>?=dC^XSa`D06Q%c13;@>xtG&2l(-ev;CXLuE66C1Kekq zJ!9Pwb#4Ph{&~*?h1QFo;>)A+4e{QdQ=jUC;$DluWaP|ZalSX-!CpC!OLcK>)h*wm zU@GOJh5oR1#UKPRBK!Pem4!@w6hhnaLT8(5^a}ym=7!^owRiSjoukFeCtp7)5_Q^L zJOQMyPzvuttDfEFd+t3+x_93L-)9PcuUUW58Q(SetP~lqn~dXtUP#orZq8q`5CSG( z=X8I9%l$#;hgNdx*Sm(Ry~NI{4lmtX+qGt9+s3%}7vk(?rfY|^;hGhdQ(5n_Iyjvt zc0Vj;RIJ6a(|e-HT~H@4U4G|(@RS^Td~?w81n9b0bR9w30Kh}4?9Ye$!afTJfCZ3% zjs&AjE}+wMoKTYaasYt<5nlkT((~;eaLq3;>7ze=A((mZ;PdUr)n}(@t%V>VKs7gJ zv+EeB0jPkz$5R3wJMGVXK~N@Ue?cA{O=b0y_3Kb=)>XT-SmHIg_bz??`xAiwjV@@} z0gh*4J>UIXvu6@l==TaG0gH<+`EL(TfUFNyS|Ax3*4S-0pltTf1uS6Khxc;r39x?m zwQUD$qQVz|)|fZZ{bvuzNNJ8g>EF({ze`fmH>Cl#p7SfOR&3_sK4Z*&$))E0hK~>=UeyR-sJ<754pSuGL@3JQzxVvs1-=4Gz zdw^~*ob%?O70%Dj&*DUUxwMM9LBxM35HP>P0|8_%*6tVUt8LIXh16pQU1LfBDr(}e z^ho1WWcUAc1bn7&b`k)fG;85%FvE$l8ZB8m%1EwFO6sjk41`&l?SD|=`I6;#XymkS zr9a!>+g!~8fQ;KM^3+*G6`5s%P^AKJ{nrur{`v0fryZz|{1&Ys2!obw@|*)jZ$B-P zfM{5yfAZbwnqVv8V)d1m$Gu1D*NOA{yDS3(gXPVGw_X`e(`s{j*hmVcL$3nwDr^$v zu9~YjwEQ!p&R#v(I`_Vo{uh|wMeE|=j*Z9 zoQy5A{{l==Pi}HaTaTP5f_COz&|(}0FDs!b}W&{t##4A4mL7UY8Vk6 zAAj6Lx6Gol!x3a9Xf^lu2mwHOPVg+ccS8$-<|zJLa-!UIa}`M-XtDzU%jO`5|4pd| zA?dmN#DZ1EVT%=Zl=ugUKJ%-0vK#*pN&tAr&l?nI32f2W;aK9V;Ypu~yH$B!r>Qkj zr~MD^0^#l(Xnh4LV5Y3}dK*eaV#x{%*DKS7I5-6uuw2~bRrTBjR5N)we0 zm{!pbL)W;E)8k=;8eQrRr=yxq;t+=%4$@E=^qfDpAwegI*t`o|zjx-Be!KbYfhVh= z!|}~y#-xg#B*6%l;g+*Q8W+d8w17=H>$L@B-N2i9)1A4xI_KU8VpcOTkMLa_pM$Y0 z%6Sq+7Q&tfaCUVmJgjzCgk@+9BbLz8D@Co~mm9*{KvM&j4^IF|)_$V&Jy|JaH8Brd z3UTV#U(%wzB7xI4H;dt{(eao`=o8&j{Dkq-ZOezUrQ3$-?P9g>3%}$S4w@uZlLFCN z0?isd*CLEydpHRdUxEe|_i*ENj9jUdKT!I}VP_gxTt&yzG9ZZ|x9~9%A6Iv2Iqv^Y0l{XXmV4lO0H~3AUG$w$Lh8F$nmOKWollma5{!O8n zskjYHlAOK_ID_QL(AHvLG**FjHx&%J#?bpvGt9R?Z&2RL@6DFh^P0p8lyEC2ehS<1$ zR$di*JR*R_usv%SD`q!Lao9w*# z&k2{hnXHMY@-($}&>n=X_!>Nfm0L)_Xz8SYu+tD*UiARy+lckmA&g)FTvPTDrYAr` z)n(P!C90*;GC_}fW&)|t(I#k-OUbpPOi@5EE{5-cnt4hx+FK;q5Q&U#_Zl5B+Ka=5 zwF%~J?z9uh)vTA+b$Z4}={q<+Ls^#OJ^|uscp*E;n)jOKaXLx#TRkBo^v4l#MEEV< z71X3v6$Y9HV{E(qS20WGRz#%-WvwZ7OuZv0W&UA3*A21eNn0{s21|L2!X8_`ts-3J zX^~)^kr>Evv_`}m6snkxhp^4)q>YWK0N<+htBI)t*LR374_o9sQ}$fM*UxdVnIh(3 zP5Ss2tKo~l3>z|Zt1qj@8A8s0y>&fS;yDsrE=x?Gsdf; z;Nqd`w8_Iz_f+Tu)0W`;@%@Dg+9ff^!!3uU1?7?8tvEDj;w;fAU)>h5OUW%aBh7|A zkND|~Ob`#lPWg2gIiCO^I1TBPRi@T8*b%v-wUs~6*N#gzDv#$?8Yv%(EoB+O8sQ_# z473iFEjgx`vg?Ck9eeO7nShg4Eht|8biQ*DG9=kBOC?2}5|jXE175U*gjv65-+ANp z89HRDKhcUn4SMF@vl!3RbC{gy%1|=44u~l}R3u=L*5 zMK)9W-&q!{?8I>tMc~m1H_ZOC*!HT`M>*cpkep!S! zy^%3imT))EN{RC~{6d)25JNK;1(7BWg@|_-ms*FiWxHA=b24#wabW~6!7W0zSmQ8< z3rN?UiARkp^MrkbKr@=u?!;n<*k4e_D2b0ewbALbKp92xYSls`^22ZZvNrW^Kc6yXwV%$CW<$IxeCyLjp%_!i?F2Qg{NJ+}rMkdr-ps8Sr0`m`TElNBkl)6b@d+ zpy|TJN8y>a=)j4?Q2xmjI`9!A{Bdbyfz&khOei)l1g>PzlAv+=32+joJ}O{mqc}vF z+B-DnNA}(HrdPZrX`E0Z^Ioo7R%+O~$m{-g%k;wExc4QBxtqv3)(R?1i0_9z2-qs5 z?`uPYly*H-84z3u|B8()HzVS%%&?6>9Y^7Tv`bPXHl=!GMrx9+>1e36m1QiYLMoU9 zY*5zpfK?+LS~0sS1sDZbuR*Sa2Ghcnn9)=UKXFhujXItGu(hLn72#gsHzF}%7J0+N zd^qxaaZyYvBV0P*7#&tmUI>(Z_aXo?>e}$bqsXV#$rygfcL_BWAr|oJeJ=1NwFWb3 z;3F`w6vgN)2=M9%W_;t$mfV01HtZ-aIOWZ`NfrH;5BJG8=XsMZmYoz0ljzzOp^WT( zNvWH%a%Yr9?s2nx(pFjj?-&Vg%=Lc#ROJrN0z#K)_bQR2Vytyd27+4n+MxSdRzatj zTA#D#11OEm%_zQ$KYc}dY_|9(<_tB&$O9XdUx1fUsQ7>yw>2bk?Qd_7nq*uCl{xRF zHq97=V=nfqdrmqE5K^)&f0hDM7^<(Z-Sn1qwX2A>HW#mGi09HF7aU@GEQ<5I%zS-7 zAqS3!3@*Xjr{{^qZv1uW1J@?mk^25g8P#X&dFwJB;5KMIqRvHk=OVf?TJ`K;Zvfwl zdXdTWM`U)vL<)=Zf`~`;*zA!w_B#y;AG)E*9WA^cC0^5?HV&{XIEc&nrCm|wSXS9O zX%PC>Ibh>(uZa@n2+vY|tAeqlYWvjzh#F95z6wm=zitYsns#=Af82`c6n=BX5x-ur zg}cd*J1_-oRc76vX7wlf?DBCXb&~~w=zEa)VF&jnawcVPu=PlGDUl*)qK!Hj>>Cj< z|6BHVX&y$rLW$QxNH!i}IYJbqqqc6zrgFLdaL7EnYeG`kiUb^PxYR&CC!)tiG%TNm zt#cim-kA884kL#LDiMo8GYG*pWOA*FzK8qrI^c^XWZz~p=s9cGQELlyi0N}*GZbd( z3-(8ri}6>qg_%QIpUZM{vb?u-Jk3XZ=Wxe<9%SbF(n46Ia);({i8{;_84}hRMqWZk zTEtwa&@0_C7I7Q{e&K8tpJp;wtE#!$%yEgzr@wQMx#AZb)1<%6I0dh5nF;H+$@J;2 zyXmvatFc*!a-jki(!l5|WK5k_H0hPg&_4!GGE z&r}Gn8`~~;=1@EOm3be4}L4n*7VjKE|if`T?mXvK(b227}==h^vCsO!6TnlI9MUOOTBXPtDztBC;++mWU zUkZw(BKHkbk`AFulEDvqUADP`rZU#evfMk4dR=#zsSNL)7)R-w$W@JDl${oYao6w! zkf2@$hIygD>07+7X77EI`h|DwG;`No;ewCaBMZHK96CQAc8i%{}pbcq=M`8-c7|a^v%pCS`J+3v+tXCyHkMsmkMT8)D%c3B} z>n>Baqplpg`IgTvnKV$PnSPR(ufVIEzw^B=>_2eprB{v559fGnNFmUX zCSmct9Qci~X#Ta{b-C^6ua^JAilu>*D~_a2#d#?=r#SY6*o=aR9SRFtavo{C5vd6k ziT}$=%Q3c(6>40Se^|tn5Z}b0k~u%CWuixHBEW5KJDyt4Wj?l?&i>z#d})eznC*qv zQhM}&PH~r*s3=mB>7Fp3IgadeS`XQym`47u>6%@Y$-QQf|9(R3trz8CYAWKf@97ZU7slW$EM#Nz(S{O+9nVJW8-Z{Vu@}L1f z(gvC}1EXmTqWRkUT|T-LW(bGZ(57ZCk*juZN%Dxmx8%PfPBvZ$X`n_s$)Fja^|qk z0$&@)QZfGCLza<2Xl^MaOZnzUXZ)i$Gp%(b1fBrD>lg)&ce;aY%QB23g7P{2 z0qx%etXdzCn~}g_N5LXZ1>Be)^7%>FV6KUbqME{@KvifFPNMWF&3{k%Ug95t(VB!7 zN|{T*5mq~CqEC*vX$4W?{RChxcml-Aju@B*&ai3&P^&{JoQd6v6#>>d4!mSuu~d?}$aH8j z?shD@fFYRH1QA~Xjs=cJD4XV7mqMrLVuY1jO5!%dO}NW+{w@1YqiBGRFU_)Zdg$j* z8_Z)a$6{u;6>C}xlrx57iw(c+h1=-lO%1{J^SI>3na!P}5$Ks{`aqjp0->OozTB0B zI(^5nylg>igtQpLoZas~n&PLdyKgLN;8-l!kBRhPykwtaNQB3n?T^fh*S)DxrF@7; z)y3;_L(@Iuc|R%@or22hpOY{?ezu@!vgv0$H(`j2SR6@)fK6LTCdJq@A8RDzt+q4) zWzsa7j5($IDS)3`oqQ8TAn9D6aO@nVYj@K-u-}>oi*O}{@!dW3?KuayjdGDvoF2SgPYdmlWt5(4{VKyel1S?h}ZX~mGVM;k`J&ZN3YvTcP~VY zJ2Xz_`BlNiK2}<^G2H5%l%oL!mNOi4cppgnpW~Fjv=L~TUinO@kyg2MZ_z?!ES%N~ zO4|7&v&7T}J060h7U7;pbl&vpbcjj`tVL>0d1)-*$gu`AEXVdz7c>XxflHd&_`vR# zvtw(^>Z-jxH#b3g;4;uL%jw1r|ZY9N@EX+>>v_hm@ht6f$dt0DE=4p|TVV#Y^+a>G?R1{WIg zn)%jDHEqz+=p-#mYBR~0mlHY3~b39?5&WX$0v2)&K zLsmyIQ`$$W)lpz*QD}Y`@YYOdgvh}p0c^7>QHX)3x>(_|9W~X77tA?f-^RmE(ci*p zr|_OLwccir^A#ac@{y59V8}{ee6?9QCbgds+8Rk!rxVAEDOGS*gp{S(FYCxMaP2(S4dP`j|aNL*&-&zDl6$`<=C?RWBM7zAPN5M=b zucOo5ZPLSOY2L*=s`WifeFC(!m9ZG@G6JO?nZhD6kd?F)>YQ1?`|S3#S9N_VVV(dp zwKI7^T3GnNxzeeRX|>G!>ogxo!PgT|uzt$$iz-D0mrUCTOUJ;b`Px_UerP@I29fW* zb}d^E2RCS6=z}hB6^pFU!!+?$n!-~r(svhN4RA22MLq3f6$1`U<@&TLjHK?e4Vf~1 z5uSr#fk^K=Km)nxe-E zX2VEehHvg1$)Yr>zyZvbk%cFj7}CE8G&eEKGowgz6_Qc{u571+9Z;ZPsC_uvC3Y5q zs8O*{H~c7#FPWAuQ!#Q!6F}UJ3?+dJ6vvF@!W((1B&#PMi6%gefj<;a z5p;3Hw+wiI0=@B3n`s>qX$ZT6*$T5D!snY6=jCY1qE*kK)v>jo0=;;WA zBbnb=DEQlCnbY=-$pVIxtkRSVRzmd2Xs8keZ;QNWl`Ex7hnBn^_ z?-vdeTBG3@f_JNdv9H|WR((zjl=zaag(2uIhPA_!IZT;8Aum?jFvz|$g?oI)UT}-F zF^+Yj#0u6Pv@#eo_m-Snq@bL1rN9a+GobU0+%|nW)C^C5f+dF|Y;ud#89G^Y3N$vR zqwK)skJTpXg>?{=v0f?qZiAraaW0Pg0e?V5-H#+_+2%T|FAtIqX>xOZ4=W@}6|dgZ zy1$>mWXXvGcQl^G9g@pM&Jzet*Nc2LzZU+>j0MtFH?3wd8>{00wE_}Lm=15pbR%L#v6MuSXHYAmJyT!vE5?R+4f!ed zVKxiD0^C4unlj0_QL%#fb}cZ~f^G0T$Dq#?nhBR~(=@#7G)jC0Gl^t^==Z;LcL#^9 zg|2aTOz>w!-)|HpSoL?Nc_l7N=+Vv)2K7VnKmZ~ZWGU)JJ#%0jm!}6(AjN~|jf&!& zpzHq`VWHsw!)Kd(j^O9qG2;b8p3fHUVduRu7RzLep(iSU?^L+JlK|drL*g%7tb(Hj zqb`-Ike@Ct&O~K3S0@RrB2x)^&xdd;t8=^n@ZJf+z#=QtdF9TbXbv7OnMJkwTUC;x zck8<(LGL$hAOnTSM>Q|wtk_4+V#;4Ld2>Q+yreNh!*OLYb)`eWi~(HCy(}qcpgR); zK!!y-LF*AW8nI%$PT5Q7;f6#x)lpKxZf3{G7-y(?z*f`jUERkSgU))*M59PQ8W@Uq zx{89ejHhFo>Rp{h_$;D`=0 zVt7NbcGhx*o(FY{vWrMMrpgstVquV*g(`Z=Q8v)hyfus+>&V$}f67*zYIg;F%I9AA zwxXFL3B(CAY8{i|$&HTS&4QDj19Cxr(ujrKm=Mdl+2 zJ0;*KtQDAGUDS)>VtO)p-75ZStLU5>#Aq}f86=&V)QU-Yl4dPX3FvgN55mfMnYlvG zuKNkLkC+kqBTUkBfb)1DX)tb_2DNnka)pA%6oW1DG_b|YxV2kj;e)sd2`#biaRyL# z=>;n%+bA(1`W=^_ry3N|8A7moxK-rnwqWk}Ez6^-NKfuQCy6m2eXUACdL|e5EYSVc z#kmGCa{G_O;DBmDN~4Xhlf%DNLV~|cF6QQi2~ZM^y-Ra;8eK7YJJF}rPEE7^EnLz?hu zf|g~fKkywUtf>VXe+d2@(CT~<0upT_L{frs*!}zf8vrCKila<{RpRNFrZJO-Kr<)Q z(}t2M4eC?GjKea1LC2i7IEGz}0uW!!tkska$_@(mA!1S^n{xreT{OdF&rKs_Qyx zWPhZS=u=fk zg&%eB&3@Q?uR0iI8;v)lpobuhgDX`^;cRYZrefQ>V6c#9S&;0kZ)t)4Ih?^T?h}#( zp~UncJx=}ZYfi=nzZ8rD4Ju1?bFW=`aNB15o>F4BaLm|(W>_E_HrNrP=-v2KA_aY< zW9~~?nz}Qg;qj{ZV28W*Yo$eU8tPe36*4$SKBC-gE`4&&3?MMvO1szeqw!o5(XCFs z7DB|R9gaG+Rs}VH71yDlPb{^n4Gc!8b_=Dw=@)A7A{^^K*#**wa>$bEB@<>SOWn*+ z#^f|-(3Y@7siKwEDw^SmoVlmflfQKU21|8jp%K~PFDtQbaSk-htF+HNyrgm{IMhQY zqG_v(qmvw`7y+-SB;`slZzTbmtRfm$0E9 zIt&s`lwIc!PG-&LEe5G$VX)oC-;h^c>y2)|eYlsy^dbg~3tKaz1=iE{lVKfT$w4Y0;`oP{=$`QGx zLI+!IMyB-CO~1(BEgg)B7M#+bjfOnqv&;1RgpVJ|)@_E9iv5Bh;8OzJeL~!~Bi};B z)%5j1lad;lol2>UN!$3d>mn!#QxOq@Hg{jzmOJpLcI7?+Ubr|kDCbVKlk0HOYN{Fw zl&s?;ArW|>+I7NmAVuX1g+JJ*Tq8Z?*MRa>wl*^H8{O~VVXH%JvpT6b+BVK)U*+vh z^{gt#!YesjV_l1y*_av@cEuOcLfr**5dxG3R%yU4Xn4BkkXL$p z$cB1WUxDNK)=*p3U>ZlkioSl`kE+8CfTOC35i>m?>dT0}tiwY8_i{S>4d zXD;Mil?h`xV}k%;jMI}afPf+|McHTkg0GIw3=;G{VQUS*PH^f85Zy+DZEOqJ;|wx- z=Oj*0K!Zxuv{%6wm867=s`#uOXufhvQgbp%@O$Jj0lJsH7>5vZ?|E=jUE_DiNFu=x^YOhpG!XE*7vY;80Hu zx(SesNMkAEb;1*|d_O`)^M}YM28DtfEd|PhZ)J6zNE)tzkh5l3#xstBxR3Qv2D9)4 zXi`|H(9l9AH6AIGRuV;b1snHvxj?;vGF6|Fh%}0-W7fK55Q%XHCK}p$!=y!s6SBfn zM+o>T`pX{zMJ7p9j8jdy_D2)83rJN18KLgjG0Zc@5TbNOvnr`pWjcBgnWVymFd?5v2Bpr ze_=*M9)Vv`ZTzIALMZvDwV50ypuSdF+{w%wKvL53#xfI239KqM-=Ys3!lSv(WQiux z2q8vthchI0=!XTjot6c@3E|kgu7^6eMg?DQ3aS06&oaJ?NVnB*(;Q))X7wS_TW1_x zA86qXchtqjW{gHR&>F(ik6TL#H+QB?8{f;mbD(YX4(IUQ#Gvf8f(rA1?F%cM_CwB;CL#gN@JHMslmM!H;=W1__`CG*R2RfS#^6tp~p2?W6N^lAeMx zHp&F6waj3FF^@hJ9Q=%RTVCK0bvFxoH^-8|N_5B2sVQG4)&GhfDkz!(aZzlsT-@Fs zVXlG!)$G-#j=mqHSYFqcr0E|v7W0z^ryqPan;gVlB0uTFaXYZ&;=tgIFg2hu)W)=m z+ej@d9&$fU!eA$vKYk;r-Z9sQOEz9yF-n(_q7`Br3(OwkDaUMaCV*BM(%$WDR+38^ z{lS+DJgi(+O))idM>00oW#z_$4&t=~Rwx*EyTAI1CO|CyDpv<;dK@^Wc7$Bo2-Y!6 z+|vQhHPD#i5h+Lvv)TMTz~Bm1@KB(E%5~$teRu^hZM9wA-kb{bZAd3Mv~sL(Vb|Ek z>5Q}0Jq*kuO_WrzXOvYFKSdRqSEy03TWHigAqM;a`<1m9t3RP}KNAxIY@tZxy;fa7 zQEgPsXm}r$Y2ygZrms}xj28tNk5JZOkft=e6@~T}A+B7jHA-ejI{|x#1E0aqm|&*d zwUxsS-EnYztbnv`;+7i?sJ264e3Fq6q~rD=DZeeo}%cM#3&FvX+J4b z(Q(->8%P+i?pBU@J^{X2uP%OF$zdsMNAE9_jBM1C#wy69NOP0GPn8cT-9hK_|1v&V z{1zDxaO%})8t8?)sI&%diQKZ52aIPS#YoX0))I|Tc5qloH1gxajaN4cBB7}pLdwwC z6D5T_OG#hY>4vQw^UL^38EJ0|FV)yH|LoDO1~>S9jL(EIEs+SZO}z;@X`C^%yDg6i zmXG;Ayr*wa*uVpY@;*kw@ThH7>5YpdY35d4+{pJr{=@TOn(rs!maVds~L~Q_35(3zIrusczyS|!x@b)l8y$PLY ztguV5)kg!fPKueWe0q}j)6b)gU;xKx{kz;|oysrycxSY&-E zjJ^PUI%s2k^~qQ}dRga7Ecd7~`Zmc})?vD&6j zs`>k^>6GuCQJ47EZbTSgGE2TuUw2g@XJ=xb{~B1*%P3MO1%K+7e3Brz2>&8RE6Ss6tn z7u}KuIvQfoNh(yzoee~=h54B70JC@Vzr3>(DNA4rUL9wO+`MSaL!5nG9ZK*Y1X9#? zhkdQ_8@dGTIK0WGc>`P=>03f6TlKJ4^MGo|mWG&B{Ly%^Y6E^RG5uQnIzU#yE4MBs z^2imR%`_nfAey4nOJM`hkuHV!04||NE~L7&u|W3{ln_+u-?zmHEjG=Q4HH?kX0Ab0&$3tE+T+)XbRbOu|ttV%vd?E0lhB87e}dk z8*tFk&jtyMddHV@2RFh1n6bCY8B5W^$C8sa4mD=9x5K8CHz9%TM|c^>7T6ljxYXF* zG_RW1KBa&z8pcwttp~j|srnwHhdcl)0Ft%4x%ql+;Qgjm9`Y_(YS(+1oBc+M$pix) zybiAEd+(cHIf39V6#u-G>w(tnFug5Fzw|@te`alU z5)ku7jmp1`=YM{lz}!y&g71ULKdinXojgYz{A>igE4CD0O0(N+q2XwmF$IfkaqWl0 zH+;jmFau!F5o58+T41-DX%C#p*#%g@1*QF9NW%GHqG!^~Oh_SVoWh%| zTkbPp1A~q4KJADcufPu5*o>oJwz?Nc8JTH@Oyr^?yzDvCu0pIvdD>8bVCckeUP}J0JalYr++89Fu6 zl)Q|OvIwgvG_|E0DWTZh2yAE4{GBp=BC|Ok#0aq=M(7b|#-9Lfl&Uu7Gx}Tld310L zh3abUrmf~9jET|whSOs9*eVt?$$&!$g#e}0*F&4MRH9yWwxbg;Joup(D~!lf6*9-h zyGjKfm2W>P*j}rINYQvJ;0M)bWnvCXmB7GRo;*vqvCS zV-f>*sicN$&ThBq6o!h72lquU{%+4#pa;Pe-x-`;iVE1 zQJ6p$D1qX%9D<0PUzO9(f{}|hay~FiBaUPfTXbIy6aj9bn>M}Z#Z&UZQSlX~%1ILr z&W4XoB{ijB5;q_6@|{y+uq1;ueV-}%U5dor?{)7z0)Ug&BUYCf&U= z+_QscS2rIR@IC?S%|`#KEmdw5?m$7Lh~GXIq8*h=YM8%Mrtx>CbZ3p*_VIj<)irph#P}=9s8avJi=^D}3PDCu9N;_c zAf}ohjsImcNHItP+K%In*~j^_H^mR-?Y@HyUlE$WjK4;yG*SY;m4Jg~%rge&T+at8 zWYEdP3Ya?OB&jzZSL|X}VY4ExBBkMc$g<5_q#mMYm`rS8Sya;11s=^X*rYCV7+Sox z?bF3-A(?Z`urV5|#LkAT0S?A(udXX$fm;OOIK4EZ`r0Uw*7L!SS7HZ)v?fHLm?S_K zP8bHUluW=R@45xMG&i2Hf;@C)t#)=Z8f3eNKQBUl?CUC|xqDcwm-K-;3a^Zj4ZXcN z={}zghhNg?>nxWVld=Y5p;883fa7Ver!0jHBfhUkPd0?3SdXl#aWo!9m(t~z6mr$ffyORjD-6v4@Vm0*V#s!0RegYZksn_4Af=nefny}8-l zURnPDfob|+(heR*4KE5uNh&<1@_t6LBnG$b4x=?K)6@<4r~+v9aj(V3N~yCtUWxtd zwz_mQ;1{dc{oB;&Fa)7bfN+WWygaM06XS`2uQrLTPF(&_LETpjakxQZ%)zeQXr<7{ zC2_9!X)XyRGk2HV>At1ZzFDxR6cHmui<>Lkb2Ay!ytoAmNoL^pP?6DLP`**>31%xr zoAp&*GMmx6(APhjixcWN#+W2p4S1dH=7AMz6v+rIYg%Tck`9+gzYG|Y~! zXsh`U4)DkF6vA7{K>xNTEf7A(tT()9K_BZp(x3OQ8@M-EGL3_Kh|x9m&X|LPICPpKg2={*bR zKc|3xa|HSGCrH13xCDj<&H{mo&PvKAYzT?TE~JD(qHILw2&E$O+V|%}dT=nX=ZEyI zaU}k*|NKFs%3AYo`|f)EM)q5Mn}K)c%=e>{WtTXoT5uhWt&hWivx;c$s$N7ILo zcZh_H3R>zeQ^uQg>xidxUm$u4&sY!T`{&hARs~~v47&TARkQ09OEqR)e>P_l(;U0_ zsXtwqT3vFf%(Eqa44EMDLq<+yofg(*t^$-C2VM*xn9aO@1mTd;@4BUAlrxR4e7{3{ z?tRQb2|o%_{*NnG9*t++e|5Gh6;a9?t-J1g6h(-|y5N-$9jQC&sP2mC-4^$nnAL^M z^~5ZA^JnAGk!wYpZ`&-bXa?*JQwhvwGv8YdgRISq{;Z=XP*7zk(?e(+E(pumt^V#vCXAN@h#ut{)IvQ zIHqQ${7LfXk>(M!t=g+CADkWpvq4UXh*kE0;q6!z|3pEPPZUA*1tP3}ad81xeBkh{+nZJ|x5id>p~BI&7T8Fh#sFk7^co z$q0{n5wjo2{l_{FKfEh>w%$hMCz_`Igff3%5Bk4A{TI5QiOAJbRAI=!{a}`8{TxKw zRzcJ97v+gyc}(b_oZwV0-;0sB@^USoW=A8dpAP2_zCCJ2Ib(S3S%-x3NF{%m8a@5H zp@AzrNxFtrQXUPf&({(^6)kI`T^Z-?{DZY3rJ$bn#yd9hkS$6*<9X^yt%JQ3dnOdr zPQH^9WWn<|Yp$t*#yx!kY#W*MhK13qSK*QOnY_}@weR2zz zI5INYU#D}S3LHGR%k%Y`zDbZVt4|K&+>F!-bIbTAS7;K!)ctuKReXQx2P3u~UOjbh3bi)jB$Hic9WEf-(peoH$`0qO(q zG7RbgVtCaLmi^n#M84lF^(dbg=5|09zZgd*nic+8-86FY2cgzWssrmrr5#=*WXCgz zIodkq*VZLpS8Ww>h~y4cD_j&X)IXuS_G^2TR!NGON__dt)f9%NMcIsdeLvq-Xiw2Jq6Z z?6Bywl+|rjZU3}n4sTgukv8ZcQTgVxC&fXW%jD0|Tjg*f=qg&iwo*SEdIjyK+Bjm= zW~SzSqd)x0H1iJus!&_-EXP(_7TibX`#Y?3(!I|EmkFvI{Lft!Jwv z+l|!z&??pTn_|wAXD7p7%1v}^fa1@#yq3km;83=#Xk?P}hz3s{ftvsg`b6L_<3um;8t@g6mmuA(8U&On3XIR<{Dd>9gssQN7$t4yf2F;oqmVKJ^DeV z4D?3OMivqsSpK*M;RxxP5isvg;blzeoGXKN+eqmZF|GcEo|#C+iK|RmBx9 zVp{C_ZM@yea&( z`p;w!)>7VYt8EirHgepGe(S$VenD$UnQG|l8(QJ(leyMsthQ!;#TmLbFZm9V&<#~C82Mgwz+lnI$bO>_3tY*=9;OeaxavJ>AG#OuF!QE zQL{k4389XjQ%zel3JMOKvS>|w_ak_V-OOoIj&prU`MG@C>yAJBX67vST)JL+{pR1( z_zo?!b?|f(9;*8v?`G1}>A4uyton95H{^@RI?Zbx~ zIcj5n>+jy3nz2Rd-J}gu4l%d9*!g7!M?zfIS|v*jsH z$)^L2X2tM6(b^+@I@E2k#LI%IVVANJ7O;9dNWUb5ujNh-Wr9UAS(h z#p%1|fWlU8Z|TK>TejZXDs@JA(=x9O-)nO0*NN7Bynkm|(;HETh`e7MXSS6eb${H> zliqN=&xT!K&xFe#1XuofP=HW-PXMw4)W_gy$i3PJ_?CkRX{LN1Pn?>_C zJ+)6gRd;&I-|3pa^@6`qMK~->%!pr_|8;Bpp7QUSi$1n|6SLUz$L;X1hvDyK|DH;0 zXIizx^6BA6X7bm$_E)@r&r~GEy@B(suDO4g-1@}71*^aJ{d*d$xNg#F3(HS70bi?O zMy*wM`8I8arEY>!OyA)H=2zc9LuyqDdlE>kQC7mm2hp#-LCk#@9Oc*fR@a~?tIWMI zfAx*}7_GmL_wNij!6yV%{G?~r+=)nv1B4e<=gqJ*wN`F84_CY=Z^!D^7_EolTsuM! zt=uN78YVtp5yoF967lfR@{|g(UwhlX9$J6T`uEXW&$q{k8fJXZ{aU{)T)Z+h>fijc zrQfu!m0vXY$9FYSgZ=c~1+lKP#l%*}@_B!_@XS1fWz`Pn8t&TIb-G5P@~bSHu_n?Yb5Rjkx8L`CqC%@NQIGqjs@3A20K28{%~p* mKhyoTgm1@ZDcM_`%s-~;|KNUQW`sd&2>FL?uvtPeYkBAg0S=~~4#u{=7>hV9uT*3AG!HB>P(Rem z(%!?#!NJqX*~P`x)y2&P=i-LDzzgT*hWGUF^Y;(Bc{4K5H!j4L6y{Ef@{5bOc`H6K zgAlth=A578UlfhYNwKU+Fl@bL-5clINWxd71XkS%ueuXonorKl$xBVoOw7F-SCJH0 z7U@z-wz*$oLoBrOYt;!ES0jCvxO=Qo_778j98|fPm_CB9>F}$sPHv-Q4P@Uduc)i3 ztgX2F=zdniePTy(-1D-C;i{C5n(Su{1ye1Bqg@Ys+G|^zsvDb{AGfzZ>1yqN(mK@L zI6lxa)lYdl{AgjccCN4L>pU#$HoC zQwO$w&91DzU--DVxVW^uO#Qn0{mb0EKR7rzJp6U^>(}q2->(+mZp@AT{4lh<`119ew=c#po}6CX&i?&5Jw3U2{_pSa-)Bd^{~VwE{q_6L(b4Jeqtjo%P7i;b z?EX3v$B0PlOUM|YdH!? zXfM&w(D0uCfp=-nX(TS@H2+&BDc3IG;km}cdkrpljsGeS&&A-q`0(%}co5fkFQ)&4 zkC#{Ae-}PJUJ)K%iEICF%5(9J?*dUCI1ewJmmki@2fsi-2rkRRFCix&C?Fs#At)p$ zCU~wN+ZQY#r}t?p}45{g-PO;Lg z)c;v1DlHs@Th2n0n&^W91vfV^tT$3$xX=)HOX-G|T>JKK!Tk;7se`zY9m8 zb57A^f6-8y*!I6We(l7Z)ENd^;&A zk%UhrkyFw#veNxC(+5to!_)6Pc$|}ym!F?ekU}UZxLZ_wuXyD2-q1lwUSdhf{n7_z z4gyXC8=G318d_W1+UEYWw|927J??IK)ZN|F)AQs)l1~@@^!4=* z3_N@GVSjLFcxdGL*!aZ6i>ZmPr&H6@uV0P5dHZhe!~FdG$N7cjkISo{J}rO#O#SlZ z>&ol8m6g@imk(CgHrCeG*AD)zU$F6iY;HbJ+58_{TNmSk?F)9cfBoCp{kC_(uRq_v z|J>jI`QyihtPg)49$)DB;OOk|Le&@iI{tOR(eGbJC%-OqdGzQ1#nxX(|Kmce7s5UJ zeSCI&d=Y+6E`smr>7Ua-7vtamF8|}~&xP#I{+<2#ccK4_(ERs*|e^ctq6d?*>hD&{oNQZbUkBxqQQ$t`cqpevO0U~j5=eh03de$Ak*`b9p{ zvfk-MTg_Av%44$BpuP6xedVz2r5EjWuODhA)AJa1P-d$z5jIYf9gp7DVXF-L@DeNI z)pqTEpC&sSK0L;ar1KbcHGXWzza1`^>}p!<4tZeFIceVXsW<9i`_okS7Le==Q)B#WyVk1)~1T(jChfaVxNY}wKI54o_1`#rC2uX#q4vSye3m$ zl$rE)eOv4a+gX0u+x`9XU^0xa>}qk#%5!GF#@?q-4mM`0j2@cyKmCI&8B&?$vtw6~wUn>v5?z~LGf?Bml}!0L z{7RPcy0QmH2v%TPhRtQ|OO6h^?`keam}_2hd{Y)C=!`KREwHljT`RQnezbPiDROJA z2uBuOFZPU78NFwFNHNNp7TsFEA2KewQ5s${x+>E?I0R&lJMtZUm_h_rOG;({Z?iJ%9x2TtpmJW+Cvcj*rv1@y~p=JDB z%)i2bEtmMnf|}&3b%n6K^LP;KxFA2u?!VhQA>1I*$|t$A+xk+yx3NW>Q9e_)C0u5w zYbkQ)TleG3xjW6kUxweFY}PjH*}myoY?gmHy3^dgKY8PO>kmc2EQPc6Ug>8vMzoEc zFFav;LlBY1md3B!C0kFK^d)|bs+fEH7~}M5{4svb)#>GPo|MKP&)=tBlYQ|XUnM&! z*4@acK+Ed6KP>+t;NYd6yz!pqy2b9nYfZXuxv{(aVbilZw|wN^?C~(JYMSyL8c$DH zDagIDTNJ}TEMWgq_sECaLHt9Uup6yyB@PfBuAIfvadIr>s3*w+}^m-5xvpBVnqTYD!P)fmex%$z*yw0PBO!sK4x@4Y|YDL~grvDU+{3Q|wI zRXK0EJ^WhXPj1ZB$3!B_Fn@L=`2h3zOgL|xF*}G)1&&#d?9L6SHp!nV2P+-3B=X?ci<_*@FavI;l7k*t%HV?;;hvn~ele&k@k%7mb%MY#eSDwsJVjZ% zPi-Z&yPPCb!p!0;B$zg#{8cU$qsy(Q&DEI$GJMmmPBRx^NOHKkP@E&DQ(=-B(0k?l z--fO}w8aUiYQR+V1C~9wp8agX;|tqyh7%5(Jq3q<^5loz0L$jE0AEOJny0%9F$uL% zbtIKBFAN<*g=SL6+YQTYZB>J{rw3Pg6#Dl~Wr^V2Yglg`|C|!l( zds$v{h4&Va!Ul0#u%OdlMejAoznDZ(FZB-}hy-PHgwvzFMa(`2LmnHF|5W z!DI+5iB37HnZbFGRuGv4eof8^bH)MgT+bkf8D3({R7SF@W`O0-LxMUo2S=_HpolMI z=_}!Rc!NfnbOlwn{~johun}^S;mSi`fqAqD*eA@H(rtQ7t^)+0`or}c9j$t<;+cC$^Sai&Q?LE^(ND^4}EoL znyip;bAnrZg<}%yjcWRl?z$hC#P@?FarU$uerkQSib8hO_?hXj^!4#1a!fjW+RoZ4 zN64w5G5G2jaoX=5))1-9&ne^mYCZCy#bzi!#($AXop9dqm`4u5c`4<(zfHH^gaq%x zy%d%ENb7*b(8|P&%(8nrTu(xP>`|JjhRT(Ua`R|T91Tc&o)FXo*AcpV6DkE9V(P|^ zF_aFb!Iy{s6T@`UUu@YTr9+Iji*;mg--Kd-U!m@l(Q9u8cjV$O0o^yhe^JeYUX8oN z7I~m21CasAISFu&9n&)G(u`^t9xu#;*-2W{fey`ANY#lXDXD9FK^ea^3MObbsDr8J zan@r3zCSXOxw4Fy&{fBCn|x=QsYck3MXED{L;GT zx=+c;=nrk?g_)EilF%zS)YMpj$&!)?ZevO{>~QBA!|O2hXu@0=2z1^PKp~oa5}J39 zt+;0tA^Zb$g%iN0(U*R!t_7m2Okl51B`uk?LU?nu=BGbugN~^gLp;qamNbKGVp>YZ zKz`VRsE{s>*T-goi%Y9zVH-La=qvLC)-}Z)LD}!tw;YSvN=e!e4QB0C4^;|9vz_~# z8CG1qS2(gLI-*uue-<91uM}c{a*<0bd?`8;30`0kAvYLF++OJN=k;Y}w^T*U(!_M1 z@@K=mwK9%vV{HujEOQXJ=$8SR=R=<})94aETBwKlm&qaanHGM%ZGU~g3aI|E$>+Z7 zB}RF(XFfMnGS1o1^!;A(q@Q8KB-QBxgj9L&3grVvh;DrbHY0!C3GZB*!k8E7dLT8@L|F#+9kVB`^okD4&k z7C>P+gY3A6D;6M!ib|@c_Zg(k!s4=i6S2p_Gv#XcSZzkAe0CW6CyAu?eVF)0(BBQd zdnrMr9J&x1#{#N-X=eh78YK9}&~B4Y4M1*5gspWYls>m9Q*hWEhj`ABc@)E{2Sa#< zlpkYn_IEJkz#%y_(7f~g)I0O3g!0(bO~y+bvG6aj*8SjTqYUcojHOJ_PMq>fqUUoC zmKPt4hkYPLIJ)u~e7tR1pAchB;!OebcmNXVLl9GXE=a-&92JRO!UNZe>;sP>Q7zsw zbadbJWBk|o!fe30aPKM-UCo#&;-^EjCjB2P2mWv}U4+IHP9zW?C^!VWH$xZKLRUrw zbK}6iIkearuqRE@08JpV9ALNa(v^cpF#$#1GmquKfcIdnnqV3@6l9PSJxa)?f!fi~ zBH#cN3dEI~e7#3`jtYeh(oS?Sx6Xi7Tfhu9&@2yF5Do3^K@0QwJSi0+%N~P*M7Mja zXhr+{&GWk|37F_GMTZEa`_>$}ut+6Z)4lQv`cZhw_VX-{xgfcXl>0@YnK-woM7n>s z$o`7%CGV{R$Iw$DsW)us6PRMP6|C|G>FNg4`ohy_KUmzKFdoZ+R<$sU=)=ZP$u&J8 z6C0i{3mD61AVrDJeS^@ywG7ib8C{$#J<8d)^WxX73awQfO05g`Mjeyhcty2DPuLZ7 zSwqaxX5qC#`~`Ho`oL&mv=7Fog#?DY2Q5vQ9@mxJ^(x6U$i6J6xsGHRz(LGtXyG)_ zKzNiL9H7zy!s71mRM2~uUkbv~-|zy{;OL&rSdd$?V_WDV_klDy;1AIc;s>GP^Mr`= z=d?H)uxbu1IV4#nRcSf|O0y4iv4y>v0~hXtFvWy~7MdU`O!YAD-^s%h=6q|4jaPB# zi+LYw`)H5aAjI9E5L`^)L}6VDNvJEKt0rg-j7HdyfBr7#G%R5{&9J;@^F*Pft~0B& zFvXdRg^bKtE@vRjK&L`rH6gG%9Bh&bz1pT%zqp(cw7kOeY4MJ(`T0mmwIkY$whZyd!R-*kb zoWl5+3ZaRmpGDCp4OR_~-qqN?>$hdp7Xr?DEMs`x`TeyM#A@ptVy!6C|-K{b#95#l9I#Gdn`}9ToAS@MfeG_(@ zh9(Hh@N%Ii`zp_CZkw$x`#{LO0)rU8c^^)P=F;})wd*#5~v@22xCA6jGo|*MybhAy;Bs`aj(LdZn8EIjoQdatjWr z=`yLsq?Q!4E*-*R%afPn+n_L(3@%DmwD70%=Lz%r_meCVj=QsT?>X{s)GEi4QFPh9 zXrIGy)88VuZ+|V>EumBVAZTz^ae1*r^heAsc&Al{D@_Z)jtS^+)7gFS>0eSz!rqKZsS{AxKg50LJG=v zjP~uZe=#rkq{f~#-KKWqws&zMVzRJvs?t?7Ma`Z*S>-}RczW%Msr_rE)TUgN5m9gjv78(^(uk!#O`Y+khaB0;5sW?LfB zw~IsXt(ig!eg5-%=ByXcb+JJgu}FGXrZ+bS1J(O;&O$CMr)9+ zbB5k`kO)GKZuh;4(HeEV(i=V4b}yOyD1cm3N>}zL^Gf8?zpXxpK*yuL+fBfj$gZ&# zO>bX2_sXJ~=KBMRADg_Q+#iYbpVAx03POcXU}c&xh57CrCPsGKzQu+#*iMY#L>1zt z(QI$gf42P}?!W1;o^bSQ%Q|-V;aj4SxzG!?cAmbYYTyni$XK~qwKJLyG!%gFF9zr zZT@jod_N0(cnki(=c79k?tUCC+C2m8pP{4}3h3DJ@&`B38Gri-c@P5YZh^Hl(WjxH zBln*R{EY>5dko~jP-_bV@i6s1WAUt3$$pc`a!9skh>c!b_r%1q51b~ zx5)me2v!Y&qc>YVS4&vCYW96pk@_6if3b1Im?r#u)(zEv@}jbi3Rau}sO*E>b0C(B zbdfFKTUcn{4E^dF@Cpiadj^_?B_iNp0u8+f%<~D&gTno}n`*SK{fm867{>%OF^Bfn z2WIk!FNy{X_@fm67V57FCT_rpG_*nG$$wAgQM@Z2%pdbiyrUt}%4I9wq@Z{8p=Cue z;%qFGe&Z!9ER9MRm$PaZ1bNI9-~Ax%38S%B!>ZLQx&$h2SZ%`CW@51*1l50cs=uFa zwtv~^el6E~A|PSKALue7f|D2hQMDe=vaxz~<7*wTjz!}OherLdp2eqS=3mQjbB6px z5dA-p%6WM`mip2%(bY$@SgRc1Dh}<|^eD#BnbOdzB#zvhpJQ&;43Yf6IMCV~12!8h z&F;`2$N_83Kq7!z9Vlov&g-#ek1$kAwruC=@)dbr_ZiOA2Da3}Y&T@>O30?0Da+H! zTka!Lsamhx8})7Dzrh5KVHVq!caK-K*w+Tbhn`^>sJwgf>x^y6OGKw7@P_Rhk#%0J z^(4Rboj~OI+YMEHM4biD-*RBpDB-7vSYWdrHc-RobV&#G9?{BB972~90=ox?_=kXf za9~f=5AP7V$5^O8iY^ZhQ_-95PJ||)=!&RBz(+dVAT&6z-#rBC5v!bJ1s#Y4OPotv z(6loY;=t_lgI66H&K@z8!ohK<2fgZtI>BrXW7c@Ytf$v*8*$~Kr(4o-;FwhEoG{}} zUB*cfohuB=Q8l*im*XaVv|qB>>A%ytoMLyI^ld~UYFFJgklxgOTuK9$WikbQBxLf)vB&AnOv7w_Dm^OZm4hvchAh8@7*?gFFU zL)!!e;}`{&iNm$QbK_4r3{5B#@7pFHiARHI&TpJvqK-=y=quOvEJ zr_DLQqsj6bEE9NBXhJ7J@?z|Bdg-lTQy_&y~Jt!4F4;JI>GkW2y#T7P7;_ zCG5ag@+x(%EV@%_Bs zw5SwJ?TX)FSC5MdS?Ns{Jdcl!8u59b?%+5lJ}0`{)DgU~{3-UgT@QZTD{|-kt;5gO zO|GQLcnRFg>Y{=p275`HJ@cB4&++dY>~bo0i`=qB$($C&eqw0?+!8xl5dL?D&_}}6 zIbyH}lS~az!9x^UxOzfcvTLETL4nQE>ZUHcWB3nUPLG0{dffgUKgMSbC4O-7-kJTO z4^L(b#Rz2R)arZYnYZZ+e=r2=BdrB<^qn!HBjOi7|5aP6ciOK(dZ^S-TX3SIUR(NY zM?ItiH#^(ZS6L8ddVQzkz*P0%U6`5rM)kUxCT&eEHyWgHh}B`WJ`Xq7V?}h7Xz~?W zT{V2Pb?~OLR>zy&VkRVAi|##}HCpDOtlTtgO6Q>7T0i1_sz5W6sMAFqJ+yHOdmmxz z68r0yE%GsQq@DW%w{<)BeCtSi@B0x)_V|3Y$d-ZH&aTI!iDO~KfZQ#wyXa<|093Fq z=9klD$M;xg%7kjf^sU)73wn9v?AlA#&iA)m6D|q~u8FiZf!qX`qMbl8t4)*}`D)~` zTPo{a40n3nlnwRjM=>y`#XfR{UKGt9?U`q!c;Z=LX%p>r*D>-^NO(cd3mTy5CViEp{c z?*z0>DgFuQ`0zEd*ga=fx5$3^ZfwxgovuGYeFt-~!2^Ge{sa%wa;ScH36bgJa7o{K zIu|z^d3JL`uqZBcQvCHSW?Jq;T-d7{zt6&Ew3UAPPZ=rw4S#288~@8$_txKtd5@y_ z$aw>wzmZE}AL4H<#};KqQj<9nqP{A0f4a4rZ<`RkexI%|i z_spyR#qN*%_xW^hO6ffA@a=P#xTD2eu8}|A7u`-c-RXX|`}^R-?ep2I&(3etfU$|R z@2;FPL}}7q*5(3w;%Iq}H6e4OQnShopb9s}0GtQvp%#u#4SBBKjWZ$&3b{c|qR?!$ zSdzG>8_f9_%{h)G%T>EE21aRLTgRr{Satg^{#Y9hHc!M9GlJj>tH_xp?67JN zo^Mh5s_W)=Vpl!*{vPXVhRO&|9h0OJzOehso9Ez1-358LdO@M)dHL0z2+3#zV{eQ6 z`>UQJY9|I*vPD6ake8@Qw4qh4MPY-dm$>tZq20K}-S%oP$-rnMr*(^>-s&~QzMEOz zc0O#SGeBWfMVd+Y@Rd$Zvb^yL2Cvs1Hj)aKZi+SuIQO=^zqsnH)PG_ULbfbj5%N)a z8Exu^N@gz`)IkU_L!-v!Sn_*_!g{%|z;KU(H>*AxDLOC1z*gn72)q_!43;QtRRQzD zYd`U2bc*`QlAkkrV@>pRx+#G5F@`LlwPlt=K19+);0;V-EDCC^YQ(*Kjhs&{>>Nhe zS(_-j(&@A5HtwvGR6XQvp-j(fx(3<^Kl7{@t7>8EM@C+LmJd#?D0iNS1dj zxoowLYks!qZZ+EyeO5>4r=4ELX-hO%ONJ9FKqIYiy562DvHsty@2iUGsPS zdulrfwrNR51h_HA+Kqy{I5Ng`QHirK9^bXf$$M<#ehHh!Mt^-*7 zNFt8G_2v#(kLK%@x)Sa`=;z06y4q_30|R3nm)32%d)ESkG&3Cl%f(BFU=V%U>;hX={)~=P;}?U@n>*}nG~;sI zzg&*TZ3hl&g5$r%e*R-&_-vsZEQPsbD$v+;prR zfH`;`ARx94(K-BbIc9!@UoX}?!*G~&@?7(~Ie|ITrO};>(X>VTk5R^|cf=Fx_0XdJ zGcQ!IB6r(GPEWHUbMTnJdU1%5u8&BUeMorWw5un1eKv#5tolmPppHc2j|@}Jx5cIN zvKi+L^bU5~SNkgl?kxgr%6CV3DVn4`Uc%*wC|XVmoP3?Zy$O@xgK()}eBx&jxGYP`9LdB6 zrV0%@Bt&*L#RmrX^p>|V=?HLZL5y`gxbm2E1c5SX?smznL_4AY@C(!c;K35hq+51F z3!=>lU}~MvMX=|7S7#z{6nJKyORPIXy+_e3w<$WwftR3KNB|R~H2BH0ZxBb!8}2qZ zke}NPqA$eFN(dQ!rY587KMG)pA0*0aUV>^cT`zo(Cn40C)6JC8|3%Cs^D>#!p@4tq zJfpOE&Sp}h(8J7?&i=xAxx2<~2(Esnv4+mTSAnTOPQXCShyH&NEh#*OIrG28;cgIz ziQ&sdnhksoxiD?{$}87`N7>tlpqd>?tfEIGK}bcqITL{889q=#=LensS!LdUT@R0t zIfK?&#ecaWG8}S0=uAf|Sz;$Z$g>cr1Lf;MMk`rXj30tsa$mhS4;t||r`N^+UdlR; z^6k;sY5?8;E1DM&D?D#aD=~Cuj=|`PMM{BX&m0)sX-0nTBtkIq0A}rGZJxcHRDP3YI~GT|W_lf~aV$rT2?)5+ znQ3N%W65?j#1KcYaY%B~jZDiH+VB1?BNHS$2+#lrwvPg#bBLF3WVUO|xfatJ_Yl=M zmA&yqO--^L7UaDNv{q&?V*;wv5^Z}(PAw#_`5yBYu!AN@vxn%_0&HH28lrPdD^H0vgl;04<|@tpLk$FQkYN< z#(^<#pwb^y7PS&T0iq|udfsXpOp^3Mh>{LqjbfrPHQCk04e20;q z2`k#loorH$%y{#}Z&#(p$+RsB70*H4U zY0*^T2k{B>9FRk*T5v8_FHAd-y$D*Yb40 z88T8N)5$#3&WcyN5|(mmZ<{;FI>pK77z)vF0_dtaqf6CCQ$V<5T6MgoqGeshCQ)q= z=o-?J)k1df0n!%`Ums+|P7x*7-dtWGF`TtHVTlHIGH=+3D&M-?sml3hU3SMLqeP%X zltzqfx9J2ZZiHw6AsS-I>Q>~SR1oJGDtkq3V^zqdm>9LKtv+MU^J(^L2JIJB;)6vM z)_H&{1-!2F$}ytFixYIKQpA4ZjS)A=HWloOC0UkZ9WY8+;$UnPC?HqJD+d&&_5oH6 z8hfj3CEgKWN4Bt=Dy;@NM3Eed``x2R7V{r0lu5Z12^k(ySzSwlq+PO!hQH>l-u{%q z1PLpyf}tVdHOX-YQ|Ed;!1)5QODfQQ|II-g*{KH@+zTAbAe&Nvjx9GtxIY}6k_@T9 zXE?B;V{6(zP#;6YFn#dazo@bU^*Kp{c5+^+Q#x>sm=g{#{GTo7dPa3YS#uK2(7_w&$=mQH$DMHdAr1Q!u z&|~JaDFxI!FW|RH(#ruln*cLPh-)CSX%wg|3XJOknsdrHmfOa7)xCSrn5avZ&2(l} zcVqh}i~8Hft>A>AsPBC;SKD`~YorxPO?ICr|9DXSv$Ms5LhM-}gBr-TJw&@6q5LMK zNx6s#MdZX8Y>sSC+s2BH1G_hgrcr4)3ehGi@4p7Ja}Lo7L$(rsaz27+e?|}Z~0MCslh#mDJ>b*mTy+j|* z_c4<|Q>=o+43V%-?Dlh440CO5^Y&ZzE=uq%4x%vK_bqAmy+7bv`pWmgvTxa*@59N#Qonc^RNW4tYq?G|<*8;raNHz=ETOMwgv-X!}VK_)#Q&tg|A zf4LoJU$wc(;-G-%3A+K=vrEJ}9qe|slVPiZdd;fJ!(^aIv7A?!Bdi_Vh_EzX2d
oPS-M)RMI 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

edtDnO%9Ih-&%xLEm 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 Vc~w=F?ZT*aRVQ9nr$4i-J_#3p&n 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 z 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

u9Ff73z{ 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%0A+_1 zNaWYof;`N6u4g8taE<413m#^5AZr;FMIcZgki3^51V#CFWNovcToyFURl^$L-We** zv0#4^RF;~m0!em?l^Eh>@stT>qqv3e{#8f4680@bONX=KxpCphJ+!Mtl`BU+5J9-k zE|fBw#o1a7$V+W99QCrB(;1|1Ku;N~LsB?4@4dY|di7A73z`EzN|%X z(r{IV63^ov%V%S#rusfI^dGdSzCKgsI=SM0QZ}HGUj8!}Jk1o9^$tASdb5_F$#Gmi z0x;|$O2%bD#*?k_fcA4`3-0IE9(*;`|!6? z37Edn5I9d%+lH2%>>G^I)<_GD(NUM~H$JpzS~d};_w?8b3~Jk?A)pzl?xqD+i4>pe zlrw^>PER(3Uu;RsP-V~wl#6}!p?r+Y?qd}H-7Nlg&;^hQmc@eAkyQLR_+E(Q%IW{? z0>w*dj9efQ9;neQsg=)Cz%x|3i*&#kc?eoH$R!6YB2x{{VVi{(kuR7KX_8!1#m{6v z@yR`JT}GWP7k!n>^wrkRmRJ3BwPJL}9GA(i5yv?%7JjfHAU;swa<=;MZV^#N*JMNK z(L24hl)k88`k5eHGJZHyaEO5v2~%0{Q1NAEJoP{{%$mIj-RfZCCY{;f6l@iY z)UspLgv@s6;&(1vZc3Sf3oh)|nG7Dg*jF=)hvInxj@7@-lKlw1^QW~sWp)<^t?^3K zDIt!eknt6GC$kOFCIITW<()q7CZgP&^+Nd436KU8%|xQ$8j&(*&;YKZ_3n7WAZ&7L zbkd@&$=XVMCy+xD=(~kxrlIv-9F_{Qf_ji>qu{a+7E+d(LzlY^KB%0##sbrIwcyGJo`qRSF2#0$wv6v;>(M zlPc+uqFPjoO6AO#sPD&v@WGOOvdiY9qt0<>1b=Z*tSQB zIUWM;lIGhJ#oG=n!xBq$7X0MeF@G-VC9qoRBa@RR~W z#3!z11R2R3Pk3Vet0M7Lyn6OTZ8_peh)Z}Kgkj;HtJ7aI8Pv{YS;)nl!k9uAI5^{;$pPkDY@bpQ$GdvvP8OjH({WPZjNFzUZg$g+;5E!Kvl0cV}wvceB9h?#M+m z%`=l2+azN-_)LsM-RH3JM#BR|jUysEtyfBYzJA08q(PtM7xduNu@m(G9AE$w49~2%caXLq-UzseqqaCKxbrrFw42fxIz(TnRv2^ z1bTKz8wcc1b|z<+`kCdhpi9+5I&wL8xnV{-S!GBu(M9V7B*$*cuQMtCSJPxbn{yF~4tb z{wz7^L!Y)W1$D>O;*t-CBW%eeq9fi#Y~^Fm>xH1O>J5OhHJ8ef=wy2Je?- z{oU$`9@T$lvq~!r=IyZ%;U-g2w^aZYhkX5!%E9rIsMb*@p_u2v52Now6CuA@Qu>?e zIgdP6v51xHuYlPv6C=Em{|Z*XFo+n%aGr~iBC(6}vQr`)S`<3O30HSrbEwub*;$SB6r7}`y`CG5 zKP$E$#8?-umrT7WXls-+dz)-hdpJ~tglhFR#s1ld5~et{e{KjG4(<_XG@(dBzfmb4kA6=B z;@<@AGd6myz1WeT`+7#(HIBf+95j3{@^8-+uBtz) zpRe$rf8B|OK4j{3eOSV^?5SFjlk@^*{Im7SSAFWtZ2VmVGMPPwn0 z>mUEw{5qKE5HK6K8J&oQqO5)gdv32>t16|!DFxJDj~=dbBS#(2=K2$3VyO@a{Ged2 zCJ!adph&N!Ogou|V`}*)(fwT-Qc2MTYvvvsOQXkYR4}7U&y67rYi03MX7Qo12n>i{ zkHDp#sg(?>teGy?OIybG$@$Eb$(3`Yxm9RU(A%~yH*52!E{JQXg*BV^4=p@L1ubs$ zV%}vITWlSmzWFYLeMQ#^9s=ew6Sh+@$mW2*cnhTi_^^{M~;wTw7 zWId*cm}{@AP}$e~W4TY0>n7v*%Kd-woJs+GsrNLidw!km&(=G?fAjkHkMOOr=kC>S z9;tr&dib{gO8x4Ju+OT07sBGRfrAR#Zt#7ybzhE!y7U{#&pWu=0S?eLn+XW@;mJf~ zf^b>VJf5vg?X<^zD@D9xe=AjbNO&7BKku`hhT5yyPFMNA74m09m0|{Ln!>bnsBvO) zrsbqehMM`DnI@3J!PwTT)-zZ3Z!Z|?lJJX^KWgl}-1@2x^<6+sZ9 zzgJ;YM1{{H)Q}Ezx=M=G4u+NnXt9PLe$nQ@?M&;o$-@G>L}i*8^hJyZu20hEy`OT` zcI7Qi(O*O?8BImBrA$=5J)O%1U0?hu5gwJyHs0|H*0c&mySMEX!=~o%=ne=BN^qdu zSjD2^R~KCP^Ntq8RiM^lafSJ3&S7*p zjTiZu-l)a}#e|M5iD-lgT^Z>XhihP5cO$wUD{)7CVs~_U__cUJr)*ncpbm?%NNgfP2e)nD~%svlswwP&-$Phh5#of3Lk2qTdJpM zcO$uZqE8y|NJ~?BcixI=;`4`Quu4Y5%j3(#=6906k5HG;3PKeFJ2q2Kv_MXAWP=g$ z#2ke=`F8$qN;7|a`0QwY>N%b<$h|GD$E?yy`R(9$3L zGz)f6nIC8-Qu)Nq0Q9%K%BQzF zPjL?sF@RiXt^#qe!uP76oGag@Pa8ob+$%VV6P8)%^1(=k;yn0|WGP>*UPy8S$e@sV z)@Nm9{l(BD5#<3k#!GE2;S*}g8+!SDYR$|Dy#_Z}YW{$*m&{8KBX{(0FGo5K9}tI^ z5~SXCw#`hn@ptE_@SSN>JeM7{C6_BKEocqQ9imef;aUySdQS!CT!J<-YNftk%9_vl ztn~>sslO9i=hc4TpnG@ebNa#?mXh=F0QCSddI2Tn5~Ju4|2!IjM;l(xde$0mMb~Gn zTPG7gGzc=S!T{SeHl@@e>A{^lQ}~b5cxRt7PQXuXr9xa+xRpEm(7q1eV{;mJ*fuMm zIYCkpGvQ}#dBrYLR;>7PWFj#7?5XMtM?!*C1)qSgek$KPg&tQ5IivWB=(6H_sIe49F*H(HH!cT2$zZY`RpQE#` z6hj*cx4QO;{?&SN>oVEkHW2R87`R!7*g#wB2K9lM7sX7mU@oXxBBVfzos|r~4j)XK*U>l%JErK1Ng? z5@m2$hgQR4Z?bo7KJA`pi?|tH!n$WGNESdAXynfMTzFfI{Ap?*$M@zW?FKQ2Yuh!7 zB@KU-xxuj{%&s&lo?cW??sKrn6HbWb80j70cKjuGi;7k;O3t1$x0p2DOS1o{PDLqIIXP9VYN?cG#Zp({fC#mELXbI$i!OKB4wOg+0LCFColdY=KD}TD zKeIsqM5{yQSWG#@Aq`hZLJo0=!xEOjggyKD&wviJpa)IpJ;RF!h)%Sk7u^6KI2zH7 zR)VAbENMsgpa(b1*jSMgg)46QC>Actg-0Fgw5C-`x7~sbu)-FtTp0|o216EXK!UB< zq6o773K7zBzabbwV?~jI2#k;nyEyjP$WFGhmwjw*977uE5Ry4|+Mzba433+r8E7`U zI0>qQ8prrVCLBTTa#JD`m`Jy}GXWdc)CU)BK*JfNWY)758DkiW3sG>pogTMg4FLBt zE|~w@T<9A74s|0C!56NdOA6e}$-QJM5q|LhJOV%#C#}Xe&hd_WT;t`|<8>Rc3t~`% zF|(XdEfW{smYXI%I$&4;IQr{2v4lXG|G^j)_TGY)1DV;7uYcOS=2iI9ifm3Q#6!2#amHS{L1W@Ldg< z&pg00&-pdvf$w<;z34|z`qGP@KcSbe>ha)+#=HLY>Gp)|J+TXJfFm7bmNvCH)KmWp z0<~Fe9x8|8yqIabLmJu;g(W7zeBCtv`OuHP^rugK=0`$51Gj^h%VP)r;Gzz5h{Pkb zU!O>r-~5j_f8Nr5hwH+|>>`K1{L3!?`M)3jWH)~ zmj`!14ANj1$MAp<7=aQvffI-f(*SM3at_mG4(h;Q8e(m4G&7lHGu$RD({@MFa16AWeb@+_unf(BbL&M0m^TJCrwhwqbU>E}euxL~ z6=0S_b(VOEn3#z!Vs*JwPQ8OpjDtG`kPg=n4bX5psF;eWC>fHWiqG&2%s?!!_=?d0 zi?YZJ0FaBZxQo2Vi?fJ}xd;HgxQnw`jK3(0s)&k|a}CU>jG|*YstAqIKn>JjETeOc z)j*BYIE@u@4c1UP#zGB}F^kY(jOG}N#VCvBXp8FDi?7Iv@OX-*_>Qdj4A1zD_?VC1 z$c)U0kJXS4oY8W<_fr@YRKe$F=`>m5Kn|oxjSl&c5E+pYIgu0zk?;SQj>I^Q<~WYT zLJh@OjvtvM$|#MgSURU8kMfv|Drt(WxRNfJilZYs==h2=NsFRmis%Rp%wRgK7#igm zi!BL_k}(?OAP(p-4(O1SO1YFw*_7k(lt?)a3)ylYcv;z|SvZ4B;t(ta5CGx8jMxyC zm5~l)84lr)4qs`O{OFC}NRls!ilz9BqO+ECS(p1*m&YQOdbyWk36^1rmw<_ve<_%K zxt4s%mw|aKgn5|Q;FpZ~n0jfJ;h>j}iI{o0m)9TwMTfR@hoFf7G4c$*P;+E} zh?s|ndC(2jun9akU-ZR;zXSkAM@=)A2i^bx`t=M?r-{D#o524Gb)48>o(N%~d7Q|Z zoXWYJ%-NjI`5*(SW(N5z2>G1Wd7apqo!Ysblm#;(h*{c3N8CA{#3r+pZR>}861Y7Dv%UwFU^%Rr=+NT|BH ztHEigiGiqE$Em_PtjOu8(|I#kO03Gdtc+u+S2?1Y%B<2lt)=0q5Go_>5D(qJhx^3{ z(6n>yfFjV)3;<9J#qbNJil>aw4c@?qH>V3X2MxK(tMpp0Q|GHWRh+>}t^C@r#%iU= z+OGmjt|24Daed#gA~4D8ShGsk(T zsRjXNuO@r435Ku0`k)HivYPq~&|nQ=NdVHR83Z5z;eaHO>ask$s4pY40$`N}E1o@D zw2B%I_E<{vst$Hr zig|0gu)Cpt3%G)Nsn8m`xErB}TS1GP8IAwzu%L6Xkjo7Gf4r0b;ta28F#p)2a z9=yPIT*L%$iWb7bw%eH2Fus4>!$?~}$)d#Q*~X9@$)Wqk9>}ES)R^`t4v9>~<4`QA zJ2Ml!J2-1Nn>@wpfDJHn89;2Xb8HPKT*?i6$}n6R=peyrEXlZ>%TG(mS4RMoi4CsUE&l#)^;UGHi zytK@0p5eUE46U*>W4A914P5^$(Ai4>pB-QW?*zn2}ZMxe03^Hp$nK4<} z=F6aAkHQ?%H##K?l1JG=AC*KmsA0Mb7F zwgeEyX~2ecI#9IB{g$Myto+HNmtE-!1#w zF8AK{ecThQ&J>Cx{re%stGSqy*%*tw)&1D(&<>ED-OO9y*1g@1jj_uM;T6l^0uJF7 zZr;!9znLB2>&@P`v)?3M;wFCLuqz|MySyNoBGJ$c@+zv(@N~`44AmeHFFxZro+9Hq z@dTKIW4Ob1DB)4dkk-%diYHzN*cTIcq-SYhJah+O?tq09g(Igebntfam0j=V(rd z)==lBGw08ci!fg2r<02$t>-rW=i|EOFiWm<-sj_r=y*$r!`tO5{N-f+>7X9!qVCl& zq9X6$=vrQm)nEWD`LIyV>M@(@(U7W8&JLA@<4~SCKpx~a4(nFylQ(XPQErM^{^Xrg zj#(bmsUGXK*f~Bat~0KxFg`h)&FRqV>7$}F9-2?9<)tGJ7Q4I*j@_VR0GdnQ z{JZX;tvXnnv72rp@1VKxuD_{@BIo|@7ONuf-q^j_x$h3%_WmIm8x4eL?J11y-M;V) z-|!9}BR~JrwFkfs2QaVZ-njuBsMsyu0PiBj3nCc5BJ}?8AwMGN9rEd}Iv;KTt17t& zAH53i@G_sUs8J5)Fb_DN^E$utJkRs#P!5$$si3UKL|^nqKfyC^xZ_2Lz1ivQFz8MH z^4H$pNpHu^UAX5k09wEGT;KIx&-FB`I8I^?!SD@cfA(mf_G+K@!C(MD?-*JC^>RP= zVUL~!jU+57wtBz!e9!lRUG;9e^w2QzOsu(R4%t!s&-#Yo3nQK(SS2PNZ1TBE$gDtOa=1rVAVJ=)4ZJE4+JcSM=TGZ%KqzVmGD^@M(Q>am;PNiDa>eZp|;x#*W zPUly!Va1LmTh{DZo$9bf!=?`H+pr%4pj5fC9lHa0r#6g8PTf3Tg$NBMT)5C-#EJhA z4>lC6Yea&2r-rKwccQv*&njDfi0zpG&Y?w*CS6+eQE0{Fxx?DpbyRuH-o2(>+xBhT zShaHXYTEa2;K7AkR%fm4aO4jKJj2#<0A9&aDfcY^Sa4zNinSkREKHnNa}NjP4JviI zX|>SAmChV4`Fr^B<;O&AEMEEC`LW7_#_pW~{Q?wlKmwuK?Xme1RB%BCb2_QH21k3) zIq2|v>O6?d3C0lYI`r^ELxe#GJn_h*3PaM;TaUE%B6RUZ7{@wFo@m;ctw0=)s>T?3 z-nlVHAcGY0wF7&L@kk_-TuB_aRzgrbm(1LB+nkj}8Wzr@~|~8Pt*Xo#q(<@Neh<<@t$tC8JlW!(PSBDhS4P6%G5E2 zm)SWfhD!v*!A6-p7~x(MJcjA0Pkb>o`y!T>X8&r)PNy4XEJGQ* z-~<6TQ9xesq8s2F&fNlLlx8#|9(^NNs|J@HL-=At2`d~#8YYx#Gy@ui8p`9Y6b)r; z0)36xL?al{2u*kc8qDyTF^VxcMkr$##HfaziWL*Jbi;i8dma7Ah5%*^qZ$~izrfuuH1P|0m#i?2bc$5C?grkD2D5j<3LpnA9H&+1;7{UJl%wR5P%ML!|gFG=_J^1MZaZ3?H6uCgRWq8+k#xBF@Y$rs9MP z^MHp&G-4ObFc}%$nIzOK1B+n1%8shhMSA8li(-6dKVhVfXDp+f!!U+28qp17gkxQ9 z!G;>T;gDf40st5hz%q>Sj9=s-51TMXHLfvCViA%ejLd^KtkI2wZch`?KtM!axI-2a z5drNg1|vvE0Ah@C6L`?YD3<|JuncP)0cdCVPKCFM2uvaGXa+Hi5e;_W^B}OiC1DET z3k~{$gS2XsE~Sc)cSNHY#28`Y90ydgF)5;Wz(fBYez6R=?t&NyFox`O!Hj4aLzB-? zn?!yH6LBP?8wG?$QC9-Hhd_f7y0~FChOvuH@Xnu;J<^Pb;Yxj8P7AalhIBvm=af*t{izxLVvuUcw zqWV>KP&Hw`5QbLCq_D2u3cdybraX!P#o?V5CLTHCMe*7XuhZWIz8!S`^DCo}LBmRASR37PnZ&jS_8&Z5*6% zFk>0JXaq5qk&J_~>{9CBg(i|Q3{I#4Y!Gp<0s$~Ui}H~ero@y;kGLF7FqaYCuop8p zF^p>sLwam9A3~o|GQ~nIe42pmGIa3_Yho42LTW*OaB z#(hdy!N18Wt6Qy=SF>Ue0CWQY@HOE@$RWf{@WQRX;Dv&TF#uvPLK6U31~GoY(_t(_ z09VuIOagF?*=;x`h(*9N-e`^Lo&(9tIPr~r4V)Q8xdX2hTqj&s1>H`;2C}`BR85T;2eDAyHs+K92 z`Xx$iSl1t(RN5k!ZbUNipo{@}8UcLd=;Ruq1{NyA8rigPOkN#p!(N2ewyvB4TS~`X zJ6q_SIf^v^AdeU;yD0W#N79x4be?VHNkgYPT(F_z68m_tsn(2@x={>kTqA}1hKM*K z-OLs;^R3~?x!J5dk!WN&!oz3>5*KU@Wgzf{2~=QVtFeh;K&ao!5JyB^Ch)d{<&eby zKwW!ykv}Jum;Q;kF(yvIzF7Z|gTc^h#_{XH{rcw_tg8mnLylIz3Xv1K*t9OdO^kC` zBzy(qS~2C}7p z{iyS%`P(mxItXA476O2CMQ)u$3@TV7JA~ijz&kIR;SF9qLl?Xd{~*y#k!t+08YLe< z0E|Jof}0(0SflxKJ@+66PGAOnn<+LUfN5fxP3VU2LJQ0Q0H?Z!-fOs2QVMo(hXa5! zXOM?>_$rG7oW|e~&)c|NT8L_Zt7vdKgAlnwQU{dsngdfJ1sjG<0XN!fhh|`eDWfp5 zX*HSfJ+BEqmp~am!3O_gxEv7^F)-skDRjP~u!d&9zN1ha)5Ah8#0_SfLNJ60Y+$5a zSO$*>000=khl&UkIv{0$1}bBQB4fM+Vuxk89oo8*x08st>xOplBafLTW*~rqb3d;^ zq;9wdaR?o6XbX=hyt+E5Zs??mO1!Ti00O9nRcpJ*qX_jv2oF2}Xn2Nr7(rpmyhQU5 zL->Y_!zHc)y`kWbYOuc3dkONI9E-vO<}!xK`7}`Dg?a0SQ&XT}V5cFhIhY^-G{l%a z8NP_X1_f(`uUQ5^%CbHSLu}lMF2o80_>W-PMsReEFU&@AybKgtAdeWT;_!xYvyyNS zfM{TjnZgK8T895-Xars4lxIi=3}=(KT? zA7)SnJm3Xr=q7<%7vd14mgvNZ_{4+oK!eZ=5=69Ex-Y?DwESAZ#3>3HOgDA7t?g-q zWx%L8imr2zoo*nYBFi*gfTLsNy*`l=)T;<36o+S6y<&g{dw~Z4;IwWlN2zRyqX4FP z3de(39IV{RsTjwplu9Wnq_sIBjv%jeLx8Yzh%-#QQ29r{s)iK-C;*6}jsZLx${|by zzfw~Mc48=s)FeVgo1N+gp%ja<>_`nvFNZ#QIDO@e`u@O4h6hu6#|{ED*13P1+odwzRnwVLwdTpP8E} z(t!p5P|J0R$7>h|V5ulYP*bsD^6Ly^{D-KrM-NS&qkJQ|?4lL~T(uHB?5$ zFtxEnq)`iJgggNV2TQ$FOwCkH#njczka+-xLoftU9aT~-RZ|_+L+FKOD2KYSluF%H zSdCRLrL$*v251_ZVP#VSxK(UuR%d-yXpL5B zomOhCR%^XhY|YkYg$5+Vlt2a4&(sqL6%%0y26H`EbWK-vMb~C92Uhi~Z~fD74UYBA zyas!{SA5M^ece}n-B)Zd&Sa%cV;xx7MAm>U({QMU8G;#xZCHnWScr{SiJe%Atyqh_ z*k^zr(;x>nDTi^`6p#&BksVo)b=P-=mVG%`mF<&tAy}8iN`qZlCyiH{ty!DBS)4VA z`1B2RAP1lI*#hWTp&eSH?O1W>*eoGWnO)j%0nV3w+AfS)rkz^WGzY7FlMC|*j%AYq zfK;me+9V+W_=8%qy*{Z0TeJnktHs)zu-deJTS*asVIYG%C|kQ#y0e8_y&XEF&55-w pfco59!F`Wq$b$_~gTYN)#a&#+ZCuBFT*!@F$(>xvRos9806TBlV)+07 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 87443f197..c9f469966 100644 --- a/README.md +++ b/README.md @@ -220,12 +220,12 @@ ### 商城系统 +演示地址: + ![功能图](/.image/common/mall-feature.png) ![功能图](/.image/common/mall-preview.png) -演示地址: - ### 会员中心 | | 功能 | 描述 | @@ -238,15 +238,23 @@ ### ERP 系统 -![功能图](/.image/common/erp-feature.png) - 演示地址: -### ERP 系统 +![功能图](/.image/common/erp-feature.png) + +### CRM 系统 + +演示地址: ![功能图](/.image/common/crm-feature.png) -演示地址: +### AI 大模型 + +演示地址: + +![功能图](/.image/common/ai-feature.png) + +![功能图](/.image/common/ai-preview.gif) ## 🐨 技术栈 From c6937cf199454312674b3e5fbde06839d90426c1 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 13 Jul 2024 11:26:17 +0800 Subject: [PATCH 007/107] =?UTF-8?q?=E3=80=90=E6=A8=A1=E5=9D=97=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E3=80=91AI=EF=BC=9A=E6=94=AF=E6=8C=81=E9=80=9A?= =?UTF-8?q?=E4=B9=89=E5=8D=83=E9=97=AE=E3=80=81=E6=96=87=E5=BF=83=E4=B8=80?= =?UTF-8?q?=E8=A8=80=E3=80=81=E8=AE=AF=E9=A3=9E=E6=98=9F=E7=81=AB=E3=80=81?= =?UTF-8?q?=E6=99=BA=E8=B0=B1=E3=80=81DeepSeek=20=E7=AD=89=E5=9B=BD?= =?UTF-8?q?=E5=86=85=E5=A4=96=E5=A4=A7=E6=A8=A1=E5=9E=8B=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 18 + .../banner/core/BannerApplicationRunner.java | 4 +- .../src/main/resources/application.yaml | 10 + yudao-module-ai/pom.xml | 27 + yudao-module-ai/yudao-module-ai-api/pom.xml | 32 ++ .../yudao/module/ai/api/package-info.java | 4 + .../yudao/module/ai/enums/AiChatRoleEnum.java | 64 +++ .../module/ai/enums/DictTypeConstants.java | 16 + .../module/ai/enums/ErrorCodeConstants.java | 53 ++ .../ai/enums/image/AiImageStatusEnum.java | 37 ++ .../enums/music/AiMusicGenerateModeEnum.java | 37 ++ .../ai/enums/music/AiMusicStatusEnum.java | 39 ++ .../ai/enums/write/AiWriteTypeEnum.java | 42 ++ yudao-module-ai/yudao-module-ai-biz/pom.xml | 95 ++++ .../yudao/module/ai/AiServerApplication.java | 30 ++ .../chat/AiChatConversationController.java | 114 +++++ .../admin/chat/AiChatMessageController.http | 29 ++ .../admin/chat/AiChatMessageController.java | 120 +++++ .../AiChatConversationCreateMyReqVO.java | 13 + .../AiChatConversationPageReqVO.java | 26 + .../AiChatConversationRespVO.java | 71 +++ .../AiChatConversationUpdateMyReqVO.java | 36 ++ .../vo/message/AiChatMessagePageReqVO.java | 29 ++ .../chat/vo/message/AiChatMessageRespVO.java | 50 ++ .../vo/message/AiChatMessageSendReqVO.java | 25 + .../vo/message/AiChatMessageSendRespVO.java | 36 ++ .../admin/image/AiImageController.http | 42 ++ .../admin/image/AiImageController.java | 134 +++++ .../admin/image/vo/AiImageDrawReqVO.java | 52 ++ .../admin/image/vo/AiImagePageReqVO.java | 34 ++ .../admin/image/vo/AiImageRespVO.java | 60 +++ .../admin/image/vo/AiImageUpdateReqVO.java | 18 + .../midjourney/AiMidjourneyActionReqVO.java | 20 + .../midjourney/AiMidjourneyImagineReqVO.java | 35 ++ .../admin/mindmap/AiMindMapController.java | 35 ++ .../mindmap/vo/AiMindMapGenerateReqVO.java | 15 + .../admin/model/AiApiKeyController.java | 84 +++ .../admin/model/AiChatModelController.java | 84 +++ .../admin/model/AiChatRoleController.java | 124 +++++ .../model/vo/apikey/AiApiKeyPageReqVO.java | 25 + .../admin/model/vo/apikey/AiApiKeyRespVO.java | 28 + .../model/vo/apikey/AiApiKeySaveReqVO.java | 34 ++ .../vo/chatModel/AiChatModelPageReqVO.java | 20 + .../model/vo/chatModel/AiChatModelRespVO.java | 45 ++ .../vo/chatModel/AiChatModelSaveReqVO.java | 50 ++ .../vo/chatRole/AiChatRolePageReqVO.java | 20 + .../model/vo/chatRole/AiChatRoleRespVO.java | 57 +++ .../vo/chatRole/AiChatRoleSaveMyReqVO.java | 32 ++ .../vo/chatRole/AiChatRoleSaveReqVO.java | 54 ++ .../admin/music/AiMusicController.http | 26 + .../admin/music/AiMusicController.java | 98 ++++ .../admin/music/vo/AiMusicPageReqVO.java | 42 ++ .../admin/music/vo/AiMusicRespVO.java | 70 +++ .../admin/music/vo/AiMusicUpdateMyReqVO.java | 18 + .../admin/music/vo/AiMusicUpdateReqVO.java | 18 + .../admin/music/vo/AiSunoGenerateReqVO.java | 57 +++ .../admin/write/AiWriteController.java | 59 +++ .../admin/write/vo/AiWriteGenerateReqVO.java | 39 ++ .../admin/write/vo/AiWritePageReqVO.java | 33 ++ .../admin/write/vo/AiWriteRespVO.java | 54 ++ .../ai/controller/app/package-info.java | 4 + .../module/ai/controller/package-info.java | 6 + .../dataobject/chat/AiChatConversationDO.java | 99 ++++ .../dal/dataobject/chat/AiChatMessageDO.java | 90 ++++ .../ai/dal/dataobject/image/AiImageDO.java | 135 +++++ .../dal/dataobject/mindmap/AiMindMapDO.java | 58 +++ .../ai/dal/dataobject/model/AiApiKeyDO.java | 55 ++ .../dal/dataobject/model/AiChatModelDO.java | 82 +++ .../ai/dal/dataobject/model/AiChatRoleDO.java | 82 +++ .../ai/dal/dataobject/music/AiMusicDO.java | 117 +++++ .../ai/dal/dataobject/write/AiWriteDO.java | 95 ++++ .../mysql/chat/AiChatConversationMapper.java | 38 ++ .../dal/mysql/chat/AiChatMessageMapper.java | 59 +++ .../ai/dal/mysql/image/AiImageMapper.java | 46 ++ .../ai/dal/mysql/mindmap/AiMindMapMapper.java | 14 + .../ai/dal/mysql/model/AiApiKeyMapper.java | 35 ++ .../ai/dal/mysql/model/AiChatModelMapper.java | 43 ++ .../ai/dal/mysql/model/AiChatRoleMapper.java | 54 ++ .../ai/dal/mysql/music/AiMusicMapper.java | 44 ++ .../ai/dal/mysql/write/AiWriteMapper.java | 27 + .../module/ai/framework/package-info.java | 6 + .../rpc/config/RpcConfiguration.java | 11 + .../module/ai/framework/rpc/package-info.java | 4 + .../config/SecurityConfiguration.java | 40 ++ .../framework/security/core/package-info.java | 4 + .../ai/job/image/AiMidjourneySyncJob.java | 27 + .../module/ai/job/music/AiSunoSyncJob.java | 28 + .../iocoder/yudao/module/ai/package-info.java | 10 + .../chat/AiChatConversationService.java | 90 ++++ .../chat/AiChatConversationServiceImpl.java | 157 ++++++ .../ai/service/chat/AiChatMessageService.java | 87 ++++ .../chat/AiChatMessageServiceImpl.java | 259 ++++++++++ .../ai/service/image/AiImageService.java | 121 +++++ .../ai/service/image/AiImageServiceImpl.java | 350 +++++++++++++ .../ai/service/mindmap/AiMindMapService.java | 23 + .../service/mindmap/AiMindMapServiceImpl.java | 134 +++++ .../ai/service/model/AiApiKeyService.java | 114 +++++ .../ai/service/model/AiApiKeyServiceImpl.java | 135 +++++ .../ai/service/model/AiChatModelService.java | 92 ++++ .../service/model/AiChatModelServiceImpl.java | 114 +++++ .../ai/service/model/AiChatRoleService.java | 129 +++++ .../service/model/AiChatRoleServiceImpl.java | 146 ++++++ .../ai/service/music/AiMusicService.java | 87 ++++ .../ai/service/music/AiMusicServiceImpl.java | 218 ++++++++ .../ai/service/write/AiWriteService.java | 41 ++ .../ai/service/write/AiWriteServiceImpl.java | 177 +++++++ .../src/main/resources/application-dev.yaml | 118 +++++ .../src/main/resources/application-local.yaml | 125 +++++ .../src/main/resources/application.yaml | 155 ++++++ .../src/main/resources/bootstrap-local.yaml | 23 + .../src/main/resources/bootstrap.yaml | 14 + .../src/main/resources/logback-spring.xml | 76 +++ .../yudao-spring-boot-starter-ai/pom.xml | 71 +++ .../ai/config/YudaoAiAutoConfiguration.java | 76 +++ .../ai/config/YudaoAiProperties.java | 84 +++ .../ai/core/enums/AiPlatformEnum.java | 52 ++ .../ai/core/factory/AiModelFactory.java | 82 +++ .../ai/core/factory/AiModelFactoryImpl.java | 294 +++++++++++ .../model/deepseek/DeepSeekChatModel.java | 165 ++++++ .../model/deepseek/DeepSeekChatOptions.java | 55 ++ .../model/midjourney/api/MidjourneyApi.java | 348 +++++++++++++ .../ai/core/model/suno/api/SunoApi.java | 200 ++++++++ .../core/model/xinghuo/XingHuoChatModel.java | 162 ++++++ .../model/xinghuo/XingHuoChatOptions.java | 55 ++ .../yudao/framework/ai/core/util/AiUtils.java | 61 +++ .../yudao/framework/ai/package-info.java | 10 + .../ai/tongyi/TongYiAutoConfiguration.java | 253 +++++++++ .../ai/tongyi/TongYiConnectionProperties.java | 52 ++ .../ai/tongyi/audio/AudioSpeechModels.java | 40 ++ .../audio/AudioTranscriptionModels.java | 43 ++ .../audio/speech/TongYiAudioSpeechModel.java | 228 +++++++++ .../speech/TongYiAudioSpeechOptions.java | 261 ++++++++++ .../speech/TongYiAudioSpeechProperties.java | 77 +++ .../ai/tongyi/audio/speech/api/Speech.java | 87 ++++ .../audio/speech/api/SpeechMessage.java | 80 +++ .../audio/speech/api/SpeechMetadata.java | 43 ++ .../tongyi/audio/speech/api/SpeechModel.java | 51 ++ .../tongyi/audio/speech/api/SpeechPrompt.java | 89 ++++ .../audio/speech/api/SpeechResponse.java | 100 ++++ .../audio/speech/api/SpeechStreamModel.java | 54 ++ .../TongYiAudioTranscriptionModel.java | 186 +++++++ .../TongYiAudioTranscriptionOptions.java | 203 ++++++++ .../TongYiAudioTranscriptionProperties.java | 72 +++ .../api/AudioTranscriptionPrompt.java | 56 ++ .../api/AudioTranscriptionResponse.java | 67 +++ .../api/AudioTranscriptionResult.java | 68 +++ .../cloud/ai/tongyi/chat/TongYiChatModel.java | 482 ++++++++++++++++++ .../ai/tongyi/chat/TongYiChatOptions.java | 463 +++++++++++++++++ .../ai/tongyi/chat/TongYiChatProperties.java | 83 +++ .../common/constants/TongYiConstants.java | 44 ++ .../common/exception/TongYiException.java | 38 ++ .../exception/TongYiImagesException.java | 39 ++ .../embedding/TongYiEmbeddingOptions.java | 84 +++ .../embedding/TongYiTextEmbeddingModel.java | 175 +++++++ .../TongYiTextEmbeddingProperties.java | 50 ++ .../ai/tongyi/image/TongYiImagesModel.java | 237 +++++++++ .../ai/tongyi/image/TongYiImagesOptions.java | 187 +++++++ .../tongyi/image/TongYiImagesProperties.java | 77 +++ .../TongYiAiChatResponseMetadata.java | 89 ++++ .../ai/tongyi/metadata/TongYiAiUsage.java | 81 +++ .../TongYiImagesResponseMetadata.java | 131 +++++ .../TongYiTextEmbeddingResponseMetadata.java | 53 ++ .../TongYiAudioSpeechResponseMetadata.java | 133 +++++ .../TongYiAudioTranscriptionMetadata.java | 43 ++ ...gYiAudioTranscriptionResponseMetadata.java | 98 ++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../ai/chat/DeepSeekChatModelTests.java | 53 ++ .../ai/chat/LlamaChatModelTests.java | 63 +++ .../ai/chat/OpenAIChatModelTests.java | 64 +++ .../ai/chat/TongYiChatModelTests.java | 75 +++ .../ai/chat/XingHuoChatModelTests.java | 55 ++ .../ai/chat/YiYanChatModelTests.java | 61 +++ .../ai/chat/ZhiPuAiChatModelTests.java | 61 +++ .../ai/image/MidjourneyApiTests.java | 62 +++ .../ai/image/OpenAiImageModelTests.java | 42 ++ .../framework/ai/image/QianFanImageTests.java | 42 ++ .../ai/image/StabilityAiImageModelTests.java | 65 +++ .../ai/image/TongYiImagesModelTest.java | 43 ++ .../ai/image/ZhiPuAiImageModelTests.java | 35 ++ .../framework/ai/music/SunoApiTests.java | 83 +++ .../module/system/api/dict/DictDataApi.java | 17 + 181 files changed, 14204 insertions(+), 1 deletion(-) create mode 100644 yudao-module-ai/pom.xml create mode 100644 yudao-module-ai/yudao-module-ai-api/pom.xml create mode 100644 yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/api/package-info.java create mode 100644 yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java create mode 100644 yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/DictTypeConstants.java create mode 100644 yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java create mode 100644 yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/image/AiImageStatusEnum.java create mode 100644 yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java create mode 100644 yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java create mode 100644 yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteTypeEnum.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/pom.xml create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/AiServerApplication.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationCreateMyReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationPageReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationUpdateMyReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessagePageReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.http create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageRespVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdateReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyActionReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapGenerateReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatModelController.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyPageReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyRespVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeySaveReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelPageReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelRespVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelSaveReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRolePageReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicPageReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicRespVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateMyReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteGenerateReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteRespVO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/app/package-info.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/package-info.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiApiKeyDO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatModelDO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/chat/AiChatConversationMapper.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/chat/AiChatMessageMapper.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/mindmap/AiMindMapMapper.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiApiKeyMapper.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatModelMapper.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatRoleMapper.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/music/AiMusicMapper.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/write/AiWriteMapper.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/package-info.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/rpc/config/RpcConfiguration.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/rpc/package-info.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/image/AiMidjourneySyncJob.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/music/AiSunoSyncJob.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/package-info.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationService.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelService.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleService.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/resources/application-dev.yaml create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/resources/application-local.yaml create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/resources/application.yaml create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/resources/bootstrap-local.yaml create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/resources/bootstrap.yaml create mode 100644 yudao-module-ai/yudao-module-ai-biz/src/main/resources/logback-spring.xml create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatOptions.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/suno/api/SunoApi.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatModel.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/xinghuo/XingHuoChatOptions.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/package-info.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiAutoConfiguration.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/TongYiConnectionProperties.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioSpeechModels.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/AudioTranscriptionModels.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechModel.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechOptions.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/TongYiAudioSpeechProperties.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/Speech.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMessage.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechMetadata.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechModel.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechPrompt.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechResponse.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/speech/api/SpeechStreamModel.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionModel.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionOptions.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/TongYiAudioTranscriptionProperties.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionPrompt.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResponse.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/audio/transcription/api/AudioTranscriptionResult.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatModel.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatOptions.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/chat/TongYiChatProperties.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/constants/TongYiConstants.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiException.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/common/exception/TongYiImagesException.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiEmbeddingOptions.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingModel.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/embedding/TongYiTextEmbeddingProperties.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesModel.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesOptions.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/image/TongYiImagesProperties.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiChatResponseMetadata.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiAiUsage.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiImagesResponseMetadata.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/TongYiTextEmbeddingResponseMetadata.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioSpeechResponseMetadata.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionMetadata.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/com/alibaba/cloud/ai/tongyi/metadata/audio/TongYiAudioTranscriptionResponseMetadata.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/DeepSeekChatModelTests.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/LlamaChatModelTests.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/OpenAIChatModelTests.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/TongYiChatModelTests.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/XingHuoChatModelTests.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/YiYanChatModelTests.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/ZhiPuAiChatModelTests.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/MidjourneyApiTests.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/OpenAiImageModelTests.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/QianFanImageTests.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/StabilityAiImageModelTests.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/TongYiImagesModelTest.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/image/ZhiPuAiImageModelTests.java create mode 100644 yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/music/SunoApiTests.java diff --git a/pom.xml b/pom.xml index cbabdc183..03b9cd58b 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ yudao-module-mall yudao-module-crm yudao-module-erp + yudao-module-ai ${project.artifactId} @@ -142,6 +143,23 @@ aliyun https://maven.aliyun.com/repository/public + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java index 4c4f72585..6c01b9497 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/banner/core/BannerApplicationRunner.java @@ -42,7 +42,9 @@ public class BannerApplicationRunner implements ApplicationRunner { // 微信公众号 System.out.println("[微信公众号 yudao-module-mp 教程][参考 https://cloud.iocoder.cn/mp/build/ 开启]"); // 支付平台 - System.out.println("[支付系统 yudao-module-pay - 教程][参考 https://doc.iocoder.cn/pay/build/ 开启]"); + System.out.println("[支付系统 yudao-module-pay - 教程][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); + // AI 大模型 + System.out.println("[AI 大模型 yudao-module-ai - 教程][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); }); } diff --git a/yudao-gateway/src/main/resources/application.yaml b/yudao-gateway/src/main/resources/application.yaml index d20d17176..c400791a8 100644 --- a/yudao-gateway/src/main/resources/application.yaml +++ b/yudao-gateway/src/main/resources/application.yaml @@ -152,6 +152,13 @@ spring: - Path=/admin-api/crm/** filters: - RewritePath=/admin-api/crm/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs + ## ai-server 服务 + - id: ai-admin-api # 路由的编号 + uri: grayLb://ai-server + predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组 + - Path=/admin-api/ai/** + filters: + - RewritePath=/admin-api/ai/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs x-forwarded: prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀 @@ -196,6 +203,9 @@ knife4j: - name: crm-server service-name: crm-server url: /admin-api/crm/v3/api-docs + - name: ai-server + service-name: crm-server + url: /admin-api/crm/v3/api-docs --- #################### 芋道相关配置 #################### diff --git a/yudao-module-ai/pom.xml b/yudao-module-ai/pom.xml new file mode 100644 index 000000000..1631f4b48 --- /dev/null +++ b/yudao-module-ai/pom.xml @@ -0,0 +1,27 @@ + + + + cn.iocoder.cloud + yudao + ${revision} + + 4.0.0 + + yudao-module-ai-api + yudao-module-ai-biz + yudao-spring-boot-starter-ai + + pom + yudao-module-ai + + ${project.artifactId} + + ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维脑图等功能。 + 目前已接入各种模型,不限于: + 国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek + 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno + + + \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-api/pom.xml b/yudao-module-ai/yudao-module-ai-api/pom.xml new file mode 100644 index 000000000..09d27de6a --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-api/pom.xml @@ -0,0 +1,32 @@ + + + + cn.iocoder.cloud + yudao-module-ai + ${revision} + + 4.0.0 + yudao-module-ai-api + jar + + ${project.artifactId} + + ai 模块 API,暴露给其它模块调用 + + + + + cn.iocoder.cloud + yudao-common + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/api/package-info.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/api/package-info.java new file mode 100644 index 000000000..4f94f23f8 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/api/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位,没有特别的作用 + */ +package cn.iocoder.yudao.module.ai.api; \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java new file mode 100644 index 000000000..19cbc8f8f --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/AiChatRoleEnum.java @@ -0,0 +1,64 @@ +package cn.iocoder.yudao.module.ai.enums; + +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * AI 内置聊天角色的枚举 + * + * @author xiaoxin + */ +@AllArgsConstructor +@Getter +public enum AiChatRoleEnum implements IntArrayValuable { + + AI_WRITE_ROLE(1, "写作助手", """ + 你是一位出色的写作助手,能够帮助用户生成创意和灵感,并在用户提供场景和提示词时生成对应的回复。你的任务包括: + 1. 撰写建议:根据用户提供的主题或问题,提供详细的写作建议、情节发展方向、角色设定以及背景描写,确保内容结构清晰、有逻辑。 + 2. 回复生成:根据用户提供的场景和提示词,生成合适的对话或文字回复,确保语气和风格符合场景需求。 + 除此之外不需要除了正文内容外的其他回复,如标题、开头、任何解释性语句或道歉。 + """), + + AI_MIND_MAP_ROLE(2, "脑图助手", """ + 你是一位非常优秀的思维导图助手,你会把用户的所有提问都总结成思维导图,然后以 Markdown 格式输出。markdown 只需要输出一级标题,二级标题,三级标题,四级标题,最多输出四级,除此之外不要输出任何其他 markdown 标记。下面是一个合格的例子: + # Geek-AI 助手 + ## 完整的开源系统 + ### 前端开源 + ### 后端开源 + ## 支持各种大模型 + ### OpenAI + ### Azure + ### 文心一言 + ### 通义千问 + ## 集成多种收费方式 + ### 支付宝 + ### 微信 + 除此之外不要任何解释性语句。 + """); + + // TODO @xin:这个 role 是不是删除掉好点哈。= = 目前主要是没做角色枚举。这里多了 role 反倒容易误解哈 + /** + * 角色 + */ + private final Integer role; + /** + * 角色名 + */ + private final String name; + + /** + * 角色设定 + */ + private final String systemMessage; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiChatRoleEnum::getRole).toArray(); + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/DictTypeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/DictTypeConstants.java new file mode 100644 index 000000000..73782a2cb --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/DictTypeConstants.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.ai.enums; + +/** + * AI 字典类型的枚举类 + * + * @author xiaoxin + */ +public interface DictTypeConstants { + + // ========== AI Write ========== + String AI_WRITE_FORMAT = "ai_write_format"; // 写作格式 + String AI_WRITE_LENGTH = "ai_write_length"; // 写作长度 + String AI_WRITE_LANGUAGE = "ai_write_language"; // 写作语言 + String AI_WRITE_TONE = "ai_write_tone"; // 写作语气 + +} diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java new file mode 100644 index 000000000..ddfb489f3 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/ErrorCodeConstants.java @@ -0,0 +1,53 @@ +package cn.iocoder.yudao.module.ai.enums; + +import cn.iocoder.yudao.framework.common.exception.ErrorCode; + +/** + * AI 错误码枚举类 + * + * ai 系统,使用 1-040-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== API 密钥 1-040-000-000 ========== + ErrorCode API_KEY_NOT_EXISTS = new ErrorCode(1_040_000_000, "API 密钥不存在"); + ErrorCode API_KEY_DISABLE = new ErrorCode(1_040_000_001, "API 密钥已禁用!"); + ErrorCode API_KEY_MIDJOURNEY_NOT_FOUND = new ErrorCode(1_040_000_900, "Midjourney 模型不存在"); + ErrorCode API_KEY_SUNO_NOT_FOUND = new ErrorCode(1_040_000_901, "Suno 模型不存在"); + ErrorCode API_KEY_IMAGE_NODE_FOUND = new ErrorCode(1_040_000_902, "平台({}) 图片模型未配置"); + + // ========== API 聊天模型 1-040-001-000 ========== + ErrorCode CHAT_MODEL_NOT_EXISTS = new ErrorCode(1_040_001_000, "模型不存在!"); + ErrorCode CHAT_MODEL_DISABLE = new ErrorCode(1_040_001_001, "模型({})已禁用!"); + ErrorCode CHAT_MODEL_DEFAULT_NOT_EXISTS = new ErrorCode(1_040_001_002, "操作失败,找不到默认聊天模型"); + + // ========== API 聊天模型 1-040-002-000 ========== + ErrorCode CHAT_ROLE_NOT_EXISTS = new ErrorCode(1_040_002_000, "聊天角色不存在"); + ErrorCode CHAT_ROLE_DISABLE = new ErrorCode(1_040_001_001, "聊天角色({})已禁用!"); + + // ========== API 聊天会话 1-040-003-000 ========== + + ErrorCode CHAT_CONVERSATION_NOT_EXISTS = new ErrorCode(1_040_003_000, "对话不存在!"); + ErrorCode CHAT_CONVERSATION_MODEL_ERROR = new ErrorCode(1_040_003_001, "操作失败,该聊天模型的配置不完整"); + + // ========== API 聊天消息 1-040-004-000 ========== + + ErrorCode CHAT_MESSAGE_NOT_EXIST = new ErrorCode(1_040_004_000, "消息不存在!"); + ErrorCode CHAT_STREAM_ERROR = new ErrorCode(1_040_004_001, "对话生成异常!"); + + // ========== API 绘画 1-040-005-000 ========== + + ErrorCode IMAGE_NOT_EXISTS = new ErrorCode(1_022_005_000, "图片不存在!"); + ErrorCode IMAGE_MIDJOURNEY_SUBMIT_FAIL = new ErrorCode(1_022_005_001, "Midjourney 提交失败!原因:{}"); + ErrorCode IMAGE_CUSTOM_ID_NOT_EXISTS = new ErrorCode(1_022_005_002, "Midjourney 按钮 customId 不存在! {}"); + ErrorCode IMAGE_FAIL = new ErrorCode(1_022_005_002, "图片绘画失败! {}"); + + // ========== API 音乐 1-040-006-000 ========== + ErrorCode MUSIC_NOT_EXISTS = new ErrorCode(1_022_006_000, "音乐不存在!"); + + + // ========== API 写作 1-022-007-000 ========== + ErrorCode WRITE_NOT_EXISTS = new ErrorCode(1_022_007_000, "作文不存在!"); + ErrorCode WRITE_STREAM_ERROR = new ErrorCode(1_022_07_001, "写作生成异常!"); + +} diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/image/AiImageStatusEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/image/AiImageStatusEnum.java new file mode 100644 index 000000000..cf8076150 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/image/AiImageStatusEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.ai.enums.image; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * AI 绘画状态的枚举 + * + * @author fansili + */ +@AllArgsConstructor +@Getter +public enum AiImageStatusEnum { + + IN_PROGRESS(10, "进行中"), + SUCCESS(20, "已完成"), + FAIL(30, "已失败"); + + /** + * 状态 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + public static AiImageStatusEnum valueOfStatus(Integer status) { + for (AiImageStatusEnum statusEnum : AiImageStatusEnum.values()) { + if (statusEnum.getStatus().equals(status)) { + return statusEnum; + } + } + throw new IllegalArgumentException("未知会话状态: " + status); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java new file mode 100644 index 000000000..651731b60 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicGenerateModeEnum.java @@ -0,0 +1,37 @@ +package cn.iocoder.yudao.module.ai.enums.music; + +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * AI 音乐生成模式的枚举 + * + * @author xiaoxin + */ +@AllArgsConstructor +@Getter +public enum AiMusicGenerateModeEnum implements IntArrayValuable { + + DESCRIPTION(1, "描述模式"), + LYRIC(2, "歌词模式"); + + /** + * 模式 + */ + private final Integer mode; + /** + * 模式名 + */ + private final String name; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiMusicGenerateModeEnum::getMode).toArray(); + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java new file mode 100644 index 000000000..f1298cf56 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/music/AiMusicStatusEnum.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.ai.enums.music; + +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * AI 音乐状态的枚举 + * + * @author xiaoxin + */ +@AllArgsConstructor +@Getter +public enum AiMusicStatusEnum implements IntArrayValuable { + + IN_PROGRESS(10, "进行中"), + SUCCESS(20, "已完成"), + FAIL(30, "已失败"); + + /** + * 状态 + */ + private final Integer status; + + /** + * 状态名 + */ + private final String name; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiMusicStatusEnum::getStatus).toArray(); + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteTypeEnum.java b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteTypeEnum.java new file mode 100644 index 000000000..49d825be8 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-api/src/main/java/cn/iocoder/yudao/module/ai/enums/write/AiWriteTypeEnum.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.ai.enums.write; + +import cn.iocoder.yudao.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * AI 写作类型的枚举 + * + * @author xiaoxin + */ +@AllArgsConstructor +@Getter +public enum AiWriteTypeEnum implements IntArrayValuable { + + WRITING(1, "撰写", "请撰写一篇关于 [{}] 的文章。文章的内容格式:{},语气:{},语言:{},长度:{}。请确保涵盖主要内容,不需要除了正文内容外的其他回复,如标题、额外的解释或道歉。"), + REPLY(2, "回复", "请针对如下内容:[{}] 做个回复。回复内容参考:[{}], 回复格式:{},语气:{},语言:{},长度:{}。不需要除了正文内容外的其他回复,如标题、开头、额外的解释或道歉。"); + + /** + * 类型 + */ + private final Integer type; + /** + * 类型名 + */ + private final String name; + + /** + * 模版 + */ + private final String prompt; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AiWriteTypeEnum::getType).toArray(); + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/pom.xml b/yudao-module-ai/yudao-module-ai-biz/pom.xml new file mode 100644 index 000000000..83c6764b1 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/pom.xml @@ -0,0 +1,95 @@ + + + + cn.iocoder.cloud + yudao-module-ai + ${revision} + + 4.0.0 + yudao-module-ai-biz + + ${project.artifactId} + + ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维脑图等功能。 + 目前已接入各种模型,不限于: + 国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek + 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno + + + + + + org.springframework.cloud + spring-cloud-starter-bootstrap + + + + + cn.iocoder.cloud + yudao-module-ai-api + ${revision} + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-ai + ${revision} + + + + cn.iocoder.cloud + yudao-spring-boot-starter-biz-tenant + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-security + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-mybatis + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-rpc + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-job + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-test + + + + + cn.iocoder.cloud + yudao-spring-boot-starter-monitor + + + \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/AiServerApplication.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/AiServerApplication.java new file mode 100644 index 000000000..2679dcb52 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/AiServerApplication.java @@ -0,0 +1,30 @@ +package cn.iocoder.yudao.module.ai; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 项目的启动类 + *

+ * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + * 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + * + * @author 芋道源码 + */ +@SpringBootApplication +public class AiServerApplication { + + public static void main(String[] args) { + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + + SpringApplication.run(AiServerApplication.class, args); + + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://cloud.iocoder.cn/quick-start/ 文章 + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java new file mode 100644 index 000000000..5142cde44 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java @@ -0,0 +1,114 @@ +package cn.iocoder.yudao.module.ai.controller.admin.chat; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationCreateMyReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationUpdateMyReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; +import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService; +import cn.iocoder.yudao.module.ai.service.chat.AiChatMessageService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - AI 聊天对话") +@RestController +@RequestMapping("/ai/chat/conversation") +@Validated +public class AiChatConversationController { + + @Resource + private AiChatConversationService chatConversationService; + @Resource + private AiChatMessageService chatMessageService; + + @PostMapping("/create-my") + @Operation(summary = "创建【我的】聊天对话") + public CommonResult createChatConversationMy(@RequestBody @Valid AiChatConversationCreateMyReqVO createReqVO) { + return success(chatConversationService.createChatConversationMy(createReqVO, getLoginUserId())); + } + + @PutMapping("/update-my") + @Operation(summary = "更新【我的】聊天对话") + public CommonResult updateChatConversationMy(@RequestBody @Valid AiChatConversationUpdateMyReqVO updateReqVO) { + chatConversationService.updateChatConversationMy(updateReqVO, getLoginUserId()); + return success(true); + } + + @GetMapping("/my-list") + @Operation(summary = "获得【我的】聊天对话列表") + public CommonResult> getChatConversationMyList() { + List list = chatConversationService.getChatConversationListByUserId(getLoginUserId()); + return success(BeanUtils.toBean(list, AiChatConversationRespVO.class)); + } + + @GetMapping("/get-my") + @Operation(summary = "获得【我的】聊天对话") + @Parameter(name = "id", required = true, description = "对话编号", example = "1024") + public CommonResult getChatConversationMy(@RequestParam("id") Long id) { + AiChatConversationDO conversation = chatConversationService.getChatConversation(id); + if (conversation != null && ObjUtil.notEqual(conversation.getUserId(), getLoginUserId())) { + conversation = null; + } + return success(BeanUtils.toBean(conversation, AiChatConversationRespVO.class)); + } + + @DeleteMapping("/delete-my") + @Operation(summary = "删除聊天对话") + @Parameter(name = "id", required = true, description = "对话编号", example = "1024") + public CommonResult deleteChatConversationMy(@RequestParam("id") Long id) { + chatConversationService.deleteChatConversationMy(id, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete-by-unpinned") + @Operation(summary = "删除未置顶的聊天对话") + public CommonResult deleteChatConversationMyByUnpinned() { + chatConversationService.deleteChatConversationMyByUnpinned(getLoginUserId()); + return success(true); + } + + // ========== 对话管理 ========== + + @GetMapping("/page") + @Operation(summary = "获得对话分页", description = "用于【对话管理】菜单") + @PreAuthorize("@ss.hasPermission('ai:chat-conversation:query')") + public CommonResult> getChatConversationPage(AiChatConversationPageReqVO pageReqVO) { + PageResult pageResult = chatConversationService.getChatConversationPage(pageReqVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + // 拼接关联数据 + Map messageCountMap = chatMessageService.getChatMessageCountMap( + convertList(pageResult.getList(), AiChatConversationDO::getId)); + return success(BeanUtils.toBean(pageResult, AiChatConversationRespVO.class, + conversation -> conversation.setMessageCount(messageCountMap.getOrDefault(conversation.getId(), 0)))); + } + + @Operation(summary = "管理员删除对话") + @DeleteMapping("/delete-by-admin") + @Parameter(name = "id", required = true, description = "对话编号", example = "1024") + @PreAuthorize("@ss.hasPermission('ai:chat-conversation:delete')") + public CommonResult deleteChatConversationByAdmin(@RequestParam("id") Long id) { + chatConversationService.deleteChatConversationByAdmin(id); + return success(true); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http new file mode 100644 index 000000000..e75e0d333 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http @@ -0,0 +1,29 @@ +### 发送消息(段式) +POST {{baseUrl}}/ai/chat/message/send +Content-Type: application/json +Authorization: {{token}} +tenant-id: {{adminTenentId}} + +{ + "conversationId": "1781604279872581724", + "content": "你是 OpenAI 么?" +} + +### 发送消息(流式) +POST {{baseUrl}}/ai/chat/message/send-stream +Content-Type: application/json +Authorization: {{token}} +tenant-id: {{adminTenentId}} + +{ + "conversationId": "1781604279872581724", + "content": "1+1=?" +} + +### 获得指定对话的消息列表 +GET {{baseUrl}}/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581649 +Authorization: {{token}} + +### 删除消息 +DELETE {{baseUrl}}/ai/chat/message/delete?id=50 +Authorization: {{token}} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java new file mode 100644 index 000000000..357dbec5e --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.java @@ -0,0 +1,120 @@ +package cn.iocoder.yudao.module.ai.controller.admin.chat; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.MapUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService; +import cn.iocoder.yudao.module.ai.service.chat.AiChatMessageService; +import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 聊天消息") +@RestController +@RequestMapping("/ai/chat/message") +@Slf4j +public class AiChatMessageController { + + @Resource + private AiChatMessageService chatMessageService; + @Resource + private AiChatConversationService chatConversationService; + @Resource + private AiChatRoleService chatRoleService; + + @Operation(summary = "发送消息(段式)", description = "一次性返回,响应较慢") + @PostMapping("/send") + public CommonResult sendMessage(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) { + return success(chatMessageService.sendMessage(sendReqVO, getLoginUserId())); + } + + @Operation(summary = "发送消息(流式)", description = "流式返回,响应较快") + @PostMapping(value = "/send-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题 + public Flux> sendChatMessageStream(@Valid @RequestBody AiChatMessageSendReqVO sendReqVO) { + return chatMessageService.sendChatMessageStream(sendReqVO, getLoginUserId()); + } + + @Operation(summary = "获得指定对话的消息列表") + @GetMapping("/list-by-conversation-id") + @Parameter(name = "conversationId", required = true, description = "对话编号", example = "1024") + public CommonResult> getChatMessageListByConversationId( + @RequestParam("conversationId") Long conversationId) { + AiChatConversationDO conversation = chatConversationService.getChatConversation(conversationId); + if (conversation == null || ObjUtil.notEqual(conversation.getUserId(), getLoginUserId())) { + return success(Collections.emptyList()); + } + List messageList = chatMessageService.getChatMessageListByConversationId(conversationId); + return success(BeanUtils.toBean(messageList, AiChatMessageRespVO.class)); + } + + @Operation(summary = "删除消息") + @DeleteMapping("/delete") + @Parameter(name = "id", required = true, description = "消息编号", example = "1024") + public CommonResult deleteChatMessage(@RequestParam("id") Long id) { + chatMessageService.deleteChatMessage(id, getLoginUserId()); + return success(true); + } + + @Operation(summary = "删除指定对话的消息") + @DeleteMapping("/delete-by-conversation-id") + @Parameter(name = "conversationId", required = true, description = "对话编号", example = "1024") + public CommonResult deleteChatMessageByConversationId(@RequestParam("conversationId") Long conversationId) { + chatMessageService.deleteChatMessageByConversationId(conversationId, getLoginUserId()); + return success(true); + } + + // ========== 对话管理 ========== + + @GetMapping("/page") + @Operation(summary = "获得消息分页", description = "用于【对话管理】菜单") + @PreAuthorize("@ss.hasPermission('ai:chat-conversation:query')") + public CommonResult> getChatMessagePage(AiChatMessagePageReqVO pageReqVO) { + PageResult pageResult = chatMessageService.getChatMessagePage(pageReqVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + // 拼接数据 + Map roleMap = chatRoleService.getChatRoleMap( + convertSet(pageResult.getList(), AiChatMessageDO::getRoleId)); + return success(BeanUtils.toBean(pageResult, AiChatMessageRespVO.class, + respVO -> MapUtils.findAndThen(roleMap, respVO.getRoleId(), role -> respVO.setRoleName(role.getName())))); + } + + @Operation(summary = "管理员删除消息") + @DeleteMapping("/delete-by-admin") + @Parameter(name = "id", required = true, description = "消息编号", example = "1024") + @PreAuthorize("@ss.hasPermission('ai:chat-message:delete')") + public CommonResult deleteChatMessageByAdmin(@RequestParam("id") Long id) { + chatMessageService.deleteChatMessageByAdmin(id); + return success(true); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationCreateMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationCreateMyReqVO.java new file mode 100644 index 000000000..c13200b6a --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationCreateMyReqVO.java @@ -0,0 +1,13 @@ +package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - AI 聊天对话创建【我的】 Request VO") +@Data +public class AiChatConversationCreateMyReqVO { + + @Schema(description = "聊天角色编号", example = "666") + private Long roleId; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationPageReqVO.java new file mode 100644 index 000000000..967e866ea --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationPageReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - AI 聊天对话的分页 Request VO") +@Data +public class AiChatConversationPageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "1024") + private Long userId; + + @Schema(description = "对话标题", example = "你好") + private String title; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java new file mode 100644 index 000000000..66eb24db5 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationRespVO.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation; + +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import com.fhs.core.trans.anno.Trans; +import com.fhs.core.trans.constant.TransType; +import com.fhs.core.trans.vo.VO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - AI 聊天对话 Response VO") +@Data +public class AiChatConversationRespVO implements VO { + + @Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long userId; + + @Schema(description = "对话标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是一个标题") + private String title; + + @Schema(description = "是否置顶", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean pinned; + + @Schema(description = "角色编号", example = "1") + @Trans(type = TransType.SIMPLE, target = AiChatRoleDO.class, fields = {"name", "avatar"}, refs = {"roleName", "roleAvatar"}) + private Long roleId; + + @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @Trans(type = TransType.SIMPLE, target = AiChatModelDO.class, fields = "name", ref = "modelName") + private Long modelId; + + @Schema(description = "模型标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "ERNIE-Bot-turbo-0922") + private String model; + + @Schema(description = "模型名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + private String modelName; + + @Schema(description = "角色设定", example = "一个快乐的程序员") + private String systemMessage; + + @Schema(description = "温度参数", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.8") + private Double temperature; + + @Schema(description = "单条回复的最大 Token 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096") + private Integer maxTokens; + + @Schema(description = "上下文的最大 Message 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer maxContexts; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + // ========== 关联 role 信息 ========== + + @Schema(description = "角色头像", example = "https://www.iocoder.cn/1.png") + private String roleAvatar; + + @Schema(description = "角色名字", example = "小黄") + private String roleName; + + // ========== 仅在【对话管理】时加载 ========== + + @Schema(description = "消息数量", example = "20") + private Integer messageCount; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationUpdateMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationUpdateMyReqVO.java new file mode 100644 index 000000000..f9ce64bae --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/conversation/AiChatConversationUpdateMyReqVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - AI 聊天对话更新【我的】 Request VO") +@Data +public class AiChatConversationUpdateMyReqVO { + + @Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "对话编号不能为空") + private Long id; + + @Schema(description = "对话标题", example = "我是一个标题") + private String title; + + @Schema(description = "是否置顶", example = "true") + private Boolean pinned; + + @Schema(description = "模型编号", example = "1") + private Long modelId; + + @Schema(description = "角色设定", example = "一个快乐的程序员") + private String systemMessage; + + @Schema(description = "温度参数", example = "0.8") + private Double temperature; + + @Schema(description = "单条回复的最大 Token 数量", example = "4096") + private Integer maxTokens; + + @Schema(description = "上下文的最大 Message 数量", example = "10") + private Integer maxContexts; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessagePageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessagePageReqVO.java new file mode 100644 index 000000000..7ccb6aa0b --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessagePageReqVO.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - AI 聊天消息的分页 Request VO") +@Data +public class AiChatMessagePageReqVO extends PageParam { + + @Schema(description = "对话编号", example = "2048") + private Long conversationId; + + @Schema(description = "用户编号", example = "1024") + private Long userId; + + @Schema(description = "消息内容", example = "你好") + private String content; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java new file mode 100644 index 000000000..9b358df6f --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - AI 聊天消息 Response VO") +@Data +public class AiChatMessageRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long conversationId; + + @Schema(description = "回复消息编号", example = "1024") + private Long replyId; + + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "role") + private String type; // 参见 MessageType 枚举类 + + @Schema(description = "用户编号", example = "4096") + private Long userId; + + @Schema(description = "角色编号", example = "888") + private Long roleId; + + @Schema(description = "模型标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-3.5-turbo") + private String model; + + @Schema(description = "模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "123") + private Long modelId; + + @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊") + private String content; + + @Schema(description = "是否携带上下文", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean useContext; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-05-12 12:51") + private LocalDateTime createTime; + + // ========== 仅在【对话管理】时加载 ========== + + @Schema(description = "角色名字", example = "小黄") + private String roleName; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java new file mode 100644 index 000000000..89a84bcbd --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.experimental.Accessors; + +@Schema(description = "管理后台 - AI 聊天消息发送 Request VO") +@Data +public class AiChatMessageSendReqVO { + + @Schema(description = "聊天对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "聊天对话编号不能为空") + private Long conversationId; + + @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "帮我写个 Java 算法") + @NotEmpty(message = "聊天内容不能为空") + private String content; + + @Schema(description = "是否携带上下文", example = "true") + private Boolean useContext; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java new file mode 100644 index 000000000..58ba05659 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - AI 聊天消息发送 Response VO") +@Data +public class AiChatMessageSendRespVO { + + @Schema(description = "发送消息", requiredMode = Schema.RequiredMode.REQUIRED) + private Message send; + + @Schema(description = "接收消息", requiredMode = Schema.RequiredMode.REQUIRED) + private Message receive; + + @Schema(description = "消息") + @Data + public static class Message { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "消息类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "role") + private String type; // 参见 MessageType 枚举类 + + @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊") + private String content; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.http new file mode 100644 index 000000000..9047610c0 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.http @@ -0,0 +1,42 @@ +### 生成图片:OpenAI(DALL) +POST {{baseUrl}}/ai/image/draw +Content-Type: application/json +Authorization: {{token}} + +{ + "platform": "OpenAI", + "prompt": "可爱的小喵星人", + "model": "dall-e-3", + "height": "1024", + "width": "1024", + "options": { + "style": "vivid" + } +} + +### 生成图片:StableDiffusion +POST {{baseUrl}}/ai/image/draw +Content-Type: application/json +Authorization: {{token}} + +{ + "platform": "StableDiffusion", + "prompt": "中国长城", + "model": "stable-diffusion-v1-6", + "height": "1024", + "width": "1024", + "style": "vivid" +} + +### 生成图片:生成图片(Midjourney) +POST {{baseUrl}}/ai/image/midjourney/imagine +Content-Type: application/json +Authorization: {{token}} + +{ + "prompt": "中国旗袍", + "model": "midjourney", + "width": "1", + "height": "1", + "version": "6.0" +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java new file mode 100644 index 000000000..de12ee1e0 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/AiImageController.java @@ -0,0 +1,134 @@ +package cn.iocoder.yudao.module.ai.controller.admin.image; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO; +import cn.iocoder.yudao.module.ai.service.image.AiImageService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - AI 绘画") +@RestController +@RequestMapping("/ai/image") +@Slf4j +public class AiImageController { + + @Resource + private AiImageService imageService; + + @GetMapping("/my-page") + @Operation(summary = "获取【我的】绘图分页") + public CommonResult> getImagePageMy(@Validated PageParam pageReqVO) { + PageResult pageResult = imageService.getImagePageMy(getLoginUserId(), pageReqVO); + return success(BeanUtils.toBean(pageResult, AiImageRespVO.class)); + } + + @GetMapping("/get-my") + @Operation(summary = "获取【我的】绘图记录") + @Parameter(name = "id", required = true, description = "绘画编号", example = "1024") + public CommonResult getImageMy(@RequestParam("id") Long id) { + AiImageDO image = imageService.getImage(id); + if (image == null || ObjUtil.notEqual(getLoginUserId(), image.getUserId())) { + return success(null); + } + return success(BeanUtils.toBean(image, AiImageRespVO.class)); + } + + @GetMapping("/my-list-by-ids") + @Operation(summary = "获取【我的】绘图记录列表") + @Parameter(name = "ids", required = true, description = "绘画编号数组", example = "1024,2048") + public CommonResult> getImageListMyByIds(@RequestParam("ids") List ids) { + List imageList = imageService.getImageList(ids); + imageList.removeIf(item -> !ObjUtil.equal(getLoginUserId(), item.getUserId())); + return success(BeanUtils.toBean(imageList, AiImageRespVO.class)); + } + + @Operation(summary = "生成图片") + @PostMapping("/draw") + public CommonResult drawImage(@Valid @RequestBody AiImageDrawReqVO drawReqVO) { + return success(imageService.drawImage(getLoginUserId(), drawReqVO)); + } + + @Operation(summary = "删除【我的】绘画记录") + @DeleteMapping("/delete-my") + @Parameter(name = "id", required = true, description = "绘画编号", example = "1024") + public CommonResult deleteImageMy(@RequestParam("id") Long id) { + imageService.deleteImageMy(id, getLoginUserId()); + return success(true); + } + + // ================ midjourney 专属 ================ + + @Operation(summary = "【Midjourney】生成图片") + @PostMapping("/midjourney/imagine") + public CommonResult midjourneyImagine(@Valid @RequestBody AiMidjourneyImagineReqVO reqVO) { + Long imageId = imageService.midjourneyImagine(getLoginUserId(), reqVO); + return success(imageId); + } + + @Operation(summary = "【Midjourney】通知图片进展", description = "由 Midjourney Proxy 回调") + @PostMapping("/midjourney/notify") // 必须是 POST 方法,否则会报错 + @PermitAll + public CommonResult midjourneyNotify(@Valid @RequestBody MidjourneyApi.Notify notify) { + imageService.midjourneyNotify(notify); + return success(true); + } + + @Operation(summary = "【Midjourney】Action 操作(二次生成图片)", description = "例如说:放大、缩小、U1、U2 等") + @PostMapping("/midjourney/action") + public CommonResult midjourneyAction(@Valid @RequestBody AiMidjourneyActionReqVO reqVO) { + Long imageId = imageService.midjourneyAction(getLoginUserId(), reqVO); + return success(imageId); + } + + // ================ 绘图管理 ================ + + @GetMapping("/page") + @Operation(summary = "获得绘画分页") + @PreAuthorize("@ss.hasPermission('ai:image:query')") + public CommonResult> getImagePage(@Valid AiImagePageReqVO pageReqVO) { + PageResult pageResult = imageService.getImagePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, AiImageRespVO.class)); + } + + @PutMapping("/update") + @Operation(summary = "更新绘画") + @PreAuthorize("@ss.hasPermission('ai:image:update')") + public CommonResult updateImage(@Valid @RequestBody AiImageUpdateReqVO updateReqVO) { + imageService.updateImage(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除绘画") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('ai:image:delete')") + public CommonResult deleteImage(@RequestParam("id") Long id) { + imageService.deleteImage(id); + return success(true); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java new file mode 100644 index 000000000..a38935ef7 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageDrawReqVO.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.module.ai.controller.admin.image.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import org.springframework.ai.openai.OpenAiImageOptions; +import org.springframework.ai.stabilityai.api.StabilityAiImageOptions; + +import java.util.Map; + +@Schema(description = "管理后台 - AI 绘画 Request VO") +@Data +public class AiImageDrawReqVO { + + @Schema(description = "模型平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI") + private String platform; // 参见 AiPlatformEnum 枚举 + + @Schema(description = "提示词", requiredMode = Schema.RequiredMode.REQUIRED, example = "画一个长城") + @NotEmpty(message = "提示词不能为空") + @Size(max = 1200, message = "提示词最大 1200") + private String prompt; + + @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "stable-diffusion-v1-6") + @NotEmpty(message = "模型不能为空") + private String model; + + /** + * 1. dall-e-2 模型:256x256、512x512、1024x1024 + * 2. dall-e-3 模型:1024x1024, 1792x1024, 或 1024x1792 + */ + @Schema(description = "图片高度") + @NotNull(message = "图片高度不能为空") + private Integer height; + + @Schema(description = "图片宽度") + @NotNull(message = "图片宽度不能为空") + private Integer width; + + // ========== 各平台绘画的拓展参数 ========== + + /** + * 绘制参数,不同 platform 的不同参数 + * + * 1. {@link OpenAiImageOptions} + * 2. {@link StabilityAiImageOptions} + */ + @Schema(description = "绘制参数") + private Map options; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java new file mode 100644 index 000000000..bdf329c61 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImagePageReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.ai.controller.admin.image.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - AI 绘画分页 Request VO") +@Data +public class AiImagePageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "28987") + private Long userId; + + @Schema(description = "平台", example = "OpenAI") + private String platform; + + @Schema(description = "绘画状态", example = "1") + private Integer status; + + @Schema(description = "是否发布", example = "1") + private Boolean publicStatus; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageRespVO.java new file mode 100644 index 000000000..f73d05aaa --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageRespVO.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.ai.controller.admin.image.vo; + +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - AI 绘画 Response VO") +@Data +public class AiImageRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long userId; + + @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI") + private String platform; // 参见 AiPlatformEnum 枚举 + + @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "stable-diffusion-v1-6") + private String model; + + @Schema(description = "提示词", requiredMode = Schema.RequiredMode.REQUIRED, example = "南极的小企鹅") + private String prompt; + + @Schema(description = "图片宽度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer width; + + @Schema(description = "图片高度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer height; + + @Schema(description = "绘画状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer status; + + @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "public") + private Boolean publicStatus; + + @Schema(description = "图片地址", example = "https://www.iocoder.cn/1.png") + private String picUrl; + + @Schema(description = "绘画错误信息", example = "图片错误信息") + private String errorMessage; + + @Schema(description = "绘制参数") + private Map options; + + @Schema(description = "mj buttons 按钮") + private List buttons; + + @Schema(description = "完成时间") + private LocalDateTime finishTime; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdateReqVO.java new file mode 100644 index 000000000..45df01015 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/AiImageUpdateReqVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.ai.controller.admin.image.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - AI 绘画修改 Request VO") +@Data +public class AiImageUpdateReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "是否发布", example = "true") + private Boolean publicStatus; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyActionReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyActionReqVO.java new file mode 100644 index 000000000..28803a051 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyActionReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - AI 绘图操作(Midjourney) Request VO") +@Data +public class AiMidjourneyActionReqVO { + + @Schema(description = "图片编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "图片编号不能为空") + private Long id; + + @Schema(description = "操作按钮编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "MJ::JOB::variation::4::06aa3e66-0e97-49cc-8201-e0295d883de4") + @NotEmpty(message = "操作按钮编号不能为空") + private String customId; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java new file mode 100644 index 000000000..b90882639 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/image/vo/midjourney/AiMidjourneyImagineReqVO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - AI 绘画生成(Midjourney) Request VO") +@Data +public class AiMidjourneyImagineReqVO { + + @Schema(description = "提示词", requiredMode = Schema.RequiredMode.REQUIRED, example = "中国神龙") + @NotEmpty(message = "提示词不能为空!") + private String prompt; + + @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "midjourney") + @NotEmpty(message = "模型不能为空") + private String model; // 参考 MidjourneyApi.ModelEnum + + @Schema(description = "图片宽度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "图片宽度不能为空") + private Integer width; + + @Schema(description = "图片高度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "图片高度不能为空") + private Integer height; + + @Schema(description = "版本号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6.0") + @NotEmpty(message = "版本号不能为空") + private String version; + + @Schema(description = "参考图", example = "https://www.iocoder.cn/x.png") + private String referImageUrl; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java new file mode 100644 index 000000000..015180265 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/AiMindMapController.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.ai.controller.admin.mindmap; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO; +import cn.iocoder.yudao.module.ai.service.mindmap.AiMindMapService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import jakarta.validation.Valid; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; + +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - AI 思维导图") +@RestController +@RequestMapping("/ai/mind-map") +public class AiMindMapController { + + @Resource + private AiMindMapService mindMapService; + + @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + @Operation(summary = "脑图生成(流式)", description = "流式返回,响应较快") + @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题 + public Flux> generateMindMap(@RequestBody @Valid AiMindMapGenerateReqVO generateReqVO) { + return mindMapService.generateMindMap(generateReqVO, getLoginUserId()); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapGenerateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapGenerateReqVO.java new file mode 100644 index 000000000..08404bb0f --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/mindmap/vo/AiMindMapGenerateReqVO.java @@ -0,0 +1,15 @@ +package cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +@Schema(description = "管理后台 - AI 思维导图生成 Request VO") +@Data +public class AiMindMapGenerateReqVO { + + @Schema(description = "思维导图内容提示", example = "Java 学习路线") + @NotBlank(message = "思维导图内容提示不能为空") + private String prompt; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java new file mode 100644 index 000000000..2bc190051 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiApiKeyController.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelRespVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - AI API 密钥") +@RestController +@RequestMapping("/ai/api-key") +@Validated +public class AiApiKeyController { + + @Resource + private AiApiKeyService apiKeyService; + + @PostMapping("/create") + @Operation(summary = "创建 API 密钥") + @PreAuthorize("@ss.hasPermission('ai:api-key:create')") + public CommonResult createApiKey(@Valid @RequestBody AiApiKeySaveReqVO createReqVO) { + return success(apiKeyService.createApiKey(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新 API 密钥") + @PreAuthorize("@ss.hasPermission('ai:api-key:update')") + public CommonResult updateApiKey(@Valid @RequestBody AiApiKeySaveReqVO updateReqVO) { + apiKeyService.updateApiKey(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除 API 密钥") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('ai:api-key:delete')") + public CommonResult deleteApiKey(@RequestParam("id") Long id) { + apiKeyService.deleteApiKey(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得 API 密钥") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:api-key:query')") + public CommonResult getApiKey(@RequestParam("id") Long id) { + AiApiKeyDO apiKey = apiKeyService.getApiKey(id); + return success(BeanUtils.toBean(apiKey, AiApiKeyRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得 API 密钥分页") + @PreAuthorize("@ss.hasPermission('ai:api-key:query')") + public CommonResult> getApiKeyPage(@Valid AiApiKeyPageReqVO pageReqVO) { + PageResult pageResult = apiKeyService.getApiKeyPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, AiApiKeyRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得 API 密钥分页列表") + public CommonResult> getApiKeySimpleList() { + List list = apiKeyService.getApiKeyList(); + return success(convertList(list, key -> new AiChatModelRespVO().setId(key.getId()).setName(key.getName()))); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatModelController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatModelController.java new file mode 100644 index 000000000..08a53b286 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatModelController.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +@Tag(name = "管理后台 - AI 聊天模型") +@RestController +@RequestMapping("/ai/chat-model") +@Validated +public class AiChatModelController { + + @Resource + private AiChatModelService chatModelService; + + @PostMapping("/create") + @Operation(summary = "创建聊天模型") + @PreAuthorize("@ss.hasPermission('ai:chat-model:create')") + public CommonResult createChatModel(@Valid @RequestBody AiChatModelSaveReqVO createReqVO) { + return success(chatModelService.createChatModel(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新聊天模型") + @PreAuthorize("@ss.hasPermission('ai:chat-model:update')") + public CommonResult updateChatModel(@Valid @RequestBody AiChatModelSaveReqVO updateReqVO) { + chatModelService.updateChatModel(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除聊天模型") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('ai:chat-model:delete')") + public CommonResult deleteChatModel(@RequestParam("id") Long id) { + chatModelService.deleteChatModel(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得聊天模型") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:chat-model:query')") + public CommonResult getChatModel(@RequestParam("id") Long id) { + AiChatModelDO chatModel = chatModelService.getChatModel(id); + return success(BeanUtils.toBean(chatModel, AiChatModelRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得聊天模型分页") + @PreAuthorize("@ss.hasPermission('ai:chat-model:query')") + public CommonResult> getChatModelPage(@Valid AiChatModelPageReqVO pageReqVO) { + PageResult pageResult = chatModelService.getChatModelPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, AiChatModelRespVO.class)); + } + + @GetMapping("/simple-list") + @Operation(summary = "获得聊天模型列表") + @Parameter(name = "status", description = "状态", required = true, example = "1") + public CommonResult> getChatModelSimpleList(@RequestParam("status") Integer status) { + List list = chatModelService.getChatModelListByStatus(status); + return success(convertList(list, model -> new AiChatModelRespVO().setId(model.getId()) + .setName(model.getName()).setModel(model.getModel()))); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java new file mode 100644 index 000000000..02f698b94 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java @@ -0,0 +1,124 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRolePageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleRespVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveMyReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - AI 聊天角色") +@RestController +@RequestMapping("/ai/chat-role") +@Validated +public class AiChatRoleController { + + @Resource + private AiChatRoleService chatRoleService; + + @GetMapping("/my-page") + @Operation(summary = "获得【我的】聊天角色分页") + public CommonResult> getChatRoleMyPage(@Valid AiChatRolePageReqVO pageReqVO) { + PageResult pageResult = chatRoleService.getChatRoleMyPage(pageReqVO, getLoginUserId()); + return success(BeanUtils.toBean(pageResult, AiChatRoleRespVO.class)); + } + + @GetMapping("/get-my") + @Operation(summary = "获得【我的】聊天角色") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + public CommonResult getChatRoleMy(@RequestParam("id") Long id) { + AiChatRoleDO chatRole = chatRoleService.getChatRole(id); + if (ObjUtil.notEqual(chatRole.getUserId(), getLoginUserId())) { + return success(null); + } + return success(BeanUtils.toBean(chatRole, AiChatRoleRespVO.class)); + } + + @PostMapping("/create-my") + @Operation(summary = "创建【我的】聊天角色") + public CommonResult createChatRoleMy(@Valid @RequestBody AiChatRoleSaveMyReqVO createReqVO) { + return success(chatRoleService.createChatRoleMy(createReqVO, getLoginUserId())); + } + + @PutMapping("/update-my") + @Operation(summary = "更新【我的】聊天角色") + public CommonResult updateChatRoleMy(@Valid @RequestBody AiChatRoleSaveMyReqVO updateReqVO) { + chatRoleService.updateChatRoleMy(updateReqVO, getLoginUserId()); + return success(true); + } + + @DeleteMapping("/delete-my") + @Operation(summary = "删除【我的】聊天角色") + @Parameter(name = "id", description = "编号", required = true) + public CommonResult deleteChatRoleMy(@RequestParam("id") Long id) { + chatRoleService.deleteChatRoleMy(id, getLoginUserId()); + return success(true); + } + + @GetMapping("/category-list") + @Operation(summary = "获得聊天角色的分类列表") + public CommonResult> getChatRoleCategoryList() { + return success(chatRoleService.getChatRoleCategoryList()); + } + + // ========== 角色管理 ========== + + @PostMapping("/create") + @Operation(summary = "创建聊天角色") + @PreAuthorize("@ss.hasPermission('ai:chat-role:create')") + public CommonResult createChatRole(@Valid @RequestBody AiChatRoleSaveReqVO createReqVO) { + return success(chatRoleService.createChatRole(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新聊天角色") + @PreAuthorize("@ss.hasPermission('ai:chat-role:update')") + public CommonResult updateChatRole(@Valid @RequestBody AiChatRoleSaveReqVO updateReqVO) { + chatRoleService.updateChatRole(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除聊天角色") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('ai:chat-role:delete')") + public CommonResult deleteChatRole(@RequestParam("id") Long id) { + chatRoleService.deleteChatRole(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得聊天角色") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:chat-role:query')") + public CommonResult getChatRole(@RequestParam("id") Long id) { + AiChatRoleDO chatRole = chatRoleService.getChatRole(id); + return success(BeanUtils.toBean(chatRole, AiChatRoleRespVO.class)); + } + + @GetMapping("/page") + @Operation(summary = "获得聊天角色分页") + @PreAuthorize("@ss.hasPermission('ai:chat-role:query')") + public CommonResult> getChatRolePage(@Valid AiChatRolePageReqVO pageReqVO) { + PageResult pageResult = chatRoleService.getChatRolePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, AiChatRoleRespVO.class)); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyPageReqVO.java new file mode 100644 index 000000000..063696244 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyPageReqVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - AI API 密钥分页 Request VO") +@Data +public class AiApiKeyPageReqVO extends PageParam { + + @Schema(description = "名称", example = "文心一言") + private String name; + + @Schema(description = "平台", example = "OpenAI") + private String platform; + + @Schema(description = "状态", example = "1") + private Integer status; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyRespVO.java new file mode 100644 index 000000000..55d6d802b --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeyRespVO.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Schema(description = "管理后台 - AI API 密钥 Response VO") +@Data +public class AiApiKeyRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23538") + private Long id; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "文心一言") + private String name; + + @Schema(description = "密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABC") + private String apiKey; + + @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI") + private String platform; + + @Schema(description = "自定义 API 地址", example = "https://aip.baidubce.com") + private String url; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeySaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeySaveReqVO.java new file mode 100644 index 000000000..8fbc8fde7 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/apikey/AiApiKeySaveReqVO.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import jakarta.validation.constraints.*; + +@Schema(description = "管理后台 - AI API 密钥新增/修改 Request VO") +@Data +public class AiApiKeySaveReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23538") + private Long id; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "文心一言") + @NotEmpty(message = "名称不能为空") + private String name; + + @Schema(description = "密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABC") + @NotEmpty(message = "密钥不能为空") + private String apiKey; + + @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI") + @NotEmpty(message = "平台不能为空") + private String platform; + + @Schema(description = "自定义 API 地址", example = "https://aip.baidubce.com") + private String url; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelPageReqVO.java new file mode 100644 index 000000000..ce2f83b4b --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelPageReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.iocoder.yudao.framework.common.pojo.PageParam; + +@Schema(description = "管理后台 - API 聊天模型分页 Request VO") +@Data +public class AiChatModelPageReqVO extends PageParam { + + @Schema(description = "模型名字", example = "张三") + private String name; + + @Schema(description = "模型标识", example = "gpt-3.5-turbo-0125") + private String model; + + @Schema(description = "模型平台", example = "OpenAI") + private String platform; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelRespVO.java new file mode 100644 index 000000000..681dabe68 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelRespVO.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - AI 聊天模型 Response VO") +@Data +public class AiChatModelRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2630") + private Long id; + + @Schema(description = "API 秘钥编号", example = "22042") + private Long keyId; + + @Schema(description = "模型名字", example = "张三") + private String name; + + @Schema(description = "模型标识", example = "gpt-3.5-turbo-0125") + private String model; + + @Schema(description = "模型平台", example = "OpenAI") + private String platform; + + @Schema(description = "排序", example = "1") + private Integer sort; + + @Schema(description = "状态", example = "2") + private Integer status; + + @Schema(description = "温度参数", example = "1") + private Double temperature; + + @Schema(description = "单条回复的最大 Token 数量", example = "4096") + private Integer maxTokens; + + @Schema(description = "上下文的最大 Message 数量", example = "8192") + private Integer maxContexts; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelSaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelSaveReqVO.java new file mode 100644 index 000000000..4fad5a1fc --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatModel/AiChatModelSaveReqVO.java @@ -0,0 +1,50 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import jakarta.validation.constraints.*; + +@Schema(description = "管理后台 - API 聊天模型新增/修改 Request VO") +@Data +public class AiChatModelSaveReqVO { + + @Schema(description = "编号", example = "2630") + private Long id; + + @Schema(description = "API 秘钥编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "22042") + @NotNull(message = "API 秘钥编号不能为空") + private Long keyId; + + @Schema(description = "模型名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + @NotEmpty(message = "模型名字不能为空") + private String name; + + @Schema(description = "模型标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-3.5-turbo-0125") + @NotEmpty(message = "模型标识不能为空") + private String model; + + @Schema(description = "模型平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "OpenAI") + @NotEmpty(message = "模型平台不能为空") + private String platform; + + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "排序不能为空") + private Integer sort; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(CommonStatusEnum.class) + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "温度参数", example = "1") + private Double temperature; + + @Schema(description = "单条回复的最大 Token 数量", example = "4096") + private Integer maxTokens; + + @Schema(description = "上下文的最大 Message 数量", example = "8192") + private Integer maxContexts; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRolePageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRolePageReqVO.java new file mode 100644 index 000000000..0a9d08de5 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRolePageReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; +import cn.iocoder.yudao.framework.common.pojo.PageParam; + +@Schema(description = "管理后台 - AI 聊天角色分页 Request VO") +@Data +public class AiChatRolePageReqVO extends PageParam { + + @Schema(description = "角色名称", example = "李四") + private String name; + + @Schema(description = "角色类别", example = "创作") + private String category; + + @Schema(description = "是否公开", example = "1") + private Boolean publicStatus; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java new file mode 100644 index 000000000..eb34da274 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole; + +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import com.fhs.core.trans.anno.Trans; +import com.fhs.core.trans.constant.TransType; +import com.fhs.core.trans.vo.VO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - AI 聊天角色 Response VO") +@Data +public class AiChatRoleRespVO implements VO { + + @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "32746") + private Long id; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "9442") + private Long userId; + + @Schema(description = "模型编号", example = "17640") + @Trans(type = TransType.SIMPLE, target = AiChatModelDO.class, fields = {"name", "model"}, refs = {"modelName", "model"}) + private Long modelId; + @Schema(description = "模型名字", example = "张三") + private String modelName; + @Schema(description = "模型标识", example = "gpt-3.5-turbo-0125") + private String model; + + @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + private String name; + + @Schema(description = "角色头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + private String avatar; + + @Schema(description = "角色类别", requiredMode = Schema.RequiredMode.REQUIRED, example = "创作") + private String category; + + @Schema(description = "角色排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer sort; + + @Schema(description = "角色描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "你说的对") + private String description; + + @Schema(description = "角色设定", requiredMode = Schema.RequiredMode.REQUIRED) + private String systemMessage; + + @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Boolean publicStatus; + + @Schema(description = "状态", example = "1") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java new file mode 100644 index 000000000..4673901d3 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +@Schema(description = "管理后台 - AI 聊天角色新增/修改【我的】 Request VO") +@Data +public class AiChatRoleSaveMyReqVO { + + @Schema(description = "角色编号", example = "32746") + private Long id; + + @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotEmpty(message = "角色名称不能为空") + private String name; + + @Schema(description = "角色头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + @NotEmpty(message = "角色头像不能为空") + @URL(message = "角色头像必须是 URL 格式") + private String avatar; + + @Schema(description = "角色描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "你说的对") + @NotEmpty(message = "角色描述不能为空") + private String description; + + @Schema(description = "角色设定", requiredMode = Schema.RequiredMode.REQUIRED, example = "现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题") + @NotEmpty(message = "角色设定不能为空") + private String systemMessage; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java new file mode 100644 index 000000000..bdda027ef --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import jakarta.validation.constraints.*; +import org.hibernate.validator.constraints.URL; + +@Schema(description = "管理后台 - AI 聊天角色新增/修改 Request VO") +@Data +public class AiChatRoleSaveReqVO { + + @Schema(description = "角色编号", example = "32746") + private Long id; + + @Schema(description = "模型编号", example = "17640") + private Long modelId; + + @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotEmpty(message = "角色名称不能为空") + private String name; + + @Schema(description = "角色头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + @NotEmpty(message = "角色头像不能为空") + @URL(message = "角色头像必须是 URL 格式") + private String avatar; + + @Schema(description = "角色类别", requiredMode = Schema.RequiredMode.REQUIRED, example = "创作") + @NotEmpty(message = "角色类别不能为空") + private String category; + + @Schema(description = "角色排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "角色排序不能为空") + private Integer sort; + + @Schema(description = "角色描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "你说的对") + @NotEmpty(message = "角色描述不能为空") + private String description; + + @Schema(description = "角色设定", requiredMode = Schema.RequiredMode.REQUIRED, example = "现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题") + @NotEmpty(message = "角色设定不能为空") + private String systemMessage; + + @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "是否公开不能为空") + private Boolean publicStatus; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http new file mode 100644 index 000000000..ae68c82ea --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.http @@ -0,0 +1,26 @@ +### 生成音乐:Suno + 歌词模式 +POST {{baseUrl}}/ai/music/generate +Content-Type: application/json +Authorization: {{token}} + +{ + "platform": "Suno", + "generateMode": 2, + "prompt": "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。", + "model": "chirp-v3.5", + "tags": ["Happy"], + "title": "Happy Song" +} + +### 生成音乐:Suno + 描述模式 +POST {{baseUrl}}/ai/music/generate +Content-Type: application/json +Authorization: {{token}} + +{ + "platform": "Suno", + "generateMode": 1, + "model": "chirp-v3.5", + "prompt": "happy music", + "makeInstrumental": false +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java new file mode 100644 index 000000000..6c09e4b30 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/AiMusicController.java @@ -0,0 +1,98 @@ +package cn.iocoder.yudao.module.ai.controller.admin.music; + +import cn.hutool.core.util.ObjUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.*; +import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; +import cn.iocoder.yudao.module.ai.service.music.AiMusicService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.validation.Valid; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - AI 音乐") +@RestController +@RequestMapping("/ai/music") +public class AiMusicController { + + @Resource + private AiMusicService musicService; + + @GetMapping("/my-page") + @Operation(summary = "获得【我的】音乐分页") + public CommonResult> getMusicMyPage(@Valid AiMusicPageReqVO pageReqVO) { + PageResult pageResult = musicService.getMusicMyPage(pageReqVO, getLoginUserId()); + return success(BeanUtils.toBean(pageResult, AiMusicRespVO.class)); + } + + @PostMapping("/generate") + @Operation(summary = "音乐生成") + public CommonResult> generateMusic(@RequestBody @Valid AiSunoGenerateReqVO reqVO) { + return success(musicService.generateMusic(getLoginUserId(), reqVO)); + } + + @Operation(summary = "删除【我的】音乐记录") + @DeleteMapping("/delete-my") + @Parameter(name = "id", required = true, description = "音乐编号", example = "1024") + public CommonResult deleteMusicMy(@RequestParam("id") Long id) { + musicService.deleteMusicMy(id, getLoginUserId()); + return success(true); + } + + @GetMapping("/get-my") + @Operation(summary = "获取【我的】音乐") + @Parameter(name = "id", required = true, description = "音乐编号", example = "1024") + public CommonResult getMusicMy(@RequestParam("id") Long id) { + AiMusicDO music = musicService.getMusic(id); + if (music == null || ObjUtil.notEqual(getLoginUserId(), music.getUserId())) { + return success(null); + } + return success(BeanUtils.toBean(music, AiMusicRespVO.class)); + } + + @PostMapping("/update-my") + @Operation(summary = "修改【我的】音乐 目前只支持修改标题") + @Parameter(name = "title", required = true, description = "音乐名称", example = "夜空中最亮的星") + public CommonResult updateMy(AiMusicUpdateMyReqVO updateReqVO) { + musicService.updateMyMusic(updateReqVO, getLoginUserId()); + return success(true); + } + + // ================ 音乐管理 ================ + + @GetMapping("/page") + @Operation(summary = "获得音乐分页") + @PreAuthorize("@ss.hasPermission('ai:music:query')") + public CommonResult> getMusicPage(@Valid AiMusicPageReqVO pageReqVO) { + PageResult pageResult = musicService.getMusicPage(pageReqVO); + return success(BeanUtils.toBean(pageResult, AiMusicRespVO.class)); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除音乐") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('ai:music:delete')") + public CommonResult deleteMusic(@RequestParam("id") Long id) { + musicService.deleteMusic(id); + return success(true); + } + + @PutMapping("/update") + @Operation(summary = "更新音乐") + @PreAuthorize("@ss.hasPermission('ai:music:update')") + public CommonResult updateMusic(@Valid @RequestBody AiMusicUpdateReqVO updateReqVO) { + musicService.updateMusic(updateReqVO); + return success(true); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicPageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicPageReqVO.java new file mode 100644 index 000000000..678edae3d --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicPageReqVO.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.ai.controller.admin.music.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum; +import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - AI 音乐分页 Request VO") +@Data +public class AiMusicPageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "12212") + private Long userId; + + @Schema(description = "音乐名称", example = "夜空中最亮的星") + private String title; + + @Schema(description = "音乐状态", example = "20") + @InEnum(AiMusicStatusEnum.class) + private Integer status; + + @Schema(description = "生成模式", example = "1") + @InEnum(AiMusicGenerateModeEnum.class) + private Integer generateMode; + + @Schema(description = "是否发布", example = "true") + private Boolean publicStatus; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicRespVO.java new file mode 100644 index 000000000..05044a4e7 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicRespVO.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.ai.controller.admin.music.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - AI 音乐 Response VO") +@Data +public class AiMusicRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790") + private Long id; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "12212") + private Long userId; + + @Schema(description = "音乐名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "夜空中最亮的星") + private String title; + + @Schema(description = "歌词", example = "oh~卖糕的") + private String lyric; + + @Schema(description = "图片地址", example = "https://www.iocoder.cn") + private String imageUrl; + + @Schema(description = "音频地址", example = "https://www.iocoder.cn") + private String audioUrl; + + @Schema(description = "视频地址", example = "https://www.iocoder.cn") + private String videoUrl; + + @Schema(description = "音乐状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer status; + + @Schema(description = "描述词", example = "一首轻快的歌曲") + private String gptDescriptionPrompt; + + @Schema(description = "提示词", example = "创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。") + private String prompt; + + @Schema(description = "模型平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "Suno") + private String platform; + + @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "chirp-v3.5") + private String model; + + @Schema(description = "生成模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer generateMode; + + @Schema(description = "音乐风格标签") + private List tags; + + @Schema(description = "音乐时长", example = "[\"pop\",\"jazz\",\"punk\"]") + private Double duration; + + @Schema(description = "是否发布", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean publicStatus; + + @Schema(description = "任务编号", example = "11369") + private String taskId; + + @Schema(description = "错误信息") + private String errorMessage; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateMyReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateMyReqVO.java new file mode 100644 index 000000000..457670115 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateMyReqVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.ai.controller.admin.music.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - AI 修改我的音乐 Request VO") +@Data +public class AiMusicUpdateMyReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "音乐名称", example = "夜空中最亮的星") + private String title; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java new file mode 100644 index 000000000..447bc9765 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiMusicUpdateReqVO.java @@ -0,0 +1,18 @@ +package cn.iocoder.yudao.module.ai.controller.admin.music.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - AI 音乐修改 Request VO") +@Data +public class AiMusicUpdateReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15583") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "是否发布", example = "true") + private Boolean publicStatus; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java new file mode 100644 index 000000000..f72d2b54a --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/music/vo/AiSunoGenerateReqVO.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.ai.controller.admin.music.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - AI 音乐生成 Request VO") +@Data +public class AiSunoGenerateReqVO { + + @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "Suno") + @NotBlank(message = "平台不能为空") + private String platform; // 参见 AiPlatformEnum 枚举 + + /** + * 1. 描述模式:描述词 + 是否纯音乐 + 模型 + * 2. 歌词模式:歌词 + 音乐风格 + 标题 + 模型 + */ + @Schema(description = "生成模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "生成模式不能为空") + private Integer generateMode; // 参见 AiMusicGenerateModeEnum 枚举 + + @Schema(description = "用于生成音乐音频的歌词提示", + example = """ + 1.描述模式:创作一首带有轻松吉他旋律的流行歌曲,[verse] 描述夏日海滩的宁静,[chorus] 节奏加快,表达对自由的向往。 + 2.歌词模式: + [Verse] + 阳光下奔跑 多么欢快 + 假期就要来 心都飞起来 + 朋友在一旁 笑声又灿烂 + 无忧无虑的 每一天甜蜜 + [Chorus] + 马上放假了 快来庆祝 + 一起去旅行 快去冒险 + 日子太短暂 别再等待 + 马上放假了 梦想起飞 + """) + private String prompt; + + @Schema(description = "是否纯音乐", example = "true") + private Boolean makeInstrumental; + + @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "chirp-v3.5") + @NotEmpty(message = "模型不能为空") + private String model; + + @Schema(description = "音乐风格", example = "[\"pop\",\"jazz\",\"punk\"]") + private List tags; + + @Schema(description = "音乐/歌曲名称", example = "夜空中最亮的星") + private String title; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java new file mode 100644 index 000000000..d27204d21 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/AiWriteController.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.ai.controller.admin.write; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteGenerateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWritePageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteRespVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.write.AiWriteDO; +import cn.iocoder.yudao.module.ai.service.write.AiWriteService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import jakarta.annotation.security.PermitAll; +import jakarta.validation.Valid; +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - AI 写作") +@RestController +@RequestMapping("/ai/write") +public class AiWriteController { + + @Resource + private AiWriteService writeService; + + @PostMapping(value = "/generate-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + @Operation(summary = "写作生成(流式)", description = "流式返回,响应较快") + @PermitAll // 解决 SSE 最终响应的时候,会被 Access Denied 拦截的问题 + public Flux> generateWriteContent(@RequestBody @Valid AiWriteGenerateReqVO generateReqVO) { + return writeService.generateWriteContent(generateReqVO, getLoginUserId()); + } + + // ================ 写作管理 ================ + + @DeleteMapping("/delete") + @Operation(summary = "删除写作") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('ai:write:delete')") + public CommonResult deleteWrite(@RequestParam("id") Long id) { + writeService.deleteWrite(id); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获得写作分页") + @PreAuthorize("@ss.hasPermission('ai:write:query')") + public CommonResult> getWritePage(@Valid AiWritePageReqVO pageReqVO) { + PageResult pageResult = writeService.getWritePage(pageReqVO); + return success(BeanUtils.toBean(pageResult, AiWriteRespVO.class)); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteGenerateReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteGenerateReqVO.java new file mode 100644 index 000000000..21c60420d --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteGenerateReqVO.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.module.ai.controller.admin.write.vo; + +import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +@Schema(description = "管理后台 - AI 写作生成 Request VO") +@Data +public class AiWriteGenerateReqVO { + + @Schema(description = "写作类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(value = AiWriteTypeEnum.class, message = "写作类型必须是 {value}") + private Integer type; + + @Schema(description = "写作内容提示", example = "1.撰写:田忌赛马;2.回复:不批") + private String prompt; + + @Schema(description = "原文", example = "领导我要辞职") + private String originalContent; + + @Schema(description = "长度", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "长度不能为空") + private Integer length; + + @Schema(description = "格式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "格式不能为空") + private Integer format; + + @Schema(description = "语气", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "语气不能为空") + private Integer tone; + + @Schema(description = "语言", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "语言不能为空") + private Integer language; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java new file mode 100644 index 000000000..047380e42 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWritePageReqVO.java @@ -0,0 +1,33 @@ +package cn.iocoder.yudao.module.ai.controller.admin.write.vo; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - AI 写作分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AiWritePageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "28404") + private Long userId; + + @Schema(description = "写作类型", example = "1") + private Integer type; + + @Schema(description = "平台", example = "TongYi") + private String platform; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteRespVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteRespVO.java new file mode 100644 index 000000000..4160de9ad --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/write/vo/AiWriteRespVO.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.ai.controller.admin.write.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - AI 写作 Response VO") +@Data +public class AiWriteRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5311") + private Long id; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "28404") + private Long userId; + + @Schema(description = "写作类型", example = "1") + private Integer type; + + @Schema(description = "平台", requiredMode = Schema.RequiredMode.REQUIRED, example = "TongYi") + private String platform; + + @Schema(description = "模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "qwen") + private String model; + + @Schema(description = "生成内容提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "撰写:田忌赛马") + private String prompt; + + @Schema(description = "生成的内容", example = "你非常不错") + private String generatedContent; + + @Schema(description = "原文", example = "真的么?") + private String originalContent; + + @Schema(description = "长度提示词", example = "1") + private Integer length; + + @Schema(description = "格式提示词", example = "2") + private Integer format; + + @Schema(description = "语气提示词", example = "3") + private Integer tone; + + @Schema(description = "语言提示词", example = "4") + private Integer language; + + @Schema(description = "错误信息") + private String errorMessage; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/app/package-info.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/app/package-info.java new file mode 100644 index 000000000..05b1ce233 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/app/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO 芋艿:站位,无特殊作用 + */ +package cn.iocoder.yudao.module.ai.controller.app; \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/package-info.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/package-info.java new file mode 100644 index 000000000..68dfddfa3 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 yudao-ui-admin 前端项目 + * 2. app 包:提供给用户 APP yudao-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package cn.iocoder.yudao.module.ai.controller; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java new file mode 100644 index 000000000..0b7eb0233 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatConversationDO.java @@ -0,0 +1,99 @@ +package cn.iocoder.yudao.module.ai.dal.dataobject.chat; + +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * AI Chat 对话 DO + * + * 用户每次发起 Chat 聊天时,会创建一个 {@link AiChatConversationDO} 对象,将它的消息关联在一起 + * + * @author fansili + * @since 2024/4/14 17:35 + */ +@TableName("ai_chat_conversation") +@KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiChatConversationDO extends BaseDO { + + public static final String TITLE_DEFAULT = "新对话"; + + /** + * ID 编号,自增 + */ + @TableId + private Long id; + + /** + * 用户编号 + * + * 关联 AdminUserDO 的 userId 字段 + */ + private Long userId; + + /** + * 对话标题 + * + * 默认由系统自动生成,可用户手动修改 + */ + private String title; + /** + * 是否置顶 + */ + private Boolean pinned; + /** + * 置顶时间 + */ + private LocalDateTime pinnedTime; + + /** + * 角色编号 + * + * 关联 {@link AiChatRoleDO#getId()} + */ + private Long roleId; + + /** + * 模型编号 + * + * 关联 {@link AiChatModelDO#getId()} 字段 + */ + private Long modelId; + /** + * 模型标志 + */ + private String model; + + // ========== 对话配置 ========== + + /** + * 角色设定 + */ + private String systemMessage; + /** + * 温度参数 + * + * 用于调整生成回复的随机性和多样性程度:较低的温度值会使输出更收敛于高频词汇,较高的则增加多样性 + */ + private Double temperature; + /** + * 单条回复的最大 Token 数量 + */ + private Integer maxTokens; + /** + * 上下文的最大 Message 数量 + */ + private Integer maxContexts; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java new file mode 100644 index 000000000..973c593ce --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java @@ -0,0 +1,90 @@ +package cn.iocoder.yudao.module.ai.dal.dataobject.chat; + +import com.baomidou.mybatisplus.annotation.TableId; +import org.springframework.ai.chat.messages.MessageType; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * AI Chat 消息 DO + * + * @since 2024/4/14 17:35 + * @since 2024/4/14 17:35 + */ +@TableName("ai_chat_message") +@KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiChatMessageDO extends BaseDO { + + /** + * 编号,作为每条聊天记录的唯一标识符 + */ + @TableId + private Long id; + + /** + * 对话编号 + * + * 关联 {@link AiChatConversationDO#getId()} 字段 + */ + private Long conversationId; + /** + * 回复消息编号 + * + * 关联 {@link #id} 字段 + * + * 大模型回复的消息编号,用于“问答”的关联 + */ + private Long replyId; + + /** + * 消息类型 + * + * 也等价于 OpenAPI 的 role 字段 + * + * 枚举 {@link MessageType} + */ + private String type; + /** + * 用户编号 + * + * 关联 AdminUserDO 的 userId 字段 + */ + private Long userId; + /** + * 角色编号 + * + * 关联 {@link AiChatRoleDO#getId()} 字段 + */ + private Long roleId; + + /** + * 模型标志 + */ + private String model; + /** + * 模型编号 + * + * 关联 {@link AiChatModelDO#getId()} 字段 + */ + private Long modelId; + + /** + * 聊天内容 + */ + private String content; + + /** + * 是否携带上下文 + */ + private Boolean useContext; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java new file mode 100644 index 000000000..6768d904b --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/image/AiImageDO.java @@ -0,0 +1,135 @@ +package cn.iocoder.yudao.module.ai.dal.dataobject.image; + +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import org.springframework.ai.openai.OpenAiImageOptions; +import org.springframework.ai.stabilityai.api.StabilityAiImageOptions; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * AI 绘画 DO + * + * @author fansili + */ +@TableName(value = "ai_image", autoResultMap = true) +@Data +public class AiImageDO extends BaseDO { + + /** + * 编号 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户编号 + * + * 关联 {@link AdminUserRespDTO#getId()} + */ + private Long userId; + + /** + * 提示词 + */ + private String prompt; + + /** + * 平台 + * + * 枚举 {@link cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum} + */ + private String platform; + /** + * 模型 + * + * 冗余 {@link AiChatModelDO#getModel()} + */ + private String model; + + /** + * 图片宽度 + */ + private Integer width; + /** + * 图片高度 + */ + private Integer height; + + /** + * 生成状态 + * + * 枚举 {@link AiImageStatusEnum} + */ + private Integer status; + + /** + * 完成时间 + */ + private LocalDateTime finishTime; + + /** + * 绘画错误信息 + */ + private String errorMessage; + + /** + * 图片地址 + */ + private String picUrl; + /** + * 是否公开 + */ + private Boolean publicStatus; + + /** + * 绘制参数,不同 platform 的不同参数 + * + * 1. {@link OpenAiImageOptions} + * 2. {@link StabilityAiImageOptions} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map options; + + /** + * mj buttons 按钮 + */ + @TableField(typeHandler = ButtonTypeHandler.class) + private List buttons; + + /** + * 任务编号 + * + * 1. midjourney proxy:关联的 task id + */ + private String taskId; + + public static class ButtonTypeHandler extends AbstractJsonTypeHandler { + + @Override + protected Object parse(String json) { + return JsonUtils.parseArray(json, MidjourneyApi.Button.class); + } + + @Override + protected String toJson(Object obj) { + return JsonUtils.toJsonString(obj); + } + + } + +} + diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java new file mode 100644 index 000000000..0442a52d7 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/mindmap/AiMindMapDO.java @@ -0,0 +1,58 @@ +package cn.iocoder.yudao.module.ai.dal.dataobject.mindmap; + +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * AI 思维导图 DO + * + * @author xiaoxin + */ +@TableName(value = "ai_mind_map") +@Data +public class AiMindMapDO extends BaseDO { + + /** + * 编号 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户编号 + *

+ * 关联 AdminUserDO 的 userId 字段 + */ + private Long userId; + + /** + * 平台 + *

+ * 枚举 {@link AiPlatformEnum} + */ + private String platform; + /** + * 模型 + */ + private String model; + + /** + * 生成内容提示 + */ + private String prompt; + + /** + * 生成的内容 + */ + private String generatedContent; + + /** + * 错误信息 + */ + private String errorMessage; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiApiKeyDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiApiKeyDO.java new file mode 100644 index 000000000..e251d55c8 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiApiKeyDO.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.module.ai.dal.dataobject.model; + +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * AI API 秘钥 DO + * + * @author 芋道源码 + */ +@TableName("ai_api_key") +@KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiApiKeyDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 名称 + */ + private String name; + /** + * 密钥 + */ + private String apiKey; + /** + * 平台 + * + * 枚举 {@link AiPlatformEnum} + */ + private String platform; + /** + * API 地址 + */ + private String url; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatModelDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatModelDO.java new file mode 100644 index 000000000..7197f8b58 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatModelDO.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.ai.dal.dataobject.model; + +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * AI 聊天模型 DO + * + * 默认聊天模型:{@link #status} 为开启,并且 {@link #sort} 排序第一 + * + * @author fansili + * @since 2024/4/24 19:39 + */ +@TableName("ai_chat_model") +@KeySequence("ai_chat_model_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiChatModelDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * API 秘钥编号 + * + * 关联 {@link AiApiKeyDO#getId()} + */ + private Long keyId; + /** + * 模型名称 + */ + private String name; + /** + * 模型标志 + */ + private String model; + /** + * 平台 + * + * 枚举 {@link AiPlatformEnum} + */ + private String platform; + + /** + * 排序值 + */ + private Integer sort; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + + // ========== 对话配置 ========== + + /** + * 温度参数 + * + * 用于调整生成回复的随机性和多样性程度:较低的温度值会使输出更收敛于高频词汇,较高的则增加多样性 + */ + private Double temperature; + /** + * 单条回复的最大 Token 数量 + */ + private Integer maxTokens; + /** + * 上下文的最大 Message 数量 + */ + private Integer maxContexts; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java new file mode 100644 index 000000000..28f6cda43 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.module.ai.dal.dataobject.model; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.*; +import lombok.*; + +/** + * AI 聊天角色 DO + * + * @author fansili + * @since 2024/4/24 19:39 + */ +@TableName(value = "ai_chat_role", autoResultMap = true) +@KeySequence("ai_chat_role_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiChatRoleDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 角色名称 + */ + private String name; + /** + * 角色头像 + */ + private String avatar; + /** + * 角色分类 + */ + private String category; + /** + * 角色描述 + */ + private String description; + /** + * 角色设定 + */ + private String systemMessage; + + /** + * 用户编号 + * + * 关联 AdminUserDO 的 userId 字段 + */ + private Long userId; + + /** + * 模型编号 + * + * 关联 {@link AiChatModelDO#getId()} 字段 + */ + private Long modelId; + + /** + * 是否公开 + * + * 1. true - 公开;由管理员在【角色管理】所创建 + * 2. false - 私有;由个人在【我的角色】所创建 + */ + private Boolean publicStatus; + + /** + * 排序值 + */ + private Integer sort; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java new file mode 100644 index 000000000..8a6cbe828 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/music/AiMusicDO.java @@ -0,0 +1,117 @@ +package cn.iocoder.yudao.module.ai.dal.dataobject.music; + +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum; +import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; + +import java.util.List; + +/** + * AI 音乐 DO + * + * @author xiaoxin + */ +@TableName(value = "ai_music", autoResultMap = true) +@Data +public class AiMusicDO extends BaseDO { + + /** + * 编号 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户编号 + *

+ * 关联 AdminUserDO 的 userId 字段 + */ + private Long userId; + + /** + * 音乐名称 + */ + private String title; + + /** + * 歌词 + */ + private String lyric; + + /** + * 图片地址 + */ + private String imageUrl; + /** + * 音频地址 + */ + private String audioUrl; + /** + * 视频地址 + */ + private String videoUrl; + + /** + * 音乐状态 + *

+ * 枚举 {@link AiMusicStatusEnum} + */ + private Integer status; + + /** + * 生成模式 + *

+ * 枚举 {@link AiMusicGenerateModeEnum} + */ + private Integer generateMode; + + /** + * 描述词 + */ + private String description; + + /** + * 平台 + *

+ * 枚举 {@link AiPlatformEnum} + */ + private String platform; + /** + * 模型 + */ + private String model; + + /** + * 音乐风格标签 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List tags; + + /** + * 音乐时长 + */ + private Double duration; + + /** + * 是否公开 + */ + private Boolean publicStatus; + + /** + * 任务编号 + */ + private String taskId; + + /** + * 错误信息 + */ + private String errorMessage; + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java new file mode 100644 index 000000000..752876f2a --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/write/AiWriteDO.java @@ -0,0 +1,95 @@ +package cn.iocoder.yudao.module.ai.dal.dataobject.write; + +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * AI 写作 DO + * + * @author xiaoxin + */ +@TableName("ai_write") +@Data +public class AiWriteDO extends BaseDO { + + /** + * 编号 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户编号 + * + * 关联 AdminUserDO 的 userId 字段 + */ + private Long userId; + + /** + * 写作类型 + *

+ * 枚举 {@link AiWriteTypeEnum} + */ + private Integer type; + + /** + * 平台 + * + * 枚举 {@link AiPlatformEnum} + */ + private String platform; + /** + * 模型 + */ + private String model; + + /** + * 生成内容提示 + */ + private String prompt; + + /** + * 生成的内容 + */ + private String generatedContent; + /** + * 原文 + */ + private String originalContent; + + /** + * 长度提示词 + * + * 字典:{@link cn.iocoder.yudao.module.ai.enums.DictTypeConstants#AI_WRITE_LENGTH} + */ + private Integer length; + /** + * 格式提示词 + * + * 字典:{@link cn.iocoder.yudao.module.ai.enums.DictTypeConstants#AI_WRITE_FORMAT} + */ + private Integer format; + /** + * 语气提示词 + * + * 字典:{@link cn.iocoder.yudao.module.ai.enums.DictTypeConstants#AI_WRITE_TONE} + */ + private Integer tone; + /** + * 语言提示词 + * + * 字典:{@link cn.iocoder.yudao.module.ai.enums.DictTypeConstants#AI_WRITE_LANGUAGE} + */ + private Integer language; + + /** + * 错误信息 + */ + private String errorMessage; + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/chat/AiChatConversationMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/chat/AiChatConversationMapper.java new file mode 100644 index 000000000..ce9da2f24 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/chat/AiChatConversationMapper.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.chat; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationPageReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * AI 聊天对话 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface AiChatConversationMapper extends BaseMapperX { + + default List selectListByUserId(Long userId) { + return selectList(AiChatConversationDO::getUserId, userId); + } + + default List selectListByUserIdAndPinned(Long userId, boolean pinned) { + return selectList(new LambdaQueryWrapperX() + .eq(AiChatConversationDO::getUserId, userId) + .eq(AiChatConversationDO::getPinned, pinned)); + } + + default PageResult selectChatConversationPage(AiChatConversationPageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eqIfPresent(AiChatConversationDO::getUserId, pageReqVO.getUserId()) + .likeIfPresent(AiChatConversationDO::getTitle, pageReqVO.getTitle()) + .betweenIfPresent(AiChatConversationDO::getCreateTime, pageReqVO.getCreateTime()) + .orderByDesc(AiChatConversationDO::getId)); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/chat/AiChatMessageMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/chat/AiChatMessageMapper.java new file mode 100644 index 000000000..5020f3944 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/chat/AiChatMessageMapper.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.chat; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * AI 聊天对话 Mapper + * + * @author fansili + */ +@Mapper +public interface AiChatMessageMapper extends BaseMapperX { + + default List selectListByConversationId(Long conversationId) { + return selectList(new LambdaQueryWrapperX() + .eq(AiChatMessageDO::getConversationId, conversationId) + .orderByAsc(AiChatMessageDO::getId)); + } + + default Map selectCountMapByConversationId(Collection conversationIds) { + // SQL count 查询 + List> result = selectMaps(new QueryWrapper() + .select("COUNT(id) AS count, conversation_id AS conversationId") + .in("conversation_id", conversationIds) + .groupBy("conversation_id")); + if (CollUtil.isEmpty(result)) { + return Collections.emptyMap(); + } + // 转换数据 + return CollectionUtils.convertMap(result, + record -> MapUtil.getLong(record, "conversationId"), + record -> MapUtil.getInt(record, "count" )); + } + + default PageResult selectPage(AiChatMessagePageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eqIfPresent(AiChatMessageDO::getConversationId, pageReqVO.getConversationId()) + .eqIfPresent(AiChatMessageDO::getUserId, pageReqVO.getUserId()) + .likeIfPresent(AiChatMessageDO::getContent, pageReqVO.getContent()) + .betweenIfPresent(AiChatMessageDO::getCreateTime, pageReqVO.getCreateTime()) + .orderByDesc(AiChatMessageDO::getId)); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java new file mode 100644 index 000000000..fd6e4b398 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/image/AiImageMapper.java @@ -0,0 +1,46 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.image; + +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * AI 绘图 Mapper + * + * @author fansili + */ +@Mapper +public interface AiImageMapper extends BaseMapperX { + + default AiImageDO selectByTaskId(String taskId) { + return this.selectOne(AiImageDO::getTaskId, taskId); + } + + default PageResult selectPage(AiImagePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(AiImageDO::getUserId, reqVO.getUserId()) + .eqIfPresent(AiImageDO::getPlatform, reqVO.getPlatform()) + .eqIfPresent(AiImageDO::getStatus, reqVO.getStatus()) + .eqIfPresent(AiImageDO::getPublicStatus, reqVO.getPublicStatus()) + .betweenIfPresent(AiImageDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(AiImageDO::getId)); + } + + default PageResult selectPage(Long userId, PageParam pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eq(AiImageDO::getUserId, userId) + .orderByDesc(AiImageDO::getId)); + } + + default List selectListByStatusAndPlatform(Integer status, String platform) { + return selectList(AiImageDO::getStatus, status, + AiImageDO::getPlatform, platform); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/mindmap/AiMindMapMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/mindmap/AiMindMapMapper.java new file mode 100644 index 000000000..ff25e89ff --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/mindmap/AiMindMapMapper.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.mindmap; + +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * AI 思维导图 Mapper + * + * @author xiaoxin + */ +@Mapper +public interface AiMindMapMapper extends BaseMapperX { +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiApiKeyMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiApiKeyMapper.java new file mode 100644 index 000000000..0a2efe36f --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiApiKeyMapper.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.model; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * AI API 密钥 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface AiApiKeyMapper extends BaseMapperX { + + default PageResult selectPage(AiApiKeyPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(AiApiKeyDO::getName, reqVO.getName()) + .eqIfPresent(AiApiKeyDO::getPlatform, reqVO.getPlatform()) + .eqIfPresent(AiApiKeyDO::getStatus, reqVO.getStatus()) + .orderByDesc(AiApiKeyDO::getId)); + } + + default AiApiKeyDO selectFirstByPlatformAndStatus(String platform, Integer status) { + return selectOne(new QueryWrapperX() + .eq("platform", platform) + .eq("status", status) + .limitN(1) + .orderByAsc("id")); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatModelMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatModelMapper.java new file mode 100644 index 000000000..a3136fa9f --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatModelMapper.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.model; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelPageReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * API 聊天模型 Mapper + * + * @author fansili + */ +@Mapper +public interface AiChatModelMapper extends BaseMapperX { + + default AiChatModelDO selectFirstByStatus(Integer status) { + return selectOne(new QueryWrapperX() + .eq("status", status) + .limitN(1) + .orderByAsc("sort")); + } + + default PageResult selectPage(AiChatModelPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(AiChatModelDO::getName, reqVO.getName()) + .eqIfPresent(AiChatModelDO::getModel, reqVO.getModel()) + .eqIfPresent(AiChatModelDO::getPlatform, reqVO.getPlatform()) + .orderByAsc(AiChatModelDO::getSort)); + } + + default List selectList(Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(AiChatModelDO::getStatus, status) + .orderByAsc(AiChatModelDO::getSort)); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatRoleMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatRoleMapper.java new file mode 100644 index 000000000..ed91edf3f --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/model/AiChatRoleMapper.java @@ -0,0 +1,54 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.model; + +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRolePageReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * AI 聊天角色 Mapper + * + * @author fansili + */ +@Mapper +public interface AiChatRoleMapper extends BaseMapperX { + + default PageResult selectPage(AiChatRolePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(AiChatRoleDO::getName, reqVO.getName()) + .eqIfPresent(AiChatRoleDO::getCategory, reqVO.getCategory()) + .eqIfPresent(AiChatRoleDO::getPublicStatus, reqVO.getPublicStatus()) + .orderByAsc(AiChatRoleDO::getSort)); + } + + default PageResult selectPageByMy(AiChatRolePageReqVO reqVO, Long userId) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(AiChatRoleDO::getName, reqVO.getName()) + .eqIfPresent(AiChatRoleDO::getCategory, reqVO.getCategory()) + // 情况一:公开 + .eq(Boolean.TRUE.equals(reqVO.getPublicStatus()), AiChatRoleDO::getPublicStatus, reqVO.getPublicStatus()) + // 情况二:私有 + .eq(Boolean.FALSE.equals(reqVO.getPublicStatus()), AiChatRoleDO::getUserId, userId) + .eq(Boolean.FALSE.equals(reqVO.getPublicStatus()), AiChatRoleDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) + .orderByAsc(AiChatRoleDO::getSort)); + } + + default List selectListGroupByCategory(Integer status) { + return selectList(new LambdaQueryWrapperX() + .select(AiChatRoleDO::getCategory) + .eq(AiChatRoleDO::getStatus, status) + .groupBy(AiChatRoleDO::getCategory)); + } + + default List selectListByName(String name) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(AiChatRoleDO::getName, name) + .orderByAsc(AiChatRoleDO::getSort)); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/music/AiMusicMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/music/AiMusicMapper.java new file mode 100644 index 000000000..025f5e018 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/music/AiMusicMapper.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.music; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * AI 音乐 Mapper + * + * @author xiaoxin + */ +@Mapper +public interface AiMusicMapper extends BaseMapperX { + + default List selectListByStatus(Integer status) { + return selectList(AiMusicDO::getStatus, status); + } + + default PageResult selectPage(AiMusicPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(AiMusicDO::getUserId, reqVO.getUserId()) + .eqIfPresent(AiMusicDO::getTitle, reqVO.getTitle()) + .eqIfPresent(AiMusicDO::getStatus, reqVO.getStatus()) + .eqIfPresent(AiMusicDO::getGenerateMode, reqVO.getGenerateMode()) + .betweenIfPresent(AiMusicDO::getCreateTime, reqVO.getCreateTime()) + .eqIfPresent(AiMusicDO::getPublicStatus, reqVO.getPublicStatus()) + .orderByDesc(AiMusicDO::getId)); + } + + default PageResult selectPageByMy(AiMusicPageReqVO reqVO, Long userId) { + return selectPage(reqVO, new LambdaQueryWrapperX() + // 情况一:公开 + .eq(Boolean.TRUE.equals(reqVO.getPublicStatus()), AiMusicDO::getPublicStatus, reqVO.getPublicStatus()) + // 情况二:私有 + .eq(Boolean.FALSE.equals(reqVO.getPublicStatus()), AiMusicDO::getUserId, userId) + .orderByAsc(AiMusicDO::getId)); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/write/AiWriteMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/write/AiWriteMapper.java new file mode 100644 index 000000000..c4983eb44 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/write/AiWriteMapper.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.ai.dal.mysql.write; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWritePageReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.write.AiWriteDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * AI 写作 Mapper + * + * @author xiaoxin + */ +@Mapper +public interface AiWriteMapper extends BaseMapperX { + + default PageResult selectPage(AiWritePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(AiWriteDO::getUserId, reqVO.getUserId()) + .eqIfPresent(AiWriteDO::getType, reqVO.getType()) + .eqIfPresent(AiWriteDO::getPlatform, reqVO.getPlatform()) + .betweenIfPresent(AiWriteDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(AiWriteDO::getId)); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/package-info.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/package-info.java new file mode 100644 index 000000000..a4452f290 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 ai 模块的 framework 封装 + * + * @author 芋道源码 + */ +package cn.iocoder.yudao.module.ai.framework; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/rpc/config/RpcConfiguration.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/rpc/config/RpcConfiguration.java new file mode 100644 index 000000000..a28a64cf4 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/rpc/config/RpcConfiguration.java @@ -0,0 +1,11 @@ +package cn.iocoder.yudao.module.ai.framework.rpc.config; + +import cn.iocoder.yudao.module.infra.api.file.FileApi; +import cn.iocoder.yudao.module.system.api.dict.DictDataApi; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableFeignClients(clients = {DictDataApi.class, FileApi.class}) +public class RpcConfiguration { +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/rpc/package-info.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/rpc/package-info.java new file mode 100644 index 000000000..3e2bce96b --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/rpc/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.ai.framework.rpc; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java new file mode 100644 index 000000000..84a92692c --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.module.ai.framework.security.config; + +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; +import cn.iocoder.yudao.module.infra.enums.ApiConstants; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; + +/** + * AI 模块的 Security 配置 + */ +@Configuration(proxyBeanMethods = false, value = "aiSecurityConfiguration") +public class SecurityConfiguration { + + + @Bean("infraAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(AuthorizeHttpRequestsConfigurer.AuthorizationManagerRequestMatcherRegistry registry) { + // Swagger 接口文档 + registry.requestMatchers("/v3/api-docs/**").permitAll() // 元数据 + .requestMatchers("/swagger-ui.html").permitAll(); // Swagger UI + // Spring Boot Actuator 的安全配置 + registry.requestMatchers("/actuator").permitAll() + .requestMatchers("/actuator/**").permitAll(); + // Druid 监控 + registry.requestMatchers("/druid/**").permitAll(); + + // TODO 芋艿:这个每个项目都需要重复配置,得捉摸有没通用的方案 + // RPC 服务的安全配置 + registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll(); + } + + }; + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java new file mode 100644 index 000000000..87969449d --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package cn.iocoder.yudao.module.ai.framework.security.core; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/image/AiMidjourneySyncJob.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/image/AiMidjourneySyncJob.java new file mode 100644 index 000000000..242d82a21 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/image/AiMidjourneySyncJob.java @@ -0,0 +1,27 @@ +package cn.iocoder.yudao.module.ai.job.image; + +import cn.iocoder.yudao.module.ai.service.image.AiImageService; +import com.xxl.job.core.handler.annotation.XxlJob; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * Midjourney 同步 Job:定时拉去 midjourney 绘制状态 + * + * @author fansili + */ +@Component +@Slf4j +public class AiMidjourneySyncJob { + + @Resource + private AiImageService imageService; + + @XxlJob("aiMidjourneySyncJob") + public void execute(String param) { + Integer count = imageService.midjourneySync(); + log.info("[execute][同步 Midjourney ({}) 个]", count); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/music/AiSunoSyncJob.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/music/AiSunoSyncJob.java new file mode 100644 index 000000000..ca17a46cc --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/job/music/AiSunoSyncJob.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.ai.job.music; + +import cn.iocoder.yudao.module.ai.service.music.AiMusicService; +import com.xxl.job.core.handler.annotation.XxlJob; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + + +/** + * 同步 Suno 任务状态以及回写对应的音乐信息 Job + * + * @author xiaoxin + */ +@Component +@Slf4j +public class AiSunoSyncJob { + + @Resource + private AiMusicService musicService; + + @XxlJob("aiSunoSyncJob") + public void execute(String param) { + Integer count = musicService.syncMusic(); + log.info("[execute][同步 Suno ({}) 个]", count); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/package-info.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/package-info.java new file mode 100644 index 000000000..0983f04fe --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/package-info.java @@ -0,0 +1,10 @@ +/** + * ai 模块下,接入 LLM 大模型,支持聊天、绘图、音乐、写作、思维脑图等功能。 + * 目前已接入各种模型,不限于: + * 国内:通义千问、文心一言、讯飞星火、智谱 GLM、DeepSeek + * 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno + * + * 1. Controller URL:以 /ai/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 ai_ 开头,方便在数据库中区分 + */ +package cn.iocoder.yudao.module.ai; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationService.java new file mode 100644 index 000000000..bce6d435d --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationService.java @@ -0,0 +1,90 @@ +package cn.iocoder.yudao.module.ai.service.chat; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationCreateMyReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationUpdateMyReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageRespVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; + +import java.util.List; + +/** + * AI 聊天对话 Service 接口 + * + * @author fansili + */ +public interface AiChatConversationService { + + /** + * 创建【我的】聊天对话 + * + * @param createReqVO 创建信息 + * @param userId 用户编号 + * @return 编号 + */ + Long createChatConversationMy(AiChatConversationCreateMyReqVO createReqVO, Long userId); + + /** + * 更新【我的】聊天对话 + * + * @param updateReqVO 更新信息 + * @param userId 用户编号 + */ + void updateChatConversationMy(AiChatConversationUpdateMyReqVO updateReqVO, Long userId); + + /** + * 获得【我的】聊天对话列表 + * + * @param userId 用户编号 + * @return 聊天对话列表 + */ + List getChatConversationListByUserId(Long userId); + + /** + * 获得聊天对话 + * + * @param id 编号 + * @return 聊天对话 + */ + AiChatConversationDO getChatConversation(Long id); + + /** + * 删除【我的】聊天对话 + * + * @param id 编号 + * @param userId 用户编号 + */ + void deleteChatConversationMy(Long id, Long userId); + + /** + * 【管理员】删除聊天对话 + * + * @param id 编号 + */ + void deleteChatConversationByAdmin(Long id); + + /** + * 校验聊天对话是否存在 + * + * @param id 编号 + * @return 聊天对话 + */ + AiChatConversationDO validateChatConversationExists(Long id); + + /** + * 删除【我的】 + 非置顶的聊天对话 + * + * @param userId 用户编号 + */ + void deleteChatConversationMyByUnpinned(Long userId); + + /** + * 获得聊天对话的分页列表 + * + * @param pageReqVO 分页查询 + * @return 聊天对话的分页列表 + */ + PageResult getChatConversationPage(AiChatConversationPageReqVO pageReqVO); + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java new file mode 100644 index 000000000..83dcd8dff --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatConversationServiceImpl.java @@ -0,0 +1,157 @@ +package cn.iocoder.yudao.module.ai.service.chat; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationCreateMyReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatConversationUpdateMyReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatConversationMapper; +import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; +import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_CONVERSATION_MODEL_ERROR; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_CONVERSATION_NOT_EXISTS; + +/** + * AI 聊天对话 Service 实现类 + * + * @author fansili + */ +@Service +@Validated +@Slf4j +public class AiChatConversationServiceImpl implements AiChatConversationService { + + @Resource + private AiChatConversationMapper chatConversationMapper; + + @Resource + private AiChatModelService chatModalService; + @Resource + private AiChatRoleService chatRoleService; + + @Override + public Long createChatConversationMy(AiChatConversationCreateMyReqVO createReqVO, Long userId) { + // 1.1 获得 AiChatRoleDO 聊天角色 + AiChatRoleDO role = createReqVO.getRoleId() != null ? chatRoleService.validateChatRole(createReqVO.getRoleId()) : null; + // 1.2 获得 AiChatModelDO 聊天模型 + AiChatModelDO model = role != null && role.getModelId() != null ? chatModalService.validateChatModel(role.getModelId()) + : chatModalService.getRequiredDefaultChatModel(); + Assert.notNull(model, "必须找到默认模型"); + validateChatModel(model); + + // 2. 创建 AiChatConversationDO 聊天对话 + AiChatConversationDO conversation = new AiChatConversationDO().setUserId(userId).setPinned(false) + .setModelId(model.getId()).setModel(model.getModel()) + .setTemperature(model.getTemperature()).setMaxTokens(model.getMaxTokens()).setMaxContexts(model.getMaxContexts()); + if (role != null) { + conversation.setTitle(role.getName()).setRoleId(role.getId()).setSystemMessage(role.getSystemMessage()); + } else { + conversation.setTitle(AiChatConversationDO.TITLE_DEFAULT); + } + chatConversationMapper.insert(conversation); + return conversation.getId(); + } + + @Override + public void updateChatConversationMy(AiChatConversationUpdateMyReqVO updateReqVO, Long userId) { + // 1.1 校验对话是否存在 + AiChatConversationDO conversation = validateChatConversationExists(updateReqVO.getId()); + if (ObjUtil.notEqual(conversation.getUserId(), userId)) { + throw exception(CHAT_CONVERSATION_NOT_EXISTS); + } + // 1.2 校验模型是否存在(修改模型的情况) + AiChatModelDO model = null; + if (updateReqVO.getModelId() != null) { + model = chatModalService.validateChatModel(updateReqVO.getModelId()); + } + + // 2. 更新对话信息 + AiChatConversationDO updateObj = BeanUtils.toBean(updateReqVO, AiChatConversationDO.class); + if (Boolean.TRUE.equals(updateReqVO.getPinned())) { + updateObj.setPinnedTime(LocalDateTime.now()); + } + if (model != null) { + updateObj.setModel(model.getModel()); + } + chatConversationMapper.updateById(updateObj); + } + + @Override + public List getChatConversationListByUserId(Long userId) { + return chatConversationMapper.selectListByUserId(userId); + } + + @Override + public AiChatConversationDO getChatConversation(Long id) { + return chatConversationMapper.selectById(id); + } + + @Override + public void deleteChatConversationMy(Long id, Long userId) { + // 1. 校验对话是否存在 + AiChatConversationDO conversation = validateChatConversationExists(id); + if (conversation == null || ObjUtil.notEqual(conversation.getUserId(), userId)) { + throw exception(CHAT_CONVERSATION_NOT_EXISTS); + } + // 2. 执行删除 + chatConversationMapper.deleteById(id); + } + + @Override + public void deleteChatConversationByAdmin(Long id) { + // 1. 校验对话是否存在 + AiChatConversationDO conversation = validateChatConversationExists(id); + if (conversation == null) { + throw exception(CHAT_CONVERSATION_NOT_EXISTS); + } + // 2. 执行删除 + chatConversationMapper.deleteById(id); + } + + private void validateChatModel(AiChatModelDO model) { + if (ObjectUtil.isAllNotEmpty(model.getTemperature(), model.getMaxTokens(), model.getMaxContexts())) { + return; + } + throw exception(CHAT_CONVERSATION_MODEL_ERROR); + } + + public AiChatConversationDO validateChatConversationExists(Long id) { + AiChatConversationDO conversation = chatConversationMapper.selectById(id); + if (conversation == null) { + throw exception(CHAT_CONVERSATION_NOT_EXISTS); + } + return conversation; + } + + @Override + public void deleteChatConversationMyByUnpinned(Long userId) { + List list = chatConversationMapper.selectListByUserIdAndPinned(userId, false); + if (CollUtil.isEmpty(list)) { + return; + } + chatConversationMapper.deleteBatchIds(convertList(list, AiChatConversationDO::getId)); + } + + @Override + public PageResult getChatConversationPage(AiChatConversationPageReqVO pageReqVO) { + return chatConversationMapper.selectChatConversationPage(pageReqVO); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java new file mode 100644 index 000000000..f572bddd9 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageService.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.module.ai.service.chat; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO; +import reactor.core.publisher.Flux; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * AI 聊天消息 Service 接口 + * + * @author fansili + */ +public interface AiChatMessageService { + + /** + * 发送消息 + * + * @param sendReqVO 发送信息 + * @param userId 用户编号 + * @return 发送结果 + */ + AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId); + + /** + * 发送消息 + * + * @param sendReqVO 发送信息 + * @param userId 用户编号 + * @return 发送结果 + */ + Flux> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, Long userId); + + /** + * 获得指定对话的消息列表 + * + * @param conversationId 对话编号 + * @return 消息列表 + */ + List getChatMessageListByConversationId(Long conversationId); + + /** + * 删除消息 + * + * @param id 消息编号 + * @param userId 用户编号 + */ + void deleteChatMessage(Long id, Long userId); + + /** + * 删除指定对话的消息 + * + * @param conversationId 对话编号 + * @param userId 用户编号 + */ + void deleteChatMessageByConversationId(Long conversationId, Long userId); + + /** + * 【管理员】删除消息 + * + * @param id 消息编号 + */ + void deleteChatMessageByAdmin(Long id); + + /** + * 获得聊天对话的消息数量 Map + * + * @param conversationIds 对话编号数组 + * @return 消息数量 Map + */ + Map getChatMessageCountMap(Collection conversationIds); + + /** + * 获得聊天消息的分页 + * + * @param pageReqVO 分页查询 + * @return 聊天消息的分页 + */ + PageResult getChatMessagePage(AiChatMessagePageReqVO pageReqVO); + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java new file mode 100644 index 000000000..72fa06a79 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java @@ -0,0 +1,259 @@ +package cn.iocoder.yudao.module.ai.service.chat; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.ai.core.util.AiUtils; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper; +import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; +import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.messages.*; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.model.StreamingChatModel; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import reactor.core.publisher.Flux; + +import java.time.LocalDateTime; +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_CONVERSATION_NOT_EXISTS; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_NOT_EXIST; + +/** + * AI 聊天消息 Service 实现类 + * + * @author fansili + */ +@Service +@Slf4j +public class AiChatMessageServiceImpl implements AiChatMessageService { + + @Resource + private AiChatMessageMapper chatMessageMapper; + + @Resource + private AiChatConversationService chatConversationService; + @Resource + private AiChatModelService chatModalService; + @Resource + private AiApiKeyService apiKeyService; + + @Transactional(rollbackFor = Exception.class) + public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) { + // 1.1 校验对话存在 + AiChatConversationDO conversation = chatConversationService.validateChatConversationExists(sendReqVO.getConversationId()); + if (ObjUtil.notEqual(conversation.getUserId(), userId)) { + throw exception(CHAT_CONVERSATION_NOT_EXISTS); + } + List historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId()); + // 1.2 校验模型 + AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId()); + ChatModel chatModel = apiKeyService.getChatModel(model.getKeyId()); + + // 2. 插入 user 发送消息 + AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, + userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext()); + + // 3.1 插入 assistant 接收消息 + AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, + userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext()); + + // 3.2 创建 chat 需要的 Prompt + Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO); + ChatResponse chatResponse = chatModel.call(prompt); + + // 3.3 段式返回 + String newContent = chatResponse.getResult().getOutput().getContent(); + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent)); + return new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) + .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent)); + } + + @Override + public Flux> sendChatMessageStream(AiChatMessageSendReqVO sendReqVO, Long userId) { + // 1.1 校验对话存在 + AiChatConversationDO conversation = chatConversationService.validateChatConversationExists(sendReqVO.getConversationId()); + if (ObjUtil.notEqual(conversation.getUserId(), userId)) { + throw exception(CHAT_CONVERSATION_NOT_EXISTS); + } + List historyMessages = chatMessageMapper.selectListByConversationId(conversation.getId()); + // 1.2 校验模型 + AiChatModelDO model = chatModalService.validateChatModel(conversation.getModelId()); + StreamingChatModel chatModel = apiKeyService.getChatModel(model.getKeyId()); + + // 2. 插入 user 发送消息 + AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, + userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext()); + + // 3.1 插入 assistant 接收消息 + AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, + userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext()); + + // 3.2 构建 Prompt,并进行调用 + Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO); + Flux streamResponse = chatModel.stream(prompt); + + // 3.3 流式返回 + // TODO 注意:Schedulers.immediate() 目的是,避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题 + StringBuffer contentBuffer = new StringBuffer(); + return streamResponse.map(chunk -> { + String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null; + newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况 + contentBuffer.append(newContent); + // 响应结果 + return success(new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) + .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent))); + }).doOnComplete(() -> { + // 忽略租户,因为 Flux 异步无法透传租户 + TenantUtils.executeIgnore(() -> + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString()))); + }).doOnError(throwable -> { + log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable); + // 忽略租户,因为 Flux 异步无法透传租户 + TenantUtils.executeIgnore(() -> + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage()))); + }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR))); + } + + private Prompt buildPrompt(AiChatConversationDO conversation, List messages, + AiChatModelDO model, AiChatMessageSendReqVO sendReqVO) { + // 1. 构建 Prompt Message 列表 + List chatMessages = new ArrayList<>(); + // 1.1 system context 角色设定 + if (StrUtil.isNotBlank(conversation.getSystemMessage())) { + chatMessages.add(new SystemMessage(conversation.getSystemMessage())); + } + // 1.2 history message 历史消息 + List contextMessages = filterContextMessages(messages, conversation, sendReqVO); + contextMessages.forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent()))); + // 1.3 user message 新发送消息 + chatMessages.add(new UserMessage(sendReqVO.getContent())); + + // 2. 构建 ChatOptions 对象 + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); + ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), + conversation.getTemperature(), conversation.getMaxTokens()); + return new Prompt(chatMessages, chatOptions); + } + + /** + * 从历史消息中,获得倒序的 n 组消息作为消息上下文 + * + * n 组:指的是 user + assistant 形成一组 + * + * @param messages 消息列表 + * @param conversation 对话 + * @param sendReqVO 发送请求 + * @return 消息上下文 + */ + private List filterContextMessages(List messages, + AiChatConversationDO conversation, + AiChatMessageSendReqVO sendReqVO) { + if (conversation.getMaxContexts() == null || ObjUtil.notEqual(sendReqVO.getUseContext(), Boolean.TRUE)) { + return Collections.emptyList(); + } + List contextMessages = new ArrayList<>(conversation.getMaxContexts() * 2); + for (int i = messages.size() - 1; i >= 0; i--) { + AiChatMessageDO assistantMessage = CollUtil.get(messages, i); + if (assistantMessage == null || assistantMessage.getReplyId() == null) { + continue; + } + AiChatMessageDO userMessage = CollUtil.get(messages, i - 1); + if (userMessage == null || ObjUtil.notEqual(assistantMessage.getReplyId(), userMessage.getId()) + || StrUtil.isEmpty(assistantMessage.getContent())) { + continue; + } + // 由于后续要 reverse 反转,所以先添加 assistantMessage + contextMessages.add(assistantMessage); + contextMessages.add(userMessage); + // 超过最大上下文,结束 + if (contextMessages.size() >= conversation.getMaxContexts() * 2) { + break; + } + } + Collections.reverse(contextMessages); + return contextMessages; + } + + private AiChatMessageDO createChatMessage(Long conversationId, Long replyId, + AiChatModelDO model, Long userId, Long roleId, + MessageType messageType, String content, Boolean useContext) { + AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId) + .setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId) + .setType(messageType.getValue()).setContent(content).setUseContext(useContext); + message.setCreateTime(LocalDateTime.now()); + chatMessageMapper.insert(message); + return message; + } + + @Override + public List getChatMessageListByConversationId(Long conversationId) { + return chatMessageMapper.selectListByConversationId(conversationId); + } + + @Override + public void deleteChatMessage(Long id, Long userId) { + // 1. 校验消息存在 + AiChatMessageDO message = chatMessageMapper.selectById(id); + if (message == null || ObjUtil.notEqual(message.getUserId(), userId)) { + throw exception(CHAT_MESSAGE_NOT_EXIST); + } + // 2. 执行删除 + chatMessageMapper.deleteById(id); + } + + @Override + public void deleteChatMessageByConversationId(Long conversationId, Long userId) { + // 1. 校验消息存在 + List messages = chatMessageMapper.selectListByConversationId(conversationId); + if (CollUtil.isEmpty(messages) || ObjUtil.notEqual(messages.get(0).getUserId(), userId)) { + throw exception(CHAT_MESSAGE_NOT_EXIST); + } + // 2. 执行删除 + chatMessageMapper.deleteBatchIds(convertList(messages, AiChatMessageDO::getId)); + } + + @Override + public void deleteChatMessageByAdmin(Long id) { + // 1. 校验消息存在 + AiChatMessageDO message = chatMessageMapper.selectById(id); + if (message == null) { + throw exception(CHAT_MESSAGE_NOT_EXIST); + } + // 2. 执行删除 + chatMessageMapper.deleteById(id); + } + + @Override + public Map getChatMessageCountMap(Collection conversationIds) { + return chatMessageMapper.selectCountMapByConversationId(conversationIds); + } + + @Override + public PageResult getChatMessagePage(AiChatMessagePageReqVO pageReqVO) { + return chatMessageMapper.selectPage(pageReqVO); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java new file mode 100644 index 000000000..716c7ea8a --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageService.java @@ -0,0 +1,121 @@ +package cn.iocoder.yudao.module.ai.service.image; + +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * AI 绘图 Service 接口 + * + * @author fansili + */ +public interface AiImageService { + + /** + * 获取【我的】绘图分页 + * + * @param userId 用户编号 + * @param pageReqVO 分页条件 + * @return 绘图分页 + */ + PageResult getImagePageMy(Long userId, PageParam pageReqVO); + + /** + * 获得绘图记录 + * + * @param id 绘图编号 + * @return 绘图记录 + */ + AiImageDO getImage(Long id); + + /** + * 获得绘图列表 + * + * @param ids 绘图编号数组 + * @return 绘图记录列表 + */ + List getImageList(List ids); + + /** + * 绘制图片 + * + * @param userId 用户编号 + * @param drawReqVO 绘制请求 + * @return 绘画编号 + */ + Long drawImage(Long userId, AiImageDrawReqVO drawReqVO); + + /** + * 删除【我的】绘画记录 + * + * @param id 绘画编号 + * @param userId 用户编号 + */ + void deleteImageMy(Long id, Long userId); + + /** + * 获得绘画分页 + * + * @param pageReqVO 分页查询 + * @return 绘画分页 + */ + PageResult getImagePage(AiImagePageReqVO pageReqVO); + + /** + * 更新绘画 + * + * @param updateReqVO 更新信息 + */ + void updateImage(@Valid AiImageUpdateReqVO updateReqVO); + + /** + * 删除绘画 + * + * @param id 编号 + */ + void deleteImage(Long id); + + // ================ midjourney 专属 ================ + + /** + * 【Midjourney】生成图片 + * + * @param userId 用户编号 + * @param reqVO 绘制请求 + * @return 绘画编号 + */ + Long midjourneyImagine(Long userId, AiMidjourneyImagineReqVO reqVO); + + /** + * 【Midjourney】同步图片进展 + * + * @return 同步成功数量 + */ + Integer midjourneySync(); + + /** + * 【Midjourney】通知图片进展 + * + * @param notify 通知 + */ + void midjourneyNotify(MidjourneyApi.Notify notify); + + /** + * 【Midjourney】Action 操作(放大、缩小、U1、U2...) + * + * @param userId 用户编号 + * @param reqVO 绘制请求 + * @return 绘画编号 + */ + Long midjourneyAction(Long userId, AiMidjourneyActionReqVO reqVO); + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java new file mode 100644 index 000000000..3a8ff8346 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java @@ -0,0 +1,350 @@ +package cn.iocoder.yudao.module.ai.service.image; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.codec.Base64; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.http.HttpUtil; +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImagePageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyActionReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.image.vo.midjourney.AiMidjourneyImagineReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO; +import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper; +import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum; +import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; +import cn.iocoder.yudao.module.infra.api.file.FileApi; +import com.alibaba.cloud.ai.tongyi.image.TongYiImagesOptions; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.image.ImageModel; +import org.springframework.ai.image.ImageOptions; +import org.springframework.ai.image.ImagePrompt; +import org.springframework.ai.image.ImageResponse; +import org.springframework.ai.openai.OpenAiImageOptions; +import org.springframework.ai.qianfan.QianFanImageOptions; +import org.springframework.ai.stabilityai.api.StabilityAiImageOptions; +import org.springframework.ai.zhipuai.ZhiPuAiImageOptions; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; + +/** + * AI 绘画 Service 实现类 + * + * @author fansili + */ +@Service +@Slf4j +public class AiImageServiceImpl implements AiImageService { + + @Resource + private AiImageMapper imageMapper; + + @Resource + private FileApi fileApi; + + @Resource + private AiApiKeyService apiKeyService; + + @Override + public PageResult getImagePageMy(Long userId, PageParam pageReqVO) { + return imageMapper.selectPage(userId, pageReqVO); + } + + @Override + public AiImageDO getImage(Long id) { + return imageMapper.selectById(id); + } + + @Override + public List getImageList(List ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return imageMapper.selectBatchIds(ids); + } + + @Override + public Long drawImage(Long userId, AiImageDrawReqVO drawReqVO) { + // 1. 保存数据库 + AiImageDO image = BeanUtils.toBean(drawReqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false) + .setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus()); + imageMapper.insert(image); + // 2. 异步绘制,后续前端通过返回的 id 进行轮询结果 + getSelf().executeDrawImage(image, drawReqVO); + return image.getId(); + } + + @Async + public void executeDrawImage(AiImageDO image, AiImageDrawReqVO req) { + try { + // 1.1 构建请求 + ImageOptions request = buildImageOptions(req); + // 1.2 执行请求 + ImageModel imageModel = apiKeyService.getImageModel(AiPlatformEnum.validatePlatform(req.getPlatform())); + ImageResponse response = imageModel.call(new ImagePrompt(req.getPrompt(), request)); + + // 2. 上传到文件服务 + String b64Json = response.getResult().getOutput().getB64Json(); + byte[] fileContent = StrUtil.isNotEmpty(b64Json) ? Base64.decode(b64Json) + : HttpUtil.downloadBytes(response.getResult().getOutput().getUrl()); + String filePath = fileApi.createFile(fileContent); + + // 3. 更新数据库 + imageMapper.updateById(new AiImageDO().setId(image.getId()).setStatus(AiImageStatusEnum.SUCCESS.getStatus()) + .setPicUrl(filePath).setFinishTime(LocalDateTime.now())); + } catch (Exception ex) { + log.error("[doDall][image({}) 生成异常]", image, ex); + imageMapper.updateById(new AiImageDO().setId(image.getId()) + .setStatus(AiImageStatusEnum.FAIL.getStatus()) + .setErrorMessage(ex.getMessage()).setFinishTime(LocalDateTime.now())); + } + } + + private static ImageOptions buildImageOptions(AiImageDrawReqVO draw) { + if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.OPENAI.getPlatform())) { + // https://platform.openai.com/docs/api-reference/images/create + return OpenAiImageOptions.builder().withModel(draw.getModel()) + .withHeight(draw.getHeight()).withWidth(draw.getWidth()) + .withStyle(MapUtil.getStr(draw.getOptions(), "style")) // 风格 + .withResponseFormat("b64_json") + .build(); + } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.STABLE_DIFFUSION.getPlatform())) { + // https://platform.stability.ai/docs/api-reference#tag/SDXL-and-SD1.6/operation/textToImage + // https://platform.stability.ai/docs/api-reference#tag/Text-to-Image/operation/textToImage + return StabilityAiImageOptions.builder().withModel(draw.getModel()) + .withHeight(draw.getHeight()).withWidth(draw.getWidth()) + .withSeed(Long.valueOf(draw.getOptions().get("seed"))) + .withCfgScale(Float.valueOf(draw.getOptions().get("scale"))) + .withSteps(Integer.valueOf(draw.getOptions().get("steps"))) + .withSampler(String.valueOf(draw.getOptions().get("sampler"))) + .withStylePreset(String.valueOf(draw.getOptions().get("stylePreset"))) + .withClipGuidancePreset(String.valueOf(draw.getOptions().get("clipGuidancePreset"))) + .build(); + } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.TONG_YI.getPlatform())) { + return TongYiImagesOptions.builder() + .withModel(draw.getModel()).withN(1) + .withHeight(draw.getHeight()).withWidth(draw.getWidth()) + .build(); + } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) { + return QianFanImageOptions.builder() + .withModel(draw.getModel()).withN(1) + .withHeight(draw.getHeight()).withWidth(draw.getWidth()) + .build(); + } else if (ObjUtil.equal(draw.getPlatform(), AiPlatformEnum.ZHI_PU.getPlatform())) { + return ZhiPuAiImageOptions.builder() + .withModel(draw.getModel()) + .build(); + } + throw new IllegalArgumentException("不支持的 AI 平台:" + draw.getPlatform()); + } + + @Override + public void deleteImageMy(Long id, Long userId) { + // 1. 校验是否存在 + AiImageDO image = validateImageExists(id); + if (ObjUtil.notEqual(image.getUserId(), userId)) { + throw exception(IMAGE_NOT_EXISTS); + } + // 2. 删除记录 + imageMapper.deleteById(id); + } + + @Override + public PageResult getImagePage(AiImagePageReqVO pageReqVO) { + return imageMapper.selectPage(pageReqVO); + } + + @Override + public void updateImage(AiImageUpdateReqVO updateReqVO) { + // 1. 校验存在 + validateImageExists(updateReqVO.getId()); + // 2. 更新发布状态 + imageMapper.updateById(BeanUtils.toBean(updateReqVO, AiImageDO.class)); + } + + @Override + public void deleteImage(Long id) { + // 1. 校验存在 + validateImageExists(id); + // 2. 删除 + imageMapper.deleteById(id); + } + + private AiImageDO validateImageExists(Long id) { + AiImageDO image = imageMapper.selectById(id); + if (image == null) { + throw exception(IMAGE_NOT_EXISTS); + } + return image; + } + + // ================ midjourney 专属 ================ + + @Override + @Transactional(rollbackFor = Exception.class) + public Long midjourneyImagine(Long userId, AiMidjourneyImagineReqVO reqVO) { + MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi(); + // 1. 保存数据库 + AiImageDO image = BeanUtils.toBean(reqVO, AiImageDO.class).setUserId(userId).setPublicStatus(false) + .setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus()) + .setPlatform(AiPlatformEnum.MIDJOURNEY.getPlatform()); + imageMapper.insert(image); + + // 2. 调用 Midjourney Proxy 提交任务 + List base64Array = StrUtil.isBlank(reqVO.getReferImageUrl()) ? null : + Collections.singletonList("data:image/jpeg;base64,".concat(Base64.encode(HttpUtil.downloadBytes(reqVO.getReferImageUrl())))); + MidjourneyApi.ImagineRequest imagineRequest = new MidjourneyApi.ImagineRequest( + base64Array, reqVO.getPrompt(),null, + MidjourneyApi.ImagineRequest.buildState(reqVO.getWidth(), + reqVO.getHeight(), reqVO.getVersion(), reqVO.getModel())); + MidjourneyApi.SubmitResponse imagineResponse = midjourneyApi.imagine(imagineRequest); + + // 3. 情况一【失败】:抛出业务异常 + if (!MidjourneyApi.SubmitCodeEnum.SUCCESS_CODES.contains(imagineResponse.code())) { + String description = imagineResponse.description().contains("quota_not_enough") ? + "账户余额不足" : imagineResponse.description(); + throw exception(IMAGE_MIDJOURNEY_SUBMIT_FAIL, description); + } + + // 4. 情况二【成功】:更新 taskId 和参数 + imageMapper.updateById(new AiImageDO().setId(image.getId()) + .setTaskId(imagineResponse.result()).setOptions(BeanUtil.beanToMap(reqVO))); + return image.getId(); + } + + @Override + public Integer midjourneySync() { + MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi(); + // 1.1 获取 Midjourney 平台,状态在 “进行中” 的 image + List imageList = imageMapper.selectListByStatusAndPlatform( + AiImageStatusEnum.IN_PROGRESS.getStatus(), AiPlatformEnum.MIDJOURNEY.getPlatform()); + if (CollUtil.isEmpty(imageList)) { + return 0; + } + // 1.2 调用 Midjourney Proxy 获取任务进展 + List taskList = midjourneyApi.getTaskList(convertSet(imageList, AiImageDO::getTaskId)); + Map taskMap = convertMap(taskList, MidjourneyApi.Notify::id); + + // 2. 逐个处理,更新进展 + int count = 0; + for (AiImageDO image : imageList) { + MidjourneyApi.Notify notify = taskMap.get(image.getTaskId()); + if (notify == null) { + log.error("[midjourneySync][image({}) 查询不到进展]", image); + continue; + } + count++; + updateMidjourneyStatus(image, notify); + } + return count; + } + + @Override + public void midjourneyNotify(MidjourneyApi.Notify notify) { + // 1. 校验 image 存在 + AiImageDO image = imageMapper.selectByTaskId(notify.id()); + if (image == null) { + log.warn("[midjourneyNotify][回调任务({}) 不存在]", notify.id()); + return; + } + // 2. 更新状态 + updateMidjourneyStatus(image, notify); + } + + private void updateMidjourneyStatus(AiImageDO image, MidjourneyApi.Notify notify) { + // 1. 转换状态 + Integer status = null; + LocalDateTime finishTime = null; + if (StrUtil.isNotBlank(notify.status())) { + MidjourneyApi.TaskStatusEnum taskStatusEnum = MidjourneyApi.TaskStatusEnum.valueOf(notify.status()); + if (MidjourneyApi.TaskStatusEnum.SUCCESS == taskStatusEnum) { + status = AiImageStatusEnum.SUCCESS.getStatus(); + finishTime = LocalDateTime.now(); + } else if (MidjourneyApi.TaskStatusEnum.FAILURE == taskStatusEnum) { + status = AiImageStatusEnum.FAIL.getStatus(); + finishTime = LocalDateTime.now(); + } + } + + // 2. 上传图片 + String picUrl = null; + if (StrUtil.isNotBlank(notify.imageUrl())) { + try { + picUrl = fileApi.createFile(HttpUtil.downloadBytes(notify.imageUrl())); + } catch (Exception e) { + picUrl = notify.imageUrl(); + log.warn("[updateMidjourneyStatus][图片({}) 地址({}) 上传失败]", image.getId(), notify.imageUrl(), e); + } + } + + // 3. 更新 image 状态 + imageMapper.updateById(new AiImageDO().setId(image.getId()).setStatus(status) + .setPicUrl(picUrl).setButtons(notify.buttons()).setErrorMessage(notify.failReason()) + .setFinishTime(finishTime)); + } + + @Override + public Long midjourneyAction(Long userId, AiMidjourneyActionReqVO reqVO) { + MidjourneyApi midjourneyApi = apiKeyService.getMidjourneyApi(); + // 1.1 检查 image + AiImageDO image = validateImageExists(reqVO.getId()); + if (ObjUtil.notEqual(userId, image.getUserId())) { + throw exception(IMAGE_NOT_EXISTS); + } + // 1.2 检查 customId + MidjourneyApi.Button button = CollUtil.findOne(image.getButtons(), + buttonX -> buttonX.customId().equals(reqVO.getCustomId())); + if (button == null) { + throw exception(IMAGE_CUSTOM_ID_NOT_EXISTS); + } + + // 2. 调用 Midjourney Proxy 提交任务 + MidjourneyApi.SubmitResponse actionResponse = midjourneyApi.action( + new MidjourneyApi.ActionRequest(button.customId(), image.getTaskId(), null)); + if (!MidjourneyApi.SubmitCodeEnum.SUCCESS_CODES.contains(actionResponse.code())) { + String description = actionResponse.description().contains("quota_not_enough") ? + "账户余额不足" : actionResponse.description(); + throw exception(IMAGE_MIDJOURNEY_SUBMIT_FAIL, description); + } + + // 3. 新增 image 记录 + AiImageDO newImage = new AiImageDO().setUserId(image.getUserId()).setPublicStatus(false).setPrompt(image.getPrompt()) + .setStatus(AiImageStatusEnum.IN_PROGRESS.getStatus()) + .setPlatform(AiPlatformEnum.MIDJOURNEY.getPlatform()) + .setModel(image.getModel()).setWidth(image.getWidth()).setHeight(image.getHeight()) + .setOptions(image.getOptions()).setTaskId(actionResponse.result()); + imageMapper.insert(newImage); + return newImage.getId(); + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private AiImageServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java new file mode 100644 index 000000000..2eb1f1b1a --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapService.java @@ -0,0 +1,23 @@ +package cn.iocoder.yudao.module.ai.service.mindmap; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO; +import reactor.core.publisher.Flux; + +/** + * AI 思维导图 Service 接口 + * + * @author xiaoxin + */ +public interface AiMindMapService { + + /** + * 生成思维导图内容 + * + * @param generateReqVO 请求参数 + * @param userId 用户编号 + * @return 生成结果 + */ + Flux> generateMindMap(AiMindMapGenerateReqVO generateReqVO, Long userId); + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java new file mode 100644 index 000000000..72be20c54 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/mindmap/AiMindMapServiceImpl.java @@ -0,0 +1,134 @@ +package cn.iocoder.yudao.module.ai.service.mindmap; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.ai.core.util.AiUtils; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.ai.controller.admin.mindmap.vo.AiMindMapGenerateReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.mindmap.AiMindMapDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import cn.iocoder.yudao.module.ai.dal.mysql.mindmap.AiMindMapMapper; +import cn.iocoder.yudao.module.ai.enums.AiChatRoleEnum; +import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; +import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; +import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +/** + * AI 思维导图 Service 实现类 + * + * @author xiaoxin + */ +@Service +@Slf4j +public class AiMindMapServiceImpl implements AiMindMapService { + + @Resource + private AiApiKeyService apiKeyService; + @Resource + private AiChatModelService chatModalService; + @Resource + private AiChatRoleService chatRoleService; + + @Resource + private AiMindMapMapper mindMapMapper; + + @Override + public Flux> generateMindMap(AiMindMapGenerateReqVO generateReqVO, Long userId) { + // 1. 获取脑图模型。尝试获取思维导图助手角色,如果没有则使用默认模型 + AiChatRoleDO role = CollUtil.getFirst( + chatRoleService.getChatRoleListByName(AiChatRoleEnum.AI_MIND_MAP_ROLE.getName())); + // 1.1 获取脑图执行模型 + AiChatModelDO model = getModel(role); + // 1.2 获取角色设定消息 + String systemMessage = role != null && StrUtil.isNotBlank(role.getSystemMessage()) + ? role.getSystemMessage() : AiChatRoleEnum.AI_MIND_MAP_ROLE.getSystemMessage(); + // 1.3 校验平台 + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); + ChatModel chatModel = apiKeyService.getChatModel(model.getKeyId()); + + // 2. 插入思维导图信息 + AiMindMapDO mindMapDO = BeanUtils.toBean(generateReqVO, AiMindMapDO.class, + mindMap -> mindMap.setUserId(userId).setModel(model.getModel()).setPlatform(platform.getPlatform())); + mindMapMapper.insert(mindMapDO); + + // 3.1 构建 Prompt,并进行调用 + Prompt prompt = buildPrompt(generateReqVO, model, systemMessage); + Flux streamResponse = chatModel.stream(prompt); + + // 3.2 流式返回 + StringBuffer contentBuffer = new StringBuffer(); + return streamResponse.map(chunk -> { + String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null; + newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况 + contentBuffer.append(newContent); + // 响应结果 + return success(newContent); + }).doOnComplete(() -> { + // 忽略租户,因为 Flux 异步无法透传租户 + TenantUtils.executeIgnore(() -> + mindMapMapper.updateById(new AiMindMapDO().setId(mindMapDO.getId()).setGeneratedContent(contentBuffer.toString()))); + }).doOnError(throwable -> { + log.error("[generateWriteContent][generateReqVO({}) 发生异常]", generateReqVO, throwable); + // 忽略租户,因为 Flux 异步无法透传租户 + TenantUtils.executeIgnore(() -> + mindMapMapper.updateById(new AiMindMapDO().setId(mindMapDO.getId()).setErrorMessage(throwable.getMessage()))); + }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.WRITE_STREAM_ERROR))); + + } + + private Prompt buildPrompt(AiMindMapGenerateReqVO generateReqVO, AiChatModelDO model, String systemMessage) { + // 1. 构建 message 列表 + List chatMessages = buildMessages(generateReqVO, systemMessage); + // 2. 构建 options 对象 + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); + ChatOptions options = AiUtils.buildChatOptions(platform, model.getModel(), model.getTemperature(), model.getMaxTokens()); + return new Prompt(chatMessages, options); + } + + private static List buildMessages(AiMindMapGenerateReqVO generateReqVO, String systemMessage) { + List chatMessages = new ArrayList<>(); + // 1. 角色设定 + if (StrUtil.isNotBlank(systemMessage)) { + chatMessages.add(new SystemMessage(systemMessage)); + } + // 2. 用户输入 + chatMessages.add(new UserMessage(generateReqVO.getPrompt())); + return chatMessages; + } + + private AiChatModelDO getModel(AiChatRoleDO role) { + AiChatModelDO model = null; + if (role != null && role.getModelId() != null) { + model = chatModalService.getChatModel(role.getModelId()); + } + if (model != null) { + model = chatModalService.getRequiredDefaultChatModel(); + } + Assert.notNull(model, "[AI] 获取不到模型"); + return model; + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java new file mode 100644 index 000000000..fe8fdd194 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyService.java @@ -0,0 +1,114 @@ +package cn.iocoder.yudao.module.ai.service.model; + +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; +import jakarta.validation.Valid; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.image.ImageModel; + +import java.util.List; + +/** + * AI API 密钥 Service 接口 + * + * @author 芋道源码 + */ +public interface AiApiKeyService { + + /** + * 创建 API 密钥 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createApiKey(@Valid AiApiKeySaveReqVO createReqVO); + + /** + * 更新 API 密钥 + * + * @param updateReqVO 更新信息 + */ + void updateApiKey(@Valid AiApiKeySaveReqVO updateReqVO); + + /** + * 删除 API 密钥 + * + * @param id 编号 + */ + void deleteApiKey(Long id); + + /** + * 获得 API 密钥 + * + * @param id 编号 + * @return API 密钥 + */ + AiApiKeyDO getApiKey(Long id); + + /** + * 校验 API 密钥 + * + * @param id 比那好 + * @return API 密钥 + */ + AiApiKeyDO validateApiKey(Long id); + + /** + * 获得 API 密钥分页 + * + * @param pageReqVO 分页查询 + * @return API 密钥分页 + */ + PageResult getApiKeyPage(AiApiKeyPageReqVO pageReqVO); + + /** + * 获得 API 密钥列表 + * + * @return API 密钥列表 + */ + List getApiKeyList(); + + // ========== 与 spring-ai 集成 ========== + + /** + * 获得 ChatModel 对象 + * + * @param id 编号 + * @return ChatModel 对象 + */ + ChatModel getChatModel(Long id); + + /** + * 获得 ImageModel 对象 + * + * TODO 可优化点:目前默认获取 platform 对应的第一个开启的配置用于绘画;后续可以支持配置选择 + * + * @param platform 平台 + * @return ImageModel 对象 + */ + ImageModel getImageModel(AiPlatformEnum platform); + + /** + * 获得 MidjourneyApi 对象 + * + * TODO 可优化点:目前默认获取 Midjourney 对应的第一个开启的配置用于绘画;后续可以支持配置选择 + * + * @return MidjourneyApi 对象 + */ + MidjourneyApi getMidjourneyApi(); + + /** + * 获得 SunoApi 对象 + * + * TODO 可优化点:目前默认获取 Suno 对应的第一个开启的配置用于音乐;后续可以支持配置选择 + * + * @return SunoApi 对象 + */ + SunoApi getSunoApi(); + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java new file mode 100644 index 000000000..590b10a4c --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiApiKeyServiceImpl.java @@ -0,0 +1,135 @@ +package cn.iocoder.yudao.module.ai.service.model; + +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeyPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.apikey.AiApiKeySaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; +import cn.iocoder.yudao.module.ai.dal.mysql.model.AiApiKeyMapper; +import jakarta.annotation.Resource; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.image.ImageModel; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; + +/** + * AI API 密钥 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class AiApiKeyServiceImpl implements AiApiKeyService { + + @Resource + private AiApiKeyMapper apiKeyMapper; + + @Resource + private AiModelFactory modelFactory; + + @Override + public Long createApiKey(AiApiKeySaveReqVO createReqVO) { + // 插入 + AiApiKeyDO apiKey = BeanUtils.toBean(createReqVO, AiApiKeyDO.class); + apiKeyMapper.insert(apiKey); + // 返回 + return apiKey.getId(); + } + + @Override + public void updateApiKey(AiApiKeySaveReqVO updateReqVO) { + // 校验存在 + validateApiKeyExists(updateReqVO.getId()); + // 更新 + AiApiKeyDO updateObj = BeanUtils.toBean(updateReqVO, AiApiKeyDO.class); + apiKeyMapper.updateById(updateObj); + } + + @Override + public void deleteApiKey(Long id) { + // 校验存在 + validateApiKeyExists(id); + // 删除 + apiKeyMapper.deleteById(id); + } + + private AiApiKeyDO validateApiKeyExists(Long id) { + AiApiKeyDO apiKey = apiKeyMapper.selectById(id); + if (apiKey == null) { + throw exception(API_KEY_NOT_EXISTS); + } + return apiKey; + } + + @Override + public AiApiKeyDO getApiKey(Long id) { + return apiKeyMapper.selectById(id); + } + + @Override + public AiApiKeyDO validateApiKey(Long id) { + AiApiKeyDO apiKey = validateApiKeyExists(id); + if (CommonStatusEnum.isDisable(apiKey.getStatus())) { + throw exception(API_KEY_DISABLE); + } + return apiKey; + } + + @Override + public PageResult getApiKeyPage(AiApiKeyPageReqVO pageReqVO) { + return apiKeyMapper.selectPage(pageReqVO); + } + + @Override + public List getApiKeyList() { + return apiKeyMapper.selectList(); + } + + // ========== 与 spring-ai 集成 ========== + + @Override + public ChatModel getChatModel(Long id) { + AiApiKeyDO apiKey = validateApiKey(id); + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); + return modelFactory.getOrCreateChatModel(platform, apiKey.getApiKey(), apiKey.getUrl()); + } + + @Override + public ImageModel getImageModel(AiPlatformEnum platform) { + AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus(platform.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); + if (apiKey == null) { + throw exception(API_KEY_IMAGE_NODE_FOUND, platform.getName()); + } + return modelFactory.getOrCreateImageModel(platform, apiKey.getApiKey(), apiKey.getUrl()); + } + + @Override + public MidjourneyApi getMidjourneyApi() { + AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus( + AiPlatformEnum.MIDJOURNEY.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); + if (apiKey == null) { + throw exception(API_KEY_MIDJOURNEY_NOT_FOUND); + } + return modelFactory.getOrCreateMidjourneyApi(apiKey.getApiKey(), apiKey.getUrl()); + } + + @Override + public SunoApi getSunoApi() { + AiApiKeyDO apiKey = apiKeyMapper.selectFirstByPlatformAndStatus( + AiPlatformEnum.SUNO.getPlatform(), CommonStatusEnum.ENABLE.getStatus()); + if (apiKey == null) { + throw exception(API_KEY_SUNO_NOT_FOUND); + } + return modelFactory.getOrCreateSunoApi(apiKey.getApiKey(), apiKey.getUrl()); + } +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelService.java new file mode 100644 index 000000000..f83ac73c9 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelService.java @@ -0,0 +1,92 @@ +package cn.iocoder.yudao.module.ai.service.model; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; + +import java.util.Set; + +/** + * AI 聊天模型 Service 接口 + * + * @author fansili + * @since 2024/4/24 19:42 + */ +public interface AiChatModelService { + + /** + * 创建聊天模型 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createChatModel(@Valid AiChatModelSaveReqVO createReqVO); + + /** + * 更新聊天模型 + * + * @param updateReqVO 更新信息 + */ + void updateChatModel(@Valid AiChatModelSaveReqVO updateReqVO); + + /** + * 删除聊天模型 + * + * @param id 编号 + */ + void deleteChatModel(Long id); + + /** + * 获得聊天模型 + * + * @param id 编号 + * @return 聊天模型 + */ + AiChatModelDO getChatModel(Long id); + + /** + * 获得默认的聊天模型 + * + * 如果获取不到,则抛出 {@link cn.iocoder.yudao.framework.common.exception.ServiceException} 业务异常 + * + * @return 聊天模型 + */ + AiChatModelDO getRequiredDefaultChatModel(); + + /** + * 获得聊天模型分页 + * + * @param pageReqVO 分页查询 + * @return 聊天模型分页 + */ + PageResult getChatModelPage(AiChatModelPageReqVO pageReqVO); + + /** + * 校验聊天模型 + * + * @param id 编号 + * @return 聊天模型 + */ + AiChatModelDO validateChatModel(Long id); + + /** + * 获得聊天模型列表 + * + * @param status 状态 + * @return 聊天模型列表 + */ + List getChatModelListByStatus(Integer status); + + /** + * 获得聊天模型列表 + * + * @param ids 编号数组 + * @return 模型列表 + */ + List getChatModelList(Collection ids); +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java new file mode 100644 index 000000000..4b11602f5 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatModelServiceImpl.java @@ -0,0 +1,114 @@ +package cn.iocoder.yudao.module.ai.service.model; + +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatModel.AiChatModelSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.mysql.model.AiChatModelMapper; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.Collection; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; + +/** + * AI 聊天模型 Service 实现类 + * + * @author fansili + */ +@Service +@Validated +public class AiChatModelServiceImpl implements AiChatModelService { + + @Resource + private AiApiKeyService apiKeyService; + + @Resource + private AiChatModelMapper chatModelMapper; + + @Override + public Long createChatModel(AiChatModelSaveReqVO createReqVO) { + // 1. 校验 + AiPlatformEnum.validatePlatform(createReqVO.getPlatform()); + apiKeyService.validateApiKey(createReqVO.getKeyId()); + + // 2. 插入 + AiChatModelDO chatModel = BeanUtils.toBean(createReqVO, AiChatModelDO.class); + chatModelMapper.insert(chatModel); + return chatModel.getId(); + } + + @Override + public void updateChatModel(AiChatModelSaveReqVO updateReqVO) { + // 1. 校验 + validateChatModelExists(updateReqVO.getId()); + AiPlatformEnum.validatePlatform(updateReqVO.getPlatform()); + apiKeyService.validateApiKey(updateReqVO.getKeyId()); + + // 2. 更新 + AiChatModelDO updateObj = BeanUtils.toBean(updateReqVO, AiChatModelDO.class); + chatModelMapper.updateById(updateObj); + } + + @Override + public void deleteChatModel(Long id) { + // 校验存在 + validateChatModelExists(id); + // 删除 + chatModelMapper.deleteById(id); + } + + private AiChatModelDO validateChatModelExists(Long id) { + AiChatModelDO model = chatModelMapper.selectById(id); + if (chatModelMapper.selectById(id) == null) { + throw exception(CHAT_MODEL_NOT_EXISTS); + } + return model; + } + + @Override + public AiChatModelDO getChatModel(Long id) { + return chatModelMapper.selectById(id); + } + + @Override + public AiChatModelDO getRequiredDefaultChatModel() { + AiChatModelDO model = chatModelMapper.selectFirstByStatus(CommonStatusEnum.ENABLE.getStatus()); + if (model == null) { + throw exception(CHAT_MODEL_DEFAULT_NOT_EXISTS); + } + return model; + } + + @Override + public PageResult getChatModelPage(AiChatModelPageReqVO pageReqVO) { + return chatModelMapper.selectPage(pageReqVO); + } + + @Override + public AiChatModelDO validateChatModel(Long id) { + AiChatModelDO model = validateChatModelExists(id); + if (CommonStatusEnum.isDisable(model.getStatus())) { + throw exception(CHAT_MODEL_DISABLE); + } + return model; + } + + @Override + public List getChatModelListByStatus(Integer status) { + return chatModelMapper.selectList(status); + } + + @Override + public List getChatModelList(Collection ids) { + return chatModelMapper.selectBatchIds(ids); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleService.java new file mode 100644 index 000000000..81c8d259b --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleService.java @@ -0,0 +1,129 @@ +package cn.iocoder.yudao.module.ai.service.model; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRolePageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveMyReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import jakarta.validation.Valid; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * AI 聊天角色 Service 接口 + * + * @author fansili + */ +public interface AiChatRoleService { + + /** + * 创建聊天角色 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createChatRole(@Valid AiChatRoleSaveReqVO createReqVO); + + /** + * 创建【我的】聊天角色 + * + * @param createReqVO 创建信息 + * @param userId 用户编号 + * @return 编号 + */ + Long createChatRoleMy(AiChatRoleSaveMyReqVO createReqVO, Long userId); + + /** + * 更新聊天角色 + * + * @param updateReqVO 更新信息 + */ + void updateChatRole(@Valid AiChatRoleSaveReqVO updateReqVO); + + /** + * 创建【我的】聊天角色 + * + * @param updateReqVO 更新信息 + * @param userId 用户编号 + */ + void updateChatRoleMy(AiChatRoleSaveMyReqVO updateReqVO, Long userId); + + /** + * 删除聊天角色 + * + * @param id 编号 + */ + void deleteChatRole(Long id); + + /** + * 删除【我的】聊天角色 + * + * @param id 编号 + * @param userId 用户编号 + */ + void deleteChatRoleMy(Long id, Long userId); + + /** + * 获得聊天角色 + * + * @param id 编号 + * @return AI 聊天角色 + */ + AiChatRoleDO getChatRole(Long id); + + /** + * 获得聊天角色列表 + * + * @param ids 编号数组 + * @return 聊天角色列表 + */ + List getChatRoleList(Collection ids); + + default Map getChatRoleMap(Collection ids) { + return convertMap(getChatRoleList(ids), AiChatRoleDO::getId); + } + + /** + * 校验聊天角色是否合法 + * + * @param id 角色编号 + */ + AiChatRoleDO validateChatRole(Long id); + + /** + * 获得聊天角色分页 + * + * @param pageReqVO 分页查询 + * @return 聊天角色分页 + */ + PageResult getChatRolePage(AiChatRolePageReqVO pageReqVO); + + /** + * 获得【我的】聊天角色分页 + * + * @param pageReqVO 分页查询 + * @param userId 用户编号 + * @return 聊天角色分页 + */ + PageResult getChatRoleMyPage(AiChatRolePageReqVO pageReqVO, Long userId); + + /** + * 获得聊天角色的分类列表 + * + * @return 分类列表 + */ + List getChatRoleCategoryList(); + + /** + * 根据名字获得聊天角色 + * + * @param name 名字 + * @return 聊天角色列表 + */ + List getChatRoleListByName(String name); + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java new file mode 100644 index 000000000..2cf4d46d1 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiChatRoleServiceImpl.java @@ -0,0 +1,146 @@ +package cn.iocoder.yudao.module.ai.service.model; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRolePageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveMyReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import cn.iocoder.yudao.module.ai.dal.mysql.model.AiChatRoleMapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*; + +/** + * AI 聊天角色 Service 实现类 + * + * @author fansili + */ +@Service +@Slf4j +public class AiChatRoleServiceImpl implements AiChatRoleService { + + @Resource + private AiChatRoleMapper chatRoleMapper; + + @Override + public Long createChatRole(AiChatRoleSaveReqVO createReqVO) { + AiChatRoleDO chatRole = BeanUtils.toBean(createReqVO, AiChatRoleDO.class); + chatRoleMapper.insert(chatRole); + return chatRole.getId(); + } + + @Override + public Long createChatRoleMy(AiChatRoleSaveMyReqVO createReqVO, Long userId) { + AiChatRoleDO chatRole = BeanUtils.toBean(createReqVO, AiChatRoleDO.class).setUserId(userId) + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setPublicStatus(false); + chatRoleMapper.insert(chatRole); + return chatRole.getId(); + } + + @Override + public void updateChatRole(AiChatRoleSaveReqVO updateReqVO) { + // 校验存在 + validateChatRoleExists(updateReqVO.getId()); + // 更新 + AiChatRoleDO updateObj = BeanUtils.toBean(updateReqVO, AiChatRoleDO.class); + chatRoleMapper.updateById(updateObj); + } + + @Override + public void updateChatRoleMy(AiChatRoleSaveMyReqVO updateReqVO, Long userId) { + // 校验存在 + AiChatRoleDO chatRole = validateChatRoleExists(updateReqVO.getId()); + if (ObjectUtil.notEqual(chatRole.getUserId(), userId)) { + throw exception(CHAT_ROLE_NOT_EXISTS); + } + + // 更新 + AiChatRoleDO updateObj = BeanUtils.toBean(updateReqVO, AiChatRoleDO.class); + chatRoleMapper.updateById(updateObj); + } + + @Override + public void deleteChatRole(Long id) { + // 校验存在 + validateChatRoleExists(id); + // 删除 + chatRoleMapper.deleteById(id); + } + + @Override + public void deleteChatRoleMy(Long id, Long userId) { + // 校验存在 + AiChatRoleDO chatRole = validateChatRoleExists(id); + if (ObjectUtil.notEqual(chatRole.getUserId(), userId)) { + throw exception(CHAT_ROLE_NOT_EXISTS); + } + // 删除 + chatRoleMapper.deleteById(id); + } + + private AiChatRoleDO validateChatRoleExists(Long id) { + AiChatRoleDO chatRole = chatRoleMapper.selectById(id); + if (chatRole == null) { + throw exception(CHAT_ROLE_NOT_EXISTS); + } + return chatRole; + } + + @Override + public AiChatRoleDO getChatRole(Long id) { + return chatRoleMapper.selectById(id); + } + + @Override + public List getChatRoleList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return chatRoleMapper.selectBatchIds(ids); + } + + @Override + public AiChatRoleDO validateChatRole(Long id) { + AiChatRoleDO chatRole = validateChatRoleExists(id); + if (CommonStatusEnum.isDisable(chatRole.getStatus())) { + throw exception(CHAT_ROLE_DISABLE, chatRole.getName()); + } + return chatRole; + } + + @Override + public PageResult getChatRolePage(AiChatRolePageReqVO pageReqVO) { + return chatRoleMapper.selectPage(pageReqVO); + } + + @Override + public PageResult getChatRoleMyPage(AiChatRolePageReqVO pageReqVO, Long userId) { + return chatRoleMapper.selectPageByMy(pageReqVO, userId); + } + + @Override + public List getChatRoleCategoryList() { + List list = chatRoleMapper.selectListGroupByCategory(CommonStatusEnum.ENABLE.getStatus()); + return convertList(list, AiChatRoleDO::getCategory, role -> role != null && StrUtil.isNotBlank(role.getCategory())); + } + + @Override + public List getChatRoleListByName(String name) { + return chatRoleMapper.selectListByName(name); + } + +} + diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java new file mode 100644 index 000000000..49f8332de --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicService.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.module.ai.service.music; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.*; +import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; +import jakarta.validation.Valid; + +import java.util.List; + +/** + * AI 音乐 Service 接口 + * + * @author xiaoxin + */ +public interface AiMusicService { + + /** + * 音乐生成 + * + * @param userId 用户编号 + * @param reqVO 请求参数 + * @return 生成的音乐ID + */ + List generateMusic(Long userId, AiSunoGenerateReqVO reqVO); + + /** + * 同步音乐任务 + * + * @return 同步数量 + */ + Integer syncMusic(); + + /** + * 更新音乐发布状态 + * + * @param updateReqVO 更新信息 + */ + void updateMusic(@Valid AiMusicUpdateReqVO updateReqVO); + + /** + * 更新我的音乐 + * + * @param updateReqVO 更新信息 + */ + void updateMyMusic(@Valid AiMusicUpdateMyReqVO updateReqVO, Long userId); + + /** + * 删除AI 音乐 + * + * @param id 编号 + */ + void deleteMusic(Long id); + + /** + * 删除【我的】音乐记录 + * + * @param id 音乐编号 + * @param userId 用户编号 + */ + void deleteMusicMy(Long id, Long userId); + + /** + * 获得AI 音乐 + * + * @param id 音乐编号 + * @return 音乐内容 + */ + AiMusicDO getMusic(Long id); + + /** + * 获得音乐分页 + * + * @param pageReqVO 分页查询 + * @return 音乐分页 + */ + PageResult getMusicPage(AiMusicPageReqVO pageReqVO); + + /** + * 获得【我的】音乐分页 + * + * @param pageReqVO 分页查询 + * @param userId 用户编号 + * @return 音乐分页 + */ + PageResult getMusicMyPage(AiMusicPageReqVO pageReqVO, Long userId); + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java new file mode 100644 index 000000000..3f10ec840 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/music/AiMusicServiceImpl.java @@ -0,0 +1,218 @@ +package cn.iocoder.yudao.module.ai.service.music; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.text.StrPool; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicPageReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicUpdateMyReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiMusicUpdateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.music.vo.AiSunoGenerateReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.music.AiMusicDO; +import cn.iocoder.yudao.module.ai.dal.mysql.music.AiMusicMapper; +import cn.iocoder.yudao.module.ai.enums.music.AiMusicGenerateModeEnum; +import cn.iocoder.yudao.module.ai.enums.music.AiMusicStatusEnum; +import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; +import cn.iocoder.yudao.module.infra.api.file.FileApi; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.IMAGE_NOT_EXISTS; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.MUSIC_NOT_EXISTS; + +/** + * AI 音乐 Service 实现类 + * + * @author xiaoxin + */ +@Service +@Slf4j +public class AiMusicServiceImpl implements AiMusicService { + + @Resource + private AiApiKeyService apiKeyService; + + @Resource + private AiMusicMapper musicMapper; + + @Resource + private FileApi fileApi; + + @Override + @Transactional(rollbackFor = Exception.class) + public List generateMusic(Long userId, AiSunoGenerateReqVO reqVO) { + // 1. 调用 Suno 生成音乐 + SunoApi sunoApi = apiKeyService.getSunoApi(); + List musicDataList; + if (Objects.equals(AiMusicGenerateModeEnum.DESCRIPTION.getMode(), reqVO.getGenerateMode())) { + // 1.1 描述模式 + SunoApi.MusicGenerateRequest generateRequest = new SunoApi.MusicGenerateRequest( + reqVO.getPrompt(), reqVO.getModel(), reqVO.getMakeInstrumental()); + musicDataList = sunoApi.generate(generateRequest); + } else if (Objects.equals(AiMusicGenerateModeEnum.LYRIC.getMode(), reqVO.getGenerateMode())) { + // 1.2 歌词模式 + SunoApi.MusicGenerateRequest generateRequest = new SunoApi.MusicGenerateRequest( + reqVO.getPrompt(), reqVO.getModel(), CollUtil.join(reqVO.getTags(), StrPool.COMMA), reqVO.getTitle()); + musicDataList = sunoApi.customGenerate(generateRequest); + } else { + throw new IllegalArgumentException(StrUtil.format("未知生成模式({})", reqVO)); + } + + // 2. 插入数据库 + if (CollUtil.isEmpty(musicDataList)) { + return Collections.emptyList(); + } + List musicList = buildMusicDOList(musicDataList); + musicList.forEach(music -> music.setUserId(userId).setPlatform(reqVO.getPlatform()).setGenerateMode(reqVO.getGenerateMode())); + musicMapper.insertBatch(musicList); + return convertList(musicList, AiMusicDO::getId); + } + + @Override + public Integer syncMusic() { + List streamingTask = musicMapper.selectListByStatus(AiMusicStatusEnum.IN_PROGRESS.getStatus()); + if (CollUtil.isEmpty(streamingTask)) { + return 0; + } + log.info("[syncMusic][Suno 开始同步, 共 ({}) 个任务]", streamingTask.size()); + + // GET 请求,为避免参数过长,分批次处理 + SunoApi sunoApi = apiKeyService.getSunoApi(); + CollUtil.split(streamingTask, 36).forEach(chunkList -> { + Map taskIdMap = convertMap(chunkList, AiMusicDO::getTaskId, AiMusicDO::getId); + List musicTaskList = sunoApi.getMusicList(new ArrayList<>(taskIdMap.keySet())); + if (CollUtil.isEmpty(musicTaskList)) { + log.warn("Suno 任务同步失败, 任务ID: [{}]", taskIdMap.keySet()); + return; + } + // 更新进度 + List updateMusicList = buildMusicDOList(musicTaskList); + updateMusicList.forEach(music -> music.setId(taskIdMap.get(music.getTaskId()))); + musicMapper.updateBatch(updateMusicList); + }); + return streamingTask.size(); + } + + @Override + public void updateMusic(AiMusicUpdateReqVO updateReqVO) { + // 校验存在 + validateMusicExists(updateReqVO.getId()); + // 更新 + musicMapper.updateById(new AiMusicDO().setId(updateReqVO.getId()).setPublicStatus(updateReqVO.getPublicStatus())); + } + + @Override + public void updateMyMusic(AiMusicUpdateMyReqVO updateReqVO, Long userId) { + // 校验音乐是否存在 + AiMusicDO musicDO = validateMusicExists(updateReqVO.getId()); + if (ObjUtil.notEqual(musicDO.getUserId(), userId)) { + throw exception(MUSIC_NOT_EXISTS); + } + // 更新 + musicMapper.updateById(new AiMusicDO().setId(updateReqVO.getId()).setTitle(updateReqVO.getTitle())); + } + + @Override + public void deleteMusic(Long id) { + // 校验存在 + validateMusicExists(id); + // 删除 + musicMapper.deleteById(id); + } + + @Override + public void deleteMusicMy(Long id, Long userId) { + // 1. 校验是否存在 + AiMusicDO music = validateMusicExists(id); + if (ObjUtil.notEqual(music.getUserId(), userId)) { + throw exception(IMAGE_NOT_EXISTS); + } + // 2. 删除记录 + musicMapper.deleteById(id); + } + + @Override + public AiMusicDO getMusic(Long id) { + return musicMapper.selectById(id); + } + + @Override + public PageResult getMusicPage(AiMusicPageReqVO pageReqVO) { + return musicMapper.selectPage(pageReqVO); + } + + @Override + public PageResult getMusicMyPage(AiMusicPageReqVO pageReqVO, Long userId) { + return musicMapper.selectPageByMy(pageReqVO, userId); + } + + /** + * 构建 AiMusicDO 集合 + * + * @param musicList suno 音乐任务列表 + * @return AiMusicDO 集合 + */ + private List buildMusicDOList(List musicList) { + return convertList(musicList, musicData -> { + Integer status = Objects.equals("complete", musicData.status()) ? AiMusicStatusEnum.SUCCESS.getStatus() + : Objects.equals("error", musicData.status()) ? AiMusicStatusEnum.FAIL.getStatus() + : AiMusicStatusEnum.IN_PROGRESS.getStatus(); + return new AiMusicDO() + .setTaskId(musicData.id()).setModel(musicData.modelName()) + .setDescription(musicData.gptDescriptionPrompt()) + .setAudioUrl(downloadFile(status, musicData.audioUrl())) + .setVideoUrl(downloadFile(status, musicData.videoUrl())) + .setImageUrl(downloadFile(status, musicData.imageUrl())) + .setTitle(musicData.title()).setDuration(musicData.duration()) + .setLyric(musicData.lyric()).setTags(StrUtil.split(musicData.tags(), StrPool.COMMA)) + .setErrorMessage(musicData.errorMessage()) + .setStatus(status); + }); + } + + /** + * 音乐生成好后,将音频文件上传到文件服务器 + * + * @param status 音乐状态 + * @param url 音频文件地址 + * @return 内部文件地址 + */ + private String downloadFile(Integer status, String url) { + if (StrUtil.isBlank(url) || ObjectUtil.notEqual(status, AiMusicStatusEnum.SUCCESS.getStatus())) { + return url; + } + try { + byte[] bytes = HttpUtil.downloadBytes(url); + return fileApi.createFile(bytes); + } catch (Exception e) { + log.error("[downloadFile][url({}) 下载失败]", url, e); + return url; + } + } + + /** + * 校验音乐是否存在 + * + * @param id 音乐编号 + * @return 音乐信息 + */ + private AiMusicDO validateMusicExists(Long id) { + AiMusicDO music = musicMapper.selectById(id); + if (music == null) { + throw exception(MUSIC_NOT_EXISTS); + } + return music; + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java new file mode 100644 index 000000000..f2dd489ff --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteService.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.module.ai.service.write; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteGenerateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWritePageReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.write.AiWriteDO; +import reactor.core.publisher.Flux; + +/** + * AI 写作 Service 接口 + * + * @author xiaoxin + */ +public interface AiWriteService { + + /** + * 生成写作内容 + * + * @param generateReqVO 作文生成请求参数 + * @param userId 用户编号 + * @return 生成结果 + */ + Flux> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId); + + /** + * 删除写作 + * + * @param id 编号 + */ + void deleteWrite(Long id); + + /** + * 获得写作分页 + * + * @param pageReqVO 分页查询 + * @return AI 写作分页 + */ + PageResult getWritePage(AiWritePageReqVO pageReqVO); + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java new file mode 100644 index 000000000..2fae31d59 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java @@ -0,0 +1,177 @@ +package cn.iocoder.yudao.module.ai.service.write; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.ai.core.util.AiUtils; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWriteGenerateReqVO; +import cn.iocoder.yudao.module.ai.controller.admin.write.vo.AiWritePageReqVO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; +import cn.iocoder.yudao.module.ai.dal.dataobject.write.AiWriteDO; +import cn.iocoder.yudao.module.ai.dal.mysql.write.AiWriteMapper; +import cn.iocoder.yudao.module.ai.enums.AiChatRoleEnum; +import cn.iocoder.yudao.module.ai.enums.DictTypeConstants; +import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; +import cn.iocoder.yudao.module.ai.enums.write.AiWriteTypeEnum; +import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; +import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; +import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; +import cn.iocoder.yudao.module.system.api.dict.DictDataApi; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.model.StreamingChatModel; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error; +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.WRITE_NOT_EXISTS; + +/** + * AI 写作 Service 实现类 + * + * @author xiaoxin + */ +@Service +@Slf4j +public class AiWriteServiceImpl implements AiWriteService { + + @Resource + private AiApiKeyService apiKeyService; + @Resource + private AiChatModelService chatModalService; + @Resource + private AiChatRoleService chatRoleService; + + @Resource + private DictDataApi dictDataApi; + + @Resource + private AiWriteMapper writeMapper; + + @Override + public Flux> generateWriteContent(AiWriteGenerateReqVO generateReqVO, Long userId) { + // 1 获取写作模型。尝试获取写作助手角色,没有则使用默认模型 + AiChatRoleDO writeRole = CollUtil.getFirst( + chatRoleService.getChatRoleListByName(AiChatRoleEnum.AI_WRITE_ROLE.getName())); + // 1.1 获取写作执行模型 + AiChatModelDO model = getModel(writeRole); + // 1.2 获取角色设定消息 + String systemMessage = Objects.nonNull(writeRole) && StrUtil.isNotBlank(writeRole.getSystemMessage()) + ? writeRole.getSystemMessage() : AiChatRoleEnum.AI_WRITE_ROLE.getSystemMessage(); + // 1.3 校验平台 + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); + StreamingChatModel chatModel = apiKeyService.getChatModel(model.getKeyId()); + + // 2. 插入写作信息 + AiWriteDO writeDO = BeanUtils.toBean(generateReqVO, AiWriteDO.class, + write -> write.setUserId(userId).setPlatform(platform.getPlatform()).setModel(model.getModel())); + writeMapper.insert(writeDO); + + // 3.1 构建 Prompt,并进行调用 + Prompt prompt = buildPrompt(generateReqVO, model, systemMessage); + Flux streamResponse = chatModel.stream(prompt); + + // 3.2 流式返回 + StringBuffer contentBuffer = new StringBuffer(); + return streamResponse.map(chunk -> { + String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getContent() : null; + newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况 + contentBuffer.append(newContent); + // 响应结果 + return success(newContent); + }).doOnComplete(() -> { + // 忽略租户,因为 Flux 异步无法透传租户 + TenantUtils.executeIgnore(() -> + writeMapper.updateById(new AiWriteDO().setId(writeDO.getId()).setGeneratedContent(contentBuffer.toString()))); + }).doOnError(throwable -> { + log.error("[generateWriteContent][generateReqVO({}) 发生异常]", generateReqVO, throwable); + // 忽略租户,因为 Flux 异步无法透传租户 + TenantUtils.executeIgnore(() -> + writeMapper.updateById(new AiWriteDO().setId(writeDO.getId()).setErrorMessage(throwable.getMessage()))); + }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.WRITE_STREAM_ERROR))); + } + + private AiChatModelDO getModel(AiChatRoleDO writeRole) { + AiChatModelDO model = null; + if (Objects.nonNull(writeRole) && Objects.nonNull(writeRole.getModelId())) { + model = chatModalService.getChatModel(writeRole.getModelId()); + } + if (Objects.isNull(model)) { + model = chatModalService.getRequiredDefaultChatModel(); + } + Assert.notNull(model, "[AI] 获取不到模型"); + return model; + } + + private Prompt buildPrompt(AiWriteGenerateReqVO generateReqVO, AiChatModelDO model, String systemMessage) { + // 1. 构建 message 列表 + List chatMessages = buildMessages(generateReqVO, systemMessage); + // 2. 构建 options 对象 + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); + ChatOptions options = AiUtils.buildChatOptions(platform, model.getModel(), model.getTemperature(), model.getMaxTokens()); + return new Prompt(chatMessages, options); + } + + private List buildMessages(AiWriteGenerateReqVO generateReqVO, String systemMessage) { + List chatMessages = new ArrayList<>(); + if (StrUtil.isNotBlank(systemMessage)) { + // 1.1 角色设定 + chatMessages.add(new SystemMessage(systemMessage)); + } + // 1.2 用户输入 + chatMessages.add(new UserMessage(buildUserMessage(generateReqVO))); + return chatMessages; + } + + private String buildUserMessage(AiWriteGenerateReqVO generateReqVO) { + String format = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_FORMAT, generateReqVO.getFormat()); + String tone = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_TONE, generateReqVO.getTone()); + String language = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_LANGUAGE, generateReqVO.getLanguage()); + String length = dictDataApi.getDictDataLabel(DictTypeConstants.AI_WRITE_LENGTH, generateReqVO.getLength()); + // 格式化 prompt + String prompt = generateReqVO.getPrompt(); + if (Objects.equals(generateReqVO.getType(), AiWriteTypeEnum.WRITING.getType())) { + return StrUtil.format(AiWriteTypeEnum.WRITING.getPrompt(), prompt, format, tone, language, length); + } else { + return StrUtil.format(AiWriteTypeEnum.REPLY.getPrompt(), generateReqVO.getOriginalContent(), prompt, format, tone, language, length); + } + } + + @Override + public void deleteWrite(Long id) { + // 校验存在 + validateWriteExists(id); + // 删除 + writeMapper.deleteById(id); + } + + private void validateWriteExists(Long id) { + if (writeMapper.selectById(id) == null) { + throw exception(WRITE_NOT_EXISTS); + } + } + + @Override + public PageResult getWritePage(AiWritePageReqVO pageReqVO) { + return writeMapper.selectPage(pageReqVO); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/resources/application-dev.yaml b/yudao-module-ai/yudao-module-ai-biz/src/main/resources/application-dev.yaml new file mode 100644 index 000000000..d103eefa3 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/resources/application-dev.yaml @@ -0,0 +1,118 @@ +--- #################### 数据库相关配置 #################### +spring: + # 数据源配置项 + autoconfigure: + exclude: + datasource: + druid: # Druid 【监控】相关的全局配置 + web-stat-filter: + enabled: true + stat-view-servlet: + enabled: true + allow: # 设置白名单,不填则允许所有访问 + url-pattern: /druid/* + login-username: # 控制台管理用户名和密码 + login-password: + filter: + stat: + enabled: true + log-slow-sql: true # 慢 SQL 记录 + slow-sql-millis: 100 + merge-sql: true + wall: + config: + multi-statement-allow: true + dynamic: # 多数据源配置 + druid: # Druid 【连接池】相关的全局配置 + initial-size: 5 # 初始连接数 + min-idle: 10 # 最小连接池数量 + max-active: 20 # 最大连接池数量 + max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 + max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 + validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 + test-while-idle: true + test-on-borrow: false + test-on-return: false + primary: master + datasource: + master: + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 + username: root + password: 123456 + slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 + lazy: true # 开启懒加载,保证启动速度 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 + username: root + password: 123456 + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + data: + redis: + host: 400-infra.server.iocoder.cn # 地址 + port: 6379 # 端口 + database: 1 # 数据库索引 +# password: 123456 # 密码,建议生产环境开启 + +--- #################### MQ 消息队列相关配置 #################### + +# rocketmq 配置项,对应 RocketMQProperties 配置类 +rocketmq: + name-server: 127.0.0.1:9876 # RocketMQ Namesrv + +spring: + # RabbitMQ 配置项,对应 RabbitProperties 配置类 + rabbitmq: + host: 127.0.0.1 # RabbitMQ 服务的地址 + port: 5672 # RabbitMQ 服务的端口 + username: guest # RabbitMQ 服务的账号 + password: guest # RabbitMQ 服务的密码 + # Kafka 配置项,对应 KafkaProperties 配置类 + kafka: + bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔 + +--- #################### 定时任务相关配置 #################### +xxl: + job: + admin: + addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址 + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项 +lock4j: + acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 + expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒 + +--- #################### 监控相关配置 #################### + +# Actuator 监控端点的配置项 +management: + endpoints: + web: + base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator + exposure: + include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 + +# Spring Boot Admin 配置项 +spring: + boot: + admin: + # Spring Boot Admin Client 客户端的相关配置 + client: + instance: + service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME] + # Spring Boot Admin Server 服务端的相关配置 + context-path: /admin # 配置 Spring + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + xss: + enable: false + exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系 + - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 + - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 + demo: true # 开启演示模式 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/resources/application-local.yaml b/yudao-module-ai/yudao-module-ai-biz/src/main/resources/application-local.yaml new file mode 100644 index 000000000..f159bd32f --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/resources/application-local.yaml @@ -0,0 +1,125 @@ +--- #################### 数据库相关配置 #################### +spring: + # 数据源配置项 + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源 + - de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置 + datasource: + druid: # Druid 【监控】相关的全局配置 + web-stat-filter: + enabled: true + stat-view-servlet: + enabled: true + allow: # 设置白名单,不填则允许所有访问 + url-pattern: /druid/* + login-username: # 控制台管理用户名和密码 + login-password: + filter: + stat: + enabled: true + log-slow-sql: true # 慢 SQL 记录 + slow-sql-millis: 100 + merge-sql: true + wall: + config: + multi-statement-allow: true + dynamic: # 多数据源配置 + druid: # Druid 【连接池】相关的全局配置 + initial-size: 1 # 初始连接数 + min-idle: 1 # 最小连接池数量 + max-active: 20 # 最大连接池数量 + max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 + max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 + validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 + test-while-idle: true + test-on-borrow: false + test-on-return: false + primary: master + datasource: + master: + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 + # url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai # MySQL Connector/J 5.X 连接的示例 + # url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例 + # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 + # url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=ruoyi-vue-pro # SQLServer 连接的示例 + # url: jdbc:dm://10.211.55.4:5236?schema=RUOYI_VUE_PRO # DM 连接的示例 + username: root + password: 123456 + # username: sa # SQL Server 连接的示例 + # password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W # SQL Server 连接的示例 + # username: SYSDBA # DM 连接的示例 + # password: SYSDBA # DM 连接的示例 + slave: # 模拟从库,可根据自己需要修改 + lazy: true # 开启懒加载,保证启动速度 + url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true + username: root + password: 123456 + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + data: + host: 127.0.0.1 # 地址 + port: 6379 # 端口 + database: 0 # 数据库索引 + # password: 123456 # 密码,建议生产环境开启 + +--- #################### MQ 消息队列相关配置 #################### + +--- #################### 定时任务相关配置 #################### + +xxl: + job: + admin: + addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址 + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项 +lock4j: + acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 + expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒 + +--- #################### 监控相关配置 #################### + +# Actuator 监控端点的配置项 +management: + endpoints: + web: + base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator + exposure: + include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 + +# Spring Boot Admin 配置项 +spring: + boot: + admin: + # Spring Boot Admin Client 客户端的相关配置 + client: + instance: + service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME] + +# 日志文件配置 +logging: + level: + # 配置自己写的 MyBatis Mapper 打印日志 + cn.iocoder.yudao.module.ai.dal.mysql: debug + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yudao: + env: # 多环境的配置项 + tag: ${HOSTNAME} + security: + mock-enable: true + xss: + enable: false + exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系 + - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 + - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 + access-log: # 访问日志的配置项 + enable: false + demo: false # 关闭演示模式 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/resources/application.yaml b/yudao-module-ai/yudao-module-ai-biz/src/main/resources/application.yaml new file mode 100644 index 000000000..dcc4506f2 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/resources/application.yaml @@ -0,0 +1,155 @@ +spring: + main: + allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。 + allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Feign 等会存在重复定义的服务 + + # Servlet 配置 + servlet: + # 文件上传相关配置项 + multipart: + max-file-size: 16MB # 单个文件大小 + max-request-size: 32MB # 设置总上传的文件大小 + mvc: + pathmatch: + matching-strategy: ANT_PATH_MATCHER # 解决 SpringFox 与 SpringBoot 2.6.x 不兼容的问题,参见 SpringFoxHandlerProviderBeanPostProcessor 类 + + # Jackson 配置项 + jackson: + serialization: + write-dates-as-timestamps: true # 设置 LocalDateTime 的格式,使用时间戳 + write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401 + write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳 + fail-on-empty-beans: false # 允许序列化无属性的 Bean + + # Cache 配置项 + cache: + type: REDIS + redis: + time-to-live: 1h # 设置过期时间为 1 小时 + +server: + servlet: + encoding: + enabled: true + charset: UTF-8 # 必须设置 UTF-8,避免 WebFlux 流式返回(AI 场景)会乱码问题 + force: true + +--- #################### 接口文档配置 #################### + +springdoc: + api-docs: + enabled: true # 1. 是否开启 Swagger 接文档的元数据 + path: /v3/api-docs + swagger-ui: + enabled: true # 2.1 是否开启 Swagger 文档的官方 UI 界面 + path: /swagger-ui.html + default-flat-param-object: true # 参见 https://doc.xiaominfo.com/docs/faq/v4/knife4j-parameterobject-flat-param 文档 + +knife4j: + enable: true # 2.2 是否开启 Swagger 文档的 Knife4j UI 界面 + setting: + language: zh_cn + +# MyBatis Plus 的配置项 +mybatis-plus: + configuration: + map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。 + global-config: + db-config: + id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。 + # id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库 + # id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库 + # id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解 + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + banner: false # 关闭控制台的 Banner 打印 + type-aliases-package: ${yudao.info.base-package}.module.*.dal.dataobject + encryptor: + password: XDV71a+xqStEA3WH # 加解密的秘钥,可使用 https://www.imaegoo.com/2020/aes-key-generator/ 网站生成 + +mybatis-plus-join: + banner: false # 关闭控制台的 Banner 打印 + +# VO 转换(数据翻译)相关 +easy-trans: + is-enable-global: true # 启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口 + is-enable-cloud: false # 禁用 TransType.RPC 微服务模式 + +--- #################### RPC 远程调用相关配置 #################### + +--- #################### MQ 消息队列相关配置 #################### + +--- #################### 定时任务相关配置 #################### + +xxl: + job: + executor: + appname: ${spring.application.name} # 执行器 AppName + logpath: ${user.home}/logs/xxl-job/${spring.application.name} # 执行器运行日志文件存储磁盘路径 + accessToken: default_token # 执行器通讯TOKEN + +--- #################### AI 相关配置 #################### + +spring: + ai: + qianfan: # 文心一言 + api-key: x0cuLZ7XsaTCU08vuJWO87Lg + secret-key: R9mYF9dl9KASgi5RUq0FQt3wRisSnOcK + zhipuai: # 智谱 AI + api-key: 32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs + openai: + api-key: sk-yzKea6d8e8212c3bdd99f9f44ced1cae37c097e5aa3BTS7z + base-url: https://api.gptsapi.net + ollama: + base-url: http://127.0.0.1:11434 + chat: + model: llama3 + stabilityai: + api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx + cloud: + ai: + tongyi: # 通义千问 + tongyi: + api-key: sk-Zsd81gZYg7 + +yudao: + ai: + deep-seek: # DeepSeek + enable: true + api-key: sk-e94db327cc7d457d99a8de8810fc6b12 + model: deepseek-chat + xinghuo: # 讯飞星火 + enable: true + appId: 13c8cca6 + appKey: cb6415c19d6162cda07b47316fcb0416 + secretKey: Y2JiYTIxZjA3MDMxMjNjZjQzYzVmNzdh + model: generalv3.5 + midjourney: + enable: true + # base-url: https://api.holdai.top/mj-relax/mj + base-url: https://api.holdai.top/mj + api-key: sk-dZEPiVaNcT3FHhef51996bAa0bC74806BeAb620dA5Da10Bf + notify-url: http://java.nat300.top/admin-api/ai/image/midjourney/notify + suno: + enable: true + # base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app + base-url: http://127.0.0.1:3001 + +--- #################### 芋道相关配置 #################### + +yudao: + info: + version: 1.0.0 + base-package: cn.iocoder.yudao.module.ai + web: + admin-ui: + url: http://dashboard.yudao.iocoder.cn # Admin 管理后台 UI 的地址 + swagger: + title: 管理后台 + description: 提供管理员管理的所有功能 + version: ${yudao.info.version} + base-package: ${yudao.info.base-package} + tenant: # 多租户相关配置项 + enable: true + +debug: false diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/resources/bootstrap-local.yaml b/yudao-module-ai/yudao-module-ai-biz/src/main/resources/bootstrap-local.yaml new file mode 100644 index 000000000..2de0efbf7 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/resources/bootstrap-local.yaml @@ -0,0 +1,23 @@ +--- #################### 注册中心相关配置 #################### + +spring: + cloud: + nacos: + server-addr: 127.0.0.1:8848 + discovery: + namespace: dev # 命名空间。这里使用 dev 开发环境 + metadata: + version: 1.0.0 # 服务实例的版本号,可用于灰度发布 + +--- #################### 配置中心相关配置 #################### + +spring: + cloud: + nacos: + # Nacos Config 配置项,对应 NacosConfigProperties 配置属性类 + config: + server-addr: 127.0.0.1:8848 # Nacos 服务器地址 + namespace: dev # 命名空间 dev 的ID,不能直接使用 dev 名称。创建命名空间的时候需要指定ID为 dev,这里使用 dev 开发环境 + group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP + name: ${spring.application.name} # 使用的 Nacos 配置集的 dataId,默认为 spring.application.name + file-extension: yaml # 使用的 Nacos 配置集的 dataId 的文件拓展名,同时也是 Nacos 配置集的配置格式,默认为 properties diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/resources/bootstrap.yaml b/yudao-module-ai/yudao-module-ai-biz/src/main/resources/bootstrap.yaml new file mode 100644 index 000000000..2725914ea --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/resources/bootstrap.yaml @@ -0,0 +1,14 @@ +spring: + application: + name: ai-server + + profiles: + active: local + +server: + port: 48090 + +# 日志文件配置。注意,如果 logging.file.name 不放在 bootstrap.yaml 配置文件,而是放在 application.yaml 中,会导致出现 LOG_FILE_IS_UNDEFINED 文件 +logging: + file: + name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/resources/logback-spring.xml b/yudao-module-ai/yudao-module-ai-biz/src/main/resources/logback-spring.xml new file mode 100644 index 000000000..b1b9f3faf --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/resources/logback-spring.xml @@ -0,0 +1,76 @@ + + + + + + + + + +       + + + ${PATTERN_DEFAULT} + + + + + + + + + + ${PATTERN_DEFAULT} + + + + ${LOG_FILE} + + + ${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz} + + ${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false} + + ${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB} + + ${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0} + + ${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-30} + + + + + + 0 + + 256 + + + + + + + + ${PATTERN_DEFAULT} + + + + + + + + + + + + + + + + + + + + + + diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml new file mode 100644 index 000000000..59fad8756 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -0,0 +1,71 @@ + + + + cn.iocoder.cloud + yudao-module-ai + ${revision} + + 4.0.0 + yudao-spring-boot-starter-ai + jar + + ${project.artifactId} + AI 大模型拓展,接入国内外大模型 + + 1.0.0-M1 + + + + + org.springframework.ai + spring-ai-zhipuai-spring-boot-starter + ${spring-ai.version} + + + + org.springframework.ai + spring-ai-openai-spring-boot-starter + ${spring-ai.version} + + + org.springframework.ai + spring-ai-ollama-spring-boot-starter + ${spring-ai.version} + + + org.springframework.ai + spring-ai-stability-ai-spring-boot-starter + ${spring-ai.version} + + + + cn.iocoder.cloud + yudao-common + + + + + group.springframework.ai + spring-ai-qianfan-spring-boot-starter + 1.1.0 + + + + + + com.alibaba + dashscope-sdk-java + 2.14.0 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java new file mode 100644 index 000000000..05a317294 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java @@ -0,0 +1,76 @@ +package cn.iocoder.yudao.framework.ai.config; + +import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; +import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl; +import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; +import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions; +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatOptions; +import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +/** + * 芋道 AI 自动配置 + * + * @author fansili + */ +@AutoConfiguration +@EnableConfigurationProperties(YudaoAiProperties.class) +@Slf4j +@Import(TongYiAutoConfiguration.class) +public class YudaoAiAutoConfiguration { + + @Bean + public AiModelFactory aiModelFactory() { + return new AiModelFactoryImpl(); + } + + // ========== 各种 AI Client 创建 ========== + + @Bean + @ConditionalOnProperty(value = "yudao.ai.deepseek.enable", havingValue = "true") + public DeepSeekChatModel deepSeekChatModel(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.DeepSeekProperties properties = yudaoAiProperties.getDeepSeek(); + DeepSeekChatOptions options = DeepSeekChatOptions.builder() + .model(properties.getModel()) + .temperature(properties.getTemperature()) + .maxTokens(properties.getMaxTokens()) + .topP(properties.getTopP()) + .build(); + return new DeepSeekChatModel(properties.getApiKey(), options); + } + + @Bean + @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true") + public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo(); + XingHuoChatOptions options = XingHuoChatOptions.builder() + .model(properties.getModel()) + .temperature(properties.getTemperature()) + .maxTokens(properties.getMaxTokens()) + .topK(properties.getTopK()) + .build(); + return new XingHuoChatModel(properties.getAppKey(), properties.getSecretKey(), options); + } + + @Bean + @ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true") + public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) { + YudaoAiProperties.MidjourneyProperties config = yudaoAiProperties.getMidjourney(); + return new MidjourneyApi(config.getBaseUrl(), config.getApiKey(), config.getNotifyUrl()); + } + + @Bean + @ConditionalOnProperty(value = "yudao.ai.suno.enable", havingValue = "true") + public SunoApi sunoApi(YudaoAiProperties yudaoAiProperties) { + return new SunoApi(yudaoAiProperties.getSuno().getBaseUrl()); + } + +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java new file mode 100644 index 000000000..82c74b0c6 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiProperties.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.framework.ai.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * 芋道 AI 配置类 + * + * @author fansili + * @since 1.0 + */ +@ConfigurationProperties(prefix = "yudao.ai") +@Data +public class YudaoAiProperties { + + /** + * DeepSeek + */ + private DeepSeekProperties deepSeek; + + /** + * 讯飞星火 + */ + private XingHuoProperties xinghuo; + + /** + * Midjourney 绘图 + */ + private MidjourneyProperties midjourney; + + /** + * Suno 音乐 + */ + private SunoProperties suno; + + @Data + public static class XingHuoProperties { + + private String enable; + private String appId; + private String appKey; + private String secretKey; + + private String model; + private Float temperature; + private Integer maxTokens; + private Integer topK; + + } + + @Data + public static class DeepSeekProperties { + + private String enable; + private String apiKey; + + private String model; + private Float temperature; + private Integer maxTokens; + private Float topP; + + } + + @Data + public static class MidjourneyProperties { + + private String enable; + private String baseUrl; + + private String apiKey; + private String notifyUrl; + + } + + @Data + public static class SunoProperties { + + private boolean enable = false; + + private String baseUrl; + + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java new file mode 100644 index 000000000..596118168 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/enums/AiPlatformEnum.java @@ -0,0 +1,52 @@ +package cn.iocoder.yudao.framework.ai.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * AI 模型平台 + * + * @author fansili + */ +@Getter +@AllArgsConstructor +public enum AiPlatformEnum { + + // ========== 国内平台 ========== + + TONG_YI("TongYi", "通义千问"), // 阿里 + YI_YAN("YiYan", "文心一言"), // 百度 + DEEP_SEEK("DeepSeek", "DeepSeek"), // DeepSeek + ZHI_PU("ZhiPu", "智谱"), // 智谱 AI + XING_HUO("XingHuo", "星火"), // 讯飞 + + // ========== 国外平台 ========== + + OPENAI("OpenAI", "OpenAI"), + OLLAMA("Ollama", "Ollama"), + + STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI + MIDJOURNEY("Midjourney", "Midjourney"), // Midjourney + SUNO("Suno", "Suno"), // Suno AI + + ; + + /** + * 平台 + */ + private final String platform; + /** + * 平台名 + */ + private final String name; + + public static AiPlatformEnum validatePlatform(String platform) { + for (AiPlatformEnum platformEnum : AiPlatformEnum.values()) { + if (platformEnum.getPlatform().equals(platform)) { + return platformEnum; + } + } + throw new IllegalArgumentException("非法平台: " + platform); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java new file mode 100644 index 000000000..b6d7b3dd0 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactory.java @@ -0,0 +1,82 @@ +package cn.iocoder.yudao.framework.ai.core.factory; + +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.image.ImageModel; + +/** + * AI Model 模型工厂的接口类 + * + * @author fansili + */ +public interface AiModelFactory { + + /** + * 基于指定配置,获得 ChatModel 对象 + * + * 如果不存在,则进行创建 + * + * @param platform 平台 + * @param apiKey API KEY + * @param url API URL + * @return ChatModel 对象 + */ + ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url); + + /** + * 基于默认配置,获得 ChatModel 对象 + * + * 默认配置,指的是在 application.yaml 配置文件中的 spring.ai 相关的配置 + * + * @param platform 平台 + * @return ChatModel 对象 + */ + ChatModel getDefaultChatModel(AiPlatformEnum platform); + + /** + * 基于默认配置,获得 ImageModel 对象 + * + * 默认配置,指的是在 application.yaml 配置文件中的 spring.ai 相关的配置 + * + * @param platform 平台 + * @return ImageModel 对象 + */ + ImageModel getDefaultImageModel(AiPlatformEnum platform); + + /** + * 基于指定配置,获得 ImageModel 对象 + * + * 如果不存在,则进行创建 + * + * @param platform 平台 + * @param apiKey API KEY + * @param url API URL + * @return ImageModel 对象 + */ + ImageModel getOrCreateImageModel(AiPlatformEnum platform, String apiKey, String url); + + /** + * 基于指定配置,获得 MidjourneyApi 对象 + * + * 如果不存在,则进行创建 + * + * @param apiKey API KEY + * @param url API URL + * @return MidjourneyApi 对象 + */ + MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url); + + /** + * 基于指定配置,获得 SunoApi 对象 + * + * 如果不存在,则进行创建 + * + * @param apiKey API KEY + * @param url API URL + * @return SunoApi 对象 + */ + SunoApi getOrCreateSunoApi(String apiKey, String url); + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java new file mode 100644 index 000000000..a5df28246 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java @@ -0,0 +1,294 @@ +package cn.iocoder.yudao.framework.ai.core.factory; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.lang.Singleton; +import cn.hutool.core.lang.func.Func0; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.ai.config.YudaoAiAutoConfiguration; +import cn.iocoder.yudao.framework.ai.config.YudaoAiProperties; +import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; +import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; +import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; +import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; +import com.alibaba.cloud.ai.tongyi.TongYiConnectionProperties; +import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel; +import com.alibaba.cloud.ai.tongyi.chat.TongYiChatProperties; +import com.alibaba.cloud.ai.tongyi.image.TongYiImagesModel; +import com.alibaba.cloud.ai.tongyi.image.TongYiImagesProperties; +import com.alibaba.dashscope.aigc.generation.Generation; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; +import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration; +import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration; +import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration; +import org.springframework.ai.autoconfigure.qianfan.QianFanChatProperties; +import org.springframework.ai.autoconfigure.qianfan.QianFanConnectionProperties; +import org.springframework.ai.autoconfigure.qianfan.QianFanImageProperties; +import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration; +import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiChatProperties; +import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiConnectionProperties; +import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiImageProperties; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.image.ImageModel; +import org.springframework.ai.model.function.FunctionCallbackContext; +import org.springframework.ai.ollama.OllamaChatModel; +import org.springframework.ai.ollama.api.OllamaApi; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.OpenAiImageModel; +import org.springframework.ai.openai.api.ApiUtils; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.openai.api.OpenAiImageApi; +import org.springframework.ai.qianfan.QianFanChatModel; +import org.springframework.ai.qianfan.QianFanImageModel; +import org.springframework.ai.qianfan.api.QianFanApi; +import org.springframework.ai.qianfan.api.QianFanImageApi; +import org.springframework.ai.stabilityai.StabilityAiImageModel; +import org.springframework.ai.stabilityai.api.StabilityAiApi; +import org.springframework.ai.zhipuai.ZhiPuAiChatModel; +import org.springframework.ai.zhipuai.ZhiPuAiImageModel; +import org.springframework.ai.zhipuai.api.ZhiPuAiApi; +import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestClient; + +import java.util.List; + +/** + * AI Model 模型工厂的实现类 + * + * @author 芋道源码 + */ +public class AiModelFactoryImpl implements AiModelFactory { + + @Override + public ChatModel getOrCreateChatModel(AiPlatformEnum platform, String apiKey, String url) { + String cacheKey = buildClientCacheKey(ChatModel.class, platform, apiKey, url); + return Singleton.get(cacheKey, (Func0) () -> { + //noinspection EnhancedSwitchMigration + switch (platform) { + case TONG_YI: + return buildTongYiChatModel(apiKey); + case YI_YAN: + return buildYiYanChatModel(apiKey); + case DEEP_SEEK: + return buildDeepSeekChatModel(apiKey); + case ZHI_PU: + return buildZhiPuChatModel(apiKey, url); + case XING_HUO: + return buildXingHuoChatModel(apiKey); + case OPENAI: + return buildOpenAiChatModel(apiKey, url); + case OLLAMA: + return buildOllamaChatModel(url); + default: + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); + } + }); + } + + @Override + public ChatModel getDefaultChatModel(AiPlatformEnum platform) { + //noinspection EnhancedSwitchMigration + switch (platform) { + case TONG_YI: + return SpringUtil.getBean(TongYiChatModel.class); + case YI_YAN: + return SpringUtil.getBean(QianFanChatModel.class); + case DEEP_SEEK: + return SpringUtil.getBean(DeepSeekChatModel.class); + case ZHI_PU: + return SpringUtil.getBean(ZhiPuAiChatModel.class); + case XING_HUO: + return SpringUtil.getBean(XingHuoChatModel.class); + case OPENAI: + return SpringUtil.getBean(OpenAiChatModel.class); + case OLLAMA: + return SpringUtil.getBean(OllamaChatModel.class); + default: + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); + } + } + + @Override + public ImageModel getDefaultImageModel(AiPlatformEnum platform) { + //noinspection EnhancedSwitchMigration + switch (platform) { + case TONG_YI: + return SpringUtil.getBean(TongYiImagesModel.class); + case YI_YAN: + return SpringUtil.getBean(QianFanImageModel.class); + case ZHI_PU: + return SpringUtil.getBean(ZhiPuAiImageModel.class); + case OPENAI: + return SpringUtil.getBean(OpenAiImageModel.class); + case STABLE_DIFFUSION: + return SpringUtil.getBean(StabilityAiImageModel.class); + default: + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); + } + } + + @Override + public ImageModel getOrCreateImageModel(AiPlatformEnum platform, String apiKey, String url) { + //noinspection EnhancedSwitchMigration + switch (platform) { + case TONG_YI: + return buildTongYiImagesModel(apiKey); + case YI_YAN: + return buildQianFanImageModel(apiKey); + case ZHI_PU: + return buildZhiPuAiImageModel(apiKey, url); + case OPENAI: + return buildOpenAiImageModel(apiKey, url); + case STABLE_DIFFUSION: + return buildStabilityAiImageModel(apiKey, url); + default: + throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); + } + } + + @Override + public MidjourneyApi getOrCreateMidjourneyApi(String apiKey, String url) { + String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, url); + return Singleton.get(cacheKey, (Func0) () -> { + YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class).getMidjourney(); + return new MidjourneyApi(url, apiKey, properties.getNotifyUrl()); + }); + } + + @Override + public SunoApi getOrCreateSunoApi(String apiKey, String url) { + String cacheKey = buildClientCacheKey(SunoApi.class, AiPlatformEnum.SUNO.getPlatform(), apiKey, url); + return Singleton.get(cacheKey, (Func0) () -> new SunoApi(url)); + } + + private static String buildClientCacheKey(Class clazz, Object... params) { + if (ArrayUtil.isEmpty(params)) { + return clazz.getName(); + } + return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_")); + } + + // ========== 各种创建 spring-ai 客户端的方法 ========== + + /** + * 可参考 {@link TongYiAutoConfiguration#tongYiChatClient(Generation, TongYiChatProperties, TongYiConnectionProperties)} + */ + private static TongYiChatModel buildTongYiChatModel(String key) { + com.alibaba.dashscope.aigc.generation.Generation generation = SpringUtil.getBean(Generation.class); + TongYiChatProperties chatOptions = SpringUtil.getBean(TongYiChatProperties.class); + // TODO @芋艿:貌似 apiKey 是全局唯一的???得测试下 + // TODO @芋艿:貌似阿里云不是增量返回的 + // 该 issue 进行跟进中 https://github.com/alibaba/spring-cloud-alibaba/issues/3790 + TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); + connectionProperties.setApiKey(key); + return new TongYiAutoConfiguration().tongYiChatClient(generation, chatOptions, connectionProperties); + } + + private static TongYiImagesModel buildTongYiImagesModel(String key) { + ImageSynthesis imageSynthesis = SpringUtil.getBean(ImageSynthesis.class); + TongYiImagesProperties imagesOptions = SpringUtil.getBean(TongYiImagesProperties.class); + TongYiConnectionProperties connectionProperties = new TongYiConnectionProperties(); + connectionProperties.setApiKey(key); + return new TongYiAutoConfiguration().tongYiImagesClient(imageSynthesis, imagesOptions, connectionProperties); + } + + /** + * 可参考 {@link QianFanAutoConfiguration#qianFanChatModel(QianFanConnectionProperties, QianFanChatProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} + */ + private static QianFanChatModel buildYiYanChatModel(String key) { + List keys = StrUtil.split(key, '|'); + Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); + String appKey = keys.get(0); + String secretKey = keys.get(1); + QianFanApi qianFanApi = new QianFanApi(appKey, secretKey); + return new QianFanChatModel(qianFanApi); + } + + /** + * 可参考 {@link QianFanAutoConfiguration#qianFanImageModel(QianFanConnectionProperties, QianFanImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} + */ + private QianFanImageModel buildQianFanImageModel(String key) { + List keys = StrUtil.split(key, '|'); + Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); + String appKey = keys.get(0); + String secretKey = keys.get(1); + QianFanImageApi qianFanApi = new QianFanImageApi(appKey, secretKey); + return new QianFanImageModel(qianFanApi); + } + + /** + * 可参考 {@link YudaoAiAutoConfiguration#deepSeekChatModel(YudaoAiProperties)} + */ + private static DeepSeekChatModel buildDeepSeekChatModel(String apiKey) { + return new DeepSeekChatModel(apiKey); + } + + /** + * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiChatModel( + * ZhiPuAiConnectionProperties, ZhiPuAiChatProperties, RestClient.Builder, List, FunctionCallbackContext, RetryTemplate, ResponseErrorHandler)} + */ + private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { + url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); + ZhiPuAiApi zhiPuAiApi = new ZhiPuAiApi(url, apiKey); + return new ZhiPuAiChatModel(zhiPuAiApi); + } + + /** + * 可参考 {@link ZhiPuAiAutoConfiguration#zhiPuAiImageModel( + * ZhiPuAiConnectionProperties, ZhiPuAiImageProperties, RestClient.Builder, RetryTemplate, ResponseErrorHandler)} + */ + private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) { + url = StrUtil.blankToDefault(url, ZhiPuAiConnectionProperties.DEFAULT_BASE_URL); + ZhiPuAiImageApi zhiPuAiApi = new ZhiPuAiImageApi(url, apiKey, RestClient.builder()); + return new ZhiPuAiImageModel(zhiPuAiApi); + } + + /** + * 可参考 {@link YudaoAiAutoConfiguration#xingHuoChatClient(YudaoAiProperties)} + */ + private static XingHuoChatModel buildXingHuoChatModel(String key) { + List keys = StrUtil.split(key, '|'); + Assert.equals(keys.size(), 3, "XingHuoChatClient 的密钥需要 (appid|appKey|secretKey) 格式"); + String appKey = keys.get(1); + String secretKey = keys.get(2); + return new XingHuoChatModel(appKey, secretKey); + } + + /** + * 可参考 {@link OpenAiAutoConfiguration} + */ + private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) { + url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL); + OpenAiApi openAiApi = new OpenAiApi(url, openAiToken); + return new OpenAiChatModel(openAiApi); + } + + /** + * 可参考 {@link OpenAiAutoConfiguration} + */ + private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) { + url = StrUtil.blankToDefault(url, ApiUtils.DEFAULT_BASE_URL); + OpenAiImageApi openAiApi = new OpenAiImageApi(url, openAiToken, RestClient.builder()); + return new OpenAiImageModel(openAiApi); + } + + /** + * 可参考 {@link OllamaAutoConfiguration} + */ + private static OllamaChatModel buildOllamaChatModel(String url) { + OllamaApi ollamaApi = new OllamaApi(url); + return new OllamaChatModel(ollamaApi); + } + + private StabilityAiImageModel buildStabilityAiImageModel(String apiKey, String url) { + url = StrUtil.blankToDefault(url, StabilityAiApi.DEFAULT_BASE_URL); + StabilityAiApi stabilityAiApi = new StabilityAiApi(apiKey, StabilityAiApi.DEFAULT_IMAGE_MODEL, url); + return new StabilityAiImageModel(stabilityAiApi); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java new file mode 100644 index 000000000..1437404e8 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatModel.java @@ -0,0 +1,165 @@ +package cn.iocoder.yudao.framework.ai.core.model.deepseek; + +import cn.hutool.core.lang.Assert; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.metadata.ChatGenerationMetadata; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.model.Generation; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.model.ModelOptionsUtils; +import org.springframework.ai.openai.OpenAiChatOptions; +import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.openai.metadata.OpenAiChatResponseMetadata; +import org.springframework.ai.retry.RetryUtils; +import org.springframework.http.ResponseEntity; +import org.springframework.retry.support.RetryTemplate; +import reactor.core.publisher.Flux; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions.MODEL_DEFAULT; + +/** + * DeepSeek {@link ChatModel} 实现类 + * + * @author fansili + */ +@Slf4j +public class DeepSeekChatModel implements ChatModel { + + private static final String BASE_URL = "https://api.deepseek.com"; + + private final DeepSeekChatOptions defaultOptions; + private final RetryTemplate retryTemplate; + + /** + * DeepSeek 兼容 OpenAI 的 HTTP 接口,所以复用它的实现,简化接入成本 + * + * 不过要注意,DeepSeek 没有完全兼容,所以不能使用 {@link org.springframework.ai.openai.OpenAiChatModel} 调用,但是实现会参考它 + */ + private final OpenAiApi openAiApi; + + public DeepSeekChatModel(String apiKey) { + this(apiKey, DeepSeekChatOptions.builder().model(MODEL_DEFAULT).temperature(0.7F).build()); + } + + public DeepSeekChatModel(String apiKey, DeepSeekChatOptions options) { + this(apiKey, options, RetryUtils.DEFAULT_RETRY_TEMPLATE); + } + + public DeepSeekChatModel(String apiKey, DeepSeekChatOptions options, RetryTemplate retryTemplate) { + Assert.notEmpty(apiKey, "apiKey 不能为空"); + Assert.notNull(options, "options 不能为空"); + Assert.notNull(retryTemplate, "retryTemplate 不能为空"); + this.openAiApi = new OpenAiApi(BASE_URL, apiKey); + this.defaultOptions = options; + this.retryTemplate = retryTemplate; + } + + @Override + public ChatResponse call(Prompt prompt) { + OpenAiApi.ChatCompletionRequest request = createRequest(prompt, false); + return this.retryTemplate.execute(ctx -> { + // 1.1 发起调用 + ResponseEntity completionEntity = openAiApi.chatCompletionEntity(request); + // 1.2 校验结果 + OpenAiApi.ChatCompletion chatCompletion = completionEntity.getBody(); + if (chatCompletion == null) { + log.warn("No chat completion returned for prompt: {}", prompt); + return new ChatResponse(List.of()); + } + List choices = chatCompletion.choices(); + if (choices == null) { + log.warn("No choices returned for prompt: {}", prompt); + return new ChatResponse(List.of()); + } + + // 2. 转换 ChatResponse 返回 + List generations = choices.stream().map(choice -> { + Generation generation = new Generation(choice.message().content(), toMap(chatCompletion.id(), choice)); + if (choice.finishReason() != null) { + generation.withGenerationMetadata(ChatGenerationMetadata.from(choice.finishReason().name(), null)); + } + return generation; + }).toList(); + return new ChatResponse(generations, + OpenAiChatResponseMetadata.from(completionEntity.getBody())); + }); + } + + private Map toMap(String id, OpenAiApi.ChatCompletion.Choice choice) { + Map map = new HashMap<>(); + OpenAiApi.ChatCompletionMessage message = choice.message(); + if (message.role() != null) { + map.put("role", message.role().name()); + } + if (choice.finishReason() != null) { + map.put("finishReason", choice.finishReason().name()); + } + map.put("id", id); + return map; + } + + @Override + public Flux stream(Prompt prompt) { + OpenAiApi.ChatCompletionRequest request = createRequest(prompt, true); + return this.retryTemplate.execute(ctx -> { + // 1. 发起调用 + Flux response = this.openAiApi.chatCompletionStream(request); + return response.map(chatCompletion -> { + String id = chatCompletion.id(); + // 2. 转换 ChatResponse 返回 + List generations = chatCompletion.choices().stream().map(choice -> { + String finish = (choice.finishReason() != null ? choice.finishReason().name() : ""); + String role = (choice.delta().role() != null ? choice.delta().role().name() : ""); + if (choice.finishReason() == OpenAiApi.ChatCompletionFinishReason.STOP) { + // 兜底处理 DeepSeek 返回 STOP 时,role 为空的情况 + role = OpenAiApi.ChatCompletionMessage.Role.ASSISTANT.name(); + } + Generation generation = new Generation(choice.delta().content(), + Map.of("id", id, "role", role, "finishReason", finish)); + if (choice.finishReason() != null) { + generation = generation.withGenerationMetadata( + ChatGenerationMetadata.from(choice.finishReason().name(), null)); + } + return generation; + }).toList(); + return new ChatResponse(generations); + }); + }); + } + + OpenAiApi.ChatCompletionRequest createRequest(Prompt prompt, boolean stream) { + // 1. 构建 ChatCompletionMessage 对象 + List chatCompletionMessages = prompt.getInstructions().stream().map(m -> + new OpenAiApi.ChatCompletionMessage(m.getContent(), OpenAiApi.ChatCompletionMessage.Role.valueOf(m.getMessageType().name()))).toList(); + OpenAiApi.ChatCompletionRequest request = new OpenAiApi.ChatCompletionRequest(chatCompletionMessages, stream); + + // 2.1 补充 prompt 内置的 options + if (prompt.getOptions() != null) { + if (prompt.getOptions() instanceof ChatOptions runtimeOptions) { + OpenAiChatOptions updatedRuntimeOptions = ModelOptionsUtils.copyToTarget(runtimeOptions, + ChatOptions.class, OpenAiChatOptions.class); + request = ModelOptionsUtils.merge(updatedRuntimeOptions, request, OpenAiApi.ChatCompletionRequest.class); + } else { + throw new IllegalArgumentException("Prompt options are not of type ChatOptions: " + + prompt.getOptions().getClass().getSimpleName()); + } + } + // 2.2 补充默认 options + if (this.defaultOptions != null) { + request = ModelOptionsUtils.merge(request, this.defaultOptions, OpenAiApi.ChatCompletionRequest.class); + } + return request; + } + + @Override + public ChatOptions getDefaultOptions() { + return DeepSeekChatOptions.fromOptions(defaultOptions); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatOptions.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatOptions.java new file mode 100644 index 000000000..e07e3f086 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/deepseek/DeepSeekChatOptions.java @@ -0,0 +1,55 @@ +package cn.iocoder.yudao.framework.ai.core.model.deepseek; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.ai.chat.prompt.ChatOptions; + +/** + * DeepSeek {@link ChatOptions} 实现类 + * + * 参考文档:快速开始 + * + * @author fansili + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class DeepSeekChatOptions implements ChatOptions { + + public static final String MODEL_DEFAULT = "deepseek-chat"; + + /** + * 模型 + */ + private String model; + /** + * 温度 + */ + private Float temperature; + /** + * 最大 Token + */ + private Integer maxTokens; + /** + * topP + */ + private Float topP; + + @Override + public Integer getTopK() { + return null; + } + + public static DeepSeekChatOptions fromOptions(DeepSeekChatOptions fromOptions) { + return DeepSeekChatOptions.builder() + .model(fromOptions.getModel()) + .temperature(fromOptions.getTemperature()) + .maxTokens(fromOptions.getMaxTokens()) + .topP(fromOptions.getTopP()) + .build(); + } + +} diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java new file mode 100644 index 000000000..55091c78d --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/model/midjourney/api/MidjourneyApi.java @@ -0,0 +1,348 @@ +package cn.iocoder.yudao.framework.ai.core.model.midjourney.api; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.openai.api.ApiUtils; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatusCode; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * Midjourney API + * + * @author fansili + * @since 1.0 + */ +@Slf4j +public class MidjourneyApi { + + private final Predicate STATUS_PREDICATE = status -> !status.is2xxSuccessful(); + + private final Function>> EXCEPTION_FUNCTION = + reqParam -> response -> response.bodyToMono(String.class).handle((responseBody, sink) -> { + HttpRequest request = response.request(); + log.error("[midjourney-api] 调用失败!请求方式:[{}],请求地址:[{}],请求参数:[{}],响应数据: [{}]", + request.getMethod(), request.getURI(), reqParam, responseBody); + sink.error(new IllegalStateException("[midjourney-api] 调用失败!")); + }); + + private final WebClient webClient; + + /** + * 回调地址 + */ + private final String notifyUrl; + + public MidjourneyApi(String baseUrl, String apiKey, String notifyUrl) { + this.webClient = WebClient.builder() + .baseUrl(baseUrl) + .defaultHeaders(ApiUtils.getJsonContentHeaders(apiKey)) + .build(); + this.notifyUrl = notifyUrl; + } + + /** + * imagine - 根据提示词提交绘画任务 + * + * @param request 请求 + * @return 提交结果 + */ + public SubmitResponse imagine(ImagineRequest request) { + if (StrUtil.isEmpty(request.getNotifyHook())) { + request.setNotifyHook(notifyUrl); + } + String response = post("/submit/imagine", request); + return JsonUtils.parseObject(response, SubmitResponse.class); + } + + /** + * action - 放大、缩小、U1、U2... + * + * @param request 请求 + * @return 提交结果 + */ + public SubmitResponse action(ActionRequest request) { + if (StrUtil.isEmpty(request.getNotifyHook())) { + request.setNotifyHook(notifyUrl); + } + String response = post("/submit/action", request); + return JsonUtils.parseObject(response, SubmitResponse.class); + } + + /** + * 批量查询 task 任务 + * + * @param ids 任务编号数组 + * @return task 任务 + */ + public List getTaskList(Collection ids) { + String res = post("/task/list-by-condition", ImmutableMap.of("ids", ids)); + return JsonUtils.parseArray(res, Notify.class); + } + + private String post(String uri, Object body) { + return webClient.post() + .uri(uri) + .body(Mono.just(JsonUtils.toJsonString(body)), String.class) + .retrieve() + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(body)) + .bodyToMono(String.class) + .block(); + } + + // ========== record 结构 ========== + + /** + * Imagine 请求(生成图片) + */ + @Data + public static final class ImagineRequest { + + /** + * 垫图(参考图) base64 数组 + */ + private List base64Array; + /** + * 提示词 + */ + private String prompt; + /** + * 通知地址 + */ + private String notifyHook; + /** + * 自定义参数 + */ + private String state; + + public ImagineRequest(List base64Array, String prompt, String notifyHook, String state) { + this.base64Array = base64Array; + this.prompt = prompt; + this.notifyHook = notifyHook; + this.state = state; + } + + public static String buildState(Integer width, Integer height, String version, String model) { + StringBuilder params = new StringBuilder(); + // --ar 来设置尺寸 + params.append(String.format(" --ar %s:%s ", width, height)); + // --niji 模型 + if (ModelEnum.NIJI.getModel().equals(model)) { + params.append(String.format(" --niji %s ", version)); + } else { + params.append(String.format(" --v %s ", version)); + } + return params.toString(); + } + + } + + /** + * Action 请求 + */ + @Data + public static final class ActionRequest { + + private String customId; + private String taskId; + private String notifyHook; + + public ActionRequest(String taskId, String customId, String notifyHook) { + this.customId = customId; + this.taskId = taskId; + this.notifyHook = notifyHook; + } + + } + + /** + * Submit 统一返回 + * + * @param code 状态码: 1(提交成功), 21(已存在), 22(排队中), other(错误) + * @param description 描述 + * @param properties 扩展字段 + * @param result 任务ID + */ + public record SubmitResponse(String code, + String description, + Map properties, + String result) { + } + + /** + * 通知 request + * + * @param id job id + * @param action 任务类型 {@link TaskActionEnum} + * @param status 任务状态 {@link TaskStatusEnum} + * @param prompt 提示词 + * @param promptEn 提示词-英文 + * @param description 任务描述 + * @param state 自定义参数 + * @param submitTime 提交时间 + * @param startTime 开始执行时间 + * @param finishTime 结束时间 + * @param imageUrl 图片url + * @param progress 任务进度 + * @param failReason 失败原因 + * @param buttons 任务完成后的可执行按钮 + */ + public record Notify(String id, + String action, + String status, + + String prompt, + String promptEn, + + String description, + String state, + + Long submitTime, + Long startTime, + Long finishTime, + + String imageUrl, + String progress, + String failReason, + List