Commit b48aa988 authored by youxiaoji's avatar youxiaoji

* [增加差旅助手智能体]

parent 53570949
package pangea.hiagent.controller;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
......@@ -29,22 +30,22 @@ import java.util.concurrent.Executors;
@RestController
@RequestMapping("/api/v1/agent")
public class AgentChatController {
@Autowired
private AgentService agentService;
@Autowired
private AgentChatService agentChatService;
@Autowired
private SseEventManager sseEventManager;
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
/**
* 流式对话接口
* 支持 SSE (Server-Sent Events) 格式的流式输出
*
*
* @param agentId Agent ID
* @param chatRequest 对话请求
* @return SSE emitter
......@@ -55,13 +56,13 @@ public class AgentChatController {
@RequestParam String agentId,
@RequestBody ChatRequest chatRequest,
HttpServletResponse response) {
// 将ChatRequest转换为AgentRequest
AgentRequest request = new AgentRequest();
request.setUserMessage(chatRequest.getMessage());
log.info("开始处理流式对话请求,AgentId: {}, 用户消息: {}", agentId, request.getUserMessage());
// 检查用户消息是否为空
if (request.getUserMessage() == null || request.getUserMessage().trim().isEmpty()) {
log.error("用户消息不能为空");
......@@ -70,7 +71,7 @@ public class AgentChatController {
log.warn("响应已经提交,无法发送用户消息为空错误");
return new SseEmitter(300000L); // 返回一个空的emitter
}
SseEmitter emitter = new SseEmitter(300000L);
try {
emitter.send(SseEmitter.event()
......@@ -84,7 +85,6 @@ public class AgentChatController {
}
return emitter;
}
String userId = getCurrentUserId();
if (userId == null) {
log.error("用户未认证");
......
......@@ -587,7 +587,7 @@ public class AgentChatService {
// 这里只需要等待足够的时间让异步的onComplete回调执行完成
try {
// 通过轮询检查是否已完成,最多等待5秒
long maxWaitTime = 5000;
long maxWaitTime = 10*60*1000;
long startTime = System.currentTimeMillis();
while (!isCompleted.get() && (System.currentTimeMillis() - startTime) < maxWaitTime) {
Thread.sleep(100); // 每100ms检查一次
......
......@@ -8,6 +8,9 @@ import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.context.annotation.Lazy;
import pangea.hiagent.service.AgentService;
import pangea.hiagent.service.InfoCollectorService;
import pangea.hiagent.tools.HisenseTripTool;
import pangea.hiagent.workpanel.IWorkPanelDataCollector;
import pangea.hiagent.core.AgentChatService;
import pangea.hiagent.memory.MemoryService;
......@@ -40,6 +43,12 @@ public class DefaultReactExecutor implements ReactExecutor {
@Autowired
private MemoryService memoryService;
@Autowired
private AgentService agentService;
@Autowired
private InfoCollectorService infoCollectorService;
/**
* 添加ReAct回调
......@@ -213,10 +222,12 @@ public class DefaultReactExecutor implements ReactExecutor {
// 构建Prompt,包含历史对话记录
Prompt prompt = buildPromptWithHistory(systemPrompt, userInput, agent);
HisenseTripTool tripTool = new HisenseTripTool(agentService,infoCollectorService);
tripTool.initialize();
// 订阅流式响应
chatClient.prompt(prompt)
.tools(tools.toArray())
.tools(tripTool)
.stream()
.chatResponse()
.subscribe(
......
package pangea.hiagent.service;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import pangea.hiagent.tools.HisenseTripTool;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Service
@Slf4j
public class InfoCollectorService {
private static final ConcurrentHashMap<String, JSONArray> infos = new ConcurrentHashMap<>(16);
private static final ConcurrentHashMap<String, Object> values = new ConcurrentHashMap<>(16);
public void register(String pageId, JSONArray info) {
infos.put(pageId, info);
}
public boolean exists(String pageId) {
return infos.containsKey(pageId);
}
public JSONArray getInfo(String pageId) {
return infos.get(pageId);
}
public void saveValue(String key, Object value) {
log.info("key {} value {}",key,value);
values.put(key, value);
}
public Object getValue(String key) {
return values.get(key);
}
public Set<String> findLackInfo() {
Set<String> valueKeys = values.keySet();
Set<String> allKeys = infos.get(HisenseTripTool.pageId).stream().map(t -> ((JSONObject) t).getString("field_name")).collect(Collectors.toSet());
allKeys.removeAll(valueKeys);
log.info("lack keys {}", allKeys);
return allKeys;
}
}
package pangea.hiagent.tools;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.Cookie;
import com.microsoft.playwright.options.WaitForSelectorState;
import com.microsoft.playwright.options.WaitUntilState;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cglib.core.Local;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileUrlResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import pangea.hiagent.dto.ApplyData;
import pangea.hiagent.model.Agent;
import pangea.hiagent.service.AgentService;
import pangea.hiagent.service.InfoCollectorService;
import pangea.hiagent.utils.Contants;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@Slf4j
public class HisenseTripTool {
public static final String pageId = "tripApply";
// SSO用户名(从配置文件读取)
private String ssoToken;
// SSO密码(从配置文件读取)
private String ldapToken;
private String tripToken;
// Playwright实例
private Playwright playwright;
// 浏览器实例
private Browser browser;
// 共享的浏览器上下文,用于保持登录状态
private BrowserContext sharedContext;
// 上次登录时间
private long lastLoginTime = 0;
private AgentService agentService;
private InfoCollectorService infoCollectorService;
// 登录状态有效期(毫秒),设置为30分钟
private static final long LOGIN_VALIDITY_PERIOD = 30 * 60 * 1000;
public HisenseTripTool(AgentService agentService, InfoCollectorService infoCollectorService) {
this.agentService = agentService;
this.infoCollectorService = infoCollectorService;
}
@PostConstruct
public void initialize() {
try {
log.info("正在初始化海信SSO认证工具的Playwright...");
this.playwright = Playwright.create();
// 使用chromium浏览器,无头模式(headless=true),适合服务器运行
// 可根据需要修改为有头模式(headless=false)用于调试
this.browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(true));
// 初始化共享上下文
this.sharedContext = browser.newContext();
// 检查是否已有有效的登录会话
Cookie ssoToken = new Cookie("ssoLoginToken", "d10bc61aa4e00dcc6f08de64ca42012814fdbcee9b88aa977f7fb07d3a4018f4");
ssoToken.setDomain(".hisense.com");
ssoToken.setPath("/");
Cookie ldapToken = new Cookie("LtpaToken", "AAECAzY5NDRBNTQ1Njk0NTRFMDV5b3V4aWFvamlaLv+jUGNEEORN24GLIC3OlqcCdw==");
ldapToken.setDomain(".hisense.com");
ldapToken.setPath("/");
Cookie tripCookie = new Cookie("FCC_SESSION", "305c71973f8a495fbf00713d93a0b3c4");
tripCookie.setDomain("trip.hisense.com");
tripCookie.setPath("/");
List<Cookie> cookies = new ArrayList<>();
cookies.add(ssoToken);
cookies.add(ldapToken);
cookies.add(tripCookie);
sharedContext.addCookies(cookies);
log.info("海信SSO认证工具的Playwright初始化成功");
} catch (Exception e) {
log.error("海信SSO认证工具的Playwright初始化失败: ", e);
}
}
/**
* 销毁Playwright资源
*/
@PreDestroy
public void destroy() {
try {
if (sharedContext != null) {
sharedContext.close();
log.info("海信SSO认证工具的共享浏览器上下文已关闭");
}
if (browser != null) {
browser.close();
log.info("海信SSO认证工具的浏览器实例已关闭");
}
if (playwright != null) {
playwright.close();
log.info("海信SSO认证工具的Playwright实例已关闭");
}
} catch (Exception e) {
log.error("海信SSO认证工具的Playwright资源释放失败: ", e);
}
}
@Tool(description = "提交出差申请")
public String apply(){
String tripApplyUrl = "https://trip.hisense.com/fcc/fcapply/ccsqd/add.html?state=1";
long startTime = System.currentTimeMillis();
Page page = null;
try {
page = sharedContext.newPage();
page.setDefaultTimeout(60*1000);
// 访问业务系统页面
log.info("正在访问业务系统页面: {}", tripApplyUrl);
page.navigate(tripApplyUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// 检查是否重定向到了SSO登录页面
String currentUrl = page.url();
log.info("当前页面URL: {}", currentUrl);
// 如果页面尚未导航到业务系统URL,则导航到该URL
if (!page.url().equals(tripApplyUrl) && !page.url().startsWith(tripApplyUrl)) {
log.info("正在访问业务系统页面: {}", tripApplyUrl);
page.navigate(tripApplyUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
}
sharedContext.tracing().start(new Tracing.StartOptions()
.setScreenshots(true)
.setSnapshots(true)
.setSources(true));
JSONArray jsonArray = infoCollectorService.getInfo(pageId);
Locator.WaitForOptions waitForOptions = new Locator.WaitForOptions();
waitForOptions.setTimeout(5*1000);
Locator.WaitForOptions waitUntilOptions = new Locator.WaitForOptions();
waitUntilOptions.setState(WaitForSelectorState.ATTACHED);
page.locator("[id^='layui-layer-shade']").waitFor(waitUntilOptions);
final Locator btnLocator = page.locator("[class^='jsAgreed btn']");
page.waitForCondition(() ->
(boolean) btnLocator.evaluate("el => !el.classList.contains('active')")
);
btnLocator.click();
for(int i=0;i<jsonArray.size();i++){
log.info("index {} ",i);
JSONObject obj = jsonArray.getJSONObject(i);
log.info("json {}",obj);
String fieldName = obj.getString("field_name");
String fieldValue = infoCollectorService.getValue(fieldName).toString();
String locator = obj.getString("locator");
JSONObject attributes = obj.getJSONObject("attributes");
if(attributes.containsKey("class")){
Locator.FillOptions fillOptions = new Locator.FillOptions();
fillOptions.setForce(true);
List<Locator> list = page.locator("[class*='"+attributes.getString("class")+"']").all();
for(Locator loc : list){
String tagName = (String) loc.evaluate("el => el.tagName");
log.info("标签类型: {}" , tagName);
if(tagName.toLowerCase().contains("div") || tagName.toLowerCase().contains("span") || (tagName.compareToIgnoreCase("a")==0)){
continue;
}
loc.fill(fieldValue,fillOptions);
page.mouse().click(0, 0);
this.saveScreenShot(page.screenshot(),obj.getString("field_name"));
}
//page.locator(locator).and(page.locator("[class*='"+attributes.getString("class")+"']")).fill(fieldValue,fillOptions);
}else{
page.locator(locator).fill(fieldValue);
page.mouse().click(0, 0);
this.saveScreenShot(page.screenshot(),obj.getString("field_name"));
}
}
page.mouse().click(0, 0);
page.locator("[class='jsCostDepart validate[required]']").click();
page.locator(".layui-layer[type='dialog']").waitFor(new Locator.WaitForOptions()
.setState(WaitForSelectorState.DETACHED));
saveScreenShot(page.screenshot(),"list");
List<Locator> itemList = page.locator("[class^='zdyTable-checkItem jsZdyTableChecks']").all();
itemList.get(1).click();
saveScreenShot(page.screenshot(),"choose");
page.locator("[class='btn jsCheckData']").click();
// page.onDialog(dialog -> dialog.accept());
saveScreenShot(page.screenshot(),"filled");
page.locator("[class*='btn theme jsSave']").click(new Locator.ClickOptions().setForce(true));
Locator locator = page.locator("[class*='layui-layer layui-layer-dialog layer-anim']");
locator.waitFor(waitForOptions);
//page.locator("[id^='layui-layer-shade']").click();
saveScreenShot(page.screenshot(),"confirm");
page.locator("[class^='layui-layer-btn0']").click(new Locator.ClickOptions().setForce(true));
saveScreenShot(page.screenshot(),"submit");
//.page.locator("text=操作成功").waitFor();
Locator successMsg = page.locator("text=操作成功");
successMsg.waitFor();
successMsg.waitFor(new Locator.WaitForOptions().setState(WaitForSelectorState.HIDDEN).setTimeout(10000));
saveScreenShot(page.screenshot(),"saved");
return "申请已暂存,请进入信息提交";
} catch (Exception e) {
long endTime = System.currentTimeMillis();
String errorMsg = "获取海信出差申请页面内容失败: " + e.getMessage();
log.error("获取海信海信出差申请内容失败,耗时: {} ms", endTime - startTime, e);
return errorMsg;
} finally {
// 释放页面资源
if (page != null) {
try {
saveScreenShot(page.screenshot(),"closed");
page.close();
} catch (Exception e) {
log.warn("关闭页面时发生异常: {}", e.getMessage());
}
}
sharedContext.tracing().stop(new Tracing.StopOptions()
.setPath(Paths.get("trace.zip")));
}
}
private void saveScreenShot(byte[] bytes,String suffix){
// 生成一个唯一的文件名,防止覆盖
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
String fileName = "screenshot_" + timestamp +"_"+suffix+ ".png";
try {
// Paths.get() 指定存储路径,默认在项目根目录
Files.write(Paths.get(fileName), bytes);
log.info("截图已保存至: {}" , fileName);
} catch (IOException e) {
log.info("保存截图失败: " + e.getMessage());
e.printStackTrace();
}
}
@Tool(description = "存储用户提交的出差申请信息")
public String applyInfoSave(@ToolParam(required = true) JSONObject infos){
infos.keySet().forEach(key -> {
infoCollectorService.saveValue(key,infos.get(key));
});
Set<String> keys = infoCollectorService.findLackInfo();
StringBuilder sb = new StringBuilder();
if(keys.isEmpty()){
sb.append("用户已提交全部数据,提示用户提交申请");
}else{
sb.append("用户还有以下信息未提交:");
sb.append("\n");
for(String key:keys){
sb.append(key);
sb.append("\n");
}
sb.append("提示用户继续以json格式提交信息");
}
return sb.toString();
}
/**
* 工具方法:获取海信差旅平台出差申请的网页内容
*
* @return 页面内容(HTML文本)
*/
@Tool(description = "获取出差申请必要信息")
public String getTripApplyNecessaryInfo() {
String tripApplyUrl = "https://trip.hisense.com/fcc/fcapply/ccsqd/add.html?state=1";
long startTime = System.currentTimeMillis();
Page page = null;
try {
page = sharedContext.newPage();
page.setDefaultTimeout(60*1000);
// 访问业务系统页面
log.info("正在访问业务系统页面: {}", tripApplyUrl);
page.navigate(tripApplyUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// 检查是否重定向到了SSO登录页面
String currentUrl = page.url();
log.info("当前页面URL: {}", currentUrl);
// 如果页面尚未导航到业务系统URL,则导航到该URL
if (!page.url().equals(tripApplyUrl) && !page.url().startsWith(tripApplyUrl)) {
log.info("正在访问业务系统页面: {}", tripApplyUrl);
page.navigate(tripApplyUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
}
JSONArray jsonArray = null;
if (!infoCollectorService.exists(pageId)) {
JSONArray tmp = getLocators(page.locator("body").innerHTML());
jsonArray = tmp;
infoCollectorService.register(pageId,tmp);
}else{
jsonArray = infoCollectorService.getInfo(pageId);
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("用户需要提供的信息包括:");
stringBuilder.append("\n");
for(int i=0;i<jsonArray.size();i++){
log.info("index {} ",i);
JSONObject obj = jsonArray.getJSONObject(i);
stringBuilder.append(obj.getString("field_name"));
stringBuilder.append("\n");
}
stringBuilder.append("提示用户以json格式提交信息");
// 提取页面内容
String content = stringBuilder.toString();
long endTime = System.currentTimeMillis();
log.info("成功获取海信出差申请页面内容,耗时: {} ms", endTime - startTime);
log.info("用户需要提交的信息包括:{}",content);
return content;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
String errorMsg = "获取海信出差申请页面内容失败: " + e.getMessage();
log.error("获取海信海信出差申请内容失败,耗时: {} ms", endTime - startTime, e);
return errorMsg;
} finally {
// 释放页面资源
if (page != null) {
try {
page.close();
} catch (Exception e) {
log.warn("关闭页面时发生异常: {}", e.getMessage());
}
}
}
}
private JSONArray getLocators(String html) throws MalformedURLException {
System.out.println("----------------------------------------");
System.out.println(html);
System.out.println("----------------------------------------");
Agent agent = agentService.getAgent("agent-7");
ChatClient chatClient = ChatClient.builder(agentService.getChatModelForAgent(agent)).build();
String systemPrompt = "你是一个网页解析助手,你可以将html {htmlData} 中所有的必填项的名称标题和对应的html元素的定位表达式,attributes完整的解析出来;无论元素是否动态生成,都需要解析;以{jsonSchema}格式告诉我;定位表达式可以被playwright直接用来定位元素";
JSONArray response = chatClient.prompt().user(u -> u.text(systemPrompt).param("htmlData", html).param("jsonSchema", Contants.LOCATOR_SCHEMA))
.call()
.entity(JSONArray.class);
// 获取响应文本
// String responseText = response.getResult().getOutput().getText();
log.info(response.toJSONString());
return response;
}
}
package pangea.hiagent.utils;
public class Contants {
public static final String LOCATOR_SCHEMA = "{\n" +
" \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n" +
" \"type\": \"array\",\n" +
" \"items\": {\n" +
" \"type\": \"object\",\n" +
" \"properties\": {\n" +
" \"field_name\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"locator\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"attributes\": {\n" +
" \"type\": \"object\",\n" +
" \"properties\": {\n" +
" \"type\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"maxlength\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"class\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"name\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"value\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"autocomplete\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"placeholder\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"readonly\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"id\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"droptreeids\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"vetitle\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"contenteditable\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"style\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"tipstext\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"fylx\": {\n" +
" \"type\": \"string\"\n" +
" }\n" +
" },\n" +
" \"additionalProperties\": false,\n" +
" \"required\": [\n" +
" \"class\",\n" +
" \"value\"\n" +
" ]\n" +
" }\n" +
" },\n" +
" \"additionalProperties\": false,\n" +
" \"required\": [\n" +
" \"field_name\",\n" +
" \"locator\",\n" +
" \"attributes\"\n" +
" ]\n" +
" }\n" +
"}";
}
......@@ -4,7 +4,7 @@ spring:
# 数据源配置
datasource:
url: jdbc:mysql://${DB_HOST:192.168.219.129}:3306/hiagent?allowMultiQueries=true&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai
url: jdbc:mysql://${DB_HOST:127.0.0.1}:3306/hiagent?allowMultiQueries=true&allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai
driver-class-name: ${DB_DRIVER:com.mysql.cj.jdbc.Driver}
username: ${DB_NAME:root}
password: ${DB_PASSWORD:123456Aa?}
......@@ -195,7 +195,7 @@ hiagent:
llm:
providers:
deepseek:
default-api-key: ${DEEPSEEK_API_KEY:}
default-api-key: ${DEEPSEEK_API_KEY:sk-e8ef4359d20b413696512db21c09db87}
default-model: deepseek-chat
base-url: https://api.deepseek.com
openai:
......
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