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
...@@ -5,24 +5,16 @@ import org.springframework.ai.chat.client.ChatClient; ...@@ -5,24 +5,16 @@ import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.*; import org.springframework.ai.chat.messages.*;
import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import pangea.hiagent.agent.service.ErrorHandlerService; import pangea.hiagent.agent.service.ErrorHandlerService;
import pangea.hiagent.agent.service.SseTokenEmitter;
import pangea.hiagent.agent.service.TokenConsumerWithCompletion; import pangea.hiagent.agent.service.TokenConsumerWithCompletion;
import pangea.hiagent.agent.service.UserSseService;
import pangea.hiagent.memory.MemoryService; import pangea.hiagent.memory.MemoryService;
import pangea.hiagent.model.Agent; import pangea.hiagent.model.Agent;
import pangea.hiagent.model.UserToken;
import pangea.hiagent.tool.AgentToolManager; import pangea.hiagent.tool.AgentToolManager;
import pangea.hiagent.tool.impl.DateTimeTools;
import pangea.hiagent.common.utils.UserUtils; import pangea.hiagent.common.utils.UserUtils;
import pangea.hiagent.web.service.UserTokenService;
import java.util.List; import java.util.List;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**
...@@ -32,116 +24,26 @@ import java.util.function.Consumer; ...@@ -32,116 +24,26 @@ import java.util.function.Consumer;
@Service @Service
public class DefaultReactExecutor implements ReactExecutor { public class DefaultReactExecutor implements ReactExecutor {
private static final String DEFAULT_SYSTEM_PROMPT = @Value("${hiagent.react.system-prompt}")
"You are a powerful professional AI assistant powered by the enhanced ReAct (Reasoning + Acting) iterative framework, specialized for Spring AI tool orchestration. Your core mission is to solve complex, multi-step user queries with high accuracy by following the upgraded rules. The TOP PRIORITY principle is: ALWAYS CALL TOOLS FIRST, and answer questions EXCLUSIVELY based on tool execution results. You have full authority to intelligently select, combine, and serially invoke multiple tools, and iterate reasoning until a complete and satisfactory answer is obtained.\n\n" + private String defaultSystemPrompt;
"=== CORE UPGRADED RULE - NON-NEGOTIABLE (Tool-First Priority Highlighted) ===\n\n" +
"1. Tool-First Mandate: For any query that requires factual verification, data calculation, information extraction, content analysis, or scenario-based processing, YOU MUST CALL RELEVANT TOOLS FIRST. Never answer directly relying on internal knowledge without tool invocation, except for extremely simple common-sense questions (e.g., \"What is 1+1?\").\n" +
"2. Answer Based on Tool Results Only: All conclusions, data, and insights in the final answer must be strictly derived from the real execution results of Spring AI tools. Never fabricate any data, assumptions, or inferences that are not supported by tool outputs.\n" +
"3. Serial Multi-Tool Invocation Supported: You can invoke multiple tools in serial order in one Action phase. By default, the output of the previous tool is the directly valid input of the next tool (first-class support for tool chaining).\n" +
"4. Iterative ReAct Closed-Loop: The ReAct thinking process is a cyclic loop. After each Observation phase, you can return to the Thought phase to re-analyze, reselect tools, and re-execute until the answer is complete/satisfactory.\n" +
"5. Mandatory Tool Synergy: Complex queries must use multi-tool combinations. A single tool can only solve simple problems; never rely on a single tool for complex tasks.\n" +
"6. Strict Compliance with Spring AI Mechanism: All tool calls are executed automatically by the Spring AI framework. You only need to make optimal tool selection and sequence planning.\n\n\n" +
"=== ENHANCED TOOL SYNERGY & ORCHESTRATION STRATEGY ===\n\n" +
"You have access to a full set of specialized Spring AI tools and must create value through intelligent tool collocation, with tool-first logic throughout:\n\n" +
"- Serial Chaining (Highest Priority): The output of one tool directly feeds into the input of another, forming a closed tool call chain (e.g., File Reader → Text Processor → Calculator → File Writer → Chart Generator).\n\n" +
"- Parallel Combination: Call multiple independent tools simultaneously to collect multi-dimensional data, then merge results for comprehensive analysis.\n\n" +
"- Preprocessing & Postprocessing: Use formatting tools to clean raw data before core tool execution; use conversion tools to optimize result presentation afterward.\n\n" +
"- Layered Enrichment: Combine extraction, analysis, and calculation tools to gain in-depth insights instead of superficial data.\n\n" +
"- Priority Matching: Select lightweight tools first for simple sub-tasks; use heavyweight tools only for complex ones (resource efficiency).\n\n" +
"- Fault Tolerance Fallback: If a selected tool is unavailable/returns invalid results, immediately invoke an alternative tool with the same function to re-execute the sub-task.\n\n\n" +
"=== Typical High-Value Tool Synergy Examples ===\n\n" +
"1. Web Content Extractor → Text Parser & Cleaner → NLP Analyzer → Statistical Calculator → Result Formatter → File Saver\n\n" +
"2. Current DateTime Tool → Date Formatter → Data Filter → Time Series Analyzer → Visualization Tool\n\n" +
"3. Document Reader → Table Extractor → Data Validator → Formula Calculator → Report Generator\n\n" +
"4. Input Parameter Parser → Multiple Business Tools (Serial) → Result Aggregator → Answer Polisher\n\n\n" +
"=== UPGRADED ITERATIVE ReAct THINKING PROCESS (Tool-First Oriented) ===\n\n" +
"This is a cyclic, repeatable process for EVERY query, with tool-first logic as the core. Execute in order and loop infinitely until the answer meets completeness requirements.\n\n" +
"▶ Cycle Trigger Rule: After Step 4 (Observation), if results are incomplete/insufficient/need optimization → Return to Step 1 (Thought) to re-analyze and re-execute.\n\n" +
"▶ Cycle Termination Rule: After Step 4 (Observation), if results are complete/accurate/satisfactory → Enter Step 5 (Final Answer) directly.\n\n\n" +
"Step 1 - THOUGHT (Tool-First Iterative Reasoning & Planning): Deeply analyze the user's core query and current context with tool-first logic\n" +
" - Break down the main problem into hierarchical sub-tasks (primary → secondary → fine-grained).\n" +
" - Tool-First Matching: For each sub-task, FIRST identify relevant tools (never consider direct answering first). Mark alternative tools for fault tolerance.\n" +
" - Confirm Tool Synergy Feasibility: Judge serial/parallel combination of multi-tools and define the exact invocation sequence.\n" +
" - Iterative Scenario Adjustment: Re-analyze the gap between current tool results and expected answers, adjust tool selection/sequence.\n" +
" - Verify Preconditions: Ensure input format and parameter validity for tool invocation are met.\n\n\n" +
"Step 2 - ACTION (Multi-Tool Serial/Parallel Execution): Execute the planned tool chain with clear purpose, adhering to tool-first principle\n" +
" - Call tools in the pre-defined serial/parallel order based on Thought phase analysis.\n" +
" - Support multiple consecutive tool calls in one Action phase (serial chain) for Spring AI, no limit on the number of tools.\n" +
" - Wait for ALL tool execution results (serial: one by one / parallel: all at once) before proceeding; never jump early.\n" +
" - Fault Tolerance Execution: If a tool returns invalid/empty results, immediately invoke the pre-marked alternative tool and re-execute the sub-task.\n\n\n" +
"Step 3 - OBSERVATION (Tool Result-Centric Analysis & Validation): Comprehensively interpret all tool execution results\n" +
" - Examine data/results from each tool in detail, cross-verify accuracy, completeness, and logical consistency.\n" +
" - Extract key information, patterns, and insights EXCLUSIVELY from combined tool results.\n" +
" - Judge Completion Status: Confirm if current results cover all sub-tasks and meet the user's core needs.\n" +
" - Identify Gaps: Mark missing information/unsolved sub-tasks that require further tool invocation.\n" +
" - Evaluate Tool Synergy Effect: Confirm if the tool chain provides deeper insights than single-tool usage.\n\n\n" +
"Step 4 - ITERATION DECISION: Critical judgment for ReAct cycle\n" +
" - ✅ TERMINATE CYCLE: If observation results are complete, accurate, sufficient, and fully meet the user's query → Proceed to Step 5.\n\n" +
" ♻️ RESTART CYCLE: If observation results are incomplete/insufficient/have missing information → Return to Step 1.\n\n\n" +
"Step 5 - FINAL ANSWER (Tool Result-Synthesized Response): Generate the ultimate answer based solely on tool results\n" +
" - Synthesize all valid tool results (from iterative cycles) into a coherent, logical, and complete answer.\n" +
" - Present information in clear, easy-to-understand natural language, distinguishing key insights from basic information.\n" +
" - Explicitly explain tool synergy logic (e.g., \"Tool A processed raw data for Tool B, enabling accurate calculation by Tool C\").\n" +
" - Provide actionable conclusions, recommendations, or follow-up suggestions based on integrated tool results.\n" +
" - Keep the answer conversational and business-oriented; remove redundant technical tool details.\n\n\n" +
"=== STANDARDIZED RESPONSE FORMAT ===\n\n" +
"Strictly follow this fixed structure for all responses to ensure correct parsing by Spring AI:\n\n\n" +
"1. Thought: Detailed explanation of problem analysis, sub-task breakdown, tool-first selection strategy, and invocation sequence\n" +
" - Identified Sub-Problems: List all primary/secondary sub-tasks clearly.\n" +
" - Tool-First Matching: Tools assigned to each sub-task + alternative tools (if any).\n" +
" - Execution Sequence: Exact serial/parallel order of multi-tool invocation and its optimality.\n" +
" - Iteration Note: If re-analyzing (loop), explain gaps in previous results and tool selection adjustments.\n\n\n" +
"2. Action: Clear description of all tool calls in this phase (serial number + tool name + core purpose)\n" +
" - Tool_Call: 1.[Tool Name] → Purpose: [Exact business objective and core value]\n" +
" - Tool_Call: 2.[Tool Name] → Purpose: [Complement the previous tool, use its output as input]\n" +
" - Tool_Call: N.[Tool Name] → Purpose: [Final enrichment/validation/formatting of the result chain]\n" +
" - (Fallback) If Tool X Unavailable: Use [Alternative Tool Name] → Purpose: [Same objective as Tool X]\n\n\n" +
"3. Observation: Comprehensive interpretation of all tool execution results\n" +
" - Results from each individual tool (key data, no redundant details).\n" +
" - Logical relationship between multiple tool results (how they connect and complement).\n" +
" - Core patterns/insights from the tool chain.\n" +
" - Completion Status: Whether results cover all sub-tasks and missing information (if any).\n\n\n" +
"4. Iteration_Decision: Explicit single choice\n" +
" - Option 1: Terminate Cycle → Proceed to Final Answer (complete results)\n" +
" - Option 2: Restart Cycle → Re-enter Thought phase (incomplete results)\n\n\n" +
"5. Final_Answer: Polished, complete, and user-friendly natural language solution\n" +
" - Direct answer to the original query, with core conclusions first.\n" +
" - Highlight key insights from tool synergy/iterative reasoning.\n" +
" - Provide actionable follow-up suggestions.\n" +
" - Conversational tone; no technical jargon about tools/frameworks.\n\n\n\n" +
"=== CRITICAL HARD RULES (Tool-First as Core) ===\n\n" +
"1. Tool-First is Non-Negotiable: For non-trivial queries, call tools first. Never answer directly with internal knowledge unless it's extremely simple common sense.\n" +
"2. Tool Results are the Sole Basis: All answers must rely on real Spring AI tool execution results. Never fabricate data/results.\n" +
"3. Mandatory Multi-Tool Synergy: Complex queries must use tool combinations. Never rely on a single tool for complex tasks.\n" +
"4. Full Support for Serial Invocation: One Action phase can call N tools in sequence, with prior output as next input.\n" +
"5. Iterative ReAct is Mandatory: Never stop at one-time execution; loop until the answer is complete and satisfactory.\n" +
"6. Explicit Tool Strategy: All tool selection, sequence planning, and fallback options must be clearly stated in Thought.\n" +
"7. Unavailable Tool Handling: Immediately use an alternative tool if the selected one is unavailable; do not suspend execution.\n" +
"8. User Experience Priority: The Final Answer must be conversational and business-focused, hiding technical tool details.\n" +
"9. Spring AI Compliance: All tool calls follow the framework's automatic execution rules; no custom execution logic.";
private final List<ReactCallback> reactCallbacks = new ArrayList<>(); private final List<ReactCallback> reactCallbacks = new ArrayList<>();
private final AtomicInteger stepCounter = new AtomicInteger(0);
private final EventSplitter eventSplitter;
@Autowired
private DateTimeTools dateTimeTools;
@Autowired
private MemoryService memoryService; private MemoryService memoryService;
@Autowired
private ErrorHandlerService errorHandlerService;
@Autowired private ErrorHandlerService errorHandlerService;
private UserTokenService userTokenService;
private final AgentToolManager agentToolManager; private final AgentToolManager agentToolManager;
@Autowired
private UserSseService userSseService; public DefaultReactExecutor(EventSplitter eventSplitter, AgentToolManager agentToolManager ,
MemoryService memoryService, ErrorHandlerService errorHandlerService) {
public DefaultReactExecutor(AgentToolManager agentToolManager) { this.eventSplitter = eventSplitter;
this.agentToolManager = agentToolManager; this.agentToolManager = agentToolManager;
this.memoryService = memoryService;
this.errorHandlerService = errorHandlerService;
} }
@Override @Override
...@@ -154,7 +56,7 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -154,7 +56,7 @@ public class DefaultReactExecutor implements ReactExecutor {
@Override @Override
public String execute(ChatClient chatClient, String userInput, List<Object> tools, Agent agent) { public String execute(ChatClient chatClient, String userInput, List<Object> tools, Agent agent) {
// 调用带用户ID的方法,首先尝试获取当前用户ID // 调用带用户ID的方法,首先尝试获取当前用户ID
String userId = UserUtils.getCurrentUserId(); String userId = UserUtils.getCurrentUserIdStatic();
return execute(chatClient, userInput, tools, agent, userId); return execute(chatClient, userInput, tools, agent, userId);
} }
...@@ -162,14 +64,10 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -162,14 +64,10 @@ public class DefaultReactExecutor implements ReactExecutor {
public String execute(ChatClient chatClient, String userInput, List<Object> tools, Agent agent, String userId) { public String execute(ChatClient chatClient, String userInput, List<Object> tools, Agent agent, String userId) {
log.info("开始执行ReAct流程,用户输入: {}", userInput); log.info("开始执行ReAct流程,用户输入: {}", userInput);
stepCounter.set(0);
List<Object> agentTools = getAgentTools(agent); List<Object> agentTools = getAgentTools(agent);
try { try {
// triggerThinkStep("开始处理用户请求: " + userInput); Prompt prompt = buildPromptWithHistory(defaultSystemPrompt, userInput, agent, userId);
Prompt prompt = buildPromptWithHistory(DEFAULT_SYSTEM_PROMPT, userInput, agent, userId);
ChatResponse response = chatClient.prompt(prompt) ChatResponse response = chatClient.prompt(prompt)
.tools(agentTools.toArray()) .tools(agentTools.toArray())
...@@ -178,12 +76,8 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -178,12 +76,8 @@ public class DefaultReactExecutor implements ReactExecutor {
String responseText = response.getResult().getOutput().getText(); String responseText = response.getResult().getOutput().getText();
// triggerObservationStep(responseText);
log.info("最终答案: {}", responseText); log.info("最终答案: {}", responseText);
// triggerFinalAnswerStep(responseText);
// 保存助手回复到内存,使用提供的用户ID // 保存助手回复到内存,使用提供的用户ID
saveAssistantResponseToMemory(agent, responseText, userId); saveAssistantResponseToMemory(agent, responseText, userId);
...@@ -201,6 +95,7 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -201,6 +95,7 @@ public class DefaultReactExecutor implements ReactExecutor {
* @return 错误处理结果 * @return 错误处理结果
*/ */
private String handleReActError(Exception e) { private String handleReActError(Exception e) {
log.error("ReAct执行过程中发生错误", e);
return errorHandlerService.handleSyncError(e, "处理ReAct请求时发生错误"); return errorHandlerService.handleSyncError(e, "处理ReAct请求时发生错误");
} }
...@@ -222,7 +117,7 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -222,7 +117,7 @@ public class DefaultReactExecutor implements ReactExecutor {
try { try {
// 如果没有提供用户ID,则尝试获取当前用户ID // 如果没有提供用户ID,则尝试获取当前用户ID
if (userId == null) { if (userId == null) {
userId = UserUtils.getCurrentUserId(); userId = UserUtils.getCurrentUserIdStatic();
} }
String sessionId = memoryService.generateSessionId(agent, userId); String sessionId = memoryService.generateSessionId(agent, userId);
...@@ -247,7 +142,7 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -247,7 +142,7 @@ public class DefaultReactExecutor implements ReactExecutor {
@Override @Override
public void executeStream(ChatClient chatClient, String userInput, List<Object> tools, Consumer<String> tokenConsumer, Agent agent) { public void executeStream(ChatClient chatClient, String userInput, List<Object> tools, Consumer<String> tokenConsumer, Agent agent) {
// 调用带用户ID的方法,但首先尝试获取当前用户ID // 调用带用户ID的方法,但首先尝试获取当前用户ID
String userId = UserUtils.getCurrentUserId(); String userId = UserUtils.getCurrentUserIdStatic();
executeStream(chatClient, userInput, tools, tokenConsumer, agent, userId); executeStream(chatClient, userInput, tools, tokenConsumer, agent, userId);
} }
...@@ -255,29 +150,21 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -255,29 +150,21 @@ public class DefaultReactExecutor implements ReactExecutor {
public void executeStream(ChatClient chatClient, String userInput, List<Object> tools, Consumer<String> tokenConsumer, Agent agent, String userId) { public void executeStream(ChatClient chatClient, String userInput, List<Object> tools, Consumer<String> tokenConsumer, Agent agent, String userId) {
log.info("使用stream()方法处理ReAct流程,支持真正的流式输出"); log.info("使用stream()方法处理ReAct流程,支持真正的流式输出");
stepCounter.set(0);
List<Object> agentTools = getAgentTools(agent); List<Object> agentTools = getAgentTools(agent);
StringBuilder fullResponse = new StringBuilder(); StringBuilder fullResponse = new StringBuilder();
try { try {
// triggerThinkStep("开始处理用户请求: " + userInput); Prompt prompt = buildPromptWithHistory(defaultSystemPrompt, userInput, agent, userId);
SseTokenEmitter sseTokenEmitter = (SseTokenEmitter)tokenConsumer;
String emitterId = sseTokenEmitter.getEmitterId();
Prompt prompt = buildPromptWithHistory(DEFAULT_SYSTEM_PROMPT, userInput, agent, userId);
UserToken userToken = userTokenService.getUserToken(sseTokenEmitter.getUserId(),"pangea");
chatClient.prompt(prompt) chatClient.prompt(prompt)
.tools(agentTools.toArray()) .tools(agentTools.toArray())
.toolContext(Map.of("emitterId",emitterId))
.stream() .stream()
.chatResponse() .chatResponse()
.subscribe( .subscribe(
chatResponse -> handleTokenResponse(chatResponse, tokenConsumer, fullResponse), chatResponse -> handleTokenResponse(chatResponse, tokenConsumer, fullResponse),
throwable -> handleStreamError(throwable, tokenConsumer,emitterId), throwable -> handleStreamError(throwable, tokenConsumer),
() -> handleStreamCompletion(tokenConsumer, fullResponse, agent, userId,emitterId) () -> handleStreamCompletion(tokenConsumer, fullResponse, agent, userId)
); );
} catch (Exception e) { } catch (Exception e) {
...@@ -300,20 +187,15 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -300,20 +187,15 @@ public class DefaultReactExecutor implements ReactExecutor {
if (isValidToken(token)) { if (isValidToken(token)) {
fullResponse.append(token); fullResponse.append(token);
// analyzeAndRecordToolEvents(token, fullResponse.toString());
if (tokenConsumer != null) { if (tokenConsumer != null) {
tokenConsumer.accept(token); tokenConsumer.accept(token);
} }
// tokenTextSegmenter.inputChar(token);
// tokenTextSegmenter.finishInput();
// 改进:在流式处理过程中实时解析关键词 eventSplitter.feedToken(token);
// processTokenForStepsWithFullResponse(token, fullResponse.toString());
} }
} catch (Exception e) { } catch (Exception e) {
log.error("处理token时发生错误", e); log.error("处理token时发生错误", e);
errorHandlerService.handleReactFlowError(e, tokenConsumer);
} }
} }
...@@ -325,19 +207,12 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -325,19 +207,12 @@ public class DefaultReactExecutor implements ReactExecutor {
* @param agent 智能体对象 * @param agent 智能体对象
* @param userId 用户ID * @param userId 用户ID
*/ */
private void handleStreamCompletion(Consumer<String> tokenConsumer, StringBuilder fullResponse, Agent agent, String userId,String emitterId) { private void handleStreamCompletion(Consumer<String> tokenConsumer, StringBuilder fullResponse, Agent agent, String userId) {
try { try {
log.info("流式处理完成"); log.info("流式处理完成");
// 检查是否已经处理了Final Answer,如果没有,则将整个响应作为最终答案
String responseStr = fullResponse.toString(); String responseStr = fullResponse.toString();
if (!hasFinalAnswerBeenTriggered(responseStr)) {
// triggerFinalAnswerStep(responseStr);
}
saveAssistantResponseToMemory(agent, responseStr, userId); saveAssistantResponseToMemory(agent, responseStr, userId);
log.info("complete, remove emitterId {}",emitterId);
userSseService.removeEmitter(emitterId);
sendCompletionEvent(tokenConsumer, responseStr); sendCompletionEvent(tokenConsumer, responseStr);
} catch (Exception e) { } catch (Exception e) {
log.error("处理流式完成回调时发生错误", e); log.error("处理流式完成回调时发生错误", e);
...@@ -345,22 +220,6 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -345,22 +220,6 @@ public class DefaultReactExecutor implements ReactExecutor {
} }
} }
/**
* 检查是否已经触发了Final Answer步骤
*
* @param fullResponse 完整响应内容
* @return 如果已经触发了Final Answer则返回true,否则返回false
*/
private boolean hasFinalAnswerBeenTriggered(String fullResponse) {
String[] finalAnswerPatterns = {"Final Answer:", "final answer:", "FINAL ANSWER:", "Final_Answer:", "final_answer:", "FINAL_ANSWER:", "最终答案:"};
for (String pattern : finalAnswerPatterns) {
if (fullResponse.toLowerCase().contains(pattern.toLowerCase())) {
return true;
}
}
return false;
}
/** /**
* 将助手的回复保存到内存中 * 将助手的回复保存到内存中
* *
...@@ -390,11 +249,9 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -390,11 +249,9 @@ public class DefaultReactExecutor implements ReactExecutor {
try { try {
String errorId = errorHandlerService.generateErrorId(); String errorId = errorHandlerService.generateErrorId();
String fullErrorMessage = errorHandlerService.buildFullErrorMessage("处理完成时发生错误", e, errorId, "ReAct"); String fullErrorMessage = errorHandlerService.buildFullErrorMessage("处理完成时发生错误", e, errorId, "ReAct");
try { ((TokenConsumerWithCompletion) tokenConsumer).onComplete("[" + errorId + "] " + fullErrorMessage);
((TokenConsumerWithCompletion) tokenConsumer).onComplete("[" + errorId + "] " + fullErrorMessage); } catch (NoClassDefFoundError ex) {
} catch (NoClassDefFoundError ex) { log.error("TokenConsumerWithCompletion依赖类未找到,跳过完成回调: {}", ex.getMessage());
log.error("TokenConsumerWithCompletion依赖类未找到,跳过完成回调: {}", ex.getMessage());
}
} catch (Exception ex) { } catch (Exception ex) {
log.error("调用onComplete时发生错误", ex); log.error("调用onComplete时发生错误", ex);
} }
...@@ -417,9 +274,7 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -417,9 +274,7 @@ public class DefaultReactExecutor implements ReactExecutor {
* @param throwable 异常对象 * @param throwable 异常对象
* @param tokenConsumer token消费者 * @param tokenConsumer token消费者
*/ */
private void handleStreamError(Throwable throwable, Consumer<String> tokenConsumer,String emitterId) { private void handleStreamError(Throwable throwable, Consumer<String> tokenConsumer) {
log.info("error,remove emitterId:{}", emitterId);
userSseService.removeEmitter(emitterId);
errorHandlerService.handleStreamError(throwable, tokenConsumer, "ReAct流式处理"); errorHandlerService.handleStreamError(throwable, tokenConsumer, "ReAct流式处理");
} }
...@@ -462,25 +317,17 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -462,25 +317,17 @@ public class DefaultReactExecutor implements ReactExecutor {
* @return 智能体可用的工具列表 * @return 智能体可用的工具列表
*/ */
private List<Object> getAgentTools(Agent agent) { private List<Object> getAgentTools(Agent agent) {
if (agent == null) { List<Object> tools = new ArrayList<>();
List<Object> defaultTools = new ArrayList<>();
defaultTools.add(dateTimeTools);
return defaultTools;
}
try { if (agent != null) {
List<Object> tools = agentToolManager.getAvailableToolInstances(agent); try {
tools = agentToolManager.getAvailableToolInstances(agent);
if (dateTimeTools != null && !tools.contains(dateTimeTools)) { } catch (Exception e) {
tools.add(dateTimeTools); log.error("获取工具实例时发生错误: {}", e.getMessage());
// 发生异常时,tools 保持为空列表
} }
return tools;
} catch (Exception e) {
log.error("获取工具实例时发生错误: {}", e.getMessage());
List<Object> fallbackTools = new ArrayList<>();
fallbackTools.add(dateTimeTools);
return fallbackTools;
} }
return tools;
} }
} }
\ 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
...@@ -13,11 +13,8 @@ import pangea.hiagent.web.dto.ChatRequest; ...@@ -13,11 +13,8 @@ import pangea.hiagent.web.dto.ChatRequest;
import pangea.hiagent.model.Agent; import pangea.hiagent.model.Agent;
import pangea.hiagent.tool.AgentToolManager; import pangea.hiagent.tool.AgentToolManager;
import pangea.hiagent.web.dto.AgentRequest; import pangea.hiagent.web.dto.AgentRequest;
import pangea.hiagent.workpanel.event.EventService;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import java.util.UUID;
/** /**
* Agent 对话服务 * Agent 对话服务
* 职责:协调整个AI对话流程,作为流式处理的统一入口和协调者 * 职责:协调整个AI对话流程,作为流式处理的统一入口和协调者
...@@ -29,81 +26,79 @@ public class AgentChatService { ...@@ -29,81 +26,79 @@ public class AgentChatService {
private final ErrorHandlerService errorHandlerService; private final ErrorHandlerService errorHandlerService;
private final AgentProcessorFactory agentProcessorFactory; private final AgentProcessorFactory agentProcessorFactory;
private final AgentToolManager agentToolManager; private final AgentToolManager agentToolManager;
private final UserSseService userSseSerivce; private final UserSseService userSseService;
private final pangea.hiagent.web.service.AgentService agentService; private final pangea.hiagent.web.service.AgentService agentService;
private final SseTokenEmitter sseTokenEmitter;
public AgentChatService( public AgentChatService(
EventService eventService,
ErrorHandlerService errorHandlerService, ErrorHandlerService errorHandlerService,
AgentProcessorFactory agentProcessorFactory, AgentProcessorFactory agentProcessorFactory,
AgentToolManager agentToolManager, AgentToolManager agentToolManager,
UserSseService workPanelSseService, UserSseService userSseService,
pangea.hiagent.web.service.AgentService agentService, pangea.hiagent.web.service.AgentService agentService) {
SseTokenEmitter sseTokenEmitter) {
this.errorHandlerService = errorHandlerService; this.errorHandlerService = errorHandlerService;
this.agentProcessorFactory = agentProcessorFactory; this.agentProcessorFactory = agentProcessorFactory;
this.agentToolManager = agentToolManager; this.agentToolManager = agentToolManager;
this.userSseSerivce = workPanelSseService; this.userSseService = userSseService;
this.agentService = agentService; this.agentService = agentService;
this.sseTokenEmitter = sseTokenEmitter;
} }
// /** // /**
// * 处理同步对话请求的统一入口 // * 处理同步对话请求的统一入口
// * @param agent Agent对象 // * @param agent Agent对象
// * @param request 请求对象 // * @param request 请求对象
// * @param userId 用户ID // * @param userId 用户ID
// * @return 处理结果 // * @return 处理结果
// */ // */
// public String handleChatSync(Agent agent, AgentRequest request, String userId) { // public String handleChatSync(Agent agent, AgentRequest request, String
// log.info("开始处理同步对话请求,AgentId: {}, 用户消息: {}", agent.getId(), request.getUserMessage()); // userId) {
// // log.info("开始处理同步对话请求,AgentId: {}, 用户消息: {}", agent.getId(),
// try { // request.getUserMessage());
// // 获取处理器 //
// AgentProcessor processor = agentProcessorFactory.getProcessor(agent); // try {
// if (processor == null) { // // 获取处理器
// log.error("无法获取Agent处理器"); // AgentProcessor processor = agentProcessorFactory.getProcessor(agent);
// return "[错误] 无法获取Agent处理器"; // if (processor == null) {
// } // log.error("无法获取Agent处理器");
// // return "[错误] 无法获取Agent处理器";
// // 处理请求 // }
// return processor.processRequest(agent, request, userId); //
// } catch (Exception e) { // // 处理请求
// log.error("处理普通Agent请求时发生错误", e); // return processor.processRequest(agent, request, userId);
// return "[错误] 处理请求时发生错误: " + e.getMessage(); // } catch (Exception e) {
// } // log.error("处理普通Agent请求时发生错误", e);
// return "[错误] 处理请求时发生错误: " + e.getMessage();
// }
// } // }
/** /**
* 处理流式对话请求的统一入口 * 处理流式对话请求的统一入口
* *
* @param agentId Agent ID * @param agentId Agent ID
* @param chatRequest 对话请求 * @param chatRequest 对话请求
* @param response HTTP响应 * @param response HTTP响应
* @return SSE emitter * @return SSE emitter
*/ */
public SseEmitter handleChatStream(String agentId, ChatRequest chatRequest, HttpServletResponse response) { public SseEmitter handleChatStream(String agentId, ChatRequest chatRequest, HttpServletResponse response) {
log.info("开始处理流式对话请求,AgentId: {}, 用户消息: {}", agentId, chatRequest.getMessage()); log.info("开始处理流式对话请求,AgentId: {}, 用户消息: {}", agentId, chatRequest.getMessage());
// 尝试获取当前用户ID,优先从SecurityContext获取,其次从请求中解析JWT // 尝试获取当前用户ID,优先从SecurityContext获取,其次从请求中解析JWT
String userId = UserUtils.getCurrentUserId(); String userId = UserUtils.getCurrentUserIdStatic();
// 如果在主线程中未能获取到用户ID,尝试在异步环境中获取 // 如果在主线程中未能获取到用户ID,再次尝试获取(支持异步环境)
if (userId == null) { if (userId == null) {
userId = UserUtils.getCurrentUserIdInAsync(); userId = UserUtils.getCurrentUserIdStatic();
} }
if (userId == null) { if (userId == null) {
log.error("用户未认证"); log.error("用户未认证");
SseEmitter emitter = userSseSerivce.createEmitter(); SseEmitter emitter = userSseService.createEmitter();
// 检查响应是否已经提交 // 检查响应是否已经提交
if (!response.isCommitted()) { if (!response.isCommitted()) {
errorHandlerService.handleChatError(emitter, "用户未认证,请重新登录"); errorHandlerService.handleChatError(emitter, "用户未认证,请重新登录");
} else { } else {
log.warn("响应已提交,无法发送用户未认证错误信息"); log.warn("响应已提交,无法发送用户未认证错误信息");
// 检查emitter是否已经完成,避免重复关闭 // 检查emitter是否已经完成,避免重复关闭
if (!userSseSerivce.isEmitterCompleted(emitter)) { if (!userSseService.isEmitterCompleted(emitter)) {
emitter.complete(); emitter.complete();
} }
} }
...@@ -114,14 +109,14 @@ public class AgentChatService { ...@@ -114,14 +109,14 @@ public class AgentChatService {
Agent agent = agentService.getAgent(agentId); Agent agent = agentService.getAgent(agentId);
if (agent == null) { if (agent == null) {
log.warn("Agent不存在: {}", agentId); log.warn("Agent不存在: {}", agentId);
SseEmitter emitter = userSseSerivce.createEmitter(); SseEmitter emitter = userSseService.createEmitter();
// 检查响应是否已经提交 // 检查响应是否已经提交
if (!response.isCommitted()) { if (!response.isCommitted()) {
errorHandlerService.handleChatError(emitter, "Agent不存在"); errorHandlerService.handleChatError(emitter, "Agent不存在");
} else { } else {
log.warn("响应已提交,无法发送Agent不存在错误信息"); log.warn("响应已提交,无法发送Agent不存在错误信息");
// 检查emitter是否已经完成,避免重复关闭 // 检查emitter是否已经完成,避免重复关闭
if (!userSseSerivce.isEmitterCompleted(emitter)) { if (!userSseService.isEmitterCompleted(emitter)) {
emitter.complete(); emitter.complete();
} }
} }
...@@ -129,34 +124,32 @@ public class AgentChatService { ...@@ -129,34 +124,32 @@ public class AgentChatService {
} }
// 创建 SSE emitter // 创建 SSE emitter
SseEmitter emitter = userSseSerivce.createEmitter(); SseEmitter emitter = userSseService.createAndRegisterConnection(userId);
String emitterId = UUID.randomUUID().toString();
log.info("emitterId: {}", emitterId);
userSseSerivce.registerEmitter(emitterId, emitter);
// 异步处理对话,避免阻塞HTTP连接 // 异步处理对话,避免阻塞HTTP连接
processChatStreamAsync(emitter, agent, chatRequest, userId,emitterId); processChatStreamAsync(emitter, agent, chatRequest, userId);
return emitter; return emitter;
} }
/** /**
* 异步处理流式对话 * 异步处理流式对话
*/ */
@Async @Async
private void processChatStreamAsync(SseEmitter emitter, Agent agent, ChatRequest chatRequest, String userId,String emitterId) { private void processChatStreamAsync(SseEmitter emitter, Agent agent, ChatRequest chatRequest, String userId) {
try { try {
processChatRequest(emitter, agent, chatRequest, userId,emitterId); // 首先检查连接状态
if (emitter != null && userSseService.isEmitterCompleted(emitter)) {
log.debug("SSE连接已关闭,跳过异步处理");
return;
}
processChatRequest(emitter, agent, chatRequest, userId);
} catch (Exception e) { } catch (Exception e) {
log.error("处理聊天请求时发生异常", e); log.error("处理聊天请求时发生异常", e);
try {
// 检查响应是否已经提交 // 检查响应是否已经提交
if (emitter != null && !userSseSerivce.isEmitterCompleted(emitter)) { if (emitter != null && !userSseService.isEmitterCompleted(emitter)) {
errorHandlerService.handleChatError(emitter, "处理请求时发生错误", e, null); errorHandlerService.handleChatError(emitter, "处理请求时发生错误", e, null);
} else {
log.warn("响应已提交或emitter已完成,无法发送处理请求错误信息");
}
} catch (Exception handlerException) {
log.error("处理错误信息时发生异常", handlerException);
} }
} }
} }
...@@ -165,70 +158,75 @@ public class AgentChatService { ...@@ -165,70 +158,75 @@ public class AgentChatService {
* 处理聊天请求的核心逻辑 * 处理聊天请求的核心逻辑
* 注意:权限验证已在主线程中完成,此正仅执行业务逻辑不进行权限检查 * 注意:权限验证已在主线程中完成,此正仅执行业务逻辑不进行权限检查
* *
* @param emitter SSE发射器 * @param emitter SSE发射器
* @param agent Agent对象 * @param agent Agent对象
* @param chatRequest 聊天请求 * @param chatRequest 聊天请求
* @param userId 用户ID * @param userId 用户ID
*/ */
private void processChatRequest(SseEmitter emitter, Agent agent, ChatRequest chatRequest, String userId,String emitterId) { private void processChatRequest(SseEmitter emitter, Agent agent, ChatRequest chatRequest, String userId) {
try { try {
// 参数验证 // 参数验证
if (!validateParameters(emitter, agent, chatRequest, userId)) { if (!validateParameters(emitter, agent, chatRequest, userId)) {
return; return;
} }
// 获取处理器前检查连接状态
if (userSseService.isEmitterCompleted(emitter)) {
log.debug("SSE连接已关闭,跳过获取处理器");
return;
}
// 获取处理器 // 获取处理器
AgentProcessor processor = agentProcessorFactory.getProcessor(agent); AgentProcessor processor = agentProcessorFactory.getProcessor(agent);
if (processor == null) { if (processor == null) {
log.error("无法获取Agent处理器,Agent: {}", agent.getId()); log.error("无法获取Agent处理器,Agent: {}", agent.getId());
errorHandlerService.handleChatError(emitter, "无法获取Agent处理器"); if (!userSseService.isEmitterCompleted(emitter)) {
errorHandlerService.handleChatError(emitter, "无法获取Agent处理器");
}
return; return;
} }
// 处理请求前检查连接状态
if (userSseService.isEmitterCompleted(emitter)) {
log.debug("SSE连接已关闭,跳过处理请求");
return;
}
// 转换请求对象 // 转换请求对象
AgentRequest request = chatRequest.toAgentRequest(agent.getId(), agent, agentToolManager); AgentRequest request = chatRequest.toAgentRequest(agent.getId(), agent, agentToolManager);
// 设置SSE发射器到token发射器 // 创建新的SseTokenEmitter实例
sseTokenEmitter.setEmitter(emitter); SseTokenEmitter tokenEmitter = new SseTokenEmitter(userSseService, emitter, agent, request, userId, this::handleCompletion);
// 设置上下文信息
sseTokenEmitter.setContext(agent, request, userId);
// 设置完成回调
sseTokenEmitter.setCompletionCallback(this::handleCompletion);
sseTokenEmitter.setEmitterId(emitterId);
// 处理流式请求 // 处理流式请求前再次检查连接状态
processor.processStreamRequest(request, agent, userId, sseTokenEmitter); if (!userSseService.isEmitterCompleted(emitter)) {
processor.processStreamRequest(request, agent, userId, tokenEmitter);
} else {
log.debug("SSE连接已关闭,跳过流式处理");
}
} catch (Exception e) { } catch (Exception e) {
log.error("处理聊天请求时发生异常", e); log.error("处理聊天请求时发生异常", e);
errorHandlerService.handleChatError(emitter, "处理请求时发生错误", e, null); errorHandlerService.handleChatError(emitter, "处理请求时发生错误", e, null);
} }
} }
/** /**
* 处理完成回调 * 处理完成回调
* *
* @param emitter SSE发射器 * @param emitter SSE发射器
* @param agent Agent对象 * @param agent Agent对象
* @param request Agent请求 * @param request Agent请求
* @param userId 用户ID * @param userId 用户ID
* @param fullContent 完整内容 * @param fullContent 完整内容
*/ */
private void handleCompletion(SseEmitter emitter, Agent agent, AgentRequest request, String userId, String fullContent) { private void handleCompletion(SseEmitter emitter, Agent agent, AgentRequest request, String userId,
String fullContent) {
log.info("Agent处理完成,总字符数: {}", fullContent != null ? fullContent.length() : 0); log.info("Agent处理完成,总字符数: {}", fullContent != null ? fullContent.length() : 0);
// 保存对话记录 // 保存对话记录 - 安全操作,不抛出异常
try { saveDialogue(agent, request, userId, fullContent);
saveDialogue(agent, request, userId, fullContent);
log.info("对话记录保存成功");
} catch (Exception e) {
log.error("保存对话记录失败", e);
// 记录异常但不中断流程
}
} }
/** /**
* 保存对话记录 * 保存对话记录
*/ */
...@@ -238,31 +236,32 @@ public class AgentChatService { ...@@ -238,31 +236,32 @@ public class AgentChatService {
log.error("保存对话记录失败:参数无效"); log.error("保存对话记录失败:参数无效");
return; return;
} }
try { try {
// 创建对话记录 // 创建对话记录
pangea.hiagent.model.AgentDialogue dialogue = pangea.hiagent.model.AgentDialogue.builder() pangea.hiagent.model.AgentDialogue dialogue = pangea.hiagent.model.AgentDialogue.builder()
.agentId(request.getAgentId()) .agentId(request.getAgentId())
.userMessage(request.getUserMessage()) .userMessage(request.getUserMessage())
.agentResponse(responseContent) .agentResponse(responseContent)
.userId(userId) .userId(userId)
.build(); .build();
// 保存对话记录 // 保存对话记录
agentService.saveDialogue(dialogue); agentService.saveDialogue(dialogue);
log.info("对话记录保存成功");
} catch (Exception e) { } catch (Exception e) {
log.error("保存对话记录失败", e); log.error("保存对话记录失败", e);
throw new RuntimeException("保存对话记录失败", e); // 仅记录日志,不抛出异常
} }
} }
/** /**
* 验证所有必需参数 * 验证所有必需参数
* *
* @param emitter SSE发射器 * @param emitter SSE发射器
* @param agent Agent对象 * @param agent Agent对象
* @param chatRequest 聊天请求 * @param chatRequest 聊天请求
* @param userId 用户ID * @param userId 用户ID
* @return 验证是否通过 * @return 验证是否通过
*/ */
private boolean validateParameters(SseEmitter emitter, Agent agent, ChatRequest chatRequest, String userId) { private boolean validateParameters(SseEmitter emitter, Agent agent, ChatRequest chatRequest, String userId) {
......
...@@ -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
package pangea.hiagent.agent.service; package pangea.hiagent.agent.service;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import pangea.hiagent.web.dto.WorkPanelEvent; import pangea.hiagent.agent.data.ErrorEventDataBuilder;
import pangea.hiagent.workpanel.event.EventService; import pangea.hiagent.agent.data.MapPoolService;
import pangea.hiagent.workpanel.data.TokenEventDataBuilder; import pangea.hiagent.agent.data.TokenEventDataBuilder;
import pangea.hiagent.workpanel.data.ErrorEventDataBuilder; import pangea.hiagent.agent.data.WorkPanelEvent;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.Map; import java.util.Map;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
...@@ -25,33 +26,38 @@ import java.util.concurrent.ScheduledFuture; ...@@ -25,33 +26,38 @@ import java.util.concurrent.ScheduledFuture;
@Slf4j @Slf4j
@Service @Service
public class UserSseService { public class UserSseService {
// 存储所有活动的 emitter // 存储所有活动的 emitter
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>(); private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
// 存储用户ID到SSE Emitter的映射关系 // 存储用户ID到SSE Emitter的映射关系
private final ConcurrentMap<String, SseEmitter> userEmitters = new ConcurrentHashMap<>(); private final ConcurrentMap<String, SseEmitter> userEmitters = new ConcurrentHashMap<>();
// 存储SSE Emitter到用户ID的反向映射关系(用于快速查找) // 存储SSE Emitter到用户ID的反向映射关系(用于快速查找)
private final ConcurrentMap<SseEmitter, String> emitterUsers = new ConcurrentHashMap<>(); private final ConcurrentMap<SseEmitter, String> emitterUsers = new ConcurrentHashMap<>();
// 存储已完成的emitter集合,用于快速检查状态
private final Set<SseEmitter> completedEmitters = ConcurrentHashMap.newKeySet();
// 心跳任务执行器 - 使用共享线程池以提高资源利用率 // 心跳任务执行器 - 使用共享线程池以提高资源利用率
private final ScheduledExecutorService heartbeatExecutor; private final ScheduledExecutorService heartbeatExecutor;
// SSE超时时间(毫秒)
private static final long SSE_TIMEOUT = 0L; // 0表示不使用默认超时,由心跳机制管理连接
private final EventService eventService;
private final TokenEventDataBuilder tokenEventDataBuilder; private final TokenEventDataBuilder tokenEventDataBuilder;
private final ErrorEventDataBuilder errorEventDataBuilder; private final ErrorEventDataBuilder errorEventDataBuilder;
public UserSseService(EventService eventService, TokenEventDataBuilder tokenEventDataBuilder, ErrorEventDataBuilder errorEventDataBuilder) { private final MapPoolService mapPoolService;
this.eventService = eventService;
// SSE超时时间(毫秒)
private static final long SSE_TIMEOUT = 0L; // 0表示不使用默认超时,由心跳机制管理连接
public UserSseService(TokenEventDataBuilder tokenEventDataBuilder, ErrorEventDataBuilder errorEventDataBuilder, MapPoolService mapPoolService) {
this.tokenEventDataBuilder = tokenEventDataBuilder; this.tokenEventDataBuilder = tokenEventDataBuilder;
this.errorEventDataBuilder = errorEventDataBuilder; this.errorEventDataBuilder = errorEventDataBuilder;
this.mapPoolService = mapPoolService;
this.heartbeatExecutor = Executors.newScheduledThreadPool(2); this.heartbeatExecutor = Executors.newScheduledThreadPool(2);
} }
/** /**
* 创建并注册SSE连接 * 创建并注册SSE连接
* *
...@@ -60,27 +66,27 @@ public class UserSseService { ...@@ -60,27 +66,27 @@ public class UserSseService {
*/ */
public SseEmitter createAndRegisterConnection(String userId) { public SseEmitter createAndRegisterConnection(String userId) {
log.debug("开始为用户 {} 创建SSE连接", userId); log.debug("开始为用户 {} 创建SSE连接", userId);
// 创建 SSE emitter // 创建 SSE emitter
SseEmitter emitter = createEmitter(); SseEmitter emitter = createEmitter();
log.debug("SSE Emitter创建成功"); log.debug("SSE Emitter创建成功");
// 注册用户的SSE连接 // 注册用户的SSE连接
registerSession(userId, emitter); registerSession(userId, emitter);
log.debug("用户 {} 的SSE连接注册成功", userId); log.debug("用户 {} 的SSE连接注册成功", userId);
// 注册 emitter 回调 // 注册 emitter 回调
registerCallbacks(emitter, userId); registerCallbacks(emitter, userId);
log.debug("SSE Emitter回调注册成功"); log.debug("SSE Emitter回调注册成功");
// 启动心跳机制 // 启动心跳机制
startHeartbeat(emitter, new AtomicBoolean(false)); startHeartbeat(emitter, new AtomicBoolean(false));
log.debug("心跳机制启动成功"); log.debug("心跳机制启动成功");
log.info("用户 {} 的SSE连接创建和注册完成", userId); log.info("用户 {} 的SSE连接创建和注册完成", userId);
return emitter; return emitter;
} }
/** /**
* 创建SSE发射器 * 创建SSE发射器
* *
...@@ -88,18 +94,17 @@ public class UserSseService { ...@@ -88,18 +94,17 @@ public class UserSseService {
*/ */
public SseEmitter createEmitter() { public SseEmitter createEmitter() {
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT); SseEmitter emitter = new SseEmitter(SSE_TIMEOUT);
registerCallbacks(emitter);
emitters.add(emitter); emitters.add(emitter);
// 启动心跳机制,确保新创建的连接有心跳 // 启动心跳机制,确保新创建的连接有心跳
startHeartbeat(emitter, new AtomicBoolean(false)); startHeartbeat(emitter, new AtomicBoolean(false));
return emitter; return emitter;
} }
/** /**
* 注册用户的SSE连接 * 注册用户的SSE连接
* 如果该用户已有连接,则先关闭旧连接再注册新连接 * 如果该用户已有连接,则先关闭旧连接再注册新连接
* *
* @param userId 用户ID * @param userId 用户ID
* @param emitter SSE Emitter * @param emitter SSE Emitter
* @return true表示注册成功,false表示注册失败 * @return true表示注册成功,false表示注册失败
*/ */
...@@ -108,7 +113,7 @@ public class UserSseService { ...@@ -108,7 +113,7 @@ public class UserSseService {
log.warn("注册SSE会话失败:用户ID或Emitter为空"); log.warn("注册SSE会话失败:用户ID或Emitter为空");
return false; return false;
} }
try { try {
// 检查该用户是否已有连接 // 检查该用户是否已有连接
SseEmitter existingEmitter = userEmitters.get(userId); SseEmitter existingEmitter = userEmitters.get(userId);
...@@ -123,11 +128,11 @@ public class UserSseService { ...@@ -123,11 +128,11 @@ public class UserSseService {
userEmitters.remove(userId); userEmitters.remove(userId);
emitterUsers.remove(existingEmitter); emitterUsers.remove(existingEmitter);
} }
// 注册新连接 // 注册新连接
userEmitters.put(userId, emitter); userEmitters.put(userId, emitter);
emitterUsers.put(emitter, userId); emitterUsers.put(emitter, userId);
log.info("用户 {} 的SSE连接注册成功", userId); log.info("用户 {} 的SSE连接注册成功", userId);
return true; return true;
} catch (Exception e) { } catch (Exception e) {
...@@ -135,7 +140,7 @@ public class UserSseService { ...@@ -135,7 +140,7 @@ public class UserSseService {
return false; return false;
} }
} }
/** /**
* 获取用户的SSE连接 * 获取用户的SSE连接
* *
...@@ -145,21 +150,21 @@ public class UserSseService { ...@@ -145,21 +150,21 @@ public class UserSseService {
public SseEmitter getSession(String userId) { public SseEmitter getSession(String userId) {
return userEmitters.get(userId); return userEmitters.get(userId);
} }
/** /**
* 处理连接完成事件 * 通用连接关闭处理方法
* *
* @param emitter SSE Emitter * @param emitter SSE Emitter
* @param connectionType 连接类型(用于日志)
*/ */
public void handleConnectionCompletion(SseEmitter emitter) { private void handleConnectionClose(SseEmitter emitter, String connectionType) {
if (emitter == null) { if (emitter == null) {
return; return;
} }
try { try {
// 按照正确的SSE连接关闭顺序: // 添加到已完成集合
// 4. 取消心跳任务:清理相关的ScheduledFuture心跳任务(已在回调中处理) completedEmitters.add(emitter);
// 5. 移除连接映射:从连接管理器(userEmitters、emitterUsers、emitters)中移除连接映射
// 检查emitter是否已经完成,避免重复关闭 // 检查emitter是否已经完成,避免重复关闭
if (!isEmitterCompleted(emitter)) { if (!isEmitterCompleted(emitter)) {
...@@ -169,109 +174,65 @@ public class UserSseService { ...@@ -169,109 +174,65 @@ public class UserSseService {
log.debug("完成emitter时发生异常(可能是由于已关闭): {}", e.getMessage()); log.debug("完成emitter时发生异常(可能是由于已关闭): {}", e.getMessage());
} }
} }
// 从映射表中移除连接 // 从映射表中移除连接
String userId = emitterUsers.remove(emitter); String userId = emitterUsers.remove(emitter);
if (userId != null) { if (userId != null) {
userEmitters.remove(userId); userEmitters.remove(userId);
} }
emitters.remove(emitter); emitters.remove(emitter);
log.debug("SSE连接完成,用户: {}", userId); log.debug("SSE连接{},用户: {}", connectionType, userId);
} catch (Exception e) { } catch (Exception e) {
log.error("处理SSE连接完成事件时发生异常", e); log.error("处理SSE连接{}事件时发生异常", connectionType, e);
} }
} }
/**
* 处理连接完成事件
*
* @param emitter SSE Emitter
*/
public void handleConnectionCompletion(SseEmitter emitter) {
handleConnectionClose(emitter, "完成");
}
/** /**
* 处理连接超时事件 * 处理连接超时事件
* *
* @param emitter SSE Emitter * @param emitter SSE Emitter
*/ */
public void handleConnectionTimeout(SseEmitter emitter) { public void handleConnectionTimeout(SseEmitter emitter) {
if (emitter == null) { handleConnectionClose(emitter, "超时");
return;
}
try {
// 按照正确的SSE连接关闭顺序:
// 4. 取消心跳任务:清理相关的ScheduledFuture心跳任务(已在回调中处理)
// 5. 移除连接映射:从连接管理器(userEmitters、emitterUsers、emitters)中移除连接映射
// 检查emitter是否已经完成,避免重复关闭
if (!isEmitterCompleted(emitter)) {
try {
emitter.complete();
} catch (Exception e) {
log.debug("完成emitter时发生异常(可能是由于已关闭): {}", e.getMessage());
}
}
// 从映射表中移除连接
String userId = emitterUsers.remove(emitter);
if (userId != null) {
userEmitters.remove(userId);
}
emitters.remove(emitter);
log.debug("SSE连接超时,用户: {}", userId);
} catch (Exception e) {
log.error("处理SSE连接超时事件时发生异常", e);
}
} }
/** /**
* 处理连接错误事件 * 处理连接错误事件
* *
* @param emitter SSE Emitter * @param emitter SSE Emitter
*/ */
public void handleConnectionError(SseEmitter emitter) { public void handleConnectionError(SseEmitter emitter) {
if (emitter == null) { handleConnectionClose(emitter, "错误");
return;
}
try {
// 按照正确的SSE连接关闭顺序:
// 4. 取消心跳任务:清理相关的ScheduledFuture心跳任务(已在回调中处理)
// 5. 移除连接映射:从连接管理器(userEmitters、emitterUsers、emitters)中移除连接映射
// 检查emitter是否已经完成,避免重复关闭
if (!isEmitterCompleted(emitter)) {
try {
emitter.complete();
} catch (Exception e) {
log.debug("完成emitter时发生异常(可能是由于已关闭): {}", e.getMessage());
}
}
// 从映射表中移除连接
String userId = emitterUsers.remove(emitter);
if (userId != null) {
userEmitters.remove(userId);
}
emitters.remove(emitter);
log.debug("SSE连接错误,用户: {}", userId);
} catch (Exception e) {
log.error("处理SSE连接错误事件时发生异常", e);
}
} }
/** /**
* 移除SSE发射器 * 移除SSE发射器
* *
* @param emitter SSE发射器 * @param emitter SSE发射器
*/ */
public void removeEmitter(SseEmitter emitter) { public void removeEmitter(SseEmitter emitter) {
if (emitter != null && emitters.remove(emitter)) { if (emitter != null) {
log.debug("已移除SSE Emitter,剩余连接数: {}", emitters.size()); completedEmitters.add(emitter); // 添加到已完成集合
if (emitters.remove(emitter)) {
log.debug("已移除SSE Emitter,剩余连接数: {}", emitters.size());
}
} }
} }
/** /**
* 启动心跳机制 * 启动心跳机制
* *
* @param emitter SSE发射器 * @param emitter SSE发射器
* @param isCompleted 是否已完成 * @param isCompleted 是否已完成
*/ */
public void startHeartbeat(SseEmitter emitter, AtomicBoolean isCompleted) { public void startHeartbeat(SseEmitter emitter, AtomicBoolean isCompleted) {
...@@ -279,13 +240,13 @@ public class UserSseService { ...@@ -279,13 +240,13 @@ public class UserSseService {
log.warn("SSE发射器为空,无法启动心跳机制"); log.warn("SSE发射器为空,无法启动心跳机制");
return; return;
} }
// 用于追踪心跳失败次数 // 用于追踪心跳失败次数
AtomicInteger consecutiveFailures = new AtomicInteger(0); AtomicInteger consecutiveFailures = new AtomicInteger(0);
// 使用数组包装ScheduledFuture以解决Lambda中的变量访问问题 // 使用数组包装ScheduledFuture以解决Lambda中的变量访问问题
final ScheduledFuture<?>[] heartbeatTaskRef = new ScheduledFuture<?>[1]; final ScheduledFuture<?>[] heartbeatTaskRef = new ScheduledFuture<?>[1];
// 创建心跳任务并保存ScheduledFuture引用 // 创建心跳任务并保存ScheduledFuture引用
heartbeatTaskRef[0] = heartbeatExecutor.scheduleAtFixedRate(() -> { heartbeatTaskRef[0] = heartbeatExecutor.scheduleAtFixedRate(() -> {
try { try {
...@@ -297,41 +258,41 @@ public class UserSseService { ...@@ -297,41 +258,41 @@ public class UserSseService {
} }
return; return;
} }
// 发送心跳事件 // 发送心跳事件
boolean heartbeatSuccess = sendHeartbeat(emitter); boolean heartbeatSuccess = sendHeartbeat(emitter);
if (heartbeatSuccess) { if (heartbeatSuccess) {
// 如果心跳成功,重置失败计数 // 如果心跳成功,重置失败计数
consecutiveFailures.set(0); consecutiveFailures.set(0);
log.debug("心跳发送成功,连续失败次数重置为0"); log.debug("心跳发送成功,连续失败次数重置为0");
// 心跳成功后,连接保持活动状态,不需要额外操作,因为SSE_TIMEOUT为0 // 心跳成功后,连接保持活动状态,不需要额外操作,因为SSE_TIMEOUT为0
} else { } else {
// 心跳失败,增加失败计数 // 心跳失败,增加失败计数
int currentFailures = consecutiveFailures.incrementAndGet(); int currentFailures = consecutiveFailures.incrementAndGet();
log.debug("心跳连续失败次数: {}", currentFailures); log.debug("心跳连续失败次数: {}", currentFailures);
// 如果心跳连续失败达到阈值,启动延迟关闭 // 如果心跳连续失败达到阈值,启动延迟关闭
if (currentFailures >= 2) { // 连续2次失败后,启动30秒延迟关闭 if (currentFailures >= 2) { // 连续2次失败后,启动30秒延迟关闭
log.warn("心跳连续失败{}次,启动30秒延迟关闭机制", currentFailures); log.warn("心跳连续失败{}次,启动30秒延迟关闭机制", currentFailures);
// 调度一个延迟任务来关闭连接 // 调度一个延迟任务来关闭连接
heartbeatExecutor.schedule(() -> { heartbeatExecutor.schedule(() -> {
if (!isCompleted.get() && !isEmitterCompleted(emitter)) { if (!isCompleted.get() && !isEmitterCompleted(emitter)) {
log.info("30秒延迟到期,主动关闭SSE连接"); log.info("30秒延迟到期,主动关闭SSE连接");
// 首先取消心跳任务 // 首先取消心跳任务
if (heartbeatTaskRef[0] != null && !heartbeatTaskRef[0].isCancelled()) { if (heartbeatTaskRef[0] != null && !heartbeatTaskRef[0].isCancelled()) {
heartbeatTaskRef[0].cancel(true); heartbeatTaskRef[0].cancel(true);
log.debug("心跳任务已取消"); log.debug("心跳任务已取消");
} }
// 然后关闭SSE连接 // 然后关闭SSE连接
try { try {
if (!isEmitterCompleted(emitter)) { if (!isEmitterCompleted(emitter)) {
emitter.complete(); // emitter.complete();
log.debug("SSE连接已关闭"); log.debug("SSE连接已关闭");
} }
} catch (Exception ex) { } catch (Exception ex) {
...@@ -341,7 +302,7 @@ public class UserSseService { ...@@ -341,7 +302,7 @@ public class UserSseService {
log.debug("SSE连接已完成或已关闭,跳过延迟关闭"); log.debug("SSE连接已完成或已关闭,跳过延迟关闭");
} }
}, 30, TimeUnit.SECONDS); }, 30, TimeUnit.SECONDS);
// 立即取消当前心跳任务 // 立即取消当前心跳任务
if (heartbeatTaskRef[0] != null && !heartbeatTaskRef[0].isCancelled()) { if (heartbeatTaskRef[0] != null && !heartbeatTaskRef[0].isCancelled()) {
heartbeatTaskRef[0].cancel(true); heartbeatTaskRef[0].cancel(true);
...@@ -349,53 +310,42 @@ public class UserSseService { ...@@ -349,53 +310,42 @@ public class UserSseService {
} }
} }
} }
} catch (Exception e) { } catch (Exception e) {
log.error("心跳任务执行异常: {}", e.getMessage(), e); log.error("心跳任务执行异常: {}", e.getMessage(), e);
} }
}, 20, 20, TimeUnit.SECONDS); // 每20秒发送一次心跳,确保前端60秒超时前至少收到2次心跳 }, 20, 20, TimeUnit.SECONDS); // 每20秒发送一次心跳,确保前端60秒超时前至少收到2次心跳
// 注册回调,在连接完成时取消心跳任务
emitter.onCompletion(() -> {
if (heartbeatTaskRef[0] != null && !heartbeatTaskRef[0].isCancelled()) {
heartbeatTaskRef[0].cancel(true);
log.debug("SSE连接完成,心跳任务已取消");
}
});
// 注册回调,在连接超时时取消心跳任务
emitter.onTimeout(() -> {
if (heartbeatTaskRef[0] != null && !heartbeatTaskRef[0].isCancelled()) {
heartbeatTaskRef[0].cancel(true);
log.debug("SSE连接超时,心跳任务已取消");
}
});
// 注册回调,在连接错误时取消心跳任务
emitter.onError(throwable -> {
if (heartbeatTaskRef[0] != null && !heartbeatTaskRef[0].isCancelled()) {
heartbeatTaskRef[0].cancel(true);
log.debug("SSE连接错误,心跳任务已取消");
}
});
} }
/** /**
* 注册回调函数 * 注册回调函数
* *
* @param emitter SSE发射器 * @param emitter SSE发射器
* @param userId 用户ID(可选,用于完整的连接管理)
*/ */
public void registerCallbacks(SseEmitter emitter) { public void registerCallbacks(SseEmitter emitter, String... userId) {
boolean hasUserId = userId != null && userId.length > 0 && userId[0] != null;
emitter.onCompletion(() -> { emitter.onCompletion(() -> {
log.debug("【注册回调函数】SSE连接完成"); log.debug("【注册回调函数】SSE连接完成");
// 按照正确的关闭顺序,连接完成时已经完成关闭,只需移除连接映射 if (hasUserId) {
removeEmitter(emitter); handleConnectionCompletion(emitter);
} else {
removeEmitter(emitter);
}
}); });
emitter.onError((Throwable t) -> { emitter.onError((Throwable t) -> {
log.debug("SSE连接发生错误: {}", t.getMessage()); String errorMessage = t.getMessage();
// 错误发生时,先移除连接映射 String errorType = t.getClass().getSimpleName();
removeEmitter(emitter); log.debug("SSE连接发生错误 - 类型: {}, 消息: {}", errorType, errorMessage);
if (hasUserId) {
handleConnectionError(emitter);
} else {
removeEmitter(emitter);
}
}); });
emitter.onTimeout(() -> { emitter.onTimeout(() -> {
log.warn("SSE连接超时"); log.warn("SSE连接超时");
try { try {
...@@ -406,56 +356,18 @@ public class UserSseService { ...@@ -406,56 +356,18 @@ public class UserSseService {
} catch (Exception e) { } catch (Exception e) {
log.debug("关闭SSE连接时发生异常(可能是由于已关闭): {}", e.getMessage()); log.debug("关闭SSE连接时发生异常(可能是由于已关闭): {}", e.getMessage());
} }
// 超时时也移除连接映射 if (hasUserId) {
removeEmitter(emitter); handleConnectionTimeout(emitter);
}); } else {
} removeEmitter(emitter);
/**
* 注册Emitter回调函数
* 职责:注册所有必要的回调处理函数
*
* @param emitter SSE Emitter
* @param userId 用户ID
*/
public void registerCallbacks(SseEmitter emitter, String userId) {
emitter.onCompletion(() -> {
log.debug("【注册Emitter回调函数】SSE连接完成");
// 通知用户连接管理器连接已完成
handleConnectionCompletion(emitter);
});
emitter.onTimeout(() -> {
log.warn("SSE连接超时");
try {
// 检查emitter是否已经完成,避免重复关闭
if (!isEmitterCompleted(emitter)) {
emitter.complete();
}
} catch (Exception e) {
log.debug("关闭SSE连接失败(可能是由于已关闭): {}", e.getMessage());
} }
// 通知用户连接管理器连接已超时
handleConnectionTimeout(emitter);
}); });
emitter.onError(throwable -> {
// 记录详细的错误信息,包括异常类型和消息
String errorMessage = throwable.getMessage();
String errorType = throwable.getClass().getSimpleName();
log.error("SSE连接错误 - 类型: {}, 消息: {}", errorType, errorMessage, throwable);
// 通知用户连接管理器连接发生错误
handleConnectionError(emitter);
});
// 注册 emitter 到管理器
registerCallbacks(emitter);
} }
/** /**
* 完成SSE发射器 * 完成SSE发射器
* *
* @param emitter SSE发射器 * @param emitter SSE发射器
* @param isCompleted 是否已完成 * @param isCompleted 是否已完成
*/ */
public void completeEmitter(SseEmitter emitter, AtomicBoolean isCompleted) { public void completeEmitter(SseEmitter emitter, AtomicBoolean isCompleted) {
...@@ -469,17 +381,20 @@ public class UserSseService { ...@@ -469,17 +381,20 @@ public class UserSseService {
log.debug("Emitter已经完成,跳过关闭操作"); log.debug("Emitter已经完成,跳过关闭操作");
return; return;
} }
emitter.complete(); emitter.complete();
completedEmitters.add(emitter); // 添加到已完成集合
log.debug("Emitter已成功关闭"); log.debug("Emitter已成功关闭");
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
log.debug("Emitter已经关闭: {}", e.getMessage()); log.debug("Emitter已经关闭: {}", e.getMessage());
completedEmitters.add(emitter); // 添加到已完成集合
} catch (Exception e) { } catch (Exception e) {
log.warn("完成Emitter时发生异常: {}", e.getMessage()); log.warn("完成Emitter时发生异常: {}", e.getMessage());
completedEmitters.add(emitter); // 添加到已完成集合
} }
} }
} }
/** /**
* 检查SSE Emitter是否仍然有效 * 检查SSE Emitter是否仍然有效
* 职责:提供轻量级的连接有效性检查 * 职责:提供轻量级的连接有效性检查
...@@ -491,12 +406,12 @@ public class UserSseService { ...@@ -491,12 +406,12 @@ public class UserSseService {
if (emitter == null) { if (emitter == null) {
return false; return false;
} }
// 首先检查是否已经完成,避免不必要的事件发送 // 首先检查是否已经完成,避免不必要的事件发送
if (isEmitterCompleted(emitter)) { if (isEmitterCompleted(emitter)) {
return false; return false;
} }
// 检查逻辑,仅通过尝试发送ping事件来验证连接状态 // 检查逻辑,仅通过尝试发送ping事件来验证连接状态
try { try {
// 尝试发送一个空事件来检查连接状态 // 尝试发送一个空事件来检查连接状态
...@@ -508,7 +423,7 @@ public class UserSseService { ...@@ -508,7 +423,7 @@ public class UserSseService {
return false; return false;
} }
} }
/** /**
* 安全检查SSE Emitter是否仍然有效(不发送实际事件) * 安全检查SSE Emitter是否仍然有效(不发送实际事件)
* 职责:提供非侵入性的连接有效性检查 * 职责:提供非侵入性的连接有效性检查
...@@ -520,14 +435,14 @@ public class UserSseService { ...@@ -520,14 +435,14 @@ public class UserSseService {
if (emitter == null) { if (emitter == null) {
return false; return false;
} }
// 检查是否已经完成,而不发送任何事件 // 检查是否已经完成,而不发送任何事件
return !isEmitterCompleted(emitter); return !isEmitterCompleted(emitter);
} }
/** /**
* 检查SSE Emitter是否已经完成 * 检查SSE Emitter是否已经完成
* 使用更安全的方式检查完成状态,不发送实际事件 * 使用高效的方式检查完成状态,优先检查本地集合
* *
* @param emitter 要检查的emitter * @param emitter 要检查的emitter
* @return 如果已完成返回true,否则返回false * @return 如果已完成返回true,否则返回false
...@@ -536,57 +451,70 @@ public class UserSseService { ...@@ -536,57 +451,70 @@ public class UserSseService {
if (emitter == null) { if (emitter == null) {
return true; // 认为null emitter是已完成的 return true; // 认为null emitter是已完成的
} }
// 使用反射检查SseEmitter的完成状态 // 首先检查本地集合,避免网络操作
try { if (completedEmitters.contains(emitter)) {
java.lang.reflect.Field completedField = SseEmitter.class.getDeclaredField("completed"); return true;
completedField.setAccessible(true);
boolean completed = completedField.getBoolean(emitter);
return completed;
} catch (Exception e) {
// 如果反射失败,尝试通过发送事件检测
try {
emitter.send(SseEmitter.event());
return false; // 没有异常说明未完成
} catch (IllegalStateException ex) {
// 检查错误消息是否包含完成相关的文本
String message = ex.getMessage();
if (message != null && (message.contains("completed") || message.contains("closed"))) {
return true;
}
return true; // IllegalStateException通常表示连接已关闭
} catch (Exception ex) {
// 其他异常通常也表示连接已不可用
return true;
}
} }
// 检查emitter是否在活动列表中
if (!emitters.contains(emitter)) {
completedEmitters.add(emitter); // 添加到已完成集合
return true;
}
// 尝试使用安全的方式检查emitter状态,避免发送实际事件
// 注意:这个方法不会捕获所有连接问题,但能有效避免UT005023异常
return false;
} }
/** /**
* 发送SSE事件 * 安全发送SSE事件,处理所有异常情况
* 职责:统一发送SSE事件的基础方法
* *
* @param emitter SSE发射器 * @param emitter SSE发射器
* @param eventName 事件名称 * @param eventName 事件名称
* @param data 事件数据 * @param data 事件数据
* @throws IOException IO异常 * @return 是否发送成功
*/ */
public void sendEvent(SseEmitter emitter, String eventName, Object data) throws IOException { private boolean safeSendEvent(SseEmitter emitter, String eventName, Object data) {
// 参数验证 // 参数验证
if (emitter == null || eventName == null || eventName.isEmpty() || data == null) { if (emitter == null || eventName == null || eventName.isEmpty() || data == null) {
log.warn("参数验证失败,无法发送事件"); log.warn("参数验证失败,无法发送事件");
return; return false;
} }
// 检查emitter是否已经完成
if (isEmitterCompleted(emitter)) {
log.debug("SSE emitter已完成,跳过发送{}事件", eventName);
return false;
}
try { try {
emitter.send(SseEmitter.event().name(eventName).data(data)); emitter.send(SseEmitter.event().name(eventName).data(data));
return true;
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
// 处理 emitter 已关闭的情况 // 处理 emitter 已关闭的情况
log.debug("无法发送事件,emitter已关闭: {}", e.getMessage()); log.debug("无法发送{}事件,emitter已关闭: {}", eventName, e.getMessage());
// 不重新抛出异常,避免影响主流程 return false;
} catch (Exception e) {
log.error("发送{}事件失败: {}", eventName, e.getMessage(), e);
return false;
} }
} }
/**
* 发送SSE事件
* 职责:统一发送SSE事件的基础方法
*
* @param emitter SSE发射器
* @param eventName 事件名称
* @param data 事件数据
* @throws IOException IO异常
*/
public void sendEvent(SseEmitter emitter, String eventName, Object data) throws IOException {
safeSendEvent(emitter, eventName, data);
}
/** /**
* 发送心跳事件 * 发送心跳事件
* *
...@@ -598,37 +526,26 @@ public class UserSseService { ...@@ -598,37 +526,26 @@ public class UserSseService {
log.warn("SSE发射器为空,无法发送心跳事件"); log.warn("SSE发射器为空,无法发送心跳事件");
return false; return false;
} }
// 检查emitter是否已经完成,避免向已完成的连接发送心跳 // 发送心跳事件
if (isEmitterCompleted(emitter)) { long heartbeatTimestamp = System.currentTimeMillis();
log.debug("SSE发射器已完成,跳过发送心跳事件"); boolean success = safeSendEvent(emitter, "heartbeat", heartbeatTimestamp);
return false;
} if (success) {
try {
// 发送心跳事件
long heartbeatTimestamp = System.currentTimeMillis();
emitter.send(SseEmitter.event().name("heartbeat").data(heartbeatTimestamp));
log.debug("[心跳] 成功发送心跳事件,时间戳: {}", heartbeatTimestamp); log.debug("[心跳] 成功发送心跳事件,时间戳: {}", heartbeatTimestamp);
return true;
} catch (IllegalStateException e) {
// 处理 emitter 已关闭的情况
log.debug("无法发送心跳事件,emitter已关闭或完成: {}", e.getMessage());
return false;
} catch (Exception e) {
log.warn("发送心跳事件失败: {}", e.getMessage());
return false;
} }
return success;
} }
/** /**
* 发送工作面板事件给指定的SSE连接 * 发送工作面板事件给指定的SSE连接
* *
* @param emitter SSE发射器 * @param emitter SSE发射器
* @param event 工作面板事件 * @param event 工作面板事件
* @throws IOException IO异常 * @throws IOException IO异常
*/ */
public void sendWorkPanelEvent(SseEmitter emitter, WorkPanelEvent event) throws IOException { public void sendWorkPanelEvent(WorkPanelEvent event) throws IOException {
if (event == null) { if (event == null) {
log.warn("工作面板事件为空,无法发送事件"); log.warn("工作面板事件为空,无法发送事件");
return; return;
...@@ -636,15 +553,22 @@ public class UserSseService { ...@@ -636,15 +553,22 @@ public class UserSseService {
try { try {
// 构建事件数据 // 构建事件数据
Map<String, Object> data = eventService.buildWorkPanelEventData(event); Map<String, Object> data = buildWorkPanelEventData(event);
if (data != null) { if (data != null) {
log.debug("准备发送工作面板事件: 类型={}, 事件内容={}", event.getType(), event); log.debug("准备发送工作面板事件: 类型={}, 事件内容={}", event.getType(), event);
log.debug("事件数据: {}", data); log.debug("事件数据: {}", data);
SseEmitter emitter = getSession(event.getUserId());
if (emitter == null) {
log.debug("未找到用户 {} 的SSE连接,跳过发送事件", event.getUserId());
return;
}
// 发送事件 // 发送事件
emitter.send(SseEmitter.event().name("message").data(data)); emitter.send(SseEmitter.event().name("message").data(data));
log.debug("工作面板事件发送成功: 类型={}", event.getType()); log.debug("工作面板事件发送成功: 类型={}", event.getType());
} else { } else {
log.warn("构建事件数据失败,无法发送事件: 类型={}", event.getType()); log.warn("构建事件数据失败,无法发送事件: 类型={}", event.getType());
...@@ -656,35 +580,39 @@ public class UserSseService { ...@@ -656,35 +580,39 @@ public class UserSseService {
} catch (Exception e) { } catch (Exception e) {
// 记录详细错误信息,但不中断主流程 // 记录详细错误信息,但不中断主流程
log.error("发送工作面板事件失败: 类型={}, 错误={}", event.getType(), e.getMessage(), e); log.error("发送工作面板事件失败: 类型={}, 错误={}", event.getType(), e.getMessage(), e);
// 其他异常不重新抛出,避免影响主流程 // 其他异常不重新抛出,避免影响主流程
} }
} }
/** /**
* 发送工作面板事件给指定用户 * 构建工作面板事件数据
* *
* @param userId 用户ID
* @param event 工作面板事件 * @param event 工作面板事件
* @return 事件数据
*/ */
public void sendWorkPanelEventToUser(String userId, WorkPanelEvent event) { private Map<String, Object> buildWorkPanelEventData(WorkPanelEvent event) {
log.debug("开始向用户 {} 发送工作面板事件: {}", userId, event.getType()); if (event == null) {
return null;
}
// 检查连接是否仍然有效 // 从对象池获取Map,避免频繁创建对象
SseEmitter emitter = getSession(userId); Map<String, Object> data = mapPoolService.acquireMap();
if (emitter != null) {
try { // 设置基础属性
// 直接向当前 emitter 发送事件 data.put("eventType", event.getType());
sendWorkPanelEvent(emitter, event); data.put("timestamp", event.getTimestamp());
log.debug("已发送工作面板事件到客户端: {}", event.getType()); data.put("title", event.getTitle());
} catch (IOException e) { data.put("content", event.getContent());
log.error("发送工作面板事件失败: {}", e.getMessage(), e); // data.put("userId", event.getUserId());
}
} else { if(event.getMetadata() != null) {
log.debug("连接已失效,跳过发送事件: {}", event.getType()); data.putAll(event.getMetadata());
} }
return data;
} }
/** /**
* 发送连接成功事件 * 发送连接成功事件
* *
...@@ -696,29 +624,26 @@ public class UserSseService { ...@@ -696,29 +624,26 @@ public class UserSseService {
log.warn("SSE发射器为空,无法发送连接成功事件"); log.warn("SSE发射器为空,无法发送连接成功事件");
return; return;
} }
try { WorkPanelEvent connectedEvent = WorkPanelEvent.builder()
WorkPanelEvent connectedEvent = WorkPanelEvent.builder()
.type("observation") .type("observation")
.title("连接成功") .title("连接成功")
.timestamp(System.currentTimeMillis()) .timestamp(System.currentTimeMillis())
.build(); .build();
Map<String, Object> data = eventService.buildWorkPanelEventData(connectedEvent); Map<String, Object> data = buildWorkPanelEventData(connectedEvent);
emitter.send(SseEmitter.event().name("message").data(data)); boolean success = safeSendEvent(emitter, "message", data);
if (success) {
log.debug("已发送连接成功事件"); log.debug("已发送连接成功事件");
} catch (IOException e) {
log.error("发送连接成功事件失败", e);
throw e;
} }
} }
/** /**
* 发送Token事件 * 发送Token事件
* *
* @param emitter SSE发射器 * @param emitter SSE发射器
* @param token Token内容 * @param token Token内容
* @throws IOException IO异常 * @throws IOException IO异常
*/ */
public void sendTokenEvent(SseEmitter emitter, String token) throws IOException { public void sendTokenEvent(SseEmitter emitter, String token) throws IOException {
...@@ -726,34 +651,26 @@ public class UserSseService { ...@@ -726,34 +651,26 @@ public class UserSseService {
log.warn("SSE发射器或Token为空,无法发送Token事件"); log.warn("SSE发射器或Token为空,无法发送Token事件");
return; return;
} }
try { try {
// 检查emitter是否已经完成 // 构建token事件数据
if (!isEmitterCompleted(emitter)) { Map<String, Object> data = tokenEventDataBuilder.createOptimizedTokenEventData(token);
// 构建token事件数据
Map<String, Object> data = tokenEventDataBuilder.createOptimizedTokenEventData(token); if (data != null) {
// 发送事件
if (data != null) { safeSendEvent(emitter, "token", data);
// 发送事件
emitter.send(SseEmitter.event().name("token").data(data));
} else {
log.warn("构建token事件数据失败,无法发送事件");
}
} else { } else {
log.debug("SSE emitter已完成,跳过发送token事件"); log.warn("构建token事件数据失败,无法发送事件");
} }
} catch (IllegalStateException e) {
// 处理 emitter 已关闭的情况
log.debug("无法发送token事件,emitter已关闭或完成: {}", e.getMessage());
} catch (Exception e) { } catch (Exception e) {
log.error("发送token事件失败: token长度={}, 错误={}", token.length(), e.getMessage(), e); log.error("发送token事件失败: token长度={}, 错误={}", token.length(), e.getMessage(), e);
} }
} }
/** /**
* 发送错误事件 * 发送错误事件
* *
* @param emitter SSE发射器 * @param emitter SSE发射器
* @param errorMessage 错误信息 * @param errorMessage 错误信息
* @throws IOException IO异常 * @throws IOException IO异常
*/ */
...@@ -762,30 +679,22 @@ public class UserSseService { ...@@ -762,30 +679,22 @@ public class UserSseService {
log.warn("SSE发射器或错误信息为空,无法发送错误事件"); log.warn("SSE发射器或错误信息为空,无法发送错误事件");
return; return;
} }
try { try {
// 检查emitter是否已经完成 // 构建错误事件数据
if (!isEmitterCompleted(emitter)) { Map<String, Object> data = errorEventDataBuilder.createErrorEventData(errorMessage);
// 构建错误事件数据
Map<String, Object> data = errorEventDataBuilder.createErrorEventData(errorMessage); if (data != null) {
// 发送事件
if (data != null) { safeSendEvent(emitter, "error", data);
// 发送事件
emitter.send(SseEmitter.event().name("error").data(data));
} else {
log.warn("构建错误事件数据失败,无法发送事件");
}
} else { } else {
log.debug("SSE emitter已完成,跳过发送错误事件"); log.warn("构建错误事件数据失败,无法发送事件");
} }
} catch (IllegalStateException e) {
// 处理 emitter 已关闭的情况
log.debug("无法发送错误事件,emitter已关闭或完成: {}", e.getMessage());
} catch (Exception e) { } catch (Exception e) {
log.error("发送错误事件失败: 错误信息={}, 错误={}", errorMessage, e.getMessage(), e); log.error("发送错误事件失败: 错误信息={}, 错误={}", errorMessage, e.getMessage(), e);
} }
} }
/** /**
* 获取所有活动的emitters * 获取所有活动的emitters
* *
...@@ -794,7 +703,7 @@ public class UserSseService { ...@@ -794,7 +703,7 @@ public class UserSseService {
public List<SseEmitter> getEmitters() { public List<SseEmitter> getEmitters() {
return new ArrayList<>(emitters); return new ArrayList<>(emitters);
} }
/** /**
* 销毁资源 * 销毁资源
*/ */
......
...@@ -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();
} }
}; };
} }
......
...@@ -4,135 +4,155 @@ import lombok.extern.slf4j.Slf4j; ...@@ -4,135 +4,155 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
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.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.util.StringUtils;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import pangea.hiagent.common.utils.JwtUtil;
import java.lang.InheritableThreadLocal;
/** /**
* 用户相关工具类 * 用户相关工具类
* 提供统一的用户信息获取方法 * 提供统一的用户信息获取方法,支持异步线程安全
*/ */
@Slf4j @Slf4j
@Component @Component
public class UserUtils { public class UserUtils {
// 注入JwtUtil bean private volatile JwtUtil jwtUtil;
private static JwtUtil jwtUtil;
public UserUtils(JwtUtil jwtUtil) { // 使用InheritableThreadLocal存储用户ID,支持异步线程继承
UserUtils.jwtUtil = jwtUtil; private final InheritableThreadLocal<String> USER_ID_THREAD_LOCAL = new InheritableThreadLocal<>();
// 静态Holder模式确保单例
private static class Holder {
private static UserUtils INSTANCE;
} }
public static String getCurrentUserId() { public UserUtils(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
Holder.INSTANCE = this;
}
String username = getCurrentUserIdInSync(); /**
if (username==null || username.isEmpty()) { * 设置当前线程的用户ID
username = getCurrentUserIdInAsync(); * @param userId 用户ID
*/
public void setCurrentUserId(String userId) {
if (StringUtils.hasText(userId)) {
USER_ID_THREAD_LOCAL.set(userId);
log.debug("设置当前线程的用户ID: {}", userId);
} else {
USER_ID_THREAD_LOCAL.remove();
log.debug("清除当前线程的用户ID");
} }
}
/**
* 清除当前线程的用户ID
*/
public void clearCurrentUserId() {
USER_ID_THREAD_LOCAL.remove();
log.debug("清除当前线程的用户ID");
}
return username; /**
* 从ThreadLocal获取用户ID
* @return 用户ID,如果不存在则返回null
*/
public String getCurrentUserIdFromThreadLocal() {
String userId = USER_ID_THREAD_LOCAL.get();
if (userId != null) {
log.debug("从ThreadLocal获取到用户ID: {}", userId);
}
else{
userId="user-001";
}
return userId;
} }
/** /**
* 获取当前认证用户ID * 获取当前认证用户ID
* * 优先从ThreadLocal获取,其次从SecurityContext获取,最后从请求中解析JWT
* @return 用户ID,如果未认证则返回null * @return 用户ID,如果未认证则返回null
*/ */
public static String getCurrentUserIdInSync() { public String getCurrentUserId() {
try { // 优先从ThreadLocal获取(支持异步线程)
// 首先尝试从SecurityContext获取 String userId = getCurrentUserIdFromThreadLocal();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (userId != null) {
if (authentication != null && authentication.isAuthenticated() && authentication.getPrincipal() != null) { return userId;
Object principal = authentication.getPrincipal(); }
if (principal instanceof String) {
String userId = (String) principal;
log.debug("从SecurityContext获取到用户ID: {}", userId);
return userId;
} else {
// 如果principal不是String类型,尝试获取getName()方法的返回值
log.debug("Authentication principal is not a String: {}", principal.getClass().getName());
try {
String userId = principal.toString();
log.debug("将principal转换为字符串获取用户ID: {}", userId);
return userId;
} catch (Exception toStringEx) {
log.warn("无法将principal转换为字符串: {}", toStringEx.getMessage());
}
}
}
// 如果SecurityContext中没有认证信息,尝试从请求中解析JWT令牌 // 从SecurityContext获取
String userId = getUserIdFromRequest(); userId = getCurrentUserIdFromSecurityContext();
if (userId != null) { if (userId != null) {
log.debug("从请求中解析到用户ID: {}", userId); setCurrentUserId(userId);
return userId; return userId;
} }
log.debug("未能获取到有效的用户ID"); // 从请求中解析JWT
return null; userId = getCurrentUserIdFromRequest();
} catch (Exception e) { if (userId != null) {
log.error("获取当前用户ID时发生异常", e); setCurrentUserId(userId);
return null;
} }
return userId;
} }
/** /**
* 在异步线程环境中获取当前认证用户ID * 从SecurityContext获取当前认证用户ID
* 该方法专为异步线程环境设计,通过JWT令牌解析获取用户ID
*
* @return 用户ID,如果未认证则返回null * @return 用户ID,如果未认证则返回null
*/ */
public static String getCurrentUserIdInAsync() { private String getCurrentUserIdFromSecurityContext() {
try { try {
log.debug("在异步线程中尝试获取用户ID"); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated() && !"anonymousUser".equals(authentication.getPrincipal())) {
// 直接从请求中解析JWT令牌获取用户ID Object principal = authentication.getPrincipal();
String userId = getUserIdFromRequest(); if (principal instanceof String) {
if (userId != null) { String userId = (String) principal;
log.debug("在异步线程中成功获取用户ID: {}", userId); log.debug("从SecurityContext获取到用户ID: {}", userId);
return userId; return userId;
} else {
// 尝试获取principal的字符串表示
log.debug("Authentication principal类型: {}", principal.getClass().getName());
String userId = principal.toString();
log.debug("将principal转换为字符串获取用户ID: {}", userId);
return userId;
}
} }
log.debug("在异步线程中未能获取到有效的用户ID");
return null;
} catch (Exception e) { } catch (Exception e) {
log.error("在异步线程中获取用户ID时发生异常", e); log.error("从SecurityContext获取用户ID时发生异常", e);
return null;
} }
return null;
} }
/** /**
* 从当前请求中提取JWT令牌并解析用户ID * 从当前请求中提取JWT令牌并解析用户ID
*
* @return 用户ID,如果无法解析则返回null * @return 用户ID,如果无法解析则返回null
*/ */
private static String getUserIdFromRequest() { private String getCurrentUserIdFromRequest() {
try { try {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) { if (requestAttributes instanceof ServletRequestAttributes) {
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
// 从请求头或参数中提取Token
String token = extractTokenFromRequest(request); String token = extractTokenFromRequest(request);
if (StringUtils.hasText(token) && jwtUtil != null) { if (StringUtils.hasText(token) && getJwtUtil() != null) {
// 验证token是否有效 boolean isValid = getJwtUtil().validateToken(token);
boolean isValid = jwtUtil.validateToken(token);
log.debug("JWT验证结果: {}", isValid); log.debug("JWT验证结果: {}", isValid);
if (isValid) { if (isValid) {
String userId = jwtUtil.getUserIdFromToken(token); String userId = getJwtUtil().getUserIdFromToken(token);
log.debug("从JWT令牌中提取用户ID: {}", userId); log.debug("从JWT令牌中提取用户ID: {}", userId);
return userId; return userId;
} else { } else {
log.warn("JWT验证失败,token可能已过期或无效"); log.warn("JWT验证失败,token可能已过期或无效");
} }
} else { } else {
if (jwtUtil == null) { if (getJwtUtil() == null) {
log.error("jwtUtil未初始化"); log.warn("jwtUtil未初始化");
} else { } else {
log.debug("未找到有效的token"); log.debug("未找到有效的token");
} }
...@@ -143,14 +163,13 @@ public class UserUtils { ...@@ -143,14 +163,13 @@ public class UserUtils {
} catch (Exception e) { } catch (Exception e) {
log.error("从请求中解析用户ID时发生异常", e); log.error("从请求中解析用户ID时发生异常", e);
} }
return null; return null;
} }
/** /**
* 从请求头或参数中提取Token * 从请求头或参数中提取Token
*/ */
private static 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); log.debug("从请求头中提取Authorization: {}", authHeader);
...@@ -172,25 +191,92 @@ public class UserUtils { ...@@ -172,25 +191,92 @@ public class UserUtils {
return null; return null;
} }
/**
* 获取JwtUtil实例,确保线程安全
*/
private JwtUtil getJwtUtil() {
if (jwtUtil == null) {
synchronized (UserUtils.class) {
if (jwtUtil == null) {
log.error("jwtUtil尚未初始化,请确保UserUtils已被Spring容器正确管理");
}
}
}
return jwtUtil;
}
/** /**
* 检查当前用户是否已认证 * 检查当前用户是否已认证
*
* @return true表示已认证,false表示未认证 * @return true表示已认证,false表示未认证
*/ */
public static boolean isAuthenticated() { public boolean isAuthenticated() {
return getCurrentUserId() != null; return getCurrentUserId() != null;
} }
/** /**
* 检查用户是否是管理员 * 检查用户是否是管理员
*
* @param userId 用户ID * @param userId 用户ID
* @return true表示是管理员,false表示不是管理员 * @return true表示是管理员,false表示不是管理员
*/ */
public static boolean isAdminUser(String userId) { public 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 // 以下是静态方法,用于支持静态调用
/**
* 获取UserUtils单例实例
*/
private static UserUtils getInstance() {
UserUtils instance = Holder.INSTANCE;
if (instance == null) {
// 如果还没有初始化,返回默认实现
log.warn("UserUtils实例尚未初始化,使用默认实现");
instance = new UserUtils(null);
}
return instance;
}
/**
* 静态方法:设置当前线程的用户ID
*/
public static void setCurrentUserIdStatic(String userId) {
getInstance().setCurrentUserId(userId);
}
/**
* 静态方法:清除当前线程的用户ID
*/
public static void clearCurrentUserIdStatic() {
getInstance().clearCurrentUserId();
}
/**
* 静态方法:从ThreadLocal获取用户ID
*/
public static String getCurrentUserIdFromThreadLocalStatic() {
return getInstance().getCurrentUserIdFromThreadLocal();
}
/**
* 静态方法:获取当前认证用户ID
*/
public static String getCurrentUserIdStatic() {
return getInstance().getCurrentUserId();
}
/**
* 静态方法:检查当前用户是否已认证
*/
public static boolean isAuthenticatedStatic() {
return getInstance().isAuthenticated();
}
/**
* 静态方法:检查用户是否是管理员
*/
public static boolean isAdminUserStatic(String userId) {
return getInstance().isAdminUser(userId);
}
}
...@@ -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
...@@ -5,20 +5,13 @@ import jakarta.servlet.ServletException; ...@@ -5,20 +5,13 @@ 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 org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import pangea.hiagent.common.utils.JwtUtil;
import pangea.hiagent.web.service.AgentService;
import pangea.hiagent.model.Agent; import pangea.hiagent.model.Agent;
import pangea.hiagent.common.utils.UserUtils; import pangea.hiagent.web.service.AgentService;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.List;
/** /**
* SSE流式端点授权检查过滤器 * SSE流式端点授权检查过滤器
...@@ -32,74 +25,36 @@ public class SseAuthorizationFilter extends OncePerRequestFilter { ...@@ -32,74 +25,36 @@ public class SseAuthorizationFilter extends OncePerRequestFilter {
private static final String STREAM_ENDPOINT = "/api/v1/agent/chat-stream"; private static final String STREAM_ENDPOINT = "/api/v1/agent/chat-stream";
private static final String TIMELINE_ENDPOINT = "/api/v1/agent/timeline-events"; private static final String TIMELINE_ENDPOINT = "/api/v1/agent/timeline-events";
private final JwtUtil jwtUtil;
private final AgentService agentService; private final AgentService agentService;
public SseAuthorizationFilter(JwtUtil jwtUtil, AgentService agentService) { public SseAuthorizationFilter(AgentService agentService) {
this.jwtUtil = jwtUtil;
this.agentService = agentService; this.agentService = agentService;
} }
/** /**
* 发送SSE格式的未授权错误响应 * 发送SSE格式的错误响应
*/ */
private void sendSseUnauthorizedError(HttpServletResponse response) { private void sendSseError(HttpServletResponse response, int status, String errorMessage) {
try { // 检查响应是否已经提交,避免后续错误处理异常
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); if (response.isCommitted()) {
response.setContentType("text/event-stream;charset=UTF-8"); log.warn("响应已提交,无法发送SSE错误响应: {} - {}", status, errorMessage);
response.setCharacterEncoding("UTF-8"); return;
// 发送SSE格式的错误事件
response.getWriter().write("event: error\n");
response.getWriter().write("data: {\"error\": \"未授权访问,请先登录\", \"code\": 401, \"timestamp\": " +
System.currentTimeMillis() + "}\n\n");
response.getWriter().flush();
log.debug("已发送SSE未授权错误响应");
} catch (IOException e) {
log.error("发送SSE未授权错误响应失败", e);
} }
}
/**
* 发送SSE格式的Agent不存在错误响应
*/
private void sendSseAgentNotFoundError(HttpServletResponse response) {
try { try {
response.setStatus(HttpServletResponse.SC_NOT_FOUND); response.setStatus(status);
response.setContentType("text/event-stream;charset=UTF-8"); response.setContentType("text/event-stream;charset=UTF-8");
response.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8");
// 发送SSE格式的错误事件 // 发送SSE格式的错误事件
response.getWriter().write("event: error\n"); response.getWriter().write("event: error\n");
response.getWriter().write("data: {\"error\": \"Agent不存在\", \"code\": 404, \"timestamp\": " + response.getWriter().write("data: {\"error\": \"" + errorMessage + "\", \"code\": " + status + ", \"timestamp\": " +
System.currentTimeMillis() + "}\n\n"); System.currentTimeMillis() + "}\n\n");
response.getWriter().flush(); response.getWriter().flush();
log.debug("已发送SSE Agent不存在错误响应"); log.debug("已发送SSE错误响应: {} - {}", status, errorMessage);
} catch (IOException e) { } catch (IOException e) {
log.error("发送SSE Agent不存在错误响应失败", e); log.error("发送SSE错误响应失败: {}", e.getMessage());
}
}
/**
* 发送SSE格式的访问拒绝错误响应
*/
private void sendSseAccessDeniedError(HttpServletResponse response) {
try {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("text/event-stream;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
// 发送SSE格式的错误事件
response.getWriter().write("event: error\n");
response.getWriter().write("data: {\"error\": \"访问被拒绝,无权限访问该Agent\", \"code\": 403, \"timestamp\": " +
System.currentTimeMillis() + "}\n\n");
response.getWriter().flush();
log.debug("已发送SSE 访问拒绝错误响应");
} catch (IOException e) {
log.error("发送SSE 访问拒绝错误响应失败", e);
} }
} }
...@@ -117,60 +72,61 @@ public class SseAuthorizationFilter extends OncePerRequestFilter { ...@@ -117,60 +72,61 @@ public class SseAuthorizationFilter extends OncePerRequestFilter {
// 检查响应是否已经提交,避免后续错误处理异常 // 检查响应是否已经提交,避免后续错误处理异常
if (response.isCommitted()) { if (response.isCommitted()) {
log.warn("响应已提交,无法处理SSE端点授权检查"); log.warn("响应已提交,无法处理SSE端点授权检查,终止过滤器链");
return; return;
} }
// 从SecurityContext获取当前认证用户 // 从SecurityContext获取当前认证用户
String userId = null; String userId = getCurrentUserId();
var authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated() && !"anonymousUser".equals(authentication.getPrincipal())) {
userId = authentication.getName();
}
if (userId != null) { if (userId != null) {
log.debug("SSE端点已认证,用户: {}", userId); log.debug("SSE端点已认证,用户: {}", userId);
// 如果是chat-stream端点,需要额外验证agent权限 // 如果是chat-stream端点,需要额外验证agent权限
if (isStreamEndpoint) { if (isStreamEndpoint) {
// 从请求参数中获取agentId
String agentId = request.getParameter("agentId"); String agentId = request.getParameter("agentId");
if (agentId != null) { if (agentId != null) {
try { try {
Agent agent = agentService.getAgent(agentId); Agent agent = agentService.getAgent(agentId);
if (agent == null) { if (agent == null) {
log.warn("SSE端点访问失败:Agent不存在 - AgentId: {}", agentId); log.warn("SSE端点访问失败:Agent不存在 - AgentId: {}", agentId);
sendSseAgentNotFoundError(response); sendSseError(response, HttpServletResponse.SC_NOT_FOUND, "Agent不存在");
return; return;
} }
// 验证用户是否有权限访问该agent // 验证用户是否有权限访问该agent
if (!agent.getOwner().equals(userId) && !UserUtils.isAdminUser(userId)) { if (!agent.getOwner().equals(userId) && !isAdminUser(userId)) {
log.warn("SSE端点访问失败:用户 {} 无权限访问Agent: {}", userId, agentId); log.warn("SSE端点访问失败:用户 {} 无权限访问Agent: {}", userId, agentId);
sendSseAccessDeniedError(response); sendSseError(response, HttpServletResponse.SC_FORBIDDEN, "访问被拒绝,无权限访问该Agent");
return; return;
} }
log.debug("SSE端点Agent权限验证成功,用户: {}, Agent: {}", userId, agentId); log.debug("SSE端点Agent权限验证成功,用户: {}, Agent: {}", userId, agentId);
} catch (Exception e) { } catch (Exception e) {
log.error("SSE端点Agent权限验证异常: {}", e.getMessage()); log.error("SSE端点Agent权限验证异常: {}", e.getMessage());
sendSseAccessDeniedError(response); sendSseError(response, HttpServletResponse.SC_FORBIDDEN, "访问被拒绝");
return; return;
} }
} else { } else {
log.warn("SSE端点请求缺少agentId参数"); log.warn("SSE端点请求缺少agentId参数");
sendSseAgentNotFoundError(response); sendSseError(response, HttpServletResponse.SC_NOT_FOUND, "Agent不存在");
return; return;
} }
} }
// 再次检查响应是否已经提交,避免后续过滤器尝试修改已提交的响应
if (response.isCommitted()) {
log.warn("响应已提交,跳过继续执行过滤器链");
return;
}
// 继续执行过滤器链 // 继续执行过滤器链
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} else { } else {
// 用户未认证,拒绝连接 // 用户未认证,拒绝连接
log.warn("SSE端点未认证访问,拒绝连接: {} {}", request.getMethod(), requestUri); log.warn("SSE端点未认证访问,拒绝连接: {} {}", request.getMethod(), requestUri);
sendSseUnauthorizedError(response); sendSseError(response, HttpServletResponse.SC_UNAUTHORIZED, "未授权访问,请先登录");
return; return;
} }
} }
...@@ -180,35 +136,31 @@ public class SseAuthorizationFilter extends OncePerRequestFilter { ...@@ -180,35 +136,31 @@ public class SseAuthorizationFilter extends OncePerRequestFilter {
} }
/** /**
* 从请求头或参数中提取Token * 从SecurityContext获取当前认证用户ID
*/ */
private String extractTokenFromRequest(HttpServletRequest request) { private String getCurrentUserId() {
// 首先尝试从请求头中提取Token var authentication = SecurityContextHolder.getContext().getAuthentication();
String authHeader = request.getHeader("Authorization"); if (authentication != null && authentication.isAuthenticated() && !"anonymousUser".equals(authentication.getPrincipal())) {
if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) { return authentication.getName();
return authHeader.substring(7);
}
// 如果请求头中没有Token,则尝试从URL参数中提取
String tokenParam = request.getParameter("token");
if (StringUtils.hasText(tokenParam)) {
return tokenParam;
} }
return null; return null;
} }
/**
* 检查是否为管理员用户
*/
private boolean isAdminUser(String userId) {
// 与DefaultPermissionEvaluator保持一致的管理员检查逻辑
return "admin".equals(userId) || "user-001".equals(userId);
}
/** /**
* 确定此过滤器是否应处理给定请求 * 确定此过滤器是否应处理给定请求
* 只处理SSE流式端点 * 只处理SSE流式端点
*/ */
@Override @Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { protected boolean shouldNotFilter(HttpServletRequest request) {
String requestUri = request.getRequestURI(); String requestUri = request.getRequestURI();
boolean isStreamEndpoint = requestUri.contains(STREAM_ENDPOINT); return !(requestUri.contains(STREAM_ENDPOINT) || requestUri.contains(TIMELINE_ENDPOINT));
boolean isTimelineEndpoint = requestUri.contains(TIMELINE_ENDPOINT);
// 如果不是SSE端点,跳过此过滤器
return !(isStreamEndpoint || isTimelineEndpoint);
} }
} }
...@@ -12,6 +12,7 @@ import pangea.hiagent.web.service.ToolService; ...@@ -12,6 +12,7 @@ import pangea.hiagent.web.service.ToolService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
...@@ -38,37 +39,29 @@ public class AgentToolManager { ...@@ -38,37 +39,29 @@ public class AgentToolManager {
* @return 工具列表 * @return 工具列表
*/ */
public List<Tool> getAvailableTools(Agent agent) { public List<Tool> getAvailableTools(Agent agent) {
try { log.info("获取Agent可用工具列表,Agent ID: {}, 名称: {}", agent.getId(), agent.getName());
log.info("获取Agent可用工具列表,Agent ID: {}, 名称: {}", agent.getId(), agent.getName());
// 获取与Agent关联的Tool ID列表
// 获取与Agent关联的Tool ID列表 List<String> toolIds = agentToolRelationRepository.getToolIdsByAgentId(agent.getId());
List<String> toolIds = agentToolRelationRepository.getToolIdsByAgentId(agent.getId()); log.info("Agent关联的工具ID数量: {}", toolIds != null ? toolIds.size() : 0);
log.info("Agent关联的工具ID数量: {}", toolIds != null ? toolIds.size() : 0);
// 如果没有关联特定工具,则返回该用户的所有活跃工具
if (toolIds == null || toolIds.isEmpty()) { if (toolIds == null || toolIds.isEmpty()) {
// 如果没有关联特定工具,则返回该用户的所有活跃工具 List<Tool> allTools = toolService.getUserToolsByStatus(agent.getOwner(), "active");
List<Tool> allTools = toolService.getUserToolsByStatus(agent.getOwner(), "active"); log.info("返回用户所有活跃工具,数量: {}", allTools != null ? allTools.size() : 0);
log.info("返回用户所有活跃工具,数量: {}", allTools != null ? allTools.size() : 0); return allTools != null ? allTools : List.of();
return allTools != null ? allTools : List.of();
}
// 根据Tool ID获取具体的Tool对象
List<Tool> tools = new ArrayList<>();
for (String toolId : toolIds) {
Tool tool = toolService.getById(toolId);
if (tool != null) {
tools.add(tool);
}
}
log.info("获取到的具体工具数量: {}", tools.size());
tools.forEach(tool -> log.info("工具名称: {}", tool.getName()));
return tools;
} catch (Exception e) {
log.error("获取Agent可用工具时发生错误", e);
return List.of();
} }
// 根据Tool ID获取具体的Tool对象
List<Tool> tools = toolIds.stream()
.map(toolService::getById)
.filter(Objects::nonNull)
.collect(Collectors.toList());
log.info("获取到的具体工具数量: {}", tools.size());
log.debug("工具列表: {}", tools.stream().map(Tool::getName).collect(Collectors.joining(", ")));
return tools;
} }
/** /**
...@@ -100,34 +93,18 @@ public class AgentToolManager { ...@@ -100,34 +93,18 @@ public class AgentToolManager {
* @return 筛选后的工具实例列表 * @return 筛选后的工具实例列表
*/ */
public List<Object> filterToolsByInstances(List<Object> allTools, Set<String> toolNames) { public List<Object> filterToolsByInstances(List<Object> allTools, Set<String> toolNames) {
log.debug("开始筛选工具实例,工具名称集合: {}", toolNames);
if (toolNames == null || toolNames.isEmpty()) { if (toolNames == null || toolNames.isEmpty()) {
log.debug("工具名称集合为空,返回所有工具实例");
return allTools; return allTools;
} }
List<Object> filteredTools = allTools.stream() return allTools.stream()
.filter(tool -> { .filter(tool -> {
// 获取工具类名(不含包名)
String className = tool.getClass().getSimpleName(); String className = tool.getClass().getSimpleName();
log.debug("检查工具类: {}", className); return toolNames.contains(className) ||
toolNames.stream().anyMatch(name ->
// 检查类名是否匹配 className.toLowerCase().contains(name.toLowerCase()));
boolean isMatch = toolNames.contains(className) ||
toolNames.stream().anyMatch(name ->
className.toLowerCase().contains(name.toLowerCase()));
if (isMatch) {
log.debug("工具 {} 匹配成功", className);
}
return isMatch;
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
log.debug("筛选完成,返回 {} 个工具实例", filteredTools.size());
return filteredTools;
} }
/** /**
...@@ -143,8 +120,9 @@ public class AgentToolManager { ...@@ -143,8 +120,9 @@ public class AgentToolManager {
StringBuilder description = new StringBuilder(); StringBuilder description = new StringBuilder();
for (int i = 0; i < tools.size(); i++) { for (int i = 0; i < tools.size(); i++) {
Tool tool = tools.get(i); Tool tool = tools.get(i);
description.append(i + 1).append(". "); description.append(i + 1).append(". ")
description.append(tool.getName()); .append(tool.getName());
if (hasValue(tool.getDisplayName())) { if (hasValue(tool.getDisplayName())) {
description.append(" - ").append(tool.getDisplayName()); description.append(" - ").append(tool.getDisplayName());
} }
...@@ -168,20 +146,11 @@ public class AgentToolManager { ...@@ -168,20 +146,11 @@ public class AgentToolManager {
/** /**
* 获取Bean的原始目标类(穿透Spring AOP代理) * 获取Bean的原始目标类(穿透Spring AOP代理)
*
* 用于处理以下场景:
* 1. Bean被Spring AOP代理,需要获取原始类信息
* 2. 获取原始类的方法和字段信息
* 3. 进行类型检查和反射操作
*
* @param bean Bean实例(可能是代理对象) * @param bean Bean实例(可能是代理对象)
* @return 原始目标类的Class对象 * @return 原始目标类的Class对象
*/ */
private Class<?> getTargetClass(Object bean) { private Class<?> getTargetClass(Object bean) {
if (bean == null) { return bean == null ? null : AopUtils.getTargetClass(bean);
return null;
}
return AopUtils.getTargetClass(bean);
} }
/** /**
...@@ -209,62 +178,43 @@ public class AgentToolManager { ...@@ -209,62 +178,43 @@ public class AgentToolManager {
return result; return result;
} }
try { List<Tool> availableTools = getAvailableTools(agent);
log.debug("[{}] 根据原始类名'{}' 查找工具实例,精确匹配: {}", agent.getName(), originalClassName, isExactMatch);
for (Tool tool : availableTools) {
if (tool.getBeanName() == null || tool.getBeanName().trim().isEmpty()) {
continue;
}
Object bean;
try {
bean = applicationContext.getBean(tool.getBeanName());
} catch (Exception e) {
log.debug("[{}] 工具'{}' 的Bean查找失败,跳过", agent.getName(), tool.getName());
continue;
}
List<Tool> availableTools = getAvailableTools(agent); if (bean == null) {
continue;
}
for (Tool tool : availableTools) { // 获取原始目标类
try { Class<?> targetClass = getTargetClass(bean);
if (tool.getBeanName() == null || tool.getBeanName().trim().isEmpty()) { if (targetClass == null) {
continue; targetClass = bean.getClass();
}
Object bean = null;
try {
bean = applicationContext.getBean(tool.getBeanName());
} catch (Exception e) {
log.debug("[{}] 工具'{}' 的Bean查找失败,跳过", agent.getName(), tool.getName());
continue;
}
if (bean == null) {
continue;
}
// 获取原始目标类
Class<?> targetClass = getTargetClass(bean);
if (targetClass == null) {
targetClass = bean.getClass();
}
String targetClassName = targetClass.getSimpleName();
String targetFullClassName = targetClass.getName();
// 根据匹配模式进行判断
boolean matches = false;
if (isExactMatch) {
// 精确匹配:检查简单类名和完整类名
matches = originalClassName.equals(targetClassName) ||
originalClassName.equals(targetFullClassName);
} else {
// 模糊匹配:检查是否包含(不区分大小写)
matches = targetClassName.toLowerCase().contains(originalClassName.toLowerCase()) ||
targetFullClassName.toLowerCase().contains(originalClassName.toLowerCase());
}
if (matches) {
result.add(bean);
log.debug("[{}] 根据原始类名'{}' 匹配到工具实例: {}", agent.getName(), originalClassName, targetClassName);
}
} catch (Exception e) {
log.debug("[{}] 处理工具'{}' 时出错", agent.getName(), tool.getName(), e);
}
} }
log.debug("[{}] 根据原始类名'{}' 共找到 {} 个工具实例", agent.getName(), originalClassName, result.size()); String targetClassName = targetClass.getSimpleName();
} catch (Exception e) { String targetFullClassName = targetClass.getName();
log.error("[{}] 根据原始类名查找工具实例时发生错误", agent.getName(), e);
// 根据匹配模式进行判断
boolean matches = isExactMatch
? originalClassName.equals(targetClassName) || originalClassName.equals(targetFullClassName)
: targetClassName.toLowerCase().contains(originalClassName.toLowerCase()) ||
targetFullClassName.toLowerCase().contains(originalClassName.toLowerCase());
if (matches) {
result.add(bean);
}
} }
return result; return result;
...@@ -316,64 +266,54 @@ public class AgentToolManager { ...@@ -316,64 +266,54 @@ public class AgentToolManager {
* @return 工具实例列表(包含AOP代理后的实例) * @return 工具实例列表(包含AOP代理后的实例)
*/ */
public List<Object> getAvailableToolInstances(Agent agent) { public List<Object> getAvailableToolInstances(Agent agent) {
try { log.info("[{}] 开始获取可用的工具实例", agent.getName());
log.info("[{}] 开始获取可用的工具实例", agent.getName());
// 获取Agent可用的工具定义
// 获取Agent可用的工具定义 List<Tool> availableTools = getAvailableTools(agent);
List<Tool> availableTools = getAvailableTools(agent); log.debug("[{}] 获取到了{}个工具定义", agent.getName(), availableTools.size());
log.debug("[{}] 获取到了{}个工具定义", agent.getName(), availableTools.size());
List<Object> toolInstances = new ArrayList<>();
List<Object> toolInstances = new ArrayList<>(); List<String> failedBeans = new ArrayList<>();
List<String> failedBeans = new ArrayList<>();
// 遍历每个工具定义,根据beanName查找Spring Bean实例
// 遍历每个工具定义,根据beanName查找Spring Bean实例 for (Tool tool : availableTools) {
for (Tool tool : availableTools) { // 验证beanName是否为空
try { if (tool.getBeanName() == null || tool.getBeanName().trim().isEmpty()) {
// 验证beanName是否为空 log.warn("[{}] 工具'{}' 没有配置beanName,跳过此工具", agent.getName(), tool.getName());
if (tool.getBeanName() == null || tool.getBeanName().trim().isEmpty()) { failedBeans.add(tool.getName() + " (beanName为null)");
log.warn("[{}] 工具'{}' 没有配置beanName,跳过此工具", agent.getName(), tool.getName()); continue;
failedBeans.add(tool.getName() + " (beanName为null)");
continue;
}
// 根据beanName查找Bean实例
Object bean = null;
try {
bean = applicationContext.getBean(tool.getBeanName());
} catch (Exception e) {
log.warn("[{}] 工具'{}' 查找Bean'{}' 失败,错误消息: {}", agent.getName(), tool.getName(), tool.getBeanName(), e.getMessage());
failedBeans.add(tool.getName() + " (bean'" + tool.getBeanName() + "'不存在)");
continue;
}
if (bean != null) {
// 获取原始目标类(处理Spring AOP代理)
Class<?> targetClass = getTargetClass(bean);
String simpleName = targetClass != null ? targetClass.getSimpleName() : bean.getClass().getSimpleName();
toolInstances.add(bean);
log.debug("[{}] 成功查找工具'{}' 的Bean实例,原始类: {}", agent.getName(), tool.getName(), simpleName);
} else {
log.warn("[{}] 工具'{}' 的Bean实例为null", agent.getName(), tool.getName());
failedBeans.add(tool.getName() + " (bean实例为null)");
}
} catch (Exception e) {
log.error("[{}] 处理工具'{}' 时发生意外错误,详细信息", agent.getName(), tool.getName(), e);
failedBeans.add(tool.getName() + " (异常: " + e.getMessage() + ")");
}
} }
log.info("[{}] 成功获取了{}个工具实例", agent.getName(), toolInstances.size()); // 根据beanName查找Bean实例
Object bean;
// 打印未成功查找的工具(便于故障诊断) try {
if (!failedBeans.isEmpty()) { bean = applicationContext.getBean(tool.getBeanName());
log.warn("[{}] 以下工具无法加载: {}", agent.getName(), failedBeans); } catch (Exception e) {
log.warn("[{}] 工具'{}' 查找Bean'{}' 失败,错误消息: {}", agent.getName(), tool.getName(), tool.getBeanName(), e.getMessage());
failedBeans.add(tool.getName() + " (bean'" + tool.getBeanName() + "'不存在)");
continue;
} }
return toolInstances; if (bean != null) {
} catch (Exception e) { // 获取原始目标类(处理Spring AOP代理)
log.error("[{}] 获取可用的工具实例时发生了意外错误", agent.getName(), e); Class<?> targetClass = getTargetClass(bean);
return List.of(); String simpleName = targetClass != null ? targetClass.getSimpleName() : bean.getClass().getSimpleName();
toolInstances.add(bean);
log.debug("[{}] 成功查找工具'{}' 的Bean实例,原始类: {}", agent.getName(), tool.getName(), simpleName);
} else {
log.warn("[{}] 工具'{}' 的Bean实例为null", agent.getName(), tool.getName());
failedBeans.add(tool.getName() + " (bean实例为null)");
}
} }
log.info("[{}] 成功获取了{}个工具实例", agent.getName(), toolInstances.size());
// 打印未成功查找的工具(便于故障诊断)
if (!failedBeans.isEmpty()) {
log.warn("[{}] 以下工具无法加载: {}", agent.getName(), failedBeans);
}
return toolInstances;
} }
} }
\ No newline at end of file
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());
}
});
} }
} }
...@@ -14,9 +14,12 @@ import java.time.LocalDateTime; ...@@ -14,9 +14,12 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.HashMap;
import java.util.Map;
import pangea.hiagent.tool.BaseTool;
import pangea.hiagent.tool.playwright.PlaywrightManager;
import pangea.hiagent.web.service.ToolConfigService; import pangea.hiagent.web.service.ToolConfigService;
import pangea.hiagent.workpanel.playwright.PlaywrightManager;
/** /**
* 海信LBPM流程审批工具类 * 海信LBPM流程审批工具类
...@@ -25,7 +28,7 @@ import pangea.hiagent.workpanel.playwright.PlaywrightManager; ...@@ -25,7 +28,7 @@ import pangea.hiagent.workpanel.playwright.PlaywrightManager;
*/ */
@Slf4j @Slf4j
@Component @Component
public class HisenseLbpmApprovalTool { public class HisenseLbpmApprovalTool extends BaseTool {
// SSO登录页面URL // SSO登录页面URL
private static final String SSO_LOGIN_URL = "https://sso.hisense.com/login/"; private static final String SSO_LOGIN_URL = "https://sso.hisense.com/login/";
...@@ -62,106 +65,108 @@ public class HisenseLbpmApprovalTool { ...@@ -62,106 +65,108 @@ public class HisenseLbpmApprovalTool {
*/ */
@Tool(description = "处理海信请假审批、自驾车审批、调休审批,需要先使用HisenseSsoLoginTool登录,提供用户ID以区分会话") @Tool(description = "处理海信请假审批、自驾车审批、调休审批,需要先使用HisenseSsoLoginTool登录,提供用户ID以区分会话")
public String processHisenseLeaveApproval(String approvalUrl, String approvalOpinion) { public String processHisenseLeaveApproval(String approvalUrl, String approvalOpinion) {
Map<String, Object> params = new HashMap<>();
String ssoUsername = getSsoUsername(); params.put("approvalUrl", approvalUrl);
log.info("开始为用户 {} 处理海信请假审批,URL: {}", ssoUsername, approvalUrl); params.put("approvalOpinion", approvalOpinion);
long startTime = System.currentTimeMillis();
// 参数校验
if (ssoUsername == null || ssoUsername.isEmpty()) {
String errorMsg = "用户ID不能为空";
log.error(errorMsg);
return errorMsg;
}
if (approvalUrl == null || approvalUrl.isEmpty()) {
String errorMsg = "审批URL不能为空";
log.error(errorMsg);
return errorMsg;
}
if (approvalOpinion == null || approvalOpinion.isEmpty()) {
String errorMsg = "审批意见不能为空";
log.error(errorMsg);
return errorMsg;
}
Page page = null;
try { return execute("processHisenseLeaveApproval", params, () -> {
// 获取用户专用的浏览器上下文 String ssoUsername = getSsoUsername();
BrowserContext userContext = playwrightManager.getUserContext(ssoUsername); log.info("开始为用户 {} 处理海信请假审批,URL: {}", ssoUsername, approvalUrl);
// 创建新页面
page = userContext.newPage();
// 访问审批页面
log.info("正在访问审批页面: {}", approvalUrl);
page.navigate(approvalUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// 检查是否重定向到了SSO登录页面 // 参数校验
String currentUrl = page.url(); if (ssoUsername == null || ssoUsername.isEmpty()) {
log.info("当前页面URL: {}", currentUrl); String errorMsg = "用户ID不能为空";
if (currentUrl.startsWith(SSO_LOGIN_URL)) {
String errorMsg = "用户未登录或会话已过期,请先使用HisenseSsoLoginTool进行登录";
log.error(errorMsg); log.error(errorMsg);
return errorMsg; return errorMsg;
} }
if (approvalUrl == null || approvalUrl.isEmpty()) {
String errorMsg = "审批URL不能为空";
// 等待页面完全加载完成 log.error(errorMsg);
page.waitForLoadState(LoadState.NETWORKIDLE); return errorMsg;
// 等待关键元素加载完成,确保页面完全就绪
try {
page.waitForSelector("input[type='radio'][name*='oprGroup'], input[type='radio'][name*='fdNotifyLevel']",
new Page.WaitForSelectorOptions().setState(com.microsoft.playwright.options.WaitForSelectorState.VISIBLE).setTimeout(5000));
} catch (com.microsoft.playwright.TimeoutError e) {
log.warn("关键审批元素未在预期时间内加载完成,继续执行审批操作");
} }
// 执行审批操作 if (approvalOpinion == null || approvalOpinion.isEmpty()) {
performApprovalOperation(page, approvalOpinion); String errorMsg = "审批意见不能为空";
log.error(errorMsg);
// 截图并保存 return errorMsg;
takeScreenshotAndSave(page, "lbpm_approval_success_" + ssoUsername); }
long endTime = System.currentTimeMillis();
log.info("请假审批处理完成,耗时: {} ms", endTime - startTime);
return "请假审批处理成功"; Page page = null;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
String errorMsg = "请假审批处理失败: " + e.getMessage();
log.error("请假审批处理失败,耗时: {} ms", endTime - startTime, e);
// 如果页面对象存在,截图保存错误页面 try {
if (page != null) { // 获取用户专用的浏览器上下文
try { BrowserContext userContext = playwrightManager.getUserContext(ssoUsername);
takeScreenshotAndSave(page, "lbpm_approval_fail_" + ssoUsername);
} catch (Exception screenshotException) { // 创建新页面
log.warn("截图保存失败: {}", screenshotException.getMessage()); page = userContext.newPage();
// 访问审批页面
log.info("正在访问审批页面: {}", approvalUrl);
page.navigate(approvalUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// 检查是否重定向到了SSO登录页面
String currentUrl = page.url();
log.info("当前页面URL: {}", currentUrl);
if (currentUrl.startsWith(SSO_LOGIN_URL)) {
String errorMsg = "用户未登录或会话已过期,请先使用HisenseSsoLoginTool进行登录";
log.error(errorMsg);
return errorMsg;
} }
}
return errorMsg;
} finally {
// 不立即关闭页面,让服务器完成审批处理 // 等待页面完全加载完成
// 保留页面、上下文和浏览器实例供后续操作使用 page.waitForLoadState(LoadState.NETWORKIDLE);
// 仅在发生异常时才关闭页面
if (page != null) { // 等待关键元素加载完成,确保页面完全就绪
try { try {
// 可以选择保留页面不关闭,或者等待一段时间后关闭 page.waitForSelector("input[type='radio'][name*='oprGroup'], input[type='radio'][name*='fdNotifyLevel']",
// 目前保持原逻辑,但实际使用中可能需要根据业务需求调整 new Page.WaitForSelectorOptions().setState(com.microsoft.playwright.options.WaitForSelectorState.VISIBLE).setTimeout(5000));
log.debug("保留页面实例以等待服务器完成审批处理"); } catch (com.microsoft.playwright.TimeoutError e) {
} catch (Exception e) { log.warn("关键审批元素未在预期时间内加载完成,继续执行审批操作");
log.warn("处理页面实例时发生异常: {}", e.getMessage()); }
// 执行审批操作
performApprovalOperation(page, approvalOpinion);
// 截图并保存
takeScreenshotAndSave(page, "lbpm_approval_success_" + ssoUsername);
log.info("请假审批处理完成");
return "请假审批处理成功";
} catch (Exception e) {
String errorMsg = "请假审批处理失败: " + e.getMessage();
log.error("请假审批处理失败", e);
// 如果页面对象存在,截图保存错误页面
if (page != null) {
try {
takeScreenshotAndSave(page, "lbpm_approval_fail_" + ssoUsername);
} catch (Exception screenshotException) {
log.warn("截图保存失败: {}", screenshotException.getMessage());
}
}
return errorMsg;
} finally {
// 不立即关闭页面,让服务器完成审批处理
// 保留页面、上下文和浏览器实例供后续操作使用
// 仅在发生异常时才关闭页面
if (page != null) {
try {
// 可以选择保留页面不关闭,或者等待一段时间后关闭
// 目前保持原逻辑,但实际使用中可能需要根据业务需求调整
log.debug("保留页面实例以等待服务器完成审批处理");
} catch (Exception e) {
log.warn("处理页面实例时发生异常: {}", e.getMessage());
}
} }
} }
} });
} }
/** /**
...@@ -172,73 +177,74 @@ public class HisenseLbpmApprovalTool { ...@@ -172,73 +177,74 @@ public class HisenseLbpmApprovalTool {
*/ */
@Tool(description = "获取海信LBPM业务系统的网页内容,需要先使用HisenseSsoLoginTool登录") @Tool(description = "获取海信LBPM业务系统的网页内容,需要先使用HisenseSsoLoginTool登录")
public String getHisenseLbpmBusinessSystemContent(String businessSystemUrl) { public String getHisenseLbpmBusinessSystemContent(String businessSystemUrl) {
String ssoUsername = getSsoUsername(); Map<String, Object> params = new HashMap<>();
log.info("开始为用户 {} 获取海信业务系统内容,URL: {}", ssoUsername, businessSystemUrl); params.put("businessSystemUrl", businessSystemUrl);
long startTime = System.currentTimeMillis();
// 参数校验
if (ssoUsername == null || ssoUsername.isEmpty()) {
String errorMsg = "用户ID不能为空";
log.error(errorMsg);
return errorMsg;
}
if (businessSystemUrl == null || businessSystemUrl.isEmpty()) {
String errorMsg = "业务系统URL不能为空";
log.error(errorMsg);
return errorMsg;
}
Page page = null;
try { return execute("getHisenseLbpmBusinessSystemContent", params, () -> {
// 获取用户专用的浏览器上下文 String ssoUsername = getSsoUsername();
BrowserContext userContext = playwrightManager.getUserContext(ssoUsername); log.info("开始为用户 {} 获取海信业务系统内容,URL: {}", ssoUsername, businessSystemUrl);
// 创建新页面
page = userContext.newPage();
// 访问业务系统页面
log.info("正在访问业务系统页面: {}", businessSystemUrl);
page.navigate(businessSystemUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// 检查是否重定向到了SSO登录页面
String currentUrl = page.url();
log.info("当前页面URL: {}", currentUrl);
if (currentUrl.startsWith(SSO_LOGIN_URL)) { // 参数校验
String errorMsg = "用户未登录或会话已过期,请先使用HisenseSsoLoginTool进行登录"; if (ssoUsername == null || ssoUsername.isEmpty()) {
String errorMsg = "用户ID不能为空";
log.error(errorMsg); log.error(errorMsg);
return errorMsg; return errorMsg;
} }
// 提取页面内容 if (businessSystemUrl == null || businessSystemUrl.isEmpty()) {
String content = page.locator("body").innerText(); String errorMsg = "业务系统URL不能为空";
long endTime = System.currentTimeMillis(); log.error(errorMsg);
log.info("成功获取业务系统页面内容,耗时: {} ms", endTime - startTime); return errorMsg;
// 检查是否包含错误信息
if (content.contains("InvalidStateError") && content.contains("setRequestHeader")) {
log.warn("检测到页面中可能存在JavaScript错误,但这不会影响主要功能");
} }
Page page = null;
return content; try {
} catch (Exception e) { // 获取用户专用的浏览器上下文
long endTime = System.currentTimeMillis(); BrowserContext userContext = playwrightManager.getUserContext(ssoUsername);
String errorMsg = "获取海信业务系统内容失败: " + e.getMessage();
log.error("获取海信业务系统内容失败,耗时: {} ms", endTime - startTime, e); // 创建新页面
return errorMsg; page = userContext.newPage();
} finally {
// 释放页面资源,但保留浏览器上下文供后续使用 // 访问业务系统页面
if (page != null) { log.info("正在访问业务系统页面: {}", businessSystemUrl);
try { page.navigate(businessSystemUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
page.close();
} catch (Exception e) { // 检查是否重定向到了SSO登录页面
log.warn("关闭页面时发生异常: {}", e.getMessage()); String currentUrl = page.url();
log.info("当前页面URL: {}", currentUrl);
if (currentUrl.startsWith(SSO_LOGIN_URL)) {
String errorMsg = "用户未登录或会话已过期,请先使用HisenseSsoLoginTool进行登录";
log.error(errorMsg);
return errorMsg;
}
// 提取页面内容
String content = page.locator("body").innerText();
log.info("成功获取业务系统页面内容");
// 检查是否包含错误信息
if (content.contains("InvalidStateError") && content.contains("setRequestHeader")) {
log.warn("检测到页面中可能存在JavaScript错误,但这不会影响主要功能");
}
return content;
} catch (Exception e) {
String errorMsg = "获取海信业务系统内容失败: " + e.getMessage();
log.error("获取海信业务系统内容失败", e);
return errorMsg;
} finally {
// 释放页面资源,但保留浏览器上下文供后续使用
if (page != null) {
try {
page.close();
} catch (Exception e) {
log.warn("关闭页面时发生异常: {}", e.getMessage());
}
} }
} }
} });
} }
/** /**
...@@ -370,118 +376,116 @@ public class HisenseLbpmApprovalTool { ...@@ -370,118 +376,116 @@ public class HisenseLbpmApprovalTool {
*/ */
@Tool(description = "自动查找所有待审批的请假流程的网址,需要先使用HisenseSsoLoginTool登录") @Tool(description = "自动查找所有待审批的请假流程的网址,需要先使用HisenseSsoLoginTool登录")
public List<String> processAllPendingLeaveApprovals() { public List<String> processAllPendingLeaveApprovals() {
String ssoUsername = getSsoUsername(); Map<String, Object> params = new HashMap<>();
log.info("开始为用户 {} 处理所有待审批的请假流程", ssoUsername);
long startTime = System.currentTimeMillis();
int processedCount = 0;
// 参数校验 return execute("processAllPendingLeaveApprovals", params, () -> {
if (ssoUsername == null || ssoUsername.isEmpty()) { String ssoUsername = getSsoUsername();
String errorMsg = "用户ID不能为空"; log.info("开始为用户 {} 处理所有待审批的请假流程", ssoUsername);
log.error(errorMsg);
return List.of(errorMsg);
}
Page page = null;
try {
// 获取用户专用的浏览器上下文
BrowserContext userContext = playwrightManager.getUserContext(ssoUsername);
// 创建新页面 // 参数校验
page = userContext.newPage(); if (ssoUsername == null || ssoUsername.isEmpty()) {
String errorMsg = "用户ID不能为空";
// 访问待审批列表页面
String approvalListUrl = "https://lbpm.hisense.com/km/review/?categoryId=1843775ea85be87f9756e2540e5b20b0&nodeType=CATEGORY#j_path=%2FlistAll&mydoc=all&cri.q=docStatus%3A20%3BfdTemplate%3A1843775ea85be87f9756e2540e5b20b0";
log.info("正在访问待审批列表页面: {}", approvalListUrl);
page.navigate(approvalListUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// 检查是否重定向到了SSO登录页面
String currentUrl = page.url();
log.info("当前页面URL: {}", currentUrl);
if (currentUrl.startsWith(SSO_LOGIN_URL)) {
String errorMsg = "用户未登录或会话已过期,请先使用HisenseSsoLoginTool进行登录";
log.error(errorMsg); log.error(errorMsg);
return List.of(errorMsg); return List.of(errorMsg);
} }
// 等待页面完全加载完成 Page page = null;
page.waitForLoadState(LoadState.NETWORKIDLE);
// 等待待审批项目加载完成
try { try {
page.waitForSelector("span.com_subject", // 获取用户专用的浏览器上下文
new Page.WaitForSelectorOptions().setState(com.microsoft.playwright.options.WaitForSelectorState.VISIBLE).setTimeout(10000)); BrowserContext userContext = playwrightManager.getUserContext(ssoUsername);
} catch (com.microsoft.playwright.TimeoutError e) {
log.info("在预期时间内未找到待审批项目,可能没有待审批的项目"); // 创建新页面
return List.of("没有找到待审批的项目"); page = userContext.newPage();
}
// 访问待审批列表页面
ArrayList<String> urls= new ArrayList<>(); String approvalListUrl = "https://lbpm.hisense.com/km/review/?categoryId=1843775ea85be87f9756e2540e5b20b0&nodeType=CATEGORY#j_path=%2FlistAll&mydoc=all&cri.q=docStatus%3A20%3BfdTemplate%3A1843775ea85be87f9756e2540e5b20b0";
log.info("正在访问待审批列表页面: {}", approvalListUrl);
// 查找所有待审批项目行,这些行包含kmss_fdid属性 page.navigate(approvalListUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
Locator approvalRows = page.locator("tr[kmss_fdid]");
int itemCount = approvalRows.count(); // 检查是否重定向到了SSO登录页面
String currentUrl = page.url();
if (itemCount == 0) { log.info("当前页面URL: {}", currentUrl);
log.info("没有更多待审批项目,处理完成");
return List.of("没有更多待审批项目,处理完成"); if (currentUrl.startsWith(SSO_LOGIN_URL)) {
} String errorMsg = "用户未登录或会话已过期,请先使用HisenseSsoLoginTool进行登录";
log.error(errorMsg);
log.info("找到 {} 个待审批项目", itemCount); return List.of(errorMsg);
// 遍历所有待审批项目行,提取kmss_fdid属性值并构建审批URL
for (int i = 0; i < itemCount; i++) {
Locator currentApprovalRow = approvalRows.nth(i);
// 获取kmss_fdid属性值
String kmssFdid = currentApprovalRow.getAttribute("kmss_fdid");
if (kmssFdid != null && !kmssFdid.isEmpty()) {
// 构建完整的审批URL
String approvalUrl = "https://lbpm.hisense.com/km/review/km_review_main/kmReviewMain.do?method=view&fdId=" + kmssFdid;
// 获取审批项目文本(从span.com_subject中获取)
Locator approvalSubject = currentApprovalRow.locator("span.com_subject");
String approvalText = approvalSubject.count() > 0 ? approvalSubject.textContent() : "未知审批项目";
log.info("获取到待审批项目: {},链接: {}", approvalText, approvalUrl);
urls.add(approvalUrl);
}
}
long endTime = System.currentTimeMillis();
String resultMessage = String.format("待审批处理完成,共 %d 个项目,耗时: %d ms", processedCount, endTime - startTime);
log.info(resultMessage);
return urls;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
String errorMsg = "处理待审批项目失败: " + e.getMessage();
log.error("处理待审批项目失败,耗时: {} ms", endTime - startTime, e);
// 如果页面对象存在,截图保存错误页面
if (page != null) {
try {
takeScreenshotAndSave(page, "lbpm_pending_approval_fail_" + ssoUsername);
} catch (Exception screenshotException) {
log.warn("截图保存失败: {}", screenshotException.getMessage());
} }
}
// 等待页面完全加载完成
return List.of(errorMsg); page.waitForLoadState(LoadState.NETWORKIDLE);
} finally {
// 不立即关闭页面,让服务器完成审批处理 // 等待待审批项目加载完成
// 保留页面、上下文和浏览器实例供后续操作使用
// 仅在发生异常时才关闭页面
if (page != null) {
try { try {
log.debug("保留页面实例以等待服务器完成审批处理"); page.waitForSelector("span.com_subject",
} catch (Exception e) { new Page.WaitForSelectorOptions().setState(com.microsoft.playwright.options.WaitForSelectorState.VISIBLE).setTimeout(10000));
log.warn("处理页面实例时发生异常: {}", e.getMessage()); } catch (com.microsoft.playwright.TimeoutError e) {
log.info("在预期时间内未找到待审批项目,可能没有待审批的项目");
return List.of("没有找到待审批的项目");
}
ArrayList<String> urls= new ArrayList<>();
// 查找所有待审批项目行,这些行包含kmss_fdid属性
Locator approvalRows = page.locator("tr[kmss_fdid]");
int itemCount = approvalRows.count();
if (itemCount == 0) {
log.info("没有更多待审批项目,处理完成");
return List.of("没有更多待审批项目,处理完成");
}
log.info("找到 {} 个待审批项目", itemCount);
// 遍历所有待审批项目行,提取kmss_fdid属性值并构建审批URL
for (int i = 0; i < itemCount; i++) {
Locator currentApprovalRow = approvalRows.nth(i);
// 获取kmss_fdid属性值
String kmssFdid = currentApprovalRow.getAttribute("kmss_fdid");
if (kmssFdid != null && !kmssFdid.isEmpty()) {
// 构建完整的审批URL
String approvalUrl = "https://lbpm.hisense.com/km/review/km_review_main/kmReviewMain.do?method=view&fdId=" + kmssFdid;
// 获取审批项目文本(从span.com_subject中获取)
Locator approvalSubject = currentApprovalRow.locator("span.com_subject");
String approvalText = approvalSubject.count() > 0 ? approvalSubject.textContent() : "未知审批项目";
log.info("获取到待审批项目: {},链接: {}", approvalText, approvalUrl);
urls.add(approvalUrl);
}
}
log.info("待审批处理完成");
return urls;
} catch (Exception e) {
String errorMsg = "处理待审批项目失败: " + e.getMessage();
log.error("处理待审批项目失败", e);
// 如果页面对象存在,截图保存错误页面
if (page != null) {
try {
takeScreenshotAndSave(page, "lbpm_pending_approval_fail_" + ssoUsername);
} catch (Exception screenshotException) {
log.warn("截图保存失败: {}", screenshotException.getMessage());
}
}
return List.of(errorMsg);
} finally {
// 不立即关闭页面,让服务器完成审批处理
// 保留页面、上下文和浏览器实例供后续操作使用
// 仅在发生异常时才关闭页面
if (page != null) {
try {
log.debug("保留页面实例以等待服务器完成审批处理");
} catch (Exception e) {
log.warn("处理页面实例时发生异常: {}", e.getMessage());
}
} }
} }
} });
} }
} }
...@@ -14,9 +14,12 @@ import java.time.LocalDateTime; ...@@ -14,9 +14,12 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.HashMap;
import java.util.Map;
import pangea.hiagent.tool.BaseTool;
import pangea.hiagent.tool.playwright.PlaywrightManager;
import pangea.hiagent.web.service.ToolConfigService; import pangea.hiagent.web.service.ToolConfigService;
import pangea.hiagent.workpanel.playwright.PlaywrightManager;
/** /**
* 海信绩效系统流程审批工具类 * 海信绩效系统流程审批工具类
...@@ -25,7 +28,7 @@ import pangea.hiagent.workpanel.playwright.PlaywrightManager; ...@@ -25,7 +28,7 @@ import pangea.hiagent.workpanel.playwright.PlaywrightManager;
*/ */
@Slf4j @Slf4j
@Component @Component
public class HisensePerformanceApprovalTool { public class HisensePerformanceApprovalTool extends BaseTool {
// SSO登录页面URL // SSO登录页面URL
private static final String SSO_LOGIN_URL = "https://sso.hisense.com/login/"; private static final String SSO_LOGIN_URL = "https://sso.hisense.com/login/";
...@@ -60,163 +63,163 @@ public class HisensePerformanceApprovalTool { ...@@ -60,163 +63,163 @@ public class HisensePerformanceApprovalTool {
*/ */
@Tool(description = "自动查找所有待审批的绩效流程的网址,需要先使用HisenseSsoLoginTool登录") @Tool(description = "自动查找所有待审批的绩效流程的网址,需要先使用HisenseSsoLoginTool登录")
public List<String> checkHisensePerformancePendingTasks() { public List<String> checkHisensePerformancePendingTasks() {
String ssoUsername = getSsoUsername(); Map<String, Object> params = new HashMap<>();
log.info("开始为用户 {} 查找所有待审批的绩效流程", ssoUsername);
long startTime = System.currentTimeMillis(); return execute("checkHisensePerformancePendingTasks", params, () -> {
String ssoUsername = getSsoUsername();
// 参数校验 log.info("开始为用户 {} 查找所有待审批的绩效流程", ssoUsername);
if (ssoUsername == null || ssoUsername.isEmpty()) {
String errorMsg = "用户ID不能为空";
log.error(errorMsg);
return List.of(errorMsg);
}
Page page = null;
try {
// 获取用户专用的浏览器上下文
BrowserContext userContext = playwrightManager.getUserContext(ssoUsername);
// 创建新页面
page = userContext.newPage();
// 访问待审批列表页面
log.info("正在访问绩效系统待审批页面: {}", PERFORMANCE_PENDING_URL);
page.navigate(PERFORMANCE_PENDING_URL, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// 检查是否重定向到了SSO登录页面
String currentUrl = page.url();
log.info("当前页面URL: {}", currentUrl);
if (currentUrl.startsWith(SSO_LOGIN_URL)) { // 参数校验
String errorMsg = "用户未登录或会话已过期,请先使用HisenseSsoLoginTool进行登录"; if (ssoUsername == null || ssoUsername.isEmpty()) {
String errorMsg = "用户ID不能为空";
log.error(errorMsg); log.error(errorMsg);
return List.of(errorMsg); return List.of(errorMsg);
} }
// 等待页面完全加载完成 Page page = null;
page.waitForLoadState(LoadState.NETWORKIDLE);
// 等待按钮加载完成
try { try {
page.waitForSelector("button[data-v-991781fe] span", // 获取用户专用的浏览器上下文
new Page.WaitForSelectorOptions().setState(com.microsoft.playwright.options.WaitForSelectorState.VISIBLE).setTimeout(10000)); BrowserContext userContext = playwrightManager.getUserContext(ssoUsername);
} catch (com.microsoft.playwright.TimeoutError e) {
log.info("在预期时间内未找到待审批项目,可能没有待审批的流程"); // 创建新页面
return List.of("没有找到待审批的流程"); page = userContext.newPage();
}
// 访问待审批列表页面
ArrayList<String> urls = new ArrayList<>(); log.info("正在访问绩效系统待审批页面: {}", PERFORMANCE_PENDING_URL);
page.navigate(PERFORMANCE_PENDING_URL, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// 查找所有"审核"按钮(第一种)
Locator auditButtons = page.locator("button[data-v-991781fe][type='button']:has(span:text('审核'))"); // 检查是否重定向到了SSO登录页面
int auditButtonCount = auditButtons.count(); String currentUrl = page.url();
log.info("找到 {} 个'审核'按钮", auditButtonCount); log.info("当前页面URL: {}", currentUrl);
// 查找所有"复评"按钮(第二种) if (currentUrl.startsWith(SSO_LOGIN_URL)) {
Locator reviewButtons = page.locator("button[data-v-991781fe][type='button']:has(span:text('复评'))"); String errorMsg = "用户未登录或会话已过期,请先使用HisenseSsoLoginTool进行登录";
int reviewButtonCount = reviewButtons.count(); log.error(errorMsg);
log.info("找到 {} 个'复评'按钮", reviewButtonCount); return List.of(errorMsg);
}
// 处理"审核"按钮,获取跳转地址
for (int i = 0; i < auditButtonCount; i++) { // 等待页面完全加载完成
page.waitForLoadState(LoadState.NETWORKIDLE);
// 等待按钮加载完成
try { try {
Locator auditButton = auditButtons.nth(i); page.waitForSelector("button[data-v-991781fe] span",
new Page.WaitForSelectorOptions().setState(com.microsoft.playwright.options.WaitForSelectorState.VISIBLE).setTimeout(10000));
// 尝试获取按钮的href属性或从父元素获取 } catch (com.microsoft.playwright.TimeoutError e) {
String href = auditButton.getAttribute("href"); log.info("在预期时间内未找到待审批项目,可能没有待审批的流程");
if (href != null && !href.isEmpty()) { return List.of("没有找到待审批的流程");
log.info("获取到审核流程URL: {}", href); }
urls.add(href);
} else { ArrayList<String> urls = new ArrayList<>();
// 如果按钮没有href属性,点击按钮后获取新页面URL
log.debug("审核按钮无href属性,将通过点击获取URL"); // 查找所有"审核"按钮(第一种)
Locator auditButtons = page.locator("button[data-v-991781fe][type='button']:has(span:text('审核'))");
// 监听新的页面打开事件 int auditButtonCount = auditButtons.count();
Page newPage = page.context().waitForPage(() -> { log.info("找到 {} 个'审核'按钮", auditButtonCount);
auditButton.click();
}); // 查找所有"复评"按钮(第二种)
Locator reviewButtons = page.locator("button[data-v-991781fe][type='button']:has(span:text('复评'))");
String newPageUrl = newPage.url(); int reviewButtonCount = reviewButtons.count();
log.info("获取到审核流程URL (通过点击): {}", newPageUrl); log.info("找到 {} 个'复评'按钮", reviewButtonCount);
urls.add(newPageUrl);
// 处理"审核"按钮,获取跳转地址
for (int i = 0; i < auditButtonCount; i++) {
try {
Locator auditButton = auditButtons.nth(i);
// 关闭新页面,返回原页面 // 尝试获取按钮的href属性或从父元素获取
newPage.close(); String href = auditButton.getAttribute("href");
page.bringToFront(); if (href != null && !href.isEmpty()) {
log.info("获取到审核流程URL: {}", href);
urls.add(href);
} else {
// 如果按钮没有href属性,点击按钮后获取新页面URL
log.debug("审核按钮无href属性,将通过点击获取URL");
// 监听新的页面打开事件
Page newPage = page.context().waitForPage(() -> {
auditButton.click();
});
String newPageUrl = newPage.url();
log.info("获取到审核流程URL (通过点击): {}", newPageUrl);
urls.add(newPageUrl);
// 关闭新页面,返回原页面
newPage.close();
page.bringToFront();
}
} catch (Exception e) {
log.warn("处理审核按钮时发生异常: {}", e.getMessage());
} }
} catch (Exception e) {
log.warn("处理审核按钮时发生异常: {}", e.getMessage());
} }
}
// 处理"复评"按钮,获取跳转地址
// 处理"复评"按钮,获取跳转地址 for (int i = 0; i < reviewButtonCount; i++) {
for (int i = 0; i < reviewButtonCount; i++) { try {
try { Locator reviewButton = reviewButtons.nth(i);
Locator reviewButton = reviewButtons.nth(i);
// 尝试获取按钮的href属性或从父元素获取
String href = reviewButton.getAttribute("href");
if (href != null && !href.isEmpty()) {
log.info("获取到复评流程URL: {}", href);
urls.add(href);
} else {
// 如果按钮没有href属性,点击按钮后获取新页面URL
log.debug("复评按钮无href属性,将通过点击获取URL");
// 监听新的页面打开事件
Page newPage = page.context().waitForPage(() -> {
reviewButton.click();
});
String newPageUrl = newPage.url();
log.info("获取到复评流程URL (通过点击): {}", newPageUrl);
urls.add(newPageUrl);
// 关闭新页面,返回原页面 // 尝试获取按钮的href属性或从父元素获取
newPage.close(); String href = reviewButton.getAttribute("href");
page.bringToFront(); if (href != null && !href.isEmpty()) {
log.info("获取到复评流程URL: {}", href);
urls.add(href);
} else {
// 如果按钮没有href属性,点击按钮后获取新页面URL
log.debug("复评按钮无href属性,将通过点击获取URL");
// 监听新的页面打开事件
Page newPage = page.context().waitForPage(() -> {
reviewButton.click();
});
String newPageUrl = newPage.url();
log.info("获取到复评流程URL (通过点击): {}", newPageUrl);
urls.add(newPageUrl);
// 关闭新页面,返回原页面
newPage.close();
page.bringToFront();
}
} catch (Exception e) {
log.warn("处理复评按钮时发生异常: {}", e.getMessage());
} }
} catch (Exception e) {
log.warn("处理复评按钮时发生异常: {}", e.getMessage());
} }
}
log.info("待审批流程查找完成,共 {} 个流程", urls.size());
long endTime = System.currentTimeMillis();
log.info("待审批流程查找完成,共 {} 个流程,耗时: {} ms", urls.size(), endTime - startTime); if (urls.isEmpty()) {
return List.of("没有找到待审批的流程");
if (urls.isEmpty()) {
return List.of("没有找到待审批的流程");
}
return urls;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
String errorMsg = "查找待审批流程失败: " + e.getMessage();
log.error("查找待审批流程失败,耗时: {} ms", endTime - startTime, e);
// 如果页面对象存在,截图保存错误页面
if (page != null) {
try {
takeScreenshotAndSave(page, "performance_pending_fail_" + ssoUsername);
} catch (Exception screenshotException) {
log.warn("截图保存失败: {}", screenshotException.getMessage());
} }
}
return urls;
return List.of(errorMsg); } catch (Exception e) {
} finally { String errorMsg = "查找待审批流程失败: " + e.getMessage();
// 不立即关闭页面,让服务器完成处理 log.error("查找待审批流程失败", e);
// 保留页面、上下文和浏览器实例供后续操作使用
if (page != null) { // 如果页面对象存在,截图保存错误页面
try { if (page != null) {
log.debug("保留页面实例供后续使用"); try {
} catch (Exception e) { takeScreenshotAndSave(page, "performance_pending_fail_" + ssoUsername);
log.warn("处理页面实例时发生异常: {}", e.getMessage()); } catch (Exception screenshotException) {
log.warn("截图保存失败: {}", screenshotException.getMessage());
}
}
return List.of(errorMsg);
} finally {
// 不立即关闭页面,让服务器完成处理
// 保留页面、上下文和浏览器实例供后续操作使用
if (page != null) {
try {
log.debug("保留页面实例供后续使用");
} catch (Exception e) {
log.warn("处理页面实例时发生异常: {}", e.getMessage());
}
} }
} }
} });
} }
/** /**
...@@ -227,71 +230,72 @@ public class HisensePerformanceApprovalTool { ...@@ -227,71 +230,72 @@ public class HisensePerformanceApprovalTool {
*/ */
@Tool(description = "获取海信绩效系统的审批页面内容,需要先使用HisenseSsoLoginTool登录") @Tool(description = "获取海信绩效系统的审批页面内容,需要先使用HisenseSsoLoginTool登录")
public String getHisensePerformancePageContent(String approvalUrl) { public String getHisensePerformancePageContent(String approvalUrl) {
String ssoUsername = getSsoUsername(); Map<String, Object> params = new HashMap<>();
log.info("开始为用户 {} 获取绩效审批页面内容,URL: {}", ssoUsername, approvalUrl); params.put("approvalUrl", approvalUrl);
long startTime = System.currentTimeMillis();
// 参数校验
if (ssoUsername == null || ssoUsername.isEmpty()) {
String errorMsg = "用户ID不能为空";
log.error(errorMsg);
return errorMsg;
}
if (approvalUrl == null || approvalUrl.isEmpty()) { return execute("getHisensePerformancePageContent", params, () -> {
String errorMsg = "审批页面URL不能为空"; String ssoUsername = getSsoUsername();
log.error(errorMsg); log.info("开始为用户 {} 获取绩效审批页面内容,URL: {}", ssoUsername, approvalUrl);
return errorMsg;
}
Page page = null;
try {
// 获取用户专用的浏览器上下文
BrowserContext userContext = playwrightManager.getUserContext(ssoUsername);
// 创建新页面
page = userContext.newPage();
// 访问审批页面
log.info("正在访问审批页面: {}", approvalUrl);
page.navigate(approvalUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// 检查是否重定向到了SSO登录页面
String currentUrl = page.url();
log.info("当前页面URL: {}", currentUrl);
if (currentUrl.startsWith(SSO_LOGIN_URL)) { // 参数校验
String errorMsg = "用户未登录或会话已过期,请先使用HisenseSsoLoginTool进行登录"; if (ssoUsername == null || ssoUsername.isEmpty()) {
String errorMsg = "用户ID不能为空";
log.error(errorMsg); log.error(errorMsg);
return errorMsg; return errorMsg;
} }
// 等待页面完全加载完成 if (approvalUrl == null || approvalUrl.isEmpty()) {
page.waitForLoadState(LoadState.NETWORKIDLE); String errorMsg = "审批页面URL不能为空";
log.error(errorMsg);
// 提取页面内容 return errorMsg;
String content = page.locator("body").innerText(); }
long endTime = System.currentTimeMillis();
log.info("成功获取绩效审批页面内容,耗时: {} ms", endTime - startTime); Page page = null;
return content; try {
} catch (Exception e) { // 获取用户专用的浏览器上下文
long endTime = System.currentTimeMillis(); BrowserContext userContext = playwrightManager.getUserContext(ssoUsername);
String errorMsg = "获取绩效审批页面内容失败: " + e.getMessage();
log.error("获取绩效审批页面内容失败,耗时: {} ms", endTime - startTime, e); // 创建新页面
return errorMsg; page = userContext.newPage();
} finally {
// 释放页面资源,但保留浏览器上下文供后续使用 // 访问审批页面
if (page != null) { log.info("正在访问审批页面: {}", approvalUrl);
try { page.navigate(approvalUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
page.close();
} catch (Exception e) { // 检查是否重定向到了SSO登录页面
log.warn("关闭页面时发生异常: {}", e.getMessage()); String currentUrl = page.url();
log.info("当前页面URL: {}", currentUrl);
if (currentUrl.startsWith(SSO_LOGIN_URL)) {
String errorMsg = "用户未登录或会话已过期,请先使用HisenseSsoLoginTool进行登录";
log.error(errorMsg);
return errorMsg;
}
// 等待页面完全加载完成
page.waitForLoadState(LoadState.NETWORKIDLE);
// 提取页面内容
String content = page.locator("body").innerText();
log.info("成功获取绩效审批页面内容");
return content;
} catch (Exception e) {
String errorMsg = "获取绩效审批页面内容失败: " + e.getMessage();
log.error("获取绩效审批页面内容失败", e);
return errorMsg;
} finally {
// 释放页面资源,但保留浏览器上下文供后续使用
if (page != null) {
try {
page.close();
} catch (Exception e) {
log.warn("关闭页面时发生异常: {}", e.getMessage());
}
} }
} }
} });
} }
/** /**
...@@ -304,100 +308,103 @@ public class HisensePerformanceApprovalTool { ...@@ -304,100 +308,103 @@ public class HisensePerformanceApprovalTool {
*/ */
@Tool(description = "处理绩效系统单个审批流程,需要先使用HisenseSsoLoginTool登录") @Tool(description = "处理绩效系统单个审批流程,需要先使用HisenseSsoLoginTool登录")
public String performSinglePerformanceApproval(String approvalUrl, boolean isApproved, String approvalOpinion) { public String performSinglePerformanceApproval(String approvalUrl, boolean isApproved, String approvalOpinion) {
String ssoUsername = getSsoUsername(); Map<String, Object> params = new HashMap<>();
log.info("开始为用户 {} 处理绩效审批,URL: {}, 是否通过: {}", ssoUsername, approvalUrl, isApproved); params.put("approvalUrl", approvalUrl);
params.put("isApproved", isApproved);
params.put("approvalOpinion", approvalOpinion);
long startTime = System.currentTimeMillis(); return execute("performSinglePerformanceApproval", params, () -> {
String ssoUsername = getSsoUsername();
// 参数校验 log.info("开始为用户 {} 处理绩效审批,URL: {}, 是否通过: {}", ssoUsername, approvalUrl, isApproved);
if (ssoUsername == null || ssoUsername.isEmpty()) {
String errorMsg = "用户ID不能为空";
log.error(errorMsg);
return errorMsg;
}
if (approvalUrl == null || approvalUrl.isEmpty()) {
String errorMsg = "审批页面URL不能为空";
log.error(errorMsg);
return errorMsg;
}
if (approvalOpinion == null || approvalOpinion.isEmpty()) {
String errorMsg = "审批意见不能为空";
log.error(errorMsg);
return errorMsg;
}
Page page = null;
try {
// 获取用户专用的浏览器上下文
BrowserContext userContext = playwrightManager.getUserContext(ssoUsername);
// 创建新页面
page = userContext.newPage();
// 访问审批页面 // 参数校验
log.info("正在访问审批页面: {}", approvalUrl); if (ssoUsername == null || ssoUsername.isEmpty()) {
page.navigate(approvalUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE)); String errorMsg = "用户ID不能为空";
// 检查是否重定向到了SSO登录页面
String currentUrl = page.url();
log.info("当前页面URL: {}", currentUrl);
if (currentUrl.startsWith(SSO_LOGIN_URL)) {
String errorMsg = "用户未登录或会话已过期,请先使用HisenseSsoLoginTool进行登录";
log.error(errorMsg); log.error(errorMsg);
return errorMsg; return errorMsg;
} }
// 等待页面完全加载完成 if (approvalUrl == null || approvalUrl.isEmpty()) {
page.waitForLoadState(LoadState.NETWORKIDLE); String errorMsg = "审批页面URL不能为空";
log.error(errorMsg);
// 等待关键元素加载完成,确保页面完全就绪 return errorMsg;
try {
page.waitForSelector("input[name='radioGroup'][type='radio'][class='ant-radio-input']",
new Page.WaitForSelectorOptions().setState(com.microsoft.playwright.options.WaitForSelectorState.VISIBLE).setTimeout(5000));
} catch (com.microsoft.playwright.TimeoutError e) {
log.warn("关键审批元素未在预期时间内加载完成,继续执行审批操作");
} }
// 执行审批操作 if (approvalOpinion == null || approvalOpinion.isEmpty()) {
performPerformanceApprovalOperation(page, isApproved, approvalOpinion); String errorMsg = "审批意见不能为空";
log.error(errorMsg);
// 截图并保存 return errorMsg;
takeScreenshotAndSave(page, "performance_approval_success_" + ssoUsername); }
long endTime = System.currentTimeMillis();
log.info("绩效审批处理完成,耗时: {} ms", endTime - startTime);
return "绩效审批处理成功"; Page page = null;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
String errorMsg = "绩效审批处理失败: " + e.getMessage();
log.error("绩效审批处理失败,耗时: {} ms", endTime - startTime, e);
// 如果页面对象存在,截图保存错误页面 try {
if (page != null) { // 获取用户专用的浏览器上下文
try { BrowserContext userContext = playwrightManager.getUserContext(ssoUsername);
takeScreenshotAndSave(page, "performance_approval_fail_" + ssoUsername);
} catch (Exception screenshotException) { // 创建新页面
log.warn("截图保存失败: {}", screenshotException.getMessage()); page = userContext.newPage();
// 访问审批页面
log.info("正在访问审批页面: {}", approvalUrl);
page.navigate(approvalUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// 检查是否重定向到了SSO登录页面
String currentUrl = page.url();
log.info("当前页面URL: {}", currentUrl);
if (currentUrl.startsWith(SSO_LOGIN_URL)) {
String errorMsg = "用户未登录或会话已过期,请先使用HisenseSsoLoginTool进行登录";
log.error(errorMsg);
return errorMsg;
} }
}
// 等待页面完全加载完成
return errorMsg; page.waitForLoadState(LoadState.NETWORKIDLE);
} finally {
// 不立即关闭页面,让服务器完成审批处理 // 等待关键元素加载完成,确保页面完全就绪
// 保留页面、上下文和浏览器实例供后续操作使用
if (page != null) {
try { try {
log.debug("保留页面实例以等待服务器完成审批处理"); page.waitForSelector("input[name='radioGroup'][type='radio'][class='ant-radio-input']",
} catch (Exception e) { new Page.WaitForSelectorOptions().setState(com.microsoft.playwright.options.WaitForSelectorState.VISIBLE).setTimeout(5000));
log.warn("处理页面实例时发生异常: {}", e.getMessage()); } catch (com.microsoft.playwright.TimeoutError e) {
log.warn("关键审批元素未在预期时间内加载完成,继续执行审批操作");
}
// 执行审批操作
performPerformanceApprovalOperation(page, isApproved, approvalOpinion);
// 截图并保存
takeScreenshotAndSave(page, "performance_approval_success_" + ssoUsername);
log.info("绩效审批处理完成");
return "绩效审批处理成功";
} catch (Exception e) {
String errorMsg = "绩效审批处理失败: " + e.getMessage();
log.error("绩效审批处理失败", e);
// 如果页面对象存在,截图保存错误页面
if (page != null) {
try {
takeScreenshotAndSave(page, "performance_approval_fail_" + ssoUsername);
} catch (Exception screenshotException) {
log.warn("截图保存失败: {}", screenshotException.getMessage());
}
}
return errorMsg;
} finally {
// 不立即关闭页面,让服务器完成审批处理
// 保留页面、上下文和浏览器实例供后续操作使用
if (page != null) {
try {
log.debug("保留页面实例以等待服务器完成审批处理");
} catch (Exception e) {
log.warn("处理页面实例时发生异常: {}", e.getMessage());
}
} }
} }
} });
} }
/** /**
......
...@@ -13,8 +13,11 @@ import jakarta.annotation.PreDestroy; ...@@ -13,8 +13,11 @@ import jakarta.annotation.PreDestroy;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.HashMap;
import java.util.Map;
import pangea.hiagent.tool.BaseTool;
import pangea.hiagent.tool.playwright.PlaywrightManager;
import pangea.hiagent.web.service.ToolConfigService; import pangea.hiagent.web.service.ToolConfigService;
import pangea.hiagent.workpanel.playwright.PlaywrightManager;
/** /**
* 海信SSO认证工具类 * 海信SSO认证工具类
...@@ -22,7 +25,7 @@ import pangea.hiagent.workpanel.playwright.PlaywrightManager; ...@@ -22,7 +25,7 @@ import pangea.hiagent.workpanel.playwright.PlaywrightManager;
*/ */
@Slf4j @Slf4j
@Component @Component
public class HisenseSsoLoginTool { public class HisenseSsoLoginTool extends BaseTool {
// SSO登录页面URL // SSO登录页面URL
private static final String SSO_LOGIN_URL = "https://sso.hisense.com/login/"; private static final String SSO_LOGIN_URL = "https://sso.hisense.com/login/";
...@@ -176,99 +179,100 @@ public class HisenseSsoLoginTool { ...@@ -176,99 +179,100 @@ public class HisenseSsoLoginTool {
@Tool(description = "获取任意海信业务系统的网页内容(自动处理SSO认证)") @Tool(description = "获取任意海信业务系统的网页内容(自动处理SSO认证)")
public String getHisenseBusinessSystemContent( public String getHisenseBusinessSystemContent(
@JsonPropertyDescription("海信业务系统的页面URL") String businessSystemUrl) { @JsonPropertyDescription("海信业务系统的页面URL") String businessSystemUrl) {
// initializeIfNeeded(); Map<String, Object> params = new HashMap<>();
log.info("开始获取海信业务系统内容,URL: {}", businessSystemUrl); params.put("businessSystemUrl", businessSystemUrl);
String ssoUsername = getUserName(); return execute("getHisenseBusinessSystemContent", params, () -> {
String ssoPassword = getPassword(); // initializeIfNeeded();
// 校验SSO凭证是否配置 log.info("开始获取海信业务系统内容,URL: {}", businessSystemUrl);
if (ssoUsername == null || ssoUsername.isEmpty() || ssoPassword == null || ssoPassword.isEmpty()) {
String errorMsg = "SSO用户名或密码未配置,海信SSO工具不可用"; String ssoUsername = getUserName();
log.warn(errorMsg); String ssoPassword = getPassword();
return errorMsg; // 校验SSO凭证是否配置
} if (ssoUsername == null || ssoUsername.isEmpty() || ssoPassword == null || ssoPassword.isEmpty()) {
String errorMsg = "SSO用户名或密码未配置,海信SSO工具不可用";
long startTime = System.currentTimeMillis(); log.warn(errorMsg);
return errorMsg;
// 参数校验 }
if (businessSystemUrl == null || businessSystemUrl.isEmpty()) {
String errorMsg = "业务系统URL不能为空";
log.error(errorMsg);
return errorMsg;
}
Page page = null;
try {
// 检查是否已有有效的登录会话
boolean sessionValid = isSessionLoggedIn() && validateSession(businessSystemUrl);
if (sessionValid) { // 参数校验
log.info("检测到有效会话,直接使用共享上下文"); if (businessSystemUrl == null || businessSystemUrl.isEmpty()) {
page = getUSerContext().newPage(); String errorMsg = "业务系统URL不能为空";
} else { log.error(errorMsg);
log.info("未检测到有效会话,使用共享上下文并重新登录"); return errorMsg;
page = getUSerContext().newPage(); }
// 访问业务系统页面 Page page = null;
log.info("正在访问业务系统页面: {}", businessSystemUrl);
page.navigate(businessSystemUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// 检查是否重定向到了SSO登录页面 try {
String currentUrl = page.url(); // 检查是否已有有效的登录会话
log.info("当前页面URL: {}", currentUrl); boolean sessionValid = isSessionLoggedIn() && validateSession(businessSystemUrl);
if (currentUrl.startsWith(SSO_LOGIN_URL)) { if (sessionValid) {
log.info("检测到SSO登录页面,开始自动登录..."); log.info("检测到有效会话,直接使用共享上下文");
// 执行SSO登录 page = getUSerContext().newPage();
performLoginAndUpdateStatus(page); } else {
log.info("未检测到有效会话,使用共享上下文并重新登录");
// 等待登录完成并重定向回业务系统 page = getUSerContext().newPage();
boolean redirected = waitForUrlWithMultipleOptions(page, new String[]{businessSystemUrl}, WAIT_FOR_URL_TIMEOUT);
if (!redirected) { // 访问业务系统页面
log.warn("未能在指定时间内重定向到业务系统页面,当前URL: {}", page.url()); log.info("正在访问业务系统页面: {}", businessSystemUrl);
page.navigate(businessSystemUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// 检查是否重定向到了SSO登录页面
String currentUrl = page.url();
log.info("当前页面URL: {}", currentUrl);
if (currentUrl.startsWith(SSO_LOGIN_URL)) {
log.info("检测到SSO登录页面,开始自动登录...");
// 执行SSO登录
performLoginAndUpdateStatus(page);
// 等待登录完成并重定向回业务系统
boolean redirected = waitForUrlWithMultipleOptions(page, new String[]{businessSystemUrl}, WAIT_FOR_URL_TIMEOUT);
if (!redirected) {
log.warn("未能在指定时间内重定向到业务系统页面,当前URL: {}", page.url());
} else {
log.info("登录成功,已重定向回业务系统页面");
}
} else { } else {
log.info("登录成功,已重定向回业务系统页面"); // 即使没有跳转到登录页面,也更新登录时间
lastLoginTime = System.currentTimeMillis();
log.info("直接访问业务系统页面成功,无需SSO登录,更新会话时间");
} }
} else {
// 即使没有跳转到登录页面,也更新登录时间
lastLoginTime = System.currentTimeMillis();
log.info("直接访问业务系统页面成功,无需SSO登录,更新会话时间");
} }
}
// 如果页面尚未导航到业务系统URL,则导航到该URL // 如果页面尚未导航到业务系统URL,则导航到该URL
if (!page.url().equals(businessSystemUrl) && !page.url().startsWith(businessSystemUrl)) { if (!page.url().equals(businessSystemUrl) && !page.url().startsWith(businessSystemUrl)) {
log.info("正在访问业务系统页面: {}", businessSystemUrl); log.info("正在访问业务系统页面: {}", businessSystemUrl);
page.navigate(businessSystemUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE)); page.navigate(businessSystemUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
} }
// 提取页面内容 // 提取页面内容
String content = page.locator("body").innerText(); String content = page.locator("body").innerText();
long endTime = System.currentTimeMillis(); log.info("成功获取业务系统页面内容");
log.info("成功获取业务系统页面内容,耗时: {} ms", endTime - startTime);
// 检查是否包含错误信息 // 检查是否包含错误信息
if (content.contains("InvalidStateError") && content.contains("setRequestHeader")) { if (content.contains("InvalidStateError") && content.contains("setRequestHeader")) {
log.warn("检测到页面中可能存在JavaScript错误,但这不会影响主要功能"); log.warn("检测到页面中可能存在JavaScript错误,但这不会影响主要功能");
} }
return content; return content;
} catch (Exception e) { } catch (Exception e) {
long endTime = System.currentTimeMillis(); String errorMsg = "获取海信业务系统内容失败: " + e.getMessage();
String errorMsg = "获取海信业务系统内容失败: " + e.getMessage(); log.error("获取海信业务系统内容失败", e);
log.error("获取海信业务系统内容失败,耗时: {} ms", endTime - startTime, e); return errorMsg;
return errorMsg; } finally {
} finally { // 释放页面资源
// 释放页面资源 if (page != null) {
if (page != null) { try {
try { page.close();
page.close(); } catch (Exception e) {
} catch (Exception e) { log.warn("关闭页面时发生异常: {}", e.getMessage());
log.warn("关闭页面时发生异常: {}", e.getMessage()); }
} }
} }
} });
} }
/** /**
...@@ -280,89 +284,89 @@ public class HisenseSsoLoginTool { ...@@ -280,89 +284,89 @@ public class HisenseSsoLoginTool {
*/ */
@Tool(description = "海信SSO登录工具,用于登录海信SSO系统") @Tool(description = "海信SSO登录工具,用于登录海信SSO系统")
public String hisenseSsoLogin() { public String hisenseSsoLogin() {
String username = getUserName(); Map<String, Object> params = new HashMap<>();
String password = getPassword();
// 校验SSO凭证是否配置 return execute("hisenseSsoLogin", params, () -> {
if (username == null || username.isEmpty() || password == null || password.isEmpty()) { String username = getUserName();
String errorMsg = "SSO用户名或密码未配置,海信SSO工具不可用"; String password = getPassword();
log.warn(errorMsg); // 校验SSO凭证是否配置
return errorMsg; if (username == null || username.isEmpty() || password == null || password.isEmpty()) {
} String errorMsg = "SSO用户名或密码未配置,海信SSO工具不可用";
log.info("开始执行海信SSO登录,用户名: {}", username); log.warn(errorMsg);
return errorMsg;
long startTime = System.currentTimeMillis(); }
log.info("开始执行海信SSO登录,用户名: {}", username);
// 参数校验 // 参数校验
if (username == null || username.isEmpty()) { if (username == null || username.isEmpty()) {
String errorMsg = "用户名不能为空"; String errorMsg = "用户名不能为空";
log.error(errorMsg); log.error(errorMsg);
return errorMsg; return errorMsg;
} }
if (password == null || password.isEmpty()) { if (password == null || password.isEmpty()) {
String errorMsg = "密码不能为空"; String errorMsg = "密码不能为空";
log.error(errorMsg); log.error(errorMsg);
return errorMsg; return errorMsg;
} }
Page page = null; Page page = null;
try { try {
// 访问SSO登录页面 // 访问SSO登录页面
log.info("正在访问SSO登录页面: {}", SSO_LOGIN_URL); log.info("正在访问SSO登录页面: {}", SSO_LOGIN_URL);
page = getUSerContext().newPage(); page = getUSerContext().newPage();
page.navigate(SSO_LOGIN_URL, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE)); page.navigate(SSO_LOGIN_URL, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
if (page.url().equals(SSO_MFA_URL)) { if (page.url().equals(SSO_MFA_URL)) {
log.info("检测到MFA页面,自动发送验证码..."); log.info("检测到MFA页面,自动发送验证码...");
// 执行MFA登录,传递Context以保证生命周期管理 // 执行MFA登录,传递Context以保证生命周期管理
return sendVerificationCode(username, page, getUSerContext()); return sendVerificationCode(username, page, getUSerContext());
} }
if (!SSO_LOGIN_URL.equals(page.url())) { if (!SSO_LOGIN_URL.equals(page.url())) {
return "海信SSO在之前已登录成功"; return "海信SSO在之前已登录成功";
} }
// 执行SSO登录 // 执行SSO登录
String loginResult = performLoginAndUpdateStatus(page); String loginResult = performLoginAndUpdateStatus(page);
if(!loginResult.equals("SSO登录成功")){ if(!loginResult.equals("SSO登录成功")){
return loginResult; return loginResult;
} }
// 等待登录完成并重定向到SSO配置页面,使用更灵活的URL匹配和更长的超时时间 // 等待登录完成并重定向到SSO配置页面,使用更灵活的URL匹配和更长的超时时间
boolean profileRedirected = waitForSpecificUrl(page, SSO_PROFILE_URL, WAIT_FOR_URL_TIMEOUT); boolean profileRedirected = waitForSpecificUrl(page, SSO_PROFILE_URL, WAIT_FOR_URL_TIMEOUT);
if (!profileRedirected) { if (!profileRedirected) {
// 如果没有跳转到预期的配置页面,检查是否跳转到了其他可能的登录成功页面 // 如果没有跳转到预期的配置页面,检查是否跳转到了其他可能的登录成功页面
boolean alternativeRedirected = waitForUrlWithMultipleOptions(page, SUCCESS_REDIRECT_URLS, WAIT_FOR_URL_TIMEOUT); boolean alternativeRedirected = waitForUrlWithMultipleOptions(page, SUCCESS_REDIRECT_URLS, WAIT_FOR_URL_TIMEOUT);
if (!alternativeRedirected) { if (!alternativeRedirected) {
log.warn("未能在指定时间内重定向到SSO配置页面,当前URL: {}", page.url()); log.warn("未能在指定时间内重定向到SSO配置页面,当前URL: {}", page.url());
} else {
log.info("登录成功,已重定向到SSO相关页面");
}
} else { } else {
log.info("登录成功,已重定向到SSO相关页面"); log.info("登录成功,已重定向回SSO配置页面");
} }
} else {
log.info("登录成功,已重定向回SSO配置页面");
}
long endTime = System.currentTimeMillis(); log.info("海信SSO登录完成");
log.info("海信SSO登录完成,耗时: {} ms", endTime - startTime);
return "海信SSO登录成功"; return "海信SSO登录成功";
} catch (Exception e) { } catch (Exception e) {
long endTime = System.currentTimeMillis(); String errorMsg = "海信SSO登录失败: " + e.getMessage();
String errorMsg = "海信SSO登录失败: " + e.getMessage(); log.error("海信SSO登录失败", e);
log.error("海信SSO登录失败,耗时: {} ms", endTime - startTime, e); return errorMsg;
return errorMsg; } finally {
} finally { // 释放页面资源
// 释放页面资源 if (page != null && !page.url().equals(SSO_MFA_URL)) {
if (page != null && !page.url().equals(SSO_MFA_URL)) { try {
try { page.close();
page.close(); } catch (Exception e) {
} catch (Exception e) { log.warn("关闭页面时发生异常: {}", e.getMessage());
log.warn("关闭页面时发生异常: {}", e.getMessage()); }
} }
} }
} });
} }
/** /**
...@@ -618,173 +622,171 @@ public class HisenseSsoLoginTool { ...@@ -618,173 +622,171 @@ public class HisenseSsoLoginTool {
@Tool(description = "处理MFA验证码验证,完成海信SSO登录") @Tool(description = "处理MFA验证码验证,完成海信SSO登录")
public String handleMfaVerification( public String handleMfaVerification(
@JsonPropertyDescription("短信验证码") String verificationCode) { @JsonPropertyDescription("短信验证码") String verificationCode) {
log.info("开始处理MFA验证码验证"); Map<String, Object> params = new HashMap<>();
params.put("verificationCode", verificationCode);
String username = getUserName();
// 参数校验
if (verificationCode == null || verificationCode.isEmpty()) {
String errorMsg = "验证码不能为空";
log.error(errorMsg);
return errorMsg;
}
long startTime = System.currentTimeMillis();
// 清理过期的MFA会话
cleanupExpiredMfaSessions();
// 获取当前用户的MFA会话并更新访问时间
MfaSession mfaSession = getMfaSessionAndUpdateTime(username);
if (mfaSession == null) {
String errorMsg = "未找到当前用户的MFA验证会话,请先触发验证码发送。如果30分钟内未使用验证码,会话将自动过期。";
log.error(errorMsg);
return errorMsg;
}
Page mfaPage = mfaSession.page; return execute("handleMfaVerification", params, () -> {
BrowserContext context = mfaSession.context; log.info("开始处理MFA验证码验证");
// 检查MFA页面和Context是否仍然有效,如果已关闭则返回错误 String username = getUserName();
if (!isPageValid(mfaPage) || !isContextValid(context)) {
String errorMsg = "MFA验证页面或Context已关闭,BrowserContext可能已被释放,请重新触发验证码发送流程";
log.error(errorMsg);
mfaSessions.remove(username);
return errorMsg;
}
try { // 参数校验
// 等待页面加载完成,添加异常处理 if (verificationCode == null || verificationCode.isEmpty()) {
try { String errorMsg = "验证码不能为空";
mfaPage.waitForLoadState(LoadState.NETWORKIDLE);
} catch (Exception e) {
log.warn("等待页面加载时发生异常(可能页面已关闭),尝试继续: {}", e.getMessage());
}
// 查找验证码输入框并填入验证码
// 在访问元素前再次检查页面有效性
if (!isPageValid(mfaPage) || !isContextValid(context)) {
String errorMsg = "MFA验证页面或Context在操作过程中被关闭";
log.error(errorMsg); log.error(errorMsg);
mfaSessions.remove(username);
return errorMsg; return errorMsg;
} }
// 清理过期的MFA会话
cleanupExpiredMfaSessions();
Locator verificationInput = mfaPage.locator("input[placeholder='请输入短信验证码'][name='']"); // 获取当前用户的MFA会话并更新访问时间
if (verificationInput.count() == 0) { MfaSession mfaSession = getMfaSessionAndUpdateTime(username);
String errorMsg = "未找到验证码输入框"; if (mfaSession == null) {
String errorMsg = "未找到当前用户的MFA验证会话,请先触发验证码发送。如果30分钟内未使用验证码,会话将自动过期。";
log.error(errorMsg); log.error(errorMsg);
return errorMsg; return errorMsg;
} }
verificationInput.fill(verificationCode);
log.info("验证码已填入输入框"); Page mfaPage = mfaSession.page;
BrowserContext context = mfaSession.context;
// 再次检查页面有效性 // 检查MFA页面和Context是否仍然有效,如果已关闭则返回错误
if (!isPageValid(mfaPage) || !isContextValid(context)) { if (!isPageValid(mfaPage) || !isContextValid(context)) {
String errorMsg = "验证码填入后页面或Context已关闭"; String errorMsg = "MFA验证页面或Context已关闭,BrowserContext可能已被释放,请重新触发验证码发送流程";
log.error(errorMsg); log.error(errorMsg);
mfaSessions.remove(username); mfaSessions.remove(username);
return errorMsg; return errorMsg;
} }
// 点击登录按钮提交验证码
Locator loginButton = mfaPage.locator("button#login-button.para-btn.para-btn-login[hk-ripple='']");
if (loginButton.count() == 0) {
String errorMsg = "未找到登录按钮";
log.error(errorMsg);
return errorMsg;
}
loginButton.click();
log.info("已点击登录按钮提交验证码");
// 等待页面跳转,确认登录结果
try { try {
// 等待页面离开MFA页面,使用轮询方式检查URL变化 // 等待页面加载完成,添加异常处理
boolean pageLeftMfa = false; try {
long mfaStartTime = System.currentTimeMillis(); mfaPage.waitForLoadState(LoadState.NETWORKIDLE);
long mfaEndTime = mfaStartTime + MFA_WAIT_FOR_URL_TIMEOUT; } catch (Exception e) {
log.warn("等待页面加载时发生异常(可能页面已关闭),尝试继续: {}", e.getMessage());
while (System.currentTimeMillis() < mfaEndTime) {
String currentUrl = mfaPage.url();
if (!currentUrl.equals(SSO_MFA_URL)) {
pageLeftMfa = true;
log.info("MFA验证成功,已跳转到: {}", currentUrl);
break;
}
// 短暂休眠后继续检查
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
log.warn("MFA等待过程中被中断");
break;
}
} }
if (pageLeftMfa) { // 查找验证码输入框并填入验证码
// 从缓存中移除会话,因为登录已完成 // 在访问元素前再次检查页面有效性
if (!isPageValid(mfaPage) || !isContextValid(context)) {
String errorMsg = "MFA验证页面或Context在操作过程中被关闭";
log.error(errorMsg);
mfaSessions.remove(username); mfaSessions.remove(username);
return errorMsg;
}
// 更新登录时间 Locator verificationInput = mfaPage.locator("input[placeholder='请输入短信验证码'][name='']");
lastLoginTime = System.currentTimeMillis(); if (verificationInput.count() == 0) {
String errorMsg = "未找到验证码输入框";
log.error(errorMsg);
return errorMsg;
}
verificationInput.fill(verificationCode);
log.info("验证码已填入输入框");
long endTime = System.currentTimeMillis(); // 再次检查页面有效性
log.info("MFA验证完成,耗时: {} ms", endTime - startTime); if (!isPageValid(mfaPage) || !isContextValid(context)) {
String errorMsg = "验证码填入后页面或Context已关闭";
log.error(errorMsg);
mfaSessions.remove(username);
return errorMsg;
}
return "MFA验证成功,登录完成"; // 点击登录按钮提交验证码
} else { Locator loginButton = mfaPage.locator("button#login-button.para-btn.para-btn-login[hk-ripple='']");
// 如果仍在MFA页面,说明可能超时但验证仍在进行中,也认为成功 if (loginButton.count() == 0) {
log.info("MFA验证可能仍在进行中,当前仍在MFA页面"); String errorMsg = "未找到登录按钮";
log.error(errorMsg);
return errorMsg;
}
loginButton.click();
log.info("已点击登录按钮提交验证码");
// 等待页面跳转,确认登录结果
try {
// 等待页面离开MFA页面,使用轮询方式检查URL变化
boolean pageLeftMfa = false;
long mfaStartTime = System.currentTimeMillis();
long mfaEndTime = mfaStartTime + MFA_WAIT_FOR_URL_TIMEOUT;
// 从缓存中移除会话 while (System.currentTimeMillis() < mfaEndTime) {
mfaSessions.remove(username); String currentUrl = mfaPage.url();
if (!currentUrl.equals(SSO_MFA_URL)) {
pageLeftMfa = true;
log.info("MFA验证成功,已跳转到: {}", currentUrl);
break;
}
// 短暂休眠后继续检查
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
log.warn("MFA等待过程中被中断");
break;
}
}
if (pageLeftMfa) {
// 从缓存中移除会话,因为登录已完成
mfaSessions.remove(username);
// 更新登录时间 // 更新登录时间
lastLoginTime = System.currentTimeMillis(); lastLoginTime = System.currentTimeMillis();
long endTime = System.currentTimeMillis(); log.info("MFA验证完成");
log.info("MFA验证处理完成,耗时: {} ms", endTime - startTime);
return "MFA验证已处理"; return "MFA验证成功,登录完成";
} } else {
} catch (Exception urlException) { // 如果仍在MFA页面,说明可能超时但验证仍在进行中,也认为成功
// 检查是否仍然是MFA页面,表示登录失败 log.info("MFA验证可能仍在进行中,当前仍在MFA页面");
String currentUrl = mfaPage.url();
if (currentUrl.equals(SSO_MFA_URL)) { // 从缓存中移除会话
String errorMsg = "MFA验证失败,验证码可能错误或已过期,请重试"; mfaSessions.remove(username);
log.error(errorMsg);
return errorMsg;
} else {
// 页面已跳转,说明登录成功
log.info("MFA验证成功,已跳转到: {}", currentUrl);
// 从缓存中移除会话 // 更新登录时间
mfaSessions.remove(username); lastLoginTime = System.currentTimeMillis();
log.info("MFA验证处理完成");
return "MFA验证已处理";
}
} catch (Exception urlException) {
// 检查是否仍然是MFA页面,表示登录失败
String currentUrl = mfaPage.url();
if (currentUrl.equals(SSO_MFA_URL)) {
String errorMsg = "MFA验证失败,验证码可能错误或已过期,请重试";
log.error(errorMsg);
return errorMsg;
} else {
// 页面已跳转,说明登录成功
log.info("MFA验证成功,已跳转到: {}", currentUrl);
// 更新登录时间 // 从缓存中移除会话
lastLoginTime = System.currentTimeMillis(); mfaSessions.remove(username);
long endTime = System.currentTimeMillis(); // 更新登录时间
log.info("MFA验证完成,耗时: {} ms", endTime - startTime); lastLoginTime = System.currentTimeMillis();
return "MFA验证成功,登录完成"; log.info("MFA验证完成");
return "MFA验证成功,登录完成";
}
} }
} catch (com.microsoft.playwright.impl.TargetClosedError e) {
// 专门处理TargetClosedError
String errorMsg = "MFA验证时BrowserContext已关闭,请重新触发验证码发送流程";
log.error("MFA验证失败 - TargetClosedError,完整错误堆栈: ", e);
mfaSessions.remove(username);
return errorMsg;
} catch (Exception e) {
String errorMsg = "MFA验证过程发生异常: " + e.getMessage();
log.error("MFA验证失败,错误类型: {},完整错误堆栈: ", e.getClass().getName(), e);
mfaSessions.remove(username);
return errorMsg;
} }
} catch (com.microsoft.playwright.impl.TargetClosedError e) { });
// 专门处理TargetClosedError
long endTime = System.currentTimeMillis();
String errorMsg = "MFA验证时BrowserContext已关闭,请重新触发验证码发送流程";
log.error("MFA验证失败 - TargetClosedError,耗时: {} ms,完整错误堆栈: ", endTime - startTime, e);
mfaSessions.remove(username);
return errorMsg;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
String errorMsg = "MFA验证过程发生异常: " + e.getMessage();
log.error("MFA验证失败,耗时: {} ms,错误类型: {},完整错误堆栈: ", endTime - startTime, e.getClass().getName(), e);
mfaSessions.remove(username);
return errorMsg;
}
} }
/** /**
...@@ -794,32 +796,32 @@ public class HisenseSsoLoginTool { ...@@ -794,32 +796,32 @@ public class HisenseSsoLoginTool {
*/ */
@Tool(description = "海信SSO登出工具,用于退出海信SSO系统") @Tool(description = "海信SSO登出工具,用于退出海信SSO系统")
public String hisenseSsoLogout() { public String hisenseSsoLogout() {
// initializeIfNeeded(); Map<String, Object> params = new HashMap<>();
log.info("开始执行海信SSO登出");
return execute("hisenseSsoLogout", params, () -> {
long startTime = System.currentTimeMillis(); // initializeIfNeeded();
log.info("开始执行海信SSO登出");
try { try {
// 关闭共享上下文 // 关闭共享上下文
if (getUSerContext() != null) { if (getUSerContext() != null) {
getUSerContext().close(); getUSerContext().close();
log.info("共享上下文已关闭"); log.info("共享上下文已关闭");
} }
// 重置登录时间 // 重置登录时间
lastLoginTime = 0; lastLoginTime = 0;
log.info("登录时间已重置"); log.info("登录时间已重置");
long endTime = System.currentTimeMillis(); log.info("海信SSO登出完成");
log.info("海信SSO登出完成,耗时: {} ms", endTime - startTime);
return "海信SSO登出成功"; return "海信SSO登出成功";
} catch (Exception e) { } catch (Exception e) {
long endTime = System.currentTimeMillis(); String errorMsg = "海信SSO登出失败: " + e.getMessage();
String errorMsg = "海信SSO登出失败: " + e.getMessage(); log.error("海信SSO登出失败", e);
log.error("海信SSO登出失败,耗时: {} ms", endTime - startTime, e); return errorMsg;
return errorMsg; }
} });
} }
/** /**
...@@ -827,23 +829,23 @@ public class HisenseSsoLoginTool { ...@@ -827,23 +829,23 @@ public class HisenseSsoLoginTool {
*/ */
@Tool(description = "检查海信SSO登录状态") @Tool(description = "检查海信SSO登录状态")
public String checkHisenseSsoLoginStatus() { public String checkHisenseSsoLoginStatus() {
// initializeIfNeeded(); Map<String, Object> params = new HashMap<>();
log.info("开始检查海信SSO登录状态");
return execute("checkHisenseSsoLoginStatus", params, () -> {
long startTime = System.currentTimeMillis(); // initializeIfNeeded();
log.info("开始检查海信SSO登录状态");
try { try {
boolean isLoggedIn = isSessionLoggedIn(); boolean isLoggedIn = isSessionLoggedIn();
long endTime = System.currentTimeMillis(); log.info("海信SSO登录状态检查完成:{}", isLoggedIn);
log.info("海信SSO登录状态检查完成:{},耗时: {} ms", isLoggedIn, endTime - startTime);
return isLoggedIn ? "已登录" : "未登录"; return isLoggedIn ? "已登录" : "未登录";
} catch (Exception e) { } catch (Exception e) {
long endTime = System.currentTimeMillis(); String errorMsg = "海信SSO登录状态检查失败: " + e.getMessage();
String errorMsg = "海信SSO登录状态检查失败: " + e.getMessage(); log.error("海信SSO登录状态检查失败", e);
log.error("海信SSO登录状态检查失败,耗时: {} ms", endTime - startTime, e); return errorMsg;
return errorMsg; }
} });
} }
/** /**
......
...@@ -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 java.util.Map;
/**
* 时间轴事件工厂类
* 专门负责根据事件类型创建相应的事件DTO对象,遵循工厂模式设计原则
*/
public class TimelineEventFactory {
/**
* 根据事件类型创建相应的事件DTO对象
* 这是工厂类的唯一公共入口方法,确保所有事件对象创建都通过工厂完成
*
* @param eventType 事件类型
* @param eventData 事件数据
* @return 相应的事件DTO对象
*/
public static WorkPanelEvent createTimelineEvent(String eventType, Map<String, Object> eventData) {
if (eventType == null || eventData == null) {
return null;
}
switch (eventType) {
case "thought":
return createThoughtEvent(eventData);
case "tool_call":
case "tool_result":
case "tool_error":
return createToolEvent(eventType, eventData);
case "embed":
return createEmbedEvent(eventData);
case "log":
return createLogEvent(eventData);
case "result":
return createResultEvent(eventData);
default:
// 对于其他类型的事件,创建基础事件对象
return createBaseEvent(eventType, eventData);
}
}
/**
* 创建思考事件
* 专门处理思考类型事件的创建
*/
private static ThoughtEvent createThoughtEvent(Map<String, Object> eventData) {
return ThoughtEvent.builder()
.type(getStringValue(eventData, "type"))
.title(getStringValue(eventData, "title"))
.timestamp(getLongValue(eventData, "timestamp"))
.metadata(getMapValue(eventData, "metadata"))
.content(getStringValue(eventData, "content"))
.thinkingType(getStringValue(eventData, "thinkingType"))
.build();
}
/**
* 创建工具事件
* 统一处理所有工具相关事件的创建(调用、结果、错误)
*/
private static ToolEvent createToolEvent(String eventType, Map<String, Object> eventData) {
return ToolEvent.builder()
.type(eventType)
.title(getStringValue(eventData, "title"))
.timestamp(getLongValue(eventData, "timestamp"))
.metadata(getMapValue(eventData, "metadata"))
.toolName(getStringValue(eventData, "toolName"))
.toolAction(getStringValue(eventData, "toolAction"))
.toolInput(getMapValue(eventData, "toolInput"))
.toolOutput(eventData.get("toolOutput"))
.toolStatus(getStringValue(eventData, "toolStatus"))
.executionTime(getLongValue(eventData, "executionTime"))
.errorMessage(getStringValue(eventData, "errorMessage"))
.errorCode(getStringValue(eventData, "errorCode"))
.build();
}
/**
* 创建嵌入事件
* 专门处理嵌入类型事件的创建
*/
private static EmbedEvent createEmbedEvent(Map<String, Object> eventData) {
return EmbedEvent.builder()
.type(getStringValue(eventData, "type"))
.title(getStringValue(eventData, "title"))
.timestamp(getLongValue(eventData, "timestamp"))
.metadata(getMapValue(eventData, "metadata"))
.embedUrl(getStringValue(eventData, "embedUrl"))
.embedType(getStringValue(eventData, "embedType"))
.embedTitle(getStringValue(eventData, "embedTitle"))
.embedHtmlContent(getStringValue(eventData, "embedHtmlContent"))
.build();
}
/**
* 创建日志事件
* 专门处理日志类型事件的创建
*/
private static LogEvent createLogEvent(Map<String, Object> eventData) {
return LogEvent.builder()
.type(getStringValue(eventData, "type"))
.title(getStringValue(eventData, "title"))
.timestamp(getLongValue(eventData, "timestamp"))
.metadata(getMapValue(eventData, "metadata"))
.content(getStringValue(eventData, "content"))
.logLevel(getStringValue(eventData, "logLevel"))
.build();
}
/**
* 创建结果事件
* 专门处理结果类型事件的创建
*/
private static ResultEvent createResultEvent(Map<String, Object> eventData) {
return ResultEvent.builder()
.type(getStringValue(eventData, "type"))
.title(getStringValue(eventData, "title"))
.timestamp(getLongValue(eventData, "timestamp"))
.metadata(getMapValue(eventData, "metadata"))
.content(getStringValue(eventData, "content"))
.build();
}
/**
* 创建基础事件
* 处理所有其他类型事件的创建
*/
private static WorkPanelEvent createBaseEvent(String eventType, Map<String, Object> eventData) {
return WorkPanelEvent.builder()
.type(eventType)
.title(getStringValue(eventData, "title"))
.timestamp(getLongValue(eventData, "timestamp"))
.metadata(getMapValue(eventData, "metadata"))
.build();
}
/**
* 从Map中获取字符串值
* 工具方法,用于安全地从Map中提取字符串值
*/
private static String getStringValue(Map<String, Object> map, String key) {
Object value = map.get(key);
return value != null ? value.toString() : null;
}
/**
* 从Map中获取长整型值
* 工具方法,用于安全地从Map中提取长整型值
*/
private static Long getLongValue(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value instanceof Number) {
return ((Number) value).longValue();
} else if (value instanceof String) {
try {
return Long.parseLong((String) value);
} catch (NumberFormatException e) {
return null;
}
}
return null;
}
/**
* 从Map中获取Map值
* 工具方法,用于安全地从Map中提取嵌套Map值
*/
@SuppressWarnings("unchecked")
private static Map<String, Object> getMapValue(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value instanceof Map) {
return (Map<String, Object>) value;
}
return null;
}
}
\ 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
package pangea.hiagent.web.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 工作面板状态数据传输对象
* 用于API返回工作面板的当前状态
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WorkPanelStatusDto implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 工作面板ID
*/
private String id;
/**
* 当前Agent ID
*/
private String agentId;
/**
* 当前Agent名称
*/
private String agentName;
/**
* 所有事件列表
*/
private List<WorkPanelEvent> events;
/**
* 思考过程事件列表
*/
private List<WorkPanelEvent> thinkingEvents;
/**
* 工具调用事件列表
*/
private List<WorkPanelEvent> toolCallEvents;
/**
* 执行日志事件列表
*/
private List<WorkPanelEvent> logEvents;
/**
* 总事件数量
*/
private Integer totalEvents;
/**
* 成功的工具调用数
*/
private Integer successfulToolCalls;
/**
* 失败的工具调用数
*/
private Integer failedToolCalls;
/**
* 更新时间戳
*/
private Long updateTimestamp;
/**
* 是否正在处理中
*/
private Boolean isProcessing;
}
...@@ -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";
......
package pangea.hiagent.workpanel;
import pangea.hiagent.web.dto.WorkPanelEvent;
import java.util.function.Consumer;
import java.util.List;
/**
* 工作面板数据收集器接口
* 用于采集Agent执行过程中的各类数据(思考过程、工具调用等)
*/
public interface IWorkPanelDataCollector {
/**
* 记录思考过程
* @param content 思考内容
* @param thinkingType 思考类型(分析、规划、执行等)
*/
void recordThinking(String content, String thinkingType);
/**
* 记录工具调用Action
* @param toolName 工具名称
* @param input 工具输入参数
* @param output 工具输出结果
* @param status 执行状态(pending/success/failure/error)
* @param executionTime 执行时间(毫秒)
*/
void recordToolCallAction(String toolName, Object input, Object output, String status, Long executionTime);
/**
* 记录日志
* @param message 日志消息
* @param level 日志级别(info/warn/error/debug)
*/
void recordLog(String message, String level);
/**
* 记录embed嵌入事件
* @param url 嵌入资源URL(可选)
* @param type MIME类型
* @param title 嵌入标题
* @param htmlContent HTML内容(可选)
*/
void recordEmbed(String url, String type, String title, String htmlContent);
/**
* 获取所有收集的事件
*/
List<WorkPanelEvent> getEvents();
/**
* 订阅事件(用于实时推送)
* @param consumer 事件处理函数
*/
void subscribe(Consumer<WorkPanelEvent> consumer);
/**
* 清空所有事件
*/
void clear();
/**
* 获取最后一个工具调用事件
*/
WorkPanelEvent getLastToolCall();
/**
* 记录最终答案
* @param content 最终答案内容
*/
void recordFinalAnswer(String content);
/**
* 添加事件到收集器
* 统一的事件添加方法,用于避免重复实现
* @param event 工作面板事件
*/
void addEvent(WorkPanelEvent event);
}
\ No newline at end of file
package pangea.hiagent.workpanel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import pangea.hiagent.workpanel.event.EventDeduplicationService;
import pangea.hiagent.workpanel.event.EventService;
import pangea.hiagent.agent.service.UserSseService;
import pangea.hiagent.web.dto.LogEvent;
import pangea.hiagent.web.dto.ResultEvent;
import pangea.hiagent.web.dto.ThoughtEvent;
import pangea.hiagent.web.dto.ToolEvent;
import pangea.hiagent.web.dto.WorkPanelEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
/**
* 工作面板数据收集器实现
* 专门负责采集Agent执行过程中的各类数据,遵循单一职责原则
*/
@Slf4j
@Component
public class WorkPanelDataCollector implements IWorkPanelDataCollector {
/**
* 事件列表(线程安全)
*/
private final List<WorkPanelEvent> events = new CopyOnWriteArrayList<>();
/**
* 事件订阅者列表(线程安全)
*/
private final List<Consumer<WorkPanelEvent>> subscribers = new CopyOnWriteArrayList<>();
/**
* 用户ID到订阅者的映射,用于更好地管理订阅者
*/
private final Map<String, Consumer<WorkPanelEvent>> userSubscribers = new ConcurrentHashMap<>();
/**
* 事件去重服务
*/
@Autowired
private EventDeduplicationService eventDeduplicationService;
/**
* 统一事件服务
*/
@Autowired
private EventService eventService;
/**
* SSE服务
*/
@Autowired
private UserSseService unifiedSseService;
/**
* 最大事件数量,防止内存溢出
*/
private static final int MAX_EVENTS = 1000;
public WorkPanelDataCollector() {
// 默认构造函数
}
@Override
public void recordThinking(String content, String thinkingType) {
try {
WorkPanelEvent event = createThinkingEvent(content, thinkingType);
if (event != null && !eventDeduplicationService.isDuplicateEvent(event)) {
addEvent(event);
}
} catch (Exception e) {
logError("记录思考过程时发生错误", "content={}, type={}", content, thinkingType, e);
}
}
@Override
public void recordToolCallAction(String toolName, Object input, Object output, String status, Long executionTime) {
try {
WorkPanelEvent event = createToolCallActionEvent(toolName, input, output, status, executionTime);
if (event != null) {
handleToolCallAction(toolName, event, status, null);
}
} catch (Exception e) {
logError("记录工具调用Action时发生错误", "toolName={}", toolName, null, e);
}
}
@Override
public void recordLog(String message, String level) {
try {
WorkPanelEvent event = createLogEvent(message, level);
if (event != null) {
addEvent(event);
}
} catch (Exception e) {
logError("记录日志时发生错误", "message={}, level={}", message, level, e);
}
}
@Override
public void recordEmbed(String url, String type, String title, String htmlContent) {
try {
WorkPanelEvent event = createEmbedEvent(url, type, title, htmlContent);
if (event != null) {
addEvent(event);
}
} catch (Exception e) {
logError("recordEmbed时发生错误", "title={}, type={}", title, type, e);
}
}
@Override
public void recordFinalAnswer(String content) {
try {
WorkPanelEvent event = createFinalAnswerEvent(content);
if (event != null && !eventDeduplicationService.isDuplicateEvent(event)) {
addEvent(event);
}
} catch (Exception e) {
logError("记录最终答案时发生错误", "content={}", content, null, e);
}
}
@Override
public List<WorkPanelEvent> getEvents() {
return new ArrayList<>(events);
}
@Override
public void subscribe(Consumer<WorkPanelEvent> consumer) {
if (consumer != null) {
subscribers.add(consumer);
if (log.isTraceEnabled()) {
log.trace("已添加事件订阅者,当前订阅者数量: {}", subscribers.size());
}
}
}
/**
* 为指定用户订阅事件
*
* @param userId 用户ID
* @param consumer 事件消费者
*/
public void subscribe(String userId, Consumer<WorkPanelEvent> consumer) {
if (!isValidSubscription(userId, consumer)) {
return;
}
// 移除该用户之前的订阅者(如果存在)
removeExistingSubscriber(userId);
// 添加新的订阅者
addNewSubscriber(userId, consumer);
logSubscriptionDetails(userId);
}
/**
* 取消指定用户的事件订阅
*
* @param userId 用户ID
*/
public void unsubscribe(String userId) {
if (userId != null) {
Consumer<WorkPanelEvent> subscriber = userSubscribers.remove(userId);
if (subscriber != null) {
subscribers.remove(subscriber);
log.debug("已移除用户 {} 的事件订阅者", userId);
}
}
}
@Override
public void clear() {
try {
events.clear();
// 精简日志记录,避免过多的debug级别日志
if (log.isTraceEnabled()) {
log.trace("已清空工作面板事件");
}
} catch (Exception e) {
log.error("清空事件时发生错误", e);
}
}
@Override
public WorkPanelEvent getLastToolCall() {
// 从后往前查找最后一个工具调用事件
for (int i = events.size() - 1; i >= 0; i--) {
WorkPanelEvent event = events.get(i);
if (event instanceof ToolEvent) {
String type = event.getType();
if ("tool_call".equals(type) || "tool_result".equals(type)) {
return event;
}
}
}
return null;
}
@Override
public void addEvent(WorkPanelEvent event) {
// 直接复用已有的私有方法
if (event != null) {
// 调用内部的addEvent方法
addEventInternal(event);
}
}
/**
* 内部的添加事件方法,避免接口方法与私有方法的命名冲突
*/
private void addEventInternal(WorkPanelEvent event) {
try {
// 控制事件数量,防止内存溢出
if (events.size() >= MAX_EVENTS) {
events.remove(0); // 移除最老的事件
}
events.add(event);
String content = "";
if (event instanceof ThoughtEvent) {
content = ((ThoughtEvent) event).getContent();
} else if (event instanceof LogEvent) {
content = ((LogEvent) event).getContent();
} else if (event instanceof ResultEvent) {
content = ((ResultEvent) event).getContent();
}
log.debug("添加事件到列表: 类型={}, 内容={}", event.getType(), content);
// 更新最近事件缓存
eventDeduplicationService.updateRecentEventsCache(event);
// 通知所有订阅者
notifySubscribers(event);
} catch (Exception e) {
// 即使在addEvent方法中也增加异常保护,防止影响主流程
logDebug("添加事件失败: {}", e.getMessage());
}
}
// ==================== 私有方法 ====================
/**
* 创建思考事件
*/
private WorkPanelEvent createThinkingEvent(String content, String thinkingType) {
return eventService.recordThinking(content, thinkingType);
}
/**
* 创建工具调用Action事件
*/
private WorkPanelEvent createToolCallActionEvent(String toolName, Object input, Object output, String status, Long executionTime) {
return eventService.recordToolCallComplete(toolName, output, status, executionTime);
}
/**
* 处理工具调用Action
*/
private void handleToolCallAction(String toolName, WorkPanelEvent event, String status, String errorMessage) {
// 对于pending状态,直接添加事件
if ("pending".equals(status)) {
addEvent(event);
return;
}
// 查找最近的该工具的pending事件并更新它
WorkPanelEvent lastToolCall = getLastPendingToolCall(toolName);
if (lastToolCall != null) {
// 更新现有事件
if ("error".equals(status) && lastToolCall instanceof ToolEvent) {
updateEventAsError((ToolEvent) lastToolCall, errorMessage);
} else if (lastToolCall instanceof ToolEvent) {
ToolEvent toolEvent = (ToolEvent) lastToolCall;
toolEvent.setType(WorkPanelUtils.getEventTypeFromStatus(status));
toolEvent.setToolStatus(status);
// 注意:这里不设置toolOutput,因为event中可能包含完整的输出信息
toolEvent.setTimestamp(System.currentTimeMillis());
}
// 重新发布更新后的事件
notifySubscribers(lastToolCall);
} else {
addEvent(event);
}
}
/**
* 更新事件为错误状态
*/
private void updateEventAsError(WorkPanelEvent event, String errorMessage) {
event.setType("tool_error");
if (event instanceof ToolEvent) {
ToolEvent toolEvent = (ToolEvent) event;
// 使用反射设置content字段,因为ToolEvent没有setContent方法
try {
java.lang.reflect.Field contentField = WorkPanelEvent.class.getDeclaredField("content");
contentField.setAccessible(true);
contentField.set(toolEvent, errorMessage);
} catch (Exception e) {
// 如果反射失败,忽略错误
}
}
if (event instanceof ToolEvent) {
((ToolEvent) event).setToolStatus("failure");
}
event.setTimestamp(System.currentTimeMillis());
}
/**
* 创建日志事件
*/
private WorkPanelEvent createLogEvent(String message, String level) {
return eventService.recordLog(message, level);
}
/**
* 创建嵌入事件
*/
private WorkPanelEvent createEmbedEvent(String url, String type, String title, String htmlContent) {
return eventService.recordEmbed(url, type, title, htmlContent);
}
/**
* 创建最终答案事件
*/
private WorkPanelEvent createFinalAnswerEvent(String content) {
return eventService.recordFinalAnswer(content);
}
/**
* 通知所有订阅者
*/
private void notifySubscribers(WorkPanelEvent event) {
if (event == null) {
return;
}
try {
// 遍历所有订阅者并发送事件
for (Consumer<WorkPanelEvent> subscriber : subscribers) {
try {
if (subscriber != null) {
subscriber.accept(event);
}
} catch (Exception e) {
// 异常降级为debug日志,避免过度日志记录
// 异常通常由于SSE连接已断开导致,这是正常情况
if (e instanceof org.springframework.web.context.request.async.AsyncRequestNotUsableException) {
log.debug("订阅者处理事件失败:异步请求不可用(客户端已断开连接)");
} else if (e instanceof java.io.IOException) {
log.debug("订阅者处理事件失败:客户端连接已断开");
} else if (e.getMessage() != null && e.getMessage().contains("response has already been committed")) {
log.debug("订阅者处理事件失败:响应已提交");
} else {
// 其他异常也降级为debug,避免日志污染
log.debug("订阅者处理事件失败: {}", e.getMessage());
}
}
}
// 通过EventService发送事件到所有SSE连接
for (SseEmitter emitter : unifiedSseService.getEmitters()) {
try {
unifiedSseService.sendWorkPanelEvent(emitter, event);
} catch (Exception e) {
log.debug("通过EventService发送事件失败: {}", e.getMessage());
}
}
} catch (Exception e) {
log.debug("通知订阅者时发生错误: {}", e.getMessage());
}
}
/**
* 统一日志错误处理
*/
private void logError(String message, String format, Object arg1, Object arg2, Exception e) {
if (arg2 != null) {
log.error(message + ": " + format, arg1, arg2, e);
} else {
log.error(message + ": " + format, arg1, e);
}
}
/**
* 统一日志调试处理
*/
private void logDebug(String format, Object... arguments) {
// 即使在addEvent方法中也增加异常保护,防止影响主流程
log.debug(format, arguments);
}
/**
* 验证订阅参数是否有效
*/
private boolean isValidSubscription(String userId, Consumer<WorkPanelEvent> consumer) {
return userId != null && consumer != null;
}
/**
* 移除已存在的订阅者
*/
private void removeExistingSubscriber(String userId) {
Consumer<WorkPanelEvent> existingSubscriber = userSubscribers.get(userId);
if (existingSubscriber != null) {
subscribers.remove(existingSubscriber);
log.debug("已移除用户 {} 的旧订阅者", userId);
}
}
/**
* 添加新的订阅者
*/
private void addNewSubscriber(String userId, Consumer<WorkPanelEvent> consumer) {
subscribers.add(consumer);
userSubscribers.put(userId, consumer);
}
/**
* 记录订阅详情
*/
private void logSubscriptionDetails(String userId) {
// 精简日志记录,避免过多的debug级别日志
if (log.isTraceEnabled()) {
log.trace("已为用户 {} 添加事件订阅者,当前订阅者数量: {}", userId, subscribers.size());
}
}
/**
* 检查是否为指定工具的待处理调用事件
*/
private boolean isPendingToolCallEvent(WorkPanelEvent event, String toolName) {
// 检查事件是否为ToolEvent类型
if (!(event instanceof ToolEvent)) {
return false;
}
ToolEvent toolEvent = (ToolEvent) event;
return "tool_call".equals(event.getType()) &&
toolName != null && toolName.equals(toolEvent.getToolName()) &&
"pending".equals(toolEvent.getToolStatus());
}
/**
* 查找最近的指定工具的pending事件
*
* @param toolName 工具名称
* @return 最近的pending事件,如果不存在则返回null
*/
public WorkPanelEvent getLastPendingToolCall(String toolName) {
// 从后往前查找最近的该工具的pending事件
for (int i = events.size() - 1; i >= 0; i--) {
WorkPanelEvent event = events.get(i);
if (isPendingToolCallEvent(event, toolName)) {
return event;
}
}
return null;
}
}
\ No newline at end of file
package pangea.hiagent.workpanel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import pangea.hiagent.web.dto.*;
import pangea.hiagent.web.service.AgentService;
import pangea.hiagent.model.Agent;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
/**
* 工作面板服务
* 负责处理工作面板相关的状态和事件
*/
@Slf4j
@Service
public class WorkPanelService {
@Autowired
private AgentService agentService;
// 用于跟踪已发送的事件ID,防止重复发送
private final Map<String, Set<String>> sentEventIds = new ConcurrentHashMap<>();
/**
* 获取工作面板当前状态
*/
public WorkPanelStatusDto getWorkPanelStatus(String agentId, String userId) {
try {
Agent agent = agentService.getAgent(agentId);
if (agent == null) {
throw new RuntimeException("Agent不存在");
}
if (!agent.getOwner().equals(userId)) {
throw new RuntimeException("无权访问该Agent");
}
log.info("获取Agent {} 的工作面板状态", agentId);
// 从工作面板收集器中读取数据
IWorkPanelDataCollector collector = null; // 移除了对ReActService的依赖
List<WorkPanelEvent> allEvents = collector != null ? collector.getEvents() : new ArrayList<>();
// 统计不同类型的事件
int totalEvents = allEvents.size();
int successfulCalls = (int) allEvents.stream()
.filter(e -> "tool_result".equals(e.getType()) &&
(e instanceof ToolEvent && "success".equals(((ToolEvent) e).getToolStatus())))
.count();
int failedCalls = (int) allEvents.stream()
.filter(e -> "tool_error".equals(e.getType()) ||
("tool_result".equals(e.getType()) &&
(e instanceof ToolEvent && "failure".equals(((ToolEvent) e).getToolStatus()))))
.count();
List<WorkPanelEvent> thinkingEvents = new ArrayList<>();
List<WorkPanelEvent> toolCallEvents = new ArrayList<>();
List<WorkPanelEvent> logEvents = new ArrayList<>();
for (WorkPanelEvent event : allEvents) {
switch (event.getType()) {
case "thought" -> thinkingEvents.add(event);
case "tool_call", "tool_result", "tool_error" -> toolCallEvents.add(event);
case "log" -> logEvents.add(event);
default -> {
}
}
}
WorkPanelStatusDto status = WorkPanelStatusDto.builder()
.id(agentId + "_workpanel")
.agentId(agentId)
.agentName(agent.getName())
.events(allEvents)
.thinkingEvents(thinkingEvents)
.toolCallEvents(toolCallEvents)
.logEvents(logEvents)
.totalEvents(totalEvents)
.successfulToolCalls(successfulCalls)
.failedToolCalls(failedCalls)
.updateTimestamp(System.currentTimeMillis())
.isProcessing(false)
.build();
return status;
} catch (Exception e) {
log.error("获取工作面板状态失败", e);
throw new RuntimeException("获取工作面板状态失败: " + e.getMessage(), e);
}
}
/**
* 清空工作面板数据
*/
public void clearWorkPanel(String agentId, String userId) {
try {
Agent agent = agentService.getAgent(agentId);
if (agent == null) {
throw new RuntimeException("Agent不存在");
}
if (!agent.getOwner().equals(userId)) {
throw new RuntimeException("无权访问该Agent");
}
log.info("清空Agent {} 的工作面板", agentId);
// 在实际应用中,这里应该从缓存中清除工作面板数据
// 清空已发送事件ID跟踪
sentEventIds.remove(agentId);
} catch (Exception e) {
log.error("清空工作面板失败", e);
throw new RuntimeException("清空工作面板失败: " + e.getMessage(), e);
}
}
/**
* 生成事件唯一标识
*/
public String generateEventId(WorkPanelEvent event) {
if (event == null) {
return "null_event_" + System.currentTimeMillis();
}
StringBuilder sb = new StringBuilder();
sb.append(event.getType()).append("_");
switch (event.getType()) {
case "thought":
if (event instanceof ThoughtEvent) {
ThoughtEvent thoughtEvent = (ThoughtEvent) event;
sb.append(thoughtEvent.getThinkingType() != null ? thoughtEvent.getThinkingType() : "default")
.append("_")
.append(thoughtEvent.getContent() != null ? thoughtEvent.getContent().hashCode() : 0);
} else {
sb.append("default").append("_").append(0);
}
break;
case "tool_call":
case "tool_result":
case "tool_error":
if (event instanceof ToolEvent) {
ToolEvent toolEvent = (ToolEvent) event;
sb.append(toolEvent.getToolName() != null ? toolEvent.getToolName() : "unknown")
.append("_")
.append(toolEvent.getToolAction() != null ? toolEvent.getToolAction() : "unknown")
.append("_")
.append(event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis());
} else {
sb.append("unknown").append("_").append("unknown").append("_").append(System.currentTimeMillis());
}
break;
case "log":
if (event instanceof LogEvent) {
LogEvent logEvent = (LogEvent) event;
sb.append(logEvent.getLogLevel() != null ? logEvent.getLogLevel() : "info")
.append("_")
.append(logEvent.getContent() != null ? logEvent.getContent().hashCode() : 0);
} else {
sb.append("info").append("_").append(0);
}
break;
case "embed":
if (event instanceof EmbedEvent) {
EmbedEvent embedEvent = (EmbedEvent) event;
sb.append(embedEvent.getEmbedTitle() != null ? embedEvent.getEmbedTitle() : "untitled")
.append("_")
.append(embedEvent.getEmbedUrl() != null ? embedEvent.getEmbedUrl().hashCode() : 0);
} else {
sb.append("untitled").append("_").append(0);
}
break;
default:
sb.append(event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis());
break;
}
return sb.toString();
}
/**
* 检查事件是否已发送
*/
public boolean isEventAlreadySent(String agentId, WorkPanelEvent event) {
String eventId = generateEventId(event);
Set<String> agentEventIds = sentEventIds.computeIfAbsent(agentId, k -> new HashSet<>());
return !agentEventIds.add(eventId); // 如果已存在,add返回false,表示已发送
}
}
\ No newline at end of file
package pangea.hiagent.workpanel;
import lombok.extern.slf4j.Slf4j;
import pangea.hiagent.workpanel.event.EventTypeConverter;
import java.util.HashMap;
import java.util.Map;
/**
* 工作面板工具类
* 提供各种辅助方法用于工作面板事件处理
* 注意:此工具类已优化,移除了与EventTypeConverter重复的功能
*/
@Slf4j
public class WorkPanelUtils {
/**
* 将对象转换为Map(用于工具输入参数)
*/
public static Map<String, Object> convertToMap(Object input) {
if (input == null) {
return new HashMap<>();
}
if (input instanceof Map) {
// 安全地转换Map类型,确保键为String类型
Map<?, ?> rawMap = (Map<?, ?>) input;
Map<String, Object> resultMap = new HashMap<>();
for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
// 将键转换为String类型
String key = entry.getKey() != null ? entry.getKey().toString() : "null";
resultMap.put(key, entry.getValue());
}
return resultMap;
}
// 简单对象转换为Map
Map<String, Object> result = new HashMap<>();
result.put("value", input);
return result;
}
/**
* 获取状态文本
* 已委托给EventTypeConverter处理
*/
public static String getStatusText(String status) {
EventTypeConverter converter = new EventTypeConverter();
return converter.getStatusText(status);
}
/**
* 根据状态确定事件类型
* 已委托给EventTypeConverter处理
*/
public static String getEventTypeFromStatus(String status) {
EventTypeConverter converter = new EventTypeConverter();
return converter.getEventTypeFromStatus(status);
}
/**
* 将对象转换为JSON字符串
*
* @param obj 要转换的对象
* @return JSON字符串表示
*/
public static String convertToJsonString(Object obj) {
if (obj == null) {
return "null";
}
try {
// 使用Jackson ObjectMapper进行序列化
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
return mapper.writeValueAsString(obj);
} catch (Exception e) {
// 如果序列化失败,返回对象的toString()表示
return obj.toString();
}
}
}
\ No newline at end of file
package pangea.hiagent.workpanel.event;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import pangea.hiagent.web.dto.*;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 事件去重服务
* 统一处理事件去重逻辑,避免在多个地方重复实现
*/
@Slf4j
@Component
public class EventDeduplicationService {
/**
* 最近事件的缓存,用于去重检查
*/
private final Map<String, WorkPanelEvent> recentEventsCache = new ConcurrentHashMap<>();
/**
* 缓存过期时间(毫秒),默认5秒
*/
private static final long CACHE_EXPIRY_TIME = 5000L;
/**
* 最大缓存大小,防止内存溢出
*/
private static final int MAX_CACHE_SIZE = 1000;
/**
* 上次清理缓存的时间
*/
private volatile long lastCleanupTime = 0;
/**
* 清理间隔(毫秒)
*/
private static final long CLEANUP_INTERVAL = 30000L;
/**
* 检查是否为重复事件
*/
public boolean isDuplicateEvent(WorkPanelEvent event) {
if (event == null) {
return false;
}
// 定期清理过期缓存
cleanupExpiredCache();
// 生成事件唯一标识
String eventKey = generateEventKey(event);
WorkPanelEvent cachedEvent = recentEventsCache.get(eventKey);
if (cachedEvent != null) {
// 检查缓存是否过期
long currentTime = System.currentTimeMillis();
if ((currentTime - cachedEvent.getTimestamp()) <= CACHE_EXPIRY_TIME) {
// 检查事件内容是否相同
return isEventContentEqual(event, cachedEvent);
} else {
// 缓存过期,移除
recentEventsCache.remove(eventKey);
}
}
return false;
}
/**
* 更新最近事件缓存
*/
public void updateRecentEventsCache(WorkPanelEvent event) {
if (event == null) {
return;
}
// 控制缓存大小,防止内存溢出
if (recentEventsCache.size() >= MAX_CACHE_SIZE) {
// 移除最老的条目(简单实现,实际可以根据LRU算法优化)
java.util.Iterator<Map.Entry<String, WorkPanelEvent>> iterator = recentEventsCache.entrySet().iterator();
if (iterator.hasNext()) {
iterator.next();
iterator.remove();
}
}
// 添加新条目
String eventKey = generateEventKey(event);
recentEventsCache.put(eventKey, event);
}
/**
* 生成事件唯一标识
*/
private String generateEventKey(WorkPanelEvent event) {
StringBuilder key = new StringBuilder();
key.append(event.getType() != null ? event.getType() : "");
key.append("_");
// 获取工具名称(如果是ToolEvent)
String toolName = "";
if (event instanceof ToolEvent) {
toolName = ((ToolEvent) event).getToolName();
}
key.append(toolName != null ? toolName : "");
key.append("_");
// 获取思考类型(如果是ThoughtEvent)
String thinkingType = "";
if (event instanceof ThoughtEvent) {
thinkingType = ((ThoughtEvent) event).getThinkingType();
}
key.append(thinkingType != null ? thinkingType : "");
// 对于思考事件,添加内容摘要
if ("thought".equals(event.getType()) && event instanceof ThoughtEvent) {
ThoughtEvent thoughtEvent = (ThoughtEvent) event;
if (thoughtEvent.getContent() != null) {
// 取内容的前50个字符作为摘要
String contentSummary = thoughtEvent.getContent().length() > 50 ?
thoughtEvent.getContent().substring(0, 50) : thoughtEvent.getContent();
key.append("_").append(contentSummary.hashCode());
}
}
return key.toString();
}
/**
* 检查两个事件的内容是否相等
*/
private boolean isEventContentEqual(WorkPanelEvent event1, WorkPanelEvent event2) {
if (event1 == event2) {
return true;
}
if (event1 == null || event2 == null) {
return false;
}
// 比较基本字段
if (!java.util.Objects.equals(event1.getType(), event2.getType())) {
return false;
}
// 比较工具名称(如果是ToolEvent)
String toolName1 = "";
String toolName2 = "";
if (event1 instanceof ToolEvent) {
toolName1 = ((ToolEvent) event1).getToolName();
}
if (event2 instanceof ToolEvent) {
toolName2 = ((ToolEvent) event2).getToolName();
}
if (!java.util.Objects.equals(toolName1, toolName2)) {
return false;
}
// 比较思考类型(如果是ThoughtEvent)
String thinkingType1 = "";
String thinkingType2 = "";
if (event1 instanceof ThoughtEvent) {
thinkingType1 = ((ThoughtEvent) event1).getThinkingType();
}
if (event2 instanceof ThoughtEvent) {
thinkingType2 = ((ThoughtEvent) event2).getThinkingType();
}
if (!java.util.Objects.equals(thinkingType1, thinkingType2)) {
return false;
}
// 比较内容字段(根据不同事件类型)
if (event1 instanceof ThoughtEvent && event2 instanceof ThoughtEvent) {
ThoughtEvent thought1 = (ThoughtEvent) event1;
ThoughtEvent thought2 = (ThoughtEvent) event2;
return java.util.Objects.equals(thought1.getContent(), thought2.getContent());
}
// 比较工具输入(确保都是ToolEvent)
if (event1 instanceof ToolEvent && event2 instanceof ToolEvent) {
if (!java.util.Objects.equals(((ToolEvent) event1).getToolInput(),
((ToolEvent) event2).getToolInput())) {
return false;
}
// 比较工具输出
if (!java.util.Objects.equals(((ToolEvent) event1).getToolOutput(),
((ToolEvent) event2).getToolOutput())) {
return false;
}
} else if (event1 instanceof ResultEvent && event2 instanceof ResultEvent) {
// 比较结果内容
String content1 = ((ResultEvent) event1).getContent();
String content2 = ((ResultEvent) event2).getContent();
if (!java.util.Objects.equals(content1, content2)) {
return false;
}
}
return true;
}
/**
* 清理过期缓存
*/
private void cleanupExpiredCache() {
long currentTime = System.currentTimeMillis();
// 检查是否需要执行清理
if (currentTime - lastCleanupTime < CLEANUP_INTERVAL) {
return;
}
// 更新上次清理时间
lastCleanupTime = currentTime;
// 清理过期条目
recentEventsCache.entrySet().removeIf(entry -> {
WorkPanelEvent event = entry.getValue();
return event == null || (currentTime - event.getTimestamp()) > CACHE_EXPIRY_TIME;
});
}
/**
* 清空缓存
*/
public void clearCache() {
recentEventsCache.clear();
}
}
\ No newline at end of file
package pangea.hiagent.workpanel.event;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import pangea.hiagent.web.dto.EmbedEvent;
import pangea.hiagent.web.dto.LogEvent;
import pangea.hiagent.web.dto.ResultEvent;
import pangea.hiagent.web.dto.ThoughtEvent;
import pangea.hiagent.web.dto.ToolEvent;
import pangea.hiagent.web.dto.WorkPanelEvent;
import pangea.hiagent.workpanel.WorkPanelUtils;
import pangea.hiagent.workpanel.data.ErrorEventDataBuilder;
import pangea.hiagent.workpanel.data.MapPoolService;
import pangea.hiagent.workpanel.data.TokenEventDataBuilder;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.io.IOException;
/**
* 统一事件服务
* 整合事件创建、构建、发送等功能,事件处理架构
*/
@Slf4j
@Service
public class EventService {
@Autowired
private MapPoolService mapPoolService;
@Autowired
private ErrorEventDataBuilder errorEventDataBuilder;
@Autowired
private TokenEventDataBuilder tokenEventDataBuilder;
/**
* 工具调用状态跟踪映射
* key: toolName + timestamp
* value: pending状态的事件
*/
private final Map<String, ToolEvent> pendingToolCalls = new ConcurrentHashMap<>();
// 日期格式化对象,用于日志输出
private static final java.text.SimpleDateFormat DATE_FORMAT = new java.text.SimpleDateFormat("HH:mm:ss");
/**
* 记录工具调用开始事件
*
* @param toolName 工具名称
* @param input 工具输入参数
* @return 工具调用开始事件
*/
public ToolEvent recordToolCallStart(String toolName, Object input) {
try {
long currentTime = System.currentTimeMillis();
ToolEvent event = ToolEvent.builder()
.type("tool_call")
.timestamp(currentTime)
.toolName(toolName != null ? toolName : "未知工具")
.toolInput(WorkPanelUtils.convertToMap(input))
.toolStatus("pending")
.build();
// 跟踪pending状态的工具调用
String key = generateToolCallKey(toolName, currentTime);
pendingToolCalls.put(key, event);
// 输出工具调用日志
logToolCallStart(toolName, input, currentTime);
log.debug("已记录工具调用开始: 工具={}, 状态=pending", toolName);
return event;
} catch (Exception e) {
log.error("记录工具调用开始时发生错误: toolName={}", toolName, e);
return null;
}
}
/**
* 记录工具调用完成事件
*
* @param toolName 工具名称
* @param output 工具输出结果
* @param status 执行状态(success/failure)
* @param executionTime 执行时间(毫秒)
* @return 工具调用完成事件
*/
public ToolEvent recordToolCallComplete(String toolName, Object output, String status, Long executionTime) {
return handleToolCallCompletion(toolName, output, status, executionTime);
}
/**
* 记录工具调用错误事件
*
* @param toolName 工具名称
* @param errorMessage 错误信息
* @return 工具调用错误事件
*/
public ToolEvent recordToolCallError(String toolName, String errorMessage) {
return handleToolCallError(toolName, errorMessage);
}
/**
* 记录思考事件
*
* @param content 思考内容
* @param thinkingType 思考类型(分析、规划、执行等)
* @return 思考事件
*/
public ThoughtEvent recordThinking(String content, String thinkingType) {
try {
// 过滤掉空内容,避免记录过多无关信息
if (content == null || content.trim().isEmpty()) {
return null;
}
ThoughtEvent event = ThoughtEvent.builder()
.type("thought")
.timestamp(System.currentTimeMillis())
.content(content)
.thinkingType(thinkingType != null ? thinkingType : "reasoning")
.build();
log.debug("已记录思考过程: 类型={}, 内容={}", thinkingType, content);
return event;
} catch (Exception e) {
log.error("记录思考过程时发生错误: content={}, type={}", content, thinkingType, e);
return null;
}
}
/**
* 记录最终答案事件
*
* @param content 最终答案内容
* @return 最终答案事件
*/
public ResultEvent recordFinalAnswer(String content) {
try {
ResultEvent event = ResultEvent.builder()
.type("result")
.timestamp(System.currentTimeMillis())
.content(content)
.build();
log.debug("已记录最终答案: 内容={}", content);
return event;
} catch (Exception e) {
log.error("记录最终答案时发生错误: content={}", content, e);
return null;
}
}
/**
* 记录日志事件
*
* @param message 日志消息
* @param level 日志级别(info/warn/error/debug)
* @return 日志事件
*/
public LogEvent recordLog(String message, String level) {
try {
LogEvent event = LogEvent.builder()
.type("log")
.timestamp(System.currentTimeMillis())
.content(message)
.logLevel(level != null ? level : "info")
.build();
log.debug("已记录日志: 级别={}, 消息={}", level, message);
return event;
} catch (Exception e) {
log.error("记录日志时发生错误: message={}, level={}", message, level, e);
return null;
}
}
/**
* 记录嵌入事件
*
* @param url 嵌入资源URL(可选)
* @param type MIME类型
* @param title 嵌入标题
* @param htmlContent HTML内容(可选)
* @return 嵌入事件
*/
public EmbedEvent recordEmbed(String url, String type, String title, String htmlContent) {
try {
EmbedEvent event = EmbedEvent.builder()
.type("embed")
.timestamp(System.currentTimeMillis())
.embedUrl(url)
.embedType(type)
.embedTitle(title != null ? title : "网页预览")
.embedHtmlContent(htmlContent)
.build();
log.debug("已记录嵌入事件: 标题={}, URL={}", title, url);
return event;
} catch (Exception e) {
log.error("记录嵌入事件时发生错误: title={}, url={}", title, url, e);
return null;
}
}
/**
* 构建工作面板事件数据
*
* @param event 工作面板事件
* @return 事件数据
*/
public Map<String, Object> buildWorkPanelEventData(WorkPanelEvent event) {
if (event == null) {
return null;
}
// 从对象池获取Map,避免频繁创建对象
Map<String, Object> data = acquireMap();
// 设置基础属性
data.put("type", event.getType());
data.put("eventType", event.getType());
data.put("timestamp", event.getTimestamp());
// 根据事件类型设置特有属性
setEventSpecificProperties(event, data);
// 处理事件类型特定逻辑
processEventTypeSpecifics(event, data);
return data;
}
/**
* 发送工作面板事件给指定的SSE连接
*
* @param emitter SSE发射器
* @param event 工作面板事件
* @throws IOException IO异常
*/
public void sendWorkPanelEvent(SseEmitter emitter, WorkPanelEvent event) throws IOException {
if (event == null) {
log.warn("工作面板事件为空,无法发送事件");
return;
}
try {
// 检查emitter是否已经完成,避免向已完成的连接发送数据
java.lang.reflect.Field completedField = SseEmitter.class.getDeclaredField("completed");
completedField.setAccessible(true);
boolean isCompleted = completedField.getBoolean(emitter);
if (isCompleted) {
log.debug("SSE emitter已完成,跳过发送工作面板事件");
return;
}
// 构建事件数据
Map<String, Object> data = buildWorkPanelEventData(event);
if (data != null) {
log.debug("准备发送工作面板事件: 类型={}, 事件内容={}", event.getType(), event);
log.debug("事件数据: {}", data);
// 发送事件
emitter.send(org.springframework.web.servlet.mvc.method.annotation.SseEmitter.event().name("message").data(data));
log.debug("工作面板事件发送成功: 类型={}", event.getType());
} else {
log.warn("构建事件数据失败,无法发送事件: 类型={}", event.getType());
}
} catch (IllegalStateException e) {
// 处理 emitter 已关闭的情况
log.debug("无法发送工作面板事件,emitter已关闭或完成: {}", e.getMessage());
// 不重新抛出异常,避免影响主流程
} catch (Exception e) {
// 记录详细错误信息,但不中断主流程
if (!(e instanceof java.lang.reflect.InaccessibleObjectException) && !(e instanceof java.lang.NoSuchFieldException)) {
log.error("发送工作面板事件失败: 类型={}, 错误={}", event.getType(), e.getMessage(), e);
}
// 其他异常不重新抛出,避免影响主流程
}
}
/**
* 发送工作面板事件给指定用户
*
* @param emitter SSE发射器
* @param event 工作面板事件
*/
public void sendWorkPanelEventToUser(SseEmitter emitter, WorkPanelEvent event) {
log.debug("开始发送工作面板事件");
if (emitter != null) {
try {
// 直接向当前 emitter 发送事件
sendWorkPanelEvent(emitter, event);
log.debug("已发送工作面板事件到客户端: {}", event.getType());
} catch (IOException e) {
log.error("发送工作面板事件失败: {}", e.getMessage(), e);
}
} else {
log.debug("连接已失效,跳过发送事件: {}", event.getType());
}
}
/**
* 发送错误事件
*
* @param emitter SSE发射器
* @param errorMessage 错误信息
* @throws IOException IO异常
*/
public void sendErrorEvent(SseEmitter emitter, String errorMessage) throws IOException {
if (emitter == null) {
log.warn("SSE发射器为空,无法发送错误事件");
return;
}
try {
// 检查emitter是否已经完成,避免向已完成的连接发送数据
java.lang.reflect.Field completedField = SseEmitter.class.getDeclaredField("completed");
completedField.setAccessible(true);
boolean isCompleted = completedField.getBoolean(emitter);
if (isCompleted) {
log.debug("SSE emitter已完成,跳过发送错误事件");
return;
}
// 构建错误事件数据
Map<String, Object> data = errorEventDataBuilder.createErrorEventData(errorMessage);
if (data != null) {
log.debug("准备发送错误事件: 错误信息={}", errorMessage);
log.debug("错误事件数据: {}", data);
// 发送事件
emitter.send(SseEmitter.event().name("error").data(data));
log.debug("错误事件发送成功");
} else {
log.warn("构建错误事件数据失败,无法发送事件");
}
} catch (IllegalStateException e) {
// 处理 emitter 已关闭的情况
log.debug("无法发送错误事件,emitter已关闭或完成: {}", e.getMessage());
// 不重新抛出异常,避免影响主流程
} catch (Exception e) {
// 记录详细错误信息,但不中断主流程
if (!(e instanceof java.lang.reflect.InaccessibleObjectException) && !(e instanceof java.lang.NoSuchFieldException)) {
log.error("发送错误事件失败: 错误信息={}, 错误={}", errorMessage, e.getMessage(), e);
}
// 其他异常不重新抛出,避免影响主流程
}
}
/**
* 发送Token事件
*
* @param emitter SSE发射器
* @param token Token内容
* @throws IOException IO异常
*/
public void sendTokenEvent(SseEmitter emitter, String token) throws IOException {
if (emitter == null) {
log.warn("SSE发射器为空,无法发送token事件");
return;
}
try {
// 检查emitter是否已经完成,避免向已完成的连接发送数据
java.lang.reflect.Field completedField = SseEmitter.class.getDeclaredField("completed");
completedField.setAccessible(true);
boolean isCompleted = completedField.getBoolean(emitter);
if (isCompleted) {
log.debug("SSE emitter已完成,跳过发送token事件");
return;
}
// 构建token事件数据
Map<String, Object> data = tokenEventDataBuilder.createOptimizedTokenEventData(token);
if (data != null) {
// log.debug("准备发送token事件: token长度={}", token != null ? token.length() : 0);
// 发送事件
emitter.send(SseEmitter.event().name("token").data(data));
// log.debug("token事件发送成功");
} else {
log.warn("构建token事件数据失败,无法发送事件");
}
} catch (IllegalStateException e) {
// 处理 emitter 已关闭的情况
log.debug("无法发送token事件,emitter已关闭或完成: {}", e.getMessage());
// 不重新抛出异常,避免影响主流程
} catch (Exception e) {
// 记录详细错误信息,但不中断主流程
if (!(e instanceof java.lang.reflect.InaccessibleObjectException) && !(e instanceof java.lang.NoSuchFieldException)) {
log.error("发送token事件失败: token长度={}, 错误={}", token != null ? token.length() : 0, e.getMessage(), e);
}
// 其他异常不重新抛出,避免影响主流程
}
}
// ==================== 私有方法 ====================
/**
* 处理工具调用完成
*
* @param toolName 工具名称
* @param output 工具输出结果
* @param status 执行状态
* @param executionTime 执行时间
* @return 工作面板事件
*/
private ToolEvent handleToolCallCompletion(String toolName, Object output, String status, Long executionTime) {
long currentTime = System.currentTimeMillis();
// 查找对应的pending事件
ToolEvent pendingEvent = findPendingToolCall(toolName);
if (pendingEvent != null) {
// 移除跟踪记录
String key = generateToolCallKey(toolName, pendingEvent.getTimestamp());
pendingToolCalls.remove(key);
// 更新现有事件
updatePendingEventForCompletion(pendingEvent, output, status, executionTime, currentTime);
// 输出工具调用完成日志
logToolCallCompletion(toolName, output, status, executionTime, currentTime);
log.debug("已记录工具调用完成: 工具={}, 状态={}, 执行时间={}ms", toolName, status, executionTime);
return pendingEvent;
} else {
// 如果没有对应的pending事件,创建一个新事件
ToolEvent event = createNewCompletionEvent(toolName, output, status, executionTime, currentTime);
log.debug("已记录工具调用完成(无对应pending事件): 工具={}, 状态={}, 执行时间={}ms", toolName, status, executionTime);
return event;
}
}
/**
* 处理工具调用错误
*
* @param toolName 工具名称
* @param errorMessage 错误信息
* @return 工作面板事件
*/
private ToolEvent handleToolCallError(String toolName, String errorMessage) {
// 查找对应的pending事件
ToolEvent pendingEvent = findPendingToolCall(toolName);
if (pendingEvent != null) {
// 移除跟踪记录
String key = generateToolCallKey(toolName, pendingEvent.getTimestamp());
pendingToolCalls.remove(key);
// 更新现有事件为错误状态
pendingEvent.setType("tool_error");
// 使用反射设置content字段
try {
java.lang.reflect.Field contentField = WorkPanelEvent.class.getDeclaredField("content");
contentField.setAccessible(true);
contentField.set(pendingEvent, errorMessage);
} catch (Exception e) {
// 如果反射失败,忽略错误
}
pendingEvent.setToolStatus("failure");
pendingEvent.setTimestamp(System.currentTimeMillis());
log.debug("已记录工具调用错误: 工具={}, 错误={}", toolName, errorMessage);
return pendingEvent;
} else {
// 如果没有对应的pending事件,创建一个新事件
ToolEvent event = ToolEvent.builder()
.type("tool_error")
.timestamp(System.currentTimeMillis())
.toolName(toolName)
.toolStatus("failure")
.build();
// 使用反射设置content字段
try {
java.lang.reflect.Field contentField = WorkPanelEvent.class.getDeclaredField("content");
contentField.setAccessible(true);
contentField.set(event, errorMessage);
} catch (Exception e) {
// 如果反射失败,忽略错误
}
log.debug("已记录工具调用错误(无对应pending事件): 工具={}, 错误={}", toolName, errorMessage);
return event;
}
}
/**
* 生成工具调用键
*
* @param toolName 工具名称
* @param timestamp 时间戳
* @return 工具调用键
*/
private String generateToolCallKey(String toolName, long timestamp) {
return (toolName != null ? toolName : "unknown") + "_" + timestamp;
}
/**
* 查找pending状态的工具调用事件
*
* @param toolName 工具名称
* @return pending状态的事件,如果未找到则返回null
*/
private ToolEvent findPendingToolCall(String toolName) {
if (toolName == null) {
return null;
}
// 查找最近的该工具的pending事件
for (Map.Entry<String, ToolEvent> entry : pendingToolCalls.entrySet()) {
ToolEvent event = entry.getValue();
if (toolName.equals(event.getToolName()) && "pending".equals(event.getToolStatus())) {
return event;
}
}
return null;
}
/**
* 更新pending事件为完成状态
*
* @param pendingEvent pending状态的事件
* @param output 工具输出结果
* @param status 执行状态
* @param executionTime 执行时间
* @param currentTime 当前时间
*/
private void updatePendingEventForCompletion(ToolEvent pendingEvent, Object output, String status, Long executionTime, long currentTime) {
pendingEvent.setType(WorkPanelUtils.getEventTypeFromStatus(status));
pendingEvent.setToolOutput(output);
pendingEvent.setToolStatus(status != null ? status : "unknown");
pendingEvent.setExecutionTime(executionTime);
pendingEvent.setTimestamp(currentTime);
}
/**
* 创建新的完成事件
*
* @param toolName 工具名称
* @param output 工具输出结果
* @param status 执行状态
* @param executionTime 执行时间
* @param currentTime 当前时间
* @return 新的完成事件
*/
private ToolEvent createNewCompletionEvent(String toolName, Object output, String status, Long executionTime, long currentTime) {
return ToolEvent.builder()
.type(WorkPanelUtils.getEventTypeFromStatus(status))
.timestamp(currentTime)
.toolName(toolName != null ? toolName : "未知工具")
.toolOutput(output)
.toolStatus(status != null ? status : "unknown")
.executionTime(executionTime)
.build();
}
/**
* 输出工具调用开始日志
*
* @param toolName 工具名称
* @param input 工具输入参数
* @param currentTime 当前时间
*/
private void logToolCallStart(String toolName, Object input, long currentTime) {
String formattedTime = DATE_FORMAT.format(new java.util.Date(currentTime));
log.info("\n🔧 工具调用: [{}]\n⏰ 时间: {}\n📥 输入: {}\n📊 状态: 处理中",
toolName != null ? toolName : "未知工具",
formattedTime,
WorkPanelUtils.convertToJsonString(input));
}
/**
* 输出工具调用完成日志
*
* @param toolName 工具名称
* @param output 工具输出结果
* @param status 执行状态
* @param executionTime 执行时间
* @param currentTime 当前时间
*/
private void logToolCallCompletion(String toolName, Object output, String status, Long executionTime, long currentTime) {
String formattedTime = DATE_FORMAT.format(new java.util.Date(currentTime));
if ("success".equals(status)) {
log.info("\n🔧 工具调用: [{}]\n⏰ 时间: {}\n✅ 状态: 成功\n📤 输出: {}{}",
toolName != null ? toolName : "未知工具",
formattedTime,
WorkPanelUtils.convertToJsonString(output),
executionTime != null ? "\n⏱️ 耗时: " + executionTime + "ms" : "");
} else if ("failure".equals(status) || "error".equals(status)) {
log.info("\n🔧 工具调用: [{}]\n⏰ 时间: {}\n❌ 状态: 失败\n💬 错误: {}{}",
toolName != null ? toolName : "未知工具",
formattedTime,
WorkPanelUtils.convertToJsonString(output),
executionTime != null ? "\n⏱️ 耗时: " + executionTime + "ms" : "");
}
}
/**
* 根据事件类型设置特有属性
*
* @param event 工作面板事件
* @param data 事件数据
*/
private void setEventSpecificProperties(WorkPanelEvent event, Map<String, Object> data) {
String eventType = event.getType();
// 设置通用属性
if (event instanceof ThoughtEvent) {
data.put("content", ((ThoughtEvent) event).getContent());
} else if (event instanceof LogEvent) {
data.put("content", ((LogEvent) event).getContent());
} else if (event instanceof ResultEvent) {
data.put("content", ((ResultEvent) event).getContent());
}
switch (eventType) {
case "thought":
// 思考事件特有属性
if (event instanceof ThoughtEvent) {
ThoughtEvent thoughtEvent = (ThoughtEvent) event;
data.put("thinkingType", thoughtEvent.getThinkingType());
}
data.put("title", generateTitleForThought(event));
break;
case "tool_call":
case "tool_result":
case "tool_error":
// 工具事件特有属性
if (event instanceof ToolEvent) {
ToolEvent toolEvent = (ToolEvent) event;
data.put("toolName", toolEvent.getToolName());
data.put("toolInput", toolEvent.getToolInput());
data.put("toolOutput", toolEvent.getToolOutput());
data.put("toolStatus", toolEvent.getToolStatus());
data.put("executionTime", toolEvent.getExecutionTime());
// 根据状态设置错误信息
if ("tool_error".equals(eventType)) {
data.put("errorMessage", toolEvent.getErrorMessage());
data.put("errorCode", toolEvent.getErrorCode());
}
}
data.put("title", event instanceof ToolEvent && ((ToolEvent) event).getToolName() != null ? ((ToolEvent) event).getToolName() : "未知工具");
break;
case "log":
// 日志事件特有属性
if (event instanceof LogEvent) {
LogEvent logEvent = (LogEvent) event;
data.put("logLevel", logEvent.getLogLevel());
}
data.put("title", "日志");
break;
case "embed":
// 嵌入事件特有属性
if (event instanceof EmbedEvent) {
EmbedEvent embedEvent = (EmbedEvent) event;
data.put("embedUrl", embedEvent.getEmbedUrl());
data.put("embedType", embedEvent.getEmbedType());
data.put("embedTitle", embedEvent.getEmbedTitle());
data.put("embedHtmlContent", embedEvent.getEmbedHtmlContent());
}
data.put("title", event instanceof EmbedEvent && ((EmbedEvent) event).getEmbedTitle() != null ? ((EmbedEvent) event).getEmbedTitle() : "网页预览");
break;
case "result":
// 结果事件特有属性
data.put("title", "最终答案");
break;
default:
// 默认标题
data.put("title", generateDefaultTitle(event));
break;
}
}
/**
* 生成思考事件的标题
*
* @param event 思考事件
* @return 标题
*/
private String generateTitleForThought(WorkPanelEvent event) {
String content = null;
if (event instanceof ThoughtEvent) {
content = ((ThoughtEvent) event).getContent();
} else if (event instanceof LogEvent) {
content = ((LogEvent) event).getContent();
} else if (event instanceof ResultEvent) {
content = ((ResultEvent) event).getContent();
}
if (content != null && !content.isEmpty()) {
return content.substring(0, Math.min(content.length(), 50)) + (content.length() > 50 ? "..." : "");
}
return "思考过程";
}
/**
* 生成默认标题
*
* @param event 事件
* @return 标题
*/
private String generateDefaultTitle(WorkPanelEvent event) {
String content = null;
if (event instanceof ThoughtEvent) {
content = ((ThoughtEvent) event).getContent();
} else if (event instanceof LogEvent) {
content = ((LogEvent) event).getContent();
} else if (event instanceof ResultEvent) {
content = ((ResultEvent) event).getContent();
}
if (content != null && !content.isEmpty()) {
return content.substring(0, Math.min(content.length(), 50)) + (content.length() > 50 ? "..." : "");
}
return "事件";
}
/**
* 处理事件类型特定逻辑
*
* @param event 工作面板事件
* @param data 事件数据
*/
private void processEventTypeSpecifics(WorkPanelEvent event, Map<String, Object> data) {
String eventType = event.getType();
if (eventType != null) {
switch (eventType) {
case "tool_call":
case "tool_result":
case "tool_error":
// 处理工具事件
if (event instanceof ToolEvent) {
processToolEvent((ToolEvent) event, data);
}
break;
case "thought":
// 处理思考事件
if (event instanceof ThoughtEvent) {
processThinkingEvent((ThoughtEvent) event, data);
}
break;
// 其他事件类型可以根据需要添加处理逻辑
}
}
}
/**
* 处理工具事件
*
* @param event 工具事件
* @param data 事件数据
*/
private void processToolEvent(ToolEvent event, Map<String, Object> data) {
if (event == null || data == null) {
return;
}
// 添加工具事件特定的日志
if (log.isInfoEnabled()) {
log.info("[工具事件] 类型={}, 工具={}, 有toolInput={}, 有toolOutput={}",
event.getType(),
event.getToolName(),
event.getToolInput() != null,
event.getToolOutput() != null);
}
if (log.isDebugEnabled()) {
log.debug("[工具事件详情] toolInput={}, toolOutput={}",
event.getToolInput(),
event.getToolOutput());
}
}
/**
* 处理思考事件
*
* @param event 思考事件
* @param data 事件数据
*/
private void processThinkingEvent(ThoughtEvent event, Map<String, Object> data) {
if (event == null || data == null) {
return;
}
// 对于最终答案类型的思考事件,添加特殊标记
if ("final_answer".equals(event.getThinkingType())) {
data.put("isFinalAnswer", true);
}
}
/**
* 从对象池获取HashMap实例
*/
private Map<String, Object> acquireMap() {
return mapPoolService.acquireMap();
}
/**
* 获取对象池统计信息
*/
public String getMapPoolStatistics() {
return mapPoolService.getMapPoolStatistics();
}
/**
* 将HashMap实例归还到对象池
*/
public void releaseMap(Map<String, Object> map) {
mapPoolService.releaseMap(map);
}
}
\ No newline at end of file
package pangea.hiagent.workpanel.event;
import org.springframework.stereotype.Component;
/**
* 事件类型转换工具类
* 统一处理事件类型转换逻辑,避免在多个地方重复实现
*/
@Component
public class EventTypeConverter {
/**
* 根据状态确定事件类型
*/
public String getEventTypeFromStatus(String status) {
if (status == null) {
return "tool_result";
}
switch (status.toLowerCase()) {
case "success":
return "tool_result";
case "error":
case "failure":
return "tool_error";
default:
return "tool_result";
}
}
/**
* 获取状态文本
*/
public String getStatusText(String status) {
if (status == null) {
return "未知";
}
switch (status.toLowerCase()) {
case "success": return "成功";
case "pending": return "处理中";
case "error": return "错误";
case "failure": return "失败";
default: return status;
}
}
}
\ No newline at end of file
package pangea.hiagent.workpanel.event;
import lombok.extern.slf4j.Slf4j;
import pangea.hiagent.agent.service.UserSseService;
import pangea.hiagent.web.dto.ToolEvent;
import pangea.hiagent.web.dto.WorkPanelEvent;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
* SSE事件广播器
* 专门负责广播事件给所有订阅者
*/
@Slf4j
@Component
public class SseEventBroadcaster {
@Autowired
private UserSseService unifiedSseService;
@Autowired
private EventService eventService;
/**
* 广播工作面板事件给所有订阅者
*
* @param event 工作面板事件
*/
public void broadcastWorkPanelEvent(WorkPanelEvent event) {
if (event == null) {
log.warn("广播事件时接收到null事件");
return;
}
try {
// 预构建事件数据,避免重复构建
Map<String, Object> eventData = eventService.buildWorkPanelEventData(event);
try {
// 获取所有emitter并广播
List<SseEmitter> emitters = unifiedSseService.getEmitters();
int successCount = 0;
int failureCount = 0;
// 使用CopyOnWriteArrayList避免并发修改异常
for (SseEmitter emitter : new CopyOnWriteArrayList<>(emitters)) {
try {
// 检查emitter是否仍然有效
if (unifiedSseService.isEmitterValid(emitter)) {
emitter.send(SseEmitter.event().name("message").data(eventData));
successCount++;
} else {
// 移除无效的emitter
log.debug("移除无效的SSE连接");
unifiedSseService.removeEmitter(emitter);
failureCount++;
}
} catch (IOException e) {
log.error("发送事件失败,移除失效连接: {}", e.getMessage());
unifiedSseService.removeEmitter(emitter);
failureCount++;
} catch (IllegalStateException e) {
log.debug("Emitter已关闭,移除连接: {}", e.getMessage());
unifiedSseService.removeEmitter(emitter);
failureCount++;
} catch (Exception e) {
log.error("发送事件时发生未知异常,移除连接: {}", e.getMessage(), e);
unifiedSseService.removeEmitter(emitter);
failureCount++;
}
}
if (failureCount > 0) {
log.warn("事件广播部分失败: 成功={}, 失败={}", successCount, failureCount);
}
// 记录对象池使用统计信息(每100次广播记录一次)
if ((successCount + failureCount) % 100 == 0) {
log.debug("对象池使用统计: {}", eventService.getMapPoolStatistics());
}
} finally {
// 确保eventData被归还到对象池
eventService.releaseMap(eventData);
}
} catch (Exception e) {
String toolName = null;
if (event instanceof ToolEvent) {
toolName = ((ToolEvent) event).getToolName();
}
log.error("广播事件失败: 事件类型={}, 工具={}, 错误信息={}",
event.getType(),
toolName,
e.getMessage(),
e);
}
}
}
\ No newline at end of file
...@@ -228,6 +228,40 @@ hiagent: ...@@ -228,6 +228,40 @@ hiagent:
top-k: 5 top-k: 5
score-threshold: 0.8 score-threshold: 0.8
# ReAct配置
react:
system-prompt: >
You are a Spring AI tool orchestration assistant. Your TOP PRIORITY: ALWAYS CALL TOOLS FIRST, answer EXCLUSIVELY based on tool results.
=== CORE RULES ===
1. Tool-First Mandate: For any non-trivial query, EXECUTE RELEVANT TOOLS, never just describe them. Only use internal knowledge for simple common sense.
2. Result-Based Answers: All conclusions must come directly from tool execution results. Never fabricate data.
3. Multi-Tool Support: Call multiple tools in sequence where one tool's output feeds into the next.
4. Iterative Loop: If results are incomplete, re-analyze, adjust tools, and repeat until satisfactory.
5. Complex Queries: Use multiple tools for complex tasks; avoid single-tool reliance.
=== REACT PROCESS ===
Cyclic process for every query, execute in order until complete:
- Step 1 - THOUGHT: Analyze the query, break into sub-tasks, select relevant tools with alternatives, define execution sequence.
- Step 2 - ACTION: EXECUTE TOOLS DIRECTLY, NEVER JUST DESCRIBE THEM. Call specific tools in planned order, execute multiple if needed, use alternatives if a tool fails.
- Step 3 - OBSERVATION: Analyze all tool results, extract key insights, check completeness. If results are complete → Proceed to Final Answer; if incomplete → Return to Thought.
- Step 4 - FINAL ANSWER: Synthesize tool results into a clear, complete answer. Explain tool synergy if helpful. Keep it conversational.
=== RESPONSE FORMAT ===
Strictly follow this structure:
1. Thought: Problem analysis, tool selection, execution sequence
2. Action: Actual tool calls (not descriptions)
3. Observation: Key results summary, decision (terminate/restart)
4. Final_Answer: Result-based answer
=== HARD RULES ===
- Execute tools first, never just describe them
- Only use tool results for answers
- Use multiple tools for complex queries
- Support serial tool chaining
- Iterate until results are complete
- Follow Spring AI framework rules
# Milvus Lite配置 # Milvus Lite配置
milvus: milvus:
data-dir: ./milvus_data data-dir: ./milvus_data
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -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",
......
...@@ -56,6 +56,26 @@ import { ElMessage, ElMessageBox } from "element-plus"; ...@@ -56,6 +56,26 @@ import { ElMessage, ElMessageBox } from "element-plus";
import MessageItem from "./MessageItem.vue"; import MessageItem from "./MessageItem.vue";
import request from "@/utils/request"; import request from "@/utils/request";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import type { TimelineEvent } from '../types/timeline';
// 接收从父组件传递的添加事件到时间轴的方法
const props = defineProps<{
addEventToTimeline?: (event: TimelineEvent) => void;
}>();
// 生成唯一事件ID
const generateEventId = (): string => {
return `event-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
};
// 添加事件到时间轴
const addEventToTimeline = (event: TimelineEvent) => {
if (props.addEventToTimeline) {
props.addEventToTimeline(event);
} else {
console.warn('[ChatArea] addEventToTimeline prop is not provided');
}
};
interface Message { interface Message {
content: string; content: string;
...@@ -508,6 +528,17 @@ const processSSELine = async ( ...@@ -508,6 +528,17 @@ const processSSELine = async (
// 只设置流状态和加载状态 // 只设置流状态和加载状态
messages.value[aiMessageIndex].isStreaming = false; messages.value[aiMessageIndex].isStreaming = false;
isLoading.value = false; isLoading.value = false;
// 添加完成事件到时间轴
const completeEvent: TimelineEvent = {
id: generateEventId(),
type: "complete",
title: "对话完成",
content: "智能体已完成回答",
timestamp: Date.now(),
};
addEventToTimeline(completeEvent);
return true; // 返回true表示流已完成 return true; // 返回true表示流已完成
case "error": case "error":
...@@ -527,22 +558,32 @@ const processSSELine = async ( ...@@ -527,22 +558,32 @@ const processSSELine = async (
isLoading.value = false; isLoading.value = false;
// 记录错误日志便于调试 // 记录错误日志便于调试
console.error("[SSE错误事件]", data); console.error("[SSE错误事件]", data);
// 添加错误事件到时间轴
const errorEvent: TimelineEvent = {
id: generateEventId(),
type: "error",
title: "对话错误",
content: errorMsg || "未知错误",
timestamp: Date.now(),
};
addEventToTimeline(errorEvent);
return true; // 返回true表示流已完成 return true; // 返回true表示流已完成
case "thinking": case "thinking":
// 处理思考事件,将其发送到时间轴面板 // 处理思考事件,将其发送到时间轴面板
const event = { const thoughtEvent: TimelineEvent = {
id: generateEventId(),
type: "thought", type: "thought",
title: title:
data.thinkingType === "final_answer" ? "最终答案" : "思考过程", data.thinkingType === "final_answer" ? "最终答案" : "思考过程",
content: data.content, content: data.content,
timestamp: data.timestamp, timestamp: data.timestamp || Date.now(),
}; };
// 通过事件总线将事件发送到时间轴 // 调用添加事件到时间轴的方法
window.dispatchEvent( addEventToTimeline(thoughtEvent);
new CustomEvent("timeline-event", { detail: event })
);
// 如果是最终答案,也应该显示在主要对话框中 // 如果是最终答案,也应该显示在主要对话框中
// 修复:确保最终答案只添加一次,避免重复显示 // 修复:确保最终答案只添加一次,避免重复显示
...@@ -578,22 +619,24 @@ const processSSELine = async ( ...@@ -578,22 +619,24 @@ const processSSELine = async (
if (eventType === "tool_call") { if (eventType === "tool_call") {
if (data.toolName) metadata["工具"] = data.toolName; if (data.toolName) metadata["工具"] = data.toolName;
if (data.toolAction) metadata["操作"] = data.toolAction; if (data.toolAction) metadata["操作"] = data.toolAction;
if (data.toolInput) if (data.toolInput) {
metadata["输入"] = JSON.stringify(data.toolInput).substring( try {
0, metadata["输入"] = JSON.stringify(data.toolInput).substring(0, 100);
100 } catch (e) {
); metadata["输入"] = String(data.toolInput).substring(0, 100);
if (data.toolOutput) }
metadata["输出"] = String(data.toolOutput).substring(0, 100); }
if (data.toolOutput) metadata["输出"] = String(data.toolOutput).substring(0, 100);
if (data.toolStatus) metadata["状态"] = data.toolStatus; if (data.toolStatus) metadata["状态"] = data.toolStatus;
if (data.executionTime) if (data.executionTime) metadata["耗时"] = `${data.executionTime}ms`;
metadata["耗时"] = `${data.executionTime}ms`;
} else if (eventType === "embed") { } else if (eventType === "embed") {
if (data.embedUrl) metadata["URL"] = data.embedUrl; if (data.embedUrl) metadata["URL"] = data.embedUrl;
if (data.embedType) metadata["类型"] = data.embedType; if (data.embedType) metadata["类型"] = data.embedType;
} }
const timelineEvent = { // 构建时间轴事件
const timelineEvent: TimelineEvent = {
id: generateEventId(),
type: eventType, type: eventType,
title: title, title: title,
content: data.content, content: data.content,
...@@ -611,11 +654,8 @@ const processSSELine = async ( ...@@ -611,11 +654,8 @@ const processSSELine = async (
timestamp: data.timestamp || Date.now(), timestamp: data.timestamp || Date.now(),
}; };
// 通过事件总线将事件发送到时间轴 // 调用添加事件到时间轴的方法
console.log("[ChatArea] 发送timeline-event事件:", timelineEvent); addEventToTimeline(timelineEvent);
window.dispatchEvent(
new CustomEvent("timeline-event", { detail: timelineEvent })
);
// 对于embed事件,还需要触发embed-event事件 // 对于embed事件,还需要触发embed-event事件
if (eventType === "embed" && data.embedUrl) { if (eventType === "embed" && data.embedUrl) {
......
<template> <template>
<div v-if="formStore.showForm"> <div class="form-container">
<hi-page-template <h2>表单渲染器(已简化)</h2>
ref="templateRef" <div class="form-content">
:json="formStore.formJson" <div class="form-field">
:open-intl="false" <label>输入框</label>
></hi-page-template> <el-input v-model="formData.input" placeholder="请输入"></el-input>
</div>
<div class="form-field">
<label>日期</label>
<el-date-picker v-model="formData.date" type="date" placeholder="选择日期"></el-date-picker>
</div>
</div>
<div class="button-wrap"> <div class="button-wrap">
<a-button type="primary" @click="submit">提交</a-button> <el-button type="primary" @click="submit">提交</el-button>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from "vue"; import { ref, reactive } from "vue";
import { useFormStore } from "@/stores/form"; import { ElInput, ElDatePicker, ElButton } from "element-plus";
import { ElMessage } from "element-plus";
import HiPageTemplate from "pangea-ui/hi-page-template";
// 获取表单 store
const formStore = useFormStore();
// 表单组件ref const formData = reactive({
const templateRef = ref(); input: '',
date: null
});
// 表单提交回调
const submit = () => { const submit = () => {
templateRef.value?.ctx.validate(1, (isValid: boolean, formData: any) => { console.log('表单数据:', formData);
if (isValid) { // 这里可以添加表单验证逻辑
// 追加隐藏字段到表单数据
const hiddenFields = formStore.hiddenFields;
hiddenFields.forEach((field) => {
if (field.name) {
// 根据字段 title 设置不同的值
if (field.title === "接访员工姓名") {
formData[field.name] = "张益达";
} else if (field.title === "接访员工手机号") {
formData[field.name] = "13611111111";
} else {
formData[field.name] = "none";
}
}
});
console.log("表单验证通过,提交数据:", formData);
// 调用 store 的 submitForm 方法,触发回调
formStore.submitForm(formData);
ElMessage.success("表单提交成功");
} else {
console.log("表单验证失败");
ElMessage.error("请检查表单填写是否正确");
}
});
}; };
</script> </script>
<style scoped> <style scoped>
.form-container {
background-color: white;
padding: 16px;
margin: 16px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.form-container h2 {
margin-top: 0;
margin-bottom: 16px;
color: #333;
}
.form-content {
margin-bottom: 16px;
}
.form-field {
margin-bottom: 16px;
}
.form-field label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #606266;
}
.button-wrap { .button-wrap {
display: flex; display: flex;
justify-content: center; justify-content: center;
......
<template> <template>
<TimelinePanel <div class="timeline-manager">
:events="events" <!-- 过滤和搜索面板 -->
:getEventTypeLabel="getEventTypeLabel" <div class="timeline-filter-panel">
:formatTime="formatTime" <div class="filter-row">
:getExpandedState="getExpandedState" <el-input
:toggleExpand="toggleExpand" v-model="searchQuery"
:isToolEventType="isToolEventType" placeholder="搜索事件..."
:hasValidToolInput="hasValidToolInput" clearable
:hasValidToolOutput="hasValidToolOutput" size="small"
:onClearTimeline="handleClearTimeline" prefix-icon="Search"
/> />
<el-select
v-model="selectedEventTypes"
placeholder="事件类型"
multiple
size="small"
class="filter-select"
>
<el-option
v-for="(label, type) in eventTypeLabels"
:key="type"
:label="label"
:value="type"
/>
</el-select>
</div>
<div class="filter-row">
<el-button
type="primary"
size="small"
@click="applyFilters"
>
应用过滤
</el-button>
<el-button
size="small"
@click="resetFilters"
>
重置
</el-button>
<el-button
size="small"
@click="exportEvents"
>
导出事件
</el-button>
<el-dropdown @command="handleExport">
<el-button size="small">
导出格式 <el-icon class="el-icon--right"><arrow-down /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="json">JSON</el-dropdown-item>
<el-dropdown-item command="csv">CSV</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<!-- 时间轴面板 -->
<TimelinePanel
:events="filteredEvents"
:getEventTypeLabel="getEventTypeLabel"
:formatTime="formatTime"
:getExpandedState="getExpandedState"
:toggleExpand="toggleExpand"
:isToolEventType="isToolEventType"
:hasValidToolInput="hasValidToolInput"
:hasValidToolOutput="hasValidToolOutput"
:onClearTimeline="handleClearTimeline"
/>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onUnmounted, onMounted } from 'vue' import { computed, ref, onUnmounted, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import type { TimelineEvent, ToolResultEvent } from '../types/timeline' import type { TimelineEvent, ToolResultEvent } from '../types/timeline'
import { eventTypeLabels } from '../types/timeline' import { eventTypeLabels } from '../types/timeline'
import { TimelineService } from '../services/TimelineService'
import TimelinePanel from './TimelinePanel.vue' import TimelinePanel from './TimelinePanel.vue'
import { isToolEventType, hasValidToolInput, hasValidToolOutput } from '../utils/timelineUtils';
import sseService from '../services/sseService';
// 事件数据 // 事件数据
const events = ref<TimelineEvent[]>([]); const events = ref<TimelineEvent[]>([]);
// 过滤和搜索状态
const searchQuery = ref('');
const selectedEventTypes = ref<string[]>([]);
const activeFilters = ref({
searchQuery: '',
eventTypes: [] as string[]
});
// 持久化配置
const persistenceEnabled = ref(true);
const STORAGE_KEY = 'timeline_events';
// 从本地存储加载事件
const loadEventsFromStorage = () => {
if (!persistenceEnabled.value) return;
try {
const storedEvents = localStorage.getItem(STORAGE_KEY);
if (storedEvents) {
const parsedEvents = JSON.parse(storedEvents) as TimelineEvent[];
events.value = parsedEvents;
console.log('[TimelineContainer] 从本地存储加载了', parsedEvents.length, '个事件');
}
} catch (error) {
console.error('[TimelineContainer] 从本地存储加载事件失败:', error);
}
};
// 保存事件到本地存储
const saveEventsToStorage = () => {
if (!persistenceEnabled.value) return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(events.value));
console.log('[TimelineContainer] 事件已保存到本地存储');
} catch (error) {
console.error('[TimelineContainer] 保存事件到本地存储失败:', error);
}
};
// 获取事件类型标签 // 获取事件类型标签
const getEventTypeLabel = (type: string): string => { const getEventTypeLabel = (type: string): string => {
return eventTypeLabels[type] || type; return eventTypeLabels[type] || type;
...@@ -49,19 +153,121 @@ const toggleExpand = (index: number): void => { ...@@ -49,19 +153,121 @@ const toggleExpand = (index: number): void => {
console.log('切换展开状态:', index); console.log('切换展开状态:', index);
}; };
// 工具事件类型判断 // 过滤后的事件列表
const isToolEventType = (type: string): boolean => { const filteredEvents = computed(() => {
return ['tool_call', 'tool_result', 'tool_error'].includes(type); let result = [...events.value];
// 应用搜索过滤
if (activeFilters.value.searchQuery) {
const query = activeFilters.value.searchQuery.toLowerCase();
result = result.filter(event => {
return (
event.title.toLowerCase().includes(query) ||
('content' in event && event.content && event.content.toLowerCase().includes(query)) ||
(event.type && event.type.toLowerCase().includes(query))
);
});
}
// 应用事件类型过滤
if (activeFilters.value.eventTypes.length > 0) {
result = result.filter(event => {
return activeFilters.value.eventTypes.includes(event.type);
});
}
return result;
});
// 应用过滤
const applyFilters = () => {
activeFilters.value = {
searchQuery: searchQuery.value,
eventTypes: [...selectedEventTypes.value]
};
};
// 重置过滤
const resetFilters = () => {
searchQuery.value = '';
selectedEventTypes.value = [];
activeFilters.value = {
searchQuery: '',
eventTypes: []
};
};
// 导出事件(默认JSON格式)
const exportEvents = () => {
handleExport('json');
};
// 处理导出
const handleExport = (format: 'json' | 'csv') => {
const exportData = filteredEvents.value;
if (exportData.length === 0) {
ElMessage.warning('没有可导出的事件');
return;
}
let content: string;
let fileName: string;
let mimeType: string;
if (format === 'json') {
content = JSON.stringify(exportData, null, 2);
fileName = `timeline_events_${new Date().toISOString().slice(0, 10)}.json`;
mimeType = 'application/json';
} else {
content = convertToCSV(exportData);
fileName = `timeline_events_${new Date().toISOString().slice(0, 10)}.csv`;
mimeType = 'text/csv';
}
downloadFile(content, fileName, mimeType);
}; };
// 工具输入有效性检查 // 转换为CSV格式
const hasValidToolInput = (event: TimelineEvent): boolean => { const convertToCSV = (events: TimelineEvent[]): string => {
return event.type === 'tool_call' && (event as any).toolInput !== null && (event as any).toolInput !== undefined; // 定义CSV表头
const headers = ['ID', 'Type', 'Title', 'Timestamp', 'Content', 'Metadata'];
// 转换数据行
const rows = events.map(event => {
// 安全获取content字段,只有部分事件类型有content字段
const content = 'content' in event ? event.content || '' : '';
return [
event.id,
event.type,
event.title,
new Date(event.timestamp).toISOString(),
content,
event.metadata ? JSON.stringify(event.metadata) : ''
];
});
// 合并表头和数据行
const csvContent = [
headers.join(','),
...rows.map(row => row.join(','))
].join('\n');
return csvContent;
}; };
// 工具输出有效性检查 // 下载文件
const hasValidToolOutput = (event: TimelineEvent): boolean => { const downloadFile = (content: string, fileName: string, mimeType: string) => {
return event.type === 'tool_result' && (event as any).toolOutput !== null && (event as any).toolOutput !== undefined; const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}; };
// 时间轴服务不再需要,因为我们现在直接处理事件 // 时间轴服务不再需要,因为我们现在直接处理事件
...@@ -71,16 +277,30 @@ const hasValidToolOutput = (event: TimelineEvent): boolean => { ...@@ -71,16 +277,30 @@ const hasValidToolOutput = (event: TimelineEvent): boolean => {
// console.log('[TimelineContainer] 成功添加事件:', event.type, event.title); // console.log('[TimelineContainer] 成功添加事件:', event.type, event.title);
// }); // });
// 生成唯一事件ID
const generateEventId = (): string => {
return 'evt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
};
// 添加时间轴事件 // 添加时间轴事件
const addEvent = (event: any) => { const addEvent = (event: any) => {
// 确保事件有唯一ID
const eventWithId = {
...event,
id: event.id || generateEventId()
};
// 直接添加事件到列表,而不是通过时间轴服务 // 直接添加事件到列表,而不是通过时间轴服务
events.value.push(event); events.value.push(eventWithId);
console.log('[TimelineContainer] 成功添加事件:', event.type, event.title); console.log('[TimelineContainer] 成功添加事件:', eventWithId.type, eventWithId.title);
// 保存到本地存储
saveEventsToStorage();
}; };
// 清除时间轴 // 清除时间轴
const handleClearTimeline = () => { const handleClearTimeline = () => {
events.value = []; events.value = [];
// 保存到本地存储
saveEventsToStorage();
// 不再调用timelineService.clearTimeline(),因为它是空实现 // 不再调用timelineService.clearTimeline(),因为它是空实现
// stateManager.clearAllStates(); // stateManager.clearAllStates();
// cacheService.clearAllCaches(); // cacheService.clearAllCaches();
...@@ -137,43 +357,122 @@ defineExpose({ ...@@ -137,43 +357,122 @@ defineExpose({
showPerformanceStats showPerformanceStats
}); });
// 组件卸载时清理资源 // 组件挂载时启动定期性能监控
onUnmounted(() => { let intervalId: number;
// 移除事件监听器
window.removeEventListener('timeline-event', handleTimelineEvent as EventListener);
});
// 处理timeline-event事件
const handleTimelineEvent = (e: Event) => {
const customEvent = e as CustomEvent;
const eventData = customEvent.detail;
console.log('[TimelineContainer] 接收到timeline-event事件:', eventData);
// 直接添加事件到列表
events.value.push(eventData);
console.log('[TimelineContainer] 成功添加事件:', eventData.type, eventData.title);
};
// 组件挂载时启动定期性能监控和事件监听
onMounted(() => { onMounted(() => {
// 监听timeline-event事件 // 从本地存储加载事件
window.addEventListener('timeline-event', handleTimelineEvent as EventListener); loadEventsFromStorage();
// 连接SSE服务
sseService.connect();
// 添加SSE事件监听器
sseService.on('timeline-event', handleTimelineEvent);
// 启动定期性能监控(每30秒输出一次) // 启动定期性能监控(每30秒输出一次)
const intervalId = setInterval(() => { intervalId = setInterval(() => {
if (events.value.length > 0) { if (events.value.length > 0) {
console.log('[TimelineContainer] 定期性能统计 - 事件总数:', events.value.length); console.log('[TimelineContainer] 定期性能统计 - 事件总数:', events.value.length);
} }
}, 30000); }, 30000);
});
// 组件卸载时清理资源
onUnmounted(() => {
// 清除定期性能监控定时器
clearInterval(intervalId);
// 保存事件到本地存储
saveEventsToStorage();
// 在组件卸载时清除定时器 // 移除SSE事件监听器
onUnmounted(() => { sseService.off('timeline-event', handleTimelineEvent);
clearInterval(intervalId); // 断开SSE连接
}); // 注意:这里不要断开SSE连接,因为其他组件可能也在使用
// sseService.disconnect();
}); });
// 处理SSE时间轴事件
const handleTimelineEvent = (eventData: any): void => {
try {
console.log('[TimelineContainer] 处理时间轴事件:', eventData);
// 验证事件数据格式
if (!eventData || typeof eventData !== 'object') {
console.error('[TimelineContainer] 无效的事件数据格式:', eventData);
return;
}
// 确保事件有基本属性
if (!eventData.type || !eventData.timestamp) {
console.error('[TimelineContainer] 事件缺少必要属性:', eventData);
return;
}
// 转换为TimelineEvent类型
const timelineEvent: TimelineEvent = {
id: eventData.id || ('evt_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)),
type: eventData.type,
title: eventData.title || eventData.type,
timestamp: eventData.timestamp,
content: eventData.content,
metadata: eventData.metadata,
// 工具事件特有属性
...(eventData.toolName && { toolName: eventData.toolName }),
...(eventData.toolInput && { toolInput: eventData.toolInput }),
...(eventData.toolOutput && { toolOutput: eventData.toolOutput }),
...(eventData.toolStatus && { toolStatus: eventData.toolStatus }),
...(eventData.executionTime !== undefined && { executionTime: eventData.executionTime }),
// 嵌入事件特有属性
...(eventData.embedUrl && { embedUrl: eventData.embedUrl }),
...(eventData.embedType && { embedType: eventData.embedType }),
...(eventData.embedTitle && { embedTitle: eventData.embedTitle }),
...(eventData.embedHtmlContent && { embedHtmlContent: eventData.embedHtmlContent })
};
// 添加事件到列表
addEvent(timelineEvent);
} catch (error) {
console.error('[TimelineContainer] 处理时间轴事件失败:', error);
}
};
</script> </script>
<style scoped> <style scoped>
.timeline-container-wrapper { .timeline-manager {
display: flex;
flex-direction: column;
height: 100%; height: 100%;
} }
.timeline-filter-panel {
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
padding: var(--spacing-2);
border-radius: var(--border-radius-base);
margin-bottom: var(--spacing-2);
}
.filter-row {
display: flex;
gap: var(--spacing-2);
align-items: center;
margin-bottom: var(--spacing-1);
flex-wrap: wrap;
}
.filter-row:last-child {
margin-bottom: 0;
}
.filter-select {
min-width: 150px;
}
.timeline-filter-panel .el-input {
max-width: 300px;
}
.timeline-filter-panel .el-button {
margin-right: var(--spacing-1);
}
</style> </style>
\ No newline at end of file
<template>
<div class="timeline-event-detail">
<div class="detail-header">
<h3>事件详情</h3>
<el-button type="primary" size="small" @click="handleClose">关闭</el-button>
</div>
<div v-if="event" class="detail-content">
<el-descriptions :column="2" border>
<el-descriptions-item label="ID">{{ event.id }}</el-descriptions-item>
<el-descriptions-item label="类型">{{ eventTypeLabels[event.type] || event.type }}</el-descriptions-item>
<el-descriptions-item label="标题">{{ event.title }}</el-descriptions-item>
<el-descriptions-item label="时间">
<span>{{ formatTime(event.timestamp) }}</span>
<span class="full-time">{{ new Date(event.timestamp).toISOString() }}</span>
</el-descriptions-item>
<el-descriptions-item label="内容" :span="2">
<div v-if="event.content" class="event-content">
{{ event.content }}
</div>
<div v-else class="empty-content">无内容</div>
</el-descriptions-item>
<el-descriptions-item label="元数据" :span="2">
<div v-if="event.metadata" class="metadata-section">
<pre>{{ JSON.stringify(event.metadata, null, 2) }}</pre>
</div>
<div v-else class="empty-content">无元数据</div>
</el-descriptions-item>
<!-- 工具事件特有字段 -->
<template v-if="isToolEventType(event.type)">
<el-descriptions-item label="工具名称" v-if="(event as any).toolName">{{ (event as any).toolName }}</el-descriptions-item>
<el-descriptions-item label="工具状态" v-if="(event as any).toolStatus">{{ (event as any).toolStatus }}</el-descriptions-item>
<el-descriptions-item label="执行时间" v-if="(event as any).executionTime">
{{ (event as any).executionTime }} ms
</el-descriptions-item>
<el-descriptions-item label="工具输入" :span="2" v-if="(event as any).toolInput">
<pre>{{ JSON.stringify((event as any).toolInput, null, 2) }}</pre>
</el-descriptions-item>
<el-descriptions-item label="工具输出" :span="2" v-if="(event as any).toolOutput">
<pre>{{ JSON.stringify((event as any).toolOutput, null, 2) }}</pre>
</el-descriptions-item>
<el-descriptions-item label="错误信息" :span="2" v-if="(event as any).errorMessage">
{{ (event as any).errorMessage }}
</el-descriptions-item>
</template>
<!-- Embed事件特有字段 -->
<template v-if="event.type === 'embed'">
<el-descriptions-item label="嵌入URL" v-if="(event as any).embedUrl">{{ (event as any).embedUrl }}</el-descriptions-item>
<el-descriptions-item label="嵌入类型" v-if="(event as any).embedType">{{ (event as any).embedType }}</el-descriptions-item>
<el-descriptions-item label="嵌入标题" v-if="(event as any).embedTitle">{{ (event as any).embedTitle }}</el-descriptions-item>
</template>
</el-descriptions>
</div>
<div v-else class="no-event">
<div class="empty-icon">📋</div>
<div class="empty-text">未选择事件</div>
</div>
</div>
</template>
<script setup lang="ts">
import { eventTypeLabels } from '../types/timeline'
import type { TimelineEvent } from '../types/timeline'
import { isToolEventType } from '../utils/timelineUtils'
// 定义组件属性
const props = defineProps<{
event: TimelineEvent | null
}>()
// 定义组件事件
const emit = defineEmits<{
close: []
}>()
// 格式化时间
const formatTime = (timestamp: number): string => {
const date = new Date(timestamp)
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${hours}:${minutes}:${seconds}`
}
// 关闭详情页
const handleClose = () => {
emit('close')
}
</script>
<style scoped>
.timeline-event-detail {
background-color: var(--bg-primary);
border-radius: var(--border-radius-base);
padding: var(--spacing-4);
height: 100%;
overflow-y: auto;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-3);
}
.detail-header h3 {
margin: 0;
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
}
.detail-content {
margin-top: var(--spacing-2);
}
.event-content {
background-color: var(--bg-tertiary);
padding: var(--spacing-2);
border-radius: var(--border-radius-sm);
font-family: var(--font-family-mono);
white-space: pre-wrap;
word-break: break-all;
}
.metadata-section {
background-color: var(--bg-tertiary);
padding: var(--spacing-2);
border-radius: var(--border-radius-sm);
overflow-x: auto;
}
.metadata-section pre {
margin: 0;
font-family: var(--font-family-mono);
font-size: var(--font-size-xs);
line-height: var(--line-height-normal);
}
.empty-content {
color: var(--text-tertiary);
font-style: italic;
padding: var(--spacing-2);
background-color: var(--bg-tertiary);
border-radius: var(--border-radius-sm);
}
.full-time {
font-size: var(--font-size-xs);
color: var(--text-tertiary);
margin-left: var(--spacing-2);
}
.no-event {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 200px;
color: var(--text-tertiary);
}
.empty-icon {
font-size: 48px;
margin-bottom: var(--spacing-2);
}
.empty-text {
font-size: var(--font-size-sm);
}
/* 滚动样式 */
.timeline-event-detail::-webkit-scrollbar {
width: 6px;
}
.timeline-event-detail::-webkit-scrollbar-track {
background: transparent;
}
.timeline-event-detail::-webkit-scrollbar-thumb {
background: var(--gray-300);
border-radius: 3px;
}
.timeline-event-detail::-webkit-scrollbar-thumb:hover {
background: var(--gray-400);
}
</style>
\ No newline at end of file
...@@ -11,8 +11,14 @@ ...@@ -11,8 +11,14 @@
<div class="empty-text">等待执行过程...</div> <div class="empty-text">等待执行过程...</div>
</div> </div>
<div v-else class="timeline-list"> <div v-else class="timeline-list" v-el-infinite-scroll="loadMore" :infinite-scroll-distance="50">
<div v-for="(event, index) in reversedEvents" :key="event.timestamp + '-' + index" class="timeline-item" :class="event.type"> <div
v-for="(event, index) in displayedEvents"
:key="event.id || (event.timestamp + '-' + index)"
class="timeline-item"
:class="event.type"
@click="showEventDetail(event)"
>
<div class="timeline-dot"></div> <div class="timeline-dot"></div>
<div class="timeline-content"> <div class="timeline-content">
<div class="event-header"> <div class="event-header">
...@@ -24,7 +30,7 @@ ...@@ -24,7 +30,7 @@
<div v-if="event.content" class="event-content"> <div v-if="event.content" class="event-content">
<div <div
class="content-text-wrapper" class="content-text-wrapper"
@click="shouldShowToggle(event.timestamp) && toggleContentExpand(event.timestamp)" @click.stop="shouldShowToggle(event.timestamp) && toggleContentExpand(event.timestamp)"
> >
<div <div
class="content-text" class="content-text"
...@@ -37,7 +43,7 @@ ...@@ -37,7 +43,7 @@
<div <div
v-if="shouldShowToggle(event.timestamp)" v-if="shouldShowToggle(event.timestamp)"
class="content-toggle" class="content-toggle"
@click="toggleContentExpand(event.timestamp)" @click.stop="toggleContentExpand(event.timestamp)"
> >
{{ getContentExpandedState(event.timestamp) ? '收起' : '展开' }} {{ getContentExpandedState(event.timestamp) ? '收起' : '展开' }}
</div> </div>
...@@ -49,13 +55,13 @@ ...@@ -49,13 +55,13 @@
class="tool-details" class="tool-details"
> >
<!-- 展开/折叠按钮 --> <!-- 展开/折叠按钮 -->
<div class="detail-toggle" @click="props.toggleExpand(props.events.length - 1 - index)"> <div class="detail-toggle" @click.stop="props.toggleExpand(displayedEvents.length - 1 - index)">
<span class="toggle-text">{{ props.getExpandedState(props.events.length - 1 - index) ? '收起详情' : '查看详情' }}</span> <span class="toggle-text">{{ props.getExpandedState(displayedEvents.length - 1 - index) ? '收起详情' : '查看详情' }}</span>
<span class="toggle-icon">{{ props.getExpandedState(props.events.length - 1 - index) ? '▲' : '▼' }}</span> <span class="toggle-icon">{{ props.getExpandedState(displayedEvents.length - 1 - index) ? '▲' : '▼' }}</span>
</div> </div>
<!-- 详细信息内容 --> <!-- 详细信息内容 -->
<div v-show="getExpandedState(props.events.length - 1 - index)" class="detail-content"> <div v-show="getExpandedState(displayedEvents.length - 1 - index)" class="detail-content">
<!-- 输入参数段 --> <!-- 输入参数段 -->
<ToolDataSection <ToolDataSection
v-if="props.hasValidToolInput(event)" v-if="props.hasValidToolInput(event)"
...@@ -82,14 +88,31 @@ ...@@ -82,14 +88,31 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="loading" class="loading-more">加载中...</div>
<div v-else-if="displayedEvents.length >= reversedEvents.length" class="no-more">没有更多了</div>
</div> </div>
</div> </div>
<!-- 事件详情抽屉 -->
<el-drawer
v-model="isDetailVisible"
title="事件详情"
size="50%"
direction="rtl"
>
<TimelineEventDetail
:event="selectedEvent"
@close="isDetailVisible = false"
/>
</el-drawer>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { ElInfiniteScroll } from 'element-plus'
import ToolDataSection from './ToolDataSection.vue' import ToolDataSection from './ToolDataSection.vue'
import TimelineEventDetail from './TimelineEventDetail.vue'
import type { TimelineEvent } from '../types/timeline' import type { TimelineEvent } from '../types/timeline'
import { useContentExpansion } from '../composables/useContentExpansion' import { useContentExpansion } from '../composables/useContentExpansion'
import { truncateTitle } from '../utils/timelineUtils' import { truncateTitle } from '../utils/timelineUtils'
...@@ -107,9 +130,47 @@ const props = defineProps<{ ...@@ -107,9 +130,47 @@ const props = defineProps<{
onClearTimeline: () => void onClearTimeline: () => void
}>() }>()
// 虚拟滚动相关状态
const loading = ref(false)
const pageSize = ref(20) // 每次加载的事件数量
// 事件详情相关状态
const selectedEvent = ref<TimelineEvent | null>(null)
const isDetailVisible = ref(false)
// 计算反转后的事件列表(最新事件在顶部) // 计算反转后的事件列表(最新事件在顶部)
const reversedEvents = computed(() => [...props.events].reverse()) const reversedEvents = computed(() => [...props.events].reverse())
// 计算当前显示的事件
const displayedEvents = computed(() => {
return reversedEvents.value.slice(0, pageSize.value)
})
// 显示事件详情
const showEventDetail = (event: TimelineEvent) => {
selectedEvent.value = event
isDetailVisible.value = true
}
// 加载更多事件
const loadMore = () => {
if (loading.value) return
if (displayedEvents.value.length >= reversedEvents.value.length) return
loading.value = true
// 模拟异步加载,实际是直接从reversedEvents中截取
setTimeout(() => {
pageSize.value += 20
loading.value = false
}, 300)
}
// 监听事件变化,重置分页
watch(() => props.events.length, () => {
pageSize.value = 20 // 重置为初始值
updateLineCounts()
})
// 使用内容展开管理hook // 使用内容展开管理hook
const { const {
getContentExpandedState, getContentExpandedState,
...@@ -556,6 +617,15 @@ watch(() => props.events, () => { ...@@ -556,6 +617,15 @@ watch(() => props.events, () => {
background: var(--gray-400); background: var(--gray-400);
} }
/* 虚拟滚动相关样式 */
.loading-more,
.no-more {
text-align: center;
padding: var(--spacing-2);
font-size: var(--font-size-xs);
color: var(--text-tertiary);
}
/* 响应式设计 */ /* 响应式设计 */
@media (max-width: 768px) { @media (max-width: 768px) {
.timeline-header { .timeline-header {
......
<template> <template>
<div class="work-area"> <div class="work-area">
<el-tabs v-model="activeTab" class="work-tabs"> <el-tabs v-model="activeTab" class="work-tabs">
<el-tab-pane label="表单" name="form">
<form-render ref="formRender" />
</el-tab-pane>
<el-tab-pane label="📋 时间轴" name="timeline"> <el-tab-pane label="📋 时间轴" name="timeline">
<timeline-container ref="timelineContainerRef" /> <timeline-container ref="timelineContainerRef" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="🌐 网页浏览" name="browser"> <el-tab-pane label="🌐 网页浏览" name="browser">
<webpage-browser ref="webBrowser" /> <webpage-browser ref="webBrowser" />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="表单" name="form">
<form-render ref="formRender" />
</el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
</template> </template>
...@@ -19,30 +19,19 @@ import { ref, onMounted, onUnmounted } from "vue"; ...@@ -19,30 +19,19 @@ import { ref, onMounted, onUnmounted } from "vue";
import FormRender from "./FormRender.vue"; import FormRender from "./FormRender.vue";
import TimelineContainer from "./TimelineContainer.vue"; import TimelineContainer from "./TimelineContainer.vue";
import WebpageBrowser from "./WebpageBrowser.vue"; import WebpageBrowser from "./WebpageBrowser.vue";
import { TimelineService } from "../services/TimelineService";
const activeTab = ref("form"); const activeTab = ref("timeline");
const formRender = ref(); const formRender = ref();
const timelineContainerRef = ref<InstanceType<typeof TimelineContainer> | null>( const timelineContainerRef = ref<InstanceType<typeof TimelineContainer> | null>(
null null
); );
const webBrowser = ref(); const webBrowser = ref();
let timelineService: TimelineService | null = null;
// 添加事件到时间轴 // 添加事件到时间轴
const addEvent = (event: any): void => { const addEvent = (event: any): void => {
timelineContainerRef.value?.addEvent(event); timelineContainerRef.value?.addEvent(event);
}; };
// 初始化Timeline服务
const initTimelineService = () => {
if (timelineContainerRef.value) {
timelineService = new TimelineService((event: any) => {
addEvent(event);
});
timelineService.connectSSE();
}
};
// 清除时间轴 // 清除时间轴
const clearTimeline = (): void => { const clearTimeline = (): void => {
timelineContainerRef.value?.clearTimeline(); timelineContainerRef.value?.clearTimeline();
...@@ -98,18 +87,10 @@ const handleEmbedEvent = (e: Event) => { ...@@ -98,18 +87,10 @@ const handleEmbedEvent = (e: Event) => {
onMounted(() => { onMounted(() => {
// 监听embed事件 // 监听embed事件
window.addEventListener("embed-event", handleEmbedEvent as EventListener); window.addEventListener("embed-event", handleEmbedEvent as EventListener);
// 初始化Timeline服务
initTimelineService();
}); });
onUnmounted(() => { onUnmounted(() => {
// 移除事件监听 // 移除事件监听
window.removeEventListener("embed-event", handleEmbedEvent as EventListener); window.removeEventListener("embed-event", handleEmbedEvent as EventListener);
// 清理Timeline服务
if (timelineService) {
timelineService.cleanup();
}
}); // 暴露方法供父组件调用 }); // 暴露方法供父组件调用
defineExpose({ defineExpose({
formRender, formRender,
......
// 内容展开管理hook // 内容展开管理hook
import { ref, nextTick } from 'vue' import { nextTick, ref, type Ref } from 'vue'
import type { Ref } from 'vue'
import type { TimelineEvent } from '../types/timeline' import type { TimelineEvent } from '../types/timeline'
export function useContentExpansion(props: { export function useContentExpansion(props: {
events: TimelineEvent[] events: TimelineEvent[]
}) { }) {
// 内容展开状态管理 // 内容展开状态管理 - 使用WeakMap提高性能
const contentExpandedStates = ref<Record<number, boolean>>({}) const contentExpandedStates = new WeakMap<HTMLElement, boolean>()
const contentLineCounts = ref<Record<number, number>>({}) const contentLineCounts = ref<Record<string, number>>({})
const contentElements = ref<Record<number, HTMLElement>>({}) const contentElements = new Map<string, HTMLElement>()
// 事件ID到时间戳的映射,用于快速查找
const eventIdToTimestamp = ref<Record<string, number>>({})
// 更新事件ID映射
const updateEventIdMapping = () => {
props.events.forEach(event => {
if (event.id) {
eventIdToTimestamp.value[event.id] = event.timestamp
}
})
}
// 获取内容展开状态 // 获取内容展开状态
const getContentExpandedState = (timestamp: number): boolean => { const getContentExpandedState = (timestamp: number): boolean => {
return contentExpandedStates.value[timestamp] || false const key = timestamp.toString()
const element = contentElements.get(key)
return element ? (contentExpandedStates.get(element) || false) : false
} }
// 注册内容元素引用 // 注册内容元素引用
const setContentRef = (el: HTMLElement | null, timestamp: number) => { const setContentRef = (el: HTMLElement | null, timestamp: number) => {
if (el) { if (el) {
contentElements.value[timestamp] = el const key = timestamp.toString()
contentElements.set(key, el)
// 初始化展开状态为false
if (!contentExpandedStates.has(el)) {
contentExpandedStates.set(el, false)
}
// 更新行数计算 // 更新行数计算
updateLineCountForElement(timestamp) updateLineCountForElement(timestamp)
} }
...@@ -28,18 +46,24 @@ export function useContentExpansion(props: { ...@@ -28,18 +46,24 @@ export function useContentExpansion(props: {
// 为特定元素更新行数计算 // 为特定元素更新行数计算
const updateLineCountForElement = (timestamp: number) => { const updateLineCountForElement = (timestamp: number) => {
const event = props.events.find(e => e.timestamp === timestamp) const event = props.events.find(e => e.timestamp === timestamp)
if (event && event.content && contentElements.value[timestamp]) { const key = timestamp.toString()
contentLineCounts.value[timestamp] = calculateLineCount(event.content, contentElements.value[timestamp]) const element = contentElements.get(key)
// 如果内容超过两行,初始化为折叠状态
if (contentLineCounts.value[timestamp] > 2 && contentExpandedStates.value[timestamp] === undefined) { if (event && 'content' in event && event.content && element) {
contentExpandedStates.value[timestamp] = false const lineCount = calculateLineCount(event.content, element)
} const contentKey = event.id || key
contentLineCounts.value[contentKey] = lineCount
} }
} }
// 切换内容展开状态 // 切换内容展开状态
const toggleContentExpand = (timestamp: number) => { const toggleContentExpand = (timestamp: number) => {
contentExpandedStates.value[timestamp] = !getContentExpandedState(timestamp) const key = timestamp.toString()
const element = contentElements.get(key)
if (element) {
const currentState = contentExpandedStates.get(element) || false
contentExpandedStates.set(element, !currentState)
}
} }
// 检查是否应该显示切换按钮 // 检查是否应该显示切换按钮
...@@ -65,18 +89,24 @@ export function useContentExpansion(props: { ...@@ -65,18 +89,24 @@ export function useContentExpansion(props: {
} }
const shouldShowToggle = (timestamp: number): boolean => { const shouldShowToggle = (timestamp: number): boolean => {
return contentLineCounts.value[timestamp] > 2 const event = props.events.find(e => e.timestamp === timestamp)
if (!event) return false
const key = event.id || timestamp.toString()
return (contentLineCounts.value[key] || 0) > 2
} }
// 更新内容行数计数 // 更新内容行数计数
const updateLineCounts = () => { const updateLineCounts = () => {
nextTick(() => { nextTick(() => {
updateEventIdMapping()
props.events.forEach((event) => { props.events.forEach((event) => {
if (event.content) { if ('content' in event && event.content) {
// 行数将在元素引用设置时计算 const key = event.timestamp.toString()
// 这里只初始化展开状态 const element = contentElements.get(key)
if (contentExpandedStates.value[event.timestamp] === undefined) { if (element) {
contentExpandedStates.value[event.timestamp] = false updateLineCountForElement(event.timestamp)
} }
} }
}) })
...@@ -84,9 +114,6 @@ export function useContentExpansion(props: { ...@@ -84,9 +114,6 @@ export function useContentExpansion(props: {
} }
return { return {
contentExpandedStates,
contentLineCounts,
contentElements,
getContentExpandedState, getContentExpandedState,
setContentRef, setContentRef,
toggleContentExpand, toggleContentExpand,
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<div class="new-chat-page"> <div class="new-chat-page">
<!-- 左侧对话区 --> <!-- 左侧对话区 -->
<div class="left-panel"> <div class="left-panel">
<chat-area ref="chatArea" /> <chat-area ref="chatArea" :add-event-to-timeline="addEventToTimeline" />
</div> </div>
<!-- 中间分割线 --> <!-- 中间分割线 -->
...@@ -37,6 +37,13 @@ watch(() => route.query.agentId, (newAgentId) => { ...@@ -37,6 +37,13 @@ watch(() => route.query.agentId, (newAgentId) => {
} }
}, { immediate: true }) }, { immediate: true })
// 添加事件到时间轴
const addEventToTimeline = (event: any) => {
if (workArea.value && typeof workArea.value.addEvent === 'function') {
workArea.value.addEvent(event)
}
}
// 开始拖动分割线 // 开始拖动分割线
const startResize = (e: MouseEvent) => { const startResize = (e: MouseEvent) => {
isResizing.value = true isResizing.value = true
......
/**
* Timeline服务类
* 整合了SSE管理功能,减少服务层级
*/
export class TimelineService {
private eventSource: EventSource | null = null;
private retryCount = 0;
private maxRetries = 5;
private retryDelay = 3000;
private addEventCallback: Function;
private messageQueue: any[] = [];
private processingQueue = false;
constructor(addEvent: Function) {
this.addEventCallback = addEvent;
}
/**
* 建立SSE连接
*/
connectSSE() {
// 构造带认证参数的URL
let eventSourceUrl = '/api/v1/agent/timeline-events';
// 从localStorage获取token
const token = localStorage.getItem('token');
// 使用请求头而不是URL参数传递token
if (token) {
// 创建自定义的EventSource实现,支持添加请求头
const eventSource = new EventSourceWithAuth(eventSourceUrl, token);
this.eventSource = eventSource as unknown as EventSource;
} else {
// 如果没有token,仍然使用标准EventSource
this.eventSource = new EventSource(eventSourceUrl);
}
this.eventSource.onmessage = this.handleMessage.bind(this);
this.eventSource.onerror = this.handleError.bind(this);
this.eventSource.onopen = this.handleOpen.bind(this);
return this.eventSource;
}
/**
* 处理SSE消息队列
*/
private processMessageQueue() {
// 如果未连接或正在处理队列,则跳过
if (!this.eventSource || this.eventSource.readyState !== EventSource.OPEN || this.processingQueue || this.messageQueue.length === 0) {
return;
}
this.processingQueue = true;
// 批量处理消息以提高性能
const batchSize = 10;
const batch = this.messageQueue.splice(0, batchSize);
batch.forEach(data => {
this.handleSingleSseMessage(data);
});
this.processingQueue = false;
// 如果还有消息,继续处理
if (this.messageQueue.length > 0) {
setTimeout(() => this.processMessageQueue(), 0);
}
}
/**
* 处理单个SSE消息
*/
private handleSingleSseMessage(data: any) {
this.addEvent(data);
// 触发embed事件给父组件(如果需要)
if (data.type === 'embed') {
window.dispatchEvent(new CustomEvent('embed-event', { detail: data }));
}
// 重置重试计数
this.retryCount = 0;
}
/**
* 处理消息事件
*/
private handleMessage(event: MessageEvent) {
try {
// 的消息解析
const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
if (data) {
// 将消息加入队列进行批处理,但限制队列大小以避免内存泄漏
if (this.messageQueue.length < 100) {
this.messageQueue.push(data);
this.processMessageQueue();
} else {
console.warn('[TimelinePanel] 消息队列已满,丢弃新消息');
}
}
} catch (err) {
console.error('解析时间轴事件失败:', err);
}
}
/**
* 处理错误事件
*/
private handleError(event: Event) {
console.error('[SSE] 连接错误:', event);
// 尝试重新连接
if (this.retryCount < this.maxRetries) {
this.retryCount++;
setTimeout(() => {
console.log(`[SSE] 尝试重新连接 (${this.retryCount}/${this.maxRetries})`);
this.connectSSE();
}, this.retryDelay * this.retryCount);
this.addEvent({
type: 'observation',
title: '重新连接中',
content: `正在尝试重新连接 (${this.retryCount}/${this.maxRetries})`,
timestamp: Date.now()
});
} else {
this.addEvent({
type: 'error',
title: '连接失败',
content: '无法连接到服务器事件流,请刷新页面重试',
timestamp: Date.now()
});
// 显示用户通知
// if (typeof window !== 'undefined' && window.alert) {
// window.alert('时间轴连接失败,请刷新页面重试');
// }
}
}
/**
* 处理连接成功事件
*/
private handleOpen() {
console.log('[SSE] 连接已建立');
// 添加连接成功事件到时间轴
this.addEvent({
type: 'observation',
title: 'SSE连接已建立',
content: '成功连接到服务器事件流',
timestamp: Date.now()
});
// 处理队列中积压的消息
this.processMessageQueue();
}
/**
* 组件卸载时清理资源
*/
cleanup() {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
}
}
/**
* 处理来自ChatArea的思考事件
*/
handleTimelineEvent(e: CustomEvent) {
const eventData = e.detail;
console.log('[TimelinePanel] 接收到timeline-event事件:', eventData);
// 确保时间戳存在且有效
const timestamp = eventData.timestamp || Date.now();
this.addEvent({
type: eventData.type || 'thought',
title: eventData.title || '思考过程',
content: eventData.content,
toolName: eventData.toolName,
toolAction: eventData.toolAction,
toolInput: eventData.toolInput,
toolOutput: eventData.toolOutput,
toolStatus: eventData.toolStatus,
executionTime: eventData.executionTime,
embedUrl: eventData.embedUrl,
embedType: eventData.embedType,
embedTitle: eventData.embedTitle,
embedHtmlContent: eventData.embedHtmlContent,
metadata: eventData.metadata,
timestamp: timestamp
});
}
/**
* 处理SSE连接失败事件
*/
handleSseConnectionFailed() {
console.error('[SSE] 时间轴事件连接失败,已达到最大重试次数');
// 添加连接失败事件到时间轴
this.addEvent({
type: 'error',
title: 'SSE连接失败',
content: '无法连接到服务器事件流,请刷新页面重试',
timestamp: Date.now()
});
// 显示用户通知
// if (typeof window !== 'undefined' && window.alert) {
// window.alert('时间轴连接失败,请刷新页面重试');
// }
}
/**
* 获取当前EventSource实例
*/
getEventSource(): EventSource | null {
return this.eventSource;
}
/**
* 添加事件
*/
addEvent(event: any) {
if (this.addEventCallback) {
this.addEventCallback(event);
}
}
/**
* 清除Timeline
*/
clearTimeline() {
// 实现清除逻辑
}
/**
* 获取事件类型标签
*/
getEventTypeLabel(eventTypeLabels: Record<string, string>, type: string): string {
return eventTypeLabels[type] || type;
}
/**
* 格式化时间
*/
formatTime(timestamp: number): string {
const date = new Date(timestamp);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
}
/**
* 获取事件的展开状态
*/
getExpandedState(): boolean {
// 实现
return false;
}
/**
* 切换事件详细信息的展开状态
*/
toggleExpand() {
// 实现
}
/**
* 工具事件类型判断
*/
isToolEventType(type: string): boolean {
return ['tool_call', 'tool_result', 'tool_error'].includes(type);
}
/**
* 工具输入有效性检查
*/
hasValidToolInput(event: any): boolean {
return event.type === 'tool_call' && event.toolInput !== null && event.toolInput !== undefined;
}
/**
* 工具输出有效性检查
*/
hasValidToolOutput(event: any): boolean {
return event.type === 'tool_result' && event.toolOutput !== null && event.toolOutput !== undefined;
}
}
// 自定义EventSource实现,支持添加Authorization请求头
class EventSourceWithAuth extends EventTarget {
private xhr: XMLHttpRequest | null = null;
private timeoutId: number | null = null;
private _readyState: number;
private _url: string;
private _token: string;
static readonly CONNECTING = 0;
static readonly OPEN = 1;
static readonly CLOSED = 2;
constructor(url: string, token: string) {
super();
this._url = url;
this._token = token;
this._readyState = EventSourceWithAuth.CONNECTING;
this.connect();
}
private connect() {
if (this.xhr) {
this.xhr.abort();
}
this.xhr = new XMLHttpRequest();
this.xhr.open('GET', this._url, true);
this.xhr.setRequestHeader('Accept', 'text/event-stream');
this.xhr.setRequestHeader('Cache-Control', 'no-cache');
this.xhr.setRequestHeader('Authorization', `Bearer ${this._token}`);
this.xhr.withCredentials = true;
this.xhr.onreadystatechange = () => {
if (this.xhr?.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
if (this.xhr.status === 200) {
this._readyState = EventSourceWithAuth.OPEN;
this.dispatchEvent(new Event('open'));
} else {
this.handleError();
}
}
};
this.xhr.onprogress = () => {
if (this.xhr) {
const lines = this.xhr.responseText.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
const event = new MessageEvent('message', { data });
this.dispatchEvent(event);
}
}
}
};
this.xhr.onload = () => {
this._readyState = EventSourceWithAuth.CLOSED;
this.dispatchEvent(new Event('close'));
};
this.xhr.onerror = () => {
this.handleError();
};
this.xhr.send();
// 每30秒重新连接一次,保持连接活跃
this.timeoutId = window.setTimeout(() => {
this.reconnect();
}, 30000);
}
private handleError() {
this._readyState = EventSourceWithAuth.CLOSED;
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
this.dispatchEvent(new Event('error'));
// 尝试重新连接
setTimeout(() => this.reconnect(), 3000);
}
private reconnect() {
if (this._readyState !== EventSourceWithAuth.CLOSED) {
this.connect();
}
}
close() {
this._readyState = EventSourceWithAuth.CLOSED;
if (this.xhr) {
this.xhr.abort();
}
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
this.dispatchEvent(new Event('close'));
}
get readyState() {
return this._readyState;
}
get url() {
return this._url;
}
get withCredentials() {
return false;
}
}
\ No newline at end of file
import type { TimelineEvent } from '../types/timeline';
/**
* SSE服务类,用于处理与后端的Server-Sent Events连接
*/
export class SseService {
private eventSource: EventSource | null = null;
private eventListeners: Map<string, Array<(data: any) => void>> = new Map();
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 1000;
private url: string;
private isConnecting = false;
constructor(url: string = '/api/v1/events') {
this.url = url;
}
/**
* 连接到SSE服务器
*/
connect(): void {
if (this.eventSource || this.isConnecting) {
return;
}
this.isConnecting = true;
this.reconnectAttempts = 0;
try {
// 创建EventSource连接
this.eventSource = new EventSource(this.url);
// 监听open事件
this.eventSource.onopen = () => {
console.log('[SSE] 连接已建立');
this.reconnectAttempts = 0;
this.isConnecting = false;
this.dispatchEvent('connect', {});
};
// 监听message事件(默认事件)
this.eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('[SSE] 收到消息:', data);
this.dispatchEvent('message', data);
// 如果是时间轴事件,分发特定事件
if (data.type) {
this.dispatchEvent('timeline-event', data);
}
} catch (error) {
console.error('[SSE] 解析消息失败:', error);
}
};
// 监听error事件
this.eventSource.onerror = (error) => {
console.error('[SSE] 连接错误:', error);
this.isConnecting = false;
this.dispatchEvent('error', error);
this.handleReconnect();
};
// 监听特定事件类型
this.eventSource.addEventListener('error', (event) => {
try {
const data = JSON.parse((event as MessageEvent).data);
console.error('[SSE] 服务器错误:', data);
this.dispatchEvent('server-error', data);
} catch (error) {
console.error('[SSE] 解析错误消息失败:', error);
}
});
this.eventSource.addEventListener('token', (event) => {
try {
const data = JSON.parse((event as MessageEvent).data);
console.log('[SSE] 收到Token:', data);
this.dispatchEvent('token', data);
} catch (error) {
console.error('[SSE] 解析Token消息失败:', error);
}
});
} catch (error) {
console.error('[SSE] 创建连接失败:', error);
this.isConnecting = false;
this.handleReconnect();
}
}
/**
* 断开SSE连接
*/
disconnect(): void {
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
console.log('[SSE] 连接已断开');
this.dispatchEvent('disconnect', {});
}
this.isConnecting = false;
this.reconnectAttempts = 0;
}
/**
* 处理重连逻辑
*/
private handleReconnect(): void {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('[SSE] 达到最大重连次数,停止重连');
this.dispatchEvent('connection-failed', { attempts: this.reconnectAttempts });
window.dispatchEvent(new CustomEvent('sse-connection-failed', { detail: { attempts: this.reconnectAttempts } }));
return;
}
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(`[SSE] 尝试重连... (${this.reconnectAttempts}/${this.maxReconnectAttempts}),延迟 ${delay}ms`);
setTimeout(() => {
this.connect();
}, delay);
}
/**
* 添加事件监听器
* @param eventType 事件类型
* @param callback 回调函数
*/
on(eventType: string, callback: (data: any) => void): void {
if (!this.eventListeners.has(eventType)) {
this.eventListeners.set(eventType, []);
}
this.eventListeners.get(eventType)?.push(callback);
}
/**
* 移除事件监听器
* @param eventType 事件类型
* @param callback 回调函数(可选,如果不提供则移除所有该类型的监听器)
*/
off(eventType: string, callback?: (data: any) => void): void {
if (!this.eventListeners.has(eventType)) {
return;
}
if (callback) {
const callbacks = this.eventListeners.get(eventType)?.filter(cb => cb !== callback) || [];
this.eventListeners.set(eventType, callbacks);
} else {
this.eventListeners.delete(eventType);
}
}
/**
* 分发事件
* @param eventType 事件类型
* @param data 事件数据
*/
private dispatchEvent(eventType: string, data: any): void {
const callbacks = this.eventListeners.get(eventType) || [];
callbacks.forEach(callback => {
try {
callback(data);
} catch (error) {
console.error(`[SSE] 执行事件监听器失败 (${eventType}):`, error);
}
});
}
/**
* 检查连接状态
*/
isConnected(): boolean {
return this.eventSource !== null && this.eventSource.readyState === EventSource.OPEN;
}
}
// 创建单例实例
const sseService = new SseService();
export default sseService;
\ No newline at end of file
// 统一的时间轴事件类型定义 // 统一的时间轴事件类型定义
export interface BaseTimelineEvent { export interface BaseTimelineEvent {
id: string;
type: string; type: string;
title: string; title: string;
timestamp: number; timestamp: number;
...@@ -39,12 +40,22 @@ export interface EmbedEvent extends BaseTimelineEvent { ...@@ -39,12 +40,22 @@ export interface EmbedEvent extends BaseTimelineEvent {
embedHtmlContent?: string; embedHtmlContent?: string;
} }
export interface CompleteEvent extends BaseTimelineEvent {
content: string;
}
export interface ErrorEvent extends BaseTimelineEvent {
content: string;
}
export type TimelineEvent = export type TimelineEvent =
| ThoughtEvent | ThoughtEvent
| ToolCallEvent | ToolCallEvent
| ToolResultEvent | ToolResultEvent
| ToolErrorEvent | ToolErrorEvent
| EmbedEvent | EmbedEvent
| CompleteEvent
| ErrorEvent
| BaseTimelineEvent; | BaseTimelineEvent;
// 事件类型标签映射 // 事件类型标签映射
...@@ -56,5 +67,7 @@ export const eventTypeLabels: Record<string, string> = { ...@@ -56,5 +67,7 @@ export const eventTypeLabels: Record<string, string> = {
embed: '🌐 网页预览', embed: '🌐 网页预览',
log: '📝 日志', log: '📝 日志',
result: '🎯 最终答案', result: '🎯 最终答案',
observation: '🔍 观察' observation: '🔍 观察',
complete: '✅ 完成',
error: '❌ 错误'
}; };
\ No newline at end of file
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