Commit 8c6ba975 authored by ligaowei's avatar ligaowei

refactor(backend): 重构用户上下文处理和工具管理逻辑

重构UserUtils使用ThreadLocal存储用户ID以支持异步线程
简化MetaObjectHandlerConfig的用户ID获取逻辑
移除SseTokenEmitter的@Component注解并重构构造函数
优化AgentToolManager的代码结构和日志记录
重构AsyncUserContextDecorator以增强线程上下文传播

refactor(frontend): 简化表单渲染组件并移除pangea-ui依赖
parent 0306580c
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -29,7 +29,6 @@ public class AgentChatService {
private final AgentToolManager agentToolManager;
private final UserSseService userSseService;
private final pangea.hiagent.web.service.AgentService agentService;
private final SseTokenEmitter sseTokenEmitter;
public AgentChatService(
EventService eventService,
......@@ -37,14 +36,12 @@ public class AgentChatService {
AgentProcessorFactory agentProcessorFactory,
AgentToolManager agentToolManager,
UserSseService userSseService,
pangea.hiagent.web.service.AgentService agentService,
SseTokenEmitter sseTokenEmitter) {
pangea.hiagent.web.service.AgentService agentService) {
this.errorHandlerService = errorHandlerService;
this.agentProcessorFactory = agentProcessorFactory;
this.agentToolManager = agentToolManager;
this.userSseService = userSseService;
this.agentService = agentService;
this.sseTokenEmitter = sseTokenEmitter;
}
// /**
......@@ -182,7 +179,7 @@ public class AgentChatService {
AgentRequest request = chatRequest.toAgentRequest(agent.getId(), agent, agentToolManager);
// 创建新的SseTokenEmitter实例
SseTokenEmitter tokenEmitter = sseTokenEmitter.createNewInstance(emitter, agent, request, userId, this::handleCompletion);
SseTokenEmitter tokenEmitter = new SseTokenEmitter(userSseService, emitter, agent, request, userId, this::handleCompletion);
// 处理流式请求
processor.processStreamRequest(request, agent, userId, tokenEmitter);
......
package pangea.hiagent.agent.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import pangea.hiagent.model.Agent;
......@@ -13,7 +13,6 @@ import pangea.hiagent.web.dto.AgentRequest;
* 无状态设计,每次使用时创建新实例
*/
@Slf4j
@Component
public class SseTokenEmitter implements TokenConsumerWithCompletion {
private final UserSseService userSseService;
......@@ -47,6 +46,13 @@ public class SseTokenEmitter implements TokenConsumerWithCompletion {
/**
* 无参构造函数,用于Spring容器初始化
*/
public SseTokenEmitter() {
this(null, null, null, null, null, null);
}
/**
* 构造函数,用于Spring容器初始化(带UserSseService参数)
*/
public SseTokenEmitter(UserSseService userSseService) {
this(userSseService, null, null, null, null, null);
}
......
......@@ -104,30 +104,22 @@ public class MetaObjectHandlerConfig implements MetaObjectHandler {
/**
* 获取当前用户ID,支持异步线程上下文
* 该方法支持以下场景:
* 1. 同步请求:从SecurityContext获取用户ID
* 2. 异步任务:从AsyncUserContextDecorator传播的上下文获取用户ID
* 3. 故障转移:尝试直接解析Token获取用户ID
* 1. 优先从ThreadLocal获取(支持异步线程)
* 2. 从SecurityContext获取(支持同步请求和AsyncUserContextDecorator传播)
* 3. 从请求中解析Token获取用户ID
*
* @return 用户ID,如果无法获取则返回null
*/
private String getCurrentUserIdWithContext() {
try {
// 方式1:首先尝试从SecurityContext获取(支持同步请求和AsyncUserContextDecorator传播)
// 直接调用UserUtils.getCurrentUserId(),该方法已经包含了所有获取用户ID的方式
// 并且优先从ThreadLocal获取,支持异步线程
String userId = UserUtils.getCurrentUserId();
if (userId != null) {
log.debug("通过SecurityContext成功获取用户ID: {}", userId);
log.debug("成功获取用户ID: {}", userId);
return userId;
}
log.debug("无法从SecurityContext获取用户ID,可能是异步线程且未使用AsyncUserContextDecorator包装");
// 方式2:尝试直接从请求中解析Token(故障转移)
String asyncUserId = UserUtils.getCurrentUserIdInAsync();
if (asyncUserId != null) {
log.debug("通过直接解析Token成功获取用户ID: {}", asyncUserId);
return asyncUserId;
}
log.warn("无法通过任何方式获取当前用户ID,createdBy/updatedBy字段将不被填充");
return null;
} catch (Exception e) {
......
......@@ -98,17 +98,25 @@ public class AsyncUserContextDecorator {
public static Runnable wrapWithContext(Runnable runnable) {
// 捕获当前线程的用户上下文
UserContextHolder userContext = captureUserContext();
// 同时捕获当前线程的用户ID(用于ThreadLocal传播)
String currentUserId = UserUtils.getCurrentUserId();
return () -> {
try {
// 在异步线程中传播用户上下文
propagateUserContext(userContext);
// 将用户ID设置到ThreadLocal中,增强可靠性
if (currentUserId != null) {
UserUtils.setCurrentUserId(currentUserId);
}
// 执行原始任务
runnable.run();
} finally {
// 清理当前线程的用户上下文
clearUserContext();
// 清理ThreadLocal中的用户ID
UserUtils.clearCurrentUserId();
}
};
}
......@@ -122,17 +130,25 @@ public class AsyncUserContextDecorator {
public static <V> Callable<V> wrapWithContext(Callable<V> callable) {
// 捕获当前线程的用户上下文
UserContextHolder userContext = captureUserContext();
// 同时捕获当前线程的用户ID(用于ThreadLocal传播)
String currentUserId = UserUtils.getCurrentUserId();
return () -> {
try {
// 在异步线程中传播用户上下文
propagateUserContext(userContext);
// 将用户ID设置到ThreadLocal中,增强可靠性
if (currentUserId != null) {
UserUtils.setCurrentUserId(currentUserId);
}
// 执行原始任务
return callable.call();
} finally {
// 清理当前线程的用户上下文
clearUserContext();
// 清理ThreadLocal中的用户ID
UserUtils.clearCurrentUserId();
}
};
}
......
......@@ -21,19 +21,71 @@ public class UserUtils {
// 注入JwtUtil bean
private static JwtUtil jwtUtil;
// 使用InheritableThreadLocal存储用户ID,支持异步线程继承
private static final InheritableThreadLocal<String> USER_ID_THREAD_LOCAL = new InheritableThreadLocal<>();
public UserUtils(JwtUtil jwtUtil) {
UserUtils.jwtUtil = jwtUtil;
}
/**
* 设置当前线程的用户ID
* @param userId 用户ID
*/
public static void setCurrentUserId(String userId) {
if (StringUtils.hasText(userId)) {
USER_ID_THREAD_LOCAL.set(userId);
log.debug("设置当前线程的用户ID: {}", userId);
} else {
USER_ID_THREAD_LOCAL.remove();
log.debug("清除当前线程的用户ID");
}
}
/**
* 清除当前线程的用户ID
*/
public static void clearCurrentUserId() {
USER_ID_THREAD_LOCAL.remove();
log.debug("清除当前线程的用户ID");
}
/**
* 从ThreadLocal获取用户ID
* @return 用户ID,如果不存在则返回null
*/
public static String getCurrentUserIdFromThreadLocal() {
String userId = USER_ID_THREAD_LOCAL.get();
if (userId != null) {
log.debug("从ThreadLocal获取到用户ID: {}", userId);
}
return userId;
}
public static String getCurrentUserId() {
String username = getCurrentUserIdInSync();
if (username==null || username.isEmpty()) {
username = getCurrentUserIdInAsync();
// 优先从ThreadLocal获取(支持异步线程)
String userId = getCurrentUserIdFromThreadLocal();
if (userId != null) {
return userId;
}
return username;
// 从同步上下文获取
userId = getCurrentUserIdInSync();
if (userId != null) {
// 将获取到的用户ID存入ThreadLocal,供后续异步操作使用
setCurrentUserId(userId);
return userId;
}
// 从异步上下文获取
userId = getCurrentUserIdInAsync();
if (userId != null) {
// 将获取到的用户ID存入ThreadLocal,供后续异步操作使用
setCurrentUserId(userId);
}
return userId;
}
/**
......
......@@ -12,6 +12,7 @@ import pangea.hiagent.web.service.ToolService;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
......@@ -38,37 +39,29 @@ public class AgentToolManager {
* @return 工具列表
*/
public List<Tool> getAvailableTools(Agent agent) {
try {
log.info("获取Agent可用工具列表,Agent ID: {}, 名称: {}", agent.getId(), agent.getName());
// 获取与Agent关联的Tool ID列表
List<String> toolIds = agentToolRelationRepository.getToolIdsByAgentId(agent.getId());
log.info("Agent关联的工具ID数量: {}", toolIds != null ? toolIds.size() : 0);
if (toolIds == null || toolIds.isEmpty()) {
// 如果没有关联特定工具,则返回该用户的所有活跃工具
List<Tool> allTools = toolService.getUserToolsByStatus(agent.getOwner(), "active");
log.info("返回用户所有活跃工具,数量: {}", allTools != null ? allTools.size() : 0);
return allTools != null ? allTools : List.of();
}
// 根据Tool ID获取具体的Tool对象
List<Tool> tools = new ArrayList<>();
for (String toolId : toolIds) {
Tool tool = toolService.getById(toolId);
if (tool != null) {
tools.add(tool);
}
}
log.info("获取到的具体工具数量: {}", tools.size());
tools.forEach(tool -> log.info("工具名称: {}", tool.getName()));
return tools;
} catch (Exception e) {
log.error("获取Agent可用工具时发生错误", e);
return List.of();
log.info("获取Agent可用工具列表,Agent ID: {}, 名称: {}", agent.getId(), agent.getName());
// 获取与Agent关联的Tool ID列表
List<String> toolIds = agentToolRelationRepository.getToolIdsByAgentId(agent.getId());
log.info("Agent关联的工具ID数量: {}", toolIds != null ? toolIds.size() : 0);
// 如果没有关联特定工具,则返回该用户的所有活跃工具
if (toolIds == null || toolIds.isEmpty()) {
List<Tool> allTools = toolService.getUserToolsByStatus(agent.getOwner(), "active");
log.info("返回用户所有活跃工具,数量: {}", allTools != null ? allTools.size() : 0);
return allTools != null ? allTools : List.of();
}
// 根据Tool ID获取具体的Tool对象
List<Tool> tools = toolIds.stream()
.map(toolService::getById)
.filter(Objects::nonNull)
.collect(Collectors.toList());
log.info("获取到的具体工具数量: {}", tools.size());
log.debug("工具列表: {}", tools.stream().map(Tool::getName).collect(Collectors.joining(", ")));
return tools;
}
/**
......@@ -100,34 +93,18 @@ public class AgentToolManager {
* @return 筛选后的工具实例列表
*/
public List<Object> filterToolsByInstances(List<Object> allTools, Set<String> toolNames) {
log.debug("开始筛选工具实例,工具名称集合: {}", toolNames);
if (toolNames == null || toolNames.isEmpty()) {
log.debug("工具名称集合为空,返回所有工具实例");
return allTools;
}
List<Object> filteredTools = allTools.stream()
return allTools.stream()
.filter(tool -> {
// 获取工具类名(不含包名)
String className = tool.getClass().getSimpleName();
log.debug("检查工具类: {}", className);
// 检查类名是否匹配
boolean isMatch = toolNames.contains(className) ||
toolNames.stream().anyMatch(name ->
className.toLowerCase().contains(name.toLowerCase()));
if (isMatch) {
log.debug("工具 {} 匹配成功", className);
}
return isMatch;
return toolNames.contains(className) ||
toolNames.stream().anyMatch(name ->
className.toLowerCase().contains(name.toLowerCase()));
})
.collect(Collectors.toList());
log.debug("筛选完成,返回 {} 个工具实例", filteredTools.size());
return filteredTools;
}
/**
......@@ -143,8 +120,9 @@ public class AgentToolManager {
StringBuilder description = new StringBuilder();
for (int i = 0; i < tools.size(); i++) {
Tool tool = tools.get(i);
description.append(i + 1).append(". ");
description.append(tool.getName());
description.append(i + 1).append(". ")
.append(tool.getName());
if (hasValue(tool.getDisplayName())) {
description.append(" - ").append(tool.getDisplayName());
}
......@@ -168,20 +146,11 @@ public class AgentToolManager {
/**
* 获取Bean的原始目标类(穿透Spring AOP代理)
*
* 用于处理以下场景:
* 1. Bean被Spring AOP代理,需要获取原始类信息
* 2. 获取原始类的方法和字段信息
* 3. 进行类型检查和反射操作
*
* @param bean Bean实例(可能是代理对象)
* @return 原始目标类的Class对象
*/
private Class<?> getTargetClass(Object bean) {
if (bean == null) {
return null;
}
return AopUtils.getTargetClass(bean);
return bean == null ? null : AopUtils.getTargetClass(bean);
}
/**
......@@ -209,62 +178,43 @@ public class AgentToolManager {
return result;
}
try {
log.debug("[{}] 根据原始类名'{}' 查找工具实例,精确匹配: {}", agent.getName(), originalClassName, isExactMatch);
List<Tool> availableTools = getAvailableTools(agent);
for (Tool tool : availableTools) {
if (tool.getBeanName() == null || tool.getBeanName().trim().isEmpty()) {
continue;
}
Object bean;
try {
bean = applicationContext.getBean(tool.getBeanName());
} catch (Exception e) {
log.debug("[{}] 工具'{}' 的Bean查找失败,跳过", agent.getName(), tool.getName());
continue;
}
List<Tool> availableTools = getAvailableTools(agent);
if (bean == null) {
continue;
}
for (Tool tool : availableTools) {
try {
if (tool.getBeanName() == null || tool.getBeanName().trim().isEmpty()) {
continue;
}
Object bean = null;
try {
bean = applicationContext.getBean(tool.getBeanName());
} catch (Exception e) {
log.debug("[{}] 工具'{}' 的Bean查找失败,跳过", agent.getName(), tool.getName());
continue;
}
if (bean == null) {
continue;
}
// 获取原始目标类
Class<?> targetClass = getTargetClass(bean);
if (targetClass == null) {
targetClass = bean.getClass();
}
String targetClassName = targetClass.getSimpleName();
String targetFullClassName = targetClass.getName();
// 根据匹配模式进行判断
boolean matches = false;
if (isExactMatch) {
// 精确匹配:检查简单类名和完整类名
matches = originalClassName.equals(targetClassName) ||
originalClassName.equals(targetFullClassName);
} else {
// 模糊匹配:检查是否包含(不区分大小写)
matches = targetClassName.toLowerCase().contains(originalClassName.toLowerCase()) ||
targetFullClassName.toLowerCase().contains(originalClassName.toLowerCase());
}
if (matches) {
result.add(bean);
log.debug("[{}] 根据原始类名'{}' 匹配到工具实例: {}", agent.getName(), originalClassName, targetClassName);
}
} catch (Exception e) {
log.debug("[{}] 处理工具'{}' 时出错", agent.getName(), tool.getName(), e);
}
// 获取原始目标类
Class<?> targetClass = getTargetClass(bean);
if (targetClass == null) {
targetClass = bean.getClass();
}
log.debug("[{}] 根据原始类名'{}' 共找到 {} 个工具实例", agent.getName(), originalClassName, result.size());
} catch (Exception e) {
log.error("[{}] 根据原始类名查找工具实例时发生错误", agent.getName(), e);
String targetClassName = targetClass.getSimpleName();
String targetFullClassName = targetClass.getName();
// 根据匹配模式进行判断
boolean matches = isExactMatch
? originalClassName.equals(targetClassName) || originalClassName.equals(targetFullClassName)
: targetClassName.toLowerCase().contains(originalClassName.toLowerCase()) ||
targetFullClassName.toLowerCase().contains(originalClassName.toLowerCase());
if (matches) {
result.add(bean);
}
}
return result;
......@@ -316,64 +266,54 @@ public class AgentToolManager {
* @return 工具实例列表(包含AOP代理后的实例)
*/
public List<Object> getAvailableToolInstances(Agent agent) {
try {
log.info("[{}] 开始获取可用的工具实例", agent.getName());
// 获取Agent可用的工具定义
List<Tool> availableTools = getAvailableTools(agent);
log.debug("[{}] 获取到了{}个工具定义", agent.getName(), availableTools.size());
List<Object> toolInstances = new ArrayList<>();
List<String> failedBeans = new ArrayList<>();
// 遍历每个工具定义,根据beanName查找Spring Bean实例
for (Tool tool : availableTools) {
try {
// 验证beanName是否为空
if (tool.getBeanName() == null || tool.getBeanName().trim().isEmpty()) {
log.warn("[{}] 工具'{}' 没有配置beanName,跳过此工具", agent.getName(), tool.getName());
failedBeans.add(tool.getName() + " (beanName为null)");
continue;
}
// 根据beanName查找Bean实例
Object bean = null;
try {
bean = applicationContext.getBean(tool.getBeanName());
} catch (Exception e) {
log.warn("[{}] 工具'{}' 查找Bean'{}' 失败,错误消息: {}", agent.getName(), tool.getName(), tool.getBeanName(), e.getMessage());
failedBeans.add(tool.getName() + " (bean'" + tool.getBeanName() + "'不存在)");
continue;
}
if (bean != null) {
// 获取原始目标类(处理Spring AOP代理)
Class<?> targetClass = getTargetClass(bean);
String simpleName = targetClass != null ? targetClass.getSimpleName() : bean.getClass().getSimpleName();
toolInstances.add(bean);
log.debug("[{}] 成功查找工具'{}' 的Bean实例,原始类: {}", agent.getName(), tool.getName(), simpleName);
} else {
log.warn("[{}] 工具'{}' 的Bean实例为null", agent.getName(), tool.getName());
failedBeans.add(tool.getName() + " (bean实例为null)");
}
} catch (Exception e) {
log.error("[{}] 处理工具'{}' 时发生意外错误,详细信息", agent.getName(), tool.getName(), e);
failedBeans.add(tool.getName() + " (异常: " + e.getMessage() + ")");
}
log.info("[{}] 开始获取可用的工具实例", agent.getName());
// 获取Agent可用的工具定义
List<Tool> availableTools = getAvailableTools(agent);
log.debug("[{}] 获取到了{}个工具定义", agent.getName(), availableTools.size());
List<Object> toolInstances = new ArrayList<>();
List<String> failedBeans = new ArrayList<>();
// 遍历每个工具定义,根据beanName查找Spring Bean实例
for (Tool tool : availableTools) {
// 验证beanName是否为空
if (tool.getBeanName() == null || tool.getBeanName().trim().isEmpty()) {
log.warn("[{}] 工具'{}' 没有配置beanName,跳过此工具", agent.getName(), tool.getName());
failedBeans.add(tool.getName() + " (beanName为null)");
continue;
}
log.info("[{}] 成功获取了{}个工具实例", agent.getName(), toolInstances.size());
// 打印未成功查找的工具(便于故障诊断)
if (!failedBeans.isEmpty()) {
log.warn("[{}] 以下工具无法加载: {}", agent.getName(), failedBeans);
// 根据beanName查找Bean实例
Object bean;
try {
bean = applicationContext.getBean(tool.getBeanName());
} catch (Exception e) {
log.warn("[{}] 工具'{}' 查找Bean'{}' 失败,错误消息: {}", agent.getName(), tool.getName(), tool.getBeanName(), e.getMessage());
failedBeans.add(tool.getName() + " (bean'" + tool.getBeanName() + "'不存在)");
continue;
}
return toolInstances;
} catch (Exception e) {
log.error("[{}] 获取可用的工具实例时发生了意外错误", agent.getName(), e);
return List.of();
if (bean != null) {
// 获取原始目标类(处理Spring AOP代理)
Class<?> targetClass = getTargetClass(bean);
String simpleName = targetClass != null ? targetClass.getSimpleName() : bean.getClass().getSimpleName();
toolInstances.add(bean);
log.debug("[{}] 成功查找工具'{}' 的Bean实例,原始类: {}", agent.getName(), tool.getName(), simpleName);
} else {
log.warn("[{}] 工具'{}' 的Bean实例为null", agent.getName(), tool.getName());
failedBeans.add(tool.getName() + " (bean实例为null)");
}
}
log.info("[{}] 成功获取了{}个工具实例", agent.getName(), toolInstances.size());
// 打印未成功查找的工具(便于故障诊断)
if (!failedBeans.isEmpty()) {
log.warn("[{}] 以下工具无法加载: {}", agent.getName(), failedBeans);
}
return toolInstances;
}
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -19,7 +19,7 @@
"highlight.js": "^11.9.0",
"marked": "^17.0.1",
"pako": "^2.1.0",
"pangea-ui": "^0.14.2-beta.9",
"pinia": "^2.1.7",
"snabbdom": "^3.6.3",
"vue": "^3.4.0",
......
<template>
<hi-page-template
ref="templateRef"
:json="json"
:open-intl="false"
></hi-page-template>
<div class="button-wrap">
<a-button type="primary" @click="submit">提交</a-button>
<div class="form-container">
<h2>表单渲染器(已简化)</h2>
<div class="form-content">
<div class="form-field">
<label>输入框</label>
<el-input v-model="formData.input" placeholder="请输入"></el-input>
</div>
<div class="form-field">
<label>日期</label>
<el-date-picker v-model="formData.date" type="date" placeholder="选择日期"></el-date-picker>
</div>
</div>
<div class="button-wrap">
<el-button type="primary" @click="submit">提交</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import HiPageTemplate from "pangea-ui/hi-page-template";
import { ref, reactive } from "vue";
import { ElInput, ElDatePicker, ElButton } from "element-plus";
const templateRef = ref();
const formData = reactive({
input: '',
date: null
});
const submit = () => {
templateRef.value?.ctx.validate(1, (res, data) => {
console.log(res, data);
});
};
const json = {
pages: [
{
key: 0,
type: "default",
name: "默认页",
code: "",
display: "",
props: {
margin: "16px",
padding: "12px",
backgroundColor: "white",
display: {},
},
bindProps: {},
coms: [
{
key: 1,
type: "node",
name: "表单容器",
code: "HiFormContainer",
display: "",
props: {
status: "default",
backgroundColor: "transparent",
layout: "horizontal",
size: "medium",
labelAlign: "right",
display: {},
borderRadius: {},
boxShadow: {},
loop: {
data: [],
},
},
bindProps: {},
coms: [
{
key: 1766473421208,
name: "输入框",
code: "HiInput",
props: {
title: "输入框",
status: "default",
placeholder: "请输入",
name: "INPUT_6CP8HIBK",
},
bindProps: {},
coms: [],
},
{
key: 1766476676439,
name: "日期",
code: "HiDatePicker",
props: {
title: "日期",
type: "date",
format: "YYYY-MM-DD",
status: "default",
name: "DATE_PA9TUPQQ",
},
bindProps: {},
},
],
},
],
},
],
params: [],
apis: [],
funcs: [],
pageTemplate: {},
console.log('表单数据:', formData);
// 这里可以添加表单验证逻辑
};
</script>
<style scoped>
.form-container {
background-color: white;
padding: 16px;
margin: 16px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.form-container h2 {
margin-top: 0;
margin-bottom: 16px;
color: #333;
}
.form-content {
margin-bottom: 16px;
}
.form-field {
margin-bottom: 16px;
}
.form-field label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #606266;
}
.button-wrap {
display: flex;
justify-content: center;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment