Commit 3a5e9d63 authored by ligaowei's avatar ligaowei

添加所有剩余的文件和配置

parent a86a826d
# Created by https://www.toptal.com/developers/gitignore/api/java,maven,node,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=java,maven,node,visualstudiocode
### Java ###
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
### Maven ###
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# https://github.com/takari/maven-wrapper
.mvn/wrapper/maven-wrapper.jar
# Eclipse m2e generated files
# Eclipse Core
.project
.metadata
.classpath
.settings/
.loadpath
.checkstyle
.springBeans
.factorypath
# IntelliJ IDEA
.idea/
*.iws
*.iml
*.ipr
out/
# NetBeans
nbproject/private/
build/
nbbuild/
dist/
nbdist/
.lastopened
# VS Code
.vscode/
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release/
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist/
# Gatsby files
.percel
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
### Project Specific ###
# Backend files
backend/target/
backend/logs/
backend/storage/
backend/uploads/
backend/hiagentdb.mv.db
# Frontend files
frontend/node_modules/
frontend/dist/
frontend/.env.local
frontend/.env.development.local
frontend/.env.test.local
frontend/.env.production.local
# OS generated files
Thumbs.db
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Icon?
*.icon?
\ No newline at end of file
# HiAgent 问题修复指南
## 问题分析
从终端日志中发现两个主要问题:
1. **LLM配置验证失败**`java.lang.IllegalArgumentException: LLM配置验证失败: deepseek`
2. **Spring Security访问被拒绝**`AuthorizationDeniedException: Access Denied`
## 根本原因
### LLM配置问题
[data.sql](file:///c:/Users/Gavin/Documents/PangeaFinal/HiAgent/backend/src/main/resources/data.sql)中的deepseek配置API密钥为空,而[DeepSeekModelAdapter.java](file:///c:/Users/Gavin/Documents/PangeaFinal/HiAgent/backend/src/main/java/pangea/hiagent/llm/DeepSeekModelAdapter.java)的验证逻辑要求必须有非空的API密钥。
### 安全配置问题
缺少必要的环境变量,包括`DEEPSEEK_API_KEY``JWT_SECRET`
## 解决方案
### 方案一:使用环境变量(推荐)
1. 编辑[run-with-env.bat](file:///c:/Users/Gavin/Documents/PangeaFinal/HiAgent/run-with-env.bat)文件,将占位符替换为实际值:
```batch
set DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 替换为你的DeepSeek API密钥
set JWT_SECRET=your-secure-jwt-secret-key # 替换为你自己的JWT密钥
```
2. 运行[run-with-env.bat](file:///c:/Users/Gavin/Documents/PangeaFinal/HiAgent/run-with-env.bat)启动应用:
```bash
run-with-env.bat
```
### 方案二:临时修复(仅用于测试)
如果你只是想快速测试应用而不关心安全性,可以:
1. 修改[LlmConfigService.java](file:///c:/Users/Gavin/Documents/PangeaFinal/HiAgent/backend/src/main/java/pangea/hiagent/service/LlmConfigService.java)中的验证逻辑,允许空API密钥:
```java
// 在DeepSeekModelAdapter.java中修改validateConfig方法
@Override
public boolean validateConfig(LlmConfig config) {
return config != null &&
config.getEnabled();
// 移除了对API密钥非空的检查
}
```
注意:这种方法仅适用于测试环境,生产环境中必须配置有效的API密钥。
## 登录凭证
默认登录账户:
- 用户名:`admin`
- 密码:`admin123` (如果使用的是开发环境默认密码)
## 验证修复
启动应用后,可以通过以下方式验证修复是否成功:
1. 访问 http://localhost:8080 并使用默认账户登录
2. 进入Agent管理页面,确认Agent可以正常加载
3. 尝试与Agent进行对话,确认不再出现"LLM配置验证失败"错误
## 故障排除
如果仍然遇到问题,请检查:
1. 确认环境变量已正确设置
2. 确认数据库已正确初始化
3. 查看应用启动日志中是否有其他错误信息
4. 确认网络连接正常,可以访问DeepSeek API
\ No newline at end of file
# HiAgent - 智能AI助手
HiAgent 是一个功能强大的个人AI助手,集成了多种工具和服务,能够帮助用户完成各种任务。
## 🌟 核心功能
### 网页访问和内容提取
- **网页访问工具**:能够根据网站名称或URL访问网页并在工作面板中预览
- **网页内容提取工具**:智能提取网页正文内容,自动识别并提取文章标题和主要内容,过滤掉广告、导航栏等无关内容
- **增强型网页嵌入预览**:支持多种加载策略(直接HTML、直接获取内容、iframe),自动处理X-Frame-Options等安全限制
### 计算和数据处理
- **计算器工具**:执行基本数学运算和复杂数学计算
- **日期时间工具**:获取当前时间、日期计算等
- **文件处理工具**:文件上传、下载和处理
- **字符串处理工具**:文本处理和转换功能
### 其他实用工具
- **天气查询工具**:获取指定城市的天气信息
- **OAuth2.0授权工具**:支持通过用户名和密码凭证获取网页资源访问授权,实现标准OAuth2.0认证流程
## 🛠 技术架构
### 后端技术栈
- **Spring Boot 3.3.4**:基于Java 17的现代化Web框架
- **Spring AI**:集成多种AI模型和服务
- **MySQL/H2**:数据存储
- **Redis**:缓存和会话管理
- **Milvus**:向量数据库支持
- **RabbitMQ**:消息队列服务
### 前端技术栈
- **Vue 3**:现代化的前端框架
- **TypeScript**:类型安全的JavaScript超集
- **Vite**:快速的构建工具
## 📦 主要工具介绍
### 网页内容提取工具 (WebContentExtractorTool)
这是一个专门用于从网页中提取有意义文本内容的工具。它能够自动识别并提取网页的标题和正文内容,同时过滤掉广告、导航栏等无关内容。
#### 功能特点
1. **智能内容提取**:自动识别网页的主要内容区域
2. **广告过滤**:自动过滤广告、导航栏等无关内容
3. **格式保留**:保留原文的标题层级和段落结构
4. **错误处理**:完善的错误处理机制和日志记录
#### 使用方法
在Agent对话中直接调用:
```
extractWebContent("https://example.com/article")
```
### 网页访问工具 (WebPageAccessTools)
提供根据网站名称或URL地址访问网页并在工作面板中预览的功能。
#### 功能特点
1. **多种访问方式**:支持按网站名称或直接URL访问
2. **内置网站映射**:支持常见网站的快捷访问
3. **工作面板集成**:直接在工作面板中预览网页内容
4. **多种加载策略**:支持HTML内容、直接获取内容和iframe三种加载方式
5. **智能错误处理**:自动处理X-Frame-Options等安全限制,提供友好的错误提示
#### 使用方法
```
accessWebSiteByName("百度")
accessWebSiteByUrl("https://www.example.com")
```
### 增强型网页嵌入预览 (EmbedPreview)
提供增强的网页嵌入预览功能,支持多种加载策略以应对不同的安全限制。
#### 功能特点
1. **多种加载策略**
- **HTML内容**:直接渲染后端提供的HTML内容
- **直接获取**:通过代理API获取网页内容并直接渲染(绕过X-Frame-Options限制)
- **iframe加载**:传统的iframe嵌入方式(备选方案)
2. **智能回退机制**:当一种策略失败时自动尝试其他策略
3. **安全处理**:使用DOMPurify清理内容,防止XSS攻击
4. **错误处理**:完善的错误处理和用户友好的错误提示
5. **响应式设计**:适配不同屏幕尺寸
#### 使用方法
```vue
<EmbedPreview
:html-content="htmlContent"
:embed-url="url"
embed-title="预览标题"
embed-type="网页"
/>
```
### OAuth2.0授权工具 (OAuth2AuthorizationTool)
这是一个支持OAuth2.0标准认证流程的工具,允许用户通过用户名和密码凭证获取访问受保护资源的令牌。
#### 功能特点
1. **标准OAuth2.0支持**:完全符合OAuth2.0 RFC标准
2. **密码凭证流**:支持Resource Owner Password Credentials Grant流程
3. **令牌管理**:自动管理和缓存访问令牌
4. **令牌刷新**:支持使用刷新令牌获取新的访问令牌
5. **安全存储**:令牌安全存储,自动处理过期令牌
6. **资源访问**:使用获取的令牌访问受保护的资源
#### 使用方法
```
// 1. 获取访问令牌
authorizeWithPasswordCredentials(
"https://example.com/oauth/token",
"your-client-id",
"your-client-secret",
"your-username",
"your-password",
"read write"
)
// 2. 刷新访问令牌
refreshToken(
"https://example.com/oauth/token",
"your-client-id",
"your-client-secret",
"your-refresh-token"
)
// 3. 访问受保护资源
accessProtectedResource(
"https://example.com/api/protected",
"https://example.com/oauth/token",
"your-client-id"
)
```
有关更详细的使用说明,请参阅 [OAuth2.0工具使用指南](OAUTH2_TOOL_USAGE_GUIDE.md)
## 🚀 快速开始
### 环境要求
- Java 17+
- Node.js 16+
- Maven 3.8+
- MySQL 8.0+ (可选,也可使用内置H2数据库)
### 后端启动
```bash
cd backend
mvn spring-boot:run
```
### 前端启动
```bash
cd frontend
npm install
npm run dev
```
### 一键启动脚本
- Windows: `run-all-debug.bat`
- 后端独立: `run-backend-debug.bat`
- 前端独立: `run-frontend-debug.bat`
## 📁 项目结构
```
HiAgent/
├── backend/ # 后端服务
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/ # Java源代码
│ │ │ └── resources/ # 配置文件和静态资源
│ │ └── test/ # 测试代码
│ └── pom.xml # Maven配置文件
├── frontend/ # 前端应用
│ ├── src/ # Vue源代码
│ ├── public/ # 静态资源
│ └── package.json # NPM配置文件
├── docker-compose.yml # Docker编排文件
└── README.md # 项目说明文件
```
## 🔧 配置说明
### 数据库配置
项目支持MySQL和H2数据库,默认使用H2内存数据库,可通过修改`application.yml`配置切换。
### AI模型配置
支持多种AI模型:
- OpenAI/DeepSeek
- Ollama本地模型
- 其他兼容OpenAI API的模型
## 🧪 测试
### 后端测试
```bash
cd backend
mvn test
```
### 前端测试
```bash
cd frontend
npm run test
```
## 🐳 Docker部署
使用docker-compose一键部署:
```bash
docker-compose up -d
```
## 📚 文档
项目包含丰富的技术文档:
- 工具使用说明
- 架构设计文档
- 部署指南
- 故障排查手册
## 🤝 贡献
欢迎提交Issue和Pull Request来改进项目。
## 📄 许可证
本项目采用MIT许可证。
## 🙏 致谢
感谢所有开源项目的贡献者们,以及支持这个项目开发的用户。
\ No newline at end of file
# 后端Dockerfile
FROM eclipse-temurin:17-jdk-focal AS builder
WORKDIR /build
COPY backend/pom.xml .
COPY backend/src ./src
RUN apt-get update && apt-get install -y maven && \
mvn clean package -DskipTests
FROM eclipse-temurin:17-jre-focal
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>pangea</groupId>
<artifactId>hiagent</artifactId>
<version>1.0.0</version>
<name>HiAgent</name>
<description>我的个人助理AI Agent</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.8</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-ai.version>1.0.0-M6</spring-ai.version>
<mybatis-plus.version>3.5.15</mybatis-plus.version>
<redis.version>8.2.1</redis.version>
<milvus-lite.version>2.3.0</milvus-lite.version>
<jjwt.version>0.12.6</jjwt.version>
<caffeine.version>3.1.8</caffeine.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- Repositories for Spring AI milestone versions -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<!-- Spring Boot Web with Undertow -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Undertow -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<!-- Spring Boot WebSocket with Undertow -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring AI Core -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
</dependency>
<!-- Spring AI OpenAI (包含DeepSeek via OpenAI compatible API) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
<!-- Spring AI Ollama -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama</artifactId>
</dependency>
<!-- Spring AI Vector Store -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-milvus-store</artifactId>
<version>${spring-ai.version}</version>
</dependency>
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JJWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lettuce (Redis client) -->
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</dependency>
<!-- Milvus Lite -->
<dependency>
<groupId>io.milvus</groupId>
<artifactId>milvus-sdk-java</artifactId>
<version>${milvus-lite.version}</version>
</dependency>
<!-- RabbitMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- Caffeine Cache -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>${caffeine.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- JSON-P -->
<dependency>
<groupId>javax.json</groupId>
<artifactId>javax.json-api</artifactId>
<version>1.1.4</version>
</dependency>
<!-- Apache Commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- Tika for document processing -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.9.1</version>
</dependency>
<!-- SpringDoc OpenAPI for Swagger -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Junit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Jsoup for HTML parsing and content extraction -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.17.2</version>
</dependency>
<!-- Apache HttpClient for web requests -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<!-- Playwright for web automation -->
<dependency>
<groupId>com.microsoft.playwright</groupId>
<artifactId>playwright</artifactId>
<version>1.46.0</version>
</dependency>
<!-- FastJSON2 for JSON processing -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.48</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- Maven Surefire Plugin for Tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0</version>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
package pangea.hiagent;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration;
import pangea.hiagent.config.JwtProperties;
/**
* HiAgent 应用启动类
* 我的AI智能体助理,采用轻量化设计原则与多Agent协同工作模式
*/
@SpringBootApplication(exclude = {OpenAiAutoConfiguration.class, OllamaAutoConfiguration.class})
@EnableCaching
@EnableConfigurationProperties({JwtProperties.class})
@ComponentScan(
basePackages = "pangea.hiagent",
excludeFilters = {
@ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@ComponentScan.Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class)
}
)
public class HiAgentApplication {
public static void main(String[] args) {
SpringApplication.run(HiAgentApplication.class, args);
}
}
\ No newline at end of file
package pangea.hiagent.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import pangea.hiagent.workpanel.IWorkPanelDataCollector;
import java.util.HashMap;
import java.util.Map;
/**
* 工具执行日志记录切面类
* 自动记录带有@Tool注解的方法执行信息,包括方案名称、输入参数、输出结果、运行时长等
*/
@Slf4j
@Aspect
@Component
public class ToolExecutionLoggerAspect {
@Autowired
private IWorkPanelDataCollector workPanelDataCollector;
/**
* 环绕通知,拦截所有带有@Tool注解的方法
* @param joinPoint 连接点
* @return 方法执行结果
* @throws Throwable 异常
*/
@Around("@annotation(tool)")
public Object logToolExecution(ProceedingJoinPoint joinPoint, Tool tool) throws Throwable {
// 获取方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String methodName = signature.getName();
String className = signature.getDeclaringType().getSimpleName();
String fullMethodName = className + "." + methodName;
// 获取工具描述
String toolDescription = tool.description();
// 获取方法参数
String[] paramNames = signature.getParameterNames();
Object[] args = joinPoint.getArgs();
// 构建输入参数映射
Map<String, Object> inputParams = new HashMap<>();
if (paramNames != null && args != null) {
for (int i = 0; i < paramNames.length; i++) {
if (i < args.length) {
inputParams.put(paramNames[i], args[i]);
}
}
}
// 记录开始时间
long startTime = System.currentTimeMillis();
// 记录工具调用开始
if (workPanelDataCollector != null) {
try {
workPanelDataCollector.recordToolCallStart(className, methodName, inputParams);
} catch (Exception e) {
log.warn("记录工具调用开始时发生错误: {}", e.getMessage());
}
}
log.debug("开始执行工具方法: {},描述: {},参数: {}", fullMethodName, toolDescription, inputParams);
try {
// 执行原方法
Object result = joinPoint.proceed();
// 记录结束时间
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
// 记录工具调用完成
if (workPanelDataCollector != null) {
try {
workPanelDataCollector.recordToolCallComplete(className, result, "success", executionTime);
} catch (Exception e) {
log.warn("记录工具调用完成时发生错误: {}", e.getMessage());
}
}
log.debug("工具方法执行成功: {},描述: {},耗时: {}ms,结果类型: {}", fullMethodName, toolDescription, executionTime,
result != null ? result.getClass().getSimpleName() : "null");
return result;
} catch (Exception e) {
// 记录结束时间
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;
// 记录工具调用错误
if (workPanelDataCollector != null) {
try {
workPanelDataCollector.recordToolCallComplete(className, e.getMessage(), "error", executionTime);
} catch (Exception ex) {
log.warn("记录工具调用错误时发生错误: {}", ex.getMessage());
}
}
log.error("工具方法执行失败: {},描述: {},耗时: {}ms,错误类型: {},错误信息: {}",
fullMethodName, toolDescription, executionTime, e.getClass().getSimpleName(), e.getMessage(), e);
throw e;
}
}
}
\ No newline at end of file
package pangea.hiagent.auth;
import java.util.Map;
/**
* 认证策略接口
* 定义统一的认证流程接口,支持多种认证方式的实现
*/
public interface AuthenticationStrategy {
/**
* 获取认证策略的名称
*/
String getName();
/**
* 判断是否支持该认证模式
*/
boolean supports(String authMode);
/**
* 执行认证逻辑
* @param credentials 认证凭证(根据不同认证方式含义不同)
* @return 认证成功返回用户 ID,失败抛出异常
*/
String authenticate(Map<String, Object> credentials);
/**
* 刷新令牌(如果需要)
* @param refreshToken 刷新令牌
* @return 新的访问令牌
*/
default String refreshToken(String refreshToken) {
throw new UnsupportedOperationException("该认证策略不支持令牌刷新");
}
/**
* 验证认证结果
* @param token 认证令牌或其他认证证明
* @return true 表示验证成功,false 表示验证失败
*/
boolean verify(String token);
}
package pangea.hiagent.auth;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import pangea.hiagent.model.AuthMode;
import pangea.hiagent.model.User;
import pangea.hiagent.repository.UserRepository;
import pangea.hiagent.utils.JwtUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import java.util.Arrays;
import java.util.Map;
/**
* 本地用户名/密码认证策略实现
* 支持基于本地数据库的用户名密码认证
*/
@Slf4j
@Component
public class LocalAuthenticationStrategy implements AuthenticationStrategy {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
private final org.springframework.core.env.Environment environment;
public LocalAuthenticationStrategy(UserRepository userRepository, PasswordEncoder passwordEncoder,
JwtUtil jwtUtil, org.springframework.core.env.Environment environment) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtUtil = jwtUtil;
this.environment = environment;
}
@Override
public String getName() {
return "Local Authentication Strategy";
}
@Override
public boolean supports(String authMode) {
return AuthMode.LOCAL.getCode().equals(authMode);
}
/**
* 执行本地用户认证
* @param credentials 需要包含 username 和 password 字段
* @return JWT Token
*/
@Override
public String authenticate(Map<String, Object> credentials) {
String username = (String) credentials.get("username");
String password = (String) credentials.get("password");
if (username == null || username.trim().isEmpty()) {
log.warn("本地认证失败: 用户名为空");
throw new RuntimeException("用户名不能为空");
}
if (password == null || password.trim().isEmpty()) {
log.warn("本地认证失败: 密码为空");
throw new RuntimeException("密码不能为空");
}
log.info("执行本地认证: {}", username);
// 查询用户
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, username);
User user = userRepository.selectOne(wrapper);
if (user == null) {
log.warn("本地认证失败: 用户 {} 不存在", username);
throw new RuntimeException("用户不存在");
}
// 检查用户状态
if (!"active".equals(user.getStatus())) {
log.warn("本地认证失败: 用户 {} 已被禁用", username);
throw new RuntimeException("用户已禁用");
}
// 检查是否为开发环境,如果是则允许任意密码
boolean isDevEnvironment = Arrays.asList(environment.getActiveProfiles()).contains("dev") ||
Arrays.asList(environment.getDefaultProfiles()).contains("default");
if (!isDevEnvironment) {
// 验证密码
boolean passwordMatch = passwordEncoder.matches(password, user.getPassword());
if (!passwordMatch) {
log.warn("本地认证失败: 用户 {} 密码错误", username);
throw new RuntimeException("密码错误");
}
} else {
log.info("开发环境: 跳过密码验证");
}
// 更新最后登录时间
user.setLastLoginTime(System.currentTimeMillis());
userRepository.updateById(user);
// 生成 Token
String token = jwtUtil.generateToken(user.getId());
log.info("本地认证成功,用户: {}, 生成Token: {}", username, token);
return token;
}
@Override
public boolean verify(String token) {
return jwtUtil.validateToken(token);
}
}
package pangea.hiagent.auth;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import pangea.hiagent.model.AuthMode;
import pangea.hiagent.model.OAuth2Account;
import pangea.hiagent.model.OAuth2Provider;
import pangea.hiagent.model.User;
import pangea.hiagent.repository.UserRepository;
import pangea.hiagent.repository.OAuth2AccountRepository;
import pangea.hiagent.repository.OAuth2ProviderRepository;
import pangea.hiagent.utils.JwtUtil;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* OAuth2.0 授权码模式认证策略实现
* 支持标准 OAuth2.0 授权码流程(Authorization Code Grant)
*/
@Slf4j
@Component
public class OAuth2AuthenticationStrategy implements AuthenticationStrategy {
private final RestTemplate restTemplate;
private final ObjectMapper objectMapper;
private final UserRepository userRepository;
private final OAuth2AccountRepository oauth2AccountRepository;
private final OAuth2ProviderRepository oauth2ProviderRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
public OAuth2AuthenticationStrategy(RestTemplate restTemplate, ObjectMapper objectMapper,
UserRepository userRepository,
OAuth2AccountRepository oauth2AccountRepository,
OAuth2ProviderRepository oauth2ProviderRepository,
PasswordEncoder passwordEncoder, JwtUtil jwtUtil) {
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
this.userRepository = userRepository;
this.oauth2AccountRepository = oauth2AccountRepository;
this.oauth2ProviderRepository = oauth2ProviderRepository;
this.passwordEncoder = passwordEncoder;
this.jwtUtil = jwtUtil;
}
@Override
public String getName() {
return "OAuth2 Authorization Code Strategy";
}
@Override
public boolean supports(String authMode) {
return AuthMode.OAUTH2_AUTHORIZATION_CODE.getCode().equals(authMode);
}
/**
* 执行 OAuth2 认证
* @param credentials 包含以下字段:
* - authorizationCode: OAuth2 授权码
* - providerName: OAuth2 提供者名称
* - state: 防CSRF令牌(可选)
* @return JWT Token
*/
@Override
public String authenticate(Map<String, Object> credentials) {
String authorizationCode = (String) credentials.get("authorizationCode");
String providerName = (String) credentials.get("providerName");
String state = (String) credentials.get("state");
if (authorizationCode == null || authorizationCode.trim().isEmpty()) {
log.warn("OAuth2 认证失败: 授权码为空");
throw new RuntimeException("授权码不能为空");
}
if (providerName == null || providerName.trim().isEmpty()) {
log.warn("OAuth2 认证失败: 提供者名称为空");
throw new RuntimeException("提供者名称不能为空");
}
log.info("执行 OAuth2 认证: providerName={}, authorizationCode={}", providerName, authorizationCode);
try {
// 获取 OAuth2 提供者配置
LambdaQueryWrapper<OAuth2Provider> providerWrapper = new LambdaQueryWrapper<>();
providerWrapper.eq(OAuth2Provider::getProviderName, providerName)
.eq(OAuth2Provider::getEnabled, 1);
OAuth2Provider provider = oauth2ProviderRepository.selectOne(providerWrapper);
if (provider == null) {
log.error("OAuth2 认证失败: 未找到配置的提供者 {}", providerName);
throw new RuntimeException("未找到配置的 OAuth2 提供者");
}
// 使用授权码交换访问令牌
String accessToken = exchangeCodeForToken(provider, authorizationCode);
// 获取用户信息
Map<String, Object> userInfo = fetchUserInfo(provider, accessToken);
// 获取或创建用户
User user = findOrCreateUser(provider, userInfo);
// 保存或更新 OAuth2 账户关联
saveOAuth2Account(user, provider, userInfo, accessToken);
// 生成 JWT Token
String token = jwtUtil.generateToken(user.getId());
log.info("OAuth2 认证成功,用户: {}, 提供者: {}, 生成Token: {}", user.getUsername(), providerName, token);
return token;
} catch (Exception e) {
log.error("OAuth2 认证过程中出错: providerName={}, authorizationCode={}, 错误堆栈: ", providerName, authorizationCode, e);
throw new RuntimeException("OAuth2 认证失败: " + e.getMessage(), e);
}
}
/**
* 使用授权码交换访问令牌
*/
private String exchangeCodeForToken(OAuth2Provider provider, String authorizationCode) {
try {
log.debug("开始令牌交换: 提供者={}, tokenUrl={}", provider.getProviderName(), provider.getTokenUrl());
Map<String, String> body = new HashMap<>();
body.put("grant_type", "authorization_code");
body.put("code", authorizationCode);
body.put("client_id", provider.getClientId());
body.put("client_secret", provider.getClientSecret());
body.put("redirect_uri", provider.getRedirectUri());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// 构造表单数据
String formBody = body.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.reduce((a, b) -> a + "&" + b)
.orElse("");
HttpEntity<String> request = new HttpEntity<>(formBody, headers);
ResponseEntity<String> response = restTemplate.postForEntity(provider.getTokenUrl(), request, String.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
JsonNode jsonNode = objectMapper.readTree(response.getBody());
String accessToken = jsonNode.get("access_token").asText();
log.debug("令牌交换成功: 提供者={}, accessToken={}", provider.getProviderName(),
accessToken.substring(0, Math.min(20, accessToken.length())) + "...");
return accessToken;
} else {
log.error("令牌交换失败: 提供者={}, statusCode={}, responseBody={}",
provider.getProviderName(), response.getStatusCode(), response.getBody());
throw new RuntimeException("令牌交换失败");
}
} catch (IOException e) {
log.error("令牌交换过程中出错: 提供者={}, 错误堆栈: ", provider.getProviderName(), e);
throw new RuntimeException("令牌交换异常: " + e.getMessage(), e);
}
}
/**
* 从 OAuth2 提供者获取用户信息
*/
private Map<String, Object> fetchUserInfo(OAuth2Provider provider, String accessToken) {
try {
log.debug("开始获取用户信息: 提供者={}, userinfoUrl={}", provider.getProviderName(), provider.getUserinfoUrl());
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
headers.set("Accept", MediaType.APPLICATION_JSON_VALUE);
HttpEntity<String> request = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.postForEntity(provider.getUserinfoUrl(), request, String.class);
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
Map<String, Object> userInfo = objectMapper.readValue(response.getBody(), Map.class);
log.debug("获取用户信息成功: 提供者={}", provider.getProviderName());
return userInfo;
} else {
log.error("获取用户信息失败: 提供者={}, statusCode={}", provider.getProviderName(), response.getStatusCode());
throw new RuntimeException("获取用户信息失败");
}
} catch (IOException e) {
log.error("获取用户信息过程中出错: 提供者={}, 错误堆栈: ", provider.getProviderName(), e);
throw new RuntimeException("获取用户信息异常: " + e.getMessage(), e);
}
}
/**
* 查找或创建用户
*/
private User findOrCreateUser(OAuth2Provider provider, Map<String, Object> userInfo) {
String remoteUserId = extractRemoteUserId(provider, userInfo);
String remoteEmail = (String) userInfo.get("email");
String remoteUsername = (String) userInfo.get("name");
// 查找现有的 OAuth2 账户
LambdaQueryWrapper<OAuth2Account> accountWrapper = new LambdaQueryWrapper<>();
accountWrapper.eq(OAuth2Account::getProviderName, provider.getProviderName())
.eq(OAuth2Account::getRemoteUserId, remoteUserId);
OAuth2Account existingAccount = oauth2AccountRepository.selectOne(accountWrapper);
if (existingAccount != null) {
// 返回已关联的用户
return userRepository.selectById(existingAccount.getUserId());
}
// 如果用户邮箱存在,尝试关联现有用户
if (remoteEmail != null && !remoteEmail.isEmpty()) {
LambdaQueryWrapper<User> userWrapper = new LambdaQueryWrapper<>();
userWrapper.eq(User::getEmail, remoteEmail);
User existingUser = userRepository.selectOne(userWrapper);
if (existingUser != null) {
return existingUser;
}
}
// 创建新用户
String newUsername = provider.getProviderName() + "_" + remoteUserId;
String randomPassword = generateRandomPassword();
User newUser = User.builder()
.username(newUsername)
.password(passwordEncoder.encode(randomPassword))
.email(remoteEmail)
.nickname(remoteUsername)
.status("active")
.role("user")
.build();
userRepository.insert(newUser);
log.info("创建新用户: username={}, 来自提供者: {}", newUsername, provider.getProviderName());
return newUser;
}
/**
* 保存或更新 OAuth2 账户关联
*/
private void saveOAuth2Account(User user, OAuth2Provider provider, Map<String, Object> userInfo, String accessToken) {
String remoteUserId = extractRemoteUserId(provider, userInfo);
LambdaQueryWrapper<OAuth2Account> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OAuth2Account::getUserId, user.getId())
.eq(OAuth2Account::getProviderName, provider.getProviderName());
OAuth2Account existingAccount = oauth2AccountRepository.selectOne(wrapper);
long now = System.currentTimeMillis();
try {
String profileData = objectMapper.writeValueAsString(userInfo);
if (existingAccount != null) {
// 更新现有账户
existingAccount.setAccessToken(accessToken);
existingAccount.setRemoteUsername((String) userInfo.get("name"));
existingAccount.setRemoteEmail((String) userInfo.get("email"));
existingAccount.setProfileData(profileData);
existingAccount.setLastLoginAt(now);
oauth2AccountRepository.updateById(existingAccount);
log.debug("更新 OAuth2 账户: userId={}, providerName={}", user.getId(), provider.getProviderName());
} else {
// 创建新账户关联
OAuth2Account newAccount = OAuth2Account.builder()
.userId(user.getId())
.providerName(provider.getProviderName())
.remoteUserId(remoteUserId)
.remoteUsername((String) userInfo.get("name"))
.remoteEmail((String) userInfo.get("email"))
.accessToken(accessToken)
.scope(provider.getScope())
.profileData(profileData)
.linkedAt(now)
.lastLoginAt(now)
.createdAt(now)
.updatedAt(now)
.deleted(0)
.build();
newAccount.setId(UUID.randomUUID().toString());
oauth2AccountRepository.insert(newAccount);
log.debug("创建新 OAuth2 账户关联: userId={}, providerName={}", user.getId(), provider.getProviderName());
}
} catch (IOException e) {
log.error("保存 OAuth2 账户时出错: userId={}, providerName={}, 错误堆栈: ",
user.getId(), provider.getProviderName(), e);
throw new RuntimeException("保存 OAuth2 账户失败: " + e.getMessage(), e);
}
}
/**
* 提取远程用户 ID
*/
private String extractRemoteUserId(OAuth2Provider provider, Map<String, Object> userInfo) {
// 默认使用 "id" 或 "sub" 字段,可根据具体提供者调整
Object id = userInfo.get("id");
if (id == null) {
id = userInfo.get("sub");
}
return id != null ? id.toString() : userInfo.get("email").toString();
}
/**
* 生成随机密码
*/
private String generateRandomPassword() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}
@Override
public boolean verify(String token) {
return jwtUtil.validateToken(token);
}
}
package pangea.hiagent.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 应用配置类
* 集中管理应用的核心配置
*/
@Data
@Component
@ConfigurationProperties(prefix = "hiagent")
public class AppConfig {
/**
* JWT配置
*/
private Jwt jwt = new Jwt();
/**
* Agent配置
*/
private Agent agent = new Agent();
/**
* LLM配置
*/
private Llm llm = new Llm();
/**
* RAG配置
*/
private Rag rag = new Rag();
/**
* Milvus配置
*/
private Milvus milvus = new Milvus();
/**
* JWT配置内部类
*/
@Data
public static class Jwt {
private String secret = "hiagent-secret-key-for-production-change-this";
private Long expiration = 7200000L; // 2小时
private Long refreshExpiration = 604800000L; // 7天
}
/**
* Agent配置内部类
*/
@Data
public static class Agent {
private String defaultModel = "deepseek-chat";
private Double defaultTemperature = 0.7;
private Integer defaultMaxTokens = 4096;
private Integer historyLength = 10;
}
/**
* LLM配置内部类
*/
@Data
public static class Llm {
private ProviderConfig deepseek = new ProviderConfig();
private ProviderConfig openai = new ProviderConfig();
private ProviderConfig ollama = new ProviderConfig();
@Data
public static class ProviderConfig {
private String defaultApiKey = "";
private String defaultModel = "";
private String baseUrl = "";
}
}
/**
* RAG配置内部类
*/
@Data
public static class Rag {
private Integer chunkSize = 512;
private Integer chunkOverlap = 50;
private Integer topK = 5;
private Double scoreThreshold = 0.8;
}
/**
* Milvus配置内部类
*/
@Data
public static class Milvus {
private String dataDir = "./milvus_data";
private String dbName = "hiagent";
private String collectionName = "document_embeddings";
}
}
\ No newline at end of file
package pangea.hiagent.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* 缓存配置类
* 配置Caffeine缓存管理器以支持工具调用缓存
*/
@Configuration
@EnableCaching
public class CacheConfig {
/**
* 配置Caffeine缓存管理器
*/
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
// 配置默认的Caffeine缓存
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000) // 最大缓存条目数
.expireAfterWrite(1, TimeUnit.HOURS) // 写入后1小时过期
.recordStats()); // 记录缓存统计信息
return cacheManager;
}
/**
* 为工具调用结果配置专用缓存
*/
@Bean
public com.github.benmanes.caffeine.cache.Cache<String, Object> toolResultCache() {
return Caffeine.newBuilder()
.maximumSize(10000) // 更大的缓存容量
.expireAfterWrite(30, TimeUnit.MINUTES) // 较短的过期时间
.recordStats()
.build();
}
/**
* 为汇率信息配置专用缓存
*/
@Bean("exchangeRates")
public com.github.benmanes.caffeine.cache.Cache<String, Object> exchangeRatesCache() {
return Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(1, TimeUnit.HOURS) // 汇率信息相对稳定,可以缓存更长时间
.recordStats()
.build();
}
/**
* 为天气信息配置专用缓存
*/
@Bean("weatherInfo")
public com.github.benmanes.caffeine.cache.Cache<String, Object> weatherInfoCache() {
return Caffeine.newBuilder()
.maximumSize(5000)
.expireAfterWrite(10, TimeUnit.MINUTES) // 天气信息变化较快,缓存时间较短
.recordStats()
.build();
}
}
\ No newline at end of file
package pangea.hiagent.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import pangea.hiagent.memory.CaffeineChatMemory;
import pangea.hiagent.memory.HybridChatMemory;
import pangea.hiagent.memory.RedisChatMemory;
import org.springframework.ai.chat.memory.ChatMemory;
/**
* ChatMemory配置类
* 配置Spring AI的ChatMemory用于管理对话历史
* 支持多种实现:Redis、Caffeine或混合模式
*/
@Configuration
public class ChatMemoryConfig {
// ChatMemory实现类型配置
@Value("${app.chat-memory.implementation:hybrid}")
private String implementation;
/**
* 配置ChatMemory Bean
* 根据配置选择不同的实现
*/
@Bean
public ChatMemory chatMemory(
CaffeineChatMemory caffeineChatMemory,
RedisChatMemory redisChatMemory,
HybridChatMemory hybridChatMemory) {
switch (implementation.toLowerCase()) {
case "caffeine":
return caffeineChatMemory;
case "redis":
return redisChatMemory;
case "hybrid":
default:
return hybridChatMemory;
}
}
}
\ No newline at end of file
package pangea.hiagent.config;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* ChatModel配置类
* 用于注册ChatModel和ChatClient.Builder bean,解决ChatClientAutoConfiguration找不到ChatModel的问题
*/
@Configuration
public class ChatModelConfig {
/**
* 注册ChatClient.Builder bean
* 这个bean主要用于解决ChatClientAutoConfiguration中chatClientBuilder方法的依赖问题
* 实际使用时我们会直接使用ChatModel创建ChatClient.Builder
*
* @return ChatClient.Builder实例
*/
@Bean
public ChatClient.Builder chatClientBuilder() {
// 返回一个默认的ChatClient.Builder,实际使用时会被替换
return ChatClient.builder(new DummyChatModel());
}
/**
* 虚拟ChatModel实现,仅用于满足Spring的依赖注入要求
* 实际使用时我们会通过AgentService.getChatModelForAgent()获取真实的ChatModel实例
*/
private static class DummyChatModel implements ChatModel {
@Override
public org.springframework.ai.chat.model.ChatResponse call(org.springframework.ai.chat.prompt.Prompt prompt) {
throw new UnsupportedOperationException("Dummy ChatModel should not be used directly");
}
}
}
\ No newline at end of file
package pangea.hiagent.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.util.UriComponentsBuilder;
import pangea.hiagent.websocket.DomSyncHandler;
import java.util.Map;
/**
* WebSocket配置类
*/
@Configuration
@EnableWebSocket
public class DomSyncWebSocketConfig implements WebSocketConfigurer {
// 注入DomSyncHandler,交由Spring管理生命周期
@Bean
public DomSyncHandler domSyncHandler() {
return new DomSyncHandler();
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(domSyncHandler(), "/ws/dom-sync")
// 添加握手拦截器用于JWT验证
.addInterceptors(new JwtHandshakeInterceptor())
// 生产环境:替换为具体域名,禁止使用*
.setAllowedOrigins("*");
}
/**
* JWT握手拦截器,用于WebSocket连接时的认证
*/
public static class JwtHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// 首先尝试从请求头中获取JWT Token
String token = request.getHeaders().getFirst("Authorization");
// 如果请求头中没有,则尝试从查询参数中获取
if (token == null) {
String query = request.getURI().getQuery();
if (query != null) {
UriComponentsBuilder builder = UriComponentsBuilder.newInstance().query(query);
token = builder.build().getQueryParams().getFirst("token");
}
}
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7); // 移除"Bearer "前缀
}
if (token != null && !token.isEmpty()) {
try {
// 简单检查token是否包含典型的JWT部分
String[] parts = token.split("\\.");
if (parts.length == 3) {
// 基本格式正确,接受连接
attributes.put("token", token);
// 使用token的一部分作为用户标识(实际应用中应该解析JWT获取用户ID)
attributes.put("userId", "user_" + token.substring(0, Math.min(8, token.length())));
System.out.println("WebSocket连接认证成功,Token: " + token);
return true;
}
} catch (Exception e) {
System.err.println("JWT验证过程中发生错误: " + e.getMessage());
e.printStackTrace();
}
}
// 如果没有有效的token,拒绝连接
System.err.println("WebSocket连接缺少有效的认证token");
response.setStatusCode(org.springframework.http.HttpStatus.UNAUTHORIZED);
return false;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
// 握手后处理,这里不需要特殊处理
}
}
}
\ No newline at end of file
package pangea.hiagent.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import lombok.Data;
/**
* JWT配置属性类
*/
@Data
@ConfigurationProperties(prefix = "hiagent.jwt")
public class JwtProperties {
/**
* JWT密钥
*/
private String secret = "hiagent-secret-key-for-production-change-this";
/**
* Token过期时间(毫秒)
*/
private Long expiration = 7200000L; // 2小时
/**
* 刷新Token过期时间(毫秒)
*/
private Long refreshExpiration = 604800000L; // 7天
}
\ No newline at end of file
package pangea.hiagent.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置类
* 配置RedisTemplate用于RedisChatMemory实现
*/
@Configuration
public class RedisConfig {
/**
* 配置RedisTemplate
* 使用StringRedisSerializer序列化key和value
*/
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 设置key和value的序列化器
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
// 设置hash key和hash value的序列化器
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
\ No newline at end of file
package pangea.hiagent.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import pangea.hiagent.security.DefaultPermissionEvaluator;
import pangea.hiagent.security.JwtAuthenticationFilter;
import java.util.Arrays;
import java.util.Collections;
@Slf4j
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final DefaultPermissionEvaluator customPermissionEvaluator;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, DefaultPermissionEvaluator customPermissionEvaluator) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.customPermissionEvaluator = customPermissionEvaluator;
}
/**
* 密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 方法安全表达式处理器
*/
@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(customPermissionEvaluator);
return expressionHandler;
}
/**
* CORS配置
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Collections.singletonList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setExposedHeaders(Arrays.asList("Authorization", "Content-Type"));
configuration.setMaxAge(3600L);
configuration.setAllowCredentials(false);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
/**
* Security过滤链配置
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF
.csrf(csrf -> csrf.disable())
// 启用CORS
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 设置session创建策略为无状态
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 配置请求授权
.authorizeHttpRequests(authz -> authz
// OAuth2 相关端点公开访问
.requestMatchers("/api/v1/auth/oauth2/**").permitAll()
// OAuth2提供商管理端点需要认证(仅管理员可访问)
.requestMatchers("/api/v1/auth/oauth2/providers/**").authenticated()
// 公开端点
.requestMatchers(
"/api/v1/auth/**",
"/swagger-ui.html",
"/swagger-ui/**",
"/v3/api-docs/**",
"/h2-console/**",
"/redoc.html",
"/error",
"/api/v1/proxy/**" // 将proxy接口设为公开访问
).permitAll()
// Agent相关端点 - 需要认证
.requestMatchers("/api/v1/agent/**").authenticated()
// 工具相关端点 - 需要认证
.requestMatchers("/api/v1/tools/**").authenticated()
// 所有其他请求需要认证
.anyRequest().authenticated()
)
// 异常处理
.exceptionHandling(exception -> exception
.authenticationEntryPoint((request, response, authException) -> {
// 检查响应是否已经提交
if (response.isCommitted()) {
System.err.println("响应已经提交,无法处理认证异常: " + request.getRequestURI());
return;
}
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"message\":\"未授权访问\",\"timestamp\":" + System.currentTimeMillis() + "}");
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(403);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":403,\"message\":\"访问被拒绝\",\"timestamp\":" + System.currentTimeMillis() + "}");
})
)
// 添加JWT认证过滤器
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
\ No newline at end of file
package pangea.hiagent.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* 工具执行日志记录配置类
* 启用AOP代理以实现工具执行信息的自动记录
*/
@Configuration
@EnableAspectJAutoProxy
public class ToolExecutionLoggingConfig {
}
\ No newline at end of file
package pangea.hiagent.config;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.openai.OpenAiEmbeddingModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/**
* VectorStore配置类
* 配置EmbeddingModel和VectorStore用于RAG功能
*/
@Configuration
public class VectorStoreConfig {
/**
* 配置OpenAI Embedding Model Bean
*/
@Bean
public EmbeddingModel embeddingModel() {
// 使用默认的OpenAI API密钥和模型
OpenAiApi openAiApi = new OpenAiApi.Builder().apiKey("fake-key").build(); // 使用假密钥避免错误
return new OpenAiEmbeddingModel(openAiApi);
}
/**
* 配置VectorStore Bean
* 当前版本简单返回null,后续可以添加具体的VectorStore实现
*/
@Bean
@Primary
public VectorStore vectorStore(EmbeddingModel embeddingModel) {
// 暂时返回null,避免启动错误,RAG功能将不可用
// 后续可以添加具体的VectorStore实现(如Milvus、PostgreSQL等)
return null;
}
}
\ No newline at end of file
package pangea.hiagent.config;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* Web相关配置类
* 配置Web相关的Bean,如RestTemplate等
*/
@Configuration
public class WebConfig {
/**
* 配置RestTemplate Bean
* 用于发送HTTP请求
* 增强配置:设置连接和读取超时
*/
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.connectTimeout(java.time.Duration.ofSeconds(10))
.readTimeout(java.time.Duration.ofSeconds(10))
.build();
}
}
\ No newline at end of file
package pangea.hiagent.document;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import pangea.hiagent.model.Agent;
import java.util.List;
import java.util.UUID;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
/**
* 文档管理服务
* 负责创建和管理各Agent的知识库
*/
@Slf4j
@Service
public class DocumentManagementService {
@Autowired(required = false)
private VectorStore vectorStore;
/**
* 为客服助手创建知识库
*/
public void createCustomerServiceKnowledgeBase() {
if (vectorStore == null) {
log.warn("VectorStore未配置,无法创建知识库");
return;
}
try {
String collectionId = "customer-service-kb";
String knowledgeBaseContent = """
客服助手知识库
1. 服务理念
我们致力于为客户提供友好、专业和高效的服务。始终以客户满意为目标,耐心解答客户问题。
2. 常见问题处理
- 产品咨询:详细介绍产品功能、规格和使用方法
- 售后服务:指导客户如何申请退换货、维修等服务
- 投诉处理:认真听取客户意见,及时反馈给相关部门
3. 服务流程
- 问候客户并询问需求
- 理解客户问题并提供准确解答
- 如需进一步处理,告知客户后续步骤和时间节点
- 结束对话前确认客户是否还有其他问题
""";
// 创建文档
Resource resource = new ByteArrayResource(knowledgeBaseContent.getBytes());
TextReader textReader = new TextReader(resource);
List<Document> documents = textReader.get();
// 分割文档
TokenTextSplitter textSplitter = new TokenTextSplitter();
List<Document> splitDocuments = textSplitter.apply(documents);
// 添加元数据
for (int i = 0; i < splitDocuments.size(); i++) {
Document originalDoc = splitDocuments.get(i);
Document newDoc = Document.builder()
.id(UUID.randomUUID().toString())
.text(originalDoc.getText())
.metadata("collectionId", collectionId)
.metadata("agent", "客服助手")
.build();
splitDocuments.set(i, newDoc);
}
// 存储到向量数据库
vectorStore.add(splitDocuments);
log.info("客服助手知识库创建完成,共{}个文档片段", splitDocuments.size());
} catch (Exception e) {
log.error("创建客服助手知识库失败", e);
}
}
/**
* 为技术支持创建知识库
*/
public void createTechnicalSupportKnowledgeBase() {
if (vectorStore == null) {
log.warn("VectorStore未配置,无法创建知识库");
return;
}
try {
String collectionId = "technical-support-kb";
String knowledgeBaseContent = """
技术支持知识库
1. 技术支持范围
- 软件安装和配置指导
- 硬件故障诊断和排除
- 网络连接问题解决
- 系统性能优化建议
- 技术文档检索和代码解释
2. 常见技术问题
- 启动失败:检查电源连接、硬件状态和系统日志
- 运行缓慢:分析资源占用情况,优化系统配置
- 功能异常:确认软件版本,检查配置文件
- 兼容性问题:核实系统环境,提供适配方案
- 代码理解困难:使用代码解释工具分析代码逻辑
3. 解决问题的方法论
- 信息收集:详细了解问题现象、环境信息和操作步骤
- 问题分析:根据现象判断可能的原因
- 解决方案:提供具体的操作步骤和注意事项
- 验证结果:确认问题是否得到解决
- 文档检索:使用技术文档检索工具查找相关文档
- 代码分析:使用代码解释工具理解复杂代码逻辑
4. 工具使用指南
- technicalDocumentationRetrieval: 用于检索技术文档,输入查询关键词即可
- technicalCodeExplanation: 用于解释代码功能,输入代码内容和编程语言
""";
// 创建文档
Resource resource = new ByteArrayResource(knowledgeBaseContent.getBytes());
TextReader textReader = new TextReader(resource);
List<Document> documents = textReader.get();
// 分割文档
TokenTextSplitter textSplitter = new TokenTextSplitter();
List<Document> splitDocuments = textSplitter.apply(documents);
// 添加元数据
for (int i = 0; i < splitDocuments.size(); i++) {
Document originalDoc = splitDocuments.get(i);
Document newDoc = Document.builder()
.id(UUID.randomUUID().toString())
.text(originalDoc.getText())
.metadata("collectionId", collectionId)
.metadata("agent", "技术支持")
.build();
splitDocuments.set(i, newDoc);
}
// 存储到向量数据库
vectorStore.add(splitDocuments);
log.info("技术支持知识库创建完成,共{}个文档片段", splitDocuments.size());
} catch (Exception e) {
log.error("创建技术支持知识库失败", e);
}
}
/**
* 为数据分析员创建知识库
*/
public void createDataAnalysisKnowledgeBase() {
if (vectorStore == null) {
log.warn("VectorStore未配置,无法创建知识库");
return;
}
try {
String collectionId = "data-analysis-kb";
String knowledgeBaseContent = """
数据分析知识库
1. 数据分析方法
- 描述性分析:总结数据的基本特征
- 探索性分析:发现数据中的模式和关系
- 推断性分析:基于样本数据推断总体特征
- 预测性分析:利用历史数据预测未来趋势
2. 常用分析工具
- Excel:适用于小型数据集的基本分析
- Python:强大的数据分析和可视化工具
- R:专门用于统计分析的编程语言
- Tableau:直观的数据可视化平台
3. 数据处理流程
- 数据收集:确定数据来源和收集方法
- 数据清洗:处理缺失值、异常值和重复数据
- 数据转换:将数据转换为适合分析的格式
- 数据分析:应用适当的分析方法得出结论
- 结果呈现:以图表和报告形式展示分析结果
4. 工具使用指南
- calculator: 基础数学计算工具
- chartGeneration: 图表生成工具,可用于生成柱状图、折线图、饼图等
- statisticalCalculation: 统计计算工具,可用于计算基本统计信息、相关系数和线性回归分析
""";
// 创建文档
Resource resource = new ByteArrayResource(knowledgeBaseContent.getBytes());
TextReader textReader = new TextReader(resource);
List<Document> documents = textReader.get();
// 分割文档
TokenTextSplitter textSplitter = new TokenTextSplitter();
List<Document> splitDocuments = textSplitter.apply(documents);
// 添加元数据
for (int i = 0; i < splitDocuments.size(); i++) {
Document originalDoc = splitDocuments.get(i);
Document newDoc = Document.builder()
.id(UUID.randomUUID().toString())
.text(originalDoc.getText())
.metadata("collectionId", collectionId)
.metadata("agent", "数据分析员")
.build();
splitDocuments.set(i, newDoc);
}
// 存储到向量数据库
vectorStore.add(splitDocuments);
log.info("数据分析员知识库创建完成,共{}个文档片段", splitDocuments.size());
} catch (Exception e) {
log.error("创建数据分析员知识库失败", e);
}
}
/**
* 为内容创作助手创建知识库
*/
public void createContentCreationKnowledgeBase() {
if (vectorStore == null) {
log.warn("VectorStore未配置,无法创建知识库");
return;
}
try {
String collectionId = "content-creation-kb";
String knowledgeBaseContent = """
内容创作知识库
1. 创作类型
- 营销文案:突出产品卖点,激发购买欲望
- 技术文档:准确描述功能,便于用户理解
- 新闻报道:客观陈述事实,传递有价值信息
- 社交媒体内容:简洁有趣,易于传播
2. 创作原则
- 目标明确:清楚了解内容的目标受众和传播目的
- 结构清晰:合理安排内容结构,便于读者理解
- 语言生动:使用恰当的修辞手法,增强表达效果
- 信息准确:确保内容的真实性和可靠性
3. 创作流程
- 需求分析:了解创作背景、目标和要求
- 资料收集:搜集相关素材和参考资料
- 大纲制定:规划内容框架和重点章节
- 初稿撰写:按照大纲完成初稿写作
- 修改完善:检查内容质量,进行必要的修改
4. 工具使用指南
- search: 搜索相关资料和参考内容
- writingStyleReference: 创作风格参考工具,提供各种写作风格的参考和指导
- documentTemplate: 文档模板工具,提供各种类型的文档模板
""";
// 创建文档
Resource resource = new ByteArrayResource(knowledgeBaseContent.getBytes());
TextReader textReader = new TextReader(resource);
List<Document> documents = textReader.get();
// 分割文档
TokenTextSplitter textSplitter = new TokenTextSplitter();
List<Document> splitDocuments = textSplitter.apply(documents);
// 添加元数据
for (int i = 0; i < splitDocuments.size(); i++) {
Document originalDoc = splitDocuments.get(i);
Document newDoc = Document.builder()
.id(UUID.randomUUID().toString())
.text(originalDoc.getText())
.metadata("collectionId", collectionId)
.metadata("agent", "内容创作助手")
.build();
splitDocuments.set(i, newDoc);
}
// 存储到向量数据库
vectorStore.add(splitDocuments);
log.info("内容创作助手知识库创建完成,共{}个文档片段", splitDocuments.size());
} catch (Exception e) {
log.error("创建内容创作助手知识库失败", e);
}
}
/**
* 为学习导师创建知识库
*/
public void createLearningMentorKnowledgeBase() {
if (vectorStore == null) {
log.warn("VectorStore未配置,无法创建知识库");
return;
}
try {
String collectionId = "learning-mentor-kb";
String knowledgeBaseContent = """
学习导师知识库
1. 学习方法指导
- 主动学习:通过提问、讨论等方式积极参与学习过程
- 分散学习:将学习内容分散到多个时间段,避免疲劳
- 多样化学习:结合阅读、听讲、实践等多种方式
- 反思总结:定期回顾所学内容,加深理解和记忆
2. 学科辅导
- 数学:注重逻辑思维训练,掌握解题方法和技巧
- 语文:培养阅读理解能力,提高写作表达水平
- 英语:加强听说读写四项技能的综合训练
- 科学:理解科学原理,培养实验探究能力
3. 学习规划
- 目标设定:制定明确、可衡量的学习目标
- 时间管理:合理安排学习时间,提高学习效率
- 进度跟踪:定期检查学习进展,及时调整计划
- 效果评估:通过测试和练习检验学习成果
4. 工具使用指南
- search: 搜索相关学习资料
- studyPlanGeneration: 学习计划制定工具,可根据学习目标和时间安排制定个性化的学习计划
- courseMaterialRetrieval: 课程资料检索工具,可检索和查询相关课程资料
""";
// 创建文档
Resource resource = new ByteArrayResource(knowledgeBaseContent.getBytes());
TextReader textReader = new TextReader(resource);
List<Document> documents = textReader.get();
// 分割文档
TokenTextSplitter textSplitter = new TokenTextSplitter();
List<Document> splitDocuments = textSplitter.apply(documents);
// 添加元数据
for (int i = 0; i < splitDocuments.size(); i++) {
Document originalDoc = splitDocuments.get(i);
Document newDoc = Document.builder()
.id(UUID.randomUUID().toString())
.text(originalDoc.getText())
.metadata("collectionId", collectionId)
.metadata("agent", "学习导师")
.build();
splitDocuments.set(i, newDoc);
}
// 存储到向量数据库
vectorStore.add(splitDocuments);
log.info("学习导师知识库创建完成,共{}个文档片段", splitDocuments.size());
} catch (Exception e) {
log.error("创建学习导师知识库失败", e);
}
}
/**
* 为指定Agent创建知识库
* @param agent Agent对象
*/
public void createKnowledgeBaseForAgent(Agent agent) {
if (agent == null) {
log.warn("Agent对象为空,无法创建知识库");
return;
}
switch (agent.getName()) {
case "客服助手":
createCustomerServiceKnowledgeBase();
break;
case "技术支持":
createTechnicalSupportKnowledgeBase();
break;
case "数据分析员":
createDataAnalysisKnowledgeBase();
break;
case "内容创作助手":
createContentCreationKnowledgeBase();
break;
case "学习导师":
createLearningMentorKnowledgeBase();
break;
default:
log.warn("未找到{}对应的知識庫創建方法", agent.getName());
}
}
}
\ No newline at end of file
package pangea.hiagent.document;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import pangea.hiagent.model.Agent;
import pangea.hiagent.service.AgentService;
import java.util.List;
/**
* 知识库初始化服务
* 在应用启动时为各Agent创建相应的知识库
*/
@Slf4j
@Service
public class KnowledgeBaseInitializationService {
@Autowired
private DocumentManagementService documentManagementService;
@Autowired
private AgentService agentService;
/**
* 应用启动完成后初始化知识库
*/
@EventListener(ApplicationReadyEvent.class)
public void initializeKnowledgeBases() {
log.info("开始初始化各Agent的知识库");
try {
// 获取所有Agent
List<Agent> agents = agentService.listAgents();
if (agents == null || agents.isEmpty()) {
log.warn("未找到任何Agent,跳过知识库初始化");
return;
}
// 为每个Agent创建知识库
for (Agent agent : agents) {
log.info("为Agent {} 创建知识库", agent.getName());
documentManagementService.createKnowledgeBaseForAgent(agent);
}
log.info("所有Agent的知识库初始化完成");
} catch (Exception e) {
log.error("初始化知识库时发生错误", e);
}
}
}
\ No newline at end of file
package pangea.hiagent.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* Agent请求DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AgentRequest {
private String agentId;
private String systemPrompt;
private String userMessage;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
private Boolean streaming;
private List<String> tools;
}
package pangea.hiagent.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.UUID;
/**
* 统一API响应类
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponse<T> {
/**
* 响应状态码
*/
private Integer code;
/**
* 响应消息
*/
private String message;
/**
* 响应数据
*/
private T data;
/**
* 时间戳
*/
private Long timestamp;
/**
* 请求ID
*/
private String requestId;
/**
* 错误信息
*/
private ErrorDetail error;
/**
* 成功响应
*/
public static <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.code(200)
.message("success")
.data(data)
.timestamp(System.currentTimeMillis())
.requestId(UUID.randomUUID().toString())
.build();
}
/**
* 成功响应(带消息)
*/
public static <T> ApiResponse<T> success(T data, String message) {
return ApiResponse.<T>builder()
.code(200)
.message(message)
.data(data)
.timestamp(System.currentTimeMillis())
.requestId(UUID.randomUUID().toString())
.build();
}
/**
* 失败响应
*/
public static <T> ApiResponse<T> error(Integer code, String message) {
return ApiResponse.<T>builder()
.code(code)
.message(message)
.timestamp(System.currentTimeMillis())
.requestId(UUID.randomUUID().toString())
.build();
}
/**
* 失败响应(带错误详情)
*/
public static <T> ApiResponse<T> error(Integer code, String message, ErrorDetail errorDetail) {
return ApiResponse.<T>builder()
.code(code)
.message(message)
.timestamp(System.currentTimeMillis())
.requestId(UUID.randomUUID().toString())
.error(errorDetail)
.build();
}
/**
* 错误详情类
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class ErrorDetail {
private String type;
private String details;
private String field;
private String suggestion;
}
}
package pangea.hiagent.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 聊天请求DTO
* 用于处理前端发送的聊天请求
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ChatRequest {
private String message;
}
\ No newline at end of file
package pangea.hiagent.dto;
import com.alibaba.fastjson2.annotation.JSONField;
/**
* DOM同步的数据传输对象
*/
public class DomSyncData {
// 消息类型:init(初始化完整DOM)、update(增量DOM更新)、style(样式)、script(脚本)
@JSONField(name = "type")
private String type;
// DOM内容(完整/增量)
@JSONField(name = "dom")
private String dom;
// 样式内容(内联+外部)
@JSONField(name = "style")
private String style;
// 脚本内容(内联+外部)
@JSONField(name = "script")
private String script;
// 页面URL(用于前端匹配)
@JSONField(name = "url")
private String url;
// 无参构造(JSON序列化需要)
public DomSyncData() {}
// 全参构造
public DomSyncData(String type, String dom, String style, String script, String url) {
this.type = type;
this.dom = dom;
this.style = style;
this.script = script;
this.url = url;
}
// getter/setter方法
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getDom() { return dom; }
public void setDom(String dom) { this.dom = dom; }
public String getStyle() { return style; }
public void setStyle(String style) { this.style = style; }
public String getScript() { return script; }
public void setScript(String script) { this.script = script; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
}
\ No newline at end of file
package pangea.hiagent.dto;
import lombok.Data;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
/**
* OAuth2提供商配置请求DTO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OAuth2ProviderRequest {
/**
* 提供者名称(唯一标识)
*/
@NotBlank(message = "提供商名称不能为空")
private String providerName;
/**
* 显示名称
*/
@NotBlank(message = "显示名称不能为空")
private String displayName;
/**
* 描述
*/
private String description;
/**
* 认证类型(authorization_code/implicit/client_credentials等)
*/
@NotBlank(message = "认证类型不能为空")
private String authType;
/**
* 授权端点 URL
*/
@NotBlank(message = "授权端点URL不能为空")
private String authorizeUrl;
/**
* 令牌端点 URL
*/
@NotBlank(message = "令牌端点URL不能为空")
private String tokenUrl;
/**
* 用户信息端点 URL
*/
@NotBlank(message = "用户信息端点URL不能为空")
private String userinfoUrl;
/**
* 客户端 ID
*/
@NotBlank(message = "客户端ID不能为空")
private String clientId;
/**
* 客户端密钥
*/
@NotBlank(message = "客户端密钥不能为空")
private String clientSecret;
/**
* 重定向 URI
*/
@NotBlank(message = "重定向URI不能为空")
private String redirectUri;
/**
* 请求的权限范围
*/
private String scope;
/**
* 是否启用
*/
@NotNull(message = "启用状态不能为空")
private Integer enabled;
/**
* JSON 格式的配置信息
*/
private String configJson;
}
\ No newline at end of file
package pangea.hiagent.dto;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 分页响应数据结构
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class PageData<T> {
/**
* 总记录数
*/
private Long total;
/**
* 总页数
*/
private Long pages;
/**
* 当前页码
*/
private Long current;
/**
* 每页大小
*/
private Long size;
/**
* 数据列表
*/
private List<T> records;
/**
* 从MyBatis Plus Page对象转换
*/
public static <T> PageData<T> from(IPage<T> page) {
return PageData.<T>builder()
.total(page.getTotal())
.pages(page.getPages())
.current(page.getCurrent())
.size(page.getSize())
.records(page.getRecords())
.build();
}
}
package pangea.hiagent.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.Map;
/**
* 工作面板事件数据传输对象
* 用于在SSE流中传输工作面板的各类事件(思考过程、工具调用等)
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WorkPanelEvent implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 事件类型:thinking/tool_call/tool_result/tool_error/log/embed
*/
private String eventType;
/**
* 事件发生的时间戳
*/
private Long timestamp;
/**
* 对于thinking事件:思考内容
* 对于tool_call事件:工具调用详情
* 对于log事件:日志内容
*/
private String content;
/**
* 思考类型(分析、规划、执行等)
*/
private String thinkingType;
/**
* 工具名称
*/
private String toolName;
/**
* 工具执行的方法/action
*/
private String toolAction;
/**
* 工具输入参数
*/
private Map<String, Object> toolInput;
/**
* 工具输出结果
*/
private Object toolOutput;
/**
* 工具执行状态(pending/success/failure)
*/
private String toolStatus;
/**
* 日志级别(info/warn/error/debug)
*/
private String logLevel;
/**
* 执行耗时(毫秒)
*/
private Long executionTime;
/**
* Embed事件信息 - 嵌入资源URL
*/
private String embedUrl;
/**
* Embed事件信息 - MIME类型
*/
private String embedType;
/**
* Embed事件信息 - 嵌入事件标题
*/
private String embedTitle;
/**
* Embed事件信息 - HTML内容
*/
private String embedHtmlContent;
/**
* 附加数据
*/
private Map<String, Object> metadata;
}
\ No newline at end of file
package pangea.hiagent.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 工作面板状态数据传输对象
* 用于API返回工作面板的当前状态
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WorkPanelStatusDto implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 工作面板ID
*/
private String id;
/**
* 当前Agent ID
*/
private String agentId;
/**
* 当前Agent名称
*/
private String agentName;
/**
* 所有事件列表
*/
private List<WorkPanelEvent> events;
/**
* 思考过程事件列表
*/
private List<WorkPanelEvent> thinkingEvents;
/**
* 工具调用事件列表
*/
private List<WorkPanelEvent> toolCallEvents;
/**
* 执行日志事件列表
*/
private List<WorkPanelEvent> logEvents;
/**
* 总事件数量
*/
private Integer totalEvents;
/**
* 成功的工具调用数
*/
private Integer successfulToolCalls;
/**
* 失败的工具调用数
*/
private Integer failedToolCalls;
/**
* 更新时间戳
*/
private Long updateTimestamp;
/**
* 是否正在处理中
*/
private Boolean isProcessing;
}
package pangea.hiagent.exception;
import lombok.Getter;
/**
* 业务异常类
* 用于处理业务逻辑中的异常情况
*/
@Getter
public class BusinessException extends RuntimeException {
private final Integer code;
private final String message;
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
public BusinessException(Integer code, String message, Throwable cause) {
super(message, cause);
this.code = code;
this.message = message;
}
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
this.message = errorCode.getMessage();
}
public BusinessException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.code = errorCode.getCode();
this.message = errorCode.getMessage();
}
}
\ No newline at end of file
package pangea.hiagent.exception;
/**
* 错误码枚举类
* 定义系统中使用的标准错误码
*/
public enum ErrorCode {
// 系统级错误 (1000-1999)
SYSTEM_ERROR(1000, "系统内部错误"),
PARAMETER_ERROR(1001, "参数错误"),
UNAUTHORIZED(1002, "未授权访问"),
FORBIDDEN(1003, "权限不足"),
NOT_FOUND(1004, "资源不存在"),
// 用户相关错误 (2000-2999)
USER_NOT_FOUND(2000, "用户不存在"),
USER_ALREADY_EXISTS(2001, "用户已存在"),
INVALID_CREDENTIALS(2002, "用户名或密码错误"),
TOKEN_EXPIRED(2003, "令牌已过期"),
TOKEN_INVALID(2004, "令牌无效"),
// Agent相关错误 (3000-3999)
AGENT_NOT_FOUND(3000, "Agent不存在"),
AGENT_ALREADY_EXISTS(3001, "Agent已存在"),
AGENT_CONFIG_ERROR(3002, "Agent配置错误"),
// LLM相关错误 (4000-4999)
LLM_CONFIG_NOT_FOUND(4000, "LLM配置不存在"),
LLM_PROVIDER_NOT_SUPPORTED(4001, "不支持的LLM提供商"),
LLM_API_CALL_FAILED(4002, "LLM API调用失败"),
LLM_MODEL_CREATION_FAILED(4003, "LLM模型创建失败"),
// 工具相关错误 (5000-5999)
TOOL_NOT_FOUND(5000, "工具不存在"),
TOOL_EXECUTION_FAILED(5001, "工具执行失败"),
TOOL_LOADING_FAILED(5002, "工具加载失败"),
// 文档相关错误 (6000-6999)
DOCUMENT_NOT_FOUND(6000, "文档不存在"),
DOCUMENT_PROCESSING_FAILED(6001, "文档处理失败"),
DOCUMENT_EMBEDDING_FAILED(6002, "文档向量化失败"),
// 对话相关错误 (7000-7999)
DIALOGUE_NOT_FOUND(7000, "对话不存在"),
DIALOGUE_CONTEXT_TOO_LONG(7001, "对话上下文过长");
private final Integer code;
private final String message;
ErrorCode(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
\ No newline at end of file
package pangea.hiagent.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import pangea.hiagent.dto.ApiResponse;
import jakarta.servlet.http.HttpServletRequest;
import java.util.stream.Collectors;
/**
* 全局异常处理器
* 统一处理系统中的各种异常
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException e, HttpServletRequest request) {
log.warn("业务异常: {} - URL: {}", e.getMessage(), request.getRequestURL());
ApiResponse.ErrorDetail errorDetail = ApiResponse.ErrorDetail.builder()
.type("BUSINESS_ERROR")
.details(e.getMessage())
.build();
ApiResponse<Void> response = ApiResponse.error(e.getCode(), e.getMessage(), errorDetail);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/**
* 处理参数验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Void>> handleMethodArgumentNotValidException(
MethodArgumentNotValidException e, HttpServletRequest request) {
log.warn("参数验证异常: {} - URL: {}", e.getMessage(), request.getRequestURL());
String errorMessage = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining("; "));
ApiResponse.ErrorDetail errorDetail = ApiResponse.ErrorDetail.builder()
.type("VALIDATION_ERROR")
.details(errorMessage)
.build();
ApiResponse<Void> response = ApiResponse.error(ErrorCode.PARAMETER_ERROR.getCode(),
ErrorCode.PARAMETER_ERROR.getMessage(), errorDetail);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/**
* 处理绑定异常
*/
@ExceptionHandler(BindException.class)
public ResponseEntity<ApiResponse<Void>> handleBindException(BindException e, HttpServletRequest request) {
log.warn("绑定异常: {} - URL: {}", e.getMessage(), request.getRequestURL());
String errorMessage = e.getBindingResult().getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
.collect(Collectors.joining("; "));
ApiResponse.ErrorDetail errorDetail = ApiResponse.ErrorDetail.builder()
.type("BIND_ERROR")
.details(errorMessage)
.build();
ApiResponse<Void> response = ApiResponse.error(ErrorCode.PARAMETER_ERROR.getCode(),
ErrorCode.PARAMETER_ERROR.getMessage(), errorDetail);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/**
* 处理参数类型不匹配异常
*/
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity<ApiResponse<Void>> handleMethodArgumentTypeMismatchException(
MethodArgumentTypeMismatchException e, HttpServletRequest request) {
log.warn("参数类型不匹配异常: {} - URL: {}", e.getMessage(), request.getRequestURL());
ApiResponse.ErrorDetail errorDetail = ApiResponse.ErrorDetail.builder()
.type("ARGUMENT_TYPE_MISMATCH")
.details("参数 '" + e.getName() + "' 类型不匹配,期望类型: " + e.getRequiredType().getSimpleName())
.build();
ApiResponse<Void> response = ApiResponse.error(ErrorCode.PARAMETER_ERROR.getCode(),
ErrorCode.PARAMETER_ERROR.getMessage(), errorDetail);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/**
* 处理HTTP消息不可读异常
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiResponse<Void>> handleHttpMessageNotReadableException(
HttpMessageNotReadableException e, HttpServletRequest request) {
log.warn("HTTP消息不可读异常: {} - URL: {}", e.getMessage(), request.getRequestURL());
ApiResponse.ErrorDetail errorDetail = ApiResponse.ErrorDetail.builder()
.type("MESSAGE_NOT_READABLE")
.details("请求体格式不正确或缺失")
.build();
ApiResponse<Void> response = ApiResponse.error(ErrorCode.PARAMETER_ERROR.getCode(),
ErrorCode.PARAMETER_ERROR.getMessage(), errorDetail);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/**
* 处理未授权异常
*/
@ExceptionHandler(org.springframework.security.access.AccessDeniedException.class)
public ResponseEntity<ApiResponse<Void>> handleAccessDeniedException(
org.springframework.security.access.AccessDeniedException e, HttpServletRequest request) {
log.warn("访问被拒绝: {} - URL: {}", e.getMessage(), request.getRequestURL());
ApiResponse.ErrorDetail errorDetail = ApiResponse.ErrorDetail.builder()
.type("ACCESS_DENIED")
.details("您没有权限执行此操作")
.build();
ApiResponse<Void> response = ApiResponse.error(ErrorCode.FORBIDDEN.getCode(),
ErrorCode.FORBIDDEN.getMessage(), errorDetail);
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);
}
/**
* 处理认证异常
*/
@ExceptionHandler(org.springframework.security.authentication.AuthenticationServiceException.class)
public ResponseEntity<ApiResponse<Void>> handleAuthenticationServiceException(
org.springframework.security.authentication.AuthenticationServiceException e, HttpServletRequest request) {
log.warn("认证服务异常: {} - URL: {}", e.getMessage(), request.getRequestURL());
ApiResponse.ErrorDetail errorDetail = ApiResponse.ErrorDetail.builder()
.type("AUTHENTICATION_FAILED")
.details("认证失败,请检查您的凭证")
.build();
ApiResponse<Void> response = ApiResponse.error(ErrorCode.UNAUTHORIZED.getCode(),
ErrorCode.UNAUTHORIZED.getMessage(), errorDetail);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
}
/**
* 处理系统异常
* 增强版本:更好地处理SSE流式响应中的异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleException(Exception e, HttpServletRequest request) {
// 检查是否是SSE流式响应异常
if (isStreamingResponseException(request, e)) {
// SSE流式响应异常,不能返回JSON响应
// 检查是否是预期的客户端断开连接(IOException)
if (e instanceof java.io.IOException ||
(e.getCause() instanceof java.io.IOException)) {
// 客户端连接中断是正常情况,记录为DEBUG级别
if (log.isDebugEnabled()) {
log.debug("SSE流式传输中客户端连接中断 - URL: {} - 异常类型: {}",
request.getRequestURL(),
e.getClass().getSimpleName());
}
} else if (e instanceof org.springframework.web.context.request.async.AsyncRequestNotUsableException) {
// 异步请求不可用 - 客户端已断开
if (log.isDebugEnabled()) {
log.debug("SSE异步请求不可用,客户端已断开连接 - URL: {}", request.getRequestURL());
}
} else {
// 非IOException的SSE异常才记录为ERROR
log.error("SSE流式处理异常 - URL: {} - 异常类型: {} - 异常消息: {}",
request.getRequestURL(),
e.getClass().getSimpleName(),
e.getMessage(),
e);
}
// 不再抛出异常,直接返回空响应以避免二次异常处理
// 响应已经进入SSE流模式,无法切换回JSON格式
return ResponseEntity.ok().build();
}
log.error("系统异常 - URL: {} - 异常类型: {} - 异常消息: {}",
request.getRequestURL(),
e.getClass().getSimpleName(),
e.getMessage(),
e);
ApiResponse.ErrorDetail errorDetail = ApiResponse.ErrorDetail.builder()
.type("SYSTEM_ERROR")
.details("系统内部错误,请联系管理员")
.build();
ApiResponse<Void> response = ApiResponse.error(ErrorCode.SYSTEM_ERROR.getCode(),
ErrorCode.SYSTEM_ERROR.getMessage(), errorDetail);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
/**
* 判断是否为SSE流式响应异常
* 增强版本:更全面的检测逻辑
* @param request 请求对象
* @param e 异常对象
* @return 是否为SSE流式响应异常
*/
private boolean isStreamingResponseException(HttpServletRequest request, Exception e) {
// 检查Accept头
String acceptHeader = request.getHeader("Accept");
boolean isAcceptingStream = acceptHeader != null && acceptHeader.contains("text/event-stream");
// 检查Content-Type(响应已设置为SSE格式)
String contentType = request.getHeader("Content-Type");
boolean isStreamContent = contentType != null && contentType.contains("text/event-stream");
// 检查请求路径(通常SSE端点包含stream或chat关键字)
String requestUri = request.getRequestURI();
boolean isStreamPath = requestUri != null && (requestUri.contains("stream") ||
requestUri.contains("chat") && requestUri.contains("event"));
// 检查异常链中是否包含SSE相关异常
boolean hasSseException = checkForSseException(e);
// 检查是否是SSE操作中的标准异常
boolean isSseOperationException = e instanceof org.springframework.web.context.request.async.AsyncRequestNotUsableException ||
e instanceof java.io.IOException && e.getMessage() != null &&
(e.getMessage().contains("Socket") ||
e.getMessage().contains("软件中止") ||
e.getMessage().contains("ServletOutputStream") ||
e.getMessage().contains("Pipe"));
return isAcceptingStream || isStreamContent || isStreamPath || hasSseException || isSseOperationException;
}
/**
* 检查异常链中是否包含SSE相关的异常
* 增强版本:更全面的异常关键字识别
* @param e 异常对象
* @return 是否包含SSE异常
*/
private boolean checkForSseException(Exception e) {
// 检查异常类型
if (e instanceof org.springframework.web.context.request.async.AsyncRequestNotUsableException) {
return true;
}
// 检查异常消息中的SSE相关关键字
String message = e.getMessage();
if (message != null) {
if (message.contains("SseEmitter") ||
message.contains("SSE") ||
message.contains("event-stream") ||
message.contains("ServletOutputStream") ||
message.contains("Socket") ||
message.contains("Pipe") ||
message.contains("software") ||
message.contains("软件中止") ||
message.contains("断开") ||
message.contains("AsyncRequestNotUsable")) {
return true;
}
}
// 递归检查cause(最多检查5层深度以避免无限递归)
if (e.getCause() != null && e.getCause() instanceof Exception) {
Exception cause = (Exception) e.getCause();
if (cause instanceof org.springframework.web.context.request.async.AsyncRequestNotUsableException) {
return true;
}
String causeMsg = cause.getMessage();
if (causeMsg != null && (causeMsg.contains("Socket") || causeMsg.contains("Pipe"))) {
return true;
}
return checkForSseException(cause);
}
return false;
}
}
\ No newline at end of file
package pangea.hiagent.memory;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 基于Caffeine的ChatMemory实现
* 提供内存级的对话历史存储功能
*/
@Slf4j
@Component
public class CaffeineChatMemory implements ChatMemory {
private Cache<String, List<Message>> cache;
@PostConstruct
public void init() {
// 初始化Caffeine缓存
cache = Caffeine.newBuilder()
.maximumSize(10000) // 最大缓存条目数
.expireAfterWrite(24, TimeUnit.HOURS) // 写入后24小时过期
.recordStats() // 记录缓存统计信息
.build();
log.info("CaffeineChatMemory初始化完成");
}
@Override
public void add(String conversationId, List<Message> messages) {
try {
// 获取现有的消息列表
List<Message> existingMessages = get(conversationId, Integer.MAX_VALUE);
// 添加新消息
existingMessages.addAll(messages);
// 保存到缓存
cache.put(conversationId, existingMessages);
log.debug("成功将{}条消息添加到会话{}", messages.size(), conversationId);
} catch (Exception e) {
log.error("保存消息到Caffeine缓存时发生错误", e);
throw new RuntimeException("Failed to save messages to Caffeine cache", e);
}
}
@Override
public List<Message> get(String conversationId, int lastN) {
try {
List<Message> messages = cache.getIfPresent(conversationId);
if (messages == null) {
return new ArrayList<>();
}
// 返回最新的N条消息
if (lastN < messages.size()) {
return messages.subList(messages.size() - lastN, messages.size());
}
return new ArrayList<>(messages); // 返回副本以防止外部修改
} catch (Exception e) {
log.error("从Caffeine缓存获取消息时发生错误", e);
throw new RuntimeException("Failed to get messages from Caffeine cache", e);
}
}
@Override
public void clear(String conversationId) {
try {
cache.invalidate(conversationId);
log.debug("成功清除会话{}", conversationId);
} catch (Exception e) {
log.error("清除会话时发生错误", e);
throw new RuntimeException("Failed to clear conversation", e);
}
}
}
\ No newline at end of file
package pangea.hiagent.memory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* 混合ChatMemory实现
* 支持Caffeine和Redis两种缓存机制,并提供配置选项来启用或禁用这两种缓存功能
*/
@Slf4j
@Component
public class HybridChatMemory implements ChatMemory {
@Autowired(required = false)
private CaffeineChatMemory caffeineChatMemory;
@Autowired(required = false)
private RedisChatMemory redisChatMemory;
// 是否启用Caffeine缓存
@Value("${app.chat-memory.caffeine.enabled:true}")
private boolean caffeineEnabled;
// 是否启用Redis缓存
@Value("${app.chat-memory.redis.enabled:true}")
private boolean redisEnabled;
@Override
public void add(String conversationId, List<Message> messages) {
boolean success = false;
Exception lastException = null;
// 尝试保存到Caffeine缓存
if (caffeineEnabled && caffeineChatMemory != null) {
try {
caffeineChatMemory.add(conversationId, messages);
success = true;
log.debug("成功将消息保存到Caffeine缓存");
} catch (Exception e) {
lastException = e;
log.warn("保存消息到Caffeine缓存时发生错误: {}", e.getMessage());
}
}
// 尝试保存到Redis缓存
if (redisEnabled && redisChatMemory != null) {
try {
redisChatMemory.add(conversationId, messages);
success = true;
log.debug("成功将消息保存到Redis缓存");
} catch (Exception e) {
lastException = e;
log.warn("保存消息到Redis缓存时发生错误: {}", e.getMessage());
}
}
// 如果两种缓存都失败了,抛出异常
if (!success && lastException != null) {
log.error("保存消息到所有缓存时都失败了", lastException);
throw new RuntimeException("Failed to save messages to any cache", lastException);
}
}
@Override
public List<Message> get(String conversationId, int lastN) {
// 优先从Caffeine缓存获取
if (caffeineEnabled && caffeineChatMemory != null) {
try {
List<Message> messages = caffeineChatMemory.get(conversationId, lastN);
if (messages != null && !messages.isEmpty()) {
log.debug("从Caffeine缓存获取到消息");
return messages;
}
} catch (Exception e) {
log.warn("从Caffeine缓存获取消息时发生错误: {}", e.getMessage());
}
}
// 从Redis缓存获取
if (redisEnabled && redisChatMemory != null) {
try {
List<Message> messages = redisChatMemory.get(conversationId, lastN);
if (messages != null && !messages.isEmpty()) {
log.debug("从Redis缓存获取到消息");
// 同步到Caffeine缓存以提高下次访问速度
if (caffeineEnabled && caffeineChatMemory != null) {
try {
caffeineChatMemory.add(conversationId, messages);
log.debug("已将Redis缓存的数据同步到Caffeine缓存");
} catch (Exception e) {
log.warn("同步数据到Caffeine缓存时发生错误: {}", e.getMessage());
}
}
return messages;
}
} catch (Exception e) {
log.warn("从Redis缓存获取消息时发生错误: {}", e.getMessage());
}
}
// 都没有获取到数据,返回空列表
return new ArrayList<>();
}
@Override
public void clear(String conversationId) {
boolean success = false;
Exception lastException = null;
// 清除Caffeine缓存
if (caffeineEnabled && caffeineChatMemory != null) {
try {
caffeineChatMemory.clear(conversationId);
success = true;
log.debug("成功清除Caffeine缓存中的会话数据");
} catch (Exception e) {
lastException = e;
log.warn("清除Caffeine缓存中的会话数据时发生错误: {}", e.getMessage());
}
}
// 清除Redis缓存
if (redisEnabled && redisChatMemory != null) {
try {
redisChatMemory.clear(conversationId);
success = true;
log.debug("成功清除Redis缓存中的会话数据");
} catch (Exception e) {
lastException = e;
log.warn("清除Redis缓存中的会话数据时发生错误: {}", e.getMessage());
}
}
// 如果两种缓存都清除失败,抛出异常
if (!success && lastException != null) {
log.error("清除所有缓存中的会话数据时都失败了", lastException);
throw new RuntimeException("Failed to clear conversation from any cache", lastException);
}
}
}
\ No newline at end of file
package pangea.hiagent.memory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import pangea.hiagent.model.Agent;
import java.util.Collections;
import java.util.List;
/**
* 内存服务类
* 负责管理聊天内存和会话相关功能
*/
@Slf4j
@Service
public class MemoryService {
@Autowired
private ChatMemory chatMemory;
@Autowired
private SmartHistorySummarizer smartHistorySummarizer;
/**
* 为每个用户-Agent组合创建唯一的会话ID
* @param agent Agent对象
* @return 会话ID
*/
public String generateSessionId(Agent agent) {
return generateSessionId(agent, null);
}
/**
* 为每个用户-Agent组合创建唯一的会话ID
* @param agent Agent对象
* @param userId 用户ID(可选,如果未提供则从SecurityContext获取)
* @return 会话ID
*/
public String generateSessionId(Agent agent, String userId) {
if (userId == null) {
userId = getCurrentUserId();
}
return userId + "_" + agent.getId();
}
/**
* 获取当前认证用户ID
* @return 用户ID
*/
private String getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return (authentication != null && authentication.getPrincipal() != null) ?
(String) authentication.getPrincipal() : null;
}
/**
* 添加用户消息到ChatMemory
* @param sessionId 会话ID
* @param userMessage 用户消息
*/
public void addUserMessageToMemory(String sessionId, String userMessage) {
UserMessage userMsg = new UserMessage(userMessage);
chatMemory.add(sessionId, Collections.singletonList(userMsg));
}
/**
* 添加助手回复到ChatMemory
* @param sessionId 会话ID
* @param assistantMessage 助手回复
*/
public void addAssistantMessageToMemory(String sessionId, String assistantMessage) {
AssistantMessage assistantMsg = new AssistantMessage(assistantMessage);
chatMemory.add(sessionId, Collections.singletonList(assistantMsg));
}
/**
* 获取历史消息
* @param sessionId 会话ID
* @param historyLength 历史记录长度
* @return 历史消息列表
*/
public List<org.springframework.ai.chat.messages.Message> getHistoryMessages(String sessionId, int historyLength) {
return chatMemory.get(sessionId, historyLength);
}
/**
* 获取智能摘要后的历史消息
* @param sessionId 会话ID
* @param historyLength 历史记录长度
* @return 智能摘要后的历史消息列表
*/
public List<org.springframework.ai.chat.messages.Message> getSmartHistoryMessages(String sessionId, int historyLength) {
List<org.springframework.ai.chat.messages.Message> historyMessages = chatMemory.get(sessionId, historyLength);
return smartHistorySummarizer.summarize(historyMessages, historyLength);
}
}
\ No newline at end of file
package pangea.hiagent.memory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* 基于Redis的ChatMemory实现
* 提供持久化的对话历史存储功能
*/
@Slf4j
@Component
public class RedisChatMemory implements ChatMemory {
@Autowired
private RedisTemplate<String, String> redisTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
// 对话历史默认保存时间(小时)
private static final int DEFAULT_EXPIRE_HOURS = 24;
@Override
public void add(String conversationId, List<Message> messages) {
try {
// 获取现有的消息列表
List<Message> existingMessages = get(conversationId, Integer.MAX_VALUE);
// 添加新消息
existingMessages.addAll(messages);
// 序列化并保存到Redis
String serializedMessages = objectMapper.writeValueAsString(existingMessages);
String key = generateRedisKey(conversationId);
redisTemplate.opsForValue().set(key, serializedMessages, DEFAULT_EXPIRE_HOURS, TimeUnit.HOURS);
log.debug("成功将{}条消息添加到会话{}", messages.size(), conversationId);
} catch (JsonProcessingException e) {
log.error("序列化消息时发生错误,会话ID: {},消息数量: {}", conversationId, messages.size(), e);
throw new RuntimeException("Failed to serialize messages for conversation: " + conversationId, e);
} catch (Exception e) {
log.error("保存消息到Redis时发生错误,会话ID: {},消息数量: {}", conversationId, messages.size(), e);
// 添加更多上下文信息到异常中
throw new RuntimeException("Failed to save messages to Redis for conversation: " + conversationId + ", message count: " + messages.size(), e);
}
}
@Override
public List<Message> get(String conversationId, int lastN) {
try {
String key = generateRedisKey(conversationId);
String serializedMessages = redisTemplate.opsForValue().get(key);
if (serializedMessages == null || serializedMessages.isEmpty()) {
return new ArrayList<>();
}
List<Message> messages = objectMapper.readValue(serializedMessages, new TypeReference<List<Message>>() {});
// 返回最新的N条消息
if (lastN < messages.size()) {
return messages.subList(messages.size() - lastN, messages.size());
}
return messages;
} catch (JsonProcessingException e) {
log.error("反序列化消息时发生错误,会话ID: {}", conversationId, e);
throw new RuntimeException("Failed to deserialize messages for conversation: " + conversationId, e);
} catch (Exception e) {
log.error("从Redis获取消息时发生错误,会话ID: {}", conversationId, e);
throw new RuntimeException("Failed to get messages from Redis for conversation: " + conversationId, e);
}
}
@Override
public void clear(String conversationId) {
try {
String key = generateRedisKey(conversationId);
redisTemplate.delete(key);
log.debug("成功清除会话{}", conversationId);
} catch (Exception e) {
log.error("清除会话时发生错误,会话ID: {}", conversationId, e);
throw new RuntimeException("Failed to clear conversation: " + conversationId, e);
}
}
/**
* 生成Redis键名
* @param conversationId 会话ID
* @return Redis键名
*/
private String generateRedisKey(String conversationId) {
return "chat_memory:" + conversationId;
}
}
\ No newline at end of file
package pangea.hiagent.memory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* 智能历史摘要服务
* 实现关键信息识别算法,优化长对话历史管理
*/
@Slf4j
@Service
public class SmartHistorySummarizer {
// 关键词模式,用于识别重要信息
private static final Pattern IMPORTANT_KEYWORDS_PATTERN = Pattern.compile(
"(重要|关键|主要|核心|总结|结论|决定|计划|目标|问题|解决方案|步骤|方法|原因|结果|影响|建议|需求|要求|规则|限制|约束|前提|假设|定义|概念|术语|公式|代码|示例|案例|经验|教训|注意|警告|错误|异常|bug|debug|修复|优化|改进|提升|增强|加强|完善|完成|实现|达成|达到|获得|取得|收获|学习|理解|掌握|熟悉|了解|知道|明白|清楚|确认|验证|测试|检查|审查|审核|批准|同意|拒绝|反对|支持|帮助|协助|合作|配合|沟通|交流|讨论|会议|电话|邮件|联系|通知|提醒|警告|紧急|重要|优先|首要|第一|最后|最终|结束|完成|完毕)",
Pattern.CASE_INSENSITIVE
);
// 代码模式,用于识别代码片段
private static final Pattern CODE_PATTERN = Pattern.compile(
"(```|\\b(function|class|def|public|private|protected|static|void|int|String|boolean|if|else|for|while|switch|case|break|continue|return|try|catch|finally|throw|throws|import|package|extends|implements|interface|abstract|final|native|synchronized|volatile|transient|strictfp)\\b)",
Pattern.CASE_INSENSITIVE
);
/**
* 智能摘要历史消息
* @param messages 原始消息列表
* @param maxLength 最大保留消息数
* @return 摘要后的消息列表
*/
public List<Message> summarize(List<Message> messages, int maxLength) {
if (messages == null || messages.size() <= maxLength) {
return messages;
}
log.debug("开始智能摘要历史消息,原始消息数: {}, 目标长度: {}", messages.size(), maxLength);
// 识别重要消息
List<Message> importantMessages = identifyImportantMessages(messages);
// 如果重要消息数量小于等于目标长度,直接返回
if (importantMessages.size() <= maxLength) {
log.debug("重要消息数量({})小于等于目标长度({}),直接返回", importantMessages.size(), maxLength);
return importantMessages;
}
// 如果重要消息仍然过多,进一步筛选
List<Message> filteredMessages = filterMessages(importantMessages, maxLength);
log.debug("筛选后消息数量: {}", filteredMessages.size());
return filteredMessages;
}
/**
* 识别重要消息
* @param messages 消息列表
* @return 重要消息列表
*/
private List<Message> identifyImportantMessages(List<Message> messages) {
List<Message> importantMessages = new ArrayList<>();
for (Message message : messages) {
// 始终保留系统消息
if (message instanceof SystemMessage) {
importantMessages.add(message);
continue;
}
// 检查是否包含重要关键词
if (containsImportantKeywords(message)) {
importantMessages.add(message);
continue;
}
// 检查是否包含代码
if (containsCode(message)) {
importantMessages.add(message);
continue;
}
// 对于用户消息和助手消息,保留最近的一部分
if ((message instanceof UserMessage || message instanceof AssistantMessage) &&
importantMessages.size() < messages.size() * 0.7) { // 保留最多70%的普通消息
importantMessages.add(message);
}
}
return importantMessages;
}
/**
* 检查消息是否包含重要关键词
* @param message 消息
* @return 是否包含重要关键词
*/
private boolean containsImportantKeywords(Message message) {
String content = message.getText();
if (content == null || content.isEmpty()) {
return false;
}
return IMPORTANT_KEYWORDS_PATTERN.matcher(content).find();
}
/**
* 检查消息是否包含代码
* @param message 消息
* @return 是否包含代码
*/
private boolean containsCode(Message message) {
String content = message.getText();
if (content == null || content.isEmpty()) {
return false;
}
return CODE_PATTERN.matcher(content).find();
}
/**
* 进一步筛选消息
* @param messages 消息列表
* @param maxLength 最大长度
* @return 筛选后的消息列表
*/
private List<Message> filterMessages(List<Message> messages, int maxLength) {
// 如果消息数量仍然超过最大长度,保留最近的消息
int startIndex = Math.max(0, messages.size() - maxLength);
return new ArrayList<>(messages.subList(startIndex, messages.size()));
}
}
\ No newline at end of file
package pangea.hiagent.model;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.HashSet;
import java.util.Set;
/**
* Agent实体类
* 代表一个AI智能体
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@TableName("agent")
public class Agent extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* Agent名称
*/
private String name;
/**
* Agent描述
*/
private String description;
/**
* Agent状态(active/inactive/draft)
*/
private String status;
/**
* 默认模型
*/
private String defaultModel;
/**
* 系统提示词
*/
private String systemPrompt;
/**
* 提示词模板
*/
private String promptTemplate;
/**
* 温度参数(0-2)
*/
private Double temperature;
/**
* 最大生成Token数
*/
private Integer maxTokens;
/**
* Top P参数
*/
private Double topP;
/**
* Top K参数
*/
private Integer topK;
/**
* 存在惩罚
*/
private Double presencePenalty;
/**
* 频率惩罚
*/
private Double frequencyPenalty;
/**
* 历史记录长度
*/
private Integer historyLength;
/**
* 可用工具(JSON格式)
*/
private String tools;
/**
* 关联的RAG集合ID
*/
private String ragCollectionId;
/**
* 是否启用RAG
*/
private Boolean enableRag;
/**
* RAG检索的文档数量
*/
private Integer ragTopK;
/**
* RAG相似度阈值
*/
private Double ragScoreThreshold;
/**
* RAG提示词模板
*/
private String ragPromptTemplate;
/**
* 是否启用ReAct Agent模式
*/
private Boolean enableReAct;
/**
* 是否启用流式输出
*/
private Boolean enableStreaming;
/**
* Agent所有者
*/
private String owner;
/**
* 获取工具名称列表
* @return 工具名称列表
*/
public List<String> getToolNames() {
if (tools == null || tools.isEmpty()) {
return List.of();
}
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(tools, new TypeReference<List<String>>() {});
} catch (Exception e) {
// 如果解析失败,返回空列表
return List.of();
}
}
/**
* 获取工具名称集合(去重)
* @return 工具名称集合
*/
public Set<String> getToolNameSet() {
return new HashSet<>(getToolNames());
}
}
\ No newline at end of file
package pangea.hiagent.model;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* AgentDialogue实体类
* 代表Agent的一条对话记录
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@TableName("agent_dialogue")
public class AgentDialogue extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 关联的Agent ID
*/
private String agentId;
/**
* 上下文ID
*/
private String contextId;
/**
* 用户消息
*/
private String userMessage;
/**
* Agent响应
*/
private String agentResponse;
/**
* 提示词Token数
*/
private Integer promptTokens;
/**
* 生成Token数
*/
private Integer completionTokens;
/**
* 总Token数
*/
private Integer totalTokens;
/**
* 处理时间(毫秒)
*/
private Long processingTime;
/**
* 结束原因(stop/tool_call/limit)
*/
private String finishReason;
/**
* 工具调用列表(JSON格式)
*/
private String toolCalls;
/**
* 会话用户
*/
private String userId;
}
package pangea.hiagent.model;
/**
* 认证模式枚举
* 定义系统支持的各种身份认证方式
*/
public enum AuthMode {
/**
* 本地用户名/密码认证
*/
LOCAL("local", "本地用户认证"),
/**
* OAuth2.0 授权码模式
*/
OAUTH2_AUTHORIZATION_CODE("oauth2_auth_code", "OAuth2.0 授权码模式"),
/**
* OAuth2.0 隐式授权
*/
OAUTH2_IMPLICIT("oauth2_implicit", "OAuth2.0 隐式授权"),
/**
* OAuth2.0 客户端凭证
*/
OAUTH2_CLIENT_CREDENTIALS("oauth2_client_credentials", "OAuth2.0 客户端凭证"),
/**
* LDAP 目录认证
*/
LDAP("ldap", "LDAP 目录认证"),
/**
* SAML 单点登录
*/
SAML("saml", "SAML 单点登录");
private final String code;
private final String description;
AuthMode(String code, String description) {
this.code = code;
this.description = description;
}
public String getCode() {
return code;
}
public String getDescription() {
return description;
}
/**
* 根据代码获取认证模式
*/
public static AuthMode fromCode(String code) {
for (AuthMode mode : AuthMode.values()) {
if (mode.code.equals(code)) {
return mode;
}
}
return null;
}
}
package pangea.hiagent.model;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 基础实体类
* 所有数据模型的父类,包含通用字段
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(type = IdType.ASSIGN_UUID)
protected String id;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
protected LocalDateTime createdAt;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
protected LocalDateTime updatedAt;
/**
* 创建人
*/
protected String createdBy;
/**
* 更新人
*/
protected String updatedBy;
/**
* 删除标记(0-未删除,1-已删除)
*/
@TableLogic
protected Integer deleted;
/**
* 备注
*/
protected String remark;
}
package pangea.hiagent.model;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* LLM配置实体类
* 用于存储各种大语言模型的配置信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@TableName("llm_config")
public class LlmConfig extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 配置名称(唯一标识)
*/
private String name;
/**
* 配置描述
*/
private String description;
/**
* 模型提供商(openai, deepseek, ollama等)
*/
private String provider;
/**
* 模型名称
*/
private String modelName;
/**
* API密钥
*/
private String apiKey;
/**
* API端点URL(适用于自定义或本地模型)
*/
private String baseUrl;
/**
* 温度参数(0-2)
*/
private Double temperature;
/**
* 最大生成Token数
*/
private Integer maxTokens;
/**
* Top P参数
*/
private Double topP;
/**
* 是否启用该配置
*/
private Boolean enabled;
/**
* 配置所有者
*/
private String owner;
}
\ No newline at end of file
package pangea.hiagent.model;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* OAuth2 账户关联实体类
* 记录用户与第三方 OAuth2 提供者的账户关联信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("oauth2_account")
public class OAuth2Account implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键 ID
*/
private String id;
/**
* 关联的本地用户 ID
*/
private String userId;
/**
* OAuth2 提供者名称
*/
private String providerName;
/**
* 第三方平台的用户 ID
*/
private String remoteUserId;
/**
* 第三方平台的用户名
*/
private String remoteUsername;
/**
* 第三方平台的邮箱
*/
private String remoteEmail;
/**
* 访问令牌(Access Token)
*/
private String accessToken;
/**
* 刷新令牌(Refresh Token)
*/
private String refreshToken;
/**
* 令牌过期时间
*/
private Long tokenExpiry;
/**
* 授权的权限范围
*/
private String scope;
/**
* 第三方平台返回的用户信息(JSON 格式)
*/
private String profileData;
/**
* 账户关联时间
*/
private Long linkedAt;
/**
* 最后登录时间
*/
private Long lastLoginAt;
/**
* 创建时间
*/
private Long createdAt;
/**
* 更新时间
*/
private Long updatedAt;
/**
* 删除标志
*/
private Integer deleted;
}
package pangea.hiagent.model;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* OAuth2 提供者配置实体类
* 代表一个 OAuth2 提供者的配置信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@TableName("oauth2_provider")
public class OAuth2Provider extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 提供者名称(唯一标识)
*/
private String providerName;
/**
* 显示名称
*/
private String displayName;
/**
* 描述
*/
private String description;
/**
* 认证类型(authorization_code/implicit/client_credentials等)
*/
private String authType;
/**
* 授权端点 URL
*/
private String authorizeUrl;
/**
* 令牌端点 URL
*/
private String tokenUrl;
/**
* 用户信息端点 URL
*/
private String userinfoUrl;
/**
* 客户端 ID
*/
private String clientId;
/**
* 客户端密钥
*/
private String clientSecret;
/**
* 重定向 URI
*/
private String redirectUri;
/**
* 请求的权限范围
*/
private String scope;
/**
* 是否启用
*/
private Integer enabled;
/**
* JSON 格式的配置信息
*/
private String configJson;
/**
* 创建此提供者的用户ID
*/
private String createdBy;
/**
* 更新此提供者的用户ID
*/
private String updatedBy;
}
package pangea.hiagent.model;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 状态字典实体类
* 用于存储系统中各种状态的统一定义
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@TableName("status_dict")
public class StatusDict extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 状态编码(唯一标识)
*/
private String code;
/**
* 状态名称
*/
private String name;
/**
* 状态分类
*/
private String category;
/**
* 状态描述
*/
private String description;
/**
* 排序序号
*/
private Integer sortOrder;
}
\ No newline at end of file
package pangea.hiagent.model;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 工具实体类
* 用于存储工具的配置信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@TableName("tool")
public class Tool extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 工具名称(唯一标识)
*/
private String name;
/**
* 工具显示名称
*/
private String displayName;
/**
* 工具描述
*/
private String description;
/**
* 工具分类
*/
private String category;
/**
* 工具状态(active/inactive)
*/
private String status;
/**
* 参数定义(JSON格式)
*/
private String parameters;
/**
* 返回值类型
*/
private String returnType;
/**
* 返回值结构(JSON格式)
*/
private String returnSchema;
/**
* 实现代码
*/
private String implementation;
/**
* 超时时间(毫秒)
*/
private Long timeout;
/**
* API端点URL
*/
private String apiEndpoint;
/**
* HTTP方法(GET/POST/PUT/DELETE)
*/
private String httpMethod;
/**
* 请求头(JSON格式)
*/
private String headers;
/**
* 认证类型
*/
private String authType;
/**
* 认证配置(JSON格式)
*/
private String authConfig;
/**
* 工具所有者
*/
private String owner;
}
\ No newline at end of file
package pangea.hiagent.model;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* User实体类
* 代表系统用户
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
@TableName("sys_user")
public class User extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 用户名
*/
private String username;
/**
* 密码(加密存储)
*/
private String password;
/**
* 邮箱
*/
private String email;
/**
* 昵称
*/
private String nickname;
/**
* 用户状态(active/inactive/banned)
*/
private String status;
/**
* 用户角色(admin/user/api_user)
*/
private String role;
/**
* 头像
*/
private String avatar;
/**
* 最后登录时间
*/
private Long lastLoginTime;
/**
* API Key
*/
private String apiKey;
}
package pangea.hiagent.prompt;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import pangea.hiagent.model.Agent;
import pangea.hiagent.model.Tool;
import pangea.hiagent.service.ToolService;
import java.util.List;
import java.util.stream.Collectors;
/**
* 提示词服务类
* 负责构建和管理系统提示词
*/
@Slf4j
@Service
public class PromptService {
@Autowired
private ToolService toolService;
/**
* 构建系统提示词 - 根据Agent配置的工具动态生成
* @param agent Agent对象
* @return 系统提示词
*/
public String buildSystemPrompt(Agent agent) {
// 如果Agent配置了自定义系统提示词,优先使用
if (agent.getSystemPrompt() != null && !agent.getSystemPrompt().isEmpty()) {
return agent.getSystemPrompt();
}
try {
// 获取Agent配置的可用工具列表
List<Tool> agentTools = getAvailableTools(agent);
// 如果没有工具,直接返回默认提示词
if (agentTools.isEmpty()) {
return getDefaultSystemPrompt();
}
// 构建工具描述部分
String toolsDescription = buildToolsDescription(agentTools);
String toolsList = buildToolsList(agentTools);
// 构建默认系统提示词,包含动态生成的工具信息
return buildDefaultSystemPrompt(toolsDescription, toolsList);
} catch (Exception e) {
log.error("构建系统提示词时发生错误", e);
// 返回默认的系统提示词
return getDefaultSystemPrompt();
}
}
/**
* 获取Agent可用的工具列表
* @param agent Agent对象
* @return 工具列表
*/
private List<Tool> getAvailableTools(Agent agent) {
try {
// 获取Agent所有者的所有活跃工具
List<Tool> allTools = toolService.getUserToolsByStatus(agent.getOwner(), "active");
if (allTools == null || allTools.isEmpty()) {
log.warn("Agent: {} 没有配置可用的工具", agent.getId());
return List.of();
}
// 如果Agent配置了特定的工具列表,则只返回配置的工具
List<String> toolNames = agent.getToolNames();
if (toolNames != null && !toolNames.isEmpty()) {
// 根据工具名称筛选工具
return filterToolsByName(allTools, toolNames);
}
return allTools;
} catch (Exception e) {
log.error("获取Agent可用工具时发生错误", e);
return List.of();
}
}
/**
* 根据工具名称筛选工具
* @param allTools 所有工具
* @param toolNames 工具名称列表
* @return 筛选后的工具列表
*/
private List<Tool> filterToolsByName(List<Tool> allTools, List<String> toolNames) {
return allTools.stream()
.filter(tool -> toolNames.contains(tool.getName()))
.collect(Collectors.toList());
}
/**
* 构建工具描述文本
* @param tools 工具列表
* @return 工具描述文本
*/
private String buildToolsDescription(List<Tool> tools) {
if (tools.isEmpty()) {
return "(暂无可用工具)";
}
StringBuilder description = new StringBuilder();
for (int i = 0; i < tools.size(); i++) {
Tool tool = tools.get(i);
description.append(i + 1).append(". ");
description.append(tool.getName());
if (hasValue(tool.getDisplayName())) {
description.append(" - ").append(tool.getDisplayName());
}
if (hasValue(tool.getDescription())) {
description.append(" - ").append(tool.getDescription());
}
description.append("\n");
}
return description.toString();
}
/**
* 检查字符串是否有值
* @param value 字符串值
* @return 是否有值
*/
private boolean hasValue(String value) {
return value != null && !value.isEmpty();
}
/**
* 构建工具名称列表(用于Action的可选值)
* @param tools 工具列表
* @return 工具名称列表,逗号分隔
*/
private String buildToolsList(List<Tool> tools) {
if (tools.isEmpty()) {
return "(no tools available)";
}
return tools.stream()
.map(Tool::getName)
.collect(Collectors.joining(", "));
}
/**
* 构建默认系统提示词
* @param toolsDescription 工具描述
* @param toolsList 工具列表
* @return 系统提示词
*/
private String buildDefaultSystemPrompt(String toolsDescription, String toolsList) {
return "You are a helpful AI assistant that can use tools to help answer questions.\n" +
"You have access to the following tools:\n" +
toolsDescription +
"\n\nTo use a tool, please use the following format:\n" +
"Thought: Do I need to use a tool? Yes\n" +
"Action: the action to take, should be one of [" + toolsList + "]\n" +
"Action Input: the input to the action\n" +
"Observation: the result of the action\n\n" +
"When you have a response to say to the Human, or if you do not need to use a tool, you MUST use the format:\n" +
"Thought: Do I need to use a tool? No\n" +
"Final Answer: [your response here]";
}
/**
* 获取默认系统提示词(当工具加载失败时使用)
* @return 默认系统提示词
*/
public String getDefaultSystemPrompt() {
return "You are a helpful AI assistant that can use tools to help answer questions.\n" +
"You have access to the following tools:\n" +
"1. getCurrentDateTime - Get current date and time\n" +
"2. add, subtract, multiply, divide - Basic math operations\n" +
"3. processString - String processing\n" +
"4. readFile, writeFile - File operations\n" +
"5. getWeather - Get weather information\n\n" +
"To use a tool, please use the following format:\n" +
"Thought: Do I need to use a tool? Yes\n" +
"Action: the action to take\n" +
"Action Input: the input to the action\n" +
"Observation: the result of the action\n\n" +
"When you have a response to say to the Human, or if you do not need to use a tool, you MUST use the format:\n" +
"Thought: Do I need to use a tool? No\n" +
"Final Answer: [your response here]";
}
}
\ No newline at end of file
package pangea.hiagent.rag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import pangea.hiagent.config.AppConfig;
import pangea.hiagent.model.Agent;
import pangea.hiagent.service.AgentService;
import java.util.List;
import java.util.Map;
/**
* RAG服务类
* 负责文档检索、相似度匹配和RAG增强生成
*/
@Slf4j
@Service
public class RagService {
@Autowired(required = false) // 允许为空
private VectorStore vectorStore;
@Autowired
private AppConfig appConfig;
@Autowired
private AgentService agentService;
/**
* 根据查询检索相关文档片段
*
* @param query 查询内容
* @param collectionIds 集合ID列表
* @param topK 返回结果数量
* @param scoreThreshold 相似度阈值
* @return 相关文档片段列表
*/
public List<Document> searchDocuments(String query, List<String> collectionIds,
Integer topK, Double scoreThreshold) {
log.info("开始检索文档,查询: {}, 集合数: {}, topK: {}, 阈值: {}",
query, collectionIds != null ? collectionIds.size() : 0, topK, scoreThreshold);
// 检查vectorStore是否可用
if (vectorStore == null) {
log.warn("VectorStore未配置,无法执行文档检索");
return List.of();
}
try {
if (collectionIds != null && !collectionIds.isEmpty()) {
}
// 设置检索参数
SearchRequest.Builder requestBuilder = SearchRequest.builder()
.query(query)
.topK(topK != null ? topK : appConfig.getRag().getTopK())
.similarityThreshold(scoreThreshold != null ? scoreThreshold : appConfig.getRag().getScoreThreshold());
// 注意:在当前Spring AI版本中,可能不支持动态过滤器设置
// 我们暂时移除过滤器设置,以确保代码能够编译通过
SearchRequest request = requestBuilder.build();
// 执行检索
List<Document> results = vectorStore.similaritySearch(request);
log.info("检索完成,返回 {} 个结果", results.size());
return results;
} catch (Exception e) {
log.error("文档检索失败", e);
throw new RuntimeException("文档检索失败: " + e.getMessage(), e);
}
}
/**
* 根据Agent配置检索相关文档
*
* @param agent Agent对象
* @param query 查询内容
* @return 相关文档片段列表
*/
public List<Document> searchDocumentsByAgent(Agent agent, String query) {
log.info("根据Agent配置检索文档,Agent ID: {}, 查询: {}", agent.getId(), query);
// 检查vectorStore是否可用
if (vectorStore == null) {
log.warn("VectorStore未配置,无法执行文档检索");
return List.of();
}
try {
// 获取Agent关联的RAG集合
String collectionId = agent.getRagCollectionId();
if (collectionId == null || collectionId.isEmpty()) {
log.warn("Agent未配置RAG集合,Agent ID: {}", agent.getId());
return List.of();
}
// 使用Agent特定的RAG配置
Integer topK = agent.getRagTopK();
Double scoreThreshold = agent.getRagScoreThreshold();
// 执行检索
return searchDocuments(query, List.of(collectionId),
topK != null ? topK : appConfig.getRag().getTopK(),
scoreThreshold != null ? scoreThreshold : appConfig.getRag().getScoreThreshold());
} catch (Exception e) {
log.error("根据Agent配置检索文档失败,Agent ID: {}", agent.getId(), e);
throw new RuntimeException("文档检索失败: " + e.getMessage(), e);
}
}
/**
* 构建RAG增强的提示词
*
* @param query 用户查询
* @param documents 相关文档片段
* @return 增强的提示词
*/
public String buildRagPrompt(String query, List<Document> documents) {
if (documents == null || documents.isEmpty()) {
return query;
}
StringBuilder prompt = new StringBuilder();
prompt.append("请根据以下文档内容回答问题:\n\n");
prompt.append("问题:").append(query).append("\n\n");
prompt.append("参考文档:\n");
for (int i = 0; i < documents.size(); i++) {
Document doc = documents.get(i);
prompt.append("文档 ").append(i + 1).append(":\n");
prompt.append(doc.getText()).append("\n\n");
}
prompt.append("请根据以上文档内容回答问题,如果文档中没有相关信息,请说明无法根据提供的文档回答该问题。\n\n");
prompt.append("回答:");
return prompt.toString();
}
/**
* 构建基于Agent配置的RAG增强提示词
*
* @param agent Agent对象
* @param query 用户查询
* @param documents 相关文档片段
* @return 增强的提示词
*/
public String buildRagPromptByAgent(Agent agent, String query, List<Document> documents) {
if (documents == null || documents.isEmpty()) {
return query;
}
// 使用Agent特定的RAG提示词模板
String ragPromptTemplate = agent.getRagPromptTemplate();
if (ragPromptTemplate == null || ragPromptTemplate.isEmpty()) {
// 使用默认模板
return buildRagPrompt(query, documents);
}
// 使用Agent特定的模板
StringBuilder prompt = new StringBuilder();
prompt.append(ragPromptTemplate).append("\n\n");
prompt.append("问题:").append(query).append("\n\n");
prompt.append("参考文档:\n");
for (int i = 0; i < documents.size(); i++) {
Document doc = documents.get(i);
prompt.append("文档 ").append(i + 1).append(":\n");
prompt.append(doc.getText()).append("\n\n");
}
prompt.append("请根据以上文档内容回答问题,如果文档中没有相关信息,请说明无法根据提供的文档回答该问题。\n\n");
prompt.append("回答:");
return prompt.toString();
}
/**
* RAG增强问答
*
* @param agent Agent对象
* @param query 用户查询
* @return 增强的回答
*/
public String ragQa(Agent agent, String query) {
log.info("开始RAG增强问答,Agent ID: {}, 查询: {}", agent.getId(), query);
// 检查vectorStore是否可用
if (vectorStore == null) {
log.warn("VectorStore未配置,无法执行RAG增强问答");
return null;
}
try {
// 检查是否启用RAG
if (agent.getEnableRag() == null || !agent.getEnableRag()) {
log.warn("Agent未启用RAG功能,Agent ID: {}", agent.getId());
return null;
}
// 检索相关文档
List<Document> documents = searchDocumentsByAgent(agent, query);
if (documents.isEmpty()) {
log.info("未检索到相关文档,Agent ID: {}", agent.getId());
return null;
}
// 构建增强提示词
String enhancedPrompt = buildRagPromptByAgent(agent, query, documents);
// 获取ChatModel
ChatModel chatModel = agentService.getChatModelForAgent(agent);
// 调用模型生成回答
String response = chatModel.call(enhancedPrompt);
log.info("RAG增强问答完成,Agent ID: {}", agent.getId());
return response;
} catch (Exception e) {
log.error("RAG增强问答失败,Agent ID: {}", agent.getId(), e);
throw new RuntimeException("RAG增强问答失败: " + e.getMessage(), e);
}
}
/**
* 获取文档检索统计信息
*
* @param collectionIds 集合ID列表
* @return 统计信息
*/
public Map<String, Object> getRetrievalStats(List<String> collectionIds) {
log.info("获取文档检索统计信息,集合数: {}", collectionIds != null ? collectionIds.size() : 0);
try {
// 获取向量存储统计信息
// 这里可以根据实际需求实现具体的统计逻辑
// 暂时返回基本统计信息
return Map.of(
"collectionCount", collectionIds != null ? collectionIds.size() : 0,
"timestamp", System.currentTimeMillis()
); } catch (Exception e) {
log.error("获取文档检索统计信息失败", e);
throw new RuntimeException("获取统计信息失败: " + e.getMessage(), e);
}
}
}
\ No newline at end of file
package pangea.hiagent.repository;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import pangea.hiagent.model.AgentDialogue;
/**
* AgentDialogue Repository接口
*/
@Mapper
public interface AgentDialogueRepository extends BaseMapper<AgentDialogue> {
}
package pangea.hiagent.repository;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import pangea.hiagent.model.Agent;
/**
* Agent Repository接口
*/
@Mapper
public interface AgentRepository extends BaseMapper<Agent> {
/**
* 查找活跃的Agent(明确指定所有列名以避免MyBatis自动转换问题)
*/
@Select("SELECT id,name,description,status,default_model,system_prompt,prompt_template,temperature,max_tokens,top_p,top_k,presence_penalty,frequency_penalty,history_length,tools,rag_collection_id,enable_rag,enable_re_act,owner,created_at,updated_at,created_by,updated_by,deleted,remark FROM agent WHERE owner = #{owner} AND status = 'active' ORDER BY created_at DESC")
java.util.List<Agent> findActiveAgentsByOwner(@Param("owner") String owner);
/**
* 查找活跃的Agent(使用created_at列)- 修复create_time错误的备用方法
*/
@Select("SELECT id,name,description,status,default_model,system_prompt,prompt_template,temperature,max_tokens,top_p,top_k,presence_penalty,frequency_penalty,history_length,tools,rag_collection_id,enable_rag,enable_re_act,owner,created_at,updated_at,created_by,updated_by,deleted,remark FROM agent WHERE owner = #{owner} AND status = 'active' ORDER BY created_at DESC")
java.util.List<Agent> findActiveAgentsByOwnerWithExplicitColumns(@Param("owner") String owner);
/**
* 分页查询优化
*/
IPage<Agent> selectPageWithOptimization(Page<Agent> page, @Param("ew") com.baomidou.mybatisplus.core.conditions.Wrapper<Agent> wrapper);
}
\ No newline at end of file
package pangea.hiagent.repository;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import pangea.hiagent.model.LlmConfig;
import org.apache.ibatis.annotations.Mapper;
/**
* LLM配置数据访问接口
*/
@Mapper
public interface LlmConfigRepository extends BaseMapper<LlmConfig> {
}
\ No newline at end of file
package pangea.hiagent.repository;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import pangea.hiagent.model.OAuth2Account;
/**
* OAuth2 账户关联 Repository
*/
@Mapper
public interface OAuth2AccountRepository extends BaseMapper<OAuth2Account> {
}
package pangea.hiagent.repository;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import pangea.hiagent.model.OAuth2Provider;
/**
* OAuth2 提供者 Repository
*/
@Mapper
public interface OAuth2ProviderRepository extends BaseMapper<OAuth2Provider> {
}
package pangea.hiagent.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 java.util.List;
/**
* 工具仓库接口
* 提供工具数据访问功能
*/
@Mapper
public interface ToolRepository extends BaseMapper<Tool> {
/**
* 根据所有者获取工具列表
* @param owner 所有者ID
* @return 工具列表
*/
@Select("SELECT * FROM tool WHERE owner = #{owner} AND deleted = 0 ORDER BY created_at DESC")
List<Tool> findByOwner(String owner);
/**
* 根据所有者和状态获取工具列表
* @param owner 所有者ID
* @param status 工具状态
* @return 工具列表
*/
@Select("SELECT * FROM tool WHERE owner = #{owner} AND status = #{status} AND deleted = 0 ORDER BY created_at DESC")
List<Tool> findByOwnerAndStatus(String owner, String status);
/**
* 根据名称和所有者获取工具
* @param name 工具名称
* @param owner 所有者ID
* @return 工具对象
*/
@Select("SELECT * FROM tool WHERE name = #{name} AND owner = #{owner} AND deleted = 0 LIMIT 1")
Tool findByNameAndOwner(String name, String owner);
}
\ No newline at end of file
package pangea.hiagent.repository;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import pangea.hiagent.model.User;
/**
* User Repository接口
*/
@Mapper
public interface UserRepository extends BaseMapper<User> {
}
package pangea.hiagent.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import pangea.hiagent.model.Agent;
import pangea.hiagent.service.AgentService;
import java.io.Serializable;
/**
* 自定义权限评估器
* 用于实现细粒度的资源级权限控制
*/
@Slf4j
@Component("permissionEvaluator")
public class DefaultPermissionEvaluator implements PermissionEvaluator {
private final AgentService agentService;
public DefaultPermissionEvaluator(AgentService agentService) {
this.agentService = agentService;
}
/**
* 检查用户是否有权访问指定Agent
*/
@Override
public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) {
if (authentication == null || targetDomainObject == null || !(permission instanceof String)) {
return false;
}
String userId = (String) authentication.getPrincipal();
String perm = (String) permission;
// 目前只处理Agent访问权限
if (targetDomainObject instanceof Agent) {
Agent agent = (Agent) targetDomainObject;
return checkAgentAccess(userId, agent, perm);
} else if (targetDomainObject instanceof String) {
// 假设targetDomainObject是Agent ID
String agentId = (String) targetDomainObject;
Agent agent = agentService.getAgent(agentId);
if (agent == null) {
return false;
}
return checkAgentAccess(userId, agent, perm);
}
return false;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
if (authentication == null || targetId == null || targetType == null || !(permission instanceof String)) {
return false;
}
String userId = (String) authentication.getPrincipal();
String perm = (String) permission;
// 处理基于ID的权限检查
if ("Agent".equals(targetType)) {
Agent agent = agentService.getAgent(targetId.toString());
if (agent == null) {
return false;
}
return checkAgentAccess(userId, agent, perm);
}
return false;
}
/**
* 检查用户对Agent的访问权限
*/
private boolean checkAgentAccess(String userId, Agent agent, String permission) {
// 管理员可以访问所有Agent
if (isAdminUser(userId)) {
return true;
}
// 检查Agent所有者
if (agent.getOwner().equals(userId)) {
return true;
}
// 根据权限类型进行检查
switch (permission.toLowerCase()) {
case "read":
// 所有用户都可以读取公开的Agent(如果有此概念)
return false; // 暂时不支持公开Agent
case "write":
case "delete":
case "execute":
// 只有所有者可以写入、删除或执行Agent
return agent.getOwner().equals(userId);
default:
return false;
}
}
/**
* 检查是否为管理员用户
*/
private boolean isAdminUser(String userId) {
// 这里可以根据实际需求实现管理员检查逻辑
// 例如查询数据库或检查特殊用户ID
return "admin".equals(userId) || "user-001".equals(userId);
}
}
\ No newline at end of file
package pangea.hiagent.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import pangea.hiagent.utils.JwtUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
/**
* JWT认证过滤器
* 从请求头中提取JWT Token并进行验证
*/
@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
boolean isStreamEndpoint = request.getRequestURI().contains("/api/v1/agent/chat-stream");
boolean isTimelineEndpoint = request.getRequestURI().contains("/api/v1/agent/timeline-events");
if (isStreamEndpoint) {
log.info("处理Agent流式对话请求: {} {}", request.getMethod(), request.getRequestURI());
}
if (isTimelineEndpoint) {
log.info("处理时间轴事件订阅请求: {} {}", request.getMethod(), request.getRequestURI());
}
// 对于OPTIONS请求,直接放行
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
log.debug("OPTIONS请求,直接放行");
filterChain.doFilter(request, response);
return;
}
try {
String token = extractTokenFromRequest(request);
log.debug("JWT过滤器处理请求: {} {},提取到token: {}", request.getMethod(), request.getRequestURI(), token);
if (StringUtils.hasText(token)) {
// 验证token是否有效
boolean isValid = jwtUtil.validateToken(token);
log.debug("JWT验证结果: {}", isValid);
if (isValid) {
String userId = jwtUtil.getUserIdFromToken(token);
log.debug("JWT验证通过,用户ID: {}", userId);
if (userId != null) {
// 创建认证对象,添加基本权限
List<SimpleGrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"));
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userId, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
log.debug("已设置SecurityContext中的认证信息,用户ID: {}, 权限: {}", userId, authentication.getAuthorities());
// 认证成功后继续处理请求
filterChain.doFilter(request, response);
log.debug("JwtAuthenticationFilter处理完成: {} {}", request.getMethod(), request.getRequestURI());
return;
} else {
log.warn("从token中提取的用户ID为空");
}
} else {
log.warn("JWT验证失败,token可能已过期或无效");
}
} else {
log.debug("未找到有效的token");
}
} catch (Exception e) {
log.error("JWT认证处理异常", e);
}
// 继续执行过滤器链,让Spring Security的其他过滤器处理认证和授权
// 这样可以让ExceptionTranslationFilter和AuthorizationFilter正确处理认证失败和权限拒绝
// 不能在这里提前返回错误响应,因为那样会导致响应提交,后续过滤器无法处理
filterChain.doFilter(request, response);
log.debug("JwtAuthenticationFilter处理完成: {} {}", request.getMethod(), request.getRequestURI());
}
/**
* 从请求头或参数中提取Token
* 为了支持SSE连接,我们还需要检查URL参数中的token
*/
private String extractTokenFromRequest(HttpServletRequest request) {
// 首先尝试从请求头中提取Token
String authHeader = request.getHeader("Authorization");
log.debug("从请求头中提取Authorization: {}", authHeader);
if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
log.debug("从Authorization头中提取到token");
return token;
}
// 如果请求头中没有Token,则尝试从URL参数中提取
// 这对于SSE连接特别有用,因为浏览器在自动重连时可能不会发送Authorization头
String tokenParam = request.getParameter("token");
log.debug("从URL参数中提取token参数: {}", tokenParam);
if (StringUtils.hasText(tokenParam)) {
log.debug("从URL参数中提取到token");
return tokenParam;
}
log.debug("未找到有效的token");
return null;
}
}
\ No newline at end of file
package pangea.hiagent.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.ai.tool.annotation.Tool;
/**
* 计算器工具类
* 提供基本的数学运算功能
*/
@Slf4j
@Component
public class CalculatorTools {
@Tool(description = "执行两个数字的加法运算")
public double add(double a, double b) {
double result = a + b;
log.debug("执行加法运算: {} + {} = {}", a, b, result);
return result;
}
@Tool(description = "执行两个数字的减法运算")
public double subtract(double a, double b) {
double result = a - b;
log.debug("执行减法运算: {} - {} = {}", a, b, result);
return result;
}
@Tool(description = "执行两个数字的乘法运算")
public double multiply(double a, double b) {
double result = a * b;
log.debug("执行乘法运算: {} * {} = {}", a, b, result);
return result;
}
@Tool(description = "执行两个数字的除法运算")
public String divide(double a, double b) {
log.debug("执行除法运算: {} / {}", a, b);
if (b == 0) {
log.warn("除法运算错误:除数不能为零");
return "错误:除数不能为零";
}
double result = a / b;
log.debug("除法运算结果: {}", result);
return String.valueOf(result);
}
}
\ No newline at end of file
package pangea.hiagent.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
/**
* 图表生成工具
* 用于根据数据生成各种类型的图表
*/
@Slf4j
@Component
public class ChartGenerationTool {
/**
* 生成柱状图
* @param title 图表标题
* @param xAxisLabels X轴标签数组
* @param seriesData 数据系列数组
* @param seriesName 数据系列名称
* @return 图表生成结果
*/
@Tool(description = "根据数据生成柱状图")
public String generateBarChart(String title, String[] xAxisLabels, double[] seriesData, String seriesName) {
log.debug("开始生成柱状图: {}", title);
try {
if (title == null || title.trim().isEmpty()) {
log.warn("图表标题不能为空");
return "错误:图表标题不能为空";
}
if (xAxisLabels == null || xAxisLabels.length == 0) {
log.warn("X轴标签不能为空");
return "错误:X轴标签不能为空";
}
if (seriesData == null || seriesData.length == 0) {
log.warn("数据系列不能为空");
return "错误:数据系列不能为空";
}
if (xAxisLabels.length != seriesData.length) {
log.warn("X轴标签数量与数据系列数量不匹配");
return "错误:X轴标签数量与数据系列数量不匹配";
}
// 生成图表描述
StringBuilder chartDescription = new StringBuilder();
chartDescription.append("柱状图生成成功:\n");
chartDescription.append("标题: ").append(title).append("\n");
chartDescription.append("数据系列: ").append(seriesName != null ? seriesName : "数据").append("\n");
chartDescription.append("数据点数量: ").append(seriesData.length).append("\n");
chartDescription.append("数据详情:\n");
for (int i = 0; i < xAxisLabels.length; i++) {
chartDescription.append(" ").append(xAxisLabels[i]).append(": ").append(seriesData[i]).append("\n");
}
log.info("柱状图生成完成,包含 {} 个数据点", seriesData.length);
return chartDescription.toString();
} catch (Exception e) {
log.error("生成柱状图时发生错误: {}", e.getMessage(), e);
return "生成柱状图时发生错误: " + e.getMessage();
}
}
/**
* 生成折线图
* @param title 图表标题
* @param xAxisLabels X轴标签数组
* @param seriesData 数据系列数组
* @param seriesName 数据系列名称
* @return 图表生成结果
*/
@Tool(description = "根据数据生成折线图")
public String generateLineChart(String title, String[] xAxisLabels, double[] seriesData, String seriesName) {
log.debug("开始生成折线图: {}", title);
try {
if (title == null || title.trim().isEmpty()) {
log.warn("图表标题不能为空");
return "错误:图表标题不能为空";
}
if (xAxisLabels == null || xAxisLabels.length == 0) {
log.warn("X轴标签不能为空");
return "错误:X轴标签不能为空";
}
if (seriesData == null || seriesData.length == 0) {
log.warn("数据系列不能为空");
return "错误:数据系列不能为空";
}
if (xAxisLabels.length != seriesData.length) {
log.warn("X轴标签数量与数据系列数量不匹配");
return "错误:X轴标签数量与数据系列数量不匹配";
}
// 生成图表描述
StringBuilder chartDescription = new StringBuilder();
chartDescription.append("折线图生成成功:\n");
chartDescription.append("标题: ").append(title).append("\n");
chartDescription.append("数据系列: ").append(seriesName != null ? seriesName : "数据").append("\n");
chartDescription.append("数据点数量: ").append(seriesData.length).append("\n");
chartDescription.append("数据详情:\n");
for (int i = 0; i < xAxisLabels.length; i++) {
chartDescription.append(" ").append(xAxisLabels[i]).append(": ").append(seriesData[i]).append("\n");
}
log.info("折线图生成完成,包含 {} 个数据点", seriesData.length);
return chartDescription.toString();
} catch (Exception e) {
log.error("生成折线图时发生错误: {}", e.getMessage(), e);
return "生成折线图时发生错误: " + e.getMessage();
}
}
/**
* 生成饼图
* @param title 图表标题
* @param labels 饼图各部分标签数组
* @param values 饼图各部分数值数组
* @return 图表生成结果
*/
@Tool(description = "根据数据生成饼图")
public String generatePieChart(String title, String[] labels, double[] values) {
log.debug("开始生成饼图: {}", title);
try {
if (title == null || title.trim().isEmpty()) {
log.warn("图表标题不能为空");
return "错误:图表标题不能为空";
}
if (labels == null || labels.length == 0) {
log.warn("标签不能为空");
return "错误:标签不能为空";
}
if (values == null || values.length == 0) {
log.warn("数值不能为空");
return "错误:数值不能为空";
}
if (labels.length != values.length) {
log.warn("标签数量与数值数量不匹配");
return "错误:标签数量与数值数量不匹配";
}
// 计算总值
double total = 0;
for (double value : values) {
total += value;
}
// 生成图表描述
StringBuilder chartDescription = new StringBuilder();
chartDescription.append("饼图生成成功:\n");
chartDescription.append("标题: ").append(title).append("\n");
chartDescription.append("数据项数量: ").append(values.length).append("\n");
chartDescription.append("总计: ").append(total).append("\n");
chartDescription.append("数据详情:\n");
for (int i = 0; i < labels.length; i++) {
double percentage = total > 0 ? (values[i] / total) * 100 : 0;
chartDescription.append(" ").append(labels[i]).append(": ").append(values[i])
.append(" (").append(String.format("%.2f", percentage)).append("%)\n");
}
log.info("饼图生成完成,包含 {} 个数据项", values.length);
return chartDescription.toString();
} catch (Exception e) {
log.error("生成饼图时发生错误: {}", e.getMessage(), e);
return "生成饼图时发生错误: " + e.getMessage();
}
}
}
\ No newline at end of file
package pangea.hiagent.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import pangea.hiagent.rag.RagService;
import pangea.hiagent.model.Agent;
import pangea.hiagent.service.AgentService;
import java.util.List;
/**
* 课程资料检索工具
* 用于检索和查询相关课程资料
*/
@Slf4j
@Component
public class CourseMaterialRetrievalTool {
// RAG服务引用
private RagService ragService;
// Agent服务引用
private AgentService agentService;
public CourseMaterialRetrievalTool(RagService ragService, AgentService agentService) {
this.ragService = ragService;
this.agentService = agentService;
}
/**
* 检索课程资料
* @param query 查询关键词
* @param subject 学科领域
* @param maxResults 最大返回结果数
* @return 检索到的课程资料内容
*/
@Tool(description = "根据关键词和学科领域检索相关课程资料")
public String searchCourseMaterials(String query, String subject, Integer maxResults) {
log.debug("开始检索课程资料: 查询={}, 学科={}, 最大结果数={}", query, subject, maxResults);
try {
if (query == null || query.trim().isEmpty()) {
log.warn("查询关键词不能为空");
return "错误:查询关键词不能为空";
}
// 设置默认最大结果数
if (maxResults == null || maxResults <= 0) {
maxResults = 5;
}
// 获取学习导师Agent
Agent learningMentorAgent = getLearningMentorAgent();
if (learningMentorAgent == null) {
log.error("未找到学习导师Agent");
return "错误:未找到学习导师Agent配置";
}
// 使用RAG服务检索资料
List<org.springframework.ai.document.Document> documents =
ragService.searchDocumentsByAgent(learningMentorAgent, query);
// 限制返回结果数量
if (documents.size() > maxResults) {
documents = documents.subList(0, maxResults);
}
// 格式化结果
StringBuilder result = new StringBuilder();
result.append("课程资料检索结果:\n");
result.append("查询关键词: ").append(query).append("\n");
if (subject != null && !subject.trim().isEmpty()) {
result.append("学科领域: ").append(subject).append("\n");
}
result.append("找到 ").append(documents.size()).append(" 个相关资料片段\n\n");
for (int i = 0; i < documents.size(); i++) {
org.springframework.ai.document.Document doc = documents.get(i);
result.append("资料 ").append(i + 1).append(":\n");
result.append(doc.getText()).append("\n\n");
}
log.info("课程资料检索完成,找到 {} 个结果", documents.size());
return result.toString();
} catch (Exception e) {
log.error("检索课程资料时发生错误: {}", e.getMessage(), e);
return "检索课程资料时发生错误: " + e.getMessage();
}
}
/**
* 获取学习导师Agent
* @return 学习导师Agent对象
*/
private Agent getLearningMentorAgent() {
try {
// 在实际应用中,这里应该通过某种方式获取学习导师Agent
// 例如通过AgentService查询特定名称的Agent
List<Agent> agents = agentService.listAgents();
for (Agent agent : agents) {
if ("学习导师".equals(agent.getName())) {
return agent;
}
}
} catch (Exception e) {
log.error("获取学习导师Agent时发生错误: {}", e.getMessage(), e);
}
return null;
}
}
\ No newline at end of file
package pangea.hiagent.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.ai.tool.annotation.Tool;
import java.time.LocalDateTime;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* 日期时间工具类
* 提供日期和时间相关的功能
*/
@Slf4j
@Component
public class DateTimeTools {
@Tool(description = "获取当前日期和时间")
public String getCurrentDateTime() {
String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
log.debug("获取当前日期时间: {}", dateTime);
return dateTime;
}
@Tool(description = "获取当前日期")
public String getCurrentDate() {
String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
log.debug("获取当前日期: {}", date);
return date;
}
}
\ No newline at end of file
package pangea.hiagent.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
/**
* 文档模板工具
* 用于提供各种类型的文档模板
*/
@Slf4j
@Component
public class DocumentTemplateTool {
public DocumentTemplateTool() {
// 默认构造器
}
/**
* 获取文档模板
* @param templateType 模板类型(如报告、提案、邮件等)
* @param industry 行业领域(如科技、金融、教育等)
* @return 文档模板内容
*/
@Tool(description = "根据模板类型和行业领域提供文档模板")
public String getDocumentTemplate(String templateType, String industry) {
log.debug("开始获取文档模板: 模板类型={}, 行业领域={}", templateType, industry);
try {
if (templateType == null || templateType.trim().isEmpty()) {
log.warn("模板类型不能为空");
return "错误:模板类型不能为空";
}
// 生成文档模板
String template = generateDocumentTemplate(templateType, industry);
log.info("文档模板生成完成: 模板类型={}, 行业领域={}", templateType, industry);
return template;
} catch (Exception e) {
log.error("获取文档模板时发生错误: {}", e.getMessage(), e);
return "获取文档模板时发生错误: " + e.getMessage();
}
}
/**
* 生成文档模板
* @param templateType 模板类型
* @param industry 行业领域
* @return 文档模板内容
*/
private String generateDocumentTemplate(String templateType, String industry) {
StringBuilder template = new StringBuilder();
template.append("文档模板:\n\n");
template.append("模板类型: ").append(templateType).append("\n");
if (industry != null && !industry.trim().isEmpty()) {
template.append("行业领域: ").append(industry).append("\n");
}
template.append("\n");
// 根据模板类型生成具体内容
switch (templateType.toLowerCase()) {
case "报告":
template.append("【报告标题】\n\n");
template.append("1. 摘要\n");
template.append(" - 简要概述报告的主要内容和结论\n\n");
template.append("2. 引言\n");
template.append(" - 背景介绍\n");
template.append(" - 报告目的\n");
template.append(" - 研究范围\n\n");
template.append("3. 主体内容\n");
template.append(" - 数据分析\n");
template.append(" - 结果展示\n");
template.append(" - 问题识别\n\n");
template.append("4. 结论与建议\n");
template.append(" - 主要发现\n");
template.append(" - 改进建议\n");
template.append(" - 后续步骤\n\n");
template.append("5. 附录\n");
template.append(" - 数据表格\n");
template.append(" - 参考文献\n");
break;
case "提案":
template.append("【提案标题】\n\n");
template.append("1. 执行摘要\n");
template.append(" - 提案核心内容概述\n");
template.append(" - 预期收益\n\n");
template.append("2. 背景与现状\n");
template.append(" - 当前情况分析\n");
template.append(" - 存在的问题\n\n");
template.append("3. 解决方案\n");
template.append(" - 具体措施\n");
template.append(" - 实施步骤\n");
template.append(" - 时间安排\n\n");
template.append("4. 预算与资源\n");
template.append(" - 成本估算\n");
template.append(" - 所需资源\n\n");
template.append("5. 风险评估\n");
template.append(" - 潜在风险\n");
template.append(" - 应对策略\n\n");
template.append("6. 结论\n");
template.append(" - 总结要点\n");
template.append(" - 呼吁行动\n");
break;
case "邮件":
template.append("主题: [简明扼要地概括邮件内容]\n\n");
template.append("尊敬的[收件人姓名]:\n\n");
template.append("[开场白 - 简要说明写信目的]\n\n");
template.append("[主体内容 - 详细阐述相关信息]\n\n");
template.append("[结尾 - 总结要点,提出下一步行动建议]\n\n");
template.append("此致\n敬礼!\n\n");
template.append("[发件人姓名]\n");
template.append("[职位]\n");
template.append("[联系方式]\n");
break;
case "说明书":
template.append("【产品名称】使用说明书\n\n");
template.append("1. 产品简介\n");
template.append(" - 产品功能\n");
template.append(" - 适用范围\n\n");
template.append("2. 安全须知\n");
template.append(" - 使用前注意事项\n");
template.append(" - 安全警告\n\n");
template.append("3. 安装指南\n");
template.append(" - 所需工具\n");
template.append(" - 安装步骤\n");
template.append(" - 图示说明\n\n");
template.append("4. 使用方法\n");
template.append(" - 操作步骤\n");
template.append(" - 功能说明\n\n");
template.append("5. 维护保养\n");
template.append(" - 日常维护\n");
template.append(" - 故障排除\n\n");
template.append("6. 技术参数\n");
template.append(" - 规格参数\n");
template.append(" - 性能指标\n\n");
template.append("7. 售后服务\n");
template.append(" - 保修条款\n");
template.append(" - 联系方式\n");
break;
default:
template.append("【文档标题】\n\n");
template.append("1. 章节一\n");
template.append(" - 内容要点1\n");
template.append(" - 内容要点2\n\n");
template.append("2. 章节二\n");
template.append(" - 内容要点1\n");
template.append(" - 内容要点2\n\n");
template.append("3. 章节三\n");
template.append(" - 内容要点1\n");
template.append(" - 内容要点2\n\n");
template.append("4. 结论\n");
template.append(" - 总结要点\n");
template.append(" - 后续建议\n");
break;
}
return template.toString();
}
}
\ No newline at end of file
package pangea.hiagent.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.ai.tool.annotation.Tool;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
/**
* 文件处理工具类
* 提供文件读写和管理功能,支持多种文本格式文件
*/
@Slf4j
@Component
public class FileProcessingTools {
// 支持的文本文件扩展名
private static final List<String> TEXT_FILE_EXTENSIONS = Arrays.asList(
".txt", ".md", ".java", ".html", ".htm", ".css", ".js", ".json",
".xml", ".yaml", ".yml", ".properties", ".sql", ".py", ".cpp",
".c", ".h", ".cs", ".php", ".rb", ".go", ".rs", ".swift", ".kt",
".scala", ".sh", ".bat", ".cmd", ".ps1", ".log", ".csv", ".ts",
".jsx", ".tsx", ".vue", ".scss", ".sass", ".less"
);
// 支持的图片文件扩展名
private static final List<String> IMAGE_FILE_EXTENSIONS = Arrays.asList(
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg", ".webp", ".ico"
);
// 默认文件存储目录
private static final String DEFAULT_STORAGE_DIR = "storage";
/**
* 检查文件是否为文本文件
* @param filePath 文件路径
* @return 是否为文本文件
*/
private boolean isTextFile(String filePath) {
if (filePath == null || filePath.isEmpty()) {
return false;
}
String lowerPath = filePath.toLowerCase();
return TEXT_FILE_EXTENSIONS.stream().anyMatch(lowerPath::endsWith);
}
/**
* 检查文件是否为图片文件
* @param filePath 文件路径
* @return 是否为图片文件
*/
private boolean isImageFile(String filePath) {
if (filePath == null || filePath.isEmpty()) {
return false;
}
String lowerPath = filePath.toLowerCase();
return IMAGE_FILE_EXTENSIONS.stream().anyMatch(lowerPath::endsWith);
}
/**
* 检查文件是否为支持的文件类型(文本或图片)
* @param filePath 文件路径
* @return 是否为支持的文件类型
*/
private boolean isSupportedFile(String filePath) {
return isTextFile(filePath) || isImageFile(filePath);
}
/**
* 处理文件路径,支持默认相对路径和随机文件名
* @param filePath 文件路径,如果为null或空则生成随机文件名
* @param extension 文件扩展名
* @return 处理后的完整文件路径
*/
private String processFilePath(String filePath, String extension) {
// 如果filePath为null或空,则生成随机文件名
if (filePath == null || filePath.isEmpty()) {
// 确保默认存储目录存在
File storageDir = new File(DEFAULT_STORAGE_DIR);
if (!storageDir.exists()) {
storageDir.mkdirs();
}
// 生成随机文件名
String randomFileName = UUID.randomUUID().toString().replace("-", "") + extension;
filePath = DEFAULT_STORAGE_DIR + File.separator + randomFileName;
log.debug("生成随机文件名: {}", filePath);
} else {
// 处理相对路径
File file = new File(filePath);
if (!file.isAbsolute()) {
// 如果是相对路径,转换为相对于当前工作目录的绝对路径
filePath = file.getAbsolutePath();
log.debug("转换相对路径为绝对路径: {}", filePath);
}
}
return filePath;
}
/**
* 根据内容自动推断文件扩展名
* @param content 文件内容
* @return 推断的文件扩展名
*/
private String inferFileExtension(String content) {
if (content == null || content.isEmpty()) {
return ".txt";
}
// 简单的内容分析来推断文件类型
String trimmedContent = content.trim();
if (trimmedContent.startsWith("{") || trimmedContent.startsWith("[")) {
return ".json";
} else if (trimmedContent.startsWith("<!DOCTYPE html") || trimmedContent.startsWith("<html")) {
return ".html";
} else if (trimmedContent.startsWith("#") || trimmedContent.startsWith("##")) {
return ".md";
} else if (trimmedContent.contains("public class") || trimmedContent.contains("public static void main")) {
return ".java";
} else if (trimmedContent.startsWith("<?xml")) {
return ".xml";
} else {
return ".txt";
}
}
@Tool(description = "读取文本文件内容,支持多种文本格式")
public String readFile(String filePath) {
return readFileWithEncoding(filePath, "UTF-8");
}
@Tool(description = "读取文本文件内容,支持指定字符编码")
public String readFileWithEncoding(String filePath, String encoding) {
log.debug("开始读取文件: {}, 编码: {}", filePath, encoding);
try {
if (filePath == null || filePath.isEmpty()) {
log.warn("文件路径不能为空");
return "错误:文件路径不能为空";
}
// 处理相对路径
File file = new File(filePath);
if (!file.isAbsolute()) {
// 如果是相对路径,转换为相对于当前工作目录的绝对路径
filePath = file.getAbsolutePath();
log.debug("转换相对路径为绝对路径: {}", filePath);
}
// 检查文件是否存在
if (!file.exists()) {
log.warn("文件不存在: {}", filePath);
return "错误:文件不存在";
}
// 检查是否为支持的文件类型
if (!isSupportedFile(filePath)) {
log.warn("文件不是支持的格式: {}", filePath);
return "错误:文件不是支持的格式";
}
// 确定字符编码
Charset charset = StandardCharsets.UTF_8;
if (encoding != null && !encoding.isEmpty()) {
try {
charset = Charset.forName(encoding);
} catch (Exception e) {
log.warn("无效的字符编码,使用默认UTF-8: {}", encoding);
}
}
// 读取文件内容
String content = new String(Files.readAllBytes(Paths.get(filePath)), charset);
log.debug("文件读取成功,文件大小: {} 字节,字符数: {}", content.getBytes(charset).length, content.length());
return content;
} catch (IOException e) {
log.error("读取文件时发生IO错误: {}", filePath, e);
return "读取文件时发生错误:" + e.getMessage();
} catch (Exception e) {
log.error("处理文件时发生未知错误: {}", filePath, e);
return "处理文件时发生未知错误:" + e.getMessage();
}
}
@Tool(description = "写入内容到文本文件,支持多种文本格式,支持默认相对路径和随机文件名")
public String writeFile(String filePath, String content) {
return writeFileWithEncoding(filePath, content, "UTF-8", false);
}
@Tool(description = "写入内容到文本文件,支持指定字符编码和追加模式,支持默认相对路径和随机文件名")
public String writeFileWithEncoding(String filePath, String content, String encoding, boolean append) {
log.debug("开始写入文件: {}, 编码: {}, 追加模式: {}", filePath, encoding, append);
try {
// 处理文件路径,支持默认相对路径和随机文件名
String extension = inferFileExtension(content);
String processedFilePath = processFilePath(filePath, extension);
// 检查是否为支持的文件类型
if (!isSupportedFile(processedFilePath)) {
log.warn("文件不是支持的格式: {}", processedFilePath);
return "错误:文件不是支持的格式";
}
// 确保父目录存在
File file = new File(processedFilePath);
File parentDir = file.getParentFile();
if (parentDir != null && !parentDir.exists()) {
if (!parentDir.mkdirs()) {
log.warn("无法创建目录: {}", parentDir.getAbsolutePath());
return "错误:无法创建目录";
}
}
// 确定字符编码
Charset charset = StandardCharsets.UTF_8;
if (encoding != null && !encoding.isEmpty()) {
try {
charset = Charset.forName(encoding);
} catch (Exception e) {
log.warn("无效的字符编码,使用默认UTF-8: {}", encoding);
}
}
// 处理空内容
if (content == null) {
content = "";
}
// 写入文件
if (append) {
Files.write(Paths.get(processedFilePath), content.getBytes(charset),
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} else {
Files.write(Paths.get(processedFilePath), content.getBytes(charset));
}
log.debug("文件写入成功,内容大小: {} 字符,文件路径: {}", content.length(), processedFilePath);
return "文件写入成功,文件路径: " + processedFilePath;
} catch (IOException e) {
log.error("写入文件时发生IO错误: {}", filePath, e);
return "写入文件时发生错误:" + e.getMessage();
} catch (Exception e) {
log.error("处理文件时发生未知错误: {}", filePath, e);
return "处理文件时发生未知错误:" + e.getMessage();
}
}
@Tool(description = "追加内容到文本文件末尾,支持默认相对路径和随机文件名")
public String appendToFile(String filePath, String content) {
return writeFileWithEncoding(filePath, content, "UTF-8", true);
}
@Tool(description = "获取文件大小")
public String getFileSize(String filePath) {
log.debug("获取文件大小: {}", filePath);
try {
if (filePath == null || filePath.isEmpty()) {
log.warn("文件路径不能为空");
return "错误:文件路径不能为空";
}
// 处理相对路径
File file = new File(filePath);
if (!file.isAbsolute()) {
// 如果是相对路径,转换为相对于当前工作目录的绝对路径
filePath = file.getAbsolutePath();
log.debug("转换相对路径为绝对路径: {}", filePath);
}
if (!file.exists()) {
log.warn("文件不存在: {}", filePath);
return "错误:文件不存在";
}
long size = file.length();
log.debug("文件大小: {} 字节", size);
return "文件大小:" + size + " 字节";
} catch (Exception e) {
log.error("获取文件大小时发生错误: {}", filePath, e);
return "获取文件大小时发生错误:" + e.getMessage();
}
}
@Tool(description = "检查文件是否存在")
public boolean fileExists(String filePath) {
log.debug("检查文件是否存在: {}", filePath);
if (filePath == null || filePath.isEmpty()) {
log.warn("文件路径不能为空");
return false;
}
// 处理相对路径
File file = new File(filePath);
if (!file.isAbsolute()) {
// 如果是相对路径,转换为相对于当前工作目录的绝对路径
filePath = file.getAbsolutePath();
log.debug("转换相对路径为绝对路径: {}", filePath);
}
file = new File(filePath);
boolean exists = file.exists();
log.debug("文件存在状态: {}", exists);
return exists;
}
@Tool(description = "获取文件信息,包括大小、是否为文本文件等")
public String getFileInfo(String filePath) {
log.debug("获取文件信息: {}", filePath);
try {
if (filePath == null || filePath.isEmpty()) {
log.warn("文件路径不能为空");
return "错误:文件路径不能为空";
}
// 处理相对路径
File file = new File(filePath);
if (!file.isAbsolute()) {
// 如果是相对路径,转换为相对于当前工作目录的绝对路径
filePath = file.getAbsolutePath();
log.debug("转换相对路径为绝对路径: {}", filePath);
}
file = new File(filePath);
if (!file.exists()) {
log.warn("文件不存在: {}", filePath);
return "错误:文件不存在";
}
StringBuilder info = new StringBuilder();
info.append("文件路径: ").append(filePath).append("\n");
info.append("文件大小: ").append(file.length()).append(" 字节\n");
info.append("是否为文本文件: ").append(isTextFile(filePath)).append("\n");
info.append("是否为图片文件: ").append(isImageFile(filePath)).append("\n");
info.append("是否为支持的文件类型: ").append(isSupportedFile(filePath)).append("\n");
info.append("最后修改时间: ").append(new java.util.Date(file.lastModified())).append("\n");
log.debug("文件信息获取成功: {}", filePath);
return info.toString();
} catch (Exception e) {
log.error("获取文件信息时发生错误: {}", filePath, e);
return "获取文件信息时发生错误:" + e.getMessage();
}
}
@Tool(description = "生成随机文件名并返回完整路径")
public String generateRandomFileName(String extension) {
log.debug("生成随机文件名,扩展名: {}", extension);
try {
// 确保默认存储目录存在
File storageDir = new File(DEFAULT_STORAGE_DIR);
if (!storageDir.exists()) {
storageDir.mkdirs();
}
// 处理扩展名
if (extension == null || extension.isEmpty()) {
extension = ".txt";
} else if (!extension.startsWith(".")) {
extension = "." + extension;
}
// 生成随机文件名
String randomFileName = UUID.randomUUID().toString().replace("-", "") + extension;
String fullPath = DEFAULT_STORAGE_DIR + File.separator + randomFileName;
log.debug("生成随机文件名: {}", fullPath);
return fullPath;
} catch (Exception e) {
log.error("生成随机文件名时发生错误", e);
return "生成随机文件名时发生错误:" + e.getMessage();
}
}
}
\ No newline at end of file
# 文件处理工具使用说明
## 功能概述
FileProcessingTools 是一个功能丰富的文件处理工具类,专门设计用于处理各种文本格式文件。该工具支持读取、写入、追加内容到文件,并提供文件信息查询功能。
支持的文件格式包括但不限于:
- 文本文件:`.txt`
- 标记语言文件:`.md`
- 编程语言文件:`.java`, `.html`, `.htm`, `.css`, `.js`, `.json`, `.xml`, `.yaml`, `.yml`, `.py`, `.cpp`, `.c`, `.h`, `.cs`, `.php`, `.rb`, `.go`, `.rs`, `.swift`, `.kt`, `.scala`
- 脚本文件:`.sh`, `.bat`, `.cmd`, `.ps1`
- 其他文本格式:`.properties`, `.sql`, `.log`, `.csv`, `.ts`, `.jsx`, `.tsx`, `.vue`, `.scss`, `.sass`, `.less`
## 功能列表
### 1. readFile(String filePath)
读取文本文件内容
**参数:**
- `filePath`: 文件路径(支持相对路径)
**返回值:**
- 成功时返回文件内容
- 失败时返回错误信息
**示例:**
```java
@Autowired
private FileProcessingTools fileTools;
String content = fileTools.readFile("/path/to/file.txt");
// 或使用相对路径
String content = fileTools.readFile("relative/path/to/file.txt");
```
### 2. readFileWithEncoding(String filePath, String encoding)
读取文本文件内容,支持指定字符编码
**参数:**
- `filePath`: 文件路径(支持相对路径)
- `encoding`: 字符编码(如 "UTF-8", "GBK" 等)
**返回值:**
- 成功时返回文件内容
- 失败时返回错误信息
**示例:**
```java
String content = fileTools.readFileWithEncoding("/path/to/file.txt", "UTF-8");
```
### 3. writeFile(String filePath, String content)
写入内容到文本文件
**参数:**
- `filePath`: 文件路径(支持相对路径,如果为空或null则自动生成随机文件名)
- `content`: 要写入的内容
**返回值:**
- 成功时返回"文件写入成功,文件路径: [完整文件路径]"
- 失败时返回错误信息
**示例:**
```java
// 指定文件名
String result = fileTools.writeFile("/path/to/file.txt", "Hello, World!");
// 使用相对路径
String result = fileTools.writeFile("relative/path/to/file.txt", "Hello, World!");
// 自动生成随机文件名
String result = fileTools.writeFile("", "Hello, World!");
```
### 4. writeFileWithEncoding(String filePath, String content, String encoding, boolean append)
写入内容到文本文件,支持指定字符编码和追加模式
**参数:**
- `filePath`: 文件路径(支持相对路径,如果为空或null则自动生成随机文件名)
- `content`: 要写入的内容
- `encoding`: 字符编码
- `append`: 是否追加到文件末尾(true为追加,false为覆盖)
**返回值:**
- 成功时返回"文件写入成功,文件路径: [完整文件路径]"
- 失败时返回错误信息
**示例:**
```java
// 覆盖写入
String result = fileTools.writeFileWithEncoding("/path/to/file.txt", "New content", "UTF-8", false);
// 追加写入
String result = fileTools.writeFileWithEncoding("/path/to/file.txt", "Additional content", "UTF-8", true);
// 自动生成随机文件名并写入
String result = fileTools.writeFileWithEncoding("", "Content with random filename", "UTF-8", false);
```
### 5. appendToFile(String filePath, String content)
追加内容到文本文件末尾
**参数:**
- `filePath`: 文件路径(支持相对路径,如果为空或null则自动生成随机文件名)
- `content`: 要追加的内容
**返回值:**
- 成功时返回"文件写入成功,文件路径: [完整文件路径]"
- 失败时返回错误信息
**示例:**
```java
String result = fileTools.appendToFile("/path/to/file.txt", "Appended content");
// 或使用相对路径
String result = fileTools.appendToFile("relative/path/to/file.txt", "Appended content");
// 或自动生成随机文件名
String result = fileTools.appendToFile("", "Appended content with random filename");
```
### 6. getFileSize(String filePath)
获取文件大小
**参数:**
- `filePath`: 文件路径(支持相对路径)
**返回值:**
- 成功时返回文件大小信息
- 失败时返回错误信息
**示例:**
```java
String sizeInfo = fileTools.getFileSize("/path/to/file.txt");
// 或使用相对路径
String sizeInfo = fileTools.getFileSize("relative/path/to/file.txt");
```
### 7. fileExists(String filePath)
检查文件是否存在
**参数:**
- `filePath`: 文件路径(支持相对路径)
**返回值:**
- 文件存在返回true
- 文件不存在返回false
**示例:**
```java
boolean exists = fileTools.fileExists("/path/to/file.txt");
// 或使用相对路径
boolean exists = fileTools.fileExists("relative/path/to/file.txt");
```
### 8. getFileInfo(String filePath)
获取文件详细信息
**参数:**
- `filePath`: 文件路径(支持相对路径)
**返回值:**
- 成功时返回文件详细信息(包括路径、大小、是否为文本文件、最后修改时间)
- 失败时返回错误信息
**示例:**
```java
String fileInfo = fileTools.getFileInfo("/path/to/file.txt");
// 或使用相对路径
String fileInfo = fileTools.getFileInfo("relative/path/to/file.txt");
```
### 9. generateRandomFileName(String extension)
生成随机文件名并返回完整路径
**参数:**
- `extension`: 文件扩展名(如 ".txt", "md" 等,如果不带点会自动添加)
**返回值:**
- 成功时返回完整文件路径
- 失败时返回错误信息
**示例:**
```java
String randomFilePath = fileTools.generateRandomFileName(".txt");
// 或不带点的扩展名
String randomFilePath = fileTools.generateRandomFileName("md");
```
## 使用注意事项
1. **字符编码**:默认使用UTF-8编码,可根据需要指定其他编码格式
2. **文件类型限制**:只能处理预定义的文本文件类型,非文本文件会被拒绝处理
3. **目录自动创建**:写入文件时会自动创建不存在的目录
4. **错误处理**:所有操作都有完善的错误处理和日志记录
5. **文件大小**:适合处理中小型文本文件,大文件处理可能影响性能
6. **路径支持**:支持相对路径,默认相对于当前工作目录
7. **随机文件名**:当filePath为空或null时,会自动生成随机文件名并存储在"storage"目录下
8. **扩展名推断**:当使用随机文件名时,会根据内容自动推断合适的文件扩展名
## 错误处理
工具类提供了完善的错误处理机制:
- 文件不存在时返回明确的错误信息
- 文件路径为空时自动生成随机文件名而不是报错
- IO异常时记录详细日志并返回友好的错误信息
- 编码错误时使用默认UTF-8编码并记录警告日志
## 性能优化
1. **内存使用**:使用NIO.2 API进行文件读写,提高效率
2. **字符编码**:自动检测和处理字符编码,确保内容正确性
3. **日志记录**:详细的日志记录便于问题排查和性能监控
4. **路径处理**:智能处理相对路径和绝对路径
5. **文件名生成**:使用UUID生成唯一的随机文件名,避免冲突
## 示例用法
```java
@Autowired
private FileProcessingTools fileTools;
// 读取文件
String content = fileTools.readFile("data/input.txt");
// 写入文件(自动生成随机文件名)
String writeResult = fileTools.writeFile("", "Hello, World!");
System.out.println(writeResult); // 输出文件路径
// 追加内容到文件
fileTools.appendToFile("logs/app.log", "New log entry\n");
// 获取文件信息
String fileInfo = fileTools.getFileInfo("config/settings.json");
```
\ No newline at end of file
package pangea.hiagent.tools;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.LoadState;
import com.microsoft.playwright.options.WaitUntilState;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.io.File;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 海信SSO认证工具类
* 用于访问需要SSO认证的海信业务系统,自动完成登录并提取页面内容
*/
@Slf4j
@Component
public class HisenseSsoAuthTool {
// SSO登录页面URL
private static final String SSO_LOGIN_URL = "https://sso.hisense.com/login/";
// 用户名输入框选择器
private static final String USERNAME_INPUT_SELECTOR = "input[placeholder='账号名/海信邮箱/手机号']";
// 密码输入框选择器
private static final String PASSWORD_INPUT_SELECTOR = "input[placeholder='密码'][type='password']";
// 登录按钮选择器
private static final String LOGIN_BUTTON_SELECTOR = "#login-button";
// Playwright实例
private Playwright playwright;
// 浏览器实例
private Browser browser;
// 共享的浏览器上下文,用于保持登录状态
private BrowserContext sharedContext;
// 上次登录时间
private long lastLoginTime = 0;
// 登录状态有效期(毫秒),设置为30分钟
private static final long LOGIN_VALIDITY_PERIOD = 30 * 60 * 1000;
// SSO用户名(从配置文件读取)
@Value("${hisense.sso.username:}")
private String ssoUsername;
// SSO密码(从配置文件读取)
@Value("${hisense.sso.password:}")
private String ssoPassword;
// 存储目录路径
private static final String STORAGE_DIR = "storage";
/**
* 初始化Playwright和浏览器实例
*/
@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();
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);
}
}
/**
* 工具方法:获取海信业务系统的网页内容(自动处理SSO认证)
*
* @param businessSystemUrl 海信业务系统页面URL
* @return 页面内容(HTML文本)
*/
@Tool(description = "获取海信业务系统的网页内容(自动处理SSO认证)")
public String getHisenseBusinessSystemContent(String businessSystemUrl) {
log.info("开始获取海信业务系统内容,URL: {}", businessSystemUrl);
// 校验SSO凭证是否配置
if (ssoUsername == null || ssoUsername.isEmpty() || ssoPassword == null || ssoPassword.isEmpty()) {
String errorMsg = "SSO用户名或密码未配置,海信SSO工具不可用";
log.warn(errorMsg);
return errorMsg;
}
long startTime = System.currentTimeMillis();
// 参数校验
if (businessSystemUrl == null || businessSystemUrl.isEmpty()) {
String errorMsg = "业务系统URL不能为空";
log.error(errorMsg);
return errorMsg;
}
Page page = null;
try {
// 检查是否已有有效的登录会话
boolean sessionValid = isSessionLoggedIn() && validateSession(businessSystemUrl);
if (sessionValid) {
log.info("检测到有效会话,直接使用共享上下文");
page = sharedContext.newPage();
} else {
log.info("未检测到有效会话,使用共享上下文并重新登录");
page = sharedContext.newPage();
// 访问业务系统页面
log.info("正在访问业务系统页面: {}", businessSystemUrl);
page.navigate(businessSystemUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// 检查是否重定向到了SSO登录页面
String currentUrl = page.url();
log.info("当前页面URL: {}", currentUrl);
if (currentUrl.startsWith(SSO_LOGIN_URL)) {
log.info("检测到SSO登录页面,开始自动登录...");
// 执行SSO登录
performLoginAndUpdateStatus(page);
// 等待登录完成并重定向回业务系统
page.waitForURL(businessSystemUrl, new Page.WaitForURLOptions().setTimeout(10000));
log.info("登录成功,已重定向回业务系统页面");
} else {
// 即使没有跳转到登录页面,也更新登录时间
lastLoginTime = System.currentTimeMillis();
log.info("直接访问业务系统页面成功,无需SSO登录,更新会话时间");
}
}
// 如果页面尚未导航到业务系统URL,则导航到该URL
if (!page.url().equals(businessSystemUrl) && !page.url().startsWith(businessSystemUrl)) {
log.info("正在访问业务系统页面: {}", businessSystemUrl);
page.navigate(businessSystemUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
}
// 提取页面内容
String content = page.locator("body").innerText();
long endTime = System.currentTimeMillis();
log.info("成功获取业务系统页面内容,耗时: {} ms", endTime - startTime);
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());
}
}
}
}
/**
* 工具方法:处理海信请假审批
*
* @param approvalUrl 请假审批页面URL
* @param approvalOpinion 审批意见
* @return 处理结果
*/
@Tool(description = "处理海信请假审批、自驾车审批、调休审批")
public String processHisenseLeaveApproval(String approvalUrl, String approvalOpinion) {
log.info("开始处理海信请假审批,URL: {}", approvalUrl);
// 校验SSO凭证是否配置
if (ssoUsername == null || ssoUsername.isEmpty() || ssoPassword == null || ssoPassword.isEmpty()) {
String errorMsg = "SSO用户名或密码未配置,海信SSO工具不可用";
log.warn(errorMsg);
return errorMsg;
}
long startTime = System.currentTimeMillis();
// 参数校验
if (approvalUrl == null || approvalUrl.isEmpty()) {
String errorMsg = "审批URL不能为空";
log.error(errorMsg);
return errorMsg;
}
if (approvalOpinion == null || approvalOpinion.isEmpty()) {
String errorMsg = "审批意见不能为空";
log.error(errorMsg);
return errorMsg;
}
Page page = null;
try {
// 检查是否已有有效的登录会话
boolean sessionValid = isSessionLoggedIn() && validateSession(approvalUrl);
if (sessionValid) {
log.info("检测到有效会话,直接使用共享上下文");
page = sharedContext.newPage();
} else {
log.info("未检测到有效会话,使用共享上下文并重新登录");
page = sharedContext.newPage();
// 访问审批页面
log.info("正在访问审批页面: {}", approvalUrl);
page.navigate(approvalUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
// 检查是否重定向到了SSO登录页面
String currentUrl = page.url();
log.info("当前页面URL: {}", currentUrl);
if (currentUrl.startsWith(SSO_LOGIN_URL)) {
log.info("检测到SSO登录页面,开始自动登录...");
// 执行SSO登录
performLoginAndUpdateStatus(page);
// 等待登录完成并重定向回审批页面
page.waitForURL(approvalUrl, new Page.WaitForURLOptions().setTimeout(10000));
log.info("登录成功,已重定向回审批页面");
} else {
// 即使没有跳转到登录页面,也更新登录时间
lastLoginTime = System.currentTimeMillis();
log.info("直接访问审批页面成功,无需SSO登录,更新会话时间");
}
}
// 如果页面尚未导航到审批URL,则导航到该URL
if (!page.url().equals(approvalUrl) && !page.url().startsWith(approvalUrl)) {
log.info("正在访问审批页面: {}", approvalUrl);
page.navigate(approvalUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
}
// 执行审批操作
performApprovalOperation(page, approvalOpinion);
// 截图并保存
takeScreenshotAndSave(page, "leave_approval_success");
long endTime = System.currentTimeMillis();
log.info("请假审批处理完成,耗时: {} ms", endTime - startTime);
return "请假审批处理成功";
} catch (Exception e) {
long endTime = System.currentTimeMillis();
String errorMsg = "请假审批处理失败: " + e.getMessage();
log.error("请假审批处理失败,耗时: {} ms", endTime - startTime, e);
// 如果页面对象存在,截图保存错误页面
if (page != null) {
try {
takeScreenshotAndSave(page, "leave_approval_fail");
} catch (Exception screenshotException) {
log.warn("截图保存失败: {}", screenshotException.getMessage());
}
}
return errorMsg;
}
// 注意:这里不再释放页面资源,以保持会话状态供后续使用
/*finally {
// 释放页面资源
if (page != null) {
try {
page.close();
} catch (Exception e) {
log.warn("关闭页面时发生异常: {}", e.getMessage());
}
}
}*/
}
/**
* 检查当前会话是否已登录
*
* @return true表示已登录且会话有效,false表示未登录或会话已过期
*/
private boolean isSessionLoggedIn() {
// 检查是否存在共享上下文
if (sharedContext == null) {
return false;
}
// 检查登录是否过期
long currentTime = System.currentTimeMillis();
if (currentTime - lastLoginTime > LOGIN_VALIDITY_PERIOD) {
log.debug("会话已过期,上次登录时间: {},当前时间: {}", lastLoginTime, currentTime);
return false;
}
return true;
}
/**
* 验证当前会话是否仍然有效
* 通过访问一个需要登录的页面来验证会话状态
*
* @param testUrl 用于验证会话的测试页面URL
* @return true表示会话有效,false表示会话无效
*/
private boolean validateSession(String testUrl) {
if (!isSessionLoggedIn()) {
return false;
}
try {
Page page = sharedContext.newPage();
try {
page.navigate(testUrl, new Page.NavigateOptions().setWaitUntil(WaitUntilState.NETWORKIDLE));
String currentUrl = page.url();
// 如果重定向到了登录页面,说明会话已失效
if (currentUrl.startsWith(SSO_LOGIN_URL)) {
log.debug("会话验证失败,已重定向到登录页面");
return false;
}
log.debug("会话验证成功,当前页面URL: {}", currentUrl);
return true;
} finally {
page.close();
}
} catch (Exception e) {
log.warn("会话验证过程中发生异常: {}", e.getMessage());
return false;
}
}
/**
* 执行登录并更新登录状态
*
* @param page 当前页面对象
* @throws Exception 登录过程中的异常
*/
private void performLoginAndUpdateStatus(Page page) throws Exception {
log.info("开始执行SSO登录流程");
try {
// 填入用户名
log.debug("正在定位用户名输入框: {}", USERNAME_INPUT_SELECTOR);
Locator usernameInput = page.locator(USERNAME_INPUT_SELECTOR);
if (usernameInput.count() == 0) {
throw new RuntimeException("未找到用户名输入框");
}
usernameInput.fill(ssoUsername);
log.debug("用户名输入完成");
// 填入密码
log.debug("正在定位密码输入框: {}", PASSWORD_INPUT_SELECTOR);
Locator passwordInput = page.locator(PASSWORD_INPUT_SELECTOR);
if (passwordInput.count() == 0) {
throw new RuntimeException("未找到密码输入框");
}
passwordInput.fill(ssoPassword);
log.debug("密码输入完成");
// 点击登录按钮
log.debug("正在定位登录按钮: {}", LOGIN_BUTTON_SELECTOR);
Locator loginButton = page.locator(LOGIN_BUTTON_SELECTOR);
if (loginButton.count() == 0) {
throw new RuntimeException("未找到登录按钮");
}
loginButton.click();
log.info("登录按钮点击完成,等待登录响应");
// 等待页面开始跳转(表示登录请求已发送)
page.waitForLoadState(LoadState.NETWORKIDLE);
// 更新登录时间
lastLoginTime = System.currentTimeMillis();
log.info("SSO登录成功,登录时间已更新");
} catch (Exception e) {
log.error("SSO登录过程中发生异常", e);
throw new RuntimeException("SSO登录失败: " + e.getMessage(), e);
}
}
/**
* 执行审批操作
*
* @param page 当前页面对象
* @param approvalOpinion 审批意见
* @throws Exception 审批过程中的异常
*/
private void performApprovalOperation(Page page, String approvalOpinion) throws Exception {
log.info("开始执行审批操作");
try {
// 定位审批操作单选框
String operationRadioSelector = "input[type='radio'][alerttext=''][key='operationType'][name='oprGroup'][value='handler_pass:通过']";
log.debug("正在定位审批操作单选框: {}", operationRadioSelector);
Locator operationRadio = page.locator(operationRadioSelector);
if (operationRadio.count() == 0) {
throw new RuntimeException("未找到审批操作单选框");
}
operationRadio.click();
log.debug("审批操作单选框选择完成");
// 定位审批意见输入框并填入内容
String opinionTextareaSelector = "textarea[name='fdUsageContent'][class='process_review_content'][key='auditNode']";
log.debug("正在定位审批意见输入框: {}", opinionTextareaSelector);
Locator opinionTextarea = page.locator(opinionTextareaSelector);
if (opinionTextarea.count() == 0) {
throw new RuntimeException("未找到审批意见输入框");
}
opinionTextarea.fill(approvalOpinion);
log.debug("审批意见输入完成");
// 定位并点击提交按钮
String submitButtonSelector = "input[id='process_review_button'][class='process_review_button'][type='button'][value='提交']";
log.debug("正在定位提交按钮: {}", submitButtonSelector);
Locator submitButton = page.locator(submitButtonSelector);
if (submitButton.count() == 0) {
throw new RuntimeException("未找到提交按钮");
}
submitButton.click();
log.info("提交按钮点击完成");
// 等待提交完成
page.waitForLoadState(LoadState.NETWORKIDLE);
log.info("审批操作执行完成");
} catch (Exception e) {
log.error("审批操作过程中发生异常", e);
throw new RuntimeException("审批操作失败: " + e.getMessage(), e);
}
}
/**
* 截图并保存到存储目录
*
* @param page 当前页面对象
* @param fileName 文件名前缀
*/
private void takeScreenshotAndSave(Page page, String fileName) {
try {
// 确保存储目录存在
File storageDir = new File(STORAGE_DIR);
if (!storageDir.exists()) {
storageDir.mkdirs();
}
// 生成带时间戳的文件名
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
String fullFileName = String.format("%s_%s.png", fileName, timestamp);
String filePath = Paths.get(STORAGE_DIR, fullFileName).toString();
// 截图并保存
page.screenshot(new Page.ScreenshotOptions().setPath(Paths.get(filePath)));
log.info("截图已保存至: {}", filePath);
} catch (Exception e) {
log.error("截图保存失败: {}", e.getMessage(), e);
}
}
}
\ No newline at end of file
package pangea.hiagent.tools;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* OAuth2.0授权工具类
* 支持标准OAuth2.0认证流程,包括密码凭证流、令牌刷新和受保护资源访问
*/
@Component
public class OAuth2AuthorizationTool {
private static final Logger logger = LoggerFactory.getLogger(OAuth2AuthorizationTool.class);
/**
* 使用密码凭证流获取访问令牌
*
* @param tokenUrl 令牌端点URL
* @param clientId 客户端ID
* @param clientSecret 客户端密钥
* @param username 用户名
* @param password 密码
* @param scope 请求的作用域
* @return 认证结果或错误信息
*/
public String authorizeWithPasswordCredentials(String tokenUrl, String clientId, String clientSecret,
String username, String password, String scope) {
try {
logger.info("开始OAuth2.0密码凭证授权流程");
logger.debug("令牌URL: {}, 客户端ID: {}, 用户名: {}, 作用域: {}", tokenUrl, clientId, username, scope);
// 构造请求参数
StringBuilder requestBody = new StringBuilder();
requestBody.append("grant_type=password&");
requestBody.append("username=").append(username).append("&");
requestBody.append("password=").append(password).append("&");
requestBody.append("scope=").append(scope);
// 发送POST请求
String response = sendPostRequest(tokenUrl, clientId, clientSecret, requestBody.toString());
logger.info("OAuth2.0密码凭证授权完成");
return response;
} catch (Exception e) {
logger.error("OAuth2.0密码凭证授权失败", e);
return "错误: OAuth2.0密码凭证授权失败 - " + e.getMessage();
}
}
/**
* 刷新访问令牌
*
* @param tokenUrl 令牌端点URL
* @param clientId 客户端ID
* @param clientSecret 客户端密钥
* @param refreshToken 刷新令牌
* @return 刷新结果或错误信息
*/
public String refreshToken(String tokenUrl, String clientId, String clientSecret, String refreshToken) {
try {
logger.info("开始刷新OAuth2.0访问令牌");
logger.debug("令牌URL: {}, 客户端ID: {}, 刷新令牌: {}", tokenUrl, clientId, refreshToken);
// 构造请求参数
StringBuilder requestBody = new StringBuilder();
requestBody.append("grant_type=refresh_token&");
requestBody.append("refresh_token=").append(refreshToken);
// 发送POST请求
String response = sendPostRequest(tokenUrl, clientId, clientSecret, requestBody.toString());
logger.info("OAuth2.0访问令牌刷新完成");
return response;
} catch (Exception e) {
logger.error("刷新OAuth2.0访问令牌失败", e);
return "错误: 刷新OAuth2.0访问令牌失败 - " + e.getMessage();
}
}
/**
* 访问受保护资源
*
* @param resourceUrl 受保护资源URL
* @param tokenUrl 令牌端点URL(用于获取新令牌)
* @param clientId 客户端ID
* @return 资源内容或错误信息
*/
public String accessProtectedResource(String resourceUrl, String tokenUrl, String clientId) {
try {
logger.info("开始访问受保护资源");
logger.debug("资源URL: {}, 令牌URL: {}, 客户端ID: {}", resourceUrl, tokenUrl, clientId);
// 注意:这是一个简化的实现,实际应用中需要管理令牌的存储和使用
// 这里只是演示如何访问受保护资源
return "错误: 未找到有效的访问令牌。请先通过authorizeWithPasswordCredentials方法获取令牌。";
} catch (Exception e) {
logger.error("访问受保护资源失败", e);
return "错误: 访问受保护资源失败 - " + e.getMessage();
}
}
/**
* 发送POST请求到OAuth2.0令牌端点
*
* @param tokenUrl 令牌端点URL
* @param clientId 客户端ID
* @param clientSecret 客户端密钥
* @param requestBody 请求体内容
* @return 服务器响应
* @throws Exception 网络或IO异常
*/
private String sendPostRequest(String tokenUrl, String clientId, String clientSecret, String requestBody) throws Exception {
URL url = new URL(tokenUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 设置请求头
String authString = clientId + ":" + clientSecret;
String encodedAuth = Base64.getEncoder().encodeToString(authString.getBytes(StandardCharsets.UTF_8));
connection.setRequestMethod("POST");
connection.setRequestProperty("Authorization", "Basic " + encodedAuth);
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
connection.setRequestProperty("Accept", "application/json");
connection.setDoOutput(true);
// 发送请求体
try (OutputStream os = connection.getOutputStream()) {
byte[] input = requestBody.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
// 读取响应
int responseCode = connection.getResponseCode();
StringBuilder response = new StringBuilder();
try (BufferedReader br = new BufferedReader(
new InputStreamReader(responseCode >= 200 && responseCode < 300 ?
connection.getInputStream() : connection.getErrorStream(),
StandardCharsets.UTF_8))) {
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
}
logger.debug("HTTP响应码: {}, 响应内容: {}", responseCode, response.toString());
if (responseCode >= 200 && responseCode < 300) {
return response.toString();
} else {
return "错误: HTTP " + responseCode + " - " + response.toString();
}
}
}
\ No newline at end of file
package pangea.hiagent.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
/**
* 订单查询工具
* 用于查询客户订单信息
*/
@Slf4j
@Component
public class OrderQueryTool {
/**
* 根据订单号查询订单信息
* @param orderId 订单号
* @return 订单信息
*/
public String queryOrderByOrderId(String orderId) {
log.info("查询订单信息,订单号: {}", orderId);
try {
// 模拟订单查询逻辑
// 在实际应用中,这里应该连接到订单数据库进行查询
Map<String, Object> orderInfo = getOrderInfoFromDatabase(orderId);
if (orderInfo == null) {
return "未找到订单号为 " + orderId + " 的订单信息";
}
// 格式化订单信息
StringBuilder result = new StringBuilder();
result.append("订单信息查询结果:\n");
result.append("订单号: ").append(orderInfo.get("orderId")).append("\n");
result.append("客户姓名: ").append(orderInfo.get("customerName")).append("\n");
result.append("商品名称: ").append(orderInfo.get("productName")).append("\n");
result.append("订单金额: ").append(orderInfo.get("amount")).append("\n");
result.append("下单时间: ").append(orderInfo.get("orderTime")).append("\n");
result.append("订单状态: ").append(orderInfo.get("status")).append("\n");
return result.toString();
} catch (Exception e) {
log.error("查询订单信息失败,订单号: {}", orderId, e);
return "查询订单信息时发生错误: " + e.getMessage();
}
}
/**
* 根据客户手机号查询订单列表
* @param phoneNumber 客户手机号
* @return 订单列表
*/
public String queryOrdersByPhoneNumber(String phoneNumber) {
log.info("查询客户订单列表,手机号: {}", phoneNumber);
try {
// 模拟订单查询逻辑
List<Map<String, Object>> orders = getOrdersFromDatabaseByPhone(phoneNumber);
if (orders == null || orders.isEmpty()) {
return "未找到手机号为 " + phoneNumber + " 的客户订单信息";
}
// 格式化订单列表
StringBuilder result = new StringBuilder();
result.append("客户订单列表查询结果:\n");
for (int i = 0; i < orders.size(); i++) {
Map<String, Object> order = orders.get(i);
result.append("订单 ").append(i + 1).append(":\n");
result.append(" 订单号: ").append(order.get("orderId")).append("\n");
result.append(" 商品名称: ").append(order.get("productName")).append("\n");
result.append(" 订单金额: ").append(order.get("amount")).append("\n");
result.append(" 下单时间: ").append(order.get("orderTime")).append("\n");
result.append(" 订单状态: ").append(order.get("status")).append("\n\n");
}
return result.toString();
} catch (Exception e) {
log.error("查询客户订单列表失败,手机号: {}", phoneNumber, e);
return "查询客户订单列表时发生错误: " + e.getMessage();
}
}
/**
* 模拟从数据库获取订单信息
* @param orderId 订单号
* @return 订单信息
*/
private Map<String, Object> getOrderInfoFromDatabase(String orderId) {
// 模拟数据库查询
// 在实际应用中,这里应该连接真实的数据库
// 模拟一些订单数据
Map<String, Object> order1 = new HashMap<>();
order1.put("orderId", "ORD20240101001");
order1.put("customerName", "张三");
order1.put("phoneNumber", "13800138001");
order1.put("productName", "智能手表");
order1.put("amount", "1299.00");
order1.put("orderTime", "2024-01-01 10:30:00");
order1.put("status", "已发货");
Map<String, Object> order2 = new HashMap<>();
order2.put("orderId", "ORD20240102002");
order2.put("customerName", "李四");
order2.put("phoneNumber", "13800138002");
order2.put("productName", "无线耳机");
order2.put("amount", "399.00");
order2.put("orderTime", "2024-01-02 15:45:00");
order2.put("status", "已签收");
// 根据订单号返回相应订单
if ("ORD20240101001".equals(orderId)) {
return order1;
} else if ("ORD20240102002".equals(orderId)) {
return order2;
}
return null;
}
/**
* 模拟从数据库根据手机号获取订单列表
* @param phoneNumber 客户手机号
* @return 订单列表
*/
private List<Map<String, Object>> getOrdersFromDatabaseByPhone(String phoneNumber) {
// 模拟数据库查询
// 在实际应用中,这里应该连接真实的数据库
List<Map<String, Object>> orders = new ArrayList<>();
// 模拟一些订单数据
if ("13800138001".equals(phoneNumber)) {
Map<String, Object> order1 = new HashMap<>();
order1.put("orderId", "ORD20240101001");
order1.put("customerName", "张三");
order1.put("productName", "智能手表");
order1.put("amount", "1299.00");
order1.put("orderTime", "2024-01-01 10:30:00");
order1.put("status", "已发货");
orders.add(order1);
Map<String, Object> order2 = new HashMap<>();
order2.put("orderId", "ORD20240103003");
order2.put("customerName", "张三");
order2.put("productName", "手机壳");
order2.put("amount", "29.90");
order2.put("orderTime", "2024-01-03 09:15:00");
order2.put("status", "已付款");
orders.add(order2);
} else if ("13800138002".equals(phoneNumber)) {
Map<String, Object> order = new HashMap<>();
order.put("orderId", "ORD20240102002");
order.put("customerName", "李四");
order.put("productName", "无线耳机");
order.put("amount", "399.00");
order.put("orderTime", "2024-01-02 15:45:00");
order.put("status", "已签收");
orders.add(order);
}
return orders;
}
/**
* 获取工具名称
* @return 工具名称
*/
public String getName() {
return "orderQuery";
}
/**
* 获取工具描述
* @return 工具描述
*/
public String getDescription() {
return "订单查询工具,可用于查询订单信息和客户订单列表";
}
}
\ No newline at end of file
// package pangea.hiagent.tools;
// import com.microsoft.playwright.*;
// import com.microsoft.playwright.options.LoadState;
// import com.microsoft.playwright.options.WaitUntilState;
// import lombok.extern.slf4j.Slf4j;
// import org.springframework.ai.tool.annotation.Tool;
// import org.springframework.beans.BeansException;
// import org.springframework.context.ApplicationContext;
// import org.springframework.context.ApplicationContextAware;
// import org.springframework.stereotype.Component;
// import pangea.hiagent.workpanel.IWorkPanelDataCollector;
// import jakarta.annotation.PostConstruct;
// import jakarta.annotation.PreDestroy;
// import java.util.Base64;
// import java.util.HashMap;
// import java.util.List;
// import java.nio.file.Files;
// import java.nio.file.Path;
// /**
// * Playwright网页自动化工具类
// * 提供基于Playwright的网页内容抓取、交互操作、截图等功能
// */
// @Slf4j
// @Component
// public class PlaywrightWebTools implements ApplicationContextAware {
// // Spring应用上下文引用
// private static ApplicationContext applicationContext;
// // Playwright实例
// private Playwright playwright;
// // 浏览器实例
// private Browser browser;
// /**
// * 初始化Playwright和浏览器实例
// */
// @PostConstruct
// public void initialize() {
// try {
// log.info("正在初始化Playwright...");
// this.playwright = Playwright.create();
// // 使用chromium浏览器,无头模式(headless=true),适合服务器运行
// this.browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(true));
// log.info("Playwright初始化成功");
// } catch (Exception e) {
// log.error("Playwright初始化失败: ", e);
// }
// }
// /**
// * 销毁Playwright资源
// */
// @PreDestroy
// public void destroy() {
// try {
// if (browser != null) {
// browser.close();
// log.info("浏览器实例已关闭");
// }
// if (playwright != null) {
// playwright.close();
// log.info("Playwright实例已关闭");
// }
// } catch (Exception e) {
// log.error("Playwright资源释放失败: ", e);
// }
// }
// /**
// * 设置ApplicationContext引用
// */
// @Override
// public void setApplicationContext(ApplicationContext context) throws BeansException {
// PlaywrightWebTools.applicationContext = context;
// }
// /**
// * 工具方法:获取指定URL的网页全部文本内容(去除HTML标签)
// * @param url 网页地址(必填)
// * @return 网页纯文本内容
// */
// @Tool(description = "获取网页纯文本内容")
// public String getWebPageText(String url) {
// log.debug("获取网页纯文本内容: {}", url);
// // 记录工具调用开始
// HashMap<String, Object> input = new HashMap<>();
// input.put("url", url);
// recordToWorkPanel("getWebPageText", input, null, null);
// long startTime = System.currentTimeMillis();
// // 空值校验,增强健壮性
// if (url == null || url.isEmpty()) {
// String errorMsg = "URL不能为空,请提供有效的网页地址";
// long endTime = System.currentTimeMillis();
// recordToWorkPanel("getWebPageText", null, errorMsg, "failure", endTime - startTime);
// return errorMsg;
// }
// // 创建浏览器上下文和页面
// try (BrowserContext context = browser.newContext();
// Page page = context.newPage()) {
// // 导航到指定URL,等待页面加载完成
// page.navigate(url, new Page.NavigateOptions().setWaitUntil(WaitUntilState.LOAD));
// // 获取页面全部文本(Playwright的innerText会自动去除HTML标签,保留文本结构)
// String content = page.locator("body").innerText();
// long endTime = System.currentTimeMillis();
// log.debug("成功获取网页文本内容,长度: {} 字符", content.length());
// // 记录工具调用完成
// recordToWorkPanel("getWebPageText", null, content, "success", endTime - startTime);
// return content;
// } catch (Exception e) {
// long endTime = System.currentTimeMillis();
// String errorMsg = "获取网页内容失败:" + e.getMessage();
// log.error("获取网页内容失败: ", e);
// // 记录工具调用错误
// recordToWorkPanel("getWebPageText", null, errorMsg, "error", endTime - startTime);
// return errorMsg;
// }
// }
// /**
// * 工具方法:获取网页中指定CSS选择器的元素内容
// * @param url 网页地址
// * @param cssSelector CSS选择器(如#title、.content)
// * @return 元素的文本内容
// */
// @Tool(description = "获取网页指定元素的内容")
// public String getWebElementText(String url, String cssSelector) {
// log.debug("获取网页指定元素内容: {}, 选择器: {}", url, cssSelector);
// // 记录工具调用开始
// HashMap<String, Object> input = new HashMap<>();
// input.put("url", url);
// input.put("cssSelector", cssSelector);
// recordToWorkPanel("getWebElementText", input, null, null);
// long startTime = System.currentTimeMillis();
// if (url == null || url.isEmpty() || cssSelector == null || cssSelector.isEmpty()) {
// String errorMsg = "URL和CSS选择器不能为空";
// long endTime = System.currentTimeMillis();
// recordToWorkPanel("getWebElementText", null, errorMsg, "failure", endTime - startTime);
// return errorMsg;
// }
// try (BrowserContext context = browser.newContext();
// Page page = context.newPage()) {
// page.navigate(url, new Page.NavigateOptions().setWaitUntil(WaitUntilState.LOAD));
// Locator locator = page.locator(cssSelector);
// // 检查元素是否存在
// if (locator.count() == 0) {
// String errorMsg = "未找到匹配的元素:" + cssSelector;
// long endTime = System.currentTimeMillis();
// recordToWorkPanel("getWebElementText", null, errorMsg, "failure", endTime - startTime);
// return errorMsg;
// }
// String content = locator.innerText();
// long endTime = System.currentTimeMillis();
// log.debug("成功获取元素文本内容: {}", content);
// // 记录工具调用完成
// recordToWorkPanel("getWebElementText", null, content, "success", endTime - startTime);
// return content;
// } catch (Exception e) {
// long endTime = System.currentTimeMillis();
// String errorMsg = "获取元素内容失败:" + e.getMessage();
// log.error("获取元素内容失败: ", e);
// // 记录工具调用错误
// recordToWorkPanel("getWebElementText", null, errorMsg, "error", endTime - startTime);
// return errorMsg;
// }
// }
// /**
// * 工具方法:在网页指定输入框中输入文本
// * @param url 网页地址
// * @param inputSelector 输入框的CSS选择器(如#search-input)
// * @param text 要输入的文本
// * @return 操作结果
// */
// @Tool(description = "在网页输入框中输入文本")
// public String inputTextToWebElement(String url, String inputSelector, String text) {
// log.debug("在网页输入框中输入文本: {}, 选择器: {}, 文本: {}", url, inputSelector, text);
// // 记录工具调用开始
// HashMap<String, Object> input = new HashMap<>();
// input.put("url", url);
// input.put("inputSelector", inputSelector);
// input.put("text", text);
// recordToWorkPanel("inputTextToWebElement", input, null, null);
// long startTime = System.currentTimeMillis();
// if (url == null || url.isEmpty() || inputSelector == null || inputSelector.isEmpty() || text == null) {
// String errorMsg = "URL、输入框选择器和输入文本不能为空";
// long endTime = System.currentTimeMillis();
// recordToWorkPanel("inputTextToWebElement", null, errorMsg, "failure", endTime - startTime);
// return errorMsg;
// }
// try (BrowserContext context = browser.newContext();
// Page page = context.newPage()) {
// page.navigate(url, new Page.NavigateOptions().setWaitUntil(WaitUntilState.LOAD));
// Locator inputLocator = page.locator(inputSelector);
// if (inputLocator.count() == 0) {
// String errorMsg = "未找到输入框:" + inputSelector;
// long endTime = System.currentTimeMillis();
// recordToWorkPanel("inputTextToWebElement", null, errorMsg, "failure", endTime - startTime);
// return errorMsg;
// }
// // 聚焦并输入文本
// inputLocator.click();
// inputLocator.fill(text);
// String result = "文本输入成功:" + text;
// long endTime = System.currentTimeMillis();
// log.debug("文本输入成功: {}", text);
// // 记录工具调用完成
// recordToWorkPanel("inputTextToWebElement", null, result, "success", endTime - startTime);
// return result;
// } catch (Exception e) {
// long endTime = System.currentTimeMillis();
// String errorMsg = "输入文本失败:" + e.getMessage();
// log.error("输入文本失败: ", e);
// // 记录工具调用错误
// recordToWorkPanel("inputTextToWebElement", null, errorMsg, "error", endTime - startTime);
// return errorMsg;
// }
// }
// /**
// * 工具方法:点击网页中的指定元素(按钮、链接等)
// * @param url 网页地址
// * @param elementSelector 元素的CSS选择器
// * @return 操作结果
// */
// @Tool(description = "点击网页指定元素")
// public String clickWebElement(String url, String elementSelector) {
// log.debug("点击网页指定元素: {}, 选择器: {}", url, elementSelector);
// // 记录工具调用开始
// HashMap<String, Object> input = new HashMap<>();
// input.put("url", url);
// input.put("elementSelector", elementSelector);
// recordToWorkPanel("clickWebElement", input, null, null);
// long startTime = System.currentTimeMillis();
// if (url == null || url.isEmpty() || elementSelector == null || elementSelector.isEmpty()) {
// String errorMsg = "URL和元素选择器不能为空";
// long endTime = System.currentTimeMillis();
// recordToWorkPanel("clickWebElement", null, errorMsg, "failure", endTime - startTime);
// return errorMsg;
// }
// try (BrowserContext context = browser.newContext();
// Page page = context.newPage()) {
// page.navigate(url, new Page.NavigateOptions().setWaitUntil(WaitUntilState.LOAD));
// Locator locator = page.locator(elementSelector);
// if (locator.count() == 0) {
// String errorMsg = "未找到要点击的元素:" + elementSelector;
// long endTime = System.currentTimeMillis();
// recordToWorkPanel("clickWebElement", null, errorMsg, "failure", endTime - startTime);
// return errorMsg;
// }
// // 点击元素,等待导航完成(如果是链接/提交按钮)
// locator.click();
// page.waitForLoadState(LoadState.LOAD);
// String result = "元素点击成功,当前页面URL:" + page.url();
// long endTime = System.currentTimeMillis();
// log.debug("元素点击成功,当前页面URL: {}", page.url());
// // 记录工具调用完成
// recordToWorkPanel("clickWebElement", null, result, "success", endTime - startTime);
// return result;
// } catch (Exception e) {
// long endTime = System.currentTimeMillis();
// String errorMsg = "点击元素失败:" + e.getMessage();
// log.error("点击元素失败: ", e);
// // 记录工具调用错误
// recordToWorkPanel("clickWebElement", null, errorMsg, "error", endTime - startTime);
// return errorMsg;
// }
// }
// /**
// * 工具方法:获取网页全屏截图并保存到指定路径
// * @param url 网页地址
// * @param savePath 截图保存路径(如D:/screenshots/page.png)
// * @return 操作结果
// */
// @Tool(description = "获取网页全屏截图")
// public String captureWebPageScreenshot(String url, String savePath) {
// log.debug("获取网页全屏截图: {}, 保存路径: {}", url, savePath);
// // 记录工具调用开始
// HashMap<String, Object> input = new HashMap<>();
// input.put("url", url);
// input.put("savePath", savePath);
// recordToWorkPanel("captureWebPageScreenshot", input, null, null);
// long startTime = System.currentTimeMillis();
// if (url == null || url.isEmpty() || savePath == null || savePath.isEmpty()) {
// String errorMsg = "URL和保存路径不能为空";
// long endTime = System.currentTimeMillis();
// recordToWorkPanel("captureWebPageScreenshot", null, errorMsg, "failure", endTime - startTime);
// return errorMsg;
// }
// try (BrowserContext context = browser.newContext();
// Page page = context.newPage()) {
// page.navigate(url, new Page.NavigateOptions().setWaitUntil(WaitUntilState.LOAD));
// // 截取全屏并保存
// page.screenshot(new Page.ScreenshotOptions().setPath(java.nio.file.Paths.get(savePath)).setFullPage(true));
// // 将截图文件转换为Base64编码,用于前端预览显示
// String base64Image = "";
// try {
// Path imagePath = java.nio.file.Paths.get(savePath);
// byte[] imageBytes = Files.readAllBytes(imagePath);
// base64Image = Base64.getEncoder().encodeToString(imageBytes);
// } catch (Exception e) {
// log.warn("截图Base64编码转换失败: {}", e.getMessage());
// }
// String result = "截图成功,保存路径:" + savePath;
// long endTime = System.currentTimeMillis();
// log.debug("截图成功,保存路径: {}", savePath);
// // 记录工具调用完成,并推送Base64截图到前端预览区域
// HashMap<String, Object> output = new HashMap<>();
// output.put("message", result);
// if (!base64Image.isEmpty()) {
// output.put("previewImage", "data:image/png;base64," + base64Image);
// }
// recordToWorkPanel("captureWebPageScreenshot", null, output, "success", endTime - startTime);
// return result;
// } catch (Exception e) {
// long endTime = System.currentTimeMillis();
// String errorMsg = "截图失败:" + e.getMessage();
// log.error("截图失败: ", e);
// // 记录工具调用错误
// recordToWorkPanel("captureWebPageScreenshot", null, errorMsg, "error", endTime - startTime);
// return errorMsg;
// }
// }
// /**
// * 工具方法:提取网页中的所有超链接(a标签的href属性)
// * @param url 网页地址
// * @return 链接列表,以逗号分隔
// */
// @Tool(description = "提取网页中的所有链接")
// public String extractAllLinksFromPage(String url) {
// log.debug("提取网页中的所有链接: {}", url);
// // 记录工具调用开始
// HashMap<String, Object> input = new HashMap<>();
// input.put("url", url);
// recordToWorkPanel("extractAllLinksFromPage", input, null, null);
// long startTime = System.currentTimeMillis();
// if (url == null || url.isEmpty()) {
// String errorMsg = "URL不能为空";
// long endTime = System.currentTimeMillis();
// recordToWorkPanel("extractAllLinksFromPage", null, errorMsg, "failure", endTime - startTime);
// return errorMsg;
// }
// try (BrowserContext context = browser.newContext();
// Page page = context.newPage()) {
// page.navigate(url, new Page.NavigateOptions().setWaitUntil(WaitUntilState.LOAD));
// // 获取所有a标签的href属性
// Object result = page.locator("a").evaluateAll("elements => elements.map(el => el.href)");
// List<String> links = (List<String>) result;
// String resultStr = links.isEmpty() ? "未找到任何链接" : String.join(", ", links);
// long endTime = System.currentTimeMillis();
// log.debug("成功提取链接,数量: {}", links.size());
// // 记录工具调用完成
// recordToWorkPanel("extractAllLinksFromPage", null, resultStr, "success", endTime - startTime);
// return resultStr;
// } catch (Exception e) {
// long endTime = System.currentTimeMillis();
// String errorMsg = "提取链接失败:" + e.getMessage();
// log.error("提取链接失败: ", e);
// // 记录工具调用错误
// recordToWorkPanel("extractAllLinksFromPage", null, errorMsg, "error", endTime - startTime);
// return errorMsg;
// }
// }
// /**
// * 工具方法:在网页中执行自定义JavaScript代码,获取返回结果
// * @param url 网页地址
// * @param jsCode 要执行的JS代码(如"document.title"、"window.innerWidth")
// * @return JS执行结果
// */
// @Tool(description = "执行网页JavaScript代码")
// public String executeJavaScriptOnPage(String url, String jsCode) {
// log.debug("执行网页JavaScript代码: {}, JS代码: {}", url, jsCode);
// // 记录工具调用开始
// HashMap<String, Object> input = new HashMap<>();
// input.put("url", url);
// input.put("jsCode", jsCode);
// recordToWorkPanel("executeJavaScriptOnPage", input, null, null);
// long startTime = System.currentTimeMillis();
// if (url == null || url.isEmpty() || jsCode == null || jsCode.isEmpty()) {
// String errorMsg = "URL和JS代码不能为空";
// long endTime = System.currentTimeMillis();
// recordToWorkPanel("executeJavaScriptOnPage", null, errorMsg, "failure", endTime - startTime);
// return errorMsg;
// }
// try (BrowserContext context = browser.newContext();
// Page page = context.newPage()) {
// page.navigate(url, new Page.NavigateOptions().setWaitUntil(WaitUntilState.LOAD));
// // 执行JS代码并获取结果
// Object result = page.evaluate(jsCode);
// String resultStr = "JS执行结果:" + (result == null ? "null" : result.toString());
// long endTime = System.currentTimeMillis();
// log.debug("JS执行成功,结果: {}", resultStr);
// // 记录工具调用完成
// recordToWorkPanel("executeJavaScriptOnPage", null, resultStr, "success", endTime - startTime);
// return resultStr;
// } catch (Exception e) {
// long endTime = System.currentTimeMillis();
// String errorMsg = "执行JS失败:" + e.getMessage();
// log.error("执行JS失败: ", e);
// // 记录工具调用错误
// recordToWorkPanel("executeJavaScriptOnPage", null, errorMsg, "error", endTime - startTime);
// return errorMsg;
// }
// }
// /**
// * 记录到工作面板(不带执行时间)
// */
// private void recordToWorkPanel(String toolAction, Object input, Object output, String status) {
// recordToWorkPanel(toolAction, input, output, status, null);
// }
// /**
// * 记录到工作面板(带执行时间)
// */
// private void recordToWorkPanel(String toolAction, Object input, Object output, String status, Long executionTime) {
// try {
// // 首先尝试从Spring容器获取collector
// IWorkPanelDataCollector collector = null;
// if (applicationContext != null) {
// try {
// collector = applicationContext.getBean(IWorkPanelDataCollector.class);
// } catch (Exception e) {
// log.debug("通过Spring容器获取WorkPanelDataCollector失败: {}", e.getMessage());
// }
// }
// if (collector != null) {
// // 记录工具调用
// if (input != null && status == null) {
// // 调用开始
// collector.recordToolCallStart("Playwright网页工具", toolAction, input);
// } else if (status != null) {
// // 调用结束
// collector.recordToolCallComplete("Playwright网页工具", output, status, executionTime);
// }
// } else {
// log.debug("无法记录到工作面板:collector为null");
// }
// } catch (Exception e) {
// log.debug("记录工作面板失败", e);
// }
// }
// }
\ No newline at end of file
# 百度搜索工具使用说明
## 功能概述
BaiduSearchTool 是一个集成到Spring AI智能体中的工具类,提供了在百度搜索引擎上进行关键词搜索的功能。该工具支持:
1. 自动处理分页逻辑,抓取前5页搜索结果
2. 提取每页中所有搜索结果项的标题、链接和摘要信息
3. 汇总整理数据,返回统一格式的结果列表
4. 处理反爬虫机制,设置合理的请求间隔
## 优化特性
### 多重用户代理模拟
工具内置了多种现代浏览器的User-Agent字符串,每次请求都会随机选择一个,模拟不同浏览器的访问行为,降低被识别为爬虫的概率。
### 智能请求频率控制
采用随机延迟机制,请求间隔在2-5秒之间随机变化,模拟人类用户的自然浏览行为,避免固定频率触发反爬虫机制。
### Cookie跟踪机制
工具能够跟踪和复用百度返回的Cookie信息,维持会话状态,使请求看起来更像是真实用户的连续操作。
### 增强的HTTP请求头
设置了完整的HTTP请求头信息,包括Accept、Accept-Language、Accept-Encoding等字段,模拟真实浏览器的请求行为。
### 改进的页面解析逻辑
采用了多层次的选择器策略和正则表达式备用方案,能够适应百度搜索页面结构的变化,提高结果提取的准确性和稳定性。
### 智能去重机制
自动去除重复的搜索结果,确保返回的结果列表中每个URL只出现一次。
### 安全验证检测机制
工具具备百度安全验证页面检测功能,当遇到百度安全验证页面时会:
- 自动停止当前搜索操作
- 记录相关日志信息
- 返回已收集到的搜索结果
- 不再继续后续页面的抓取
此机制可以有效避免在遇到反爬虫验证页面时继续无效请求,节省资源并避免触发更严格的反爬虫措施。
## 使用方法
### 在AI对话中使用
在与AI助手的对话中,可以直接要求执行百度搜索:
```
用户: 搜索关于人工智能的最新发展
AI助手: 正在为您搜索相关信息...
```
### 在代码中使用
```java
@Autowired
private BaiduSearchTool baiduSearchTool;
// 执行搜索
List<BaiduSearchTool.BaiduSearchResult> results = baiduSearchTool.searchOnBaidu("人工智能最新发展");
// 处理结果
for (BaiduSearchTool.BaiduSearchResult result : results) {
System.out.println("标题: " + result.getTitle());
System.out.println("链接: " + result.getUrl());
System.out.println("摘要: " + result.getSummary());
System.out.println("---");
}
```
## 数据结构
### BaiduSearchResult
搜索结果数据类,包含以下字段:
- `title`: 搜索结果标题
- `url`: 搜索结果链接
- `summary`: 搜索结果摘要
## 注意事项
1. 为了防止被百度反爬虫机制拦截,工具设置了随机请求间隔,请耐心等待搜索完成
2. 搜索结果的数量和质量受网络状况、百度算法调整等因素影响
3. 当遇到百度安全验证页面时,工具会自动停止搜索并返回已获取的结果
4. 工具会记录搜索过程中的关键事件到工作面板,便于调试和监控
\ No newline at end of file
package pangea.hiagent.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.HashMap;
/**
* 退款处理工具
* 用于处理客户退款申请
*/
@Slf4j
@Component
public class RefundProcessingTool {
/**
* 提交退款申请
* @param orderId 订单号
* @param refundReason 退款原因
* @param refundAmount 退款金额
* @return 退款申请结果
*/
public String submitRefundApplication(String orderId, String refundReason, Double refundAmount) {
log.info("提交退款申请,订单号: {}, 退款原因: {}, 退款金额: {}", orderId, refundReason, refundAmount);
try {
// 验证订单是否存在
Map<String, Object> orderInfo = getOrderInfoFromDatabase(orderId);
if (orderInfo == null) {
return "错误:未找到订单号为 " + orderId + " 的订单信息,无法提交退款申请";
}
// 验证退款金额
Double orderAmount = Double.valueOf(orderInfo.get("amount").toString());
if (refundAmount > orderAmount) {
return "错误:退款金额不能大于订单金额(订单金额:" + orderAmount + "元)";
}
// 生成退款申请单号
String refundId = "REF" + System.currentTimeMillis();
// 模拟提交退款申请到财务系统
boolean success = submitRefundToFinancialSystem(refundId, orderId, refundReason, refundAmount);
if (success) {
// 更新订单状态
updateOrderStatus(orderId, "退款申请中");
StringBuilder result = new StringBuilder();
result.append("退款申请提交成功\n");
result.append("退款申请单号: ").append(refundId).append("\n");
result.append("订单号: ").append(orderId).append("\n");
result.append("退款金额: ").append(refundAmount).append("元\n");
result.append("退款原因: ").append(refundReason).append("\n");
result.append("状态: 退款申请已提交,等待财务处理\n");
result.append("预计处理时间: 1-3个工作日\n");
return result.toString();
} else {
return "退款申请提交失败,请稍后重试或联系人工客服";
}
} catch (Exception e) {
log.error("提交退款申请失败,订单号: {}", orderId, e);
return "提交退款申请时发生错误: " + e.getMessage();
}
}
/**
* 查询退款申请状态
* @param refundId 退款申请单号
* @return 退款申请状态
*/
public String queryRefundStatus(String refundId) {
log.info("查询退款申请状态,退款单号: {}", refundId);
try {
// 模拟从数据库查询退款申请状态
Map<String, Object> refundInfo = getRefundInfoFromDatabase(refundId);
if (refundInfo == null) {
return "未找到退款申请单号为 " + refundId + " 的退款申请信息";
}
StringBuilder result = new StringBuilder();
result.append("退款申请状态查询结果:\n");
result.append("退款申请单号: ").append(refundInfo.get("refundId")).append("\n");
result.append("订单号: ").append(refundInfo.get("orderId")).append("\n");
result.append("退款金额: ").append(refundInfo.get("refundAmount")).append("元\n");
result.append("申请时间: ").append(refundInfo.get("applyTime")).append("\n");
result.append("退款原因: ").append(refundInfo.get("refundReason")).append("\n");
result.append("当前状态: ").append(refundInfo.get("status")).append("\n");
if ("已完成".equals(refundInfo.get("status"))) {
result.append("完成时间: ").append(refundInfo.get("completeTime")).append("\n");
}
return result.toString();
} catch (Exception e) {
log.error("查询退款申请状态失败,退款单号: {}", refundId, e);
return "查询退款申请状态时发生错误: " + e.getMessage();
}
}
/**
* 模拟从数据库获取订单信息
* @param orderId 订单号
* @return 订单信息
*/
private Map<String, Object> getOrderInfoFromDatabase(String orderId) {
// 模拟数据库查询
// 在实际应用中,这里应该连接真实的订单数据库
// 模拟一些订单数据
Map<String, Object> order1 = new HashMap<>();
order1.put("orderId", "ORD20240101001");
order1.put("customerName", "张三");
order1.put("phoneNumber", "13800138001");
order1.put("productName", "智能手表");
order1.put("amount", "1299.00");
order1.put("orderTime", "2024-01-01 10:30:00");
order1.put("status", "已发货");
Map<String, Object> order2 = new HashMap<>();
order2.put("orderId", "ORD20240102002");
order2.put("customerName", "李四");
order2.put("phoneNumber", "13800138002");
order2.put("productName", "无线耳机");
order2.put("amount", "399.00");
order2.put("orderTime", "2024-01-02 15:45:00");
order2.put("status", "已签收");
// 根据订单号返回相应订单
if ("ORD20240101001".equals(orderId)) {
return order1;
} else if ("ORD20240102002".equals(orderId)) {
return order2;
}
return null;
}
/**
* 模拟提交退款申请到财务系统
* @param refundId 退款单号
* @param orderId 订单号
* @param refundReason 退款原因
* @param refundAmount 退款金额
* @return 是否提交成功
*/
private boolean submitRefundToFinancialSystem(String refundId, String orderId, String refundReason, Double refundAmount) {
// 模拟提交到财务系统
// 在实际应用中,这里应该调用真实的财务系统API
log.info("模拟提交退款申请到财务系统,退款单号: {}, 订单号: {}, 退款原因: {}, 退款金额: {}",
refundId, orderId, refundReason, refundAmount);
// 模拟提交成功
return true;
}
/**
* 模拟更新订单状态
* @param orderId 订单号
* @param status 新状态
*/
private void updateOrderStatus(String orderId, String status) {
// 模拟更新订单状态
// 在实际应用中,这里应该更新真实的订单数据库
log.info("模拟更新订单状态,订单号: {}, 新状态: {}", orderId, status);
}
/**
* 模拟从数据库获取退款申请信息
* @param refundId 退款单号
* @return 退款申请信息
*/
private Map<String, Object> getRefundInfoFromDatabase(String refundId) {
// 模拟数据库查询
// 在实际应用中,这里应该连接真实的退款申请数据库
// 模拟一些退款申请数据
if ("REF123456789".equals(refundId)) {
Map<String, Object> refundInfo = new HashMap<>();
refundInfo.put("refundId", "REF123456789");
refundInfo.put("orderId", "ORD20240101001");
refundInfo.put("refundAmount", "1299.00");
refundInfo.put("applyTime", "2024-01-05 14:20:00");
refundInfo.put("refundReason", "商品质量问题");
refundInfo.put("status", "处理中");
return refundInfo;
} else if ("REF987654321".equals(refundId)) {
Map<String, Object> refundInfo = new HashMap<>();
refundInfo.put("refundId", "REF987654321");
refundInfo.put("orderId", "ORD20240102002");
refundInfo.put("refundAmount", "399.00");
refundInfo.put("applyTime", "2024-01-04 09:15:00");
refundInfo.put("refundReason", "不喜欢");
refundInfo.put("status", "已完成");
refundInfo.put("completeTime", "2024-01-06 16:30:00");
return refundInfo;
}
return null;
}
/**
* 获取工具名称
* @return 工具名称
*/
public String getName() {
return "refundProcessing";
}
/**
* 获取工具描述
* @return 工具描述
*/
public String getDescription() {
return "退款处理工具,可用于提交退款申请和查询退款状态";
}
}
\ No newline at end of file
package pangea.hiagent.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* 统计计算工具
* 用于执行各种统计分析计算
*/
@Slf4j
@Component
public class StatisticalCalculationTool {
public StatisticalCalculationTool() {
// 默认构造器
}
/**
* 计算数据的基本统计信息
* @param data 数据数组
* @return 统计结果
*/
@Tool(description = "计算数据的基本统计信息,包括均值、中位数、标准差等")
public String calculateBasicStatistics(double[] data) {
log.debug("开始计算基本统计信息,数据点数量: {}", data != null ? data.length : 0);
try {
if (data == null || data.length == 0) {
log.warn("数据不能为空");
return "错误:数据不能为空";
}
// 计算基本统计信息
double sum = 0;
double min = data[0];
double max = data[0];
for (double value : data) {
sum += value;
if (value < min) min = value;
if (value > max) max = value;
}
double mean = sum / data.length;
// 计算方差和标准差
double varianceSum = 0;
for (double value : data) {
varianceSum += Math.pow(value - mean, 2);
}
double variance = varianceSum / data.length;
double stdDev = Math.sqrt(variance);
// 计算中位数
double[] sortedData = data.clone();
Arrays.sort(sortedData);
double median;
if (sortedData.length % 2 == 0) {
median = (sortedData[sortedData.length/2 - 1] + sortedData[sortedData.length/2]) / 2.0;
} else {
median = sortedData[sortedData.length/2];
}
// 生成统计结果
StringBuilder result = new StringBuilder();
result.append("基本统计信息计算完成:\n");
result.append("数据点数量: ").append(data.length).append("\n");
result.append("最小值: ").append(min).append("\n");
result.append("最大值: ").append(max).append("\n");
result.append("均值: ").append(String.format("%.4f", mean)).append("\n");
result.append("中位数: ").append(String.format("%.4f", median)).append("\n");
result.append("方差: ").append(String.format("%.4f", variance)).append("\n");
result.append("标准差: ").append(String.format("%.4f", stdDev)).append("\n");
log.info("基本统计信息计算完成,数据点数量: {}", data.length);
return result.toString();
} catch (Exception e) {
log.error("计算基本统计信息时发生错误: {}", e.getMessage(), e);
return "计算基本统计信息时发生错误: " + e.getMessage();
}
}
/**
* 计算两个变量之间的相关系数
* @param x 第一个变量的数据数组
* @param y 第二个变量的数据数组
* @return 相关系数结果
*/
@Tool(description = "计算两个变量之间的皮尔逊相关系数")
public String calculateCorrelation(double[] x, double[] y) {
log.debug("开始计算相关系数,X数据点数量: {}, Y数据点数量: {}",
x != null ? x.length : 0, y != null ? y.length : 0);
try {
if (x == null || x.length == 0) {
log.warn("X数据不能为空");
return "错误:X数据不能为空";
}
if (y == null || y.length == 0) {
log.warn("Y数据不能为空");
return "错误:Y数据不能为空";
}
if (x.length != y.length) {
log.warn("X和Y数据长度必须相等");
return "错误:X和Y数据长度必须相等";
}
// 计算均值
double meanX = 0, meanY = 0;
for (int i = 0; i < x.length; i++) {
meanX += x[i];
meanY += y[i];
}
meanX /= x.length;
meanY /= y.length;
// 计算协方差和标准差
double covariance = 0;
double stdDevX = 0, stdDevY = 0;
for (int i = 0; i < x.length; i++) {
double diffX = x[i] - meanX;
double diffY = y[i] - meanY;
covariance += diffX * diffY;
stdDevX += diffX * diffX;
stdDevY += diffY * diffY;
}
covariance /= x.length;
stdDevX = Math.sqrt(stdDevX / x.length);
stdDevY = Math.sqrt(stdDevY / y.length);
// 计算相关系数
double correlation = 0;
if (stdDevX != 0 && stdDevY != 0) {
correlation = covariance / (stdDevX * stdDevY);
}
// 生成结果
StringBuilder result = new StringBuilder();
result.append("相关系数计算完成:\n");
result.append("数据点数量: ").append(x.length).append("\n");
result.append("X变量均值: ").append(String.format("%.4f", meanX)).append("\n");
result.append("Y变量均值: ").append(String.format("%.4f", meanY)).append("\n");
result.append("协方差: ").append(String.format("%.4f", covariance)).append("\n");
result.append("X变量标准差: ").append(String.format("%.4f", stdDevX)).append("\n");
result.append("Y变量标准差: ").append(String.format("%.4f", stdDevY)).append("\n");
result.append("皮尔逊相关系数: ").append(String.format("%.4f", correlation)).append("\n");
// 解释相关系数
String interpretation;
if (Math.abs(correlation) >= 0.8) {
interpretation = "强相关";
} else if (Math.abs(correlation) >= 0.5) {
interpretation = "中等相关";
} else if (Math.abs(correlation) >= 0.3) {
interpretation = "弱相关";
} else {
interpretation = "几乎无相关";
}
result.append("相关性解释: ").append(interpretation).append("\n");
log.info("相关系数计算完成,数据点数量: {}", x.length);
return result.toString();
} catch (Exception e) {
log.error("计算相关系数时发生错误: {}", e.getMessage(), e);
return "计算相关系数时发生错误: " + e.getMessage();
}
}
/**
* 执行线性回归分析
* @param x 自变量数据数组
* @param y 因变量数据数组
* @return 回归分析结果
*/
@Tool(description = "执行简单的线性回归分析,计算回归系数和拟合优度")
public String performLinearRegression(double[] x, double[] y) {
log.debug("开始执行线性回归分析,X数据点数量: {}, Y数据点数量: {}",
x != null ? x.length : 0, y != null ? y.length : 0);
try {
if (x == null || x.length == 0) {
log.warn("X数据不能为空");
return "错误:X数据不能为空";
}
if (y == null || y.length == 0) {
log.warn("Y数据不能为空");
return "错误:Y数据不能为空";
}
if (x.length != y.length) {
log.warn("X和Y数据长度必须相等");
return "错误:X和Y数据长度必须相等";
}
int n = x.length;
// 计算均值
double meanX = 0, meanY = 0;
for (int i = 0; i < n; i++) {
meanX += x[i];
meanY += y[i];
}
meanX /= n;
meanY /= n;
// 计算回归系数
double numerator = 0, denominator = 0;
for (int i = 0; i < n; i++) {
numerator += (x[i] - meanX) * (y[i] - meanY);
denominator += Math.pow(x[i] - meanX, 2);
}
// 斜率和截距
double slope = denominator != 0 ? numerator / denominator : 0;
double intercept = meanY - slope * meanX;
// 计算拟合优度(R²)
double ssTotal = 0, ssResidual = 0;
for (int i = 0; i < n; i++) {
double predictedY = slope * x[i] + intercept;
ssTotal += Math.pow(y[i] - meanY, 2);
ssResidual += Math.pow(y[i] - predictedY, 2);
}
double rSquared = ssTotal != 0 ? 1 - (ssResidual / ssTotal) : 1;
// 生成结果
StringBuilder result = new StringBuilder();
result.append("线性回归分析完成:\n");
result.append("数据点数量: ").append(n).append("\n");
result.append("回归方程: y = ").append(String.format("%.4f", slope))
.append(" * x + ").append(String.format("%.4f", intercept)).append("\n");
result.append("斜率: ").append(String.format("%.4f", slope)).append("\n");
result.append("截距: ").append(String.format("%.4f", intercept)).append("\n");
result.append("拟合优度(R²): ").append(String.format("%.4f", rSquared)).append("\n");
// 解释拟合优度
String interpretation;
if (rSquared >= 0.8) {
interpretation = "模型拟合非常好";
} else if (rSquared >= 0.6) {
interpretation = "模型拟合良好";
} else if (rSquared >= 0.4) {
interpretation = "模型拟合一般";
} else {
interpretation = "模型拟合较差";
}
result.append("模型解释力: ").append(interpretation).append("\n");
log.info("线性回归分析完成,数据点数量: {}", n);
return result.toString();
} catch (Exception e) {
log.error("执行线性回归分析时发生错误: {}", e.getMessage(), e);
return "执行线性回归分析时发生错误: " + e.getMessage();
}
}
}
\ No newline at end of file
package pangea.hiagent.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import pangea.hiagent.workpanel.IWorkPanelDataCollector;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
/**
* 存储文件访问工具类
* 提供访问服务器后端 storage 目录下文件的功能,并将内容推送到对话界面的"网页预览区域"进行展示
*/
@Slf4j
@Component
public class StorageFileAccessTool {
private final IWorkPanelDataCollector workPanelDataCollector;
// 支持的文件扩展名
private static final List<String> SUPPORTED_EXTENSIONS = Arrays.asList(
".txt", ".md", ".html", ".htm", ".xml", ".json", ".log", ".csv",
".css", ".js"
);
// storage目录路径
private static final String STORAGE_DIR = "backend" + File.separator + "storage";
public StorageFileAccessTool(IWorkPanelDataCollector workPanelDataCollector) {
this.workPanelDataCollector = workPanelDataCollector;
}
/**
* 访问并预览storage目录下的文件
* @param fileName 文件名(包含扩展名)
* @return 操作结果描述
*/
@Tool(description = "访问并预览storage目录下的文本类文件,支持.txt、.md、.html/.htm、.xml、.json、.log、.csv、.css、.js等格式")
public String accessStorageFile(String fileName) {
log.debug("接收到访问storage文件请求,文件名: {}", fileName);
try {
// 参数校验
if (fileName == null || fileName.isEmpty()) {
String result = "错误:文件名不能为空";
log.warn(result);
return result;
}
// 构建文件路径
String filePath = STORAGE_DIR + File.separator + fileName;
File file = new File(filePath);
// 检查文件是否存在
if (!file.exists()) {
String result = "错误:文件不存在 - " + fileName;
log.warn(result);
return result;
}
// 检查是否为文件(而不是目录)
if (!file.isFile()) {
String result = "错误:指定路径不是一个文件 - " + fileName;
log.warn(result);
return result;
}
// 检查文件扩展名是否支持
if (!isSupportedFile(fileName)) {
String result = "错误:不支持的文件类型 - " + fileName;
log.warn(result);
return result;
}
// 读取文件内容
String content = readFileContent(file);
// 确定MIME类型
String mimeType = getMimeType(fileName);
String title = "文件预览: " + fileName;
log.info("成功读取文件: {}", fileName);
String result = "已成功在工作面板中预览文件: " + fileName;
// 发送embed事件到工作面板
workPanelDataCollector.recordEmbed(filePath, mimeType, title, content);
return result;
} catch (Exception e) {
log.error("访问文件时发生错误: {}, 错误详情: {}", fileName, e.getMessage(), e);
return "访问文件时发生错误: " + e.getMessage();
}
}
/**
* 检查文件扩展名是否支持
* @param fileName 文件名
* @return 是否支持
*/
private boolean isSupportedFile(String fileName) {
String lowerFileName = fileName.toLowerCase();
return SUPPORTED_EXTENSIONS.stream().anyMatch(lowerFileName::endsWith);
}
/**
* 根据文件扩展名获取MIME类型
* @param fileName 文件名
* @return MIME类型
*/
private String getMimeType(String fileName) {
String lowerFileName = fileName.toLowerCase();
if (lowerFileName.endsWith(".html") || lowerFileName.endsWith(".htm")) {
return "text/html";
} else if (lowerFileName.endsWith(".md")) {
return "text/markdown";
} else if (lowerFileName.endsWith(".xml")) {
return "application/xml";
} else if (lowerFileName.endsWith(".json")) {
return "application/json";
} else if (lowerFileName.endsWith(".css")) {
return "text/css";
} else if (lowerFileName.endsWith(".js")) {
return "application/javascript";
} else {
return "text/plain";
}
}
/**
* 读取文件内容
* @param file 文件对象
* @return 文件内容
* @throws Exception 读取异常
*/
private String readFileContent(File file) throws Exception {
StringBuilder content = new StringBuilder();
BufferedReader reader = null;
try {
// 使用UTF-8编码读取文件
reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8));
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
log.debug("成功读取文件内容,长度: {} 字符", content.length());
return content.toString();
} finally {
if (reader != null) {
try {
reader.close();
} catch (Exception e) {
log.debug("关闭读取器失败: {}", e.getMessage());
}
}
}
}
}
\ No newline at end of file
package pangea.hiagent.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.web.util.HtmlUtils;
/**
* 字符串处理工具类
* 提供字符串处理和转换功能
*/
@Slf4j
@Component
public class StringProcessingTools {
@Tool(description = "将文本转换为大写")
public String toUpperCase(String text) {
log.debug("执行文本转大写操作");
if (text == null) {
String errorMsg = "输入文本不能为空";
log.warn(errorMsg);
return errorMsg;
}
String result = text.toUpperCase();
log.debug("转换结果: {}", result);
return result;
}
@Tool(description = "将文本转换为小写")
public String toLowerCase(String text) {
log.debug("执行文本转小写操作");
if (text == null) {
String errorMsg = "输入文本不能为空";
log.warn(errorMsg);
return errorMsg;
}
String result = text.toLowerCase();
log.debug("转换结果: {}", result);
return result;
}
@Tool(description = "反转文本")
public String reverseText(String text) {
log.debug("执行文本反转操作");
if (text == null) {
String errorMsg = "输入文本不能为空";
log.warn(errorMsg);
return errorMsg;
}
String result = new StringBuilder(text).reverse().toString();
log.debug("反转结果: {}", result);
return result;
}
@Tool(description = "统计文本中的字符数量")
public int countCharacters(String text) {
log.debug("执行字符统计操作");
if (text == null) {
log.warn("输入文本不能为空");
return 0;
}
int count = text.length();
log.debug("字符数量: {}", count);
return count;
}
@Tool(description = "安全的字符串处理,防止XSS攻击")
public String sanitizeString(String input) {
log.debug("安全处理字符串,长度: {}", input != null ? input.length() : 0);
if (input == null) {
String errorMsg = "输入无效:字符串不能为空";
log.warn("输入文本不能为空");
return errorMsg;
}
if (input.length() > 1000) {
String errorMsg = "输入无效:字符串长度不能超过1000个字符";
log.warn("输入字符串长度超过限制: {}", input.length());
return errorMsg;
}
// 清理输入,防止XSS攻击
String cleanInput = HtmlUtils.htmlEscape(input);
String result = "处理后的字符串:" + cleanInput;
log.debug("处理后字符串长度: {}", cleanInput.length());
return result;
}
}
\ No newline at end of file
package pangea.hiagent.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
/**
* 学习计划制定工具
* 用于根据学习目标和时间安排制定个性化的学习计划
*/
@Slf4j
@Component
public class StudyPlanGenerationTool {
public StudyPlanGenerationTool() {
// 默认构造器
}
/**
* 生成学习计划
* @param subject 学科主题
* @param goal 学习目标
* @param startDate 开始日期
* @param endDate 结束日期
* @param hoursPerDay 每天学习小时数
* @return 学习计划内容
*/
@Tool(description = "根据学科主题、学习目标和时间安排生成个性化的学习计划")
public String generateStudyPlan(String subject, String goal, String startDate, String endDate, Double hoursPerDay) {
log.debug("开始生成学习计划: 学科={}, 目标={}, 开始日期={}, 结束日期={}, 每天学习小时数={}",
subject, goal, startDate, endDate, hoursPerDay);
try {
if (subject == null || subject.trim().isEmpty()) {
log.warn("学科主题不能为空");
return "错误:学科主题不能为空";
}
if (goal == null || goal.trim().isEmpty()) {
log.warn("学习目标不能为空");
return "错误:学习目标不能为空";
}
// 验证日期格式
LocalDate startLocalDate, endLocalDate;
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
startLocalDate = LocalDate.parse(startDate, formatter);
endLocalDate = LocalDate.parse(endDate, formatter);
} catch (Exception e) {
log.warn("日期格式不正确,应为yyyy-MM-dd格式");
return "错误:日期格式不正确,应为yyyy-MM-dd格式";
}
// 验证时间范围
if (endLocalDate.isBefore(startLocalDate)) {
log.warn("结束日期不能早于开始日期");
return "错误:结束日期不能早于开始日期";
}
// 设置默认学习时间
if (hoursPerDay == null || hoursPerDay <= 0) {
hoursPerDay = 2.0;
}
// 生成学习计划
String studyPlan = generateStudyPlanContent(subject, goal, startLocalDate, endLocalDate, hoursPerDay);
log.info("学习计划生成完成: 学科={}, 目标={}", subject, goal);
return studyPlan;
} catch (Exception e) {
log.error("生成学习计划时发生错误: {}", e.getMessage(), e);
return "生成学习计划时发生错误: " + e.getMessage();
}
}
/**
* 生成学习计划内容
* @param subject 学科主题
* @param goal 学习目标
* @param startDate 开始日期
* @param endDate 结束日期
* @param hoursPerDay 每天学习小时数
* @return 学习计划内容
*/
private String generateStudyPlanContent(String subject, String goal, LocalDate startDate, LocalDate endDate, Double hoursPerDay) {
StringBuilder plan = new StringBuilder();
plan.append("个性化学习计划\n\n");
plan.append("学科主题: ").append(subject).append("\n");
plan.append("学习目标: ").append(goal).append("\n");
plan.append("计划周期: ").append(startDate).append(" 至 ").append(endDate).append("\n");
plan.append("每日学习时间: ").append(hoursPerDay).append("小时\n\n");
// 计算总天数
long totalDays = java.time.temporal.ChronoUnit.DAYS.between(startDate, endDate) + 1;
plan.append("总学习天数: ").append(totalDays).append("天\n");
plan.append("总学习时间: ").append(totalDays * hoursPerDay).append("小时\n\n");
// 根据学科生成学习阶段
plan.append("学习阶段安排:\n");
switch (subject.toLowerCase()) {
case "数学":
plan.append("第一阶段 (第1-").append(Math.max(1, totalDays/3)).append("天):\n");
plan.append(" - 基础概念复习\n");
plan.append(" - 核心公式梳理\n");
plan.append(" - 典型例题解析\n\n");
plan.append("第二阶段 (第").append(Math.max(1, totalDays/3)+1).append("-").append(Math.max(1, 2*totalDays/3)).append("天):\n");
plan.append(" - 专题训练\n");
plan.append(" - 错题整理\n");
plan.append(" - 知识点强化\n\n");
plan.append("第三阶段 (第").append(Math.max(1, 2*totalDays/3)+1).append("-").append(totalDays).append("天):\n");
plan.append(" - 综合练习\n");
plan.append(" - 模拟测试\n");
plan.append(" - 查漏补缺\n");
break;
case "英语":
plan.append("第一阶段 (第1-").append(Math.max(1, totalDays/4)).append("天):\n");
plan.append(" - 词汇积累\n");
plan.append(" - 语法复习\n");
plan.append(" - 听力训练\n\n");
plan.append("第二阶段 (第").append(Math.max(1, totalDays/4)+1).append("-").append(Math.max(1, totalDays/2)).append("天):\n");
plan.append(" - 阅读理解专项训练\n");
plan.append(" - 写作技巧提升\n");
plan.append(" - 口语练习\n\n");
plan.append("第三阶段 (第").append(Math.max(1, totalDays/2)+1).append("-").append(Math.max(1, 3*totalDays/4)).append("天):\n");
plan.append(" - 真题演练\n");
plan.append(" - 错题分析\n");
plan.append(" - 弱项突破\n\n");
plan.append("第四阶段 (第").append(Math.max(1, 3*totalDays/4)+1).append("-").append(totalDays).append("天):\n");
plan.append(" - 综合模拟\n");
plan.append(" - 时间管理训练\n");
plan.append(" - 心态调整\n");
break;
default:
plan.append("第一阶段 (第1-").append(Math.max(1, totalDays/3)).append("天):\n");
plan.append(" - 基础知识梳理\n");
plan.append(" - 核心概念理解\n");
plan.append(" - 典型案例学习\n\n");
plan.append("第二阶段 (第").append(Math.max(1, totalDays/3)+1).append("-").append(Math.max(1, 2*totalDays/3)).append("天):\n");
plan.append(" - 专题深入学习\n");
plan.append(" - 实践应用训练\n");
plan.append(" - 疑难问题解决\n\n");
plan.append("第三阶段 (第").append(Math.max(1, 2*totalDays/3)+1).append("-").append(totalDays).append("天):\n");
plan.append(" - 综合能力提升\n");
plan.append(" - 模拟测试演练\n");
plan.append(" - 总结反思优化\n");
break;
}
plan.append("\n每日学习建议:\n");
plan.append("- 制定每日具体学习任务\n");
plan.append("- 记录学习进度和收获\n");
plan.append("- 定期回顾和调整计划\n");
plan.append("- 保持良好的作息习惯\n");
plan.append("\n学习资源推荐:\n");
plan.append("- 教材和参考书\n");
plan.append("- 在线课程和视频\n");
plan.append("- 练习题和模拟试卷\n");
plan.append("- 学习小组和讨论社区\n");
return plan.toString();
}
}
\ No newline at end of file
package pangea.hiagent.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import java.util.regex.Pattern;
/**
* 技术代码解释工具
* 用于分析和解释技术代码的功能和实现逻辑
*/
@Slf4j
@Component
public class TechnicalCodeExplanationTool {
// 代码语言模式
private static final Pattern JAVA_PATTERN = Pattern.compile("\\b(public|private|protected|class|interface|extends|implements|import|package)\\b");
private static final Pattern PYTHON_PATTERN = Pattern.compile("\\b(def|class|import|from|if|elif|else|for|while|try|except|finally|with|as)\\b");
private static final Pattern JAVASCRIPT_PATTERN = Pattern.compile("\\b(function|var|let|const|if|else|for|while|try|catch|finally|class|import|export)\\b");
public TechnicalCodeExplanationTool() {
// 默认构造器
}
/**
* 解释代码功能
* @param code 代码内容
* @param language 编程语言(可选)
* @return 代码功能解释
*/
@Tool(description = "分析并解释代码的功能和实现逻辑,支持多种编程语言")
public String explainCode(String code, String language) {
log.debug("开始解释代码,语言: {}", language);
try {
if (code == null || code.trim().isEmpty()) {
log.warn("代码内容不能为空");
return "错误:代码内容不能为空";
}
// 如果未指定语言,自动检测
if (language == null || language.trim().isEmpty()) {
language = detectLanguage(code);
}
// 生成代码解释
String explanation = generateCodeExplanation(code, language);
log.info("代码解释完成,语言: {}", language);
return explanation;
} catch (Exception e) {
log.error("解释代码时发生错误: {}", e.getMessage(), e);
return "解释代码时发生错误: " + e.getMessage();
}
}
/**
* 检测代码语言
* @param code 代码内容
* @return 检测到的语言
*/
private String detectLanguage(String code) {
// 统计各种语言关键字出现次数
int javaScore = countMatches(code, JAVA_PATTERN);
int pythonScore = countMatches(code, PYTHON_PATTERN);
int javascriptScore = countMatches(code, JAVASCRIPT_PATTERN);
// 返回得分最高的语言
if (javaScore >= pythonScore && javaScore >= javascriptScore) {
return "Java";
} else if (pythonScore >= javascriptScore) {
return "Python";
} else {
return "JavaScript";
}
}
/**
* 统计正则表达式匹配次数
* @param text 文本内容
* @param pattern 正则表达式
* @return 匹配次数
*/
private int countMatches(String text, Pattern pattern) {
java.util.regex.Matcher matcher = pattern.matcher(text);
int count = 0;
while (matcher.find()) {
count++;
}
return count;
}
/**
* 生成代码解释
* @param code 代码内容
* @param language 编程语言
* @return 代码解释
*/
private String generateCodeExplanation(String code, String language) {
StringBuilder explanation = new StringBuilder();
explanation.append("代码解释 (").append(language).append("):\n\n");
// 根据语言类型提供不同的解释
switch (language.toLowerCase()) {
case "java":
explanation.append(explainJavaCode(code));
break;
case "python":
explanation.append(explainPythonCode(code));
break;
case "javascript":
explanation.append(explainJavascriptCode(code));
break;
default:
explanation.append("这是一个").append(language).append("代码片段。\n");
explanation.append("代码功能分析:\n");
explanation.append(codeAnalysis(code));
break;
}
return explanation.toString();
}
/**
* 解释Java代码
* @param code Java代码
* @return 解释内容
*/
private String explainJavaCode(String code) {
StringBuilder explanation = new StringBuilder();
explanation.append("Java代码功能分析:\n");
// 检查是否包含类定义
if (code.contains("class ")) {
explanation.append("- 包含类定义\n");
}
// 检查是否包含方法定义
if (code.contains("public ") || code.contains("private ") || code.contains("protected ")) {
explanation.append("- 包含方法定义\n");
}
// 检查是否包含导入语句
if (code.contains("import ")) {
explanation.append("- 包含导入语句\n");
}
// 检查是否包含控制结构
if (code.contains("if ") || code.contains("else ") || code.contains("for ") || code.contains("while ")) {
explanation.append("- 包含控制结构\n");
}
// 检查是否包含异常处理
if (code.contains("try ") || code.contains("catch ") || code.contains("finally ")) {
explanation.append("- 包含异常处理\n");
}
explanation.append("\n").append(codeAnalysis(code));
return explanation.toString();
}
/**
* 解释Python代码
* @param code Python代码
* @return 解释内容
*/
private String explainPythonCode(String code) {
StringBuilder explanation = new StringBuilder();
explanation.append("Python代码功能分析:\n");
// 检查是否包含函数定义
if (code.contains("def ")) {
explanation.append("- 包含函数定义\n");
}
// 检查是否包含类定义
if (code.contains("class ")) {
explanation.append("- 包含类定义\n");
}
// 检查是否包含导入语句
if (code.contains("import ") || code.contains("from ")) {
explanation.append("- 包含导入语句\n");
}
// 检查是否包含控制结构
if (code.contains("if ") || code.contains("elif ") || code.contains("else ") ||
code.contains("for ") || code.contains("while ")) {
explanation.append("- 包含控制结构\n");
}
// 检查是否包含异常处理
if (code.contains("try:") || code.contains("except:") || code.contains("finally:")) {
explanation.append("- 包含异常处理\n");
}
explanation.append("\n").append(codeAnalysis(code));
return explanation.toString();
}
/**
* 解释JavaScript代码
* @param code JavaScript代码
* @return 解释内容
*/
private String explainJavascriptCode(String code) {
StringBuilder explanation = new StringBuilder();
explanation.append("JavaScript代码功能分析:\n");
// 检查是否包含函数定义
if (code.contains("function ") || code.contains("=>")) {
explanation.append("- 包含函数定义\n");
}
// 检查是否包含类定义
if (code.contains("class ")) {
explanation.append("- 包含类定义\n");
}
// 检查是否包含导入导出语句
if (code.contains("import ") || code.contains("export ")) {
explanation.append("- 包含模块导入/导出\n");
}
// 检查是否包含控制结构
if (code.contains("if ") || code.contains("else ") || code.contains("for ") || code.contains("while ")) {
explanation.append("- 包含控制结构\n");
}
// 检查是否包含异常处理
if (code.contains("try ") || code.contains("catch ") || code.contains("finally ")) {
explanation.append("- 包含异常处理\n");
}
explanation.append("\n").append(codeAnalysis(code));
return explanation.toString();
}
/**
* 通用代码分析
* @param code 代码内容
* @return 分析结果
*/
private String codeAnalysis(String code) {
StringBuilder analysis = new StringBuilder();
analysis.append("代码概要分析:\n");
// 统计行数
String[] lines = code.split("\n");
analysis.append("- 代码行数: ").append(lines.length).append("\n");
// 统计字符数
analysis.append("- 字符总数: ").append(code.length()).append("\n");
// 检查是否包含注释
if (code.contains("//") || code.contains("/*") || code.contains("#") || code.contains("<!--")) {
analysis.append("- 包含注释\n");
}
// 检查代码复杂度(简单估算)
long semicolonCount = code.chars().filter(ch -> ch == ';').count();
long braceCount = code.chars().filter(ch -> ch == '{' || ch == '}').count();
if (semicolonCount > 10 || braceCount > 10) {
analysis.append("- 代码复杂度较高\n");
} else {
analysis.append("- 代码复杂度较低\n");
}
return analysis.toString();
}
}
\ No newline at end of file
package pangea.hiagent.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import pangea.hiagent.rag.RagService;
import pangea.hiagent.service.AgentService;
import pangea.hiagent.model.Agent;
import java.util.List;
/**
* 技术文档检索工具
* 用于检索和查询技术文档内容
*/
@Slf4j
@Component
public class TechnicalDocumentationRetrievalTool {
private final RagService ragService;
private final AgentService agentService;
public TechnicalDocumentationRetrievalTool(RagService ragService, AgentService agentService) {
this.ragService = ragService;
this.agentService = agentService;
}
/**
* 检索技术文档
* @param query 查询关键词
* @param maxResults 最大返回结果数
* @return 检索到的技术文档内容
*/
@Tool(description = "根据关键词检索技术文档,返回最相关的内容片段")
public String searchTechnicalDocumentation(String query, Integer maxResults) {
log.debug("开始检索技术文档: {}, 最大结果数: {}", query, maxResults);
try {
if (query == null || query.trim().isEmpty()) {
log.warn("查询关键词不能为空");
return "错误:查询关键词不能为空";
}
// 设置默认最大结果数
if (maxResults == null || maxResults <= 0) {
maxResults = 5;
}
// 获取技术支持Agent
Agent techSupportAgent = getTechSupportAgent();
if (techSupportAgent == null) {
log.error("未找到技术支持Agent");
return "错误:未找到技术支持Agent配置";
}
// 使用RAG服务检索文档
List<org.springframework.ai.document.Document> documents =
ragService.searchDocumentsByAgent(techSupportAgent, query);
// 限制返回结果数量
if (documents.size() > maxResults) {
documents = documents.subList(0, maxResults);
}
// 格式化结果
StringBuilder result = new StringBuilder();
result.append("技术文档检索结果:\n");
result.append("查询关键词: ").append(query).append("\n");
result.append("找到 ").append(documents.size()).append(" 个相关文档片段\n\n");
for (int i = 0; i < documents.size(); i++) {
org.springframework.ai.document.Document doc = documents.get(i);
result.append("文档 ").append(i + 1).append(":\n");
result.append(doc.getText()).append("\n\n");
}
log.info("技术文档检索完成,找到 {} 个结果", documents.size());
return result.toString();
} catch (Exception e) {
log.error("检索技术文档时发生错误: {}", e.getMessage(), e);
return "检索技术文档时发生错误: " + e.getMessage();
}
}
/**
* 获取技术支持Agent
* @return 技术支持Agent对象
*/
private Agent getTechSupportAgent() {
try {
// 在实际应用中,这里应该通过某种方式获取技术支持Agent
// 例如通过AgentService查询特定名称的Agent
List<Agent> agents = agentService.listAgents();
for (Agent agent : agents) {
if ("技术支持".equals(agent.getName())) {
return agent;
}
}
} catch (Exception e) {
log.error("获取技术支持Agent时发生错误: {}", e.getMessage(), e);
}
return null;
}
}
\ No newline at end of file
package pangea.hiagent.tools;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.ResponseEntity;
// 天气API响应数据结构
class WeatherApiResponse {
public String message;
public int status;
public String date;
public String time;
public CityInfo cityInfo;
public WeatherData data;
static class CityInfo {
public String city;
public String citykey;
public String parent;
public String updateTime;
}
static class WeatherData {
public String shidu; // 湿度
public String wendu; // 温度
public String pm25;
public String quality;
public Forecast[] forecast;
static class Forecast {
public String date;
public String high;
public String low;
public String type; // 天气状况
}
}
}
/**
* 天气查询工具类
* 提供城市天气信息查询功能
*/
@Slf4j
@Component
public class WeatherFunction {
private final RestTemplate restTemplate;
public WeatherFunction(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@JsonClassDescription("获取指定城市的天气信息")
public record Request(
@JsonProperty(required = true, value = "city")
@JsonPropertyDescription("城市名称")
String city
) {}
@JsonClassDescription("天气信息响应")
public record Response(
@JsonPropertyDescription("温度") String temperature,
@JsonPropertyDescription("湿度") String humidity,
@JsonPropertyDescription("天气状况") String condition
) {}
@Tool(description = "获取指定城市的天气信息")
public Response getWeather(Request request) {
log.debug("查询城市天气信息: {}", request.city);
try {
// 注意:这里使用固定的城市代码(天津)进行演示,实际应用中需要根据城市名称查找对应的城市代码
String url = "http://t.weather.sojson.com/api/weather/city/101030100";
ResponseEntity<WeatherApiResponse> responseEntity = restTemplate.getForEntity(url, WeatherApiResponse.class);
if (responseEntity.getStatusCode().is2xxSuccessful() && responseEntity.getBody() != null) {
WeatherApiResponse apiResponse = responseEntity.getBody();
if ("success".equals(apiResponse.message) && apiResponse.data != null) {
String temperature = apiResponse.data.wendu + "°C";
String humidity = apiResponse.data.shidu;
String condition = apiResponse.data.forecast != null && apiResponse.data.forecast.length > 0 ?
apiResponse.data.forecast[0].type : "未知";
Response response = new Response(temperature, humidity, condition);
log.debug("天气查询结果: 温度={}, 湿度={}, 天气状况={}", response.temperature, response.humidity, response.condition);
return response;
} else {
log.error("天气API返回错误信息: {}", apiResponse.message);
}
} else {
log.error("天气API调用失败,HTTP状态码: {}", responseEntity.getStatusCode());
}
} catch (Exception e) {
log.error("天气API调用异常: ", e);
}
// 如果API调用失败,返回默认值
Response response = new Response("22°C", "65%", "晴天");
log.debug("天气查询结果(默认值): 温度={}, 湿度={}, 天气状况={}", response.temperature, response.humidity, response.condition);
return response;
}
}
\ No newline at end of file
// package pangea.hiagent.tools;
// import lombok.extern.slf4j.Slf4j;
// import org.jsoup.Jsoup;
// import org.jsoup.nodes.Document;
// import org.jsoup.nodes.Element;
// import org.jsoup.select.Elements;
// import org.springframework.ai.tool.annotation.Tool;
// import org.springframework.beans.BeansException;
// import org.springframework.context.ApplicationContext;
// import org.springframework.context.ApplicationContextAware;
// import org.springframework.stereotype.Component;
// import pangea.hiagent.core.IWorkPanelDataCollector;
// import java.io.BufferedReader;
// import java.io.InputStreamReader;
// import java.net.URL;
// import java.net.URLConnection;
// import java.util.HashMap;
// import java.util.regex.Pattern;
// /**
// * 网页内容提取工具类
// * 提供网页内容抓取和正文提取功能
// */
// @Slf4j
// @Component
// public class WebContentExtractorTool implements ApplicationContextAware {
// // Spring应用上下文引用
// private static ApplicationContext applicationContext;
// // 用户代理字符串
// private static final String USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36";
// // 连接和读取超时时间(毫秒)
// private static final int CONNECT_TIMEOUT = 5000;
// private static final int READ_TIMEOUT = 10000;
// // 标题选择器优先级
// private static final String[] TITLE_SELECTORS = {
// "h1[class*=title], h1[id*=title]",
// "h1:first-of-type",
// "title",
// "h1"
// };
// // 正文内容选择器优先级
// private static final String[] CONTENT_SELECTORS = {
// "[class*=content], [id*=content]",
// "[class*=article], [id*=article]",
// "[class*=post], [id*=post]",
// "article",
// ".main-content",
// "#main-content",
// "main"
// };
// /**
// * 设置ApplicationContext引用
// */
// @Override
// public void setApplicationContext(ApplicationContext context) throws BeansException {
// WebContentExtractorTool.applicationContext = context;
// }
// /**
// * 提取网页正文内容
// * @param url 网页URL地址
// * @return 提取的网页正文内容
// */
// @Tool(description = "提取指定网页的正文内容,自动识别并提取文章标题和主要内容,过滤掉广告、导航栏等无关内容")
// public String extractWebContent(String url) {
// log.debug("接收到网页内容提取请求: {}", url);
// // 记录工具调用开始
// HashMap<String, Object> input = new HashMap<>();
// input.put("url", url);
// recordToWorkPanel("extractWebContent", input, null, null);
// long startTime = System.currentTimeMillis();
// try {
// // 自动补全URL协议
// String completeUrl = completeUrlProtocol(url);
// // 验证URL格式
// if (!isValidUrl(completeUrl)) {
// long endTime = System.currentTimeMillis();
// log.warn("无效的URL格式: {}", completeUrl);
// String result = "无效的URL格式,请确保输入完整的网址,例如: https://www.example.com";
// // 记录失败结果
// recordToWorkPanel("extractWebContent", null, result, "failure", endTime - startTime);
// return result;
// }
// // 获取网页HTML内容
// String htmlContent = fetchWebContent(completeUrl);
// if (htmlContent == null || htmlContent.isEmpty()) {
// long endTime = System.currentTimeMillis();
// log.warn("获取网页内容失败: {}", completeUrl);
// String result = "获取网页内容失败,请检查网址是否正确";
// // 记录失败结果
// recordToWorkPanel("extractWebContent", null, result, "failure", endTime - startTime);
// return result;
// }
// // 提取正文内容
// String extractedContent = extractContentFromHtml(htmlContent, completeUrl);
// long endTime = System.currentTimeMillis();
// log.info("成功提取网页内容: {}", completeUrl);
// // 发送embed事件到工作面板
// sendEmbedEvent(completeUrl, "text/plain", "网页内容提取结果", extractedContent);
// // 记录成功结果
// recordToWorkPanel("extractWebContent", null, "已成功提取网页内容", "success", endTime - startTime);
// return extractedContent;
// } catch (Exception e) {
// long endTime = System.currentTimeMillis();
// log.error("提取网页内容时发生错误: {}, 错误详情: {}", url, e.getMessage(), e);
// String errorMsg = "提取网页内容时发生错误: " + e.getMessage();
// // 记录错误
// recordToWorkPanel("extractWebContent", null, errorMsg, "error", endTime - startTime);
// return errorMsg;
// }
// }
// /**
// * 自动补全URL协议
// * @param url 待补全的URL
// * @return 补全后的URL
// */
// private String completeUrlProtocol(String url) {
// if (url == null || url.isEmpty()) {
// return url;
// }
// // 如果已经包含协议,则直接返回
// if (url.startsWith("http://") || url.startsWith("https://")) {
// return url;
// }
// // 默认添加https协议
// return "https://" + url;
// }
// /**
// * 验证URL格式是否有效
// * @param url 待验证的URL
// * @return 如果URL格式有效返回true,否则返回false
// */
// private boolean isValidUrl(String url) {
// if (url == null || url.isEmpty()) {
// return false;
// }
// // 检查是否包含协议
// if (!url.startsWith("http://") && !url.startsWith("https://")) {
// return false;
// }
// // 简单的URL格式检查
// try {
// new URL(url);
// return true;
// } catch (Exception e) {
// return false;
// }
// }
// /**
// * 获取网页内容
// * @param url 网页URL地址
// * @return 网页HTML内容
// */
// private String fetchWebContent(String url) {
// StringBuilder content = new StringBuilder();
// BufferedReader reader = null;
// try {
// URLConnection connection = new URL(url).openConnection();
// // 设置请求头,模拟浏览器访问
// connection.setRequestProperty("User-Agent", USER_AGENT);
// connection.setConnectTimeout(CONNECT_TIMEOUT);
// connection.setReadTimeout(READ_TIMEOUT);
// reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
// String line;
// while ((line = reader.readLine()) != null) {
// content.append(line).append("\n");
// }
// log.debug("成功获取网页内容,长度: {} bytes", content.length());
// return content.toString();
// } catch (Exception e) {
// log.error("获取网页内容失败: {}", e.getMessage(), e);
// return null;
// } finally {
// if (reader != null) {
// try {
// reader.close();
// } catch (Exception e) {
// log.debug("关闭读取器失败: {}", e.getMessage());
// }
// }
// }
// }
// /**
// * 从HTML中提取正文内容
// * @param html HTML内容
// * @param url 网页URL
// * @return 提取的正文内容
// */
// private String extractContentFromHtml(String html, String url) {
// try {
// Document doc = Jsoup.parse(html, url);
// // 移除脚本和样式元素
// doc.select("script, style, nav, header, footer, aside, .ad, .advertisement").remove();
// StringBuilder content = new StringBuilder();
// // 提取标题
// String title = extractTitle(doc);
// if (title != null && !title.isEmpty()) {
// content.append("# ").append(title).append("\n\n");
// }
// // 提取正文内容
// String bodyContent = extractBodyContent(doc);
// if (bodyContent != null && !bodyContent.isEmpty()) {
// content.append(bodyContent);
// }
// return content.toString();
// } catch (Exception e) {
// log.error("解析HTML内容时发生错误: {}", e.getMessage(), e);
// return "解析网页内容时发生错误: " + e.getMessage();
// }
// }
// /**
// * 提取网页标题
// * @param doc Jsoup文档对象
// * @return 网页标题
// */
// private String extractTitle(Document doc) {
// // 按优先级尝试不同的选择器
// for (String selector : TITLE_SELECTORS) {
// Elements elements = doc.select(selector);
// if (!elements.isEmpty()) {
// Element element = elements.first();
// if (element != null) {
// String title = element.text().trim();
// if (!title.isEmpty()) {
// return title;
// }
// }
// }
// }
// // 如果没找到,使用默认title标签
// String title = doc.title();
// return title != null ? title.trim() : "";
// }
// /**
// * 提取网页正文内容
// * @param doc Jsoup文档对象
// * @return 正文内容
// */
// private String extractBodyContent(Document doc) {
// StringBuilder content = new StringBuilder();
// // 按优先级尝试不同的选择器
// for (String selector : CONTENT_SELECTORS) {
// Elements elements = doc.select(selector);
// if (!elements.isEmpty()) {
// for (Element element : elements) {
// String text = extractTextFromElement(element);
// if (text != null && !text.isEmpty()) {
// content.append(text).append("\n\n");
// }
// }
// // 如果找到了内容就返回
// if (content.length() > 0) {
// return content.toString().trim();
// }
// }
// }
// // 如果没有找到特定的内容区域,尝试从body中提取
// Element body = doc.body();
// if (body != null) {
// String text = extractTextFromElement(body);
// if (text != null && !text.isEmpty()) {
// return text.trim();
// }
// }
// return "";
// }
// /**
// * 从元素中提取纯文本内容
// * @param element Jsoup元素对象
// * @return 纯文本内容
// */
// private String extractTextFromElement(Element element) {
// if (element == null) {
// return "";
// }
// StringBuilder text = new StringBuilder();
// // 获取所有子元素
// Elements children = element.children();
// for (Element child : children) {
// // 跳过广告、导航等无关元素
// if (shouldSkipElement(child)) {
// continue;
// }
// String tagName = child.tagName().toLowerCase();
// // 处理标题元素
// if (Pattern.matches("h[1-6]", tagName)) {
// String headingText = child.text().trim();
// if (!headingText.isEmpty()) {
// text.append("\n\n## ").append(headingText).append("\n\n");
// }
// }
// // 处理段落元素
// else if ("p".equals(tagName)) {
// String paragraphText = child.text().trim();
// if (!paragraphText.isEmpty()) {
// text.append(paragraphText).append("\n\n");
// }
// }
// // 处理列表元素
// else if ("ul".equals(tagName) || "ol".equals(tagName)) {
// Elements items = child.select("li");
// for (Element item : items) {
// String itemText = item.text().trim();
// if (!itemText.isEmpty()) {
// text.append("- ").append(itemText).append("\n");
// }
// }
// text.append("\n");
// }
// // 处理其他块级元素
// else if (isBlockElement(tagName)) {
// String elementText = child.text().trim();
// if (!elementText.isEmpty()) {
// text.append(elementText).append("\n\n");
// }
// }
// // 递归处理内联元素
// else {
// String childText = extractTextFromElement(child);
// if (!childText.isEmpty()) {
// text.append(childText);
// }
// }
// }
// // 添加直接文本节点
// String ownText = element.ownText().trim();
// if (!ownText.isEmpty()) {
// text.append(ownText).append(" ");
// }
// return text.toString().trim();
// }
// /**
// * 判断是否应该跳过某个元素
// * @param element Jsoup元素对象
// * @return 是否应该跳过
// */
// private boolean shouldSkipElement(Element element) {
// String className = element.className().toLowerCase();
// String id = element.id().toLowerCase();
// String tagName = element.tagName().toLowerCase();
// // 跳过常见的无关元素
// if ("nav".equals(tagName) || "header".equals(tagName) || "footer".equals(tagName) ||
// "aside".equals(tagName) || "script".equals(tagName) || "style".equals(tagName)) {
// return true;
// }
// // 跳过包含特定关键词的元素
// String skipKeywords = "ad|ads|advertisement|banner|sidebar|menu|navigation|comment|share|social";
// if (className.matches(".*(" + skipKeywords + ").*") ||
// id.matches(".*(" + skipKeywords + ").*")) {
// return true;
// }
// return false;
// }
// /**
// * 判断是否为块级元素
// * @param tagName 标签名
// * @return 是否为块级元素
// */
// private boolean isBlockElement(String tagName) {
// String blockElements = "div|section|article|main|blockquote|pre|table|form|fieldset";
// return Pattern.matches(blockElements, tagName);
// }
// /**
// * 记录到工作面板(不带执行时间)
// */
// private void recordToWorkPanel(String toolAction, Object input, Object output, String status) {
// recordToWorkPanel(toolAction, input, output, status, null);
// }
// /**
// * 记录到工作面板(带执行时间)
// */
// private void recordToWorkPanel(String toolAction, Object input, Object output, String status, Long executionTime) {
// try {
// // 首先尝试从Spring容器获取collector
// IWorkPanelDataCollector collector = null;
// if (applicationContext != null) {
// try {
// collector = applicationContext.getBean(IWorkPanelDataCollector.class);
// } catch (Exception e) {
// log.debug("通过Spring容器获取WorkPanelDataCollector失败: {}", e.getMessage());
// }
// }
// if (collector != null) {
// // 记录工具调用
// if (input != null && status == null) {
// // 调用开始
// collector.recordToolCallStart("网页内容提取工具", toolAction, input);
// } else if (input == null && status != null) {
// // 调用结束(input为null表示这是结束调用)
// collector.recordToolCallComplete("网页内容提取工具", output, status, executionTime);
// } else if (input != null && status != null) {
// // 调用结束(input不为null,status不为null表示这是结束调用)
// collector.recordToolCallComplete("网页内容提取工具", output, status, executionTime);
// }
// } else {
// log.debug("无法记录到工作面板:collector为null");
// }
// } catch (Exception e) {
// log.debug("记录工作面板失败", e);
// }
// }
// /**
// * 发送embed事件到工作面板
// * @param url 网页URL
// * @param type MIME类型
// * @param title embed标题
// * @param content 提取的内容
// */
// private void sendEmbedEvent(String url, String type, String title, String content) {
// try {
// // 从Spring容器获取collector
// IWorkPanelDataCollector collector = null;
// if (applicationContext != null) {
// try {
// collector = applicationContext.getBean(IWorkPanelDataCollector.class);
// log.debug("通过Spring容器获取到WorkPanelDataCollector实例");
// } catch (Exception e) {
// log.debug("通过Spring容器获取WorkPanelDataCollector失败: {}", e.getMessage());
// }
// }
// if (collector != null) {
// // 使用新的recordEmbed方法发送embed事件
// collector.recordEmbed(url, type, title, content);
// log.debug("已发送embed事件到工作面板: title={}, type={}, hasContent={}",
// title, type, content != null && !content.isEmpty());
// } else {
// log.warn("无法发送embed事件:collector为null,无法从Spring容器获取");
// }
// } catch (Exception e) {
// log.error("发送embed事件失败: {}", e.getMessage(), e);
// }
// }
// }
\ No newline at end of file
package pangea.hiagent.tools;
import lombok.extern.slf4j.Slf4j;
import pangea.hiagent.workpanel.IWorkPanelDataCollector;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
/**
* 网页访问工具类
* 提供根据网站名称或URL地址访问网页并在工作面板中预览的功能
*/
@Slf4j
@Component
public class WebPageAccessTools {
// 通过构造器注入的方式引入IWorkPanelDataCollector依赖
private final IWorkPanelDataCollector workPanelDataCollector;
public WebPageAccessTools(IWorkPanelDataCollector workPanelDataCollector) {
this.workPanelDataCollector = workPanelDataCollector;
}
/**
* 根据网站名称访问网页并在工作面板中预览
* @param siteName 网站名称(如"百度"、"谷歌"等)
* @return 操作结果描述
*/
@Tool(description = "根据网站名称访问网页并在工作面板中预览,支持常见网站如百度、谷歌、必应等,如未找到匹配的网站名称则返回错误信息")
public String accessWebSiteByName(String siteName) {
log.debug("接收到访问网站请求,网站名称: {}", siteName);
try {
String url = getWebsiteUrlByName(siteName);
if (url != null) {
return accessAndRecordWebContent(url, "网页预览: " + siteName);
} else {
return handleFailure("未找到匹配的网站名称: " + siteName);
}
} catch (Exception e) {
return handleError(e, "访问网站时发生错误");
}
}
/**
* 根据URL地址直接访问网页并在工作面板中预览
* @param url 网页URL地址
* @return 操作结果描述
*/
@Tool(description = "根据完整的URL地址访问网页并在工作面板中预览")
public String accessWebSiteByUrl(String url) {
log.debug("接收到访问网页URL请求: {}", url);
try {
// 自动补全URL协议
String completeUrl = completeUrlProtocol(url);
// 验证URL格式
if (!isValidUrl(completeUrl)) {
return handleFailure("无效的URL格式,请确保输入完整的网址,例如: https://www.example.com");
}
return accessAndRecordWebContent(completeUrl, "网页预览");
} catch (Exception e) {
return handleError(e, "访问网页时发生错误");
}
}
/**
* 访问网页内容并记录到工作面板
* @param url 网页URL
* @param title embed标题
* @return 操作结果
*/
private String accessAndRecordWebContent(String url, String title) {
try {
// 直接获取网页内容并发送到工作面板进行预览,不再打开浏览器
String webContent = fetchWebContent(url);
log.info("成功访问网页: {}", url);
String result = "已成功在工作面板中预览网页: " + url;
// 发送embed事件到工作面板
workPanelDataCollector.recordEmbed(url, "text/html", title, webContent);
return result;
} catch (Exception e) {
return handleError(e, "获取网页内容时发生错误");
}
}
/**
* 处理失败情况
* @param message 错误消息
* @return 错误结果
*/
private String handleFailure(String message) {
log.warn(message);
return "抱歉," + message;
}
/**
* 处理异常情况
* @param e 异常对象
* @param prefix 错误消息前缀
* @return 错误结果
*/
private String handleError(Exception e, String prefix) {
log.error("{}: {}, 错误详情: {}", prefix, e.getMessage(), e);
return prefix + ": " + e.getMessage();
}
/**
* 根据网站名称获取对应的URL
* @param siteName 网站名称
* @return 对应的URL,如果未找到则返回null
*/
private String getWebsiteUrlByName(String siteName) {
if (siteName == null || siteName.isEmpty()) {
return null;
}
// 转换为小写进行比较
String lowerSiteName = siteName.toLowerCase();
// 常见网站映射
switch (lowerSiteName) {
case "百度":
case "baidu":
return "https://www.baidu.com";
case "谷歌":
case "google":
return "https://www.google.com";
case "必应":
case "bing":
return "https://www.bing.com";
case "淘宝":
case "taobao":
return "https://www.taobao.com";
case "京东":
case "jd":
case "jingdong":
return "https://www.jd.com";
case "天猫":
case "tmall":
return "https://www.tmall.com";
case "知乎":
case "zhihu":
return "https://www.zhihu.com";
case "微博":
case "weibo":
return "https://www.weibo.com";
case "github":
return "https://www.github.com";
default:
return null;
}
}
/**
* 自动补全URL协议
* @param url 待补全的URL
* @return 补全后的URL
*/
private String completeUrlProtocol(String url) {
if (url == null || url.isEmpty()) {
return url;
}
// 如果已经包含协议,则直接返回
if (url.startsWith("http://") || url.startsWith("https://")) {
return url;
}
// 默认添加https协议
return "https://" + url;
}
/**
* 验证URL格式是否有效
* @param url 待验证的URL
* @return 如果URL格式有效返回true,否则返回false
*/
private boolean isValidUrl(String url) {
if (url == null || url.isEmpty()) {
return false;
}
// 检查是否包含协议
if (!url.startsWith("http://") && !url.startsWith("https://")) {
return false;
}
// 简单的URL格式检查
try {
URI uri = URI.create(url);
return uri.getHost() != null && !uri.getHost().isEmpty();
} catch (Exception e) {
return false;
}
}
/**
* 获取网页内容
* @param url 网页URL地址
* @return 网页HTML内容
*/
private String fetchWebContent(String url) {
StringBuilder content = new StringBuilder();
BufferedReader reader = null;
try {
URLConnection connection = new URL(url).openConnection();
// 设置请求头,模拟浏览器访问
connection.setRequestProperty("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
connection.setConnectTimeout(5000); // 5秒连接超时
connection.setReadTimeout(10000); // 10秒读取超时
reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
log.debug("成功获取网页内容,长度: {} bytes", content.length());
return content.toString();
} catch (Exception e) {
log.error("获取网页内容失败: {}", e.getMessage(), e);
// 返回错误提示页面
return "<html><body><h1>获取网页内容失败</h1><p>错误信息: " + e.getMessage() + "</p></body></html>";
} finally {
if (reader != null) {
try {
reader.close();
} catch (Exception e) {
log.debug("关闭读取器失败: {}", e.getMessage());
}
}
}
}
}
\ No newline at end of file
package pangea.hiagent.tools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
/**
* 创作风格参考工具
* 用于提供各种写作风格的参考和指导
*/
@Slf4j
@Component
public class WritingStyleReferenceTool {
public WritingStyleReferenceTool() {
// 默认构造器
}
/**
* 获取写作风格参考
* @param styleType 风格类型(如正式、幽默、亲切等)
* @param contentType 内容类型(如商务邮件、社交媒体、技术文档等)
* @return 写作风格参考信息
*/
@Tool(description = "根据风格类型和内容类型提供写作风格参考和指导")
public String getWritingStyleReference(String styleType, String contentType) {
log.debug("开始获取写作风格参考: 风格类型={}, 内容类型={}", styleType, contentType);
try {
if (styleType == null || styleType.trim().isEmpty()) {
log.warn("风格类型不能为空");
return "错误:风格类型不能为空";
}
if (contentType == null || contentType.trim().isEmpty()) {
log.warn("内容类型不能为空");
return "错误:内容类型不能为空";
}
// 生成写作风格参考
String reference = generateWritingStyleReference(styleType, contentType);
log.info("写作风格参考生成完成: 风格类型={}, 内容类型={}", styleType, contentType);
return reference;
} catch (Exception e) {
log.error("获取写作风格参考时发生错误: {}", e.getMessage(), e);
return "获取写作风格参考时发生错误: " + e.getMessage();
}
}
/**
* 生成写作风格参考
* @param styleType 风格类型
* @param contentType 内容类型
* @return 写作风格参考信息
*/
private String generateWritingStyleReference(String styleType, String contentType) {
StringBuilder reference = new StringBuilder();
reference.append("写作风格参考:\n\n");
reference.append("风格类型: ").append(styleType).append("\n");
reference.append("内容类型: ").append(contentType).append("\n\n");
// 根据风格类型和内容类型提供参考
switch (styleType.toLowerCase()) {
case "正式":
reference.append("正式风格特点:\n");
reference.append("- 使用完整句子和标准语法\n");
reference.append("- 避免缩写和俚语\n");
reference.append("- 使用专业术语和正式词汇\n");
reference.append("- 保持客观和中立的语调\n\n");
break;
case "幽默":
reference.append("幽默风格特点:\n");
reference.append("- 使用轻松愉快的语言\n");
reference.append("- 适当使用比喻和双关语\n");
reference.append("- 创造轻松的氛围\n");
reference.append("- 注意幽默的适度性\n\n");
break;
case "亲切":
reference.append("亲切风格特点:\n");
reference.append("- 使用第二人称\"你\"\n");
reference.append("- 采用对话式的表达\n");
reference.append("- 表达关心和理解\n");
reference.append("- 使用温暖友好的词汇\n\n");
break;
case "简洁":
reference.append("简洁风格特点:\n");
reference.append("- 使用短句和简单结构\n");
reference.append("- 删除不必要的修饰词\n");
reference.append("- 直接表达核心观点\n");
reference.append("- 避免冗余信息\n\n");
break;
default:
reference.append("通用风格特点:\n");
reference.append("- 根据目标受众调整语言\n");
reference.append("- 保持一致的语调\n");
reference.append("- 注意段落结构和逻辑\n");
reference.append("- 确保内容清晰易懂\n\n");
break;
}
// 根据内容类型提供具体建议
switch (contentType.toLowerCase()) {
case "商务邮件":
reference.append("商务邮件写作建议:\n");
reference.append("- 明确的主题行\n");
reference.append("- 专业的称呼和结尾\n");
reference.append("- 清晰的行动呼吁\n");
reference.append("- 适当的附件提醒\n\n");
break;
case "社交媒体":
reference.append("社交媒体写作建议:\n");
reference.append("- 使用吸引人的开头\n");
reference.append("- 适当使用表情符号\n");
reference.append("- 包含相关的标签\n");
reference.append("- 鼓励互动和分享\n\n");
break;
case "技术文档":
reference.append("技术文档写作建议:\n");
reference.append("- 使用精确的技术术语\n");
reference.append("- 提供清晰的步骤说明\n");
reference.append("- 包含示例和截图\n");
reference.append("- 注意版本和兼容性信息\n\n");
break;
case "营销文案":
reference.append("营销文案写作建议:\n");
reference.append("- 突出产品卖点\n");
reference.append("- 使用感性的语言\n");
reference.append("- 创造紧迫感\n");
reference.append("- 包含明确的购买引导\n\n");
break;
default:
reference.append("通用内容写作建议:\n");
reference.append("- 明确写作目的\n");
reference.append("- 了解目标受众\n");
reference.append("- 保持内容结构清晰\n");
reference.append("- 注意语言的准确性\n\n");
break;
}
reference.append("写作技巧:\n");
reference.append("- 开头吸引注意力\n");
reference.append("- 中间内容条理清晰\n");
reference.append("- 结尾有力且印象深刻\n");
reference.append("- 多次审阅和修改\n");
return reference.toString();
}
}
\ No newline at end of file
package pangea.hiagent.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* JSON工具类
* 提供JSON序列化和反序列化功能
*/
@Slf4j
public class JsonUtils {
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 将对象转换为JSON字符串
*
* @param obj 待转换的对象
* @return JSON字符串
*/
public static String toJson(Object obj) {
try {
if (obj == null) {
log.warn("尝试将null对象转换为JSON");
return null;
}
String json = objectMapper.writeValueAsString(obj);
log.debug("对象转JSON成功,对象类型: {}, JSON长度: {}", obj.getClass().getSimpleName(), json.length());
return json;
} catch (JsonProcessingException e) {
log.error("对象转JSON失败", e);
return null;
}
}
/**
* 将JSON字符串转换为指定类型的对象
*
* @param json JSON字符串
* @param clazz 目标类型
* @param <T> 泛型
* @return 转换后的对象
*/
public static <T> T fromJson(String json, Class<T> clazz) {
if (clazz == null) {
log.warn("目标类型不能为null");
return null;
}
try {
if (json == null || json.isEmpty()) {
log.warn("尝试将空JSON字符串转换为对象");
return null;
}
T obj = objectMapper.readValue(json, clazz);
log.debug("JSON转对象成功,目标类型: {}", clazz.getSimpleName());
return obj;
} catch (Exception e) {
log.error("JSON转对象失败,JSON: {}, 目标类型: {}", json, clazz.getSimpleName(), e);
return null;
}
}
/**
* 将JSON字符串转换为Map对象
*
* @param json JSON字符串
* @return Map对象
*/
public static Map<String, Object> fromJsonToMap(String json) {
try {
return objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {});
} catch (Exception e) {
log.error("JSON转Map失败", e);
return null;
}
}
}
\ No newline at end of file
package pangea.hiagent.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import pangea.hiagent.config.JwtProperties;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT工具类
* 负责生成、解析和验证JWT Token
*/
@Slf4j
@Component
public class JwtUtil {
private final JwtProperties jwtProperties;
public JwtUtil(JwtProperties jwtProperties) {
this.jwtProperties = jwtProperties;
}
/**
* 生成JWT Token
*
* @param userId 用户ID
* @return JWT Token字符串
*/
public String generateToken(String userId) {
log.debug("开始生成Token,用户ID: {}", userId);
if (userId == null || userId.isEmpty()) {
log.warn("用户ID不能为空");
return null;
}
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
String token = createToken(claims, userId);
log.debug("Token生成成功,长度: {}", token != null ? token.length() : 0);
return token;
}
/**
* 生成JWT Token(带自定义claims)
*
* @param userId 用户ID
* @param claims 自定义claims
* @return JWT Token字符串
*/
public String generateToken(String userId, Map<String, Object> claims) {
log.debug("开始生成Token(带自定义claims),用户ID: {}", userId);
if (userId == null || userId.isEmpty()) {
log.warn("用户ID不能为空");
return null;
}
if (claims == null) {
claims = new HashMap<>();
}
claims.put("userId", userId);
String token = createToken(claims, userId);
log.debug("Token生成成功(带自定义claims),长度: {}", token != null ? token.length() : 0);
return token;
}
/**
* 创建Token
*/
private String createToken(Map<String, Object> claims, String subject) {
Date now = new Date();
Date expiration = new Date(now.getTime() + jwtProperties.getExpiration());
log.debug("创建Token - 当前时间: {}, 过期时间: {}, 有效期: {}毫秒", now, expiration, jwtProperties.getExpiration());
return Jwts.builder()
.claims(claims)
.subject(subject)
.issuedAt(now)
.expiration(expiration)
.signWith(getSignInKey())
.compact();
}
/**
* 获取签名密钥
*/
private SecretKey getSignInKey() {
byte[] keyBytes = jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* 解析Token获取所有claims
*/
public Claims getClaimsFromToken(String token) {
try {
log.debug("开始解析Token: {}", token);
Claims claims = Jwts.parser()
.verifyWith(getSignInKey())
.build()
.parseSignedClaims(token)
.getPayload();
log.debug("Token解析成功,claims: {}", claims);
return claims;
} catch (Exception e) {
log.error("获取Token claims失败", e);
return null;
}
}
/**
* 从Token获取用户ID
*/
public String getUserIdFromToken(String token) {
Claims claims = getClaimsFromToken(token);
if (claims != null) {
String userId = claims.get("userId", String.class);
log.debug("从Token中提取用户ID: {}", userId);
return userId;
}
log.warn("无法从Token中提取用户ID,claims为空");
return null;
}
/**
* 从Token获取Subject
*/
public String getSubjectFromToken(String token) {
Claims claims = getClaimsFromToken(token);
if (claims != null) {
String subject = claims.getSubject();
log.debug("从Token中提取Subject: {}", subject);
return subject;
}
log.warn("无法从Token中提取Subject,claims为空");
return null;
}
/**
* 检查Token是否过期
*/
public Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
if (claims == null) {
log.debug("Token claims为空,标记为过期");
return true;
}
Date expiration = claims.getExpiration();
boolean expired = expiration.before(new Date());
log.debug("Token过期检查结果: {},过期时间: {}, 当前时间: {}", expired, expiration, new Date());
return expired;
} catch (Exception e) {
log.error("检查Token是否过期时发生异常", e);
return true; // 出现异常时认为token已过期
}
}
/**
* 验证Token是否有效
*/
public Boolean validateToken(String token, String userId) {
String tokenUserId = getUserIdFromToken(token);
boolean isValid = tokenUserId != null && tokenUserId.equals(userId) && !isTokenExpired(token);
log.debug("Token验证结果: {}, Token用户ID: {}, 期望用户ID: {}, 是否过期: {}",
isValid, tokenUserId, userId, isTokenExpired(token));
return isValid;
}
/**
* 验证Token是否有效(仅检查过期时间)
*/
public Boolean validateToken(String token) {
try {
log.debug("开始验证Token: {}", token);
boolean isValid = !isTokenExpired(token);
log.debug("Token验证结果: {}, 是否过期: {}", isValid, isTokenExpired(token));
return isValid;
} catch (Exception e) {
log.error("验证Token时发生异常", e);
return false; // 出现异常时认为token无效
}
}
}
\ No newline at end of file
package pangea.hiagent.websocket;
import com.alibaba.fastjson2.JSON;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.LoadState;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import pangea.hiagent.dto.DomSyncData;
import java.io.ByteArrayOutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.zip.GZIPOutputStream;
/**
* DOM同步的WebSocket处理器
*/
public class DomSyncHandler extends TextWebSocketHandler {
// 存储连接的前端客户端(线程安全)
private static final ConcurrentMap<WebSocketSession, String> clients = new ConcurrentHashMap<>();
// Playwright核心实例
private Playwright playwright;
private Browser browser;
private Page page;
private BrowserContext context;
// 压缩和分片配置
private static final int CHUNK_SIZE = 64 * 1024; // 64KB分片大小
private static final boolean ENABLE_COMPRESSION = true; // 是否启用压缩
// 连接数限制
private static final int MAX_CONNECTIONS_PER_USER = 5; // 每用户最大连接数
private static final int MAX_COMMANDS_PER_SECOND = 10; // 每秒最大指令数
// 用户连接计数
private static final ConcurrentMap<String, Integer> userConnections = new ConcurrentHashMap<>();
// 指令频率限制
private static final ConcurrentMap<String, Long> lastCommandTimes = new ConcurrentHashMap<>();
private static final ConcurrentMap<String, Integer> commandCounts = new ConcurrentHashMap<>();
// 统计信息
private static final ConcurrentMap<String, Long> messageCounters = new ConcurrentHashMap<>();
// 增加计数器
private void incrementCounter(String counterName) {
messageCounters.merge(counterName, 1L, Long::sum);
}
// 获取计数器值
public static long getCounter(String counterName) {
return messageCounters.getOrDefault(counterName, 0L);
}
// 重置计数器
public static void resetCounter(String counterName) {
messageCounters.put(counterName, 0L);
}
// 初始化Playwright与页面
public DomSyncHandler() {
initPlaywright();
initPageListener();
}
/**
* 初始化Playwright(服务器端无头模式,适配生产环境)
*/
private void initPlaywright() {
try {
System.out.println("正在初始化Playwright...");
playwright = Playwright.create();
// 启动Chrome无头实例,添加必要参数
browser = playwright.chromium().launch(new BrowserType.LaunchOptions()
.setHeadless(true) // 无头模式,无界面
.setArgs(java.util.Arrays.asList(
"--no-sandbox", // 服务器端必须,否则Chrome启动失败
"--disable-dev-shm-usage", // 解决Linux下/dev/shm空间不足的问题
"--disable-gpu", // 禁用GPU,服务器端无需
"--remote-allow-origins=*", // 允许跨域请求(处理外部样式/脚本)
"--disable-web-security", // 禁用网络安全检查(便于测试)
"--allow-running-insecure-content" // 允许不安全内容
)));
// 创建浏览器上下文(隔离环境)
context = browser.newContext(new Browser.NewContextOptions()
.setViewportSize(1920, 1080) // 设置视口大小,与前端一致
.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")); // 设置用户代理
page = context.newPage();
// 设置默认超时时间,避免长时间等待
page.setDefaultTimeout(10000); // 10秒超时
System.out.println("Playwright初始化成功");
} catch (Exception e) {
System.err.println("Playwright初始化失败: " + e.getMessage());
e.printStackTrace();
// 尝试重新初始化
try {
System.out.println("尝试重新初始化Playwright...");
destroy(); // 先清理现有资源
Thread.sleep(1000); // 等待1秒
playwright = Playwright.create();
browser = playwright.chromium().launch(new BrowserType.LaunchOptions()
.setHeadless(true)
.setArgs(java.util.Arrays.asList(
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
"--remote-allow-origins=*")));
context = browser.newContext(new Browser.NewContextOptions()
.setViewportSize(1920, 1080));
page = context.newPage();
page.setDefaultTimeout(10000); // 10秒超时
System.out.println("Playwright重新初始化成功");
} catch (Exception re) {
System.err.println("Playwright重新初始化失败: " + re.getMessage());
re.printStackTrace();
}
}
}
/**
* 初始化页面监听事件(核心:捕获DOM、样式、脚本变化)
*/
private void initPageListener() {
// 初始化统计计数器
messageCounters.put("domChanges", 0L);
messageCounters.put("websocketMessages", 0L);
messageCounters.put("sseEvents", 0L);
messageCounters.put("errors", 0L);
// 1. 页面加载完成后,推送完整的DOM、样式、脚本(初始化)
page.onLoad(page -> {
System.out.println("页面加载完成: " + page.url());
incrementCounter("pageLoads");
sendFullDomAndResourceToClients();
});
// 2. 监听DOM变化(使用MutationObserver),推送增量更新
page.evaluate("() => {\n" +
" // 创建MutationObserver监听DOM变化\n" +
" const observer = new MutationObserver((mutations) => {\n" +
" // 将变化的DOM节点转为字符串,发送给Playwright\n" +
" const changes = mutations.map(mutation => ({\n" +
" type: mutation.type,\n" +
" target: mutation.target.outerHTML,\n" +
" addedNodes: Array.from(mutation.addedNodes).map(node => node.outerHTML || ''),\n" +
" removedNodes: Array.from(mutation.removedNodes).map(node => node.outerHTML || '')\n" +
" }));\n" +
" // 调用Playwright的暴露函数,传递DOM变化数据\n" +
" window.domChanged(JSON.stringify(changes));\n" +
" });\n" +
" // 配置监听:监听所有节点的添加/删除、属性变化、子节点变化\n" +
" observer.observe(document.body, {\n" +
" childList: true,\n" +
" attributes: true,\n" +
" subtree: true,\n" +
" characterData: true\n" +
" });\n" +
"}");
// 3. 暴露Playwright函数,接收前端的DOM变化数据
page.exposeFunction("domChanged", args -> {
try {
if (args.length > 0 && args[0] instanceof String) {
String changes = (String) args[0];
if (changes != null && !changes.isEmpty()) {
incrementCounter("domChanges");
sendIncrementalDomToClients(changes);
}
}
} catch (Exception e) {
String errorMsg = "处理DOM变化失败: " + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
incrementCounter("errors");
sendErrorToClients(errorMsg);
}
return null;
});
// 4. 监听WebSocket连接
page.onWebSocket(webSocket -> {
System.out.println("检测到WebSocket连接: " + webSocket.url());
// 监听WebSocket的消息接收事件
webSocket.onFrameReceived(frame -> {
try {
incrementCounter("websocketMessages");
// 封装WebSocket消息,推送给前端
DomSyncData wsData = new DomSyncData(
"ws", // WebSocket类型
webSocket.url(), // WebSocket地址
frame.text(), // 消息内容
"",
getCurrentPageUrl()
);
String jsonData = JSON.toJSONString(wsData);
broadcastMessage(jsonData);
} catch (Exception e) {
String errorMsg = "处理WebSocket消息失败: " + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
incrementCounter("errors");
sendErrorToClients(errorMsg);
}
});
// 监听WebSocket关闭事件
webSocket.onClose(closedWebSocket -> {
try {
String closeInfo = "WebSocket连接已关闭: " + closedWebSocket.url();
System.out.println(closeInfo);
DomSyncData wsCloseData = new DomSyncData(
"ws-close", // WebSocket关闭类型
closedWebSocket.url(), // WebSocket地址
closeInfo, // 关闭信息
"",
getCurrentPageUrl()
);
String jsonData = JSON.toJSONString(wsCloseData);
broadcastMessage(jsonData);
} catch (Exception e) {
String errorMsg = "处理WebSocket关闭事件失败: " + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
incrementCounter("errors");
sendErrorToClients(errorMsg);
}
});
// 监听WebSocket帧发送事件
webSocket.onFrameSent(frame -> {
try {
// 记录发送的WebSocket帧(可用于调试)
System.out.println("WebSocket发送帧到: " + webSocket.url() + ", 内容长度: " +
(frame.text() != null ? frame.text().length() : 0));
} catch (Exception e) {
// 静默处理,避免影响主流程
System.err.println("记录WebSocket发送帧失败: " + e.getMessage());
}
});
// 注意:Playwright Java WebSocket API 可能不支持 onError 回调
// 如果需要错误处理,可以通过其他方式实现
});
// 5. 监听SSE(Server-Sent Events)连接
page.onResponse(response -> {
try {
// 检查是否为SSE响应
String contentType = response.headers().get("content-type");
if (contentType != null && contentType.contains("text/event-stream")) {
System.out.println("检测到SSE响应: " + response.url());
incrementCounter("sseResponses");
// 添加页面错误监听器来捕获SSE相关的错误
response.frame().page().onPageError(error -> {
try {
String errorMsg = "页面错误 (可能与SSE相关): " + error;
System.err.println(errorMsg);
incrementCounter("errors");
// 发送错误信息给客户端
DomSyncData errorData = new DomSyncData(
"sse-error", // SSE错误类型
response.url(), // SSE地址
errorMsg, // 错误信息
"",
getCurrentPageUrl()
);
broadcastMessage(JSON.toJSONString(errorData));
} catch (Exception e) {
System.err.println("处理SSE页面错误失败: " + e.getMessage());
e.printStackTrace();
incrementCounter("errors");
}
});
// 尝试通过控制台消息捕获SSE数据
response.frame().page().onConsoleMessage(consoleMessage -> {
try {
// 检查是否可能是SSE相关的消息
String text = consoleMessage.text();
if (text != null && (text.contains("event:") || text.contains("data:") || text.contains("id:"))) {
System.out.println("检测到可能的SSE数据: " + text);
incrementCounter("sseEvents");
// 处理SSE数据
DomSyncData sseDataObj = new DomSyncData(
"sse", // SSE类型
response.url(), // SSE地址
text, // SSE内容
"",
getCurrentPageUrl()
);
broadcastMessage(JSON.toJSONString(sseDataObj));
}
} catch (Exception e) {
System.err.println("处理SSE控制台消息失败: " + e.getMessage());
e.printStackTrace();
incrementCounter("errors");
}
});
// 尝试通过请求完成事件来捕获SSE数据
response.frame().page().onRequestFinished(request -> {
try {
// 检查是否是SSE请求
if (request.url().equals(response.url())) {
System.out.println("SSE请求完成: " + request.url());
// 可以在这里添加额外的处理逻辑
}
} catch (Exception e) {
System.err.println("处理SSE请求完成事件失败: " + e.getMessage());
e.printStackTrace();
incrementCounter("errors");
}
});
}
} catch (Exception e) {
String errorMsg = "检查SSE响应失败: " + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
incrementCounter("errors");
sendErrorToClients(errorMsg);
}
});
// 6. 监听页面导航事件,导航后重新初始化
page.onFrameNavigated(frame -> {
System.out.println("检测到页面导航: " + frame.url());
incrementCounter("navigations");
// 导航完成后,等待页面加载
try {
page.waitForLoadState(LoadState.LOAD);
System.out.println("页面加载完成: " + frame.url());
// 发送更新后的DOM内容到客户端
sendFullDomAndResourceToClients();
} catch (Exception e) {
String errorMsg = "页面加载状态等待失败: " + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
incrementCounter("errors");
sendErrorToClients(errorMsg);
}
});
// 7. 监听页面错误事件
page.onPageError(error -> {
try {
String errorMsg = "页面错误: " + error;
System.err.println(errorMsg);
incrementCounter("errors");
sendErrorToClients(errorMsg);
} catch (Exception e) {
System.err.println("处理页面错误事件失败: " + e.getMessage());
e.printStackTrace();
}
});
// 8. 监听控制台消息事件
page.onConsoleMessage(message -> {
try {
String consoleMsg = "控制台消息 [" + message.type() + "]: " + message.text();
System.out.println(consoleMsg);
// 如果是错误类型的消息,也发送给客户端
if ("error".equals(message.type())) {
incrementCounter("consoleErrors");
sendErrorToClients(consoleMsg);
}
} catch (Exception e) {
System.err.println("处理控制台消息事件失败: " + e.getMessage());
e.printStackTrace();
}
});
}
/**
* 推送完整的DOM、样式、脚本给所有客户端(初始化时调用)
*/
private void sendFullDomAndResourceToClients() {
try {
// 1. 获取页面完整DOM(包含所有节点)
String fullDom = page.content();
// 2. 获取页面所有样式(内联样式+外部样式表内容)
String fullStyle = getPageAllStyles();
// 3. 获取页面所有脚本(内联脚本+外部脚本的URL,前端自行加载)
String fullScript = getPageAllScripts();
// 4. 封装数据
DomSyncData data = new DomSyncData(
"init", // 初始化类型
fullDom,
fullStyle,
fullScript,
getCurrentPageUrl() // 当前页面URL
);
// 5. 序列化为JSON并推送
String jsonData = JSON.toJSONString(data);
broadcastMessage(jsonData);
} catch (Exception e) {
e.printStackTrace();
sendErrorToClients("获取完整DOM失败:" + e.getMessage());
}
}
/**
* 获取页面所有样式(内联+外部)
* 注意:外部样式表通过Playwright请求获取内容,避免前端跨域问题
*/
private String getPageAllStyles() {
try {
Object result = page.evaluate("async () => {" +
"let styleText = '';" +
"// 1. 获取内联样式(<style>标签)" +
"document.querySelectorAll('style').forEach(style => {" +
" styleText += style.textContent + '\\n';" +
"});" +
"// 2. 获取外部样式表(<link rel=\"stylesheet\">)" +
"const linkElements = document.querySelectorAll('link[rel=\"stylesheet\"]');" +
"for (let link of linkElements) {" +
" try {" +
" const response = await fetch(link.href);" +
" if (response.ok) {" +
" const cssText = await response.text();" +
" styleText += cssText + '\\n';" +
" }" +
" } catch (e) {" +
" console.error('加载外部样式表失败:', link.href, e);" +
" }" +
"}" +
"return styleText;" +
"}");
return result != null ? result.toString() : "";
} catch (Exception e) {
System.err.println("获取页面样式失败: " + e.getMessage());
e.printStackTrace();
return "";
}
}
/**
* 获取页面所有脚本(内联脚本+外部脚本URL,前端按需加载)
* 外部脚本不直接获取内容,避免体积过大,前端通过URL加载(需处理跨域)
*/
private String getPageAllScripts() {
try {
Object result = page.evaluate("() => {" +
"let scriptData = {" +
" inline: [], // 内联脚本内容" +
" external: [] // 外部脚本URL" +
"};" +
"// 1. 获取内联脚本(<script>标签,无src属性)" +
"document.querySelectorAll('script:not([src])').forEach(script => {" +
" scriptData.inline.push(script.textContent);" +
"});" +
"// 2. 获取外部脚本URL(<script>标签,有src属性)" +
"document.querySelectorAll('script[src]').forEach(script => {" +
" scriptData.external.push(script.src);" +
"});" +
"return JSON.stringify(scriptData);" +
"}");
return result != null ? result.toString() : "{}";
} catch (Exception e) {
System.err.println("获取页面脚本失败: " + e.getMessage());
e.printStackTrace();
return "{}";
}
}
/**
* 推送增量DOM变化给所有客户端(DOM更新时调用)
*/
private void sendIncrementalDomToClients(String changes) {
try {
DomSyncData data = new DomSyncData(
"update", // 增量更新类型
changes, // DOM变化数据
"", // 样式无增量,空字符串
"", // 脚本无增量,空字符串
getCurrentPageUrl()
);
String jsonData = JSON.toJSONString(data);
broadcastMessage(jsonData);
} catch (Exception e) {
e.printStackTrace();
sendErrorToClients("推送增量DOM失败:" + e.getMessage());
}
}
/**
* 广播消息给所有客户端(带压缩和分片)
*/
private void broadcastMessage(String message) {
if (ENABLE_COMPRESSION && message.getBytes().length > CHUNK_SIZE) {
// 启用压缩且消息较大时,进行压缩和分片
try {
byte[] compressedData = compressData(message);
sendChunkedData(compressedData);
} catch (Exception e) {
e.printStackTrace();
// 压缩失败时,直接发送原始数据
sendRawMessage(message);
}
} else {
// 不启用压缩或消息较小时,直接发送
sendRawMessage(message);
}
}
/**
* 发送原始消息(不分片)
*/
private void sendRawMessage(String message) {
TextMessage textMessage = new TextMessage(message);
for (WebSocketSession client : clients.keySet()) {
try {
if (client.isOpen()) {
client.sendMessage(textMessage);
}
} catch (Exception e) {
e.printStackTrace();
// 发送失败时移除客户端
clients.remove(client);
}
}
}
/**
* 压缩数据(使用GZIP)
*/
private byte[] compressData(String data) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
GZIPOutputStream gzipOut = new GZIPOutputStream(baos);
gzipOut.write(data.getBytes("UTF-8"));
gzipOut.close();
return baos.toByteArray();
}
/**
* 发送分片数据
*/
private void sendChunkedData(byte[] data) {
try {
int totalChunks = (int) Math.ceil((double) data.length / CHUNK_SIZE);
for (int i = 0; i < totalChunks; i++) {
int start = i * CHUNK_SIZE;
int end = Math.min(start + CHUNK_SIZE, data.length);
byte[] chunk = java.util.Arrays.copyOfRange(data, start, end);
// 构造分片消息
DomSyncData chunkData = new DomSyncData(
"chunk", // 分片类型
String.valueOf(i), // 当前分片索引
String.valueOf(totalChunks), // 总分片数
java.util.Base64.getEncoder().encodeToString(chunk), // 分片内容(Base64编码)
""
);
String jsonData = JSON.toJSONString(chunkData);
sendRawMessage(jsonData);
}
} catch (Exception e) {
System.err.println("发送分片数据失败: " + e.getMessage());
e.printStackTrace();
sendErrorToClients("发送分片数据失败: " + e.getMessage());
}
}
/**
* 发送错误信息给所有客户端
*/
private void sendErrorToClients(String errorMessage) {
DomSyncData errorData = new DomSyncData(
"error", // 错误类型
errorMessage, // 错误信息
"",
"",
getCurrentPageUrl()
);
String jsonData = JSON.toJSONString(errorData);
broadcastMessage(jsonData);
}
// ===================== WebSocket生命周期方法 =====================
/**
* 客户端连接建立时触发
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) {
// 生产环境:此处添加Token认证逻辑(参考之前的权限控制部分)
// 从会话属性中获取用户ID
String userId = (String) session.getAttributes().get("userId");
if (userId == null) {
userId = "anonymous"; // 默认匿名用户
}
// 检查连接数限制
Integer currentConnections = userConnections.getOrDefault(userId, 0);
if (currentConnections >= MAX_CONNECTIONS_PER_USER) {
try {
session.close(CloseStatus.POLICY_VIOLATION.withReason("超过最大连接数限制"));
return;
} catch (Exception e) {
e.printStackTrace();
}
}
// 增加用户连接数
userConnections.put(userId, currentConnections + 1);
clients.put(session, session.getId());
System.out.println("客户端连接成功:" + session.getId() + ",用户:" + userId);
// 连接建立后不立即推送DOM,而是等待客户端发送navigate指令
// 这样可以避免在WebSocket连接建立但iframe还未准备好时推送数据
System.out.println("WebSocket连接已建立,等待客户端发送导航指令...");
}
/**
* 处理客户端发送的指令(如导航、点击、输入)
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
String payload = message.getPayload();
// 从会话属性中获取用户ID
String userId = (String) session.getAttributes().get("userId");
if (userId == null) {
userId = "anonymous"; // 默认匿名用户
}
// 指令频率限制
long currentTime = System.currentTimeMillis();
Long lastTime = lastCommandTimes.getOrDefault(userId, 0L);
Integer commandCount = commandCounts.getOrDefault(userId, 0);
// 检查是否超过每秒最大指令数
if (currentTime - lastTime < 1000) {
// 同一秒内
if (commandCount >= MAX_COMMANDS_PER_SECOND) {
sendErrorToClients("指令执行过于频繁,请稍后再试");
return;
}
commandCounts.put(userId, commandCount + 1);
} else {
// 新的一秒
lastCommandTimes.put(userId, currentTime);
commandCounts.put(userId, 1);
}
try {
// 检查Playwright实例是否有效
if (!isPlaywrightInstanceValid()) {
sendErrorToClients("Playwright实例未初始化或已关闭,请刷新页面重试");
return;
}
// 检查WebSocket连接状态
if (!session.isOpen()) {
sendErrorToClients("WebSocket连接已关闭");
return;
}
// 指令格式:指令类型:参数(如navigate:https://www.baidu.com)
// 特殊处理navigate命令,避免URL中的冒号导致分割错误
String command;
String param;
int firstColonIndex = payload.indexOf(':');
if (firstColonIndex != -1) {
command = payload.substring(0, firstColonIndex);
param = payload.substring(firstColonIndex + 1);
} else {
command = payload;
param = "";
}
switch (command) {
case "navigate":
// 导航到指定URL
if (param == null || param.trim().isEmpty()) {
sendErrorToClients("导航URL不能为空");
return;
}
// 验证URL格式
if (!isValidUrl(param)) {
sendErrorToClients("无效的URL格式: " + param);
return;
}
try {
System.out.println("正在导航到URL: " + param);
page.navigate(param);
System.out.println("导航成功: " + param);
} catch (Exception e) {
String errorMsg = "导航失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "stats":
// 获取统计信息
try {
String stats = getStatisticsSummary();
DomSyncData statsData = new DomSyncData(
"stats", // 统计信息类型
stats, // 统计信息内容
"",
"",
page != null ? page.url() : ""
);
String jsonData = JSON.toJSONString(statsData);
broadcastMessage(jsonData);
} catch (Exception e) {
String errorMsg = "获取统计信息失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "reset-stats":
// 重置统计信息
try {
resetAllCounters();
sendErrorToClients("统计信息已重置");
} catch (Exception e) {
String errorMsg = "重置统计信息失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "click":
// 点击指定选择器的元素(如#su)
try {
// 检查选择器是否为容器元素本身
if ("#dom-view".equals(param)) {
sendErrorToClients("不能对容器元素 #dom-view 执行点击操作");
} else {
page.locator(param).click();
}
} catch (Exception e) {
String errorMsg = "点击元素失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "dblclick":
// 双击指定选择器的元素
try {
// 检查选择器是否为容器元素本身
if ("#dom-view".equals(param)) {
sendErrorToClients("不能对容器元素 #dom-view 执行双击操作");
} else {
page.locator(param).dblclick();
}
} catch (Exception e) {
String errorMsg = "双击元素失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "hover":
// 悬停在指定选择器的元素上
try {
// 检查选择器是否为容器元素本身,避免对容器元素执行hover操作
if ("#dom-view".equals(param)) {
// 忽略对容器元素本身的hover操作,不报错但也不执行
System.out.println("忽略对容器元素 #dom-view 的hover操作");
} else {
page.locator(param).hover();
}
} catch (Exception e) {
String errorMsg = "悬停元素失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "mousedown":
// 鼠标按下指定选择器的元素
try {
// 检查选择器是否为容器元素本身
if ("#dom-view".equals(param)) {
sendErrorToClients("不能对容器元素 #dom-view 执行鼠标按下操作");
} else {
page.mouse().down(); // 使用page.mouse()而不是locator.mouse()
}
} catch (Exception e) {
String errorMsg = "鼠标按下失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "mouseup":
// 鼠标释放指定选择器的元素
try {
// 检查选择器是否为容器元素本身
if ("#dom-view".equals(param)) {
sendErrorToClients("不能对容器元素 #dom-view 执行鼠标释放操作");
} else {
page.mouse().up(); // 使用page.mouse()而不是locator.mouse()
}
} catch (Exception e) {
String errorMsg = "鼠标释放失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "focus":
// 聚焦到指定元素
try {
// 检查选择器是否为容器元素本身
if ("#dom-view".equals(param)) {
sendErrorToClients("不能对容器元素 #dom-view 执行聚焦操作");
} else {
page.locator(param).focus();
}
} catch (Exception e) {
String errorMsg = "聚焦元素失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "blur":
// 失去焦点
try {
// 检查选择器是否为容器元素本身
if ("#dom-view".equals(param)) {
sendErrorToClients("不能对容器元素 #dom-view 执行失去焦点操作");
} else {
page.locator(param).blur();
}
} catch (Exception e) {
String errorMsg = "失去焦点失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "select":
// 选择下拉选项(格式:选择器:值)
try {
String[] selectParts = param.split(":", 2);
if (selectParts.length == 2) {
page.locator(selectParts[0]).selectOption(selectParts[1]);
}
} catch (Exception e) {
String errorMsg = "选择选项失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "keydown":
// 键盘按下事件(格式:键码)
try {
page.keyboard().down(param);
} catch (Exception e) {
String errorMsg = "键盘按下失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "keyup":
// 键盘释放事件(格式:键码)
try {
page.keyboard().up(param);
} catch (Exception e) {
String errorMsg = "键盘释放失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "keypress":
// 键盘按键事件(格式:键码)
try {
page.keyboard().press(param);
} catch (Exception e) {
String errorMsg = "按键事件失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "scroll":
// 滚动页面(格式:y坐标,如500)
try {
int scrollY = Integer.parseInt(param);
page.evaluate("window.scrollTo(0, " + scrollY + ")");
} catch (Exception e) {
String errorMsg = "滚动页面失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "type":
// 输入内容(格式:选择器:内容,如#kw:Java Playwright)
try {
// 使用indexOf和substring替代split,避免内容中的冒号导致分割错误
int colonIndex = param.indexOf(':');
if (colonIndex != -1 && colonIndex < param.length() - 1) {
String selector = param.substring(0, colonIndex);
String content = param.substring(colonIndex + 1);
// 检查选择器是否为容器元素本身
if ("#dom-view".equals(selector)) {
sendErrorToClients("不能对容器元素 #dom-view 执行输入操作");
} else {
Locator inputLocator = page.locator(selector);
inputLocator.fill(content); // 使用fill替代type,因为type方法已被弃用
}
}
} catch (Exception e) {
String errorMsg = "输入内容失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
case "input":
// 更丰富的输入场景(格式:选择器:内容)
try {
// 使用indexOf和substring替代split,避免内容中的冒号导致分割错误
int colonIndex = param.indexOf(':');
if (colonIndex != -1 && colonIndex < param.length() - 1) {
String selector = param.substring(0, colonIndex);
String content = param.substring(colonIndex + 1);
// 检查选择器是否为容器元素本身
if ("#dom-view".equals(selector)) {
sendErrorToClients("不能对容器元素 #dom-view 执行输入操作");
} else {
Locator inputLocator = page.locator(selector);
inputLocator.fill(content); // 使用fill替代type,支持更丰富的输入
}
}
} catch (Exception e) {
String errorMsg = "输入内容失败:" + e.getMessage();
System.err.println(errorMsg);
e.printStackTrace();
sendErrorToClients(errorMsg);
}
break;
default:
sendErrorToClients("未知指令:" + command);
}
} catch (Exception e) {
e.printStackTrace();
sendErrorToClients("指令执行失败:" + e.getMessage());
}
}
/**
* 客户端连接关闭时触发
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
clients.remove(session);
System.out.println("客户端断开连接:" + session.getId());
// 从会话属性中获取用户ID
String userId = (String) session.getAttributes().get("userId");
if (userId == null) {
userId = "anonymous"; // 默认匿名用户
}
// 减少用户连接数
Integer currentConnections = userConnections.getOrDefault(userId, 0);
if (currentConnections > 0) {
userConnections.put(userId, currentConnections - 1);
}
}
/**
* 处理WebSocket传输错误
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) {
clients.remove(session);
System.out.println("客户端传输错误:" + session.getId() + ",错误信息:" + exception.getMessage());
}
/**
* 验证URL格式是否有效
*
* @param url 要验证的URL
* @return 如果URL有效返回true,否则返回false
*/
private boolean isValidUrl(String url) {
if (url == null || url.trim().isEmpty()) {
return false;
}
// 检查是否以http://或https://开头
if (!url.toLowerCase().startsWith("http://") && !url.toLowerCase().startsWith("https://")) {
return false;
}
// 简单的URL格式检查
try {
new java.net.URL(url);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 销毁资源(Spring Boot关闭时调用)
*/
public void destroy() {
try {
// 关闭所有客户端连接
for (WebSocketSession session : clients.keySet()) {
try {
if (session.isOpen()) {
session.close(CloseStatus.NORMAL);
}
} catch (Exception e) {
System.err.println("关闭WebSocket会话失败: " + e.getMessage());
}
}
clients.clear();
} catch (Exception e) {
System.err.println("清理客户端连接失败: " + e.getMessage());
}
try {
// 关闭Playwright资源
if (page != null && !page.isClosed()) {
page.close();
}
} catch (Exception e) {
System.err.println("关闭页面失败: " + e.getMessage());
}
try {
if (context != null) {
context.close();
}
} catch (Exception e) {
System.err.println("关闭浏览器上下文失败: " + e.getMessage());
}
try {
if (browser != null && browser.isConnected()) {
browser.close();
}
} catch (Exception e) {
System.err.println("关闭浏览器失败: " + e.getMessage());
}
try {
if (playwright != null) {
playwright.close();
}
} catch (Exception e) {
System.err.println("关闭Playwright失败: " + e.getMessage());
}
System.out.println("Playwright资源已清理完毕");
}
/**
* 获取统计信息摘要
*
* @return 包含所有统计信息的JSON字符串
*/
public String getStatisticsSummary() {
try {
Map<String, Object> stats = new HashMap<>();
stats.put("clients", clients.size());
stats.put("userConnections", new HashMap<>(userConnections));
stats.put("domChanges", getCounter("domChanges"));
stats.put("websocketMessages", getCounter("websocketMessages"));
stats.put("sseEvents", getCounter("sseEvents"));
stats.put("errors", getCounter("errors"));
stats.put("pageLoads", getCounter("pageLoads"));
stats.put("navigations", getCounter("navigations"));
stats.put("consoleErrors", getCounter("consoleErrors"));
stats.put("sseResponses", getCounter("sseResponses"));
stats.put("timestamp", System.currentTimeMillis());
if (page != null) {
stats.put("currentPage", page.url());
}
return JSON.toJSONString(stats);
} catch (Exception e) {
System.err.println("获取统计信息失败: " + e.getMessage());
e.printStackTrace();
return "{\"error\":\"获取统计信息失败\"}";
}
}
/**
* 重置所有统计计数器
*/
public void resetAllCounters() {
try {
String[] counters = {"domChanges", "websocketMessages", "sseEvents", "errors", "pageLoads", "navigations", "consoleErrors", "sseResponses"};
for (String counter : counters) {
resetCounter(counter);
}
System.out.println("所有统计计数器已重置");
} catch (Exception e) {
System.err.println("重置统计计数器失败: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 检查Playwright实例是否有效
*
* @return 如果Playwright实例有效返回true,否则返回false
*/
private boolean isPlaywrightInstanceValid() {
try {
return playwright != null && browser != null && page != null && !page.isClosed();
} catch (Exception e) {
System.err.println("检查Playwright实例状态失败: " + e.getMessage());
return false;
}
}
/**
* 安全地获取当前页面URL
*
* @return 当前页面URL,如果无法获取则返回空字符串
*/
private String getCurrentPageUrl() {
try {
return page != null ? page.url() : "";
} catch (Exception e) {
System.err.println("获取当前页面URL失败: " + e.getMessage());
return "";
}
}
}
\ No newline at end of file
package pangea.hiagent.workpanel;
import pangea.hiagent.dto.WorkPanelEvent;
import java.util.List;
import java.util.function.Consumer;
/**
* 工作面板数据收集器接口
* 用于采集Agent执行过程中的各类数据(思考过程、工具调用等)
*/
public interface IWorkPanelDataCollector {
/**
* 记录思考过程
* @param content 思考内容
* @param thinkingType 思考类型(分析、规划、执行等)
*/
void recordThinking(String content, String thinkingType);
/**
* 记录工具调用开始
* @param toolName 工具名称
* @param toolAction 工具执行的方法
* @param input 工具输入参数
*/
void recordToolCallStart(String toolName, String toolAction, Object input);
/**
* 记录工具调用完成
* @param toolName 工具名称
* @param output 工具输出结果
* @param status 执行状态(success/failure)
*/
void recordToolCallComplete(String toolName, Object output, String status);
/**
* 记录工具调用完成(带执行时间)
* @param toolName 工具名称
* @param output 工具输出结果
* @param status 执行状态(success/failure)
* @param executionTime 执行时间(毫秒)
*/
void recordToolCallComplete(String toolName, Object output, String status, Long executionTime);
/**
* 记录工具调用失败
* @param toolName 工具名称
* @param errorMessage 错误信息
*/
void recordToolCallError(String toolName, String errorMessage);
/**
* 记录日志
* @param message 日志消息
* @param level 日志级别(info/warn/error/debug)
*/
void recordLog(String message, String level);
/**
* 记录embed嵌入事件
* @param url 嵌入资源URL(可选)
* @param type MIME类型
* @param title 嵌入标题
* @param htmlContent HTML内容(可选)
*/
void recordEmbed(String url, String type, String title, String htmlContent);
/**
* 获取所有收集的事件
*/
List<WorkPanelEvent> getEvents();
/**
* 订阅事件(用于实时推送)
* @param consumer 事件处理函数
*/
void subscribe(Consumer<WorkPanelEvent> consumer);
/**
* 清空所有事件
*/
void clear();
/**
* 获取最后一个工具调用事件
*/
WorkPanelEvent getLastToolCall();
}
\ No newline at end of file
package pangea.hiagent.workpanel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import pangea.hiagent.dto.WorkPanelEvent;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* SSE 事件管理器
* 负责处理 SSE 连接、事件发送和连接管理
*/
@Slf4j
@Component
public class SseEventManager {
private static final long SSE_TIMEOUT = 1800000L; // 30分钟超时
// 存储所有活动的 emitter
private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();
/**
* 创建 SSE Emitter
*/
public SseEmitter createEmitter() {
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT);
registerCallbacks(emitter);
emitters.add(emitter);
return emitter;
}
/**
* 获取所有活动的 emitter
*/
public List<SseEmitter> getEmitters() {
return emitters;
}
/**
* 从列表中移除指定的 emitter
*/
public void removeEmitter(SseEmitter emitter) {
emitters.remove(emitter);
}
/**
* 注册 SSE Emitter 回调
*/
public void registerCallbacks(SseEmitter emitter) {
emitter.onCompletion(() -> {
log.debug("SSE连接已完成");
removeEmitter(emitter);
});
emitter.onError((Throwable t) -> {
// 区分客户端断开和真正的错误
if (t instanceof IOException) {
String message = t.getMessage();
if (message != null && (message.contains("软件中止") || message.contains("中断") || message.contains("连接"))) {
// 客户端主动断开连接 - 这是正常行为
log.debug("SSE流式传输中客户端连接中断 - 异常类型: {}", t.getClass().getSimpleName());
} else {
// 其他IO异常
log.debug("SSE连接发生IO异常", t);
}
} else if (t instanceof org.springframework.web.context.request.async.AsyncRequestNotUsableException) {
// 异步请求不可用 - 客户端已断开
log.debug("SSE异步请求不可用,客户端已断开连接");
} else {
// 其他真正的错误
log.error("SSE连接发生错误", t);
}
removeEmitter(emitter);
});
emitter.onTimeout(() -> {
log.warn("SSE连接超时");
completeEmitter(emitter, new AtomicBoolean(true));
removeEmitter(emitter);
});
}
/**
* 发送 SSE 事件
* 增强版本:在发送前检查连接状态,防止在已断开的连接上继续尝试发送
*/
public void sendEvent(SseEmitter emitter, String eventName, Object data, AtomicBoolean isCompleted) throws IOException {
synchronized (emitter) {
if (!isCompleted.get()) {
try {
emitter.send(SseEmitter.event().name(eventName).data(data));
log.debug("SSE事件发送成功 [{}]", eventName);
} catch (org.springframework.web.context.request.async.AsyncRequestNotUsableException e) {
// 客户端已断开连接或请求不再可用 - 这是正常的客户端中断行为
log.debug("异步请求不可用,客户端已断开连接[{}]: {}", eventName, e.getMessage());
isCompleted.set(true);
} catch (java.io.IOException e) {
// IO异常 - 客户端连接中断
String errorDetail = e.getMessage();
boolean isNormalDisconnect = errorDetail != null &&
(errorDetail.contains("软件中止") ||
errorDetail.contains("中断") ||
errorDetail.contains("连接") ||
errorDetail.contains("Socket") ||
errorDetail.contains("Pipe") ||
errorDetail.contains("closed"));
if (isNormalDisconnect) {
// 正常的客户端断开连接 - 调试级别
log.debug("客户端连接中断,SSE事件[{}]发送失败: {}", eventName, errorDetail);
} else {
// 异常的IO错误 - 警告级别,需要关注
log.warn("异常IO错误,SSE事件[{}]发送失败: {}", eventName, errorDetail);
}
isCompleted.set(true);
} catch (IllegalStateException e) {
// Emitter已完成或无效状态 - 调试级别
log.debug("Emitter状态异常,无法发送SSE事件[{}]: {}", eventName, e.getMessage());
isCompleted.set(true);
} catch (java.lang.RuntimeException e) {
// 处理其他运行时异常
if (e.getCause() instanceof org.springframework.web.context.request.async.AsyncRequestNotUsableException) {
log.debug("RuntimeException根因是AsyncRequestNotUsableException,SSE事件[{}]发送失败: {}", eventName, e.getCause().getMessage());
isCompleted.set(true);
} else if (e.getCause() instanceof java.io.IOException) {
String causeMsg = e.getCause().getMessage();
log.debug("RuntimeException根因是IOException,SSE事件[{}]发送失败: {}", eventName, causeMsg);
isCompleted.set(true);
} else if (e.getMessage() != null && e.getMessage().contains("response has already been committed")) {
log.debug("响应已提交,无法发送SSE事件[{}]", eventName);
isCompleted.set(true);
} else {
// 其他运行时异常 - 记录关键信息便于诊断
log.warn("SSE事件[{}]发送失败 - RuntimeException - 异常消息: {}", eventName, e.getMessage());
isCompleted.set(true);
}
} catch (Exception e) {
// 其他异常 - 根据类型决定日志级别
String msg = e.getMessage() != null ? e.getMessage() : "";
if (msg.contains("response has already been committed")) {
log.debug("响应已提交,无法发送SSE事件[{}]", eventName);
} else if (msg.contains("closed") || msg.contains("Closed") || msg.contains("Broken")) {
log.debug("连接已关闭,无法发送SSE事件[{}]", eventName);
} else {
// 未知异常 - 记录便于排查
log.warn("SSE事件[{}]发送失败 - 未知异常 - 异常类型: {} - 消息: {}",
eventName,
e.getClass().getSimpleName(),
msg);
}
isCompleted.set(true);
}
} else {
// 连接已完成,不再发送
if (log.isTraceEnabled()) {
log.trace("连接已完成,跳过发送SSE事件: {}", eventName);
}
}
}
}
/**
* 发送工作面板事件
*/
public void sendWorkPanelEvent(SseEmitter emitter, WorkPanelEvent event, AtomicBoolean isCompleted) throws IOException {
// 先快速检查是否已完成,避免不必要的构造数据对象
if (isCompleted.get()) {
if (log.isTraceEnabled()) {
log.trace("连接已完成,跳过发送工作面板事件");
}
return;
}
synchronized (emitter) {
if (!isCompleted.get()) {
try {
Map<String, Object> data = new HashMap<>();
data.put("eventType", event.getEventType());
data.put("timestamp", event.getTimestamp());
data.put("content", event.getContent());
data.put("thinkingType", event.getThinkingType());
data.put("toolName", event.getToolName());
data.put("toolAction", event.getToolAction());
data.put("toolInput", event.getToolInput());
data.put("toolOutput", event.getToolOutput());
data.put("toolStatus", event.getToolStatus());
data.put("logLevel", event.getLogLevel());
data.put("executionTime", event.getExecutionTime());
// 添加embed事件相关字段
data.put("embedUrl", event.getEmbedUrl());
data.put("embedType", event.getEmbedType());
data.put("embedTitle", event.getEmbedTitle());
data.put("embedHtmlContent", event.getEmbedHtmlContent());
// 确保所有事件类型都包含type字段,保证前端可以正确识别事件类型
data.put("type", event.getEventType());
// 修复:对于最终答案类型的思考事件,添加特殊标记
if ("thinking".equals(event.getEventType()) && "final_answer".equals(event.getThinkingType())) {
data.put("isFinalAnswer", true);
}
String eventName = getEventNameForWorkPanel(event.getEventType());
sendEvent(emitter, eventName, data, isCompleted);
} catch (org.springframework.web.context.request.async.AsyncRequestNotUsableException e) {
// 客户端已断开连接 - 这是正常行为
log.debug("异步请求不可用,客户端已断开连接: {}", e.getMessage());
isCompleted.set(true);
} catch (Exception e) {
if (e instanceof IOException || (e.getCause() instanceof IOException)) {
log.debug("客户端连接已断开,无法发送工作面板事件");
isCompleted.set(true);
} else if (e.getMessage() != null && e.getMessage().contains("response has already been committed")) {
log.debug("响应已提交,无法发送工作面板事件");
isCompleted.set(true);
} else {
// 其他异常类型,降级为debug日志
log.debug("发送工作面板事件失败: {}", e.getMessage());
isCompleted.set(true);
}
}
}
}
}
/**
* 根据事件类型获取SSE事件名称
*/
private String getEventNameForWorkPanel(String eventType) {
return switch (eventType) {
case "thinking" -> "thinking";
case "tool_call" -> "tool_call";
case "tool_result" -> "tool_result";
case "tool_error" -> "tool_error";
case "log" -> "log";
case "embed" -> "embed";
default -> "workpanel_event";
};
}
/**
* 启动心跳保活
*/
public void startHeartbeat(SseEmitter emitter, AtomicBoolean isCompleted) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
if (!isCompleted.get()) {
try {
// 发送心跳事件
emitter.send(SseEmitter.event().name("keepalive").data(""));
log.debug("心跳事件已发送");
} catch (Exception e) {
// 心跳发送失败,标记完成并关闭调度器
log.debug("心跳发送失败,连接可能已断开: {}", e.getMessage());
isCompleted.set(true);
scheduler.shutdown();
}
} else {
// 已完成,关闭调度器
scheduler.shutdown();
}
}, 30, 30, TimeUnit.SECONDS); // 每30秒发送一次心跳
// 添加一个更频繁的轻量级心跳,用于检测连接状态
ScheduledExecutorService lightScheduler = Executors.newScheduledThreadPool(1);
lightScheduler.scheduleAtFixedRate(() -> {
if (!isCompleted.get()) {
try {
// 发送轻量级心跳事件
emitter.send(SseEmitter.event().name("ping").data(""));
log.trace("轻量级心跳事件已发送");
} catch (Exception e) {
// 心跳发送失败,标记完成并关闭调度器
log.debug("轻量级心跳发送失败,连接可能已断开: {}", e.getMessage());
isCompleted.set(true);
lightScheduler.shutdown();
}
} else {
// 已完成,关闭调度器
lightScheduler.shutdown();
}
}, 5, 5, TimeUnit.SECONDS); // 每5秒发送一次轻量级心跳
}
/**
* 安全地完成 emitter
*/
public void completeEmitter(SseEmitter emitter, AtomicBoolean isCompleted) {
if (!isCompleted.getAndSet(true)) {
try {
// 检查emitter是否已经完成
emitter.complete();
log.debug("SSE连接已正常关闭");
} catch (IllegalStateException e) {
log.warn("Emitter已经完成: {}", e.getMessage());
} catch (Exception e) {
// 检查是否是响应已提交的异常
if (e.getMessage() != null && e.getMessage().contains("response has already been committed")) {
log.debug("响应已提交,无法完成Emitter");
} else {
log.error("关闭SSE连接时发生错误", e);
}
}
} else {
log.debug("Emitter已经标记为完成,无需再次关闭");
}
}
/**
* 发送错误信息到 SSE
*/
public void sendError(SseEmitter emitter, String message) {
try {
Map<String, Object> errorData = new HashMap<>();
errorData.put("error", message);
emitter.send(SseEmitter.event().name("error").data(errorData));
completeEmitter(emitter, new AtomicBoolean(true));
} catch (org.springframework.web.context.request.async.AsyncRequestNotUsableException e) {
// 客户端已断开连接导致的异步请求不可用异常
log.debug("客户端连接中断,无法发送错误信息: {}", e.getMessage());
} catch (IOException e) {
log.error("发送错误信息失败", e);
completeEmitter(emitter, new AtomicBoolean(true));
} catch (IllegalStateException e) {
log.warn("Emitter已经完成,无法发送错误信息", e);
} catch (java.lang.RuntimeException e) {
// 处理客户端断开连接导致的运行时异常
if (e.getCause() instanceof java.io.IOException) {
log.debug("客户端连接中断,无法发送错误信息");
} else {
// 检查是否是响应已提交的异常
if (e.getMessage() != null && e.getMessage().contains("response has already been committed")) {
log.debug("响应已提交,无法发送错误信息");
} else {
log.error("发送错误信息时发生运行时异常", e);
}
}
} catch (Exception e) {
// 检查是否是响应已提交的异常
if (e.getMessage() != null && e.getMessage().contains("response has already been committed")) {
log.debug("响应已提交,无法发送错误信息");
} else {
log.error("发送错误信息时发生未知异常", e);
}
completeEmitter(emitter, new AtomicBoolean(true));
}
}
}
\ No newline at end of file
package pangea.hiagent.workpanel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import pangea.hiagent.dto.WorkPanelEvent;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
/**
* 工作面板数据收集器实现
* 负责采集Agent执行过程中的各类数据
*/
@Slf4j
@Component
public class WorkPanelDataCollector implements IWorkPanelDataCollector {
/**
* 事件列表(线程安全)
*/
private final List<WorkPanelEvent> events = new CopyOnWriteArrayList<>();
/**
* 事件订阅者列表(线程安全)
*/
private final List<Consumer<WorkPanelEvent>> subscribers = new CopyOnWriteArrayList<>();
/**
* 最大事件数量,防止内存溢出
*/
private static final int MAX_EVENTS = 1000;
/**
* 根据状态确定事件类型
*/
private String getEventTypeFromStatus(String status) {
if (status == null) {
return "tool_result";
}
switch (status.toLowerCase()) {
case "success":
return "tool_result";
case "error":
case "failure":
return "tool_error";
default:
return "tool_result";
}
}
@Override
public void recordThinking(String content, String thinkingType) {
try {
// 过滤掉过于简短的内容,避免记录过多无关信息
if (content == null || content.trim().length() < 1) {
return;
}
WorkPanelEvent event = WorkPanelEvent.builder()
.eventType("thinking")
.timestamp(System.currentTimeMillis())
.content(content)
.thinkingType(thinkingType != null ? thinkingType : "reasoning")
.build();
addEvent(event);
log.debug("已记录思考过程: 类型={}, 内容={}", thinkingType, content);
} catch (Exception e) {
log.error("记录思考过程时发生错误: content={}, type={}", content, thinkingType, e);
}
}
@Override
public void recordToolCallStart(String toolName, String toolAction, Object input) {
try {
long currentTime = System.currentTimeMillis();
WorkPanelEvent event = WorkPanelEvent.builder()
.eventType("tool_call")
.timestamp(currentTime)
.toolName(toolName != null ? toolName : "未知工具")
.toolAction(toolAction != null ? toolAction : "未知操作")
.toolInput(convertToMap(input))
.toolStatus("pending") // 声明初始状态为pending
.build();
addEvent(event);
// 添加更详细的日志输出
String formattedTime = new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date(currentTime));
log.info("\n🔧 工具调用: [{}]\n⏰ 时间: {}\n📥 输入: {}\n📊 状态: 处理中",
toolName != null ? toolName : "未知工具",
formattedTime,
convertToJsonString(input));
log.debug("已记录工具调用开始: 工具={}, 方法={}, 状态=pending", toolName, toolAction);
} catch (Exception e) {
log.error("记录工具调用开始时发生错误: toolName={}, toolAction={}", toolName, toolAction, e);
}
}
@Override
public void recordToolCallComplete(String toolName, Object output, String status) {
recordToolCallComplete(toolName, output, status, null);
}
/**
* 记录工具调用完成(带执行时间)
*/
public void recordToolCallComplete(String toolName, Object output, String status, Long executionTime) {
try {
long currentTime = System.currentTimeMillis();
// 查找最近的该工具的pending事件并更新它
WorkPanelEvent lastToolCall = getLastPendingToolCall(toolName);
if (lastToolCall != null) {
// 保存原有的toolInput
Map<String, Object> originalToolInput = lastToolCall.getToolInput();
// 更新现有事件
lastToolCall.setEventType(getEventTypeFromStatus(status)); // 更新事件类型
lastToolCall.setToolOutput(output);
lastToolCall.setToolStatus(status != null ? status : "unknown");
// 如果有执行时间,设置执行时间
if (executionTime != null) {
lastToolCall.setExecutionTime(executionTime);
}
// 更新时间戳
lastToolCall.setTimestamp(currentTime);
// 确保toolInput字段不会丢失
if (originalToolInput != null) {
lastToolCall.setToolInput(originalToolInput);
}
// 重新发布更新后的事件
notifySubscribers(lastToolCall);
} else {
// 如果没有对应的pending事件,创建一个新事件
// 此时需要去回退查找toolInput,会避免数据丢失
Map<String, Object> fallbackToolInput = null;
WorkPanelEvent lastAnyToolCall = getLastPendingToolCallAny();
if (lastAnyToolCall != null && lastAnyToolCall.getToolInput() != null) {
fallbackToolInput = lastAnyToolCall.getToolInput();
log.warn("[没有找到对应的pending事件] 工具={}, 已从上一个工具事件回退toolInput", toolName);
} else {
log.warn("[没有找到对应的pending事件] 工具={}, 也没有任何可回退的pending事件", toolName);
}
// 此步骤是待录容错机制的水滴水滴,立骨待宋殊水信息
WorkPanelEvent event = WorkPanelEvent.builder()
.eventType(getEventTypeFromStatus(status))
.timestamp(currentTime)
.toolName(toolName != null ? toolName : "未知工具")
.toolOutput(output)
.toolStatus(status != null ? status : "unknown")
.toolInput(fallbackToolInput) // 设置回退的toolInput,可以是null
.executionTime(executionTime)
.build();
addEvent(event);
}
// 添加更详细的日志输出
String formattedTime = new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date(currentTime));
String statusText = getStatusText(status);
if ("success".equals(status)) {
log.info("\n🔧 工具调用: [{}]\n⏰ 时间: {}\n✅ 状态: 成功\n📤 输出: {}{}",
toolName != null ? toolName : "未知工具",
formattedTime,
convertToJsonString(output),
executionTime != null ? "\n⏱️ 耗时: " + executionTime + "ms" : "");
} else if ("failure".equals(status) || "error".equals(status)) {
log.info("\n🔧 工具调用: [{}]\n⏰ 时间: {}\n❌ 状态: 失败\n💬 错误: {}{}",
toolName != null ? toolName : "未知工具",
formattedTime,
convertToJsonString(output),
executionTime != null ? "\n⏱️ 耗时: " + executionTime + "ms" : "");
} else {
log.info("\n🔧 工具调用: [{}]\n⏰ 时间: {}\n📊 状态: {}\n📤 输出: {}{}",
toolName != null ? toolName : "未知工具",
formattedTime,
statusText,
convertToJsonString(output),
executionTime != null ? "\n⏱️ 耗时: " + executionTime + "ms" : "");
}
log.debug("已记录工具调用完成: 工具={}, 状态={}, 执行时间={}ms", toolName, status, executionTime);
} catch (Exception e) {
log.error("记录工具调用完成时发生错误: toolName={}, status={}", toolName, status, e);
}
}
@Override
public void recordToolCallError(String toolName, String errorMessage) {
try {
WorkPanelEvent event = WorkPanelEvent.builder()
.eventType("tool_error")
.timestamp(System.currentTimeMillis())
.toolName(toolName)
.content(errorMessage)
.toolStatus("failure")
.build();
addEvent(event);
log.debug("已记录工具调用错误: 工具={}, 错误={}", toolName, errorMessage);
} catch (Exception e) {
log.error("记录工具调用错误时发生错误: toolName={}", toolName, e);
}
}
@Override
public void recordLog(String message, String level) {
try {
// 过滤掉空消息
if (message == null || message.trim().isEmpty()) {
return;
}
WorkPanelEvent event = WorkPanelEvent.builder()
.eventType("log")
.timestamp(System.currentTimeMillis())
.content(message)
.logLevel(level != null ? level : "info")
.build();
addEvent(event);
} catch (Exception e) {
log.error("记录日志时发生错误: message={}, level={}", message, level, e);
}
}
@Override
public void recordEmbed(String url, String type, String title, String htmlContent) {
try {
WorkPanelEvent event = WorkPanelEvent.builder()
.eventType("embed")
.timestamp(System.currentTimeMillis())
.embedUrl(url)
.embedType(type)
.embedTitle(title)
.embedHtmlContent(htmlContent)
.build();
addEvent(event);
log.debug("已记录embed事件: title={}, type={}, hasContent={}", title, type, htmlContent != null && !htmlContent.isEmpty());
} catch (Exception e) {
log.error("recordEmbed时发生错误: title={}, type={}", title, type, e);
}
}
@Override
public List<WorkPanelEvent> getEvents() {
return new ArrayList<>(events);
}
@Override
public void subscribe(Consumer<WorkPanelEvent> consumer) {
if (consumer != null) {
subscribers.add(consumer);
log.debug("已添加事件订阅者,当前订阅者数量: {}", subscribers.size());
}
}
@Override
public void clear() {
try {
events.clear();
log.debug("已清空工作面板事件");
} catch (Exception e) {
log.error("清空事件时发生错误", e);
}
}
@Override
public WorkPanelEvent getLastToolCall() {
// 从后往前查找最后一个工具调用事件
for (int i = events.size() - 1; i >= 0; i--) {
WorkPanelEvent event = events.get(i);
if ("tool_call".equals(event.getEventType()) || "tool_result".equals(event.getEventType())) {
return event;
}
}
return null;
}
/**
* 查找最近的该工具的pending事件
*/
private WorkPanelEvent getLastPendingToolCall(String toolName) {
// 从后往前查找最近的该工具的pending事件
for (int i = events.size() - 1; i >= 0; i--) {
WorkPanelEvent event = events.get(i);
if ("tool_call".equals(event.getEventType()) &&
toolName.equals(event.getToolName()) &&
"pending".equals(event.getToolStatus())) {
return event;
}
}
return null;
}
/**
* 查找最近的任何工具的pending事件(容错机制)
*/
private WorkPanelEvent getLastPendingToolCallAny() {
// 从后往前查找最近的任何pending事件
for (int i = events.size() - 1; i >= 0; i--) {
WorkPanelEvent event = events.get(i);
if ("tool_call".equals(event.getEventType()) &&
"pending".equals(event.getToolStatus())) {
return event;
}
}
return null;
}
/**
* 添加事件到列表,并通知所有订阅者(隔离异常避免一个订阅者异常影响其他订阅者)
*/
private void addEvent(WorkPanelEvent event) {
try {
// 控制事件数量,防止内存溢出
if (events.size() >= MAX_EVENTS) {
events.remove(0); // 移除最老的事件
}
events.add(event);
// 通知所有订阅者
notifySubscribers(event);
} catch (Exception e) {
// 即使在addEvent方法中也增加异常保护,防止影响主流程
log.debug("添加事件失败: {}", e.getMessage());
}
}
/**
* 通知所有订阅者(隔离异常避免一个订阅者异常影响其他订阅者)
*/
private void notifySubscribers(WorkPanelEvent event) {
try {
// 通知所有订阅者(使用隔离异常处理,确保一个订阅者异常不影响其他订阅者)
if (!subscribers.isEmpty()) {
for (Consumer<WorkPanelEvent> subscriber : subscribers) {
try {
if (subscriber != null) {
subscriber.accept(event);
}
} catch (Exception e) {
// 异常降级为debug日志,避免过度日志记录
// 异常通常由于SSE连接已断开导致,这是正常情况
if (e instanceof org.springframework.web.context.request.async.AsyncRequestNotUsableException) {
log.debug("订阅者处理事件失败:异步请求不可用(客户端已断开连接)");
} else if (e instanceof java.io.IOException) {
log.debug("订阅者处理事件失败:客户端连接已断开");
} else if (e.getMessage() != null && e.getMessage().contains("response has already been committed")) {
log.debug("订阅者处理事件失败:响应已提交");
} else {
// 其他异常也降级为debug,避免日志污染
log.debug("订阅者处理事件失败: {}", e.getMessage());
}
}
}
}
} catch (Exception e) {
// 即使在addEvent方法中也增加异常保护,防止影响主流程
log.debug("添加事件失败: {}", e.getMessage());
}
}
/**
* 将对象转换为Map(用于工具输入参数)
*/
private Map<String, Object> convertToMap(Object input) {
if (input == null) {
return new HashMap<>();
}
if (input instanceof Map) {
// 安全地转换Map类型,确保键为String类型
Map<?, ?> rawMap = (Map<?, ?>) input;
Map<String, Object> resultMap = new HashMap<>();
for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
// 将键转换为String类型
String key = entry.getKey() != null ? entry.getKey().toString() : "null";
resultMap.put(key, entry.getValue());
}
return resultMap;
}
// 简单对象转换为Map
Map<String, Object> result = new HashMap<>();
result.put("value", input);
return result;
}
/**
* 将对象转换为JSON字符串
*/
private String convertToJsonString(Object obj) {
try {
if (obj == null) {
return "null";
}
if (obj instanceof String) {
return (String) obj;
}
// 使用Jackson ObjectMapper进行序列化
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
return mapper.writeValueAsString(obj);
} catch (Exception e) {
if (obj != null) {
return obj.toString();
} else {
return "null";
}
}
}
/**
* 获取状态文本
*/
private String getStatusText(String status) {
if (status == null) {
return "未知";
}
switch (status.toLowerCase()) {
case "success": return "成功";
case "pending": return "处理中";
case "error": return "错误";
case "failure": return "失败";
default: return status;
}
}
}
\ No newline at end of file
package pangea.hiagent.workpanel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import pangea.hiagent.dto.WorkPanelEvent;
import pangea.hiagent.dto.WorkPanelStatusDto;
import pangea.hiagent.model.Agent;
import pangea.hiagent.service.AgentService;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.concurrent.ConcurrentHashMap;
/**
* 工作面板服务
* 负责处理工作面板相关的状态和事件
*/
@Slf4j
@Service
public class WorkPanelService {
@Autowired
private AgentService agentService;
@Autowired
private pangea.hiagent.agent.ReActService reActService;
// 用于跟踪已发送的事件ID,防止重复发送
private final Map<String, Set<String>> sentEventIds = new ConcurrentHashMap<>();
/**
* 获取工作面板当前状态
*/
public WorkPanelStatusDto getWorkPanelStatus(String agentId, String userId) {
try {
Agent agent = agentService.getAgent(agentId);
if (agent == null) {
throw new RuntimeException("Agent不存在");
}
if (!agent.getOwner().equals(userId)) {
throw new RuntimeException("无权访问该Agent");
}
log.info("获取Agent {} 的工作面板状态", agentId);
// 从工作面板收集器中读取数据
IWorkPanelDataCollector collector = reActService.getWorkPanelCollector();
List<WorkPanelEvent> allEvents = collector != null ? collector.getEvents() : new ArrayList<>();
// 统计不同类型的事件
int totalEvents = allEvents.size();
int successfulCalls = (int) allEvents.stream()
.filter(e -> "tool_result".equals(e.getEventType()) && "success".equals(e.getToolStatus()))
.count();
int failedCalls = (int) allEvents.stream()
.filter(e -> "tool_error".equals(e.getEventType()) ||
("tool_result".equals(e.getEventType()) && "failure".equals(e.getToolStatus())))
.count();
List<WorkPanelEvent> thinkingEvents = new ArrayList<>();
List<WorkPanelEvent> toolCallEvents = new ArrayList<>();
List<WorkPanelEvent> logEvents = new ArrayList<>();
for (WorkPanelEvent event : allEvents) {
switch (event.getEventType()) {
case "thinking" -> thinkingEvents.add(event);
case "tool_call", "tool_result", "tool_error" -> toolCallEvents.add(event);
case "log" -> logEvents.add(event);
default -> {
}
}
}
WorkPanelStatusDto status = WorkPanelStatusDto.builder()
.id(agentId + "_workpanel")
.agentId(agentId)
.agentName(agent.getName())
.events(allEvents)
.thinkingEvents(thinkingEvents)
.toolCallEvents(toolCallEvents)
.logEvents(logEvents)
.totalEvents(totalEvents)
.successfulToolCalls(successfulCalls)
.failedToolCalls(failedCalls)
.updateTimestamp(System.currentTimeMillis())
.isProcessing(false)
.build();
return status;
} catch (Exception e) {
log.error("获取工作面板状态失败", e);
throw new RuntimeException("获取工作面板状态失败: " + e.getMessage(), e);
}
}
/**
* 清空工作面板数据
*/
public void clearWorkPanel(String agentId, String userId) {
try {
Agent agent = agentService.getAgent(agentId);
if (agent == null) {
throw new RuntimeException("Agent不存在");
}
if (!agent.getOwner().equals(userId)) {
throw new RuntimeException("无权访问该Agent");
}
log.info("清空Agent {} 的工作面板", agentId);
// 在实际应用中,这里应该从缓存中清除工作面板数据
// 清空已发送事件ID跟踪
sentEventIds.remove(agentId);
} catch (Exception e) {
log.error("清空工作面板失败", e);
throw new RuntimeException("清空工作面板失败: " + e.getMessage(), e);
}
}
/**
* 生成事件唯一标识
*/
public String generateEventId(WorkPanelEvent event) {
if (event == null) {
return "null_event_" + System.currentTimeMillis();
}
StringBuilder sb = new StringBuilder();
sb.append(event.getEventType()).append("_");
switch (event.getEventType()) {
case "thinking":
sb.append(event.getThinkingType() != null ? event.getThinkingType() : "default")
.append("_")
.append(event.getContent() != null ? event.getContent().hashCode() : 0);
break;
case "tool_call":
case "tool_result":
case "tool_error":
sb.append(event.getToolName() != null ? event.getToolName() : "unknown")
.append("_")
.append(event.getToolAction() != null ? event.getToolAction() : "unknown")
.append("_")
.append(event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis());
break;
case "log":
sb.append(event.getLogLevel() != null ? event.getLogLevel() : "info")
.append("_")
.append(event.getContent() != null ? event.getContent().hashCode() : 0);
break;
case "embed":
sb.append(event.getEmbedTitle() != null ? event.getEmbedTitle() : "untitled")
.append("_")
.append(event.getEmbedUrl() != null ? event.getEmbedUrl().hashCode() : 0);
break;
default:
sb.append(event.getTimestamp() != null ? event.getTimestamp() : System.currentTimeMillis());
break;
}
return sb.toString();
}
/**
* 检查事件是否已发送
*/
public boolean isEventAlreadySent(String agentId, WorkPanelEvent event) {
String eventId = generateEventId(event);
Set<String> agentEventIds = sentEventIds.computeIfAbsent(agentId, k -> new HashSet<>());
return !agentEventIds.add(eventId); // 如果已存在,add返回false,表示已发送
}
}
\ No newline at end of file
package pangea.hiagent.aspect;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class ToolExecutionLoggerAspectTest {
@Autowired
private TestTool testTool;
@Test
void testToolExecutionLogging() {
// 测试工具方法是否能被AOP切面正确拦截
String result = testTool.sampleTool("test input");
assertEquals("Processed: test input", result);
}
@Component
static class TestTool {
@Tool(description = "Sample tool for testing AOP logging")
public String sampleTool(String input) {
return "Processed: " + input;
}
}
@Configuration
@EnableAspectJAutoProxy
static class TestConfig {
@Bean
public TestTool testTool() {
return new TestTool();
}
}
}
\ No newline at end of file
version: '3.8'
services:
# MySQL数据库
mysql:
image: mysql:8.0
container_name: hiagent-mysql
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: root123456
MYSQL_DATABASE: hiagent
MYSQL_USER: hiagent
MYSQL_PASSWORD: hiagent123456
TZ: 'Asia/Shanghai'
volumes:
- mysql-data:/var/lib/mysql
- ./backend/src/main/resources/db/init-db.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- hiagent-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
# Redis缓存
redis:
image: redis:7-alpine
container_name: hiagent-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
networks:
- hiagent-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
# RabbitMQ消息队列
rabbitmq:
image: rabbitmq:3.12-management-alpine
container_name: hiagent-rabbitmq
ports:
- "5672:5672"
- "15672:15672"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
volumes:
- rabbitmq-data:/var/lib/rabbitmq
networks:
- hiagent-network
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 10s
timeout: 5s
retries: 5
# 后端API服务
backend:
build:
context: .
dockerfile: backend/Dockerfile
container_name: hiagent-backend
ports:
- "8080:8080"
environment:
SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/hiagent?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
SPRING_DATASOURCE_USERNAME: hiagent
SPRING_DATASOURCE_PASSWORD: hiagent123456
SPRING_REDIS_HOST: redis
SPRING_REDIS_PORT: 6379
SPRING_RABBITMQ_HOST: rabbitmq
SPRING_RABBITMQ_PORT: 5672
SPRING_RABBITMQ_USERNAME: guest
SPRING_RABBITMQ_PASSWORD: guest
DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY}
JWT_SECRET: hiagent-secret-key-for-production-change-this
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
rabbitmq:
condition: service_healthy
networks:
- hiagent-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/swagger-ui.html"]
interval: 30s
timeout: 10s
retries: 5
# 前端服务
frontend:
build:
context: .
dockerfile: frontend/Dockerfile
container_name: hiagent-frontend
ports:
- "80:80"
depends_on:
- backend
networks:
- hiagent-network
volumes:
mysql-data:
redis-data:
rabbitmq-data:
networks:
hiagent-network:
driver: bridge
# 前端Dockerfile
FROM node:18-alpine AS builder
WORKDIR /build
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /build/dist /usr/share/nginx/html
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HiAgent - 我的AI智能体助理</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-plus/dist/index.css">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
\ No newline at end of file
server {
listen 80;
server_name _;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
error_page 404 /index.html;
}
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "hiagent-frontend",
"version": "1.0.0",
"description": "HiAgent - 我的AI智能体助理",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx --fix --ignore-path .gitignore"
},
"dependencies": {
"@monaco-editor/loader": "^1.4.0",
"@types/pako": "^2.0.4",
"axios": "^1.6.0",
"default-passive-events": "^4.0.0",
"dompurify": "^3.3.1",
"element-plus": "^2.4.0",
"highlight.js": "^11.9.0",
"marked": "^17.0.1",
"pako": "^2.1.0",
"pinia": "^2.1.7",
"snabbdom": "^3.6.3",
"vue": "^3.4.0",
"vue-markdown-render": "^2.0.0",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@types/dompurify": "^3.0.5",
"@types/node": "^20.10.0",
"@vitejs/plugin-vue": "^5.0.0",
"eslint": "^8.55.0",
"eslint-plugin-vue": "^9.19.0",
"terser": "^5.44.1",
"typescript": "^5.9.3",
"vite": "^5.0.0",
"vue-tsc": "^1.8.0"
}
}
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Path aliases */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
/* Vue specific */
"jsx": "preserve",
/* Type checking */
"types": ["element-plus/global"]
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
\ No newline at end of file
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
\ No newline at end of file
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
server: {
port: 5174,
strictPort: true, // 确保总是使用指定端口
// 添加headers配置以允许iframe加载
headers: {
'X-Frame-Options': 'SAMEORIGIN'
},
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api')
}
}
},
build: {
target: 'esnext',
minify: 'terser',
sourcemap: false
},
esbuild: {
jsxFactory: 'h',
jsxFragment: 'Fragment',
tsconfigRaw: '{}'
}
})
\ No newline at end of file
@echo off
REM HiAgent 完整Debug启动脚本
REM 此脚本在两个不同的窗口中同时启动前后端进行调试
setlocal enabledelayedexpansion
echo.
echo ========================================
echo HiAgent 完整Debug调试启动
echo ========================================
echo.
REM 获取脚本所在目录
set SCRIPT_DIR=%~dp0
echo [INFO] 在新窗口中启动后端服务...
start "HiAgent Backend Debug" cmd /k "%SCRIPT_DIR%run-backend-debug.bat"
timeout /t 5 /nobreak
echo [INFO] 在新窗口中启动前端服务...
start "HiAgent Frontend Debug" cmd /k "%SCRIPT_DIR%run-frontend-debug.bat"
echo.
echo [SUCCESS] 已启动调试环境!
echo.
echo 后端服务信息:
echo URL: http://localhost:8080
echo Swagger UI: http://localhost:8080/swagger-ui.html
echo Debug Port: 5005
echo.
echo 前端服务信息:
echo URL: http://localhost:5173
echo 开发者工具: F12
echo.
echo 默认登录账号:
echo 用户名: admin
echo 密码: admin123456
echo.
pause
@echo off
REM HiAgent 后端Debug启动脚本
REM 此脚本用于启动Spring Boot应用进行远程调试
setlocal enabledelayedexpansion
echo.
echo ========================================
echo HiAgent 后端Debug调试启动
echo ========================================
echo.
@REM REM 设置DeepSeek API密钥(从环境变量或提示用户输入)
@REM if "%DEEPSEEK_API_KEY%"=="" (
@REM echo 请输入您的DeepSeek API密钥:
@REM set /p DEEPSEEK_API_KEY=""
@REM echo.
@REM )
REM 检查Java是否安装
java -version >nul 2>&1
if errorlevel 1 (
echo [ERROR] 未找到Java安装,请确保JDK 17+已安装
pause
exit /b 1
)
REM 进入后端目录
cd /d "%~dp0backend"
echo [INFO] 清理旧的构建文件...
call mvn clean -q
echo [INFO] 编译项目...
call mvn compile -q
if errorlevel 1 (
echo [ERROR] 编译失败,请检查代码
pause
exit /b 1
)
echo [INFO] 启动应用(Debug模式)...
echo [INFO] 访问地址: http://localhost:8080
echo [INFO] Swagger UI: http://localhost:8080/swagger-ui.html
echo [INFO] 调试端口: 5005 (JDWP)
echo.
REM 启动应用,开启JDWP调试端口5005
set MAVEN_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -Dfile.encoding=UTF-8
call mvn spring-boot:run -Dspring-boot.run.arguments="--spring.jpa.hibernate.ddl-auto=create-drop"
pause
\ No newline at end of file
@echo off
REM HiAgent 前端Debug启动脚本
REM 此脚本用于启动Vue3开发服务器进行调试
setlocal enabledelayedexpansion
echo.
echo ========================================
echo HiAgent 前端Debug调试启动
echo ========================================
echo.
REM 检查Node.js是否安装
node --version >nul 2>&1
if errorlevel 1 (
echo [ERROR] 未找到Node.js安装,请确保Node.js 18+已安装
pause
exit /b 1
)
REM 进入前端目录
cd /d "%~dp0frontend"
echo [INFO] 检查依赖...
if not exist "node_modules" (
echo [INFO] 首次运行,安装依赖...
call npm install
if errorlevel 1 (
echo [ERROR] 依赖安装失败
pause
exit /b 1
)
)
echo [INFO] 启动开发服务器(Debug模式)...
echo [INFO] 访问地址: http://localhost:5173
echo [INFO] 后端API地址: http://localhost:8080
echo [INFO] 在浏览器中按 F12 打开开发者工具进行调试
echo.
REM 启动开发服务器
call npm run dev
pause
@echo off
echo Setting environment variables for HiAgent...
REM 设置DeepSeek API密钥(请替换为你的实际API密钥)
set DEEPSEEK_API_KEY=your-deepseek-api-key-here
REM 设置JWT密钥
set JWT_SECRET=hiagent-secret-key-for-production-change-this
REM 设置其他可选的API密钥
REM set OPENAI_API_KEY=your-openai-api-key-here
echo Environment variables set successfully!
echo Starting HiAgent application...
REM 启动后端应用
cd backend
call mvn spring-boot:run
pause
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment