feat: Add chat front and backend
| | |
| | | package tech.aiflowy.ai.controller; |
| | | |
| | | import tech.aiflowy.ai.entity.*; |
| | | import tech.aiflowy.ai.mapper.AiBotConversationMessageMapper; |
| | | import tech.aiflowy.ai.service.*; |
| | | import tech.aiflowy.common.ai.ChatManager; |
| | | import tech.aiflowy.common.ai.MySseEmitter; |
| | |
| | | private final AiBotWorkflowService aiBotWorkflowService; |
| | | private final AiBotKnowledgeService aiBotKnowledgeService; |
| | | private final AiBotMessageService aiBotMessageService; |
| | | |
| | | @Resource |
| | | private AiBotConversationMessageService aiBotConversationMessageService; |
| | | @Resource |
| | | private AiBotConversationMessageMapper aiBotConversationMessageMapper; |
| | | public AiBotController(AiBotService service, AiLlmService aiLlmService, AiBotWorkflowService aiBotWorkflowService, AiBotKnowledgeService aiBotKnowledgeService, AiBotMessageService aiBotMessageService) { |
| | | super(service); |
| | | this.aiLlmService = aiLlmService; |
| | |
| | | |
| | | Llm llm = aiLlm.toLlm(); |
| | | |
| | | AiBotMessageMemory memory = new AiBotMessageMemory(botId, SaTokenUtil.getLoginAccount().getId(), sessionId, aiBotMessageService); |
| | | AiBotMessageMemory memory = new AiBotMessageMemory(botId, SaTokenUtil.getLoginAccount().getId(), |
| | | sessionId, aiBotMessageService, aiBotConversationMessageMapper, |
| | | aiBotConversationMessageService); |
| | | |
| | | final HistoriesPrompt historiesPrompt = new HistoriesPrompt(); |
| | | historiesPrompt.setSystemMessage(SystemMessage.of((String) llmOptions.get("systemPrompt"))); |
| | |
| | | package tech.aiflowy.ai.controller; |
| | | |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import tech.aiflowy.ai.entity.AiBotMessage; |
| | | import tech.aiflowy.ai.service.AiBotMessageService; |
| | | import tech.aiflowy.common.domain.Result; |
| | |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import tech.aiflowy.common.web.jsonbody.JsonBody; |
| | | |
| | | import java.math.BigInteger; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | |
| | | @RestController |
| | | @RequestMapping("/api/v1/aiBotMessage") |
| | | public class AiBotMessageController extends BaseCurdController<AiBotMessageService, AiBotMessage> { |
| | | public AiBotMessageController(AiBotMessageService service) { |
| | | private final AiBotMessageService aiBotMessageService; |
| | | |
| | | public AiBotMessageController(AiBotMessageService service, AiBotMessageService aiBotMessageService) { |
| | | super(service); |
| | | this.aiBotMessageService = aiBotMessageService; |
| | | } |
| | | |
| | | |
| | |
| | | @Override |
| | | public Result list(AiBotMessage entity, Boolean asTree, String sortKey, String sortType) { |
| | | |
| | | if (entity.getBotId() == null || StringUtil.noText(entity.getSessionId())){ |
| | | if (entity.getBotId() == null || StringUtil.noText(entity.getSessionId())) { |
| | | return Result.fail(); |
| | | } |
| | | |
| | |
| | | entity.setAccountId(SaTokenUtil.getLoginAccount().getId()); |
| | | return super.onSaveOrUpdateBefore(entity, isSave); |
| | | } |
| | | |
| | | @GetMapping("externalList") |
| | | public Result externalList(@RequestParam(value = "botId", required = true) BigInteger botId) { |
| | | |
| | | return aiBotMessageService.externalList(botId); |
| | | } |
| | | } |
| New file |
| | |
| | | package tech.aiflowy.ai.entity; |
| | | |
| | | import com.mybatisflex.annotation.Table; |
| | | import tech.aiflowy.ai.entity.base.AiBotConversationMessageBase; |
| | | |
| | | |
| | | /** |
| | | * 实体类。 |
| | | * |
| | | * @author Administrator |
| | | * @since 2025-04-15 |
| | | */ |
| | | @Table("tb_ai_bot_conversation_message") |
| | | public class AiBotConversationMessage extends AiBotConversationMessageBase { |
| | | } |
| | |
| | | package tech.aiflowy.ai.entity; |
| | | |
| | | import tech.aiflowy.ai.mapper.AiBotConversationMessageMapper; |
| | | import tech.aiflowy.ai.service.AiBotConversationMessageService; |
| | | import tech.aiflowy.ai.service.AiBotMessageService; |
| | | import cn.hutool.core.collection.CollectionUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.serializer.SerializerFeature; |
| | | import com.mybatisflex.core.query.QueryWrapper; |
| | | import tech.aiflowy.common.satoken.util.SaTokenUtil; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.math.BigInteger; |
| | | import java.util.ArrayList; |
| | | import java.util.Date; |
| | |
| | | private final BigInteger accountId; |
| | | private final String sessionId; |
| | | private final AiBotMessageService messageService; |
| | | |
| | | public AiBotMessageMemory(BigInteger botId, BigInteger accountId, String sessionId, AiBotMessageService messageService) { |
| | | private final AiBotConversationMessageMapper aiBotConversationMessageMapper; |
| | | private final AiBotConversationMessageService aiBotConversationService; |
| | | public AiBotMessageMemory(BigInteger botId, BigInteger accountId, String sessionId, AiBotMessageService messageService, |
| | | AiBotConversationMessageMapper aiBotConversationMessageMapper, |
| | | AiBotConversationMessageService aiBotConversationService ) { |
| | | this.botId = botId; |
| | | this.accountId = accountId; |
| | | this.sessionId = sessionId; |
| | | this.messageService = messageService; |
| | | this.aiBotConversationMessageMapper = aiBotConversationMessageMapper; |
| | | this.aiBotConversationService = aiBotConversationService; |
| | | } |
| | | |
| | | @Override |
| | |
| | | aiMessage.setContent(((SystemMessage) message).getContent()); |
| | | } |
| | | if (StrUtil.isNotEmpty(aiMessage.getContent())) { |
| | | AiBotConversationMessage aiBotConversation = aiBotConversationMessageMapper.selectOneById(aiMessage.getSessionId()); |
| | | if (aiBotConversation == null){ |
| | | AiBotConversationMessage conversation = new AiBotConversationMessage(); |
| | | conversation.setSessionId(aiMessage.getSessionId()); |
| | | conversation.setTitle(aiMessage.getContent()); |
| | | conversation.setBotId(aiMessage.getBotId()); |
| | | conversation.setCreated(new Date()); |
| | | conversation.setAccountId(SaTokenUtil.getLoginAccount().getId()); |
| | | aiBotConversationService.save(conversation); |
| | | } |
| | | messageService.save(aiMessage); |
| | | } |
| | | } |
| New file |
| | |
| | | package tech.aiflowy.ai.entity.base; |
| | | |
| | | import java.io.Serializable; |
| | | import java.math.BigInteger; |
| | | |
| | | public class AiBotConversationBase implements Serializable { |
| | | |
| | | private BigInteger sessionId; |
| | | |
| | | private String title; |
| | | |
| | | public BigInteger getSessionId() { |
| | | return sessionId; |
| | | } |
| | | |
| | | public void setSessionId(BigInteger sessionId) { |
| | | this.sessionId = sessionId; |
| | | } |
| | | |
| | | public String getTitle() { |
| | | return title; |
| | | } |
| | | |
| | | public void setTitle(String title) { |
| | | this.title = title; |
| | | } |
| | | } |
| New file |
| | |
| | | package tech.aiflowy.ai.entity.base; |
| | | |
| | | import com.mybatisflex.annotation.Column; |
| | | import com.mybatisflex.annotation.Id; |
| | | import java.io.Serializable; |
| | | import java.math.BigInteger; |
| | | import java.util.Date; |
| | | |
| | | |
| | | public class AiBotConversationMessageBase implements Serializable { |
| | | |
| | | private static final long serialVersionUID = 1L; |
| | | |
| | | /** |
| | | * 会话id |
| | | */ |
| | | @Id(comment = "会话id") |
| | | private String sessionId; |
| | | |
| | | /** |
| | | * 会话标题 |
| | | */ |
| | | @Column(comment = "会话标题") |
| | | private String title; |
| | | |
| | | /** |
| | | * BotId |
| | | */ |
| | | @Column(comment = "BotId") |
| | | private BigInteger BotId; |
| | | |
| | | /** |
| | | * 创建时间 |
| | | */ |
| | | @Column(comment = "创建时间") |
| | | private Date created; |
| | | |
| | | /** |
| | | * 用户id |
| | | */ |
| | | @Column(comment = "用户id") |
| | | private BigInteger accountId; |
| | | |
| | | public String getSessionId() { |
| | | return sessionId; |
| | | } |
| | | |
| | | public void setSessionId(String sessionId) { |
| | | this.sessionId = sessionId; |
| | | } |
| | | |
| | | public String getTitle() { |
| | | return title; |
| | | } |
| | | |
| | | public void setTitle(String title) { |
| | | this.title = title; |
| | | } |
| | | |
| | | public BigInteger getBotId() { |
| | | return BotId; |
| | | } |
| | | |
| | | public void setBotId(BigInteger botId) { |
| | | BotId = botId; |
| | | } |
| | | |
| | | public Date getCreated() { |
| | | return created; |
| | | } |
| | | |
| | | public void setCreated(Date created) { |
| | | this.created = created; |
| | | } |
| | | |
| | | public BigInteger getAccountId() { |
| | | return accountId; |
| | | } |
| | | |
| | | public void setAccountId(BigInteger accountId) { |
| | | this.accountId = accountId; |
| | | } |
| | | } |
| New file |
| | |
| | | package tech.aiflowy.ai.mapper; |
| | | |
| | | import com.mybatisflex.core.BaseMapper; |
| | | import tech.aiflowy.ai.entity.AiBotConversationMessage; |
| | | |
| | | /** |
| | | * 映射层。 |
| | | * |
| | | * @author Administrator |
| | | * @since 2025-04-15 |
| | | */ |
| | | public interface AiBotConversationMessageMapper extends BaseMapper<AiBotConversationMessage> { |
| | | |
| | | } |
| New file |
| | |
| | | package tech.aiflowy.ai.service; |
| | | |
| | | import com.mybatisflex.core.service.IService; |
| | | import tech.aiflowy.ai.entity.AiBotConversationMessage; |
| | | |
| | | /** |
| | | * 服务层。 |
| | | * |
| | | * @author Administrator |
| | | * @since 2025-04-15 |
| | | */ |
| | | public interface AiBotConversationMessageService extends IService<AiBotConversationMessage> { |
| | | |
| | | } |
| | |
| | | |
| | | import tech.aiflowy.ai.entity.AiBotMessage; |
| | | import com.mybatisflex.core.service.IService; |
| | | import tech.aiflowy.common.domain.Result; |
| | | |
| | | import java.math.BigInteger; |
| | | |
| | | /** |
| | | * Bot 消息记录表 服务层。 |
| | |
| | | */ |
| | | public interface AiBotMessageService extends IService<AiBotMessage> { |
| | | |
| | | Result externalList(BigInteger botId); |
| | | } |
| New file |
| | |
| | | package tech.aiflowy.ai.service.impl; |
| | | |
| | | import com.mybatisflex.spring.service.impl.ServiceImpl; |
| | | import tech.aiflowy.ai.entity.AiBotConversationMessage; |
| | | import tech.aiflowy.ai.mapper.AiBotConversationMessageMapper; |
| | | import tech.aiflowy.ai.service.AiBotConversationMessageService; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | /** |
| | | * 服务层实现。 |
| | | * |
| | | * @author Administrator |
| | | * @since 2025-04-15 |
| | | */ |
| | | @Service |
| | | public class AiBotConversationMessageServiceImpl extends ServiceImpl<AiBotConversationMessageMapper, AiBotConversationMessage> implements AiBotConversationMessageService{ |
| | | |
| | | } |
| | |
| | | package tech.aiflowy.ai.service.impl; |
| | | |
| | | import com.mybatisflex.core.query.QueryWrapper; |
| | | import tech.aiflowy.ai.entity.AiBotConversationMessage; |
| | | import tech.aiflowy.ai.entity.AiBotMessage; |
| | | import tech.aiflowy.ai.mapper.AiBotMessageMapper; |
| | | import tech.aiflowy.ai.service.AiBotMessageService; |
| | | import com.mybatisflex.spring.service.impl.ServiceImpl; |
| | | import org.springframework.stereotype.Service; |
| | | import tech.aiflowy.common.domain.Result; |
| | | import tech.aiflowy.common.entity.LoginAccount; |
| | | import tech.aiflowy.common.satoken.util.SaTokenUtil; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.math.BigInteger; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | /** |
| | | * Bot 消息记录表 服务层实现。 |
| | |
| | | @Service |
| | | public class AiBotMessageServiceImpl extends ServiceImpl<AiBotMessageMapper, AiBotMessage> implements AiBotMessageService { |
| | | |
| | | @Resource |
| | | private AiBotMessageMapper aiBotMessageMapper; |
| | | |
| | | @Override |
| | | public Result externalList(BigInteger botId) { |
| | | LoginAccount loginUser = SaTokenUtil.getLoginAccount(); |
| | | BigInteger accountId = loginUser.getId(); |
| | | QueryWrapper query = QueryWrapper.create() |
| | | .select("session_id") // 选择字段 |
| | | .from("tb_ai_bot_message") |
| | | .where("bot_id = ?", botId) |
| | | .where("account_id = ? ", accountId); |
| | | AiBotMessage aiBotMessage = aiBotMessageMapper.selectOneByQuery(query); |
| | | if (aiBotMessage == null){ |
| | | return Result.fail(); |
| | | } |
| | | QueryWrapper queryConversation = QueryWrapper.create() |
| | | .select("session_id","title", "bot_id") // 选择字段 |
| | | .from("tb_ai_bot_conversation_message") |
| | | .where("bot_id = ?", botId) |
| | | .where("account_id = ? ", accountId); |
| | | List<AiBotConversationMessage> cons = aiBotMessageMapper.selectListByQueryAs(queryConversation, AiBotConversationMessage.class); |
| | | Map<String, Object> result = new HashMap<>(); |
| | | result.put("cons", cons); |
| | | return Result.success(result); |
| | | } |
| | | } |
| | |
| | | import { |
| | | DownOutlined, |
| | | PlusOutlined, |
| | | SmileOutlined, |
| | | } from '@ant-design/icons'; |
| | | import { Button, Dropdown, type GetProp, MenuProps, Space } from 'antd'; |
| | | import { AiProChat, ChatMessage } from "../components/AiProChat/AiProChat.tsx"; |
| | | import { getSessionId } from "../libs/getSessionId.ts"; |
| | | import { useSse } from "../hooks/useSse.ts"; |
| | | import { useParams } from "react-router-dom"; |
| | | import {useGet} from "../hooks/useApis.ts"; |
| | | |
| | | const defaultConversationsItems = [ |
| | | { |
| | |
| | | |
| | | export const ExternalBot: React.FC = () => { |
| | | |
| | | const modelItems: MenuProps['items'] = [ |
| | | { |
| | | key: '1', |
| | | label: '通义千问', |
| | | }, |
| | | { |
| | | key: '2', |
| | | label: '星火大模型', |
| | | icon: <SmileOutlined /> |
| | | } |
| | | ]; |
| | | |
| | | const [largeModel, setLargeModel] = useState("通义千问"); |
| | | // ==================== Style ==================== |
| | | const { styles } = useStyle(); |
| | |
| | | const [conversationsItems, setConversationsItems] = React.useState(defaultConversationsItems); |
| | | |
| | | const [activeKey, setActiveKey] = React.useState(defaultConversationsItems[0].key); |
| | | const params = useParams(); |
| | | |
| | | const { start: startChat } = useSse("/api/v1/aiBot/chat"); |
| | | |
| | | const {result: llms} = useGet('/api/v1/aiLlm/list') |
| | | const {result: conversationResult} = useGet('/api/v1/aiBotMessage/externalList',{"botId": params?.id}) |
| | | console.log('conversationResult',conversationResult) |
| | | const getOptions = (options: { id: any; title: any }[]): { key: any; label: any }[] => { |
| | | if (options) { |
| | | return options.map((item) => ({ |
| | | key: item.id, |
| | | label: item.title, |
| | | })); |
| | | } |
| | | return []; |
| | | }; |
| | | const modelItems: MenuProps['items'] = getOptions(llms?.data) |
| | | const [chats, setChats] = useState<ChatMessage[]>([]); |
| | | |
| | | const params = useParams(); |
| | | console.log('params',params) |
| | | // ==================== Runtime ==================== |
| | | const [agent] = useXAgent({ |
| | | request: async ({ message }, { onSuccess }) => { |
| | |
| | | /> |
| | | </div> |
| | | <div className={styles.chat}> |
| | | <div> |
| | | <Dropdown |
| | | menu={{ |
| | | items: modelItems, |
| | | onClick: (item) => { |
| | | console.log('item',item); |
| | | // 更新 largeModel 状态为选中的模型名称 |
| | | // @ts-ignore |
| | | setLargeModel(item.domEvent.target.innerText); |
| | | }, |
| | | }} |
| | | > |
| | | <a onClick={(e) => { |
| | | e.preventDefault(); |
| | | }}> |
| | | <Space> |
| | | {largeModel} {/* 显示当前选中的模型名称 */} |
| | | <DownOutlined /> |
| | | </Space> |
| | | </a> |
| | | </Dropdown> |
| | | </div> |
| | | {/*<div>*/} |
| | | {/* <Dropdown*/} |
| | | {/* menu={{*/} |
| | | {/* items: modelItems,*/} |
| | | {/* onClick: (item) => {*/} |
| | | {/* console.log('item',item);*/} |
| | | {/* // 更新 largeModel 状态为选中的模型名称*/} |
| | | {/* // @ts-ignore*/} |
| | | {/* setLargeModel(item.domEvent.target.innerText);*/} |
| | | {/* },*/} |
| | | {/* }}*/} |
| | | {/* >*/} |
| | | {/* <a onClick={(e) => {*/} |
| | | {/* e.preventDefault();*/} |
| | | {/* }}>*/} |
| | | {/* <Space>*/} |
| | | {/* {largeModel} /!* 显示当前选中的模型名称 *!/*/} |
| | | {/* <DownOutlined />*/} |
| | | {/* </Space>*/} |
| | | {/* </a>*/} |
| | | {/* </Dropdown>*/} |
| | | {/*</div>*/} |
| | | <AiProChat |
| | | chats={chats} |
| | | onChatsChange={setChats} // 确保正确传递 onChatsChange |
| | |
| | | ); |
| | | }; |
| | | |
| | | export default ExternalBot; |
| | | |
| | | export default { |
| | | path: "/ai/externalBot/:id", |
| | | element: ExternalBot, |
| | | frontEnable: true, |
| | | }; |
| | |
| | | file.type === "application/markdown" || |
| | | file.type === "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || |
| | | file.name.endsWith(".md"); |
| | | const isLt15M = file.size / 1024 / 1024 < 15; |
| | | const isLt15M = file.size / 1024 / 1024 < 200; |
| | | |
| | | if (!isAllowedType) { |
| | | message.error("仅支持 txt, pdf, md, docx 格式的文件!"); |
| | |
| | | setDataPreView([]); |
| | | setFileList([]); |
| | | }}>取消导入</Button> |
| | | <Button type="dashed" disabled={disabledConfirm} onClick={() => { |
| | | <Button type="dashed" onClick={() => { |
| | | setPreviewListLoading({ spinning: true,tip: "正在保存文件..."}) |
| | | setDisabledConfirm(true) |
| | | // 构造 FormData 对象 |
| | |
| | | <div> |
| | | <span> |
| | | 外部访问地址 <a |
| | | href={window.location.href.substring(0, window.location.href.indexOf('/ai')) + '/bot/chat/' + detail?.data.id} |
| | | href={window.location.href.substring(0, window.location.href.indexOf('/ai')) + '/ai/externalBot/' + detail?.data.id} |
| | | target={"_blank"}>打开</a> |
| | | </span> |
| | | <TextArea readOnly disabled |
| | |
| | | import Layout from "../components/Layout"; |
| | | import Login from "../pages/commons/login.tsx"; |
| | | import React from "react"; |
| | | import {ExternalBot} from "../pages/ExternalBot.tsx"; |
| | | |
| | | /** |
| | | * 登录成功之后的路由和菜单配置 |
| | |
| | | path: "/login", |
| | | element: <Login/>, |
| | | }, |
| | | { |
| | | path: "/ai/externalBot", |
| | | element: <ExternalBot/>, |
| | | }, |
| | | ...frontRouters |
| | | ]; |
| | | |