Commit 5fae7eda authored by youxiaoji's avatar youxiaoji

+ [增加访客预约助手]

parent 4ff6c595
......@@ -8,7 +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.agent.sse.UserSseService;
import pangea.hiagent.tool.impl.HisenseTripTool;
import pangea.hiagent.tool.impl.VisitorAppointmentTool;
import pangea.hiagent.web.service.AgentService;
import pangea.hiagent.web.service.InfoCollectorService;
import pangea.hiagent.workpanel.IWorkPanelDataCollector;
......@@ -60,7 +62,9 @@ public class DefaultReactExecutor implements ReactExecutor {
private AgentService agentService;
private final AgentToolManager agentToolManager;
@Autowired
private UserSseService userSseService;
public DefaultReactExecutor(AgentToolManager agentToolManager) {
this.agentToolManager = agentToolManager;
}
......@@ -176,7 +180,7 @@ public class DefaultReactExecutor implements ReactExecutor {
memoryService.getHistoryMessages(sessionId, historyLength);
// 添加历史消息到Prompt
messages.addAll(historyMessages);
// messages.addAll(historyMessages);
// 将当前用户消息添加到内存中,以便下次对话使用
memoryService.addUserMessageToMemory(sessionId, userInput);
......@@ -231,7 +235,7 @@ public class DefaultReactExecutor implements ReactExecutor {
// 构建Prompt,包含历史对话记录
Prompt prompt = buildPromptWithHistory(DEFAULT_SYSTEM_PROMPT, userInput, agent);
HisenseTripTool hisenseTripTool = new HisenseTripTool(agentService,infoCollectorService);
VisitorAppointmentTool hisenseTripTool = new VisitorAppointmentTool(agentService,infoCollectorService,userSseService);
hisenseTripTool.initialize();
// 订阅流式响应
chatClient.prompt(prompt)
......
......@@ -121,7 +121,8 @@ public class AgentChatService {
// 创建 SSE emitter
SseEmitter emitter = workPanelSseService.createEmitter();
workPanelSseService.registerEmitter("worker1", emitter);
// 将userId设为final以在Lambda表达式中使用
final String finalUserId = userId;
......
......@@ -552,4 +552,12 @@ public class UserSseService {
Thread.currentThread().interrupt();
}
}
public void registerEmitter(String id,SseEmitter emitter) {
this.userEmitters.put(id, emitter);
}
public SseEmitter getEmitter(String id) {
return userEmitters.get(id);
}
}
\ No newline at end of file
......@@ -14,6 +14,9 @@ public class Contants {
" \"locator\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"label_tag\": {\n" +
" \"type\": \"string\"\n" +
" },\n" +
" \"attributes\": {\n" +
" \"type\": \"object\",\n" +
" \"properties\": {\n" +
......@@ -78,4 +81,6 @@ public class Contants {
" ]\n" +
" }\n" +
"}";
}
package pangea.hiagent.common.utils;
import java.security.SecureRandom;
import java.util.concurrent.atomic.AtomicLong;
public class HybridUniqueLongGenerator {
private static final SecureRandom random = new SecureRandom();
private static final AtomicLong counter = new AtomicLong(0);
public static long generateUnique13DigitNumber() {
long timestamp = System.currentTimeMillis();
long count = counter.incrementAndGet();
// 使用时间戳的前10位 + 计数器的后3位
long timestampPart = (timestamp / 1000) * 1000;
long counterPart = count % 1000;
return timestampPart + counterPart;
}
// 更随机的版本,但仍保证唯一
public static synchronized long generateRandomUnique() {
long timestamp = System.currentTimeMillis();
// 在时间戳基础上加上一个小的随机偏移
int randomOffset = random.nextInt(100);
long result = timestamp * 100 + randomOffset;
// 确保是13位
while (result >= 10000000000000L) {
result /= 10;
}
while (result < 1000000000000L) {
result *= 10;
result += random.nextInt(10);
}
return result;
}
}
\ No newline at end of file
package pangea.hiagent.common.utils;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.atomic.AtomicLong;
public class InputCodeGenerator {
private static final String CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
private static final SecureRandom random = new SecureRandom();
private static final AtomicLong sequence = new AtomicLong(0);
public static String generateUniqueInputCode(String prefix) {
// 当前时间戳(毫秒)
long timestamp = System.currentTimeMillis();
// 序列号
long seq = sequence.incrementAndGet();
// 组合时间戳和序列号
long combined = (timestamp << 10) | (seq & 0x3FF); // 取序列号后10位
// 转为36进制
String code = Long.toString(Math.abs(combined), 36).toUpperCase();
// 确保8位长度
if (code.length() > 8) {
code = code.substring(code.length() - 8);
} else if (code.length() < 8) {
// 前面补随机字符
StringBuilder sb = new StringBuilder();
for (int i = code.length(); i < 8; i++) {
sb.append(CHARS.charAt(random.nextInt(CHARS.length())));
}
code = sb.toString() + code;
}
return prefix + code;
}
}
package pangea.hiagent.tool.impl;
import com.alibaba.fastjson2.JSON;
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.WaitUntilState;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import pangea.hiagent.agent.sse.UserSseService;
import pangea.hiagent.common.utils.Contants;
import pangea.hiagent.common.utils.HybridUniqueLongGenerator;
import pangea.hiagent.common.utils.InputCodeGenerator;
import pangea.hiagent.model.Agent;
import pangea.hiagent.web.service.AgentService;
import pangea.hiagent.web.service.InfoCollectorService;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@Slf4j
public class VisitorAppointmentTool {
public static final String pageId = "visitorAppointment";
private static final String destUrl = "https://vrms-proxy.hisense.com/ipark/hichat/#/Liteapp";
private String ssoToken;
// SSO密码(从配置文件读取)
private String ldapToken;
private String iparkToken;
// Playwright实例
private Playwright playwright;
// 浏览器实例
private Browser browser;
// 共享的浏览器上下文,用于保持登录状态
private BrowserContext sharedContext;
// 上次登录时间
private long lastLoginTime = 0;
private AgentService agentService;
private InfoCollectorService infoCollectorService;
private UserSseService userSseService;
// 登录状态有效期(毫秒),设置为30分钟
private static final long LOGIN_VALIDITY_PERIOD = 30 * 60 * 1000;
public VisitorAppointmentTool(AgentService agentService, InfoCollectorService infoCollectorService, UserSseService userSseService) {
this.agentService = agentService;
this.infoCollectorService = infoCollectorService;
this.ssoToken = "d10bc61aa4e00dcc6f08de64ca42012814fdbcee9b88aa977f7fb07d3a4018f4";
this.ldapToken = "AAECAzY5NDRBNTQ1Njk0NTRFMDV5b3V4aWFvamlaLv+jUGNEEORN24GLIC3OlqcCdw==";
this.iparkToken = "208a55e4-59bc-400b-971f-a4ed3a5379ca";
this.userSseService = userSseService;
}
@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 ssoTokenCookie = new Cookie("ssoLoginToken", ssoToken);
ssoTokenCookie.setDomain(".hisense.com");
ssoTokenCookie.setPath("/");
Cookie ldapTokenCookie = new Cookie("LtpaToken", ldapToken);
ldapTokenCookie.setDomain(".hisense.com");
ldapTokenCookie.setPath("/");
Cookie tripCookie = new Cookie("jwtToken", iparkToken);
tripCookie.setDomain("trip.hisense.com");
tripCookie.setPath("/");
// String userName= SecurityContextHolder.getContext().getAuthentication().getName();
List<Cookie> cookies = new ArrayList<>();
cookies.add(ssoTokenCookie);
cookies.add(ldapTokenCookie);
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 = "存储用户提交的访客预约申请信息", returnDirect = true)
public String applyInfoSave(@ToolParam(required = true) JSONObject infos) {
log.info("applyInfoSave(infos={})", 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() {
long startTime = System.currentTimeMillis();
Page page = null;
try {
page = sharedContext.newPage();
page.setDefaultTimeout(2 * 60 * 1000);
// 访问业务系统页面
log.info("正在访问业务系统页面: {}", destUrl);
String faviconUrl = "https://vrms-proxy.hisense.com/favicon.ico";
page.navigate(faviconUrl);
page.evaluate("() => sessionStorage.setItem('Access-Token', '208a55e4-59bc-400b-971f-a4ed3a5379ca')");
page.navigate(destUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// 检查是否重定向到了SSO登录页面
String currentUrl = page.url();
log.info("当前页面URL: {}", currentUrl);
// 如果页面尚未导航到业务系统URL,则导航到该URL
if (!page.url().equals(destUrl) && !page.url().startsWith(destUrl)) {
log.info("正在访问业务系统页面: {}", destUrl);
page.navigate(destUrl, 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格式提交信息");
//stringBuilder.append("从FORMSTART开始到FORMEND结束是要返回给用户的数据,直接展示给用户就可以。FORM START:");
JSONObject jsonObject = new JSONObject();
jsonObject.put("FORM", jsonArray);
stringBuilder.append(jsonArray.toJSONString());
//stringBuilder.append("FORM END");
// 提取页面内容
String content = stringBuilder.toString();
SseEmitter sseEmitter = userSseService.getEmitter("worker1");
JSONObject formMessage = generateJson(jsonArray);
log.info("Send Form Message {}", formMessage);
sseEmitter.send(SseEmitter.event().name("form").data(formMessage));
long endTime = System.currentTimeMillis();
log.info("成功获取海信出差申请页面内容,耗时: {} ms", endTime - startTime);
log.info("用户需要提交的信息包括:{}", content);
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 {
page.close();
} catch (Exception e) {
log.warn("关闭页面时发生异常: {}", e.getMessage());
}
}
}
}
public JSONObject generateJson(JSONArray jsonArray) {
JSONArray components = new JSONArray();
for (int i = 0; i < jsonArray.size(); i++) {
JSONObject tmp = jsonArray.getJSONObject(i);
JSONObject obj = buildComponents(tmp);
tmp.put("code", obj.getJSONObject("props").getString("name"));
components.add(obj);
}
JSONObject obj = new JSONObject();
obj.put("coms", components);
return obj;
}
private JSONObject buildComponents(JSONObject source) {
JSONObject obj = new JSONObject();
obj.put("key", HybridUniqueLongGenerator.generateUnique13DigitNumber());
obj.put("name", "输入框");
String name = InputCodeGenerator.generateUniqueInputCode("INPUT_");
if (source.getString("field_name").contains("日期")) {
obj.put("code", "HiDatePicker");
name = InputCodeGenerator.generateUniqueInputCode("DATE_");
} else {
obj.put("code", "HiInput");
}
JSONObject props = new JSONObject();
props.put("title", source.getString("field_name"));
props.put("status", "default");
props.put("name", name);
obj.put("props", props);
return obj;
}
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元素的唯一的定位表达式,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;
}
}
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