Commit a2b3a8c8 authored by 高如斌's avatar 高如斌

Merge branch 'develop_tmp' of...

Merge branch 'develop_tmp' of https://gitlab-cloud.hisense.com/gavin-group/pangea-agent into feature/chat-form
parents 50254708 1dc90ff8
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<!--
| This is the configuration file for Maven. It can be specified at two levels:
|
| 1. User Level. This settings.xml file provides configuration for a single user,
| and is normally provided in ${user.home}/.m2/settings.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -s /path/to/user/settings.xml
|
| 2. Global Level. This settings.xml file provides configuration for all Maven
| users on a machine (assuming they're all using the same Maven
| installation). It's normally provided in
| ${maven.home}/conf/settings.xml.
|
| NOTE: This location can be overridden with the CLI option:
|
| -gs /path/to/global/settings.xml
|
| The sections in this sample file are intended to give you a running start at
| getting the most out of your Maven installation. Where appropriate, the default
| values (values used when the setting is not specified) are provided.
|
|-->
<settings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<!-- localRepository
| The path to the local repository maven will use to store artifacts.
|
| Default: ${user.home}/.m2/repository
<localRepository>/path/to/local/repo</localRepository>
-->
<!-- interactiveMode
| This will determine whether maven prompts you when it needs input. If set to false,
| maven will use a sensible default value, perhaps based on some other setting, for
| the parameter in question.
|
| Default: true
<interactiveMode>true</interactiveMode>
-->
<!-- offline
| Determines whether maven should attempt to connect to the network when executing a build.
| This will have an effect on artifact downloads, artifact deployment, and others.
|
| Default: false
<offline>false</offline>
-->
<!-- pluginGroups
| This is a list of additional group identifiers that will be searched when resolving plugins by their prefix, i.e.
| when invoking a command line like "mvn prefix:goal". Maven will automatically add the group identifiers
| "org.apache.maven.plugins" and "org.codehaus.mojo" if these are not already contained in the list.
|-->
<pluginGroups>
<!-- pluginGroup
| Specifies a further group identifier to use for plugin lookup.
<pluginGroup>com.your.plugins</pluginGroup>
-->
</pluginGroups>
<!-- proxies
| This is a list of proxies which can be used on this machine to connect to the network.
| Unless otherwise specified (by system property or command-line switch), the first proxy
| specification in this list marked as active will be used.
|-->
<proxies>
<!-- proxy
| Specification for one proxy, to be used in connecting to the network.
|
<proxy>
<id>optional</id>
<active>true</active>
<protocol>http</protocol>
<username>proxyuser</username>
<password>proxypass</password>
<host>proxy.host.net</host>
<port>80</port>
<nonProxyHosts>local.net|some.host.com</nonProxyHosts>
</proxy>
-->
</proxies>
<!-- servers
| This is a list of authentication profiles, keyed by the server-id used within the system.
| Authentication profiles can be used whenever maven must make a connection to a remote server.
|-->
<servers>
<server>
<id>hisense-snapshot</id>
<username>youxiaoji</username>
<password>Aa86420?</password>
</server>
<server>
<id>hisense-nexus</id>
<username>youxiaoji</username>
<password>Aa86420?</password>
</server>
<!-- server
| Specifies the authentication information to use when connecting to a particular server, identified by
| a unique name within the system (referred to by the 'id' attribute below).
|
| NOTE: You should either specify username/password OR privateKey/passphrase, since these pairings are
| used together.
|-->
<!-- Another sample, using keys to authenticate.
<server>
<id>siteServer</id>
<privateKey>/path/to/private/key</privateKey>
<passphrase>optional; leave empty if not used.</passphrase>
</server>
-->
</servers>
<!-- mirrors
| This is a list of mirrors to be used in downloading artifacts from remote repositories.
|
| It works like this: a POM may declare a repository to use in resolving certain artifacts.
| However, this repository may have problems with heavy traffic at times, so people have mirrored
| it to several places.
|
| That repository definition will have a unique id, so we can create a mirror reference for that
| repository, to be used as an alternate download site. The mirror site will be the preferred
| server for that repository.
|-->
<mirrors>
<!-- mirror
| Specifies a repository mirror site to use instead of a given repository. The repository that
| this mirror serves has an ID that matches the mirrorOf element of this mirror. IDs are used
| for inheritance and direct lookup purposes, and must be unique across the set of mirrors.
|
<mirror>
<id>mirrorId</id>
<mirrorOf>repositoryId</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://my.repository.com/repo/path</url>
</mirror>
-->
<!--central-->
<mirror>
<id>central</id>
<mirrorOf>central</mirrorOf>
<url>http://nexus.hisense.com/repository/maven-public/</url>
</mirror>
</mirrors>
<!-- profiles
| This is a list of profiles which can be activated in a variety of ways, and which can modify
| the build process. Profiles provided in the settings.xml are intended to provide local machine-
| specific paths and repository locations which allow the build to work in the local environment.
|
| For example, if you have an integration testing plugin - like cactus - that needs to know where
| your Tomcat instance is installed, you can provide a variable here such that the variable is
| dereferenced during the build process to configure the cactus plugin.
|
| As noted above, profiles can be activated in a variety of ways. One way - the activeProfiles
| section of this document (settings.xml) - will be discussed later. Another way essentially
| relies on the detection of a system property, either matching a particular value for the property,
| or merely testing its existence. Profiles can also be activated by JDK version prefix, where a
| value of '1.4' might activate a profile when the build is executed on a JDK version of '1.4.2_07'.
| Finally, the list of active profiles can be specified directly from the command line.
|
| NOTE: For profiles defined in the settings.xml, you are restricted to specifying only artifact
| repositories, plugin repositories, and free-form properties to be used as configuration
| variables for plugins in the POM.
|
|-->
<profiles>
<!-- profile
| Specifies a set of introductions to the build process, to be activated using one or more of the
| mechanisms described above. For inheritance purposes, and to activate profiles via <activatedProfiles/>
| or the command line, profiles have to have an ID that is unique.
|
| An encouraged best practice for profile identification is to use a consistent naming convention
| for profiles, such as 'env-dev', 'env-test', 'env-production', 'user-jdcasey', 'user-brett', etc.
| This will make it more intuitive to understand what the set of introduced profiles is attempting
| to accomplish, particularly when you only have a list of profile id's for debug.
|
| This profile example uses the JDK version to trigger activation, and provides a JDK-specific repo.
<profile>
<id>jdk-1.4</id>
<activation>
<jdk>1.4</jdk>
</activation>
<repositories>
<repository>
<id>jdk14</id>
<name>Repository for JDK 1.4 builds</name>
<url>http://www.myhost.com/maven/jdk14</url>
<layout>default</layout>
<snapshotPolicy>always</snapshotPolicy>
</repository>
</repositories>
</profile>
-->
<profile>
<id>hisense-nexus</id>
<repositories>
<repository>
<id>hisense-nexus</id>
<name>hisense-nexus</name>
<url>http://nexus.hisense.com/repository/maven-public/</url>
</repository>
<repository>
<id>hisense-snapshot</id>
<name>hisense-snapshot</name>
<url>http://nexus.hisense.com/repository/maven-public-snapshot/</url>
<layout>default</layout>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>hisense-nexus</id>
<name>hisense-nexus</name>
<url>http://nexus.hisense.com/repository/maven-public/</url>
</pluginRepository>
</pluginRepositories>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<!--
| Here is another profile, activated by the system property 'target-env' with a value of 'dev',
| which provides a specific path to the Tomcat instance. To use this, your plugin configuration
| might hypothetically look like:
|
| ...
| <plugin>
| <groupId>org.myco.myplugins</groupId>
| <artifactId>myplugin</artifactId>
|
| <configuration>
| <tomcatLocation>${tomcatPath}</tomcatLocation>
| </configuration>
| </plugin>
| ...
|
| NOTE: If you just wanted to inject this configuration whenever someone set 'target-env' to
| anything, you could just leave off the <value/> inside the activation-property.
|
<profile>
<id>env-dev</id>
<activation>
<property>
<name>target-env</name>
<value>dev</value>
</property>
</activation>
<properties>
<tomcatPath>/path/to/tomcat/instance</tomcatPath>
</properties>
</profile>
-->
</profiles>
<!-- activeProfiles
| List of profiles that are active for all builds.
|
<activeProfiles>
<activeProfile>alwaysActiveProfile</activeProfile>
<activeProfile>anotherAlwaysActiveProfile</activeProfile>
</activeProfiles>
-->
<activeProfiles>
<activeProfile>hisense-nexus</activeProfile>
</activeProfiles>
</settings>
...@@ -44,16 +44,16 @@ ...@@ -44,16 +44,16 @@
</dependencyManagement> </dependencyManagement>
<!-- Repositories for Spring AI milestone versions --> <!-- Repositories for Spring AI milestone versions -->
<repositories> <!-- <repositories>-->
<repository> <!-- <repository>-->
<id>spring-milestones</id> <!-- <id>spring-milestones</id>-->
<name>Spring Milestones</name> <!-- <name>Spring Milestones</name>-->
<url>https://repo.spring.io/milestone</url> <!-- <url>https://repo.spring.io/milestone</url>-->
<snapshots> <!-- <snapshots>-->
<enabled>false</enabled> <!-- <enabled>false</enabled>-->
</snapshots> <!-- </snapshots>-->
</repository> <!-- </repository>-->
</repositories> <!-- </repositories>-->
<dependencies> <dependencies>
<!-- Spring Boot Web with Undertow --> <!-- Spring Boot Web with Undertow -->
...@@ -288,7 +288,7 @@ ...@@ -288,7 +288,7 @@
<dependency> <dependency>
<groupId>com.microsoft.playwright</groupId> <groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId> <artifactId>playwright</artifactId>
<version>1.46.0</version> <version>1.49.0</version>
</dependency> </dependency>
<!-- FastJSON2 for JSON processing --> <!-- FastJSON2 for JSON processing -->
......
...@@ -5,16 +5,24 @@ import org.springframework.ai.chat.client.ChatClient; ...@@ -5,16 +5,24 @@ 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.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.common.utils.UserUtils; import pangea.hiagent.common.utils.UserUtils;
import pangea.hiagent.web.service.ChatService;
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.function.Consumer; import java.util.function.Consumer;
/** /**
...@@ -24,6 +32,8 @@ import java.util.function.Consumer; ...@@ -24,6 +32,8 @@ import java.util.function.Consumer;
@Service @Service
public class DefaultReactExecutor implements ReactExecutor { public class DefaultReactExecutor implements ReactExecutor {
private final UserSseService userSseService;
private final ChatService chatService;
@Value("${hiagent.react.system-prompt}") @Value("${hiagent.react.system-prompt}")
private String defaultSystemPrompt; private String defaultSystemPrompt;
...@@ -38,12 +48,15 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -38,12 +48,15 @@ public class DefaultReactExecutor implements ReactExecutor {
private final AgentToolManager agentToolManager; private final AgentToolManager agentToolManager;
public DefaultReactExecutor(EventSplitter eventSplitter, AgentToolManager agentToolManager ,
MemoryService memoryService, ErrorHandlerService errorHandlerService) { public DefaultReactExecutor(EventSplitter eventSplitter, AgentToolManager agentToolManager,
MemoryService memoryService, ErrorHandlerService errorHandlerService, UserSseService userSseService, ChatService chatService) {
this.eventSplitter = eventSplitter; this.eventSplitter = eventSplitter;
this.agentToolManager = agentToolManager; this.agentToolManager = agentToolManager;
this.memoryService = memoryService; this.memoryService = memoryService;
this.errorHandlerService = errorHandlerService; this.errorHandlerService = errorHandlerService;
this.userSseService = userSseService;
this.chatService = chatService;
} }
@Override @Override
...@@ -67,7 +80,7 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -67,7 +80,7 @@ public class DefaultReactExecutor implements ReactExecutor {
List<Object> agentTools = getAgentTools(agent); List<Object> agentTools = getAgentTools(agent);
try { try {
Prompt prompt = buildPromptWithHistory(defaultSystemPrompt, userInput, agent, userId); Prompt prompt = buildPromptWithHistory(defaultSystemPrompt, userInput, agent, userId, false);
ChatResponse response = chatClient.prompt(prompt) ChatResponse response = chatClient.prompt(prompt)
.tools(agentTools.toArray()) .tools(agentTools.toArray())
...@@ -108,7 +121,7 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -108,7 +121,7 @@ public class DefaultReactExecutor implements ReactExecutor {
* @param userId 用户ID(可选,如果为null则自动获取) * @param userId 用户ID(可选,如果为null则自动获取)
* @return 构建好的提示词对象 * @return 构建好的提示词对象
*/ */
private Prompt buildPromptWithHistory(String systemPrompt, String userInput, Agent agent, String userId) { private Prompt buildPromptWithHistory(String systemPrompt, String userInput, Agent agent, String userId, boolean newChat) {
List<org.springframework.ai.chat.messages.Message> messages = new ArrayList<>(); List<org.springframework.ai.chat.messages.Message> messages = new ArrayList<>();
messages.add(new SystemMessage(systemPrompt)); messages.add(new SystemMessage(systemPrompt));
...@@ -125,8 +138,9 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -125,8 +138,9 @@ public class DefaultReactExecutor implements ReactExecutor {
List<org.springframework.ai.chat.messages.Message> historyMessages = List<org.springframework.ai.chat.messages.Message> historyMessages =
memoryService.getHistoryMessages(sessionId, historyLength); memoryService.getHistoryMessages(sessionId, historyLength);
if (!newChat) {
messages.addAll(historyMessages); messages.addAll(historyMessages);
}
memoryService.addUserMessageToMemory(sessionId, userInput); memoryService.addUserMessageToMemory(sessionId, userInput);
} catch (Exception e) { } catch (Exception e) {
...@@ -135,7 +149,9 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -135,7 +149,9 @@ public class DefaultReactExecutor implements ReactExecutor {
} }
messages.add(new UserMessage(userInput)); messages.add(new UserMessage(userInput));
for (Message message : messages) {
log.info("message is {}", message);
}
return new Prompt(messages); return new Prompt(messages);
} }
...@@ -155,17 +171,40 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -155,17 +171,40 @@ public class DefaultReactExecutor implements ReactExecutor {
StringBuilder fullResponse = new StringBuilder(); StringBuilder fullResponse = new StringBuilder();
try { try {
Prompt prompt = buildPromptWithHistory(defaultSystemPrompt, userInput, agent, userId); SseTokenEmitter sseTokenEmitter = (SseTokenEmitter) tokenConsumer;
String emitterId = sseTokenEmitter.getEmitterId();
String tmpUserId = sseTokenEmitter.getUserId();
Prompt prompt = buildPromptWithHistory(defaultSystemPrompt, userInput, agent, tmpUserId, false);
log.info("agent id {}", agent.getId());
log.info("agentTools {}", agentTools);
if (agent.getId().compareToIgnoreCase("agent-8") == 0) {
if (!chatService.chatExists(tmpUserId, agent.getId())) {
log.info("new chat for {} {} ", userId, agent.getId());
prompt = buildPromptWithHistory(defaultSystemPrompt, userInput, agent, tmpUserId, true);
}
chatClient.prompt(prompt) chatClient.prompt(prompt)
.tools(agentTools.toArray()) .tools(agentTools.toArray())
.toolContext(Map.of("emitterId", emitterId, "userId", sseTokenEmitter.getUserId(),"agentId",agent.getId()))
.stream() .stream()
.chatResponse() .chatResponse()
.subscribe( .subscribe(
chatResponse -> handleTokenResponse(chatResponse, tokenConsumer, fullResponse), chatResponse -> handleTokenResponse(chatResponse, tokenConsumer, fullResponse),
throwable -> handleStreamError(throwable, tokenConsumer), throwable -> handleStreamError(throwable, tokenConsumer, emitterId),
() -> handleStreamCompletion(tokenConsumer, fullResponse, agent, userId) () -> handleStreamCompletion(tokenConsumer, fullResponse, agent, userId, emitterId)
); );
} else {
chatClient.prompt(prompt)
.tools(agentTools.toArray())
.stream()
.chatResponse()
.subscribe(
chatResponse -> handleTokenResponse(chatResponse, tokenConsumer, fullResponse),
throwable -> handleStreamError(throwable, tokenConsumer, emitterId),
() -> handleStreamCompletion(tokenConsumer, fullResponse, agent, userId, emitterId)
);
}
} catch (Exception e) { } catch (Exception e) {
log.error("流式执行ReAct流程时发生错误", e); log.error("流式执行ReAct流程时发生错误", e);
...@@ -207,12 +246,14 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -207,12 +246,14 @@ 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) { private void handleStreamCompletion(Consumer<String> tokenConsumer, StringBuilder fullResponse, Agent agent, String userId, String emitterId) {
try { try {
log.info("流式处理完成"); log.info("流式处理完成");
String responseStr = fullResponse.toString(); String responseStr = fullResponse.toString();
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);
...@@ -274,7 +315,9 @@ public class DefaultReactExecutor implements ReactExecutor { ...@@ -274,7 +315,9 @@ public class DefaultReactExecutor implements ReactExecutor {
* @param throwable 异常对象 * @param throwable 异常对象
* @param tokenConsumer token消费者 * @param tokenConsumer token消费者
*/ */
private void handleStreamError(Throwable throwable, Consumer<String> tokenConsumer) { private void handleStreamError(Throwable throwable, Consumer<String> tokenConsumer, String emitterId) {
log.info("error,remove emitterId:{}", emitterId);
userSseService.removeEmitter(emitterId);
errorHandlerService.handleStreamError(throwable, tokenConsumer, "ReAct流式处理"); errorHandlerService.handleStreamError(throwable, tokenConsumer, "ReAct流式处理");
} }
......
...@@ -15,6 +15,8 @@ import pangea.hiagent.tool.AgentToolManager; ...@@ -15,6 +15,8 @@ import pangea.hiagent.tool.AgentToolManager;
import pangea.hiagent.web.dto.AgentRequest; import pangea.hiagent.web.dto.AgentRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import java.util.UUID;
/** /**
* Agent 对话服务 * Agent 对话服务
* 职责:协调整个AI对话流程,作为流式处理的统一入口和协调者 * 职责:协调整个AI对话流程,作为流式处理的统一入口和协调者
...@@ -125,9 +127,11 @@ public class AgentChatService { ...@@ -125,9 +127,11 @@ public class AgentChatService {
// 创建 SSE emitter // 创建 SSE emitter
SseEmitter emitter = userSseService.createAndRegisterConnection(userId); SseEmitter emitter = userSseService.createAndRegisterConnection(userId);
String emitterId = UUID.randomUUID().toString();
log.info("emitterId: {}", emitterId);
userSseService.registerEmitter(emitterId, emitter);
// 异步处理对话,避免阻塞HTTP连接 // 异步处理对话,避免阻塞HTTP连接
processChatStreamAsync(emitter, agent, chatRequest, userId); processChatStreamAsync(emitter, agent, chatRequest, userId,emitterId);
return emitter; return emitter;
} }
...@@ -136,14 +140,14 @@ public class AgentChatService { ...@@ -136,14 +140,14 @@ public class AgentChatService {
* 异步处理流式对话 * 异步处理流式对话
*/ */
@Async @Async
private void processChatStreamAsync(SseEmitter emitter, Agent agent, ChatRequest chatRequest, String userId) { private void processChatStreamAsync(SseEmitter emitter, Agent agent, ChatRequest chatRequest, String userId,String emitterId) {
try { try {
// 首先检查连接状态 // 首先检查连接状态
if (emitter != null && userSseService.isEmitterCompleted(emitter)) { if (emitter != null && userSseService.isEmitterCompleted(emitter)) {
log.debug("SSE连接已关闭,跳过异步处理"); log.debug("SSE连接已关闭,跳过异步处理");
return; return;
} }
processChatRequest(emitter, agent, chatRequest, userId); processChatRequest(emitter, agent, chatRequest, userId,emitterId);
} catch (Exception e) { } catch (Exception e) {
log.error("处理聊天请求时发生异常", e); log.error("处理聊天请求时发生异常", e);
...@@ -163,7 +167,7 @@ public class AgentChatService { ...@@ -163,7 +167,7 @@ public class AgentChatService {
* @param chatRequest 聊天请求 * @param chatRequest 聊天请求
* @param userId 用户ID * @param userId 用户ID
*/ */
private void processChatRequest(SseEmitter emitter, Agent agent, ChatRequest chatRequest, String userId) { private void processChatRequest(SseEmitter emitter, Agent agent, ChatRequest chatRequest, String userId,String emitterId) {
try { try {
// 参数验证 // 参数验证
if (!validateParameters(emitter, agent, chatRequest, userId)) { if (!validateParameters(emitter, agent, chatRequest, userId)) {
...@@ -197,7 +201,7 @@ public class AgentChatService { ...@@ -197,7 +201,7 @@ public class AgentChatService {
// 创建新的SseTokenEmitter实例 // 创建新的SseTokenEmitter实例
SseTokenEmitter tokenEmitter = new SseTokenEmitter(userSseService, emitter, agent, request, userId, this::handleCompletion); SseTokenEmitter tokenEmitter = new SseTokenEmitter(userSseService, emitter, agent, request, userId, this::handleCompletion);
tokenEmitter.setEmitterId(emitterId);
// 处理流式请求前再次检查连接状态 // 处理流式请求前再次检查连接状态
if (!userSseService.isEmitterCompleted(emitter)) { if (!userSseService.isEmitterCompleted(emitter)) {
processor.processStreamRequest(request, agent, userId, tokenEmitter); processor.processStreamRequest(request, agent, userId, tokenEmitter);
......
...@@ -25,6 +25,7 @@ public class SseTokenEmitter implements TokenConsumerWithCompletion { ...@@ -25,6 +25,7 @@ public class SseTokenEmitter implements TokenConsumerWithCompletion {
private final AgentRequest request; private final AgentRequest request;
private final String userId; private final String userId;
private final CompletionCallback completionCallback; private final CompletionCallback completionCallback;
private String emitterId;
/** /**
* 构造函数 * 构造函数
...@@ -160,4 +161,13 @@ public class SseTokenEmitter implements TokenConsumerWithCompletion { ...@@ -160,4 +161,13 @@ 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 void setEmitterId(String emitterId) {
this.emitterId = emitterId;
}
public String getEmitterId() {
return emitterId;
}
public String getUserId() {
return userId;
}
} }
\ No newline at end of file
...@@ -719,4 +719,17 @@ public class UserSseService { ...@@ -719,4 +719,17 @@ public class UserSseService {
Thread.currentThread().interrupt(); Thread.currentThread().interrupt();
} }
} }
public void registerEmitter(String id,SseEmitter emitter) {
this.userEmitters.put(id, emitter);
}
public SseEmitter getEmitter(String id) {
return userEmitters.get(id);
}
public boolean removeEmitter(String id) {
userEmitters.remove(id);
return true;
}
} }
\ No newline at end of file
package pangea.hiagent.common.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" +
" \"label_tag\": {\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" +
"}";
}
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.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.model;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@TableName("sys_user_token")
public class UserToken extends BaseEntity{
private static final long serialVersionUID = 1L;
/**
* 用户名
*/
private String username;
private String userId;
private String tokenType;
private String tokenValue;
}
...@@ -299,7 +299,7 @@ public class HisenseLbpmApprovalTool extends BaseTool { ...@@ -299,7 +299,7 @@ public class HisenseLbpmApprovalTool extends BaseTool {
} }
// 定位并点击提交按钮 - 尝试新的选择器 // 定位并点击提交按钮 - 尝试新的选择器
String submitButtonSelector = "input[id='process_review_button'][class='process_review_button'][type='button'][value='提交']"; String submitButtonSelector = "input[id='process_review_button']";
log.debug("正在定位提交按钮: {}", submitButtonSelector); log.debug("正在定位提交按钮: {}", submitButtonSelector);
Locator submitButton = page.locator(submitButtonSelector); Locator submitButton = page.locator(submitButtonSelector);
if (submitButton.count() > 0) { if (submitButton.count() > 0) {
...@@ -307,7 +307,7 @@ public class HisenseLbpmApprovalTool extends BaseTool { ...@@ -307,7 +307,7 @@ public class HisenseLbpmApprovalTool extends BaseTool {
log.info("提交按钮点击完成 (使用新选择器)"); log.info("提交按钮点击完成 (使用新选择器)");
} else { } else {
// 如果新选择器未找到元素,尝试原选择器 // 如果新选择器未找到元素,尝试原选择器
submitButtonSelector = "input[id='process_review_button'][class='process_review_button'][type='button'][value='提交']"; submitButtonSelector = "input[id='process_review_button']";
log.debug("新选择器未找到元素,尝试原选择器: {}", submitButtonSelector); log.debug("新选择器未找到元素,尝试原选择器: {}", submitButtonSelector);
submitButton = page.locator(submitButtonSelector); submitButton = page.locator(submitButtonSelector);
if (submitButton.count() > 0) { if (submitButton.count() > 0) {
......
...@@ -49,10 +49,10 @@ public class HisenseSsoLoginTool extends BaseTool { ...@@ -49,10 +49,10 @@ public class HisenseSsoLoginTool extends BaseTool {
private static final int MFA_WAIT_FOR_URL_TIMEOUT = 45000; private static final int MFA_WAIT_FOR_URL_TIMEOUT = 45000;
// 用户名输入框选择器 // 用户名输入框选择器
private static final String USERNAME_INPUT_SELECTOR = "input[placeholder='账号名/海信邮箱/手机号']"; private static final String USERNAME_INPUT_SELECTOR = "#username .emailSelect-account input[type='text']";
// 密码输入框选择器 // 密码输入框选择器
private static final String PASSWORD_INPUT_SELECTOR = "input[placeholder='密码'][type='password']"; private static final String PASSWORD_INPUT_SELECTOR = "#username .emailSelect-account input[type='password']";
// 登录按钮选择器 // 登录按钮选择器
private static final String LOGIN_BUTTON_SELECTOR = "#login-button"; private static final String LOGIN_BUTTON_SELECTOR = "#login-button";
......
package pangea.hiagent.tool.impl;
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.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.stereotype.Component;
import pangea.hiagent.common.utils.Contants;
import pangea.hiagent.model.Agent;
import pangea.hiagent.web.service.AgentService;
import pangea.hiagent.web.service.InfoCollectorService;
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
@Component
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;
this.ssoToken = "d10bc61aa4e00dcc6f08de64ca42012814fdbcee9b88aa977f7fb07d3a4018f4";
this.ldapToken = "AAECAzY5NDRBNTQ1Njk0NTRFMDV5b3V4aWFvamlaLv+jUGNEEORN24GLIC3OlqcCdw==";
this.tripToken = "c88c2f09a20140e190357ebb68f27e35";
}
@PostConstruct
public void initialize() {
try {
if(StringUtils.isEmpty(tripToken)){
}
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("FCC_SESSION", tripToken);
tripCookie.setDomain("trip.hisense.com");
tripCookie.setPath("/");
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 = "提交出差申请")
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(pageId).values();
// 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.tool.impl;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.AriaRole;
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.model.ToolContext;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import pangea.hiagent.agent.service.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.model.UserToken;
import pangea.hiagent.web.service.AgentService;
import pangea.hiagent.web.service.ChatService;
import pangea.hiagent.web.service.InfoCollectorService;
import pangea.hiagent.web.service.UserTokenService;
import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
@Slf4j
@Component
public class VisitorAppointmentTool {
public static final String pageId = "visitorAppointment";
private static final String destUrl = "https://vrms-proxy.hisense.com/ipark/hichat/#/Liteapp";
private static final DateTimeFormatter customFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private String ssoToken;
// SSO密码(从配置文件读取)
private String ldapToken;
// Playwright实例
private Playwright playwright;
// 浏览器实例
private Browser browser;
// 共享的浏览器上下文,用于保持登录状态
private BrowserContext sharedContext;
// 上次登录时间
private long lastLoginTime = 0;
private AgentService agentService;
private InfoCollectorService infoCollectorService;
private UserSseService userSseService;
private UserTokenService userTokenService;
private ChatService chatService;
// 登录状态有效期(毫秒),设置为30分钟
private static final long LOGIN_VALIDITY_PERIOD = 30 * 60 * 1000;
public VisitorAppointmentTool(UserTokenService userTokenService,AgentService agentService, InfoCollectorService infoCollectorService, UserSseService userSseService,ChatService chatService) {
this.agentService = agentService;
this.infoCollectorService = infoCollectorService;
this.ssoToken = "33f667865c395f164d29487c15fe74bf76b463f2941ef6af55d14a35a11d60b1";
this.ldapToken = "AAECAzY5NDRBNTQ1Njk0NTRFMDV5b3V4aWFvamlaLv+jUGNEEORN24GLIC3OlqcCdw==";
this.userSseService = userSseService;
this.userTokenService = userTokenService;
this.chatService = chatService;
}
@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("/");
// String userName= SecurityContextHolder.getContext().getAuthentication().getName();
List<Cookie> cookies = new ArrayList<>();
cookies.add(ssoTokenCookie);
cookies.add(ldapTokenCookie);
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);
}
}
private String setAccessToken(ToolContext toolContext) {
String userId = toolContext.getContext().get("userId").toString();
log.info("start set access token for {}", userId);
UserToken userToken = userTokenService.getUserToken(userId,"pangea");
Cookie tripCookie = new Cookie("jwtToken", userToken.getTokenValue());
tripCookie.setDomain("vrms-proxy.hisense.com");
tripCookie.setPath("/");
List<Cookie> cookies = new ArrayList<>();
cookies.add(tripCookie);
sharedContext.addCookies(cookies);
log.info("end set access token for {}", userId);
return userToken.getTokenValue();
}
@Tool(description = "提交访客预约申请")
public String submitAppointmentApply(ToolContext toolContext) {
// sharedContext.tracing().start(new Tracing.StartOptions()
// .setScreenshots(true)
// .setSnapshots(true)
// .setSources(true));
log.info("submit apply info ");
String accessToken = setAccessToken(toolContext);
JSONArray jsonArray = infoCollectorService.getInfo(pageId);
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', '" + accessToken + "')");
page.navigate(destUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// 检查是否重定向到了SSO登录页面
String currentUrl = page.url();
log.info("当前页面URL: {}", currentUrl);
String parkValue = "";
List<JSONObject> dateJson = new ArrayList<>();
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(obj.getString("code")).toString();
log.info("fieldName {} fieldValue {} ", fieldName,fieldValue);
if (fieldName.compareToIgnoreCase("访问园区") == 0) {
parkValue = fieldValue;
continue;
}
if (fieldName.compareToIgnoreCase("接访员工手机号") == 0 || fieldName.compareToIgnoreCase("接访员工姓名") == 0) {
continue;
}
if (fieldName.contains("日期")) {
dateJson.add(obj);
//fieldLocator.evaluate("el => el.value = '"+fieldValue+"'");;
} else {
Locator fieldLocator = page.locator(".van-cell")
.filter(new Locator.FilterOptions().setHasText(Pattern.compile("^" + fieldName + "$")))
.locator("input");
Locator.FillOptions fillOptions = new Locator.FillOptions();
fillOptions.setForce(true);
fieldLocator.fill(fieldValue, fillOptions);
}
//saveScreenShot(page.screenshot(),"ipark");
}
// 处理园区选择
page.locator(".van-cell")
.filter(new Locator.FilterOptions().setHasText(Pattern.compile("^访问园区$")))
.locator("input").click();
Locator.WaitForOptions waitUntilOptions = new Locator.WaitForOptions();
waitUntilOptions.setState(WaitForSelectorState.ATTACHED);
page.locator(".van-overlay").waitFor(waitUntilOptions);
Locator parkColumns = page.locator(".van-picker-column");
Locator destPark = parkColumns.locator("li").filter(new Locator.FilterOptions().setHasText(parkValue));
destPark.dispatchEvent("click");
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("确认")).click();
page.locator(".van-overlay").waitFor(new Locator.WaitForOptions().setState(WaitForSelectorState.HIDDEN));
for (JSONObject tmp : dateJson) {
String fieldName = tmp.getString("field_name");
String fieldValue = infoCollectorService.getValue(tmp.getString("code")).toString();
String[] values = fieldValue.split("-");
page.locator(".van-cell")
.filter(new Locator.FilterOptions().setHasText(Pattern.compile("^" + fieldName + "$")))
.locator("input").click();
page.locator(".van-overlay").first().waitFor(waitUntilOptions);
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(values[0] + "年")).click();
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(values[1] + "月")).click();
Locator dayColumn = page.locator(".van-picker-column").nth(2);
Locator targetDay = dayColumn.locator("li").filter(new Locator.FilterOptions().setHasText(values[2] + "日"));
targetDay.dispatchEvent("click");
//page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName(values[2] + "日")).click();
// 点击确认
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("确认")).click();
page.locator(".van-overlay").first().waitFor(new Locator.WaitForOptions().setState(WaitForSelectorState.HIDDEN));
}
// saveScreenShot(page.screenshot(), "ipark");
page.getByRole(AriaRole.BUTTON, new Page.GetByRoleOptions().setName("提交"))
.click();
page.waitForTimeout(3000);
if (page.url().compareToIgnoreCase(destUrl) == 0) {
log.info("有必填项未提供");
}
log.info("Submit Success");
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());
}
}
infoCollectorService.clearValue();
//infoCollectorService.clearInfo(pageId);
String userId = toolContext.getContext().get("userId").toString();
String agentId = toolContext.getContext().get("agentId").toString();
chatService.removeChat(userId, agentId);
// sharedContext.tracing().stop(new Tracing.StopOptions()
// .setPath(Paths.get("trace1.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, ToolContext toolContext) throws IOException {
log.info("applyInfoSave(infos={})", infos);
infos.keySet().forEach(key -> {
infoCollectorService.saveValue(key, infos.get(key));
});
infoCollectorService.saveDefaultValue(pageId);
Map<String,String> keys = infoCollectorService.findLackInfo(pageId);
if (!keys.isEmpty()) {
JSONArray jsonArray = infoCollectorService.getInfo(pageId);
JSONArray lackJson = new JSONArray();
for (int i = 0; i < jsonArray.size(); i++) {
JSONObject tmp = jsonArray.getJSONObject(i);
JSONObject pangeJson = tmp.getJSONObject("pangea_json");
if(infoCollectorService.getValue(tmp.getString("code")) != null){
pangeJson.getJSONObject("props").put("value", infoCollectorService.getValue(tmp.getString("code")).toString());
}
lackJson.add(pangeJson);
}
JSONObject formMessage = new JSONObject();
formMessage.put("coms", lackJson);
sendFormMessage(formMessage, toolContext);
}
StringBuilder sb = new StringBuilder();
if (keys.isEmpty()) {
sb.append("所有必需信息已收集,准备提交申请");
} else {
sb.append("用户还有以下信息未提交:");
sb.append("\n");
for (Map.Entry<String,String> key : keys.entrySet()) {
sb.append(key.getValue());
sb.append("(");
sb.append(key);
sb.append(")");
sb.append(",");
}
sb.append("\n");
sb.append("提示用户继续提交信息");
}
log.info("notice {}",sb.toString());
return sb.toString();
}
private void sendFormMessage(JSONObject formMessage, ToolContext toolContext) throws IOException {
String emitterId = toolContext.getContext().get("emitterId").toString();
SseEmitter sseEmitter = userSseService.getEmitter(emitterId);
log.info("Send Form Message {}", formMessage);
sseEmitter.send(SseEmitter.event().name("form").data(formMessage));
}
/**
* 工具方法:获取海信访客预约申请的网页内容
*
* @return 页面内容(HTML文本)
*/
@Tool(description = "获取访客预约申请必要信息")
public String getAppointmentApplyNecessaryInfo(ToolContext toolContext) {
StringBuilder stringBuilder = new StringBuilder();
if (infoCollectorService.exists(pageId)) {
JSONArray jsonArray = infoCollectorService.getInfo(pageId);
JSONArray lackJson = new JSONArray();
for (int i = 0; i < jsonArray.size(); i++) {
JSONObject tmp = jsonArray.getJSONObject(i);
lackJson.add(tmp.getJSONObject("pangea_json"));
}
JSONObject formMessage = new JSONObject();
formMessage.put("coms", lackJson);
try {
sendFormMessage(formMessage, toolContext);
} catch (Exception e) {
e.printStackTrace();
}
stringBuilder.append(formMessage.toJSONString());
stringBuilder.append("提示用户以json格式提交信息;如果用户已提供部分信息,需要将这些信息与`props.name`属性的值进行匹配,并将匹配之后的信息以json格式提交到`applyInfoSave`以保存信息");
return stringBuilder.toString();
}
String accessToken = setAccessToken(toolContext);
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', '" + accessToken + "')");
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);
}
// 提取页面内容
String content = stringBuilder.toString();
JSONArray result = new JSONArray();
for (int i = 0; i < jsonArray.size(); i++) {
JSONObject tmp = jsonArray.getJSONObject(i);
if(tmp.getString("field_name").compareToIgnoreCase("接访员工手机号") == 0
|| tmp.getString("field_name").compareToIgnoreCase("接访员工姓名") == 0){
continue;
}
result.add(tmp);
}
JSONObject formMessage = generateJson(jsonArray);
sendFormMessage(formMessage, toolContext);
long endTime = System.currentTimeMillis();
log.info("成功获取海信出差申请页面内容,耗时: {} ms", endTime - startTime);
log.info("用户需要提交的信息包括:{}", formMessage);
stringBuilder.append(result.toJSONString());
stringBuilder.append("提示用户以json格式提交信息;如果用户已提供部分信息,需要将这些信息与`props.name`属性的值进行匹配,并将匹配之后的信息以json格式提交到`applyInfoSave`以保存信息");
return stringBuilder.toString();
} 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"));
tmp.put("pangea_json", obj);
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_");
JSONObject props = new JSONObject();
props.put("title", source.getString("field_name"));
props.put("status", "default");
if (source.getString("field_name").contains("日期")) {
obj.put("code", "HiDatePicker");
name = InputCodeGenerator.generateUniqueInputCode("DATE_");
props.put("format", "YYYY-MM-DD");
} else {
obj.put("code", "HiInput");
}
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;
}
}
package pangea.hiagent.web.repository;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import pangea.hiagent.model.Tool;
import pangea.hiagent.model.User;
import pangea.hiagent.model.UserToken;
import java.util.List;
@Mapper
public interface UserTokenRepository extends BaseMapper<User> {
@Select("SELECT * FROM sys_user_token WHERE user_id = #{userName} AND token_type=#{tokenType} ORDER BY created_at DESC")
UserToken getTokenByUserIdAndTokenType(String userName, String tokenType);
}
...@@ -82,6 +82,9 @@ public class AuthService { ...@@ -82,6 +82,9 @@ public class AuthService {
// 检查是否为开发环境,如果是则允许任意密码 // 检查是否为开发环境,如果是则允许任意密码
boolean isDevEnvironment = Arrays.asList(environment.getActiveProfiles()).contains("dev") || boolean isDevEnvironment = Arrays.asList(environment.getActiveProfiles()).contains("dev") ||
Arrays.asList(environment.getDefaultProfiles()).contains("default"); Arrays.asList(environment.getDefaultProfiles()).contains("default");
log.info("active profile {}",environment.getActiveProfiles());
log.info("default profile {}",environment.getDefaultProfiles());
if (isDevEnvironment) { if (isDevEnvironment) {
log.info("开发环境: 跳过密码验证"); log.info("开发环境: 跳过密码验证");
......
package pangea.hiagent.web.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@Slf4j
@Service
public class ChatService {
private final ConcurrentMap<String,String> chatList = new ConcurrentHashMap<>(1024);
public boolean chatExists(String userId,String agentId) {
String chatId = buildChatId(userId,agentId);
boolean exists = chatList.containsKey(chatId);
if(!exists){
log.info("put chatId:{}",chatId);
chatList.put(chatId,"exists");
}
return exists;
}
public void removeChat(String userId,String agentId) {
String chatId = buildChatId(userId,agentId);
log.info("remove chatId:{}",chatId);
chatList.remove(chatId);
}
private String buildChatId(String userId,String agentId) {
return userId+"-"+agentId;
}
}
package pangea.hiagent.web.service;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import pangea.hiagent.tool.impl.HisenseTripTool;
import pangea.hiagent.tool.impl.VisitorAppointmentTool;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
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 clearInfo(String pageId){
infos.remove(pageId);
}
public void saveValue(String key, Object value) {
log.info("key {} value {}", key, value);
if(value == null || StringUtils.isEmpty(value.toString())
|| value.toString().compareToIgnoreCase("待补充") == 0 || value.toString().contains("海信园区")) {
return;
}
values.put(key, value);
}
public void clearValue() {
values.clear();
}
public void saveDefaultValue(String pageId){
JSONArray jsonArray = infos.get(pageId);
for(int i = 0; i < jsonArray.size(); i++){
JSONObject object = jsonArray.getJSONObject(i);
if(object.getString("field_name").compareToIgnoreCase("接访员工手机号") == 0){
String code = object.getString("code");
saveValue(code, "15841169015");
}
if(object.getString("field_name").compareToIgnoreCase("接访员工姓名") == 0){
String code = object.getString("code");
saveValue(code, "杜艺");
}
if(object.getString("field_name").compareToIgnoreCase("证件类型") == 0){
String code = object.getString("code");
saveValue(code, "居民身份证");
}
}
}
public Object getValue(String key) {
return values.get(key);
}
public Map<String,String> findLackInfo(String pageId) {
Set<String> valueKeys = values.keySet();
log.info("value keys {}", valueKeys);
Set<String> allKeys = infos.get(pageId).stream().map(t -> ((JSONObject) t).getString("code")).collect(Collectors.toSet());
log.info("all keys {}", allKeys);
allKeys.removeAll(valueKeys);
log.info("lack keys {}", allKeys);
// Set<String> lackInfos = new HashSet<>();
Map<String,String> lackInfos = new HashMap<>();
if(allKeys.isEmpty()){
log.info("info is good enough");
return lackInfos;
}
infos.get(pageId).stream().forEach(t ->
{
JSONObject info = (JSONObject) t;
String code = info.getString("code");
if (allKeys.contains(code)) {
lackInfos.put(code,info.getString("field_name"));
}
});
log.info("lack infos {}", lackInfos);
return lackInfos;
}
}
package pangea.hiagent.web.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import pangea.hiagent.model.UserToken;
import pangea.hiagent.web.repository.UserTokenRepository;
@Service
public class UserTokenService {
@Autowired
private UserTokenRepository userTokenRepository;
public UserToken getUserToken(String userId,String tokenType) {
return userTokenRepository.getTokenByUserIdAndTokenType(userId,tokenType);
}
}
spring:
application:
name: hiagent
# 数据源配置
datasource:
url: jdbc:mysql://${DB_HOST:127.0.0.1}:3306/hiagent2?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?}
hikari:
maximum-pool-size: 10
minimum-idle: 2
connection-timeout: 30000
# 禁用Milvus自动配置
autoconfigure:
exclude:
- org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration
# SQL初始化配置
sql:
init:
schema-locations: classpath:schema.sql
mode: never
# mode: always
# JPA/Hibernate配置
jpa:
database-platform: org.hibernate.dialect.MySQL8Dialect
# hibernate:
# ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true
# H2 Console配置(仅开发环境)
# h2:
# console:
# enabled: true
# path: /h2-console
# Redis配置
data:
redis:
host: localhost
port: 6379
password:
timeout: 2000
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1
# RabbitMQ配置
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
connection-timeout: 15000
# Jackson配置
jackson:
serialization:
write-dates-as-timestamps: false
deserialization:
fail-on-unknown-properties: false
default-property-inclusion: non_null
# Web配置
web:
resources:
add-mappings: true
# servlet配置
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
# 默认性异步请求配置
mvc:
async:
request-timeout: 300000 # 5分钟,与SSE保持一致
# Spring AI配置
ai:
openai:
enabled: false
ollama:
enabled: false
# MyBatis Plus配置
mybatis-plus:
type-aliases-package: pangea.hiagent.model
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
cache-enabled: true
use-generated-keys: true
global-config:
db-config:
id-type: assign_uuid
table-underline: true
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# Logging配置
logging:
level:
root: INFO
pangea.hiagent: DEBUG
org.springframework: INFO
org.springframework.security: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/hiagent.log
max-size: 10MB
max-history: 30
charset:
console: UTF-8
file: UTF-8
# Server配置
server:
port: 8080
servlet:
context-path: /
compression:
enabled: true
min-response-size: 1024
# SSE和异步请求超时配置
request-timeout: 300000 # 5分钟(毫秒)
# Undertow配置
undertow:
# IO线程数,默认为处理器数量
io-threads: 4
# 工作线程数
worker-threads: 50
# 缓冲区配置
buffer-size: 65536
# 是否直接分配缓冲区
direct-buffers: true
# HTTP/2支持
enable-http2: true
# 最大HTTP头大小
max-http-header-size: 8192
# 最大参数数量
max-parameters: 1000
# 最大请求头数量
max-headers: 200
# 最大cookies数量
max-cookies: 100
# URL编码字符集
url-charset: UTF-8
# 访问日志配置
accesslog:
enabled: false
pattern: common
dir: logs
prefix: access_log.
# SSL配置
ssl:
# SSL引擎
engine:
# 密码套件
enabled-protocols: TLSv1.2,TLSv1.3
# WebSocket配置
websocket:
# WebSocket消息缓冲区大小
buffer-size: 1048576
# 最大WebSocket帧大小
max-frame-size: 10485760
# 应用自定义配置
hiagent:
# JWT配置
jwt:
secret: ${JWT_SECRET:hiagent-secret-key-for-production-change-this}
expiration: 7200000 # 2小时
refresh-expiration: 604800000 # 7天
# Agent配置
agent:
default-model: deepseek-chat
default-temperature: 0.7
default-max-tokens: 4096
history-length: 10
# LLM配置
llm:
providers:
deepseek:
default-api-key: ${DEEPSEEK_API_KEY:sk-e8ef4359d20b413696512db21c09db87}
default-model: deepseek-chat
base-url: https://api.deepseek.com
openai:
default-api-key: ${OPENAI_API_KEY:}
default-model: gpt-3.5-turbo
base-url: https://api.openai.com/v1
ollama:
default-model: llama2
base-url: http://localhost:11434
# RAG配置
rag:
chunk-size: 512
chunk-overlap: 50
top-k: 5
score-threshold: 0.8
# Milvus Lite配置
milvus:
data-dir: ./milvus_data
db-name: hiagent
collection-name: document_embeddings
# ChatMemory配置
app:
chat-memory:
# 实现类型: caffeine, redis, hybrid
implementation: caffeine
caffeine:
# 是否启用Caffeine缓存
enabled: true
redis:
# 是否启用Redis缓存
enabled: false
\ No newline at end of file
...@@ -4,24 +4,12 @@ spring: ...@@ -4,24 +4,12 @@ spring:
# 配置文件激活 # 配置文件激活
profiles: profiles:
active: dev active: test
# 启用懒加载初始化 # 启用懒加载初始化
main: main:
lazy-initialization: true lazy-initialization: true
# 数据源配置
datasource:
url: jdbc:h2:mem:hiagent;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driver-class-name: org.h2.Driver
username: sa
password: sa
hikari:
maximum-pool-size: 10
minimum-idle: 1
connection-timeout: 20000
initialization-fail-timeout: 0
# 禁用Milvus自动配置 # 禁用Milvus自动配置
autoconfigure: autoconfigure:
exclude: exclude:
...@@ -32,22 +20,6 @@ spring: ...@@ -32,22 +20,6 @@ spring:
init: init:
mode: never # 生产环境禁用SQL脚本自动初始化 mode: never # 生产环境禁用SQL脚本自动初始化
# JPA/Hibernate配置
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: create # 生产环境仅验证表结构,不修改数据库
show-sql: false
properties:
hibernate:
format_sql: true
# H2 Console配置(仅开发环境)
h2:
console:
enabled: true
path: /h2-console
# Redis配置 # Redis配置
data: data:
redis: redis:
......
...@@ -15,7 +15,6 @@ ...@@ -15,7 +15,6 @@
"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", "pangea-ui": "1.0.1-beta.2",
......
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
"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", "pangea-ui": "1.0.1-beta.2",
......
...@@ -22,7 +22,6 @@ const formStore = useFormStore(); ...@@ -22,7 +22,6 @@ const formStore = useFormStore();
// 表单组件ref // 表单组件ref
const templateRef = ref(); const templateRef = ref();
// 表单提交回调
const submit = () => { const submit = () => {
templateRef.value?.ctx.validate(1, (isValid: boolean, formData: any) => { templateRef.value?.ctx.validate(1, (isValid: boolean, formData: any) => {
if (isValid) { if (isValid) {
......
...@@ -30,14 +30,14 @@ export default defineConfig({ ...@@ -30,14 +30,14 @@ export default defineConfig({
}, },
proxy: { proxy: {
"/api": { "/api": {
// target: "http://localhost:8080", target: "http://localhost:8080",
target: "http://agent-backend.clouddev.hisense.com", // target: "http://agent-backend.clouddev.hisense.com",
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, "/api"), rewrite: (path) => path.replace(/^\/api/, "/api"),
}, },
"/ws": { "/ws": {
// target: "http://localhost:8080", target: "http://localhost:8080",
target: "http://agent-backend.clouddev.hisense.com", // target: "http://agent-backend.clouddev.hisense.com",
ws: true, // 启用WebSocket代理 ws: true, // 启用WebSocket代理
changeOrigin: true, changeOrigin: true,
}, },
......
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