Commit 02fbf9da authored by youxiaoji's avatar youxiaoji

Merge branch 'refs/heads/main' into develop_tmp

# Conflicts:
#	backend/src/main/java/pangea/hiagent/agent/react/DefaultReactExecutor.java
#	backend/src/main/java/pangea/hiagent/agent/service/AgentChatService.java
#	backend/src/main/java/pangea/hiagent/agent/service/SseTokenEmitter.java
#	frontend/package-lock.json
#	frontend/package.json
#	frontend/src/components/FormRender.vue
parents c6b7dbf6 901b31c3
...@@ -217,4 +217,6 @@ Thumbs.db ...@@ -217,4 +217,6 @@ Thumbs.db
.Trashes .Trashes
ehthumbs.db ehthumbs.db
Icon? Icon?
*.icon? *.icon?
\ No newline at end of file backend/data/hiagent_dev_db.trace.db
backend/data/hiagent_dev_db.mv.db
package pangea.hiagent.workpanel.data; package pangea.hiagent.agent.data;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
...@@ -29,4 +29,4 @@ public class CompletionEventDataBuilder { ...@@ -29,4 +29,4 @@ public class CompletionEventDataBuilder {
data.put("timestamp", System.currentTimeMillis()); data.put("timestamp", System.currentTimeMillis());
return data; return data;
} }
} }
\ No newline at end of file
package pangea.hiagent.workpanel.data; package pangea.hiagent.agent.data;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
...@@ -30,4 +30,5 @@ public class ErrorEventDataBuilder { ...@@ -30,4 +30,5 @@ public class ErrorEventDataBuilder {
data.put("type", "error"); data.put("type", "error");
return data; return data;
} }
} }
\ No newline at end of file
package pangea.hiagent.workpanel.data; package pangea.hiagent.agent.data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import pangea.hiagent.common.utils.ObjectPool; import pangea.hiagent.common.utils.ObjectPool;
...@@ -58,4 +58,5 @@ public class MapPoolService { ...@@ -58,4 +58,5 @@ public class MapPoolService {
public String getMapPoolStatistics() { public String getMapPoolStatistics() {
return mapPool.getStatistics(); return mapPool.getStatistics();
} }
} }
\ No newline at end of file
package pangea.hiagent.workpanel.data; package pangea.hiagent.agent.data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
......
package pangea.hiagent.web.dto; package pangea.hiagent.agent.data;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
...@@ -34,8 +34,17 @@ public class WorkPanelEvent implements Serializable { ...@@ -34,8 +34,17 @@ public class WorkPanelEvent implements Serializable {
*/ */
private Long timestamp; private Long timestamp;
/**
* 事件内容
*/
private String content;
/** /**
* 元数据 * 元数据
*/ */
private Map<String, Object> metadata; private Map<String, Object> metadata;
/**
* 触发事件的用户ID
*/
private String userId;
} }
\ No newline at end of file
...@@ -31,14 +31,16 @@ public class ReActAgentProcessor extends BaseAgentProcessor { ...@@ -31,14 +31,16 @@ public class ReActAgentProcessor extends BaseAgentProcessor {
@Autowired @Autowired
private RagService ragService; private RagService ragService;
@Autowired
private ReactCallback defaultReactCallback;
@Autowired @Autowired
private ReactExecutor defaultReactExecutor; private ReactExecutor defaultReactExecutor;
@Autowired @Autowired
private AgentToolManager agentToolManager; private AgentToolManager agentToolManager;
@Autowired
private ReactCallback defaultReactCallback;
@Override @Override
public String processRequest(Agent agent, AgentRequest request, String userId) { public String processRequest(Agent agent, AgentRequest request, String userId) {
...@@ -72,10 +74,6 @@ public class ReActAgentProcessor extends BaseAgentProcessor { ...@@ -72,10 +74,6 @@ public class ReActAgentProcessor extends BaseAgentProcessor {
// 处理请求的通用前置逻辑 // 处理请求的通用前置逻辑
String ragResponse = handlePreProcessing(agent, userMessage, userId, ragService, null); String ragResponse = handlePreProcessing(agent, userMessage, userId, ragService, null);
if (ragResponse != null) { if (ragResponse != null) {
// 触发最终答案回调
if (defaultReactCallback != null) {
defaultReactCallback.onFinalAnswer(ragResponse);
}
return ragResponse; return ragResponse;
} }
...@@ -83,10 +81,7 @@ public class ReActAgentProcessor extends BaseAgentProcessor { ...@@ -83,10 +81,7 @@ public class ReActAgentProcessor extends BaseAgentProcessor {
ChatClient client = ChatClient.builder(agentService.getChatModelForAgent(agent)).build(); ChatClient client = ChatClient.builder(agentService.getChatModelForAgent(agent)).build();
List<Object> tools = agentToolManager.getAvailableToolInstances(agent); List<Object> tools = agentToolManager.getAvailableToolInstances(agent);
// 添加自定义回调到ReAct执行器
if (defaultReactExecutor != null && defaultReactCallback != null) {
defaultReactExecutor.addReactCallback(defaultReactCallback);
}
// 使用ReAct执行器执行流程,传递Agent对象和用户ID以支持记忆功能 // 使用ReAct执行器执行流程,传递Agent对象和用户ID以支持记忆功能
String finalAnswer = defaultReactExecutor.execute(client, userMessage, tools, agent, userId); String finalAnswer = defaultReactExecutor.execute(client, userMessage, tools, agent, userId);
...@@ -114,10 +109,6 @@ public class ReActAgentProcessor extends BaseAgentProcessor { ...@@ -114,10 +109,6 @@ public class ReActAgentProcessor extends BaseAgentProcessor {
// 处理请求的通用前置逻辑 // 处理请求的通用前置逻辑
String ragResponse = handlePreProcessing(agent, userMessage, userId, ragService, tokenConsumer); String ragResponse = handlePreProcessing(agent, userMessage, userId, ragService, tokenConsumer);
if (ragResponse != null) { if (ragResponse != null) {
// 触发最终答案回调
if (defaultReactCallback != null) {
defaultReactCallback.onFinalAnswer(ragResponse);
}
return; return;
} }
......
package pangea.hiagent.agent.react; package pangea.hiagent.agent.react;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import pangea.hiagent.workpanel.IWorkPanelDataCollector; import pangea.hiagent.agent.service.UserSseService;
import pangea.hiagent.common.utils.UserUtils;
import pangea.hiagent.agent.data.WorkPanelEvent;
/** /**
* 简化的ReAct回调类 * 简化的ReAct回调类
...@@ -11,112 +15,33 @@ import pangea.hiagent.workpanel.IWorkPanelDataCollector; ...@@ -11,112 +15,33 @@ import pangea.hiagent.workpanel.IWorkPanelDataCollector;
@Slf4j @Slf4j
@Component @Component
public class DefaultReactCallback implements ReactCallback { public class DefaultReactCallback implements ReactCallback {
@Autowired @Autowired
private IWorkPanelDataCollector workPanelCollector; private UserSseService userSseService;
@Override @Override
public void onStep(ReactStep reactStep) { public void onStep(ReactStep reactStep) {
log.info("ReAct步骤触发: 类型={}, 内容摘要={}",
reactStep.getStepType(), String reactStepName = reactStep.getStepType().name();
reactStep.getContent() != null ?
reactStep.getContent().substring(0, Math.min(50, reactStep.getContent().length())) : "null");
recordReactStepToWorkPanel(reactStep);
}
@Override
public void onFinalAnswer(String finalAnswer) {
ReactStep finalStep = new ReactStep(0, ReactStepType.FINAL_ANSWER, finalAnswer);
recordReactStepToWorkPanel(finalStep);
}
private void recordReactStepToWorkPanel(ReactStep reactStep) {
if (workPanelCollector == null) {
return;
}
try { try {
switch (reactStep.getStepType()) { userSseService.sendWorkPanelEvent(WorkPanelEvent.builder()
case THOUGHT: .type(reactStepName)
workPanelCollector.recordThinking(reactStep.getContent(), "thought"); .content(reactStep.getContent())
log.info("[WorkPanel] 记录思考步骤: {}", .userId(UserUtils.getCurrentUserIdStatic())
reactStep.getContent().substring(0, Math.min(100, reactStep.getContent().length()))); .build());
break; } catch (IOException e) {
case ACTION: log.error("发送ReAct步骤到WorkPanel失败: 类型={}, 内容摘要={}",
if (reactStep.getAction() != null) { reactStep.getStepType(),
// 记录工具调用动作 reactStep.getContent() != null
String toolName = reactStep.getAction().getToolName(); ? reactStep.getContent().substring(0, Math.min(50, reactStep.getContent().length()))
Object parameters = reactStep.getAction().getParameters(); : "null",
e);
// 记录工具调用,初始状态为pending
workPanelCollector.recordToolCallAction(
toolName,
parameters,
null, // 结果为空
"pending", // 状态为pending
null // 错误信息为空
);
// 同时记录工具调用信息到日志
log.info("[WorkPanel] 记录工具调用: 工具={} 参数={}", toolName, parameters);
} else {
// 如果没有具体的工具信息,记录为一般动作
workPanelCollector.recordThinking(reactStep.getContent(), "action");
log.info("[WorkPanel] 记录动作步骤: {}",
reactStep.getContent().substring(0, Math.min(100, reactStep.getContent().length())));
}
break;
case OBSERVATION:
if (reactStep.getObservation() != null) {
// 检查是否有对应的动作信息
if (reactStep.getAction() != null) {
// 使用动作信息更新工具调用结果
workPanelCollector.recordToolCallAction(
reactStep.getAction().getToolName(),
reactStep.getAction().getParameters(),
reactStep.getObservation().getContent(),
"success", // 状态为success
null // 无错误信息
);
log.info("[WorkPanel] 更新工具调用结果: 工具={} 结果摘要={}",
reactStep.getAction().getToolName(),
reactStep.getObservation().getContent().substring(0, Math.min(50, reactStep.getObservation().getContent().length())));
} else {
// 如果没有动作信息,记录为观察结果
workPanelCollector.recordThinking(reactStep.getContent(), "observation");
log.info("[WorkPanel] 记录观察步骤: {}",
reactStep.getContent().substring(0, Math.min(100, reactStep.getContent().length())));
}
}
break;
case FINAL_ANSWER:
workPanelCollector.recordFinalAnswer(reactStep.getContent());
// 记录最终答案到日志
log.info("[WorkPanel] 记录最终答案: {}",
reactStep.getContent().substring(0, Math.min(100, reactStep.getContent().length())));
break;
default:
log.warn("未知的ReAct步骤类型: {}", reactStep.getStepType());
break;
}
} catch (Exception e) {
log.error("记录ReAct步骤到工作面板失败", e);
// 即使发生异常,也尝试记录错误信息到工作面板
try {
if (reactStep != null && reactStep.getAction() != null) {
workPanelCollector.recordToolCallAction(
reactStep.getAction().getToolName(),
reactStep.getAction().getParameters(),
"记录失败: " + e.getMessage(),
"error",
System.currentTimeMillis() // 使用当前时间戳作为执行时间
);
}
} catch (Exception ex) {
log.error("记录错误信息到工作面板也失败", ex);
}
} }
// 记录最终答案到日志
log.info("[WorkPanel] 记录{} {}", reactStepName,
reactStep.getContent().substring(0, Math.min(100, reactStep.getContent().length())));
} }
} }
\ No newline at end of file
package pangea.hiagent.agent.react;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
public class EventSplitter {
private final List<String> keywords = Arrays.asList(
"Thought", "Action", "Observation", "Final_Answer"
);
private final Pattern keywordPattern = Pattern.compile(
String.format("(?i)(Thought|Action|Observation|Final[ _]Answer):", String.join("|", keywords)), Pattern.CASE_INSENSITIVE
);
private String currentType = null;
private StringBuilder currentContent = new StringBuilder();
private StringBuilder buffer = new StringBuilder();
private final ReactCallback callback;
private volatile int stepNumber = 0;
public EventSplitter(ReactCallback callback) {
this.callback = callback;
}
// 每收到一个token/字符,调用此方法
public void feedToken(String token) {
buffer.append(token);
// log.debug("当前缓冲区: {}", buffer.toString());
Matcher matcher = keywordPattern.matcher(buffer);
while (matcher.find()) {
log.debug("发现新事件关键词: {}", matcher.group(1));
// 发现新事件
if (currentType != null && currentContent.length() > 0) {
// 实时输出已分割事件
callback.onStep(new ReactStep(stepNumber++, ReactStepType.fromString(currentType), currentContent.toString()));
}
// 更新事件类型
currentType = matcher.group(1);
currentContent.setLength(0);
// 累积匹配位置后的内容
currentContent.append(buffer.substring(matcher.end()));
// 重置buffer为剩余内容
buffer.setLength(0);
buffer.append(currentContent);
// 重新查找
matcher = keywordPattern.matcher(buffer);
}
// 检查是否有部分关键词在buffer末尾
if (buffer.length() > 0) {
// 检查是否可能是关键词的一部分
boolean isPartialKeyword = false;
String bufferStr = buffer.toString();
for (String keyword : keywords) {
if (keyword.startsWith(bufferStr) || bufferStr.startsWith(keyword)) {
isPartialKeyword = true;
break;
}
}
if (!isPartialKeyword) {
// 不是部分关键词,添加到内容中
currentContent.append(buffer);
buffer.setLength(0);
}
}
}
// 流式结束时,调用此方法输出最后一个事件
public void endStream(ReactCallback tokenConsumer) {
if (currentType != null && currentContent.length() > 0) {
callback.onStep(new ReactStep(stepNumber++, ReactStepType.fromString(currentType), currentContent.toString()));
}
}
}
...@@ -10,6 +10,4 @@ public interface ReactCallback { ...@@ -10,6 +10,4 @@ public interface ReactCallback {
* @param reactStep ReAct步骤对象,包含步骤的所有核心信息 * @param reactStep ReAct步骤对象,包含步骤的所有核心信息
*/ */
void onStep(ReactStep reactStep); void onStep(ReactStep reactStep);
void onFinalAnswer(String ragResponse);
} }
\ No newline at end of file
...@@ -49,9 +49,6 @@ public class ReactStep { ...@@ -49,9 +49,6 @@ public class ReactStep {
public Object getToolArgs() { return toolArgs; } public Object getToolArgs() { return toolArgs; }
public void setToolArgs(Object toolArgs) { this.toolArgs = toolArgs; } public void setToolArgs(Object toolArgs) { this.toolArgs = toolArgs; }
// 根据DefaultReactCallback.java中的使用情况添加getParameters方法
public Object getParameters() { return toolArgs; }
} }
/** /**
...@@ -66,8 +63,5 @@ public class ReactStep { ...@@ -66,8 +63,5 @@ public class ReactStep {
public String getResult() { return result; } public String getResult() { return result; }
public void setResult(String result) { this.result = result; } public void setResult(String result) { this.result = result; }
// 根据DefaultReactCallback.java中的使用情况添加getContent方法
public String getContent() { return result; }
} }
} }
\ No newline at end of file
...@@ -22,5 +22,9 @@ public enum ReactStepType { ...@@ -22,5 +22,9 @@ public enum ReactStepType {
/** /**
* 最终答案步骤:结合工具结果生成最终回答 * 最终答案步骤:结合工具结果生成最终回答
*/ */
FINAL_ANSWER FINAL_ANSWER;
public static ReactStepType fromString(String currentType) {
return ReactStepType.valueOf(currentType.toUpperCase().replace(" ", "_"));
}
} }
\ No newline at end of file
...@@ -102,7 +102,7 @@ public class ErrorHandlerService { ...@@ -102,7 +102,7 @@ public class ErrorHandlerService {
* *
* @param emitter SSE发射器 * @param emitter SSE发射器
* @param errorMessage 错误信息 * @param errorMessage 错误信息
* @param exception 异常对象 * @param exception 异常对象(可选)
* @param processorType 处理器类型(可选) * @param processorType 处理器类型(可选)
*/ */
public void handleChatError(SseEmitter emitter, String errorMessage, Exception exception, String processorType) { public void handleChatError(SseEmitter emitter, String errorMessage, Exception exception, String processorType) {
...@@ -142,44 +142,25 @@ public class ErrorHandlerService { ...@@ -142,44 +142,25 @@ public class ErrorHandlerService {
} }
/** /**
* 处理聊天过程中的异常() * 处理聊天过程中的异常(简化版
* *
* @param emitter SSE发射器 * @param emitter SSE发射器
* @param errorMessage 错误信息 * @param errorMessage 错误信息
*/ */
public void handleChatError(SseEmitter emitter, String errorMessage) { public void handleChatError(SseEmitter emitter, String errorMessage) {
// 参数验证 handleChatError(emitter, errorMessage, null, null);
if (errorMessage == null || errorMessage.isEmpty()) {
errorMessage = "未知错误";
}
// 生成错误跟踪ID
String errorId = generateErrorId();
log.error("[{}] 处理聊天请求时发生错误: {}", errorId, errorMessage);
try {
// 检查emitter是否已经完成,避免向已完成的连接发送错误信息
if (userSseService != null && !userSseService.isEmitterCompleted(emitter)) {
String fullErrorMessage = buildFullErrorMessage(errorMessage, null, errorId, null);
userSseService.sendErrorEvent(emitter, fullErrorMessage);
} else {
log.debug("[{}] SSE emitter已完成,跳过发送错误信息", errorId);
}
} catch (Exception sendErrorEx) {
log.error("[{}] 发送错误信息失败", errorId, sendErrorEx);
}
} }
/** /**
* 处理Token处理过程中的异常 * 处理带完成状态标记的异常
* *
* @param emitter SSE发射器 * @param emitter SSE发射器
* @param errorMessage 错误信息
* @param processorType 处理器类型 * @param processorType 处理器类型
* @param exception 异常对象 * @param exception 异常对象
* @param isCompleted 完成状态标记 * @param isCompleted 完成状态标记
*/ */
public void handleTokenError(SseEmitter emitter, String processorType, Exception exception, AtomicBoolean isCompleted) { private void handleErrorWithCompletion(SseEmitter emitter, String errorMessage, String processorType, Exception exception, AtomicBoolean isCompleted) {
// 参数验证 // 参数验证
if (processorType == null || processorType.isEmpty()) { if (processorType == null || processorType.isEmpty()) {
processorType = "未知处理器"; processorType = "未知处理器";
...@@ -192,17 +173,17 @@ public class ErrorHandlerService { ...@@ -192,17 +173,17 @@ public class ErrorHandlerService {
if (exception != null) { if (exception != null) {
exceptionMonitoringService.recordException( exceptionMonitoringService.recordException(
exception.getClass().getSimpleName(), exception.getClass().getSimpleName(),
"处理token时发生错误", errorMessage,
java.util.Arrays.toString(exception.getStackTrace()) java.util.Arrays.toString(exception.getStackTrace())
); );
} }
log.error("[{}] {}处理token时发生错误", errorId, processorType, exception); log.error("[{}] {}: {}", errorId, processorType, errorMessage, exception);
if (!isCompleted.getAndSet(true)) { if (!isCompleted.getAndSet(true)) {
try { try {
// 检查emitter是否已经完成,避免向已完成的连接发送错误信息 // 检查emitter是否已经完成,避免向已完成的连接发送错误信息
if (userSseService != null && !userSseService.isEmitterCompleted(emitter)) { if (userSseService != null && !userSseService.isEmitterCompleted(emitter)) {
String errorMessage = "处理响应时发生错误";
String fullErrorMessage = buildFullErrorMessage(errorMessage, exception, errorId, processorType); String fullErrorMessage = buildFullErrorMessage(errorMessage, exception, errorId, processorType);
userSseService.sendErrorEvent(emitter, fullErrorMessage); userSseService.sendErrorEvent(emitter, fullErrorMessage);
} else { } else {
...@@ -216,6 +197,18 @@ public class ErrorHandlerService { ...@@ -216,6 +197,18 @@ public class ErrorHandlerService {
} }
} }
/**
* 处理Token处理过程中的异常
*
* @param emitter SSE发射器
* @param processorType 处理器类型
* @param exception 异常对象
* @param isCompleted 完成状态标记
*/
public void handleTokenError(SseEmitter emitter, String processorType, Exception exception, AtomicBoolean isCompleted) {
handleErrorWithCompletion(emitter, "处理token时发生错误", processorType, exception, isCompleted);
}
/** /**
* 处理完成回调过程中的异常 * 处理完成回调过程中的异常
* *
...@@ -252,15 +245,13 @@ public class ErrorHandlerService { ...@@ -252,15 +245,13 @@ public class ErrorHandlerService {
} }
/** /**
* 处理流式处理中的错误 * 处理基于Consumer的流式错误
* *
* @param e 异常对象 * @param e 异常对象
* @param tokenConsumer token处理回调函数 * @param tokenConsumer token处理回调函数
* @param errorMessagePrefix 错误消息前缀 * @param errorMessage 完整错误消息
*/ */
public void handleStreamError(Throwable e, Consumer<String> tokenConsumer, String errorMessagePrefix) { private void handleConsumerError(Throwable e, Consumer<String> tokenConsumer, String errorMessage) {
String errorMessage = errorMessagePrefix + ": " + e.getMessage();
// 记录异常到监控服务 // 记录异常到监控服务
exceptionMonitoringService.recordException( exceptionMonitoringService.recordException(
e.getClass().getSimpleName(), e.getClass().getSimpleName(),
...@@ -268,12 +259,24 @@ public class ErrorHandlerService { ...@@ -268,12 +259,24 @@ public class ErrorHandlerService {
java.util.Arrays.toString(e.getStackTrace()) java.util.Arrays.toString(e.getStackTrace())
); );
log.error("流式处理错误: {}", errorMessage, e); log.error(errorMessage, e);
if (tokenConsumer != null) { if (tokenConsumer != null) {
tokenConsumer.accept("[ERROR] " + errorMessage); tokenConsumer.accept("[ERROR] " + errorMessage);
} }
} }
/**
* 处理流式处理中的错误
*
* @param e 异常对象
* @param tokenConsumer token处理回调函数
* @param errorMessagePrefix 错误消息前缀
*/
public void handleStreamError(Throwable e, Consumer<String> tokenConsumer, String errorMessagePrefix) {
String errorMessage = errorMessagePrefix + ": " + e.getMessage();
handleConsumerError(e, tokenConsumer, errorMessage);
}
/** /**
* 发送错误信息给客户端 * 发送错误信息给客户端
* *
...@@ -294,18 +297,7 @@ public class ErrorHandlerService { ...@@ -294,18 +297,7 @@ public class ErrorHandlerService {
*/ */
public void handleReactFlowError(Exception e, Consumer<String> tokenConsumer) { public void handleReactFlowError(Exception e, Consumer<String> tokenConsumer) {
String errorMessage = "处理ReAct流程时发生错误: " + e.getMessage(); String errorMessage = "处理ReAct流程时发生错误: " + e.getMessage();
handleConsumerError(e, tokenConsumer, errorMessage);
// 记录异常到监控服务
exceptionMonitoringService.recordException(
e.getClass().getSimpleName(),
errorMessage,
java.util.Arrays.toString(e.getStackTrace())
);
log.error("ReAct流程错误: {}", errorMessage, e);
if (tokenConsumer != null) {
tokenConsumer.accept("[ERROR] " + errorMessage);
}
} }
/** /**
...@@ -337,33 +329,6 @@ public class ErrorHandlerService { ...@@ -337,33 +329,6 @@ public class ErrorHandlerService {
* @param isCompleted 完成状态标记 * @param isCompleted 完成状态标记
*/ */
public void handleSaveDialogueError(SseEmitter emitter, Exception exception, AtomicBoolean isCompleted) { public void handleSaveDialogueError(SseEmitter emitter, Exception exception, AtomicBoolean isCompleted) {
// 生成错误跟踪ID handleErrorWithCompletion(emitter, "保存对话记录失败", "对话记录", exception, isCompleted);
String errorId = generateErrorId();
// 记录异常到监控服务
if (exception != null) {
exceptionMonitoringService.recordException(
exception.getClass().getSimpleName(),
"保存对话记录失败",
java.util.Arrays.toString(exception.getStackTrace())
);
}
log.error("[{}] 保存对话记录失败", errorId, exception);
if (!isCompleted.getAndSet(true)) {
try {
// 检查emitter是否已经完成,避免向已完成的连接发送错误信息
if (userSseService != null && !userSseService.isEmitterCompleted(emitter)) {
String errorMessage = "保存对话记录失败,请联系技术支持";
String fullErrorMessage = buildFullErrorMessage(errorMessage, exception, errorId, "对话记录");
userSseService.sendErrorEvent(emitter, fullErrorMessage);
} else {
log.debug("[{}] SSE emitter已完成,跳过发送错误信息", errorId);
}
} catch (Exception sendErrorEx) {
log.error("[{}] 发送错误信息失败", errorId, sendErrorEx);
}
}
} }
} }
\ No newline at end of file
...@@ -2,9 +2,10 @@ package pangea.hiagent.agent.service; ...@@ -2,9 +2,10 @@ package pangea.hiagent.agent.service;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.Map; import java.util.concurrent.locks.ReentrantReadWriteLock;
/** /**
* 异常监控服务 * 异常监控服务
...@@ -17,12 +18,18 @@ public class ExceptionMonitoringService { ...@@ -17,12 +18,18 @@ public class ExceptionMonitoringService {
// 异常统计信息 // 异常统计信息
private final Map<String, AtomicLong> exceptionCounters = new ConcurrentHashMap<>(); private final Map<String, AtomicLong> exceptionCounters = new ConcurrentHashMap<>();
// 异常详细信息缓存 // 异常详细信息缓存,使用时间戳作为键,便于按时间排序
private final Map<String, String> exceptionDetails = new ConcurrentHashMap<>(); private final Map<Long, String> exceptionDetails = new ConcurrentHashMap<>();
// 锁,用于保护缓存清理操作
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 最大缓存条目数 // 最大缓存条目数
private static final int MAX_CACHE_SIZE = 1000; private static final int MAX_CACHE_SIZE = 1000;
// 清理阈值,当缓存超过最大值时,清理到这个值
private static final int CLEANUP_THRESHOLD = MAX_CACHE_SIZE - 200;
/** /**
* 记录异常信息 * 记录异常信息
* *
...@@ -37,14 +44,31 @@ public class ExceptionMonitoringService { ...@@ -37,14 +44,31 @@ public class ExceptionMonitoringService {
counter.incrementAndGet(); counter.incrementAndGet();
// 记录异常详细信息(保留最新的) // 记录异常详细信息(保留最新的)
String detailKey = exceptionType + "_" + System.currentTimeMillis(); long timestamp = System.currentTimeMillis();
exceptionDetails.put(detailKey, formatExceptionDetail(exceptionType, errorMessage, stackTrace)); exceptionDetails.put(timestamp, formatExceptionDetail(exceptionType, errorMessage, stackTrace));
// 控制缓存大小 // 控制缓存大小,使用写锁保护清理操作
if (exceptionDetails.size() > MAX_CACHE_SIZE) { if (exceptionDetails.size() > MAX_CACHE_SIZE) {
// 移除最老的条目 lock.writeLock().lock();
String oldestKey = exceptionDetails.keySet().iterator().next(); try {
exceptionDetails.remove(oldestKey); // 再次检查,避免竞态条件
if (exceptionDetails.size() > MAX_CACHE_SIZE) {
// 找出最老的条目并移除,直到达到清理阈值
while (exceptionDetails.size() > CLEANUP_THRESHOLD) {
// 找出最小的时间戳(最老的条目)
Long oldestTimestamp = exceptionDetails.keySet().stream()
.min(Long::compare)
.orElse(null);
if (oldestTimestamp != null) {
exceptionDetails.remove(oldestTimestamp);
} else {
break;
}
}
}
} finally {
lock.writeLock().unlock();
}
} }
// 记录日志 // 记录日志
...@@ -102,7 +126,11 @@ public class ExceptionMonitoringService { ...@@ -102,7 +126,11 @@ public class ExceptionMonitoringService {
* @return 异常详细信息 * @return 异常详细信息
*/ */
public Map<String, String> getExceptionDetails() { public Map<String, String> getExceptionDetails() {
return new ConcurrentHashMap<>(exceptionDetails); Map<String, String> result = new ConcurrentHashMap<>();
for (Map.Entry<Long, String> entry : exceptionDetails.entrySet()) {
result.put(entry.getKey().toString(), entry.getValue());
}
return result;
} }
/** /**
......
package pangea.hiagent.agent.service; package pangea.hiagent.agent.service;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import pangea.hiagent.model.Agent; import pangea.hiagent.model.Agent;
import pangea.hiagent.web.dto.AgentRequest; import pangea.hiagent.web.dto.AgentRequest;
import java.io.IOException;
/** /**
* SSE Token发射器 * SSE Token发射器
* 专注于将token转换为SSE事件并发送 * 专注于将token转换为SSE事件并发送
* 无状态设计,每次使用时创建新实例
*/ */
@Slf4j @Slf4j
@Component
public class SseTokenEmitter implements TokenConsumerWithCompletion { public class SseTokenEmitter implements TokenConsumerWithCompletion {
private final UserSseService userSseService; private final UserSseService userSseService;
// 当前处理的emitter // 所有状态通过构造函数一次性传入
private SseEmitter emitter; private final SseEmitter emitter;
private final Agent agent;
// 上下文信息 private final AgentRequest request;
private Agent agent; private final String userId;
private AgentRequest request; private final CompletionCallback completionCallback;
private String userId;
private String emitterId;
// 完成回调
private CompletionCallback completionCallback;
public SseTokenEmitter(UserSseService userSseService) {
this.userSseService = userSseService;
}
/** /**
* 设置当前使用的SSE发射器 * 构造函数
* @param userSseService SSE服务
* @param emitter SSE发射器
* @param agent Agent对象
* @param request 请求对象
* @param userId 用户ID
* @param completionCallback 完成回调
*/ */
public void setEmitter(SseEmitter emitter) { public SseTokenEmitter(UserSseService userSseService, SseEmitter emitter, Agent agent,
AgentRequest request, String userId, CompletionCallback completionCallback) {
this.userSseService = userSseService;
this.emitter = emitter; this.emitter = emitter;
}
/**
* 设置上下文信息
*/
public void setContext(Agent agent, AgentRequest request, String userId) {
this.agent = agent; this.agent = agent;
this.request = request; this.request = request;
this.userId = userId; this.userId = userId;
this.completionCallback = completionCallback;
} }
/** /**
* 设置完成回调 * 无参构造函数,用于Spring容器初始化
*/ */
public void setCompletionCallback(CompletionCallback completionCallback) { public SseTokenEmitter() {
this.completionCallback = completionCallback; this(null, null, null, null, null, null);
} }
public void setEmitterId(String emitterId) {
this.emitterId = emitterId; /**
* 构造函数,用于Spring容器初始化(带UserSseService参数)
*/
public SseTokenEmitter(UserSseService userSseService) {
this(userSseService, null, null, null, null, null);
} }
public String getEmitterId() {
return emitterId; /**
* 创建新的SseTokenEmitter实例
* @param emitter SSE发射器
* @param agent Agent对象
* @param request 请求对象
* @param userId 用户ID
* @param completionCallback 完成回调
* @return 新的SseTokenEmitter实例
*/
public SseTokenEmitter createNewInstance(SseEmitter emitter, Agent agent, AgentRequest request,
String userId, CompletionCallback completionCallback) {
return new SseTokenEmitter(userSseService, emitter, agent, request, userId, completionCallback);
} }
@Override @Override
public void accept(String token) { public void accept(String token) {
// 使用JSON格式发送token,确保转义序列被正确处理 // 使用JSON格式发送token,确保转义序列被正确处理
...@@ -78,8 +89,26 @@ public class SseTokenEmitter implements TokenConsumerWithCompletion { ...@@ -78,8 +89,26 @@ public class SseTokenEmitter implements TokenConsumerWithCompletion {
} else { } else {
log.debug("SSE emitter已无效,跳过发送token"); log.debug("SSE emitter已无效,跳过发送token");
} }
} catch (IllegalStateException e) {
// 处理emitter已关闭的情况,这通常是由于客户端断开连接
log.debug("无法发送token,SSE emitter已关闭: {}", e.getMessage());
// 将emitter标记为已完成,避免后续再次尝试发送
if (emitter != null) {
userSseService.removeEmitter(emitter);
}
} catch (IOException e) {
// 处理IO异常,这通常是由于客户端断开连接或网络问题
log.debug("无法发送token,IO异常: {}", e.getMessage());
// 将emitter标记为已完成,避免后续再次尝试发送
if (emitter != null) {
userSseService.removeEmitter(emitter);
}
} catch (Exception e) { } catch (Exception e) {
log.error("发送token失败", e); log.error("发送token失败", e);
// 对于其他异常,也将emitter标记为已完成,避免后续再次尝试发送
if (emitter != null) {
userSseService.removeEmitter(emitter);
}
} }
} }
...@@ -96,6 +125,12 @@ public class SseTokenEmitter implements TokenConsumerWithCompletion { ...@@ -96,6 +125,12 @@ public class SseTokenEmitter implements TokenConsumerWithCompletion {
if (completionCallback != null) { if (completionCallback != null) {
completionCallback.onComplete(emitter, agent, request, userId, fullContent); completionCallback.onComplete(emitter, agent, request, userId, fullContent);
} }
} catch (IllegalStateException e) {
// 处理emitter已关闭的情况,这通常是由于客户端断开连接
log.debug("无法发送完成信号,SSE emitter已关闭: {}", e.getMessage());
} catch (IOException e) {
// 处理IO异常,这通常是由于客户端断开连接或网络问题
log.debug("无法发送完成信号,IO异常: {}", e.getMessage());
} catch (Exception e) { } catch (Exception e) {
log.error("处理完成事件失败", e); log.error("处理完成事件失败", e);
} finally { } finally {
...@@ -110,7 +145,7 @@ public class SseTokenEmitter implements TokenConsumerWithCompletion { ...@@ -110,7 +145,7 @@ public class SseTokenEmitter implements TokenConsumerWithCompletion {
public void closeEmitter() { public void closeEmitter() {
try { try {
if (emitter != null && !userSseService.isEmitterCompleted(emitter)) { if (emitter != null && !userSseService.isEmitterCompleted(emitter)) {
emitter.complete(); // emitter.complete();
log.debug("SSE连接已关闭"); log.debug("SSE连接已关闭");
} }
} catch (Exception ex) { } catch (Exception ex) {
...@@ -125,8 +160,4 @@ public class SseTokenEmitter implements TokenConsumerWithCompletion { ...@@ -125,8 +160,4 @@ public class SseTokenEmitter implements TokenConsumerWithCompletion {
public interface CompletionCallback { public interface CompletionCallback {
void onComplete(SseEmitter emitter, Agent agent, AgentRequest request, String userId, String fullContent); void onComplete(SseEmitter emitter, Agent agent, AgentRequest request, String userId, String fullContent);
} }
public String getUserId() {
return userId;
}
} }
\ No newline at end of file
package pangea.hiagent.agent.service; package pangea.hiagent.agent.service;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.concurrent.atomic.AtomicBoolean;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import pangea.hiagent.workpanel.event.EventService;
/** /**
* Token消费者接口,支持完成回调 * Token消费者接口,支持完成回调
...@@ -17,17 +14,4 @@ public interface TokenConsumerWithCompletion extends Consumer<String> { ...@@ -17,17 +14,4 @@ public interface TokenConsumerWithCompletion extends Consumer<String> {
default void onComplete(String fullContent) { default void onComplete(String fullContent) {
// 默认实现为空 // 默认实现为空
} }
/**
* 当流式处理完成时调用,发送完成事件到前端
* @param fullContent 完整的内容
* @param emitter SSE发射器
* @param sseEventSender SSE事件发送器
* @param isCompleted 完成状态标记
*/
default void onComplete(String fullContent, SseEmitter emitter,
EventService eventService,
AtomicBoolean isCompleted) {
// 默认实现将在子类中覆盖
}
} }
\ No newline at end of file
...@@ -15,8 +15,8 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry ...@@ -15,8 +15,8 @@ import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
import org.springframework.web.socket.server.HandshakeInterceptor; import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import pangea.hiagent.workpanel.playwright.PlaywrightManager;
import pangea.hiagent.common.utils.JwtUtil; import pangea.hiagent.common.utils.JwtUtil;
import pangea.hiagent.tool.playwright.PlaywrightManager;
import pangea.hiagent.websocket.DomSyncHandler; import pangea.hiagent.websocket.DomSyncHandler;
import java.util.Map; import java.util.Map;
......
...@@ -104,30 +104,22 @@ public class MetaObjectHandlerConfig implements MetaObjectHandler { ...@@ -104,30 +104,22 @@ public class MetaObjectHandlerConfig implements MetaObjectHandler {
/** /**
* 获取当前用户ID,支持异步线程上下文 * 获取当前用户ID,支持异步线程上下文
* 该方法支持以下场景: * 该方法支持以下场景:
* 1. 同步请求:从SecurityContext获取用户ID * 1. 优先从ThreadLocal获取(支持异步线程)
* 2. 异步任务:从AsyncUserContextDecorator传播的上下文获取用户ID * 2. 从SecurityContext获取(支持同步请求和AsyncUserContextDecorator传播)
* 3. 故障转移:尝试直接解析Token获取用户ID * 3. 从请求中解析Token获取用户ID
* *
* @return 用户ID,如果无法获取则返回null * @return 用户ID,如果无法获取则返回null
*/ */
private String getCurrentUserIdWithContext() { private String getCurrentUserIdWithContext() {
try { try {
// 方式1:首先尝试从SecurityContext获取(支持同步请求和AsyncUserContextDecorator传播) // 直接调用UserUtils.getCurrentUserIdStatic(),该方法已经包含了所有获取用户ID的方式
String userId = UserUtils.getCurrentUserId(); // 并且优先从ThreadLocal获取,支持异步线程
String userId = UserUtils.getCurrentUserIdStatic();
if (userId != null) { if (userId != null) {
log.debug("通过SecurityContext成功获取用户ID: {}", userId); log.debug("成功获取用户ID: {}", userId);
return 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字段将不被填充"); log.warn("无法通过任何方式获取当前用户ID,createdBy/updatedBy字段将不被填充");
return null; return null;
} catch (Exception e) { } catch (Exception e) {
......
...@@ -134,21 +134,25 @@ public class SecurityConfig { ...@@ -134,21 +134,25 @@ public class SecurityConfig {
try { try {
// 对于SSE端点的特殊处理 // 对于SSE端点的特殊处理
boolean isStreamEndpoint = request.getRequestURI().contains("/api/v1/agent/chat-stream"); boolean isStreamEndpoint = request.getRequestURI().contains("/api/v1/agent/chat-stream");
boolean isTimelineEndpoint = request.getRequestURI().contains("/api/v1/agent/timeline-events");
if (isStreamEndpoint) {
if (isStreamEndpoint || isTimelineEndpoint) { // 再次检查响应是否已经提交
// 对于SSE端点,发送SSE格式的错误事件 if (response.isCommitted()) {
response.setContentType("text/event-stream;charset=UTF-8"); log.warn("SSE端点响应已提交,无法发送认证错误");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("event: error\ndata: {\"error\": \"未授权访问\", \"timestamp\": " + System.currentTimeMillis() + "}\n\n");
response.getWriter().flush();
// 确保响应被正确提交
if (!response.isCommitted()) {
response.flushBuffer();
}
return; return;
} }
// 对于SSE端点,发送SSE格式的错误事件
response.setContentType("text/event-stream;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("event: error\ndata: {\"error\": \"未授权访问\", \"timestamp\": " + System.currentTimeMillis() + "}\n\n");
response.getWriter().flush();
// 确保响应被正确提交
if (!response.isCommitted()) {
response.flushBuffer();
}
return;
}
response.setStatus(401); response.setStatus(401);
response.setContentType("application/json;charset=UTF-8"); response.setContentType("application/json;charset=UTF-8");
...@@ -174,21 +178,25 @@ public class SecurityConfig { ...@@ -174,21 +178,25 @@ public class SecurityConfig {
try { try {
// 对于SSE端点的特殊处理 // 对于SSE端点的特殊处理
boolean isStreamEndpoint = request.getRequestURI().contains("/api/v1/agent/chat-stream"); boolean isStreamEndpoint = request.getRequestURI().contains("/api/v1/agent/chat-stream");
boolean isTimelineEndpoint = request.getRequestURI().contains("/api/v1/agent/timeline-events");
if (isStreamEndpoint) {
if (isStreamEndpoint || isTimelineEndpoint) { // 再次检查响应是否已经提交
// 对于SSE端点,发送SSE格式的错误事件 if (response.isCommitted()) {
response.setContentType("text/event-stream;charset=UTF-8"); log.warn("SSE端点响应已提交,无法发送访问拒绝错误");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("event: error\ndata: {\"error\": \"访问被拒绝\", \"timestamp\": " + System.currentTimeMillis() + "}\n\n");
response.getWriter().flush();
// 确保响应被正确提交
if (!response.isCommitted()) {
response.flushBuffer();
}
return; return;
} }
// 对于SSE端点,发送SSE格式的错误事件
response.setContentType("text/event-stream;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.getWriter().write("event: error\ndata: {\"error\": \"访问被拒绝\", \"timestamp\": " + System.currentTimeMillis() + "}\n\n");
response.getWriter().flush();
// 确保响应被正确提交
if (!response.isCommitted()) {
response.flushBuffer();
}
return;
}
response.setStatus(403); response.setStatus(403);
response.setContentType("application/json;charset=UTF-8"); response.setContentType("application/json;charset=UTF-8");
......
...@@ -208,6 +208,30 @@ public class GlobalExceptionHandler { ...@@ -208,6 +208,30 @@ public class GlobalExceptionHandler {
/** /**
* 处理授权拒绝异常 * 处理授权拒绝异常
*/ */
/**
* 获取当前响应对象
*/
private jakarta.servlet.http.HttpServletResponse getCurrentResponse() {
if (org.springframework.web.context.request.RequestContextHolder.getRequestAttributes() != null) {
Object requestAttributes = org.springframework.web.context.request.RequestContextHolder
.getRequestAttributes();
if (requestAttributes instanceof org.springframework.web.context.request.ServletRequestAttributes) {
org.springframework.web.context.request.ServletRequestAttributes servletRequestAttributes =
(org.springframework.web.context.request.ServletRequestAttributes) requestAttributes;
return servletRequestAttributes.getResponse();
}
}
return null;
}
/**
* 检查响应是否已提交
*/
private boolean isResponseCommitted() {
jakarta.servlet.http.HttpServletResponse httpResponse = getCurrentResponse();
return httpResponse != null && httpResponse.isCommitted();
}
@ExceptionHandler(AuthorizationDeniedException.class) @ExceptionHandler(AuthorizationDeniedException.class)
public ResponseEntity<ApiResponse<Void>> handleAuthorizationDeniedException( public ResponseEntity<ApiResponse<Void>> handleAuthorizationDeniedException(
AuthorizationDeniedException e, HttpServletRequest request) { AuthorizationDeniedException e, HttpServletRequest request) {
...@@ -217,42 +241,16 @@ public class GlobalExceptionHandler { ...@@ -217,42 +241,16 @@ public class GlobalExceptionHandler {
String requestUri = request.getRequestURI(); String requestUri = request.getRequestURI();
boolean isSseEndpoint = requestUri.contains("/api/v1/agent/chat-stream") || requestUri.contains("/api/v1/agent/timeline-events"); boolean isSseEndpoint = requestUri.contains("/api/v1/agent/chat-stream") || requestUri.contains("/api/v1/agent/timeline-events");
// 检查响应是否已经提交 // 检查响应是否已提交
jakarta.servlet.http.HttpServletResponse httpResponse = null; if (isResponseCommitted()) {
if (org.springframework.web.context.request.RequestContextHolder.getRequestAttributes() != null) { log.warn("响应已提交,无法发送访问拒绝错误: {}", request.getRequestURL());
Object requestAttributes = org.springframework.web.context.request.RequestContextHolder return ResponseEntity.ok().build();
.getRequestAttributes();
if (requestAttributes instanceof org.springframework.web.context.request.ServletRequestAttributes) {
org.springframework.web.context.request.ServletRequestAttributes servletRequestAttributes =
(org.springframework.web.context.request.ServletRequestAttributes) requestAttributes;
if (servletRequestAttributes.getResponse() instanceof jakarta.servlet.http.HttpServletResponse) {
httpResponse = (jakarta.servlet.http.HttpServletResponse) servletRequestAttributes.getResponse();
}
}
// 检查响应是否已提交
if (httpResponse != null && httpResponse.isCommitted()) {
log.warn("响应已提交,无法发送访问拒绝错误: {}", request.getRequestURL());
// 如果是SSE端点且响应已提交,返回空响应避免二次异常
return ResponseEntity.ok().build();
}
} }
// 如果是SSE端点,但响应未提交,发送SSE格式的错误响应 // 如果是SSE端点,但响应未提交,发送SSE格式的错误响应
if (isSseEndpoint) { if (isSseEndpoint) {
try { try {
jakarta.servlet.http.HttpServletResponse sseResponse = null; jakarta.servlet.http.HttpServletResponse sseResponse = getCurrentResponse();
if (org.springframework.web.context.request.RequestContextHolder.getRequestAttributes() != null) {
Object requestAttributes = org.springframework.web.context.request.RequestContextHolder
.getRequestAttributes();
if (requestAttributes instanceof org.springframework.web.context.request.ServletRequestAttributes) {
org.springframework.web.context.request.ServletRequestAttributes servletRequestAttributes =
(org.springframework.web.context.request.ServletRequestAttributes) requestAttributes;
if (servletRequestAttributes.getResponse() instanceof jakarta.servlet.http.HttpServletResponse) {
sseResponse = (jakarta.servlet.http.HttpServletResponse) servletRequestAttributes.getResponse();
}
}
}
if (sseResponse != null) { if (sseResponse != null) {
sseResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); sseResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
......
...@@ -98,17 +98,25 @@ public class AsyncUserContextDecorator { ...@@ -98,17 +98,25 @@ public class AsyncUserContextDecorator {
public static Runnable wrapWithContext(Runnable runnable) { public static Runnable wrapWithContext(Runnable runnable) {
// 捕获当前线程的用户上下文 // 捕获当前线程的用户上下文
UserContextHolder userContext = captureUserContext(); UserContextHolder userContext = captureUserContext();
// 同时捕获当前线程的用户ID(用于ThreadLocal传播)
String currentUserId = UserUtils.getCurrentUserIdStatic();
return () -> { return () -> {
try { try {
// 在异步线程中传播用户上下文 // 在异步线程中传播用户上下文
propagateUserContext(userContext); propagateUserContext(userContext);
// 将用户ID设置到ThreadLocal中,增强可靠性
if (currentUserId != null) {
UserUtils.setCurrentUserIdStatic(currentUserId);
}
// 执行原始任务 // 执行原始任务
runnable.run(); runnable.run();
} finally { } finally {
// 清理当前线程的用户上下文 // 清理当前线程的用户上下文
clearUserContext(); clearUserContext();
// 清理ThreadLocal中的用户ID
UserUtils.clearCurrentUserIdStatic();
} }
}; };
} }
...@@ -122,17 +130,25 @@ public class AsyncUserContextDecorator { ...@@ -122,17 +130,25 @@ public class AsyncUserContextDecorator {
public static <V> Callable<V> wrapWithContext(Callable<V> callable) { public static <V> Callable<V> wrapWithContext(Callable<V> callable) {
// 捕获当前线程的用户上下文 // 捕获当前线程的用户上下文
UserContextHolder userContext = captureUserContext(); UserContextHolder userContext = captureUserContext();
// 同时捕获当前线程的用户ID(用于ThreadLocal传播)
String currentUserId = UserUtils.getCurrentUserIdStatic();
return () -> { return () -> {
try { try {
// 在异步线程中传播用户上下文 // 在异步线程中传播用户上下文
propagateUserContext(userContext); propagateUserContext(userContext);
// 将用户ID设置到ThreadLocal中,增强可靠性
if (currentUserId != null) {
UserUtils.setCurrentUserIdStatic(currentUserId);
}
// 执行原始任务 // 执行原始任务
return callable.call(); return callable.call();
} finally { } finally {
// 清理当前线程的用户上下文 // 清理当前线程的用户上下文
clearUserContext(); clearUserContext();
// 清理ThreadLocal中的用户ID
UserUtils.clearCurrentUserIdStatic();
} }
}; };
} }
......
...@@ -59,7 +59,7 @@ public class MemoryService { ...@@ -59,7 +59,7 @@ public class MemoryService {
* @return 用户ID * @return 用户ID
*/ */
private String getCurrentUserId() { private String getCurrentUserId() {
String userId = UserUtils.getCurrentUserId(); String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) { if (userId == null) {
log.warn("无法通过UserUtils获取当前用户ID"); log.warn("无法通过UserUtils获取当前用户ID");
} }
......
...@@ -4,11 +4,10 @@ import lombok.extern.slf4j.Slf4j; ...@@ -4,11 +4,10 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import pangea.hiagent.web.service.AgentService;
import pangea.hiagent.web.service.TimerService;
import pangea.hiagent.model.Agent; import pangea.hiagent.model.Agent;
import pangea.hiagent.model.TimerConfig; import pangea.hiagent.model.TimerConfig;
import pangea.hiagent.web.service.AgentService;
import pangea.hiagent.web.service.TimerService;
import java.io.Serializable; import java.io.Serializable;
...@@ -20,6 +19,9 @@ import java.io.Serializable; ...@@ -20,6 +19,9 @@ import java.io.Serializable;
@Component("permissionEvaluator") @Component("permissionEvaluator")
public class DefaultPermissionEvaluator implements PermissionEvaluator { public class DefaultPermissionEvaluator implements PermissionEvaluator {
private static final String AGENT_TYPE = "Agent";
private static final String TIMER_CONFIG_TYPE = "TimerConfig";
private final AgentService agentService; private final AgentService agentService;
private final TimerService timerService; private final TimerService timerService;
...@@ -37,33 +39,21 @@ public class DefaultPermissionEvaluator implements PermissionEvaluator { ...@@ -37,33 +39,21 @@ public class DefaultPermissionEvaluator implements PermissionEvaluator {
return false; return false;
} }
Object principal = authentication.getPrincipal(); String userId = authentication.getPrincipal().toString();
if (principal == null) {
return false;
}
String userId = principal.toString();
String perm = (String) permission; String perm = (String) permission;
try { try {
// 处理Agent访问权限 // 处理Agent访问权限
if (targetDomainObject instanceof Agent) { if (targetDomainObject instanceof Agent) {
Agent agent = (Agent) targetDomainObject; return checkAgentAccess(userId, (Agent) targetDomainObject, perm);
return checkAgentAccess(userId, agent, perm);
} }
// 处理TimerConfig访问权限 // 处理TimerConfig访问权限
else if (targetDomainObject instanceof TimerConfig) { else if (targetDomainObject instanceof TimerConfig) {
TimerConfig timer = (TimerConfig) targetDomainObject; return checkTimerAccess(userId, (TimerConfig) targetDomainObject, perm);
return checkTimerAccess(userId, timer, perm);
}
// 处理基于ID的资源访问
else if (targetDomainObject instanceof String) {
// 这种情况在hasPermission(Authentication, Serializable, String, Object)方法中处理
return false;
} }
} catch (Exception e) { } catch (Exception e) {
log.error("权限检查过程中发生异常: userId={}, targetDomainObject={}, permission={}", userId, targetDomainObject, permission, e); log.error("权限检查异常: userId={}, target={}, permission={}, error={}",
return false; userId, targetDomainObject.getClass().getSimpleName(), perm, e.getMessage());
} }
return false; return false;
...@@ -75,36 +65,23 @@ public class DefaultPermissionEvaluator implements PermissionEvaluator { ...@@ -75,36 +65,23 @@ public class DefaultPermissionEvaluator implements PermissionEvaluator {
return false; return false;
} }
Object principal = authentication.getPrincipal(); String userId = authentication.getPrincipal().toString();
if (principal == null) {
return false;
}
String userId = principal.toString();
String perm = (String) permission; String perm = (String) permission;
try { try {
// 处理基于ID的权限检查 // 处理基于ID的权限检查
if ("Agent".equals(targetType)) { if (AGENT_TYPE.equals(targetType)) {
Agent agent = agentService.getAgent(targetId.toString()); Agent agent = agentService.getAgent(targetId.toString());
if (agent == null) { return agent != null && checkAgentAccess(userId, agent, perm);
log.warn("未找到ID为 {} 的Agent", targetId);
return false;
}
return checkAgentAccess(userId, agent, perm);
} }
// 处理TimerConfig资源的权限检查 // 处理TimerConfig资源的权限检查
else if ("TimerConfig".equals(targetType)) { else if (TIMER_CONFIG_TYPE.equals(targetType)) {
TimerConfig timer = timerService.getTimerById(targetId.toString()); TimerConfig timer = timerService.getTimerById(targetId.toString());
if (timer == null) { return timer != null && checkTimerAccess(userId, timer, perm);
log.warn("未找到ID为 {} 的TimerConfig", targetId);
return false;
}
return checkTimerAccess(userId, timer, perm);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("基于ID的权限检查过程中发生异常: userId={}, targetId={}, targetType={}, permission={}", userId, targetId, targetType, permission, e); log.error("基于ID的权限检查异常: userId={}, targetId={}, targetType={}, permission={}, error={}",
return false; userId, targetId, targetType, perm, e.getMessage());
} }
return false; return false;
...@@ -119,24 +96,17 @@ public class DefaultPermissionEvaluator implements PermissionEvaluator { ...@@ -119,24 +96,17 @@ public class DefaultPermissionEvaluator implements PermissionEvaluator {
return true; return true;
} }
// 检查Agent所有者 // 所有者可以访问
if (agent.getOwner().equals(userId)) { if (agent.getOwner().equals(userId)) {
return true; return true;
} }
// 根据权限类型进行检查 // 根据权限类型进行检查(目前只支持所有者访问)
switch (permission.toLowerCase()) { String permissionLower = permission.toLowerCase();
case "read": return switch (permissionLower) {
// 所有用户都可以读取公开的Agent(如果有此概念) case "read", "write", "delete", "execute" -> agent.getOwner().equals(userId);
return false; // 暂时不支持公开Agent default -> false;
case "write": };
case "delete":
case "execute":
// 只有所有者可以写入、删除或执行Agent
return agent.getOwner().equals(userId);
default:
return false;
}
} }
/** /**
...@@ -148,32 +118,24 @@ public class DefaultPermissionEvaluator implements PermissionEvaluator { ...@@ -148,32 +118,24 @@ public class DefaultPermissionEvaluator implements PermissionEvaluator {
return true; return true;
} }
// 检查定时器创建者 // 创建者可以访问
if (timer.getCreatedBy() != null && timer.getCreatedBy().equals(userId)) { if (timer.getCreatedBy() != null && timer.getCreatedBy().equals(userId)) {
return true; return true;
} }
// 根据权限类型进行检查 // 根据权限类型进行检查(目前只支持创建者访问)
switch (permission.toLowerCase()) { String permissionLower = permission.toLowerCase();
case "read": return switch (permissionLower) {
// 所有用户都可以读取公开的定时器(如果有此概念) case "read", "write", "delete" -> timer.getCreatedBy() != null && timer.getCreatedBy().equals(userId);
return false; // 暂时不支持公开定时器 default -> false;
case "write": };
case "delete":
// 只有创建者可以修改或删除定时器
return timer.getCreatedBy() != null && timer.getCreatedBy().equals(userId);
default:
return false;
}
} }
/** /**
* 检查是否为管理员用户 * 检查是否为管理员用户
*/ */
private boolean isAdminUser(String userId) { private boolean isAdminUser(String userId) {
// 这里可以根据实际需求实现管理员检查逻辑 // 管理员用户检查,可扩展为从配置或数据库读取
// 例如查询数据库或检查特殊用户ID
// 当前实现保留原有逻辑,但可以通过配置或数据库来管理管理员用户
return "admin".equals(userId) || "user-001".equals(userId); return "admin".equals(userId) || "user-001".equals(userId);
} }
} }
\ No newline at end of file
...@@ -5,18 +5,17 @@ import jakarta.servlet.ServletException; ...@@ -5,18 +5,17 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import pangea.hiagent.common.utils.JwtUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import pangea.hiagent.common.utils.JwtUtil;
import pangea.hiagent.common.utils.UserUtils;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.List;
/** /**
* JWT认证过滤器 * JWT认证过滤器
...@@ -26,95 +25,50 @@ import java.util.List; ...@@ -26,95 +25,50 @@ import java.util.List;
@Component @Component
public class JwtAuthenticationFilter extends OncePerRequestFilter { public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String BEARER_PREFIX = "Bearer ";
private final JwtUtil jwtUtil; private final JwtUtil jwtUtil;
private final UserUtils userUtils;
public JwtAuthenticationFilter(JwtUtil jwtUtil) { public JwtAuthenticationFilter(JwtUtil jwtUtil, UserUtils userUtils) {
this.jwtUtil = jwtUtil; this.jwtUtil = jwtUtil;
this.userUtils = userUtils;
} }
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException { throws ServletException, IOException {
boolean isStreamEndpoint = request.getRequestURI().contains("/api/v1/agent/chat-stream");
boolean isTimelineEndpoint = request.getRequestURI().contains("/api/v1/agent/timeline-events");
if (isStreamEndpoint) {
log.info("处理Agent流式对话请求: {} {}", request.getMethod(), request.getRequestURI());
}
if (isTimelineEndpoint) {
log.info("处理时间轴事件订阅请求: {} {}", request.getMethod(), request.getRequestURI());
}
// 对于OPTIONS请求,直接放行 // 对于OPTIONS请求,直接放行
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
log.debug("OPTIONS请求,直接放行");
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
try { try {
String token = extractTokenFromRequest(request); String token = extractTokenFromRequest(request);
log.debug("JWT过滤器处理请求: {} {},提取到token: {}", request.getMethod(), request.getRequestURI(), token);
if (StringUtils.hasText(token)) { if (StringUtils.hasText(token)) {
log.debug("开始JWT验证,token长度: {}", token.length());
// 验证token是否有效 // 验证token是否有效
boolean isValid = jwtUtil.validateToken(token); if (jwtUtil.validateToken(token)) {
log.debug("JWT验证结果: {}", isValid);
if (isValid) {
String userId = jwtUtil.getUserIdFromToken(token); String userId = jwtUtil.getUserIdFromToken(token);
log.debug("JWT验证通过,用户ID: {}", userId);
if (userId != null) { if (userId != null) {
// 创建认证对象,添加基本权限 // 创建认证对象,添加基本权限
List<SimpleGrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); var authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
UsernamePasswordAuthenticationToken authentication = var authentication = new UsernamePasswordAuthenticationToken(userId, null, authorities);
new UsernamePasswordAuthenticationToken(userId, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("已设置SecurityContext中的认证信息,用户ID: {}, 权限: {}", userId, authentication.getAuthorities());
} else { userUtils.setCurrentUserId(userId);
log.warn("从token中提取的用户ID为空");
} }
} else {
log.warn("JWT验证失败,token可能已过期或无效");
// 检查token是否过期
boolean isExpired = jwtUtil.isTokenExpired(token);
log.warn("Token过期状态: {}", isExpired);
} }
} else {
log.debug("未找到有效的token");
// 记录请求信息以便调试
log.debug("请求URL: {}", request.getRequestURL());
log.debug("请求方法: {}", request.getMethod());
log.debug("Authorization头: {}", request.getHeader("Authorization"));
log.debug("token参数: {}", request.getParameter("token"));
} }
} catch (Exception e) { } catch (Exception e) {
log.error("JWT认证处理异常", e); log.error("JWT认证处理异常: {}", e.getMessage());
// 不在此处发送错误响应,让Spring Security的ExceptionTranslationFilter处理 // 不在此处发送错误响应,让Spring Security的ExceptionTranslationFilter处理
// 这样可以避免响应被提前提交
}
// 特别处理流式端点的权限问题
if (isStreamEndpoint || isTimelineEndpoint) {
// 检查是否已认证
if (SecurityContextHolder.getContext().getAuthentication() == null) {
log.warn("流式端点未认证访问: {} {}", request.getMethod(), request.getRequestURI());
// 对于SSE端点,如果未认证,我们不立即返回错误,而是让后续处理决定
// 因为客户端可能会在重新连接时带上token
}
// 对于SSE端点,直接执行过滤器链,不进行额外的响应检查
filterChain.doFilter(request, response);
log.debug("JwtAuthenticationFilter处理完成(SSE端点): {} {}", request.getMethod(), request.getRequestURI());
return;
} }
// 继续执行过滤器链,让Spring Security的其他过滤器处理认证和授权 // 继续执行过滤器链
// 这样可以让ExceptionTranslationFilter和AuthorizationFilter正确处理认证失败和权限拒绝
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
log.debug("JwtAuthenticationFilter处理完成: {} {}", request.getMethod(), request.getRequestURI());
} }
/** /**
...@@ -124,23 +78,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { ...@@ -124,23 +78,11 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
private String extractTokenFromRequest(HttpServletRequest request) { private String extractTokenFromRequest(HttpServletRequest request) {
// 首先尝试从请求头中提取Token // 首先尝试从请求头中提取Token
String authHeader = request.getHeader("Authorization"); String authHeader = request.getHeader("Authorization");
log.debug("从请求头中提取Authorization: {}", authHeader); if (StringUtils.hasText(authHeader) && authHeader.startsWith(BEARER_PREFIX)) {
if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) { return authHeader.substring(BEARER_PREFIX.length());
String token = authHeader.substring(7);
log.debug("从Authorization头中提取到token");
return token;
} }
// 如果请求头中没有Token,则尝试从URL参数中提取 // 如果请求头中没有Token,则尝试从URL参数中提取
// 这对于SSE连接特别有用,因为浏览器在自动重连时可能不会发送Authorization头 return request.getParameter("token");
String tokenParam = request.getParameter("token");
log.debug("从URL参数中提取token参数: {}", tokenParam);
if (StringUtils.hasText(tokenParam)) {
log.debug("从URL参数中提取到token");
return tokenParam;
}
log.debug("未找到有效的token");
return null;
} }
} }
\ No newline at end of file
package pangea.hiagent.tool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import pangea.hiagent.agent.service.UserSseService;
import pangea.hiagent.common.utils.UserUtils;
import pangea.hiagent.agent.data.WorkPanelEvent;
import java.util.HashMap;
import java.util.Map;
/**
* 所有工具类的基础抽象类
* 提供工具执行监控和SSE事件发送功能
*/
@Slf4j
public abstract class BaseTool {
@Autowired
private UserSseService userSseService;
@Autowired
private UserUtils userUtils;
/**
* 工具执行包装方法
* 监控工具方法的完整执行生命周期
* @param methodName 被调用的方法名称
* @param params 方法参数映射
* @param action 实际执行的工具逻辑
* @param <T> 返回类型
* @return 工具执行结果
*/
protected <T> T execute(String methodName, Map<String, Object> params, ToolAction<T> action) {
String toolName = this.getClass().getSimpleName();
long startTime = System.currentTimeMillis();
// 在方法开始时获取用户ID,此时线程通常是原始请求线程,能够正确获取
String userId = userUtils.getCurrentUserId();
// 1. 发送工具开始执行事件
sendToolEvent(toolName, methodName, params, null, "执行中", startTime, null,null, userId);
T result = null;
String status = "成功";
Exception exception = null;
try {
// 2. 执行实际的工具逻辑
result = action.run();
} catch (Exception e) {
status = "失败";
exception = e;
throw new RuntimeException("工具执行失败: " + e.getMessage(), e);
} finally {
// 记录结束时间和耗时
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// 3. 发送工具执行完成事件
sendToolEvent(toolName, methodName, params, result, status, startTime, duration, exception, userId);
}
return result;
}
/**
* 简化版execute方法,无需手动构建参数映射
* @param methodName 被调用的方法名称
* @param action 实际执行的工具逻辑
* @param <T> 返回类型
* @return 工具执行结果
*/
protected <T> T execute(String methodName, ToolAction<T> action) {
return execute(methodName, new HashMap<>(), action);
}
/**
* 发送工具事件给前端
* @param toolName 工具名称
* @param methodName 方法名称
* @param params 参数信息
* @param result 执行结果
* @param status 执行状态(执行中/成功/失败)
* @param startTime 开始时间戳
* @param duration 执行耗时(毫秒)
* @param exception 异常信息(可选)
* @param userId 用户ID,从方法开始时传递
*/
private void sendToolEvent(String toolName, String methodName,
Map<String, Object> params, Object result, String status,
Long startTime, Long duration, Exception exception, String userId) {
try {
Map<String, Object> eventData = new HashMap<>();
eventData.put("toolName", toolName);
eventData.put("methodName", methodName);
eventData.put("params", params);
eventData.put("result", result);
eventData.put("status", status);
eventData.put("startTime", startTime);
eventData.put("duration", duration);
if (exception != null) {
eventData.put("error", exception.getMessage());
eventData.put("errorType", exception.getClass().getSimpleName());
}
WorkPanelEvent event = WorkPanelEvent.builder()
.type("tool_call")
.title(toolName + "." + methodName)
.timestamp(System.currentTimeMillis())
.metadata(eventData)
.userId(userId)
.build();
// 获取用户的SSE发射器
userSseService.sendWorkPanelEvent(event);
log.debug("已发送工具事件: {}#{}, 状态: {}", toolName, methodName, status);
} catch (Exception e) {
log.error("发送工具事件失败: {}", e.getMessage(), e);
}
}
/**
* 工具动作函数式接口
* 用于封装实际要执行的工具逻辑
* @param <T> 返回类型
*/
@FunctionalInterface
protected interface ToolAction<T> {
T run() throws Exception;
}
}
\ No newline at end of file
// package pangea.hiagent.tool.aspect;
// import lombok.extern.slf4j.Slf4j;
// import org.aspectj.lang.ProceedingJoinPoint;
// import org.aspectj.lang.annotation.Around;
// import org.aspectj.lang.annotation.Aspect;
// import org.aspectj.lang.reflect.MethodSignature;
// import org.springframework.ai.tool.annotation.Tool;
// import org.springframework.beans.factory.annotation.Autowired;
// import org.springframework.stereotype.Component;
// import pangea.hiagent.workpanel.IWorkPanelDataCollector;
// import java.util.HashMap;
// import java.util.Map;
// /**
// * 工具执行日志记录切面类
// * 自动记录带有@Tool注解的方法执行信息,包括工具名称、方法名、输入参数、输出结果、运行时长等
// */
// @Slf4j
// @Aspect
// @Component
// public class ToolExecutionLoggerAspect {
// @Autowired
// private IWorkPanelDataCollector workPanelDataCollector;
// /**
// * 环绕通知,拦截所有带有@Tool注解的方法
// * @param joinPoint 连接点
// * @return 方法执行结果
// * @throws Throwable 异常
// */
// @Around("@annotation(tool)")
// public Object logToolExecution(ProceedingJoinPoint joinPoint, Tool tool) throws Throwable {
// // 获取方法签名
// MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// String methodName = signature.getName();
// String className = signature.getDeclaringType().getSimpleName();
// String fullMethodName = className + "." + methodName;
// // 获取工具描述
// String toolDescription = tool.description();
// // 获取方法参数
// String[] paramNames = signature.getParameterNames();
// Object[] args = joinPoint.getArgs();
// // 构建输入参数映射
// Map<String, Object> inputParams = new HashMap<>();
// if (paramNames != null && args != null) {
// for (int i = 0; i < paramNames.length; i++) {
// if (i < args.length) {
// inputParams.put(paramNames[i], args[i]);
// }
// }
// }
// // 记录开始时间
// long startTime = System.currentTimeMillis();
// // 记录工具调用开始
// if (workPanelDataCollector != null) {
// try {
// workPanelDataCollector.recordToolCallAction(className, inputParams, null, "pending", null);
// } catch (Exception e) {
// log.warn("记录工具调用开始时发生错误: {}", e.getMessage(), e);
// }
// }
// log.info("开始执行工具方法: {},描述: {}", fullMethodName, toolDescription);
// log.debug("工具方法参数: {}", inputParams);
// try {
// // 执行原方法
// Object result = joinPoint.proceed();
// // 记录结束时间
// long endTime = System.currentTimeMillis();
// long executionTime = endTime - startTime;
// // 记录工具调用完成
// if (workPanelDataCollector != null) {
// try {
// workPanelDataCollector.recordToolCallAction(className, inputParams, result, "success", executionTime);
// } catch (Exception e) {
// log.warn("记录工具调用完成时发生错误: {}", e.getMessage(), e);
// }
// }
// log.info("工具方法执行成功: {},描述: {},耗时: {}ms", fullMethodName, toolDescription, executionTime);
// // 精简日志记录,避免过多的debug级别日志
// if (log.isTraceEnabled()) {
// log.trace("工具方法执行结果类型: {},结果: {}",
// result != null ? result.getClass().getSimpleName() : "null", result);
// }
// return result;
// } catch (Exception e) {
// // 记录结束时间
// long endTime = System.currentTimeMillis();
// long executionTime = endTime - startTime;
// // 记录工具调用错误
// if (workPanelDataCollector != null) {
// try {
// workPanelDataCollector.recordToolCallAction(className, inputParams, e, "error", executionTime);
// } catch (Exception ex) {
// log.warn("记录工具调用错误时发生错误: {}", ex.getMessage(), ex);
// }
// }
// log.error("工具方法执行失败: {},描述: {},耗时: {}ms,错误类型: {}",
// fullMethodName, toolDescription, executionTime, e.getClass().getSimpleName(), e);
// throw e;
// }
// }
// }
\ No newline at end of file
...@@ -2,6 +2,9 @@ package pangea.hiagent.tool.impl; ...@@ -2,6 +2,9 @@ package pangea.hiagent.tool.impl;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.Tool;
import pangea.hiagent.tool.BaseTool;
import java.util.HashMap;
import java.util.Map;
/** /**
* 计算器工具类 * 计算器工具类
...@@ -9,38 +12,62 @@ import org.springframework.ai.tool.annotation.Tool; ...@@ -9,38 +12,62 @@ import org.springframework.ai.tool.annotation.Tool;
*/ */
@Slf4j @Slf4j
@Component @Component
public class CalculatorTools { public class CalculatorTools extends BaseTool {
@Tool(description = "执行两个数字的加法运算") @Tool(description = "执行两个数字的加法运算")
public double add(double a, double b) { public double add(double a, double b) {
double result = a + b; Map<String, Object> params = new HashMap<>();
log.debug("执行加法运算: {} + {} = {}", a, b, result); params.put("a", a);
return result; params.put("b", b);
return execute("add", params, () -> {
double result = a + b;
log.debug("执行加法运算: {} + {} = {}", a, b, result);
return result;
});
} }
@Tool(description = "执行两个数字的减法运算") @Tool(description = "执行两个数字的减法运算")
public double subtract(double a, double b) { public double subtract(double a, double b) {
double result = a - b; Map<String, Object> params = new HashMap<>();
log.debug("执行减法运算: {} - {} = {}", a, b, result); params.put("a", a);
return result; params.put("b", b);
return execute("subtract", params, () -> {
double result = a - b;
log.debug("执行减法运算: {} - {} = {}", a, b, result);
return result;
});
} }
@Tool(description = "执行两个数字的乘法运算") @Tool(description = "执行两个数字的乘法运算")
public double multiply(double a, double b) { public double multiply(double a, double b) {
double result = a * b; Map<String, Object> params = new HashMap<>();
log.debug("执行乘法运算: {} * {} = {}", a, b, result); params.put("a", a);
return result; params.put("b", b);
return execute("multiply", params, () -> {
double result = a * b;
log.debug("执行乘法运算: {} * {} = {}", a, b, result);
return result;
});
} }
@Tool(description = "执行两个数字的除法运算") @Tool(description = "执行两个数字的除法运算")
public String divide(double a, double b) { public String divide(double a, double b) {
log.debug("执行除法运算: {} / {}", a, b); Map<String, Object> params = new HashMap<>();
if (b == 0) { params.put("a", a);
log.warn("除法运算错误:除数不能为零"); params.put("b", b);
return "错误:除数不能为零";
} return execute("divide", params, () -> {
double result = a / b; log.debug("执行除法运算: {} / {}", a, b);
log.debug("除法运算结果: {}", result); if (b == 0) {
return String.valueOf(result); log.warn("除法运算错误:除数不能为零");
return "错误:除数不能为零";
}
double result = a / b;
log.debug("除法运算结果: {}", result);
return String.valueOf(result);
});
} }
} }
\ No newline at end of file
...@@ -3,7 +3,7 @@ package pangea.hiagent.tool.impl; ...@@ -3,7 +3,7 @@ package pangea.hiagent.tool.impl;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.Tool;
import pangea.hiagent.tool.BaseTool;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.LocalDate; import java.time.LocalDate;
...@@ -16,7 +16,7 @@ import java.time.format.DateTimeFormatter; ...@@ -16,7 +16,7 @@ import java.time.format.DateTimeFormatter;
*/ */
@Slf4j @Slf4j
@Component @Component
public class DateTimeTools { public class DateTimeTools extends BaseTool {
private String dateTimeFormat = "yyyy-MM-dd HH:mm:ss"; private String dateTimeFormat = "yyyy-MM-dd HH:mm:ss";
...@@ -26,61 +26,69 @@ public class DateTimeTools { ...@@ -26,61 +26,69 @@ public class DateTimeTools {
@Tool(description = "获取当前日期和时间,返回格式为 'yyyy-MM-dd HH:mm:ss'") @Tool(description = "获取当前日期和时间,返回格式为 'yyyy-MM-dd HH:mm:ss'")
public String getCurrentDateTime() { public String getCurrentDateTime() {
try { return execute("getCurrentDateTime", () -> {
if (dateTimeFormat == null || dateTimeFormat.trim().isEmpty()) { try {
dateTimeFormat = "yyyy-MM-dd HH:mm:ss"; if (dateTimeFormat == null || dateTimeFormat.trim().isEmpty()) {
dateTimeFormat = "yyyy-MM-dd HH:mm:ss";
}
String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateTimeFormat));
log.info("【时间工具】获取当前日期时间: {}", dateTime);
return dateTime;
} catch (Exception e) {
log.error("获取当前日期时间时发生错误: {}", e.getMessage(), e);
// 发生错误时回退到默认格式
return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
} }
String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateTimeFormat)); });
log.info("【时间工具】获取当前日期时间: {}", dateTime);
return dateTime;
} catch (Exception e) {
log.error("获取当前日期时间时发生错误: {}", e.getMessage(), e);
// 发生错误时回退到默认格式
return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
} }
@Tool(description = "获取当前日期,返回格式为 'yyyy-MM-dd'") @Tool(description = "获取当前日期,返回格式为 'yyyy-MM-dd'")
public String getCurrentDate() { public String getCurrentDate() {
try { return execute("getCurrentDate", () -> {
if (dateFormat == null || dateFormat.trim().isEmpty()) { try {
dateFormat = "yyyy-MM-dd"; if (dateFormat == null || dateFormat.trim().isEmpty()) {
dateFormat = "yyyy-MM-dd";
}
String date = LocalDate.now().format(DateTimeFormatter.ofPattern(dateFormat));
log.info("【时间工具】获取当前日期: {}", date);
return date;
} catch (Exception e) {
log.error("获取当前日期时发生错误: {}", e.getMessage(), e);
// 发生错误时回退到默认格式
return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
} }
String date = LocalDate.now().format(DateTimeFormatter.ofPattern(dateFormat)); });
log.info("【时间工具】获取当前日期: {}", date);
return date;
} catch (Exception e) {
log.error("获取当前日期时发生错误: {}", e.getMessage(), e);
// 发生错误时回退到默认格式
return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}
} }
@Tool(description = "获取当前时间,返回格式为 'HH:mm:ss'") @Tool(description = "获取当前时间,返回格式为 'HH:mm:ss'")
public String getCurrentTime() { public String getCurrentTime() {
try { return execute("getCurrentTime", () -> {
if (timeFormat == null || timeFormat.trim().isEmpty()) { try {
timeFormat = "HH:mm:ss"; if (timeFormat == null || timeFormat.trim().isEmpty()) {
timeFormat = "HH:mm:ss";
}
String time = LocalTime.now().format(DateTimeFormatter.ofPattern(timeFormat));
log.info("【时间工具】获取当前时间: {}", time);
return time;
} catch (Exception e) {
log.error("获取当前时间时发生错误: {}", e.getMessage(), e);
// 发生错误时回退到默认格式
return LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
} }
String time = LocalTime.now().format(DateTimeFormatter.ofPattern(timeFormat)); });
log.info("【时间工具】获取当前时间: {}", time);
return time;
} catch (Exception e) {
log.error("获取当前时间时发生错误: {}", e.getMessage(), e);
// 发生错误时回退到默认格式
return LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"));
}
} }
@Tool(description = "获取当前时间戳(毫秒),返回自1970年1月1日00:00:00 UTC以来的毫秒数") @Tool(description = "获取当前时间戳(毫秒),返回自1970年1月1日00:00:00 UTC以来的毫秒数")
public String getCurrentTimeMillis() { public String getCurrentTimeMillis() {
try { return execute("getCurrentTimeMillis", () -> {
long timestamp = System.currentTimeMillis(); try {
log.info("【时间工具】获取当前时间戳: {}", timestamp); long timestamp = System.currentTimeMillis();
return String.valueOf(timestamp); log.info("【时间工具】获取当前时间戳: {}", timestamp);
} catch (Exception e) { return String.valueOf(timestamp);
log.error("获取当前时间戳时发生错误: {}", e.getMessage(), e); } catch (Exception e) {
return String.valueOf(System.currentTimeMillis()); log.error("获取当前时间戳时发生错误: {}", e.getMessage(), e);
} return String.valueOf(System.currentTimeMillis());
}
});
} }
} }
...@@ -4,10 +4,11 @@ import com.microsoft.playwright.*; ...@@ -4,10 +4,11 @@ import com.microsoft.playwright.*;
import com.microsoft.playwright.options.LoadState; import com.microsoft.playwright.options.LoadState;
import com.microsoft.playwright.options.WaitUntilState; import com.microsoft.playwright.options.WaitUntilState;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import pangea.hiagent.tool.playwright.PlaywrightManager;
import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.Tool;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import pangea.hiagent.workpanel.playwright.PlaywrightManager;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
......
...@@ -3,7 +3,6 @@ package pangea.hiagent.tool.impl; ...@@ -3,7 +3,6 @@ package pangea.hiagent.tool.impl;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import pangea.hiagent.workpanel.IWorkPanelDataCollector;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
...@@ -21,7 +20,6 @@ import java.util.List; ...@@ -21,7 +20,6 @@ import java.util.List;
@Component @Component
public class StorageFileAccessTool { public class StorageFileAccessTool {
private final IWorkPanelDataCollector workPanelDataCollector;
// 支持的文件扩展名 // 支持的文件扩展名
private static final List<String> SUPPORTED_EXTENSIONS = Arrays.asList( private static final List<String> SUPPORTED_EXTENSIONS = Arrays.asList(
...@@ -31,10 +29,7 @@ public class StorageFileAccessTool { ...@@ -31,10 +29,7 @@ public class StorageFileAccessTool {
// storage目录路径 // storage目录路径
private static final String STORAGE_DIR = "backend" + File.separator + "storage"; private static final String STORAGE_DIR = "backend" + File.separator + "storage";
public StorageFileAccessTool(IWorkPanelDataCollector workPanelDataCollector) {
this.workPanelDataCollector = workPanelDataCollector;
}
/** /**
* 访问并预览storage目录下的文件 * 访问并预览storage目录下的文件
...@@ -87,9 +82,7 @@ public class StorageFileAccessTool { ...@@ -87,9 +82,7 @@ public class StorageFileAccessTool {
log.info("成功读取文件: {}", fileName); log.info("成功读取文件: {}", fileName);
String result = "已成功在工作面板中预览文件: " + fileName; String result = "已成功在工作面板中预览文件: " + fileName;
// 发送embed事件到工作面板
workPanelDataCollector.recordEmbed(filePath, mimeType, title, content);
return result; return result;
......
package pangea.hiagent.tool.impl;import lombok.extern.slf4j.Slf4j; package pangea.hiagent.tool.impl;import lombok.extern.slf4j.Slf4j;
import pangea.hiagent.workpanel.IWorkPanelDataCollector;
import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
...@@ -18,13 +17,6 @@ import java.net.URLConnection; ...@@ -18,13 +17,6 @@ import java.net.URLConnection;
@Component @Component
public class WebPageAccessTools { public class WebPageAccessTools {
// 通过构造器注入的方式引入IWorkPanelDataCollector依赖
private final IWorkPanelDataCollector workPanelDataCollector;
public WebPageAccessTools(IWorkPanelDataCollector workPanelDataCollector) {
this.workPanelDataCollector = workPanelDataCollector;
}
/** /**
* 根据网站名称访问网页并在工作面板中预览 * 根据网站名称访问网页并在工作面板中预览
* @param siteName 网站名称(如"百度"、"谷歌"等) * @param siteName 网站名称(如"百度"、"谷歌"等)
...@@ -84,9 +76,6 @@ public class WebPageAccessTools { ...@@ -84,9 +76,6 @@ public class WebPageAccessTools {
log.info("成功访问网页: {}", url); log.info("成功访问网页: {}", url);
String result = "已成功在工作面板中预览网页: " + url; String result = "已成功在工作面板中预览网页: " + url;
// 发送embed事件到工作面板
workPanelDataCollector.recordEmbed(url, "text/html", title, webContent);
return result; return result;
} catch (Exception e) { } catch (Exception e) {
return handleError(e, "获取网页内容时发生错误"); return handleError(e, "获取网页内容时发生错误");
......
package pangea.hiagent.workpanel.playwright; package pangea.hiagent.tool.playwright;
import com.microsoft.playwright.Browser; import com.microsoft.playwright.Browser;
import com.microsoft.playwright.BrowserContext; import com.microsoft.playwright.BrowserContext;
......
package pangea.hiagent.workpanel.playwright; package pangea.hiagent.tool.playwright;
import com.microsoft.playwright.*; import com.microsoft.playwright.*;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
......
...@@ -44,7 +44,7 @@ public class AgentController { ...@@ -44,7 +44,7 @@ public class AgentController {
@PostMapping @PostMapping
public ApiResponse<Agent> createAgent(@RequestBody Agent agent) { public ApiResponse<Agent> createAgent(@RequestBody Agent agent) {
try { try {
String userId = UserUtils.getCurrentUserId(); String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) { if (userId == null) {
return ApiResponse.error(4001, "用户未认证"); return ApiResponse.error(4001, "用户未认证");
} }
...@@ -67,7 +67,7 @@ public class AgentController { ...@@ -67,7 +67,7 @@ public class AgentController {
@PostMapping("/with-tools") @PostMapping("/with-tools")
public ApiResponse<Agent> createAgentWithTools(@RequestBody AgentWithToolsDTO agentWithToolsDTO) { public ApiResponse<Agent> createAgentWithTools(@RequestBody AgentWithToolsDTO agentWithToolsDTO) {
try { try {
String userId = UserUtils.getCurrentUserId(); String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) { if (userId == null) {
return ApiResponse.error(4001, "用户未认证"); return ApiResponse.error(4001, "用户未认证");
} }
...@@ -109,7 +109,7 @@ public class AgentController { ...@@ -109,7 +109,7 @@ public class AgentController {
@PreAuthorize("@permissionEvaluator.hasPermission(authentication, #id, 'Agent', 'write')") @PreAuthorize("@permissionEvaluator.hasPermission(authentication, #id, 'Agent', 'write')")
@PutMapping("/{id}") @PutMapping("/{id}")
public ApiResponse<Agent> updateAgent(@PathVariable(name = "id") String id, @RequestBody Agent agent) { public ApiResponse<Agent> updateAgent(@PathVariable(name = "id") String id, @RequestBody Agent agent) {
String userId = UserUtils.getCurrentUserId(); String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) { if (userId == null) {
log.warn("用户未认证,无法更新Agent: {}", id); log.warn("用户未认证,无法更新Agent: {}", id);
return ApiResponse.error(4001, "用户未认证"); return ApiResponse.error(4001, "用户未认证");
...@@ -163,7 +163,7 @@ public class AgentController { ...@@ -163,7 +163,7 @@ public class AgentController {
@PreAuthorize("@permissionEvaluator.hasPermission(authentication, #id, 'Agent', 'write')") @PreAuthorize("@permissionEvaluator.hasPermission(authentication, #id, 'Agent', 'write')")
@PutMapping("/{id}/with-tools") @PutMapping("/{id}/with-tools")
public ApiResponse<Agent> updateAgentWithTools(@PathVariable(name = "id") String id, @RequestBody AgentWithToolsDTO agentWithToolsDTO) { public ApiResponse<Agent> updateAgentWithTools(@PathVariable(name = "id") String id, @RequestBody AgentWithToolsDTO agentWithToolsDTO) {
String userId = UserUtils.getCurrentUserId(); String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) { if (userId == null) {
log.warn("用户未认证,无法更新Agent: {}", id); log.warn("用户未认证,无法更新Agent: {}", id);
return ApiResponse.error(4001, "用户未认证"); return ApiResponse.error(4001, "用户未认证");
...@@ -238,7 +238,7 @@ public class AgentController { ...@@ -238,7 +238,7 @@ public class AgentController {
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public ApiResponse<Void> deleteAgent(@PathVariable(name = "id") String id) { public ApiResponse<Void> deleteAgent(@PathVariable(name = "id") String id) {
try { try {
String userId = UserUtils.getCurrentUserId(); String userId = UserUtils.getCurrentUserIdStatic();
log.info("用户 {} 开始删除Agent: {}", userId, id); log.info("用户 {} 开始删除Agent: {}", userId, id);
agentService.deleteAgent(id); agentService.deleteAgent(id);
log.info("用户 {} 成功删除Agent: {}", userId, id); log.info("用户 {} 成功删除Agent: {}", userId, id);
...@@ -292,7 +292,7 @@ public class AgentController { ...@@ -292,7 +292,7 @@ public class AgentController {
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")
public ApiResponse<java.util.List<Agent>> getUserAgents() { public ApiResponse<java.util.List<Agent>> getUserAgents() {
try { try {
String userId = UserUtils.getCurrentUserId(); String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) { if (userId == null) {
return ApiResponse.error(4001, "用户未认证"); return ApiResponse.error(4001, "用户未认证");
} }
......
...@@ -40,7 +40,7 @@ public class MemoryController { ...@@ -40,7 +40,7 @@ public class MemoryController {
@GetMapping("/dialogue") @GetMapping("/dialogue")
public ApiResponse<List<Map<String, Object>>> getDialogueMemories() { public ApiResponse<List<Map<String, Object>>> getDialogueMemories() {
try { try {
String userId = UserUtils.getCurrentUserId(); String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) { if (userId == null) {
log.warn("用户未认证,无法获取对话记忆列表"); log.warn("用户未认证,无法获取对话记忆列表");
return ApiResponse.error(401, "用户未认证"); return ApiResponse.error(401, "用户未认证");
...@@ -82,7 +82,7 @@ public class MemoryController { ...@@ -82,7 +82,7 @@ public class MemoryController {
@GetMapping("/knowledge") @GetMapping("/knowledge")
public ApiResponse<List<Map<String, Object>>> getKnowledgeMemories() { public ApiResponse<List<Map<String, Object>>> getKnowledgeMemories() {
try { try {
String userId = UserUtils.getCurrentUserId(); String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) { if (userId == null) {
log.warn("用户未认证,无法获取知识记忆列表"); log.warn("用户未认证,无法获取知识记忆列表");
return ApiResponse.error(401, "用户未认证"); return ApiResponse.error(401, "用户未认证");
...@@ -110,7 +110,7 @@ public class MemoryController { ...@@ -110,7 +110,7 @@ public class MemoryController {
@GetMapping("/dialogue/agent/{agentId}") @GetMapping("/dialogue/agent/{agentId}")
public ApiResponse<Map<String, Object>> getDialogueMemoryDetail(@PathVariable String agentId) { public ApiResponse<Map<String, Object>> getDialogueMemoryDetail(@PathVariable String agentId) {
try { try {
String userId = UserUtils.getCurrentUserId(); String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) { if (userId == null) {
log.warn("用户未认证,无法获取对话记忆详情"); log.warn("用户未认证,无法获取对话记忆详情");
return ApiResponse.error(401, "用户未认证"); return ApiResponse.error(401, "用户未认证");
...@@ -190,7 +190,7 @@ public class MemoryController { ...@@ -190,7 +190,7 @@ public class MemoryController {
@DeleteMapping("/dialogue/{sessionId}") @DeleteMapping("/dialogue/{sessionId}")
public ApiResponse<Void> clearDialogueMemory(@PathVariable String sessionId) { public ApiResponse<Void> clearDialogueMemory(@PathVariable String sessionId) {
try { try {
String userId = UserUtils.getCurrentUserId(); String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) { if (userId == null) {
log.warn("用户未认证,无法清空对话记忆"); log.warn("用户未认证,无法清空对话记忆");
return ApiResponse.error(401, "用户未认证"); return ApiResponse.error(401, "用户未认证");
...@@ -223,7 +223,7 @@ public class MemoryController { ...@@ -223,7 +223,7 @@ public class MemoryController {
@DeleteMapping("/knowledge/{id}") @DeleteMapping("/knowledge/{id}")
public ApiResponse<Void> deleteKnowledgeMemory(@PathVariable String id) { public ApiResponse<Void> deleteKnowledgeMemory(@PathVariable String id) {
try { try {
String userId = UserUtils.getCurrentUserId(); String userId = UserUtils.getCurrentUserIdStatic();
if (userId == null) { if (userId == null) {
log.warn("用户未认证,无法删除知识记忆"); log.warn("用户未认证,无法删除知识记忆");
return ApiResponse.error(401, "用户未认证"); return ApiResponse.error(401, "用户未认证");
......
// package pangea.hiagent.web.controller;
// import lombok.extern.slf4j.Slf4j;
// import org.springframework.web.bind.annotation.*;
// import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
// import pangea.hiagent.agent.sse.UserSseService;
// import pangea.hiagent.common.utils.UserUtils;
// import pangea.hiagent.workpanel.event.EventService;
// /**
// * 时间轴事件控制器
// * 提供ReAct过程的实时事件推送功能
// */
// @Slf4j
// @RestController
// @RequestMapping("/api/v1/agent")
// public class TimelineEventController {
// private final UserSseService workPanelSseService;
// public TimelineEventController(UserSseService workPanelSseService, EventService eventService) {
// this.workPanelSseService = workPanelSseService;
// }
// /**
// * 订阅时间轴事件
// * 支持 SSE (Server-Sent Events) 格式的实时事件推送
// *
// * @return SSE emitter
// */
// @GetMapping("/timeline-events")
// public SseEmitter subscribeTimelineEvents() {
// log.info("开始处理时间轴事件订阅请求");
// // 获取当前认证用户ID
// String userId = UserUtils.getCurrentUserId();
// if (userId == null) {
// log.warn("用户未认证,无法创建时间轴事件订阅");
// throw new org.springframework.security.access.AccessDeniedException("用户未认证");
// }
// log.info("开始为用户 {} 创建SSE连接", userId);
// // 创建并注册SSE连接
// return workPanelSseService.createAndRegisterConnection(userId);
// }
// }
\ No newline at end of file
...@@ -258,7 +258,7 @@ public class TimerController { ...@@ -258,7 +258,7 @@ public class TimerController {
* 获取当前认证用户ID * 获取当前认证用户ID
*/ */
private String getCurrentUserId() { private String getCurrentUserId() {
return UserUtils.getCurrentUserId(); return UserUtils.getCurrentUserIdStatic();
} }
/** /**
......
...@@ -39,7 +39,7 @@ public class ToolController { ...@@ -39,7 +39,7 @@ public class ToolController {
* @return 用户ID * @return 用户ID
*/ */
private String getCurrentUserId() { private String getCurrentUserId() {
return UserUtils.getCurrentUserId(); return UserUtils.getCurrentUserIdStatic();
} }
/** /**
......
package pangea.hiagent.web.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
/**
* 嵌入事件数据传输对象
* 用于表示需要嵌入显示的事件(如网页预览等)
*/
@Data
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class EmbedEvent extends WorkPanelEvent {
private static final long serialVersionUID = 1L;
/**
* Embed事件信息 - 嵌入资源URL
*/
private String embedUrl;
/**
* Embed事件信息 - MIME类型
*/
private String embedType;
/**
* Embed事件信息 - 嵌入事件标题
*/
private String embedTitle;
/**
* Embed事件信息 - HTML内容
*/
private String embedHtmlContent;
}
\ No newline at end of file
package pangea.hiagent.web.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
/**
* 日志事件数据传输对象
* 用于表示系统日志事件
*/
@Data
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class LogEvent extends WorkPanelEvent {
private static final long serialVersionUID = 1L;
/**
* 日志内容
*/
private String content;
/**
* 日志级别(info/warn/error/debug)
*/
private String logLevel;
}
\ No newline at end of file
package pangea.hiagent.web.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
/**
* 结果事件数据传输对象
* 用于表示最终结果事件
*/
@Data
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class ResultEvent extends WorkPanelEvent {
private static final long serialVersionUID = 1L;
/**
* 结果内容
*/
private String content;
}
\ No newline at end of file
package pangea.hiagent.web.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
/**
* 思考事件数据传输对象
* 用于表示Agent的思考过程事件
*/
@Data
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class ThoughtEvent extends WorkPanelEvent {
private static final long serialVersionUID = 1L;
/**
* 思考内容
*/
private String content;
/**
* 思考类型(分析、规划、执行等)
*/
private String thinkingType;
}
\ No newline at end of file
package pangea.hiagent.web.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.Map;
/**
* 工具事件数据传输对象
* 用于表示工具调用相关的所有事件(调用、结果、错误)
*/
@Data
@EqualsAndHashCode(callSuper = true)
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
public class ToolEvent extends WorkPanelEvent {
private static final long serialVersionUID = 1L;
/**
* 工具名称
*/
private String toolName;
/**
* 工具执行的方法/action
*/
private String toolAction;
/**
* 工具输入参数
*/
private Map<String, Object> toolInput;
/**
* 工具输出结果
*/
private Object toolOutput;
/**
* 工具执行状态(pending/success/failure/error)
*/
private String toolStatus;
/**
* 执行耗时(毫秒)
*/
private Long executionTime;
/**
* 错误信息
*/
private String errorMessage;
/**
* 错误代码
*/
private String errorCode;
}
\ No newline at end of file
...@@ -145,7 +145,7 @@ public class AgentService { ...@@ -145,7 +145,7 @@ public class AgentService {
} }
// 验证用户权限(确保用户是所有者) // 验证用户权限(确保用户是所有者)
String currentUserId = UserUtils.getCurrentUserId(); String currentUserId = UserUtils.getCurrentUserIdStatic();
if (currentUserId == null) { if (currentUserId == null) {
log.warn("用户未认证,无法更新Agent: {}", agent.getId()); log.warn("用户未认证,无法更新Agent: {}", agent.getId());
throw new BusinessException(ErrorCode.UNAUTHORIZED.getCode(), "用户未认证"); throw new BusinessException(ErrorCode.UNAUTHORIZED.getCode(), "用户未认证");
......
...@@ -2,9 +2,10 @@ package pangea.hiagent.websocket; ...@@ -2,9 +2,10 @@ package pangea.hiagent.websocket;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import pangea.hiagent.tool.playwright.PlaywrightManager;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.socket.*; import org.springframework.web.socket.*;
import pangea.hiagent.workpanel.playwright.PlaywrightManager;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
......
...@@ -3,8 +3,8 @@ package pangea.hiagent.websocket; ...@@ -3,8 +3,8 @@ package pangea.hiagent.websocket;
import com.microsoft.playwright.*; import com.microsoft.playwright.*;
import com.microsoft.playwright.options.LoadState; import com.microsoft.playwright.options.LoadState;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import pangea.hiagent.workpanel.playwright.PlaywrightManager;
import pangea.hiagent.common.utils.AsyncUserContextDecorator; import pangea.hiagent.common.utils.AsyncUserContextDecorator;
import pangea.hiagent.tool.playwright.PlaywrightManager;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
......
...@@ -2,8 +2,9 @@ package pangea.hiagent.websocket; ...@@ -2,8 +2,9 @@ package pangea.hiagent.websocket;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.*; import org.springframework.web.socket.*;
import pangea.hiagent.workpanel.playwright.PlaywrightManager;
import pangea.hiagent.common.utils.UserUtils; import pangea.hiagent.common.utils.UserUtils;
import pangea.hiagent.tool.playwright.PlaywrightManager;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
...@@ -89,7 +90,7 @@ public class WebSocketConnectionManager { ...@@ -89,7 +90,7 @@ public class WebSocketConnectionManager {
String userId = (String) session.getAttributes().get("userId"); String userId = (String) session.getAttributes().get("userId");
if (userId == null || userId.isEmpty()) { if (userId == null || userId.isEmpty()) {
// 如果没有有效的用户ID,尝试从SecurityContext获取 // 如果没有有效的用户ID,尝试从SecurityContext获取
userId = UserUtils.getCurrentUserId(); userId = UserUtils.getCurrentUserIdStatic();
if (userId == null || userId.isEmpty()) { if (userId == null || userId.isEmpty()) {
// 如果仍然无法获取用户ID,使用默认值 // 如果仍然无法获取用户ID,使用默认值
userId = "unknown-user"; userId = "unknown-user";
......
This diff is collapsed.
This diff is collapsed.
...@@ -17,10 +17,9 @@ ...@@ -17,10 +17,9 @@
"dompurify": "^3.3.1", "dompurify": "^3.3.1",
"element-plus": "^2.4.0", "element-plus": "^2.4.0",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"lodash-es": "^4.17.21",
"marked": "^17.0.1", "marked": "^17.0.1",
"pako": "^2.1.0", "pako": "^2.1.0",
"pangea-ui": "1.0.1-beta.2",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"snabbdom": "^3.6.3", "snabbdom": "^3.6.3",
"vue": "^3.4.0", "vue": "^3.4.0",
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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