aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiBotController.java
@@ -12,10 +12,14 @@ import com.agentsflex.core.message.HumanMessage; import com.agentsflex.core.message.SystemMessage; import com.agentsflex.core.prompt.HistoriesPrompt; import com.agentsflex.core.prompt.ToolPrompt; import com.agentsflex.core.util.CollectionUtil; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializeConfig; import com.jfinal.template.stat.ast.Break; import com.mybatisflex.core.query.QueryWrapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -41,10 +45,11 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.math.BigInteger; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; /** * 控制层。 @@ -66,6 +71,8 @@ private AiBotConversationMessageService aiBotConversationMessageService; @Resource private AiBotConversationMessageMapper aiBotConversationMessageMapper; private static final Logger logger = LoggerFactory.getLogger(AiBotController.class); public AiBotController(AiBotService service, AiLlmService aiLlmService, AiBotWorkflowService aiBotWorkflowService, AiBotKnowledgeService aiBotKnowledgeService, AiBotMessageService aiBotMessageService) { super(service); @@ -174,6 +181,7 @@ MySseEmitter emitter = new MySseEmitter((long) (1000 * 60 * 2)); final Boolean[] needClose = {true}; ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); // 统一使用流式处理,无论是否有 Function Calling llm.chatStream(historiesPrompt, new StreamResponseListener() { @@ -181,7 +189,7 @@ public void onMessage(ChatContext context, AiMessageResponse response) { try { RequestContextHolder.setRequestAttributes(sra, true); if (response != null){ if (response != null) { // 检查是否需要触发 Function Calling if (response.getFunctionCallers() != null && CollectionUtil.hasItems(response.getFunctionCallers())) { needClose[0] = false; @@ -422,45 +430,34 @@ } } List<FunctionCaller> functionCallers = aiMessageResponse.getFunctionCallers(); if (CollectionUtil.hasItems(functionCallers)) { needClose[0] = false; for (FunctionCaller functionCaller : functionCallers) { Object result = functionCaller.call(); if (ObjectUtil.isNotEmpty(result)) { String newPrompt = "请根据以下内容回答用户,内容是:\n" + result + "\n 用户的问题是:" + prompt; historiesPrompt.addMessageTemporary(new HumanMessage(newPrompt)); llm.chatStream(historiesPrompt, new StreamResponseListener() { @Override public void onMessage(ChatContext context, AiMessageResponse response) { needClose[0] = true; String content = response.getMessage().getContent(); Object messageContent = response.getMessage(); if (StringUtil.hasText(content)) { String jsonResult = JSON.toJSONString(messageContent); emitter.send(jsonResult); } } @Override public void onStop(ChatContext context) { if (needClose[0]) { System.out.println("function chat complete"); emitter.complete(); } historiesPrompt.clearTemporaryMessages(); } @Override public void onFailure(ChatContext context, Throwable throwable) { emitter.completeWithError(throwable); } }); System.out.println("function call 接收到的参数message:" + aiMessageResponse); llm.chatStream(ToolPrompt.of(aiMessageResponse), new StreamResponseListener() { @Override public void onMessage(ChatContext context, AiMessageResponse response) { System.out.println("function call <UNK>message<UNK>" + aiMessageResponse); String content = response.getMessage().getContent(); if (StringUtil.hasText(content)) { System.out.println("if content" + content); emitter.send(JSON.toJSONString(response.getMessage())); } } } @Override public void onStop(ChatContext context) { System.out.println("function call complete"); emitter.complete(); } @Override public void onFailure(ChatContext context, Throwable throwable) { logger.error("function_call报错:",throwable); AiMessage aiMessage = new AiMessage(); aiMessage.setContent("未查询到相关信息..."); emitter.send(JSON.toJSONString(aiMessage)); System.out.println("function call complete with error"); } }); return JSON.toJSONString(messageContent); } aiflowy-modules/aiflowy-module-system/src/main/java/tech/aiflowy/system/controller/SysTokenController.java
New file @@ -0,0 +1,75 @@ package tech.aiflowy.system.controller; import cn.dev33.satoken.stp.StpUtil; import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.query.QueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import tech.aiflowy.common.domain.Result; import tech.aiflowy.common.web.controller.BaseCurdController; import tech.aiflowy.common.web.jsonbody.JsonBody; import tech.aiflowy.system.entity.SysToken; import tech.aiflowy.system.mapper.SysTokenMapper; import tech.aiflowy.system.service.SysTokenService; import javax.servlet.http.HttpServletRequest; import java.io.Serializable; import java.util.Collection; /** * iframe 嵌入用 Token 表 控制层。 * * @author Administrator * @since 2025-05-26 */ @RestController @RequestMapping("/api/v1/sysToken") public class SysTokenController extends BaseCurdController<SysTokenService, SysToken> { public SysTokenController(SysTokenService service) { super(service); } @Autowired private SysTokenService sysTokenService; @Autowired private SysTokenMapper sysTokenMapper; // 手动生成 Token 并绑定账号 @GetMapping("/generateToken") public Result generateToken() { return sysTokenService.saveGenerateToken(); } @PostMapping("/updateToken") public Result updateToken(@JsonBody SysToken sysToken) { return sysTokenService.updateToken(sysToken); } @Override protected Result onRemoveBefore(Collection<Serializable> ids) { for (Serializable id : ids){ SysToken sysToken = sysTokenMapper.selectOneById(id); StpUtil.renewTimeout(sysToken.getToken(), 0); } return super.onRemoveBefore(ids); } @Override public Result page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) { System.out.println("hhhhhhhhhhsdfsdf"); if (pageNumber == null || pageNumber < 1) { pageNumber = 1L; } if (pageSize == null || pageSize < 1) { pageSize = 10L; } QueryWrapper queryWrapper = QueryWrapper.create().select("*").from("tb_sys_token") .where("user_id = ? ", StpUtil.getLoginIdAsString()); Page<SysToken> sysTokenPage = sysTokenMapper.paginateAs(pageNumber, pageSize, queryWrapper, SysToken.class); return Result.success(sysTokenPage); } } aiflowy-modules/aiflowy-module-system/src/main/java/tech/aiflowy/system/entity/SysToken.java
New file @@ -0,0 +1,15 @@ package tech.aiflowy.system.entity; import com.mybatisflex.annotation.Table; import tech.aiflowy.system.entity.base.SysTokenBase; /** * iframe 嵌入用 Token 表 实体类。 * * @author Administrator * @since 2025-05-26 */ @Table(value = "tb_sys_token", comment = "iframe 嵌入用 Token 表") public class SysToken extends SysTokenBase { } aiflowy-modules/aiflowy-module-system/src/main/java/tech/aiflowy/system/entity/base/SysTokenBase.java
New file @@ -0,0 +1,99 @@ package tech.aiflowy.system.entity.base; import com.mybatisflex.annotation.Column; import com.mybatisflex.annotation.Id; import com.mybatisflex.annotation.KeyType; import java.io.Serializable; import java.math.BigInteger; import java.time.LocalDateTime; public class SysTokenBase implements Serializable { private static final long serialVersionUID = 1L; /** * 主键 */ @Id(keyType = KeyType.Auto, value = "snowFlakeId", comment = "主键") private BigInteger id; /** * 生成的 token 值 */ @Column(comment = "生成的 token 值") private String token; /** * 关联用户ID */ @Column(comment = "关联用户ID") private Long userId; /** * 过期时间 */ @Column(comment = "过期时间") private LocalDateTime expireTime; /** * 创建时间 */ @Column(comment = "创建时间") private LocalDateTime createdAt; /** * Token 描述(可选) */ @Column(comment = "Token 描述(可选)") private String description; public BigInteger getId() { return id; } public void setId(BigInteger id) { this.id = id; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public LocalDateTime getExpireTime() { return expireTime; } public void setExpireTime(LocalDateTime expireTime) { this.expireTime = expireTime; } public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } } aiflowy-modules/aiflowy-module-system/src/main/java/tech/aiflowy/system/mapper/SysTokenMapper.java
New file @@ -0,0 +1,14 @@ package tech.aiflowy.system.mapper; import com.mybatisflex.core.BaseMapper; import tech.aiflowy.system.entity.SysToken; /** * iframe 嵌入用 Token 表 映射层。 * * @author Administrator * @since 2025-05-26 */ public interface SysTokenMapper extends BaseMapper<SysToken> { } aiflowy-modules/aiflowy-module-system/src/main/java/tech/aiflowy/system/service/SysTokenService.java
New file @@ -0,0 +1,22 @@ package tech.aiflowy.system.service; import com.mybatisflex.core.service.IService; import tech.aiflowy.common.domain.Result; import tech.aiflowy.system.entity.SysToken; import java.io.Serializable; /** * iframe 嵌入用 Token 表 服务层。 * * @author Administrator * @since 2025-05-26 */ public interface SysTokenService extends IService<SysToken> { Result saveGenerateToken(); Result updateToken(SysToken sysToken); void delete(Serializable id); } aiflowy-modules/aiflowy-module-system/src/main/java/tech/aiflowy/system/service/impl/SysTokenServiceImpl.java
New file @@ -0,0 +1,104 @@ package tech.aiflowy.system.service.impl; import cn.dev33.satoken.stp.SaLoginModel; import cn.dev33.satoken.stp.StpUtil; import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.spring.service.impl.ServiceImpl; import tech.aiflowy.common.ai.util.UUIDGenerator; import tech.aiflowy.common.domain.Result; import tech.aiflowy.common.util.DateUtil; import tech.aiflowy.system.entity.SysToken; import tech.aiflowy.system.mapper.SysTokenMapper; import tech.aiflowy.system.service.SysTokenService; import org.springframework.stereotype.Service; import java.io.Serializable; import java.time.Duration; import java.time.LocalDateTime; /** * iframe 嵌入用 Token 表 服务层实现。 * * @author Administrator * @since 2025-05-26 */ @Service public class SysTokenServiceImpl extends ServiceImpl<SysTokenMapper, SysToken> implements SysTokenService{ @Override public Result saveGenerateToken() { long loginId = StpUtil.getLoginIdAsLong(); // 这是要绑定的账号ID QueryWrapper queryWrapper = QueryWrapper.create() .select("*") .from("tb_sys_token") .where("user_id = ? ", loginId); SysToken sysToken = this.getMapper().selectOneByQuery(queryWrapper); if (sysToken != null){ return Result.fail(1, "已经存在 token, 无需生成"); } String customToken = UUIDGenerator.generateUUID();; // 自定义的 Token 字符串 SaLoginModel saLoginModel = new SaLoginModel(); saLoginModel.setToken(customToken); // 默认30天过期 // int timeout = 24*60*60*30; int timeout = 120; // 默认30天过期 saLoginModel.setTimeout(timeout); // 将 loginId 与 customToken 关联,并设置有效期 StpUtil.createLoginSession(loginId, saLoginModel); SysToken sysToken1 = new SysToken(); sysToken1.setToken(customToken); sysToken1.setUserId(StpUtil.getLoginIdAsLong()); sysToken1.setCreatedAt(LocalDateTime.now()); // 计算过期时间(当前时间加上30天) LocalDateTime expireTime = LocalDateTime.now().plusSeconds(timeout); sysToken1.setExpireTime(expireTime); int insert = this.getMapper().insert(sysToken1); if (insert <= 0){ return Result.fail(2, "新增失败"); } return Result.success(); } @Override public Result updateToken(SysToken sysToken) { SysToken sysToken1 = this.getMapper().selectOneById(sysToken.getId()); if (sysToken1 == null) { return Result.fail(1, "该 token 不存在,修改失败"); } LocalDateTime newExpireTime = sysToken.getExpireTime(); if (newExpireTime == null) { return Result.fail(3, "失效时间不能为空"); } // 3. 计算新的过期时间(秒数) long timeoutInSeconds = Duration.between(LocalDateTime.now(), newExpireTime).getSeconds(); if (timeoutInSeconds <= 0) { return Result.fail(4, "失效时间必须大于当前时间"); } SaLoginModel saLoginModel = new SaLoginModel(); saLoginModel.setToken(sysToken1.getToken()); int timeout = (int) timeoutInSeconds; saLoginModel.setTimeout(timeout); StpUtil.createLoginSession(StpUtil.getLoginId(), saLoginModel); // 5. 更新数据库中的 expireTime sysToken1.setExpireTime(newExpireTime); int updateResult = this.getMapper().update(sysToken1); if (updateResult <= 0) { return Result.fail(5, "更新失败"); } return Result.success(); } @Override public void delete(Serializable id) { SysToken sysToken = this.getMapper().selectOneById(id); StpUtil.renewTimeout(sysToken.getToken(), 0); } } aiflowy-ui-react/src/pages/system/SysToken.tsx
New file @@ -0,0 +1,170 @@ import React, {useState} from 'react'; import {ActionConfig, ColumnsConfig} from "../../components/AntdCrud"; import CrudPage from "../../components/CrudPage"; import {EditLayout} from "../../components/AntdCrud/EditForm.tsx"; import {Button, DatePicker, Form, message, Modal} from "antd"; import {useGetManual, usePostManual} from "../../hooks/useApis.ts"; import {EditOutlined} from "@ant-design/icons"; //字段配置 const columnsConfig: ColumnsConfig<any> = [ { hidden: true, form: { type: "hidden" }, dataIndex: "id", key: "id" }, { form: { type: "input" }, dataIndex: "token", title: "token", editCondition: () => { return false }, key: "token" }, { form: { type: "datetimepicker" }, dataIndex: "expireTime", title: "过期时间", key: "expireTime" }, { form: { type: "input" }, dataIndex: "createdAt", title: "创建时间", key: "createdAt", editCondition: () => { return false }, } ]; //编辑页面设置 const editLayout = { labelLayout: "horizontal", labelWidth: 80, columnsCount: 1, openType: "modal" } as EditLayout; export const SysToken: React.FC = () => { const {doGet: doGetGenerate} = useGetManual('/api/v1/sysToken/generateToken'); const {doPost: doPostUpdate} = usePostManual('/api/v1/sysToken/updateToken') const [expireTime, setExpireTime] = useState<string>('') const [editId, setEditId] = useState<string>('') const [isEditModalOpen, setIsEditModalOpen] = useState<boolean>(false) const [form] = Form.useForm(); const [refreshTrigger, setRefreshTrigger] = useState(0); const handleOk = () => { onFinish() }; const handleCancel = () => { setIsEditModalOpen(false); }; //操作列配置 const actionConfig = { addButtonEnable: true, detailButtonEnable: false, deleteButtonEnable: true, editButtonEnable: false, hidden: false, width: "200px", customActions: (data: any) => { return ( <> <a onClick={() => { setExpireTime(data.expireTime) setEditId(data.id) setIsEditModalOpen(true) }}> <EditOutlined/> 编辑 </a> </> ) } } as ActionConfig<any> const onFinish = () =>{ let value = JSON.parse(JSON.stringify(form.getFieldsValue())); form.resetFields() doPostUpdate({data: { id: editId, expireTime: value.expireTime }}).then((r) => { setEditId('') if (r.data.errorCode === 0){ message.success('修改成功') setIsEditModalOpen(false); } }) } return ( <> <CrudPage columnsConfig={columnsConfig} tableAlias="sysToken" addButtonEnable={false} externalRefreshTrigger={refreshTrigger} customButton={() => { return <><Button type="primary" onClick={() => { Modal.confirm({ title: '提示', content: '该操作会生成一个 token , 请确认是否生成', okText: '确定', cancelText: '取消', onOk() { doGetGenerate({params: {expireDays: 7}}).then((res) => { console.log('res'); console.log(res); if (res.data.errorCode === 0) { message.success("token生成成功"); setRefreshTrigger(prev => prev + 1); } }) }, }); }}>新增</Button></> }} actionConfig={actionConfig} editLayout={editLayout}/> <Modal title="编辑" closable={{ 'aria-label': 'Custom Close Button' }} open={isEditModalOpen} onOk={handleOk} onCancel={handleCancel} > <Form layout={'horizontal'} form={form} size = {'middle'} onFinish={onFinish} > <Form.Item label="失效时间 " name="expireTime" rules={[{ required: true, message: '请输入失效时间' }]}> <DatePicker value={expireTime} showTime showNow format={'YYYY-MM-DD HH:mm:ss'} /> </Form.Item> </Form> </Modal> </> ) }; export default { path: "sys/sysToken", element: SysToken };