18586361686
2025-05-28 e5654691ff1fb0a51a53eecafaabaf6aadc4fdc8
feat: iframe 嵌入访问外部地址鉴权功能
close #IC7TT5
1个文件已修改
7个文件已添加
580 ■■■■■ 已修改文件
aiflowy-modules/aiflowy-module-ai/src/main/java/tech/aiflowy/ai/controller/AiBotController.java 81 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-system/src/main/java/tech/aiflowy/system/controller/SysTokenController.java 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-system/src/main/java/tech/aiflowy/system/entity/SysToken.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-system/src/main/java/tech/aiflowy/system/entity/base/SysTokenBase.java 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-system/src/main/java/tech/aiflowy/system/mapper/SysTokenMapper.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-system/src/main/java/tech/aiflowy/system/service/SysTokenService.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-modules/aiflowy-module-system/src/main/java/tech/aiflowy/system/service/impl/SysTokenServiceImpl.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
aiflowy-ui-react/src/pages/system/SysToken.tsx 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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="失效时间&nbsp;&nbsp;&nbsp;&nbsp;" 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
};