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"]
This diff is collapsed.
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.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.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.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
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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