Commit a86a826d authored by ligaowei's avatar ligaowei

添加剩余的代码文件和资源文件

parent a92e8d6e
package pangea.hiagent.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import pangea.hiagent.dto.ApiResponse;
import pangea.hiagent.dto.PageData;
import pangea.hiagent.model.Agent;
import pangea.hiagent.service.AgentService;
import com.baomidou.mybatisplus.core.metadata.IPage;
/**
* Agent API控制器
* 提供Agent的増删改查功能
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/agent")
public class AgentController {
// 常量定义(已删除,对话功能已移除)
// 服务依赖
private final AgentService agentService;
public AgentController(AgentService agentService) {
this.agentService = agentService;
}
// ==================== Agent管理API ====================
/**
* 创建Agent
*/
@PostMapping
public ApiResponse<Agent> createAgent(@RequestBody Agent agent) {
try {
String userId = getCurrentUserId();
if (userId == null) {
return ApiResponse.error(4001, "用户未认证");
}
log.info("用户 {} 开始创建Agent: {}", userId, agent.getName());
agent.setOwner(userId);
Agent created = agentService.createAgent(agent);
log.info("用户 {} 成功创建Agent: {} (ID: {})", userId, created.getName(), created.getId());
return ApiResponse.success(created, "创建Agent成功");
} catch (Exception e) {
log.error("创建Agent失败", e);
return ApiResponse.error(4001, "创建Agent失败: " + e.getMessage());
}
}
/**
* 更新Agent
*/
@PreAuthorize("@permissionEvaluator.hasPermission(authentication, #id, 'Agent', 'write')")
@PutMapping("/{id}")
public ApiResponse<Agent> updateAgent(@PathVariable String id, @RequestBody Agent agent) {
try {
String userId = getCurrentUserId();
log.info("用户 {} 开始更新Agent: {}", userId, id);
agent.setId(id);
Agent updated = agentService.updateAgent(agent);
log.info("用户 {} 成功更新Agent: {}", userId, updated.getId());
return ApiResponse.success(updated, "更新Agent成功");
} catch (Exception e) {
log.error("更新Agent失败", e);
return ApiResponse.error(4001, "更新Agent失败: " + e.getMessage());
}
}
/**
* 删除Agent
*/
@PreAuthorize("@permissionEvaluator.hasPermission(authentication, #id, 'Agent', 'delete')")
@DeleteMapping("/{id}")
public ApiResponse<Void> deleteAgent(@PathVariable String id) {
try {
String userId = getCurrentUserId();
log.info("用户 {} 开始删除Agent: {}", userId, id);
agentService.deleteAgent(id);
log.info("用户 {} 成功删除Agent: {}", userId, id);
return ApiResponse.success(null, "删除Agent成功");
} catch (Exception e) {
log.error("删除Agent失败", e);
return ApiResponse.error(4001, "删除Agent失败: " + e.getMessage());
}
}
/**
* 获取Agent详情
*/
@PreAuthorize("@permissionEvaluator.hasPermission(authentication, #id, 'Agent', 'read')")
@GetMapping("/{id}")
public ApiResponse<Agent> getAgent(@PathVariable String id) {
try {
Agent agent = agentService.getAgent(id);
if (agent == null) {
return ApiResponse.error(4001, "Agent不存在");
}
return ApiResponse.success(agent);
} catch (Exception e) {
log.error("获取Agent详情失败", e);
return ApiResponse.error(4001, "获取Agent详情失败: " + e.getMessage());
}
}
/**
* 分页获取Agent列表
*/
@GetMapping("/list")
public ApiResponse<PageData<Agent>> listAgents(
@RequestParam(defaultValue = "1") Long current,
@RequestParam(defaultValue = "10") Long size,
@RequestParam(required = false) String name,
@RequestParam(required = false) String status) {
try {
IPage<Agent> page = agentService.pageAgents(current, size, name, status);
return ApiResponse.success(PageData.from(page));
} catch (Exception e) {
log.error("获取Agent列表失败", e);
return ApiResponse.error(4001, "获取Agent列表失败: " + e.getMessage());
}
}
/**
* 获取用户的Agent列表
*/
@GetMapping
public ApiResponse<java.util.List<Agent>> getUserAgents() {
try {
String userId = getCurrentUserId();
if (userId == null) {
return ApiResponse.error(4001, "用户未认证");
}
java.util.List<Agent> agents = agentService.getUserAgents(userId);
return ApiResponse.success(agents);
} catch (Exception e) {
log.error("获取用户Agent列表失败", e);
return ApiResponse.error(4001, "获取用户Agent列表失败: " + e.getMessage());
}
}
// ==================== 私有方法 ====================
// ---------- 用户认证相关 ----------
/**
* 获取当前认证用户ID
*/
private String getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return (authentication != null && authentication.getPrincipal() != null) ?
(String) authentication.getPrincipal() : null;
}
}
\ No newline at end of file
package pangea.hiagent.controller;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import pangea.hiagent.dto.ApiResponse;
import pangea.hiagent.rag.RagService;
import java.util.List;
import java.util.Map;
/**
* RAG API控制器
* 提供文档检索和RAG增强相关的API接口
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/rag")
public class RagController {
@Autowired
private RagService ragService;
/**
* 文档检索接口
*/
@PostMapping("/search")
public ApiResponse<List<org.springframework.ai.document.Document>> searchDocuments(
@RequestBody SearchRequest request) {
try {
log.info("开始文档检索,查询: {}", request.getQuery());
List<org.springframework.ai.document.Document> results = ragService.searchDocuments(
request.getQuery(),
request.getCollectionIds(),
request.getTopK(),
request.getScoreThreshold()
);
log.info("文档检索完成,返回 {} 个结果", results.size());
return ApiResponse.success(results, "文档检索成功");
} catch (Exception e) {
log.error("文档检索失败", e);
return ApiResponse.error(4002, "文档检索失败: " + e.getMessage());
}
}
/**
* 获取所有文档列表接口(供Dashboard使用)
*/
@GetMapping("/documents")
public ApiResponse<List<Object>> getAllDocuments() {
try {
log.info("获取所有文档列表");
// 由于VectorStore可能未配置,我们返回空列表而不是抛出异常
List<Object> documents = List.of();
log.info("获取文档列表完成,返回 {} 个文档", documents.size());
return ApiResponse.success(documents, "获取文档列表成功");
} catch (Exception e) {
log.error("获取文档列表失败", e);
return ApiResponse.error(4002, "获取文档列表失败: " + e.getMessage());
}
}
/**
* RAG增强问答接口
*/
@PostMapping("/qa")
public ApiResponse<String> ragQa(@RequestBody RagQaRequest request) {
try {
log.info("开始RAG增强问答,Agent ID: {}, 查询: {}", request.getAgentId(), request.getQuery());
// 根据Agent ID获取Agent对象
// 这里需要实现获取Agent的逻辑,暂时使用模拟数据
// String result = "这是RAG增强问答的结果";
// 调用RagService的实际方法
// TODO: 实现根据Agent ID获取Agent对象的逻辑
String result = ragService.ragQa(null, request.getQuery());
log.info("RAG增强问答完成");
return ApiResponse.success(result, "RAG增强问答成功");
} catch (Exception e) {
log.error("RAG增强问答失败", e);
return ApiResponse.error(4002, "RAG增强问答失败: " + e.getMessage());
}
}
/**
* 获取检索统计信息
*/
@GetMapping("/stats")
public ApiResponse<Map<String, Object>> getRetrievalStats(
@RequestParam(required = false) List<String> collectionIds) {
try {
log.info("获取检索统计信息");
Map<String, Object> stats = ragService.getRetrievalStats(collectionIds);
log.info("获取检索统计信息完成");
return ApiResponse.success(stats, "获取统计信息成功");
} catch (Exception e) {
log.error("获取检索统计信息失败", e);
return ApiResponse.error(4002, "获取统计信息失败: " + e.getMessage());
}
}
/**
* 文档检索请求类
*/
@Data
public static class SearchRequest {
private String query;
private List<String> collectionIds;
private Integer topK;
private Double scoreThreshold;
}
/**
* RAG问答请求类
*/
@Data
public static class RagQaRequest {
private String agentId;
private String query;
}
}
\ No newline at end of file
package pangea.hiagent.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import pangea.hiagent.dto.ApiResponse;
import pangea.hiagent.model.LlmConfig;
import pangea.hiagent.repository.LlmConfigRepository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/v1/test")
public class TestController {
private final LlmConfigRepository llmConfigRepository;
public TestController(LlmConfigRepository llmConfigRepository) {
this.llmConfigRepository = llmConfigRepository;
}
@GetMapping("/llm-configs")
public ApiResponse<List<LlmConfig>> getAllLlmConfigs() {
try {
List<LlmConfig> configs = llmConfigRepository.selectList(null);
log.info("查询到 {} 条LLM配置", configs.size());
for (LlmConfig config : configs) {
log.info("配置: ID={}, 名称={}, 模型名={}, 提供商={}, 启用状态={}",
config.getId(), config.getName(), config.getModelName(),
config.getProvider(), config.getEnabled());
}
return ApiResponse.success(configs);
} catch (Exception e) {
log.error("查询LLM配置失败", e);
return ApiResponse.error(5001, "查询LLM配置失败: " + e.getMessage());
}
}
@GetMapping("/hisense-config")
public ApiResponse<LlmConfig> getHisenseConfig() {
try {
LambdaQueryWrapper<LlmConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LlmConfig::getModelName, "hisense-default");
wrapper.eq(LlmConfig::getEnabled, true);
LlmConfig config = llmConfigRepository.selectOne(wrapper);
if (config != null) {
log.info("找到海信配置: ID={}, 名称={}, 模型名={}, 提供商={}, 启用状态={}",
config.getId(), config.getName(), config.getModelName(),
config.getProvider(), config.getEnabled());
return ApiResponse.success(config);
} else {
log.warn("未找到启用的海信配置");
// 尝试查找所有海信配置
LambdaQueryWrapper<LlmConfig> allWrapper = new LambdaQueryWrapper<>();
allWrapper.eq(LlmConfig::getModelName, "hisense-default");
List<LlmConfig> allConfigs = llmConfigRepository.selectList(allWrapper);
log.info("找到 {} 条海信配置", allConfigs.size());
for (LlmConfig c : allConfigs) {
log.info("海信配置详情: ID={}, 名称={}, 模型名={}, 提供商={}, 启用状态={}",
c.getId(), c.getName(), c.getModelName(),
c.getProvider(), c.getEnabled());
}
return ApiResponse.error(4001, "未找到启用的海信配置");
}
} catch (Exception e) {
log.error("查询海信配置失败", e);
return ApiResponse.error(5001, "查询海信配置失败: " + e.getMessage());
}
}
}
\ No newline at end of file
package pangea.hiagent.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import pangea.hiagent.dto.ApiResponse;
import pangea.hiagent.model.Tool;
import pangea.hiagent.service.ToolService;
import java.util.List;
/**
* 工具API控制器
* 提供工具的增删改查功能
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/tools")
@Tag(name = "工具管理", description = "工具管理相关API")
public class ToolController {
private final ToolService toolService;
public ToolController(ToolService toolService) {
this.toolService = toolService;
}
/**
* 获取当前认证用户ID
* @return 用户ID
*/
private String getCurrentUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof String) {
return (String) authentication.getPrincipal();
}
return null;
}
/**
* 创建工具
*/
@PostMapping
@Operation(summary = "创建工具", description = "创建一个新的工具")
public ApiResponse<Tool> createTool(@RequestBody Tool tool) {
try {
String userId = getCurrentUserId();
if (userId == null) {
return ApiResponse.error(4001, "用户未认证");
}
Tool created = toolService.createTool(tool, userId);
return ApiResponse.success(created, "创建工具成功");
} catch (Exception e) {
log.error("创建工具失败", e);
return ApiResponse.error(4001, "创建工具失败: " + e.getMessage());
}
}
/**
* 更新工具
*/
@PutMapping("/{id}")
@Operation(summary = "更新工具", description = "更新指定ID的工具")
public ApiResponse<Tool> updateTool(@PathVariable String id, @RequestBody Tool tool) {
try {
String userId = getCurrentUserId();
if (userId == null) {
return ApiResponse.error(4001, "用户未认证");
}
tool.setId(id);
Tool updated = toolService.updateTool(tool, userId);
return ApiResponse.success(updated, "更新工具成功");
} catch (Exception e) {
log.error("更新工具失败", e);
return ApiResponse.error(4001, "更新工具失败: " + e.getMessage());
}
}
/**
* 删除工具
*/
@DeleteMapping("/{id}")
@Operation(summary = "删除工具", description = "删除指定ID的工具")
public ApiResponse<Void> deleteTool(@PathVariable String id) {
try {
String userId = getCurrentUserId();
if (userId == null) {
return ApiResponse.error(4001, "用户未认证");
}
toolService.deleteTool(id, userId);
return ApiResponse.success(null, "删除工具成功");
} catch (Exception e) {
log.error("删除工具失败", e);
return ApiResponse.error(4001, "删除工具失败: " + e.getMessage());
}
}
/**
* 获取工具详情
*/
@GetMapping("/{id}")
@Operation(summary = "获取工具详情", description = "获取指定ID的工具详情")
public ApiResponse<Tool> getTool(@PathVariable String id) {
try {
String userId = getCurrentUserId();
if (userId == null) {
return ApiResponse.error(4001, "用户未认证");
}
Tool tool = toolService.getToolById(id, userId);
if (tool == null) {
return ApiResponse.error(4004, "工具不存在");
}
return ApiResponse.success(tool, "获取工具成功");
} catch (Exception e) {
log.error("获取工具失败", e);
return ApiResponse.error(4001, "获取工具失败: " + e.getMessage());
}
}
/**
* 获取工具列表
*/
@GetMapping
@Operation(summary = "获取工具列表", description = "获取当前用户的所有工具")
public ApiResponse<List<Tool>> getTools() {
try {
String userId = getCurrentUserId();
if (userId == null) {
return ApiResponse.error(4001, "用户未认证");
}
List<Tool> tools = toolService.getUserTools(userId);
return ApiResponse.success(tools, "获取工具列表成功");
} catch (Exception e) {
log.error("获取工具列表失败", e);
return ApiResponse.error(4001, "获取工具列表失败: " + e.getMessage());
}
}
}
\ No newline at end of file
package pangea.hiagent.llm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.stereotype.Component;
import pangea.hiagent.model.LlmConfig;
/**
* DeepSeek模型适配器
* 实现DeepSeek模型的创建和配置(使用OpenAI兼容API)
*/
@Slf4j
@Component
public class DeepSeekModelAdapter implements ModelAdapter {
@Override
public ChatModel createChatModel(LlmConfig config) {
if (!validateConfig(config)) {
throw new IllegalArgumentException("无效的DeepSeek配置");
}
log.info("创建DeepSeek ChatModel,模型名称: {}, API密钥长度: {}", config.getModelName(), config.getApiKey().length());
try {
// DeepSeek使用OpenAI兼容的API,使用Builder模式创建OpenAiApi实例
// 注意:DeepSeek API端点不需要包含/v1,因为Spring AI会自动添加
String baseUrl = config.getBaseUrl() != null && !config.getBaseUrl().isEmpty() ?
config.getBaseUrl() : "https://api.deepseek.com";
OpenAiApi openAiApi = OpenAiApi.builder()
.baseUrl(baseUrl)
.apiKey(config.getApiKey())
.build();
log.info("OpenAiApi实例创建成功");
// 使用Builder模式创建OpenAiChatModel实例
OpenAiChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(OpenAiChatOptions.builder()
.model(config.getModelName())
.temperature(config.getTemperature())
.maxTokens(config.getMaxTokens())
.build())
.build();
log.info("OpenAiChatModel实例创建成功");
return chatModel;
} catch (Exception e) {
log.error("创建DeepSeek ChatModel时发生异常", e);
throw new RuntimeException("创建DeepSeek ChatModel失败: " + e.getMessage(), e);
}
}
@Override
public String getProviderName() {
return "deepseek";
}
@Override
public boolean validateConfig(LlmConfig config) {
// 修改验证逻辑,允许在没有API密钥的情况下启动,但会给出警告
if (config == null || !config.getEnabled()) {
return false;
}
// 如果启用了配置但没有API密钥,允许启动但会在使用时抛出异常
if (config.getApiKey() == null || config.getApiKey().isEmpty()) {
log.warn("DeepSeek配置已启用但未设置API密钥,将在实际使用时抛出异常");
return true;
}
return true;
}
}
\ No newline at end of file
package pangea.hiagent.llm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarFile;
/**
* 动态模型加载器
* 支持从外部JAR文件动态加载模型适配器
*/
@Slf4j
@Component
public class DynamicModelLoader {
private static final String MODEL_ADAPTERS_DIR = "model-adapters";
@PostConstruct
public void init() {
loadExternalModelAdapters();
}
/**
* 加载外部模型适配器
*/
public void loadExternalModelAdapters() {
File adaptersDir = new File(MODEL_ADAPTERS_DIR);
if (!adaptersDir.exists()) {
log.info("模型适配器目录不存在: {}", MODEL_ADAPTERS_DIR);
return;
}
if (!adaptersDir.isDirectory()) {
log.warn("模型适配器路径不是目录: {}", MODEL_ADAPTERS_DIR);
return;
}
File[] jarFiles = adaptersDir.listFiles((dir, name) -> name.endsWith(".jar"));
if (jarFiles == null || jarFiles.length == 0) {
log.info("模型适配器目录中没有找到JAR文件: {}", MODEL_ADAPTERS_DIR);
return;
}
for (File jarFile : jarFiles) {
try {
loadModelAdaptersFromJar(jarFile.getAbsolutePath());
} catch (Exception e) {
log.error("加载模型适配器JAR文件失败: {}", jarFile.getName(), e);
}
}
}
/**
* 从JAR文件加载模型适配器
*
* @param jarPath JAR文件路径
*/
public void loadModelAdaptersFromJar(String jarPath) {
log.info("正在加载模型适配器JAR文件: {}", jarPath);
try (JarFile jarFile = new JarFile(jarPath);
URLClassLoader classLoader = new URLClassLoader(new URL[]{new File(jarPath).toURI().toURL()},
this.getClass().getClassLoader())) {
// 扫描JAR文件中的类
List<Class<?>> adapterClasses = new ArrayList<>();
// 遍历JAR文件中的所有条目
jarFile.stream().forEach(entry -> {
if (entry.getName().endsWith(".class")) {
try {
String className = entry.getName().replace("/", ".").replace(".class", "");
Class<?> clazz = classLoader.loadClass(className);
// 检查类是否实现了ModelAdapter接口
if (ModelAdapter.class.isAssignableFrom(clazz) && !clazz.isInterface() &&
!clazz.equals(ModelAdapter.class)) {
adapterClasses.add(clazz);
log.info("发现模型适配器类: {}", className);
}
} catch (ClassNotFoundException e) {
log.warn("无法加载类: {}", entry.getName(), e);
}
}
});
// 注册发现的适配器
registerModelAdapters(adapterClasses, classLoader);
} catch (Exception e) {
log.error("从JAR文件加载模型适配器时发生错误: {}", jarPath, e);
throw new RuntimeException("加载模型适配器失败: " + e.getMessage(), e);
}
}
/**
* 注册模型适配器
*
* @param adapterClasses 适配器类列表
* @param classLoader 类加载器
*/
public void registerModelAdapters(List<Class<?>> adapterClasses, ClassLoader classLoader) {
for (Class<?> adapterClass : adapterClasses) {
try {
// 创建适配器实例
Object instance = adapterClass.getDeclaredConstructor().newInstance();
ModelAdapter adapter = (ModelAdapter) instance;
// 注册适配器
// 注意:这里需要通过反射或其他方式将适配器注册到ModelAdapterManager中
log.info("成功注册模型适配器: {}", adapter.getProviderName());
} catch (Exception e) {
log.error("注册模型适配器失败: {}", adapterClass.getName(), e);
}
}
}
/**
* 卸载模型适配器
*
* @param adapterClasses 适配器类列表
*/
public void unloadModelAdapters(List<Class<?>> adapterClasses) {
// 实现卸载逻辑
for (Class<?> adapterClass : adapterClasses) {
log.info("卸载模型适配器: {}", adapterClass.getName());
}
}
}
\ No newline at end of file
package pangea.hiagent.llm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.stereotype.Component;
import pangea.hiagent.model.LlmConfig;
/**
* Hisense模型适配器
* 实现与Hisense模型服务的对接,完全遵循OpenAI API规范
*/
@Slf4j
@Component
public class HisenseModelAdapter implements ModelAdapter {
// Hisense模型服务默认配置
@Override
public ChatModel createChatModel(LlmConfig config) {
if (!validateConfig(config)) {
throw new IllegalArgumentException("无效的Hisense配置");
}
try {
// 获取配置信息,直接使用配置中的值
String baseUrl = config.getBaseUrl();
String apiKey = config.getApiKey();
String modelName = config.getModelName();
// 确保baseUrl格式正确,移除可能存在的/chat/completions路径
if (baseUrl.endsWith("/chat/completions")) {
baseUrl = baseUrl.substring(0, baseUrl.length() - "/chat/completions".length());
}
// 移除可能存在的/v1后缀,因为OpenAiApi会自动添加/v1/chat/completions
if (baseUrl.endsWith("/v1")) {
baseUrl = baseUrl.substring(0, baseUrl.length() - "/v1".length());
}
log.info("创建Hisense ChatModel, baseUrl: {}, modelName: {}", baseUrl, modelName);
// 使用Builder模式创建OpenAiApi实例
OpenAiApi openAiApi = OpenAiApi.builder()
.apiKey(apiKey)
.baseUrl(baseUrl)
.build();
// 使用Builder模式创建OpenAiChatModel实例
return OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(OpenAiChatOptions.builder()
.model(modelName)
.temperature(config.getTemperature())
.maxTokens(config.getMaxTokens())
.build())
.build();
} catch (Exception e) {
log.error("创建Hisense ChatModel失败: {}", e.getMessage(), e);
throw new RuntimeException("创建Hisense ChatModel失败: " + e.getMessage(), e);
}
}
@Override
public String getProviderName() {
return "hisense";
}
@Override
public boolean validateConfig(LlmConfig config) {
// Hisense适配器至少需要启用配置
// API密钥和基础URL可以使用默认值
return config != null && config.getEnabled();
}
}
\ No newline at end of file
package pangea.hiagent.llm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import pangea.hiagent.model.LlmConfig;
/**
* LLM模型工厂类
* 用于根据配置动态创建ChatModel实例
*/
@Slf4j
@Component
public class LlmModelFactory {
@Autowired
private ModelAdapterManager modelAdapterManager;
public ModelAdapterManager getModelAdapterManager() {
return modelAdapterManager;
}
/**
* 根据配置创建ChatModel实例
*
* @param config LLM配置
* @return ChatModel实例
*/
public ChatModel createChatModel(LlmConfig config) {
return modelAdapterManager.createChatModel(config);
}
}
\ No newline at end of file
package pangea.hiagent.llm;
import org.springframework.ai.chat.model.ChatModel;
import pangea.hiagent.model.LlmConfig;
/**
* 模型适配器接口
* 定义创建ChatModel实例的标准方法
*/
public interface ModelAdapter {
/**
* 根据配置创建ChatModel实例
*
* @param config LLM配置
* @return ChatModel实例
*/
ChatModel createChatModel(LlmConfig config);
/**
* 获取适配器支持的提供商名称
*
* @return 提供商名称
*/
String getProviderName();
/**
* 验证配置是否有效
*
* @param config LLM配置
* @return 配置是否有效
*/
boolean validateConfig(LlmConfig config);
}
\ No newline at end of file
package pangea.hiagent.llm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import pangea.hiagent.model.LlmConfig;
import jakarta.annotation.PostConstruct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 模型适配器管理器
* 负责管理和选择合适的模型适配器
*/
@Slf4j
@Component
public class ModelAdapterManager {
@Autowired
private List<ModelAdapter> adapters;
private Map<String, ModelAdapter> adapterMap;
@PostConstruct
public void init() {
adapterMap = new HashMap<>();
for (ModelAdapter adapter : adapters) {
adapterMap.put(adapter.getProviderName().toLowerCase(), adapter);
log.info("注册模型适配器: {}", adapter.getProviderName());
}
}
/**
* 根据配置创建ChatModel实例
*
* @param config LLM配置
* @return ChatModel实例
*/
public ChatModel createChatModel(LlmConfig config) {
log.info("开始创建ChatModel,配置信息: {}", config);
if (config == null || !config.getEnabled()) {
log.warn("无效的LLM配置或配置未启用");
throw new IllegalArgumentException("无效的LLM配置或配置未启用");
}
String provider = config.getProvider();
log.info("LLM提供商: {}", provider);
if (provider == null || provider.isEmpty()) {
log.warn("LLM配置缺少提供商信息");
throw new IllegalArgumentException("LLM配置缺少提供商信息");
}
ModelAdapter adapter = adapterMap.get(provider.toLowerCase());
if (adapter == null) {
log.warn("不支持的LLM提供商: {}", provider);
throw new IllegalArgumentException("不支持的LLM提供商: " + provider);
}
log.info("找到适配器: {}", adapter.getClass().getSimpleName());
if (!adapter.validateConfig(config)) {
log.warn("LLM配置验证失败: {}", provider);
throw new IllegalArgumentException("LLM配置验证失败: " + provider);
}
try {
log.info("调用适配器创建ChatModel");
ChatModel model = adapter.createChatModel(config);
log.info("成功创建ChatModel");
return model;
} catch (Exception e) {
log.error("创建ChatModel失败: {}", e.getMessage(), e);
throw new RuntimeException("创建ChatModel失败: " + e.getMessage(), e);
}
}
/**
* 获取指定提供商的适配器
*
* @param provider 提供商名称
* @return 模型适配器
*/
public ModelAdapter getAdapter(String provider) {
if (provider == null || provider.isEmpty()) {
return null;
}
return adapterMap.get(provider.toLowerCase());
}
/**
* 获取所有已注册的适配器
*
* @return 适配器映射
*/
public Map<String, ModelAdapter> getAdapters() {
return new HashMap<>(adapterMap);
}
}
\ No newline at end of file
package pangea.hiagent.llm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.stereotype.Component;
import pangea.hiagent.model.LlmConfig;
/**
* Ollama模型适配器
* 实现Ollama模型的创建和配置
*/
@Slf4j
@Component
public class OllamaModelAdapter implements ModelAdapter {
@Override
public ChatModel createChatModel(LlmConfig config) {
if (!validateConfig(config)) {
throw new IllegalArgumentException("无效的Ollama配置");
}
try {
String baseUrl = config.getBaseUrl();
if (baseUrl == null || baseUrl.isEmpty()) {
baseUrl = "http://localhost:11434";
}
// 使用Builder模式创建OllamaChatModel实例
OllamaApi ollamaApi = new OllamaApi(baseUrl);
// 创建OllamaOptions配置
OllamaOptions options = OllamaOptions.builder()
.model(config.getModelName() != null ? config.getModelName() : "llama2")
.temperature(config.getTemperature())
.numPredict(config.getMaxTokens())
.build();
// 使用Builder模式创建OllamaChatModel实例
return OllamaChatModel.builder()
.ollamaApi(ollamaApi)
.defaultOptions(options)
.build();
} catch (Exception e) {
log.error("创建Ollama ChatModel失败: {}", e.getMessage(), e);
throw new RuntimeException("创建Ollama ChatModel失败: " + e.getMessage(), e);
}
}
@Override
public String getProviderName() {
return "ollama";
}
@Override
public boolean validateConfig(LlmConfig config) {
return config != null && config.getEnabled();
}
}
\ No newline at end of file
package pangea.hiagent.llm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.stereotype.Component;
import pangea.hiagent.model.LlmConfig;
/**
* OpenAI模型适配器
* 实现OpenAI模型的创建和配置
*/
@Slf4j
@Component
public class OpenAiModelAdapter implements ModelAdapter {
@Override
public ChatModel createChatModel(LlmConfig config) {
if (!validateConfig(config)) {
throw new IllegalArgumentException("无效的OpenAI配置");
}
try {
// 使用Builder模式创建OpenAiApi实例
OpenAiApi openAiApi = OpenAiApi.builder()
.apiKey(config.getApiKey())
.baseUrl(config.getBaseUrl() != null && !config.getBaseUrl().isEmpty() ?
config.getBaseUrl() : "https://api.openai.com/v1")
.build();
// 使用Builder模式创建OpenAiChatModel实例
return OpenAiChatModel.builder()
.openAiApi(openAiApi)
.defaultOptions(OpenAiChatOptions.builder()
.model(config.getModelName())
.temperature(config.getTemperature())
.maxTokens(config.getMaxTokens())
.build())
.build();
} catch (Exception e) {
log.error("创建OpenAI ChatModel失败: {}", e.getMessage(), e);
throw new RuntimeException("创建OpenAI ChatModel失败: " + e.getMessage(), e);
}
}
@Override
public String getProviderName() {
return "openai";
}
@Override
public boolean validateConfig(LlmConfig config) {
return config != null &&
config.getEnabled() &&
config.getApiKey() != null &&
!config.getApiKey().isEmpty();
}
}
\ No newline at end of file
package pangea.hiagent.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import pangea.hiagent.model.Agent;
import pangea.hiagent.model.AgentDialogue;
import pangea.hiagent.model.LlmConfig;
import pangea.hiagent.repository.AgentRepository;
import pangea.hiagent.repository.AgentDialogueRepository;
import pangea.hiagent.repository.LlmConfigRepository;
import pangea.hiagent.llm.LlmModelFactory;
import java.util.List;
/**
* Agent服务类
* 负责Agent的管理和相关业务逻辑
*/
@Slf4j
@Service
public class AgentService {
private final AgentRepository agentRepository;
private final AgentDialogueRepository agentDialogueRepository;
private final LlmConfigRepository llmConfigRepository;
private final LlmModelFactory llmModelFactory;
public AgentService(AgentRepository agentRepository,
AgentDialogueRepository agentDialogueRepository,
LlmConfigRepository llmConfigRepository,
LlmModelFactory llmModelFactory) {
this.agentRepository = agentRepository;
this.agentDialogueRepository = agentDialogueRepository;
this.llmConfigRepository = llmConfigRepository;
this.llmModelFactory = llmModelFactory;
}
/**
* 创建Agent
*/
@Transactional
public Agent createAgent(Agent agent) {
log.info("创建Agent: {}", agent.getName());
// 设置默认值
if (agent.getTemperature() == null) {
agent.setTemperature(0.7);
}
if (agent.getMaxTokens() == null) {
agent.setMaxTokens(4096);
}
if (agent.getTopP() == null) {
agent.setTopP(0.9);
}
if (agent.getTopK() == null) {
agent.setTopK(50);
}
if (agent.getPresencePenalty() == null) {
agent.setPresencePenalty(0.0);
}
if (agent.getFrequencyPenalty() == null) {
agent.setFrequencyPenalty(0.0);
}
if (agent.getHistoryLength() == null) {
agent.setHistoryLength(10);
}
if (agent.getStatus() == null || agent.getStatus().isEmpty()) {
agent.setStatus("active");
}
agentRepository.insert(agent);
return agent;
}
/**
* 更新Agent
*/
@Transactional
public Agent updateAgent(Agent agent) {
log.info("更新Agent: {}", agent.getId());
// 保留原始所有者信息
Agent existingAgent = agentRepository.selectById(agent.getId());
if (existingAgent != null) {
agent.setOwner(existingAgent.getOwner());
agent.setCreatedBy(existingAgent.getCreatedBy());
agent.setCreatedAt(existingAgent.getCreatedAt());
}
agentRepository.updateById(agent);
return agent;
}
/**
* 删除Agent
*/
@Transactional
public void deleteAgent(String id) {
log.info("删除Agent: {}", id);
agentRepository.deleteById(id);
}
/**
* 获取Agent详情
*
* @param id Agent ID
* @return Agent对象,如果不存在则返回null
*/
public Agent getAgent(String id) {
if (id == null || id.isEmpty()) {
log.warn("尝试使用无效ID获取Agent");
return null;
}
return agentRepository.selectById(id);
}
/**
* 获取Agent列表
*
* @return Agent列表
*/
public List<Agent> listAgents() {
List<Agent> agents = agentRepository.selectList(null);
log.info("获取到 {} 个Agent", agents != null ? agents.size() : 0);
return agents != null ? agents : List.of();
}
/**
* 分页获取Agent列表
*/
public IPage<Agent> pageAgents(Long current, Long size, String name, String status) {
Page<Agent> page = new Page<>(current, size);
LambdaQueryWrapper<Agent> wrapper = new LambdaQueryWrapper<>();
if (name != null) {
wrapper.like(Agent::getName, name);
}
if (status != null) {
wrapper.eq(Agent::getStatus, status);
}
// 使用优化的分页查询方法
return agentRepository.selectPageWithOptimization(page, wrapper);
}
/**
* 获取用户的Agent列表
*/
public List<Agent> getUserAgents(String userId) {
// 使用优化的查询方法
return agentRepository.findActiveAgentsByOwnerWithExplicitColumns(userId);
}
/**
* 保存对话记录
*/
@Transactional
public void saveDialogue(AgentDialogue dialogue) {
agentDialogueRepository.insert(dialogue);
}
/**
* 根据Agent获取对应的LLM模型
*
* @param agent Agent对象
* @return 对应的ChatModel实例
*/
public org.springframework.ai.chat.model.ChatModel getChatModelForAgent(Agent agent) {
// 获取Agent配置的模型配置名称
String modelConfigName = agent.getDefaultModel();
log.info("获取Agent {} 的ChatModel,模型配置名称: {}", agent.getId(), modelConfigName);
if (modelConfigName == null || modelConfigName.isEmpty()) {
throw new IllegalArgumentException("Agent未配置默认模型");
}
// 根据模型配置名称查找对应的LLM配置
LambdaQueryWrapper<LlmConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LlmConfig::getName, modelConfigName);
wrapper.eq(LlmConfig::getEnabled, true);
LlmConfig llmConfig = llmConfigRepository.selectOne(wrapper);
if (llmConfig == null) {
log.error("未找到启用的LLM配置: {}", modelConfigName);
throw new IllegalArgumentException("未找到启用的LLM配置: " + modelConfigName);
}
log.info("找到LLM配置: {} ({})", llmConfig.getName(), llmConfig.getProvider());
// 使用LlmModelFactory创建对应的ChatModel实例
return llmModelFactory.createChatModel(llmConfig);
}
}
\ No newline at end of file
package pangea.hiagent.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import pangea.hiagent.model.AuthMode;
import java.util.HashMap;
import java.util.Map;
/**
* 认证配置管理服务
* 管理各种认证模式的启用/禁用状态和配置信息
*/
@Slf4j
@Service
public class AuthConfigService {
// 本地缓存的认证模式配置
private final Map<String, Boolean> authModeEnabledCache = new HashMap<>();
private final Map<String, Map<String, Object>> authModeConfigCache = new HashMap<>();
public AuthConfigService() {
// 初始化默认配置
initializeDefaultConfigs();
}
/**
* 初始化默认配置
*/
private void initializeDefaultConfigs() {
// 本地认证模式默认启用
authModeEnabledCache.put(AuthMode.LOCAL.getCode(), true);
// OAuth2 授权码模式默认启用(但需要配置提供者)
authModeEnabledCache.put(AuthMode.OAUTH2_AUTHORIZATION_CODE.getCode(), true);
// 其他模式默认禁用
authModeEnabledCache.put(AuthMode.OAUTH2_IMPLICIT.getCode(), false);
authModeEnabledCache.put(AuthMode.OAUTH2_CLIENT_CREDENTIALS.getCode(), false);
authModeEnabledCache.put(AuthMode.LDAP.getCode(), false);
authModeEnabledCache.put(AuthMode.SAML.getCode(), false);
log.info("认证配置初始化完成");
}
/**
* 检查认证模式是否启用
*/
public boolean isAuthModeEnabled(String authMode) {
Boolean enabled = authModeEnabledCache.get(authMode);
return enabled != null && enabled;
}
/**
* 启用认证模式
*/
public void enableAuthMode(String authMode) {
authModeEnabledCache.put(authMode, true);
log.info("启用认证模式: {}", authMode);
}
/**
* 禁用认证模式
*/
public void disableAuthMode(String authMode) {
authModeEnabledCache.put(authMode, false);
log.info("禁用认证模式: {}", authMode);
}
/**
* 获取认证模式的配置
*/
public Map<String, Object> getAuthModeConfig(String authMode) {
return authModeConfigCache.getOrDefault(authMode, new HashMap<>());
}
/**
* 设置认证模式的配置
*/
public void setAuthModeConfig(String authMode, Map<String, Object> config) {
authModeConfigCache.put(authMode, config);
log.info("更新认证模式配置: {}", authMode);
}
/**
* 获取所有启用的认证模式
*/
public Map<String, Boolean> getAllAuthModes() {
return new HashMap<>(authModeEnabledCache);
}
}
package pangea.hiagent.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import pangea.hiagent.model.User;
import pangea.hiagent.repository.UserRepository;
import pangea.hiagent.utils.JwtUtil;
import java.util.Arrays;
/**
* 认证服务类
* 负责用户的注册、登录和认证逻辑
*/
@Slf4j
@Service
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
private final Environment environment;
public AuthService(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtUtil jwtUtil, Environment environment) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
this.jwtUtil = jwtUtil;
this.environment = environment;
}
/**
* 用户注册
*/
@Transactional
public User register(String username, String password, String email) {
log.info("用户注册: {}", username);
// 检查用户名是否存在
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername, username);
User existingUser = userRepository.selectOne(wrapper);
if (existingUser != null) {
throw new RuntimeException("用户名已存在");
}
// 注意:生产环境中不应记录密码相关信息
log.debug("用户 {} 注册请求已接收", username);
// 创建新用户
User user = User.builder()
.username(username)
.password(passwordEncoder.encode(password))
.email(email)
.status("active")
.role("user")
.build();
userRepository.insert(user);
return user;
}
/**
* 用户登录
*/
public String login(String username, String password) {
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("用户不存在");
}
// 检查是否为开发环境,如果是则允许任意密码
boolean isDevEnvironment = Arrays.asList(environment.getActiveProfiles()).contains("dev") ||
Arrays.asList(environment.getDefaultProfiles()).contains("default");
if (isDevEnvironment) {
log.info("开发环境: 跳过密码验证");
} else {
// 验证密码
// 注意:生产环境中不应记录密码相关信息
// 使用BCryptPasswordEncoder验证密码
boolean passwordMatch = passwordEncoder.matches(password, user.getPassword());
if (!passwordMatch) {
log.warn("登录失败: 用户 {} 密码错误", username);
throw new RuntimeException("密码错误");
}
}
// 检查用户状态
if (!"active".equals(user.getStatus())) {
log.warn("登录失败: 用户 {} 已被禁用", username);
throw new RuntimeException("用户已禁用");
}
// 更新最后登录时间
user.setLastLoginTime(System.currentTimeMillis());
userRepository.updateById(user);
// 生成Token
String token = jwtUtil.generateToken(user.getId());
log.info("用户 {} 登录成功,生成Token: {}", username, token);
return token;
}
/**
* 获取用户信息
*/
public User getUserById(String userId) {
return userRepository.selectById(userId);
}
/**
* 验证Token
*/
public boolean validateToken(String token) {
return jwtUtil.validateToken(token);
}
/**
* 从Token获取用户ID
*/
public String getUserIdFromToken(String token) {
return jwtUtil.getUserIdFromToken(token);
}
}
\ No newline at end of file
package pangea.hiagent.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import pangea.hiagent.config.AppConfig;
import pangea.hiagent.model.LlmConfig;
import pangea.hiagent.repository.LlmConfigRepository;
import java.util.List;
/**
* LLM配置服务类
* 负责LLM配置的管理和相关业务逻辑
*/
@Slf4j
@Service
public class LlmConfigService {
private final LlmConfigRepository llmConfigRepository;
private final Environment environment;
private final AppConfig appConfig;
public LlmConfigService(LlmConfigRepository llmConfigRepository, Environment environment, AppConfig appConfig) {
this.llmConfigRepository = llmConfigRepository;
this.environment = environment;
this.appConfig = appConfig;
// 初始化默认配置
initializeDefaultConfigs();
}
/**
* 初始化默认配置
*/
private void initializeDefaultConfigs() {
// 检查是否已有配置,如果没有则创建默认配置
if (llmConfigRepository.selectCount(null) == 0) {
log.info("初始化默认LLM配置");
// 创建默认的DeepSeek配置,从环境变量获取API密钥
String deepseekApiKey = environment.getProperty("DEEPSEEK_API_KEY",
appConfig.getLlm().getDeepseek().getDefaultApiKey());
LlmConfig deepseekConfig = LlmConfig.builder()
.name("deepseek-default")
.description("DeepSeek默认配置")
.provider("deepseek")
.modelName(appConfig.getLlm().getDeepseek().getDefaultModel())
.apiKey(deepseekApiKey)
.baseUrl(appConfig.getLlm().getDeepseek().getBaseUrl())
.temperature(appConfig.getAgent().getDefaultTemperature())
.maxTokens(appConfig.getAgent().getDefaultMaxTokens())
.topP(0.9)
.enabled(true) // 总是启用,即使没有API密钥也能正常启动
.owner("system")
.build();
llmConfigRepository.insert(deepseekConfig);
// 创建默认的OpenAI配置
String openaiApiKey = environment.getProperty("OPENAI_API_KEY",
appConfig.getLlm().getOpenai().getDefaultApiKey());
LlmConfig openaiConfig = LlmConfig.builder()
.name("openai-default")
.description("OpenAI默认配置")
.provider("openai")
.modelName(appConfig.getLlm().getOpenai().getDefaultModel())
.apiKey(openaiApiKey)
.baseUrl(appConfig.getLlm().getOpenai().getBaseUrl())
.temperature(appConfig.getAgent().getDefaultTemperature())
.maxTokens(appConfig.getAgent().getDefaultMaxTokens())
.topP(0.9)
.enabled(!openaiApiKey.isEmpty()) // 仅在配置了API密钥时启用
.owner("system")
.build();
llmConfigRepository.insert(openaiConfig);
// 创建默认的Ollama配置
LlmConfig ollamaConfig = LlmConfig.builder()
.name("ollama-default")
.description("Ollama默认配置")
.provider("ollama")
.modelName(appConfig.getLlm().getOllama().getDefaultModel())
.apiKey("")
.baseUrl(appConfig.getLlm().getOllama().getBaseUrl())
.temperature(appConfig.getAgent().getDefaultTemperature())
.maxTokens(appConfig.getAgent().getDefaultMaxTokens())
.topP(0.9)
.enabled(true)
.owner("system")
.build();
llmConfigRepository.insert(ollamaConfig);
} else {
// 如果已有配置,检查是否有启用的配置,如果没有则启用所有配置
List<LlmConfig> allConfigs = llmConfigRepository.selectList(null);
boolean hasEnabledConfig = allConfigs.stream().anyMatch(LlmConfig::getEnabled);
if (!hasEnabledConfig) {
log.info("未发现启用的LLM配置,正在启用所有现有配置");
for (LlmConfig config : allConfigs) {
config.setEnabled(true);
llmConfigRepository.updateById(config);
}
}
}
}
/**
* 创建LLM配置
*/
@Transactional
public LlmConfig createLlmConfig(LlmConfig config) {
log.info("创建LLM配置: {}", config.getName());
llmConfigRepository.insert(config);
return config;
}
/**
* 更新LLM配置
*/
@Transactional
public LlmConfig updateLlmConfig(LlmConfig config) {
log.info("更新LLM配置: {}", config.getName());
llmConfigRepository.updateById(config);
return config;
}
/**
* 删除LLM配置
*/
@Transactional
public void deleteLlmConfig(String id) {
log.info("删除LLM配置: {}", id);
llmConfigRepository.deleteById(id);
}
/**
* 获取LLM配置详情
*
* @param id 配置ID
* @return LLM配置对象,如果不存在则返回null
*/
public LlmConfig getLlmConfig(String id) {
log.info("获取LLM配置,ID: {}", id);
if (id == null || id.isEmpty()) {
log.warn("尝试使用无效ID获取LLM配置");
return null;
}
LlmConfig config = llmConfigRepository.selectById(id);
log.info("获取到LLM配置: {}", config);
return config;
}
/**
* 根据名称获取LLM配置
*
* @param name 配置名称
* @return LLM配置对象,如果不存在则返回null
*/
public LlmConfig getLlmConfigByName(String name) {
if (name == null || name.isEmpty()) {
log.warn("尝试使用无效名称获取LLM配置");
return null;
}
LambdaQueryWrapper<LlmConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LlmConfig::getName, name);
return llmConfigRepository.selectOne(wrapper);
}
/**
* 获取启用的LLM配置列表
*/
public List<LlmConfig> getEnabledLlmConfigs() {
LambdaQueryWrapper<LlmConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(LlmConfig::getEnabled, true);
return llmConfigRepository.selectList(wrapper);
}
/**
* 获取LLM配置列表
*/
public List<LlmConfig> listLlmConfigs() {
return llmConfigRepository.selectList(null);
}
/**
* 分页获取LLM配置列表
*/
public IPage<LlmConfig> pageLlmConfigs(Long current, Long size, String name, String provider) {
Page<LlmConfig> page = new Page<>(current, size);
LambdaQueryWrapper<LlmConfig> wrapper = new LambdaQueryWrapper<>();
if (name != null && !name.isEmpty()) {
wrapper.like(LlmConfig::getName, name);
}
if (provider != null && !provider.isEmpty()) {
wrapper.eq(LlmConfig::getProvider, provider);
}
return llmConfigRepository.selectPage(page, wrapper);
}
}
\ No newline at end of file
package pangea.hiagent.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import pangea.hiagent.model.OAuth2Provider;
import pangea.hiagent.repository.OAuth2ProviderRepository;
import java.util.List;
/**
* OAuth2 提供者服务类
* 负责OAuth2提供商配置的管理和相关业务逻辑
*/
@Slf4j
@Service
public class OAuth2ProviderService {
private final OAuth2ProviderRepository oAuth2ProviderRepository;
public OAuth2ProviderService(OAuth2ProviderRepository oAuth2ProviderRepository) {
this.oAuth2ProviderRepository = oAuth2ProviderRepository;
}
/**
* 创建OAuth2提供商配置
*
* @param provider OAuth2提供商配置对象
* @return 创建后的OAuth2提供商配置对象
*/
@Transactional
public OAuth2Provider createProvider(OAuth2Provider provider) {
log.info("创建OAuth2提供商配置: providerName={}", provider.getProviderName());
oAuth2ProviderRepository.insert(provider);
log.info("OAuth2提供商配置创建成功: id={}", provider.getId());
return provider;
}
/**
* 更新OAuth2提供商配置
*
* @param id 提供商ID
* @param provider 更新的OAuth2提供商配置对象
* @return 更新后的OAuth2提供商配置对象
*/
@Transactional
public OAuth2Provider updateProvider(String id, OAuth2Provider provider) {
log.info("更新OAuth2提供商配置: id={}", id);
OAuth2Provider existingProvider = oAuth2ProviderRepository.selectById(id);
if (existingProvider == null) {
log.warn("尝试更新不存在的OAuth2提供商配置: id={}", id);
throw new RuntimeException("OAuth2提供商配置不存在");
}
// 更新字段
existingProvider.setProviderName(provider.getProviderName());
existingProvider.setDisplayName(provider.getDisplayName());
existingProvider.setDescription(provider.getDescription());
existingProvider.setAuthType(provider.getAuthType());
existingProvider.setAuthorizeUrl(provider.getAuthorizeUrl());
existingProvider.setTokenUrl(provider.getTokenUrl());
existingProvider.setUserinfoUrl(provider.getUserinfoUrl());
existingProvider.setClientId(provider.getClientId());
existingProvider.setClientSecret(provider.getClientSecret());
existingProvider.setRedirectUri(provider.getRedirectUri());
existingProvider.setScope(provider.getScope());
existingProvider.setEnabled(provider.getEnabled());
existingProvider.setConfigJson(provider.getConfigJson());
existingProvider.setUpdatedBy(provider.getUpdatedBy());
oAuth2ProviderRepository.updateById(existingProvider);
log.info("OAuth2提供商配置更新成功: id={}", id);
return existingProvider;
}
/**
* 删除OAuth2提供商配置
*
* @param id 提供商ID
*/
@Transactional
public void deleteProvider(String id) {
log.info("删除OAuth2提供商配置: id={}", id);
oAuth2ProviderRepository.deleteById(id);
log.info("OAuth2提供商配置删除成功: id={}", id);
}
/**
* 根据ID获取OAuth2提供商配置
*
* @param id 提供商ID
* @return OAuth2提供商配置对象,如果不存在则返回null
*/
public OAuth2Provider getProviderById(String id) {
if (id == null || id.isEmpty()) {
log.warn("尝试使用无效ID获取OAuth2提供商配置");
return null;
}
return oAuth2ProviderRepository.selectById(id);
}
/**
* 根据提供商名称获取OAuth2提供商配置
*
* @param providerName 提供商名称
* @return OAuth2提供商配置对象,如果不存在则返回null
*/
public OAuth2Provider getProviderByName(String providerName) {
if (providerName == null || providerName.isEmpty()) {
log.warn("尝试使用无效提供商名称获取OAuth2提供商配置");
return null;
}
LambdaQueryWrapper<OAuth2Provider> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OAuth2Provider::getProviderName, providerName);
return oAuth2ProviderRepository.selectOne(wrapper);
}
/**
* 获取OAuth2提供商配置列表
*
* @return OAuth2提供商配置列表
*/
public List<OAuth2Provider> listProviders() {
List<OAuth2Provider> providers = oAuth2ProviderRepository.selectList(null);
log.info("获取到 {} 个OAuth2提供商配置", providers != null ? providers.size() : 0);
return providers != null ? providers : List.of();
}
/**
* 分页获取OAuth2提供商配置列表
*
* @param current 当前页码
* @param size 每页大小
* @param providerName 提供商名称(模糊查询)
* @param displayName 显示名称(模糊查询)
* @param enabled 启用状态
* @return 分页结果
*/
public IPage<OAuth2Provider> pageProviders(Long current, Long size, String providerName, String displayName, Integer enabled) {
Page<OAuth2Provider> page = new Page<>(current, size);
LambdaQueryWrapper<OAuth2Provider> wrapper = new LambdaQueryWrapper<>();
if (providerName != null && !providerName.isEmpty()) {
wrapper.like(OAuth2Provider::getProviderName, providerName);
}
if (displayName != null && !displayName.isEmpty()) {
wrapper.like(OAuth2Provider::getDisplayName, displayName);
}
if (enabled != null) {
wrapper.eq(OAuth2Provider::getEnabled, enabled);
}
return oAuth2ProviderRepository.selectPage(page, wrapper);
}
/**
* 启用OAuth2提供商配置
*
* @param id 提供商ID
*/
@Transactional
public void enableProvider(String id) {
log.info("启用OAuth2提供商配置: id={}", id);
OAuth2Provider provider = oAuth2ProviderRepository.selectById(id);
if (provider != null) {
provider.setEnabled(1);
oAuth2ProviderRepository.updateById(provider);
log.info("OAuth2提供商配置启用成功: id={}", id);
} else {
log.warn("尝试启用不存在的OAuth2提供商配置: id={}", id);
throw new RuntimeException("OAuth2提供商配置不存在");
}
}
/**
* 禁用OAuth2提供商配置
*
* @param id 提供商ID
*/
@Transactional
public void disableProvider(String id) {
log.info("禁用OAuth2提供商配置: id={}", id);
OAuth2Provider provider = oAuth2ProviderRepository.selectById(id);
if (provider != null) {
provider.setEnabled(0);
oAuth2ProviderRepository.updateById(provider);
log.info("OAuth2提供商配置禁用成功: id={}", id);
} else {
log.warn("尝试禁用不存在的OAuth2提供商配置: id={}", id);
throw new RuntimeException("OAuth2提供商配置不存在");
}
}
}
\ No newline at end of file
package pangea.hiagent.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import pangea.hiagent.model.Agent;
import pangea.hiagent.model.Tool;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* ReAct Agent服务类
* 负责实现ReAct Agent的核心逻辑
*/
@Slf4j
@Service
public class ReActAgentService {
@Autowired
private ChatModel chatModel;
@Autowired
private ToolService toolService;
@Autowired
private AgentService agentService;
/**
* 处理用户请求的主方法
*
* @param agent Agent对象
* @param userMessage 用户消息
* @return 处理结果
*/
public String processRequest(Agent agent, String userMessage) {
log.info("开始处理ReAct Agent请求,Agent ID: {}, 用户消息: {}", agent.getId(), userMessage);
// 初始化对话历史
List<Message> conversationHistory = new ArrayList<>();
// 添加系统提示词
String systemPrompt = agent.getSystemPrompt() != null ? agent.getSystemPrompt() :
"You are a helpful AI assistant that can use tools to help answer questions. " +
"Use the following format to think through problems step by step:\n\n" +
"Thought: Think about what to do.\n" +
"Action: Call a tool with parameters.\n" +
"Observation: Observe the result of the tool call.\n\n" +
"Continue this process until you have enough information to answer the question.\n" +
"Finally, provide your answer in the format:\n" +
"Final Answer: [Your answer here]";
conversationHistory.add(new SystemMessage(systemPrompt));
// 添加用户消息
conversationHistory.add(new UserMessage(userMessage));
// 执行ReAct循环
for (int i = 0; i < 5; i++) { // 最多执行5轮
log.info("执行ReAct循环第 {} 轮", i + 1);
// 构建Prompt
Prompt prompt = new Prompt(conversationHistory);
// 调用模型
ChatResponse response = chatModel.call(prompt);
String aiResponse = response.getResult().getOutput().getText();
log.info("模型响应: {}", aiResponse);
// 检查是否需要工具调用
if (needsToolCall(aiResponse)) {
log.info("检测到需要工具调用");
// 解析工具调用
ToolCall toolCall = parseToolCall(aiResponse);
if (toolCall != null) {
log.info("解析到工具调用: 工具名称={}, 参数={}", toolCall.getToolName(), toolCall.getParameters());
// 执行工具调用
String toolResult = toolService.executeTool(toolCall.getToolName(), toolCall.getParameters());
log.info("工具执行结果: {}", toolResult);
// 将工具结果添加到对话历史
conversationHistory.add(new AssistantMessage(aiResponse));
conversationHistory.add(new UserMessage("Observation: " + toolResult));
} else {
log.warn("工具调用解析失败,结束ReAct循环");
break;
}
} else {
log.info("无需工具调用,返回最终答案");
// 返回最终结果
String finalAnswer = extractFinalAnswer(aiResponse);
log.info("最终答案: {}", finalAnswer);
return finalAnswer;
}
}
log.warn("达到最大执行轮次限制");
return "达到最大执行轮次限制";
}
/**
* 判断是否需要工具调用
*
* @param response 模型响应
* @return 是否需要工具调用
*/
private boolean needsToolCall(String response) {
return response.contains("Action:");
}
/**
* 解析工具调用
*
* @param response 模型响应
* @return 工具调用对象
*/
private ToolCall parseToolCall(String response) {
// 使用正则表达式解析Action行
// 支持多种格式:Action: tool_name(param1=value1, param2=value2) 或 Action: tool_name()
Pattern actionPattern = Pattern.compile("Action:\\s*(\\w+)\\s*\\(([^)]*)\\)");
Matcher matcher = actionPattern.matcher(response);
if (matcher.find()) {
String toolName = matcher.group(1);
String paramsStr = matcher.group(2);
// 解析参数
Map<String, Object> parameters = parseParameters(paramsStr);
return new ToolCall(toolName, parameters);
}
return null;
}
/**
* 解析参数字符串
*
* @param paramsStr 参数字符串
* @return 参数Map
*/
private Map<String, Object> parseParameters(String paramsStr) {
Map<String, Object> parameters = new HashMap<>();
// 如果参数字符串为空,直接返回空Map
if (paramsStr == null || paramsStr.trim().isEmpty()) {
return parameters;
}
// 解析参数,支持带引号和不带引号的值
// 格式: key1=value1, key2="value with spaces", key3='another value'
String[] paramPairs = paramsStr.split(","); // 简化处理,按逗号分割
for (String paramPair : paramPairs) {
String[] parts = paramPair.split("=");
if (parts.length == 2) {
String key = parts[0].trim();
String value = parts[1].trim();
// 移除引号(如果存在)
if ((value.startsWith("\"") && value.endsWith("\"")) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.substring(1, value.length() - 1);
}
parameters.put(key, value);
}
}
return parameters;
}
/**
* 提取最终答案
*
* @param response 模型响应
* @return 最终答案
*/
private String extractFinalAnswer(String response) {
// 查找Final Answer:后面的文本
Pattern answerPattern = Pattern.compile("Final Answer:\\s*(.*)", Pattern.DOTALL);
Matcher matcher = answerPattern.matcher(response);
if (matcher.find()) {
return matcher.group(1).trim();
}
// 如果没有找到Final Answer格式,返回整个响应
return response;
}
/**
* 工具调用内部类
*/
private static class ToolCall {
private final String toolName;
private final Map<String, Object> parameters;
public ToolCall(String toolName, Map<String, Object> parameters) {
this.toolName = toolName;
this.parameters = parameters;
}
public String getToolName() {
return toolName;
}
public Map<String, Object> getParameters() {
return parameters;
}
}
}
\ No newline at end of file
package pangea.hiagent.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import pangea.hiagent.model.Tool;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
/**
* 时间工具服务类
* 提供获取当前时间的功能
*/
@Slf4j
@Service
public class TimeToolService {
/**
* 执行时间工具调用
*
* @param tool 工具信息
* @param parameters 工具参数
* @return 当前时间的字符串表示
*/
public String executeTimeTool(Tool tool, Map<String, Object> parameters) {
log.info("执行时间工具调用: {}", tool.getName());
try {
// 获取当前时间
LocalDateTime now = LocalDateTime.now();
// 格式化时间
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedTime = now.format(formatter);
// 构造返回结果
return "{\n" +
" \"currentTime\": \"" + formattedTime + "\",\n" +
" \"timestamp\": " + System.currentTimeMillis() + "\n" +
"}";
} catch (Exception e) {
log.error("时间工具调用失败: {}", tool.getName(), e);
return "{\n" +
" \"error\": \"时间工具调用失败\",\n" +
" \"message\": \"" + e.getMessage() + "\"\n" +
"}";
}
}
}
\ No newline at end of file
package pangea.hiagent.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import pangea.hiagent.auth.AuthenticationStrategy;
import pangea.hiagent.model.AuthMode;
import pangea.hiagent.model.OAuth2Provider;
import pangea.hiagent.repository.OAuth2ProviderRepository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 统一认证服务
* 作为认证策略的门面,支持多种认证方式的协调调度
*/
@Slf4j
@Service
public class UnifiedAuthService {
private final List<AuthenticationStrategy> authenticationStrategies;
private final AuthConfigService authConfigService;
private final OAuth2ProviderRepository oauth2ProviderRepository;
public UnifiedAuthService(List<AuthenticationStrategy> authenticationStrategies,
AuthConfigService authConfigService,
OAuth2ProviderRepository oauth2ProviderRepository) {
this.authenticationStrategies = authenticationStrategies;
this.authConfigService = authConfigService;
this.oauth2ProviderRepository = oauth2ProviderRepository;
}
/**
* 执行统一认证
* @param authMode 认证模式(如 "local"、"oauth2_auth_code" 等)
* @param credentials 认证凭证,结构根据不同认证模式而异
* @return JWT Token
*/
public String authenticate(String authMode, Map<String, Object> credentials) {
log.info("执行统一认证: authMode={}", authMode);
// 查找支持该认证模式的策略
AuthenticationStrategy strategy = findStrategy(authMode);
if (strategy == null) {
log.error("未找到支持的认证策略: authMode={}", authMode);
throw new RuntimeException("不支持的认证模式: " + authMode);
}
try {
// 验证认证模式是否启用
if (!authConfigService.isAuthModeEnabled(authMode)) {
log.warn("认证模式未启用: authMode={}", authMode);
throw new RuntimeException("该认证模式未启用");
}
// 执行认证策略
String token = strategy.authenticate(credentials);
log.info("统一认证成功: authMode={}", authMode);
return token;
} catch (Exception e) {
log.error("统一认证失败: authMode={}, 错误堆栈: ", authMode, e);
throw e;
}
}
/**
* 执行本地用户名/密码认证
* @param username 用户名
* @param password 密码
* @return JWT Token
*/
public String loginWithLocal(String username, String password) {
Map<String, Object> credentials = new HashMap<>();
credentials.put("username", username);
credentials.put("password", password);
return authenticate(AuthMode.LOCAL.getCode(), credentials);
}
/**
* 执行 OAuth2 授权码认证
* @param authorizationCode 授权码
* @param providerName OAuth2 提供者名称
* @return JWT Token
*/
public String loginWithOAuth2(String authorizationCode, String providerName) {
Map<String, Object> credentials = new HashMap<>();
credentials.put("authorizationCode", authorizationCode);
credentials.put("providerName", providerName);
return authenticate(AuthMode.OAUTH2_AUTHORIZATION_CODE.getCode(), credentials);
}
/**
* 验证令牌
*/
public boolean verifyToken(String token) {
try {
for (AuthenticationStrategy strategy : authenticationStrategies) {
if (strategy.verify(token)) {
return true;
}
}
return false;
} catch (Exception e) {
log.error("令牌验证过程中出错: 错误堆栈: ", e);
return false;
}
}
/**
* 查找支持该认证模式的策略
*/
private AuthenticationStrategy findStrategy(String authMode) {
for (AuthenticationStrategy strategy : authenticationStrategies) {
if (strategy.supports(authMode)) {
log.debug("找到支持的认证策略: authMode={}, strategyName={}", authMode, strategy.getName());
return strategy;
}
}
return null;
}
/**
* 获取所有可用的认证模式
*/
public Map<String, Object> getAvailableAuthModes() {
Map<String, Object> modes = new HashMap<>();
for (AuthMode mode : AuthMode.values()) {
if (authConfigService.isAuthModeEnabled(mode.getCode())) {
Map<String, String> modeInfo = new HashMap<>();
modeInfo.put("code", mode.getCode());
modeInfo.put("description", mode.getDescription());
modes.put(mode.getCode(), modeInfo);
}
}
return modes;
}
/**
* 构造 OAuth2 授权 URL
* @param providerName OAuth2 提供者名称
* @return 授权 URL
*/
public String buildOAuth2AuthorizationUrl(String providerName) {
log.debug("构造 OAuth2 授权 URL: providerName={}", providerName);
LambdaQueryWrapper<OAuth2Provider> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OAuth2Provider::getProviderName, providerName)
.eq(OAuth2Provider::getEnabled, 1);
OAuth2Provider provider = oauth2ProviderRepository.selectOne(wrapper);
if (provider == null) {
log.error("未找到配置的 OAuth2 提供者: {}", providerName);
throw new RuntimeException("未找到配置的 OAuth2 提供者");
}
// 构造授权 URL
StringBuilder authUrlBuilder = new StringBuilder(provider.getAuthorizeUrl());
authUrlBuilder.append("?client_id=").append(provider.getClientId());
authUrlBuilder.append("&redirect_uri=").append(provider.getRedirectUri());
authUrlBuilder.append("&response_type=code");
authUrlBuilder.append("&scope=").append(provider.getScope());
authUrlBuilder.append("&state=").append(java.util.UUID.randomUUID().toString());
String authUrl = authUrlBuilder.toString();
log.debug("OAuth2 授权 URL 构造完成: {}", authUrl);
return authUrl;
}
}
\ No newline at end of file
{"properties": [
{
"name": "app.chat-memory.implementation",
"type": "java.lang.String",
"description": "A description for 'app.chat-memory.implementation'"
},
{
"name": "app.chat-memory.caffeine.enabled",
"type": "java.lang.String",
"description": "A description for 'app.chat-memory.caffeine.enabled'"
},
{
"name": "app.chat-memory.redis.enabled",
"type": "java.lang.String",
"description": "A description for 'app.chat-memory.redis.enabled'"
}
]}
\ No newline at end of file
This diff is collapsed.
# OAuth2 快速配置指南
## 一、系统要求
- Java 17+
- Spring Boot 3.x
- MySQL 8.0+ 或 H2 数据库
- Redis(可选,用于缓存)
## 二、配置 OAuth2 提供者(以 GitHub 为例)
### 2.1 获取 OAuth2 凭证
访问 GitHub Developer Settings:
1. 登录 GitHub 账户
2. 进入 Settings → Developer settings → OAuth Apps
3. 点击 "New OAuth App"
4. 填写应用信息:
- **Application name**: HiAgent
- **Homepage URL**: http://localhost:8081
- **Authorization callback URL**: http://localhost:8081/api/v1/auth/oauth2/callback?providerName=github
5. 获取 **Client ID****Client Secret**
### 2.2 配置数据库
在数据库中插入 OAuth2 提供者配置:
```sql
INSERT INTO oauth2_provider (
id,
provider_name,
display_name,
description,
auth_type,
authorize_url,
token_url,
userinfo_url,
client_id,
client_secret,
redirect_uri,
scope,
enabled
) VALUES (
UUID(),
'github',
'GitHub 登录',
'GitHub OAuth2 认证',
'authorization_code',
'https://github.com/login/oauth/authorize',
'https://github.com/login/oauth/access_token',
'https://api.github.com/user',
'YOUR_CLIENT_ID_HERE',
'YOUR_CLIENT_SECRET_HERE',
'http://localhost:8081/api/v1/auth/oauth2/callback?providerName=github',
'user:email',
1
);
```
### 2.3 配置前端
在 Login.vue 中,OAuth2 提供者会自动显示在登录页面。
## 三、OAuth2 认证流程
### 3.1 用户点击 "GitHub 登录"
```
用户 → 前端 Login.vue
点击 GitHub 按钮
window.location.href = '/api/v1/auth/oauth2/authorize?providerName=github'
```
### 3.2 后端授权端点处理
```
GET /api/v1/auth/oauth2/authorize?providerName=github
获取 GitHub 配置
构造授权 URL(包含 client_id, redirect_uri, scope)
重定向到 GitHub 授权服务器
GitHub: https://github.com/login/oauth/authorize?client_id=xxx&redirect_uri=...
```
### 3.3 用户在 GitHub 上授权
用户在 GitHub 上验证身份并授权应用访问用户信息。
### 3.4 GitHub 回调
```
GitHub 重定向到:
http://localhost:8081/api/v1/auth/oauth2/callback?
code=AUTH_CODE&
state=STATE&
providerName=github
后端处理回调请求
```
### 3.5 后端令牌交换
```
后端收到授权码后:
1. 使用授权码向 GitHub 请求访问令牌
POST https://github.com/login/oauth/access_token
Content-Type: application/x-www-form-urlencoded
code=AUTH_CODE&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
redirect_uri=REDIRECT_URI
2. 使用访问令牌获取用户信息
GET https://api.github.com/user
Authorization: Bearer ACCESS_TOKEN
3. 查找或创建用户
├─ 检查是否存在 oauth2_account 关联
├─ 检查是否存在同邮箱的用户
└─ 否则创建新用户
4. 保存 OAuth2 账户关联
INSERT INTO oauth2_account ...
5. 生成 JWT Token
token = jwtUtil.generateToken(user.getId())
6. 重定向到前端
redirect('/login?token=JWT_TOKEN&method=oauth2')
```
### 3.6 前端处理回调
```
onMounted() {
检查 URL 参数
├─ 发现 token 和 method=oauth2
├─ 保存 token 到 localStorage
├─ 更新 authStore
└─ 重定向到 /agent/chat
}
```
## 四、配置其他 OAuth2 提供者
### 4.1 Google 登录
1. **获取凭证**
- 访问 [Google Cloud Console](https://console.cloud.google.com/)
- 创建 OAuth2 凭证
2. **配置数据库**
```sql
INSERT INTO oauth2_provider (
id, provider_name, display_name, auth_type,
authorize_url, token_url, userinfo_url,
client_id, client_secret, redirect_uri, scope, enabled
) VALUES (
UUID(), 'google', 'Google 登录', 'authorization_code',
'https://accounts.google.com/o/oauth2/v2/auth',
'https://oauth2.googleapis.com/token',
'https://www.googleapis.com/oauth2/v2/userinfo',
'YOUR_CLIENT_ID', 'YOUR_CLIENT_SECRET',
'http://localhost:8081/api/v1/auth/oauth2/callback?providerName=google',
'openid email profile', 1
);
```
### 4.2 Microsoft 登录
类似配置,使用 Microsoft Azure AD 的端点:
- authorize_url: `https://login.microsoftonline.com/common/oauth2/v2.0/authorize`
- token_url: `https://login.microsoftonline.com/common/oauth2/v2.0/token`
- userinfo_url: `https://graph.microsoft.com/v1.0/me`
## 五、测试 OAuth2 流程
### 5.1 本地测试环境
确保使用正确的回调 URL:
```
http://localhost:8081/api/v1/auth/oauth2/callback?providerName=github
```
### 5.2 测试步骤
1. 启动后端:`mvn spring-boot:run`
2. 启动前端:`npm run dev`
3. 访问 http://localhost:5173/login
4. 点击 "GitHub 登录" 按钮
5. 在 GitHub 上授权应用
6. 应自动重定向到 /agent/chat
## 六、故障排查
### 常见错误
| 错误 | 原因 | 解决方案 |
|------|------|--------|
| "未找到配置的 OAuth2 提供者" | 数据库中没有提供者配置 | 检查 oauth2_provider 表 |
| "令牌交换失败" | client_id/secret 错误或网络问题 | 检查凭证和日志 |
| "获取用户信息失败" | 访问令牌无效或 API 端点错误 | 检查 userinfo_url 和作用域 |
| "用户创建失败" | 用户名重复 | 检查 sys_user 表中的 username 唯一性 |
### 查看日志
所有错误都会记录详细的堆栈信息:
```bash
# 查看 ERROR 级别的日志
grep ERROR logs/hiagent.log
# 查看认证相关的日志
grep "OAuth2\|认证\|登录" logs/hiagent.log
```
## 七、安全建议
1. **保护 Client Secret**
- 不要在前端代码中暴露 client_secret
- 只在后端服务器上存储
2. **HTTPS**
- 生产环境必须使用 HTTPS
- 确保 redirect_uri 使用 HTTPS
3. **State 参数**
- 系统自动生成 state 参数防止 CSRF
- 用户不需要手动配置
4. **Scope 权限**
- 只请求必要的权限
- 定期审查和更新权限列表
## 八、参考资源
- [GitHub OAuth 文档](https://docs.github.com/en/developers/apps/building-oauth-apps)
- [Google OAuth 文档](https://developers.google.com/identity/protocols/oauth2)
- [OAuth2 标准](https://tools.ietf.org/html/rfc6749)
## 九、常见问题
**Q: 用户已使用 OAuth2 登录,现在想用用户名/密码登录?**
A: 系统支持同一用户使用多种认证方式。用户邮箱相同时会自动关联。
**Q: 如何撤销 OAuth2 账户关联?**
A: 手动删除 oauth2_account 表中的相应记录。
**Q: 能否强制用户只使用某种认证方式?**
A: 可以。在 AuthConfigService 中禁用不需要的认证模式。
---
**最后更新**:2024-12-15
**文档版本**:1.0
# 智能体(Agent)优化方案
## 1. 概述
本方案旨在提升系统中所有智能体的记忆功能支持能力,包括记忆存储能力和基于历史对话的多轮交互支持。通过实施以下优化措施,我们将显著改善用户体验和系统性能。
## 2. 当前状态分析
经过对现有系统的深入分析,我们发现以下问题:
1. **记忆存储能力不足**:当前系统使用内存存储对话历史,存在易失性问题,重启后数据丢失
2. **历史消息管理策略简单**:缺乏智能化的消息摘要机制,长对话可能导致性能下降
3. **RAG集成不完善**:缺少Agent级别的个性化配置,无法满足不同场景需求
4. **Agent功能单一**:各Agent工具集不够丰富,限制了其专业服务能力
## 3. 优化方案
### 3.1 增强记忆持久化机制
#### 3.1.1 实施方案
- 引入Redis作为外部存储,替代当前的内存存储方案
- 创建RedisChatMemory实现类,实现ChatMemory接口
- 修改ChatMemoryConfig配置类,使用RedisChatMemory替换InMemoryChatMemory
#### 3.1.2 预期效果
- 对话历史持久化存储,系统重启后数据不丢失
- 提升系统稳定性和可靠性
- 支持更大规模的并发用户访问
#### 3.1.3 开发步骤
1. 添加Redis依赖到pom.xml
2. 创建RedisConfig配置类,配置RedisTemplate
3. 实现RedisChatMemory类,完成对话历史的存取逻辑
4. 修改ChatMemoryConfig,注入RedisChatMemory实例
5. 编写单元测试验证功能正确性
### 3.2 优化历史消息管理策略
#### 3.2.1 实施方案
- 实现智能历史消息摘要算法(SmartHistorySummarizer)
- 在AgentChatService中集成摘要功能,在构建Prompt时自动应用
- 识别并保留关键信息,如代码片段、技术术语、数字等
#### 3.2.2 预期效果
- 有效控制Prompt长度,避免超出LLM上下文窗口限制
- 保留对话中的关键信息,维持对话连贯性
- 提升系统响应速度和处理效率
#### 3.2.3 开发步骤
1. 创建SmartHistorySummarizer类,实现智能摘要算法
2. 在AgentChatService中集成摘要功能
3. 调整Prompt构建逻辑,应用历史消息摘要
4. 测试不同场景下的摘要效果
### 3.3 完善RAG集成功能
#### 3.3.1 实施方案
- 扩展Agent模型,增加RAG相关配置字段(ragTopK, ragScoreThreshold, ragPromptTemplate)
- 修改RagService,支持使用Agent特定的RAG配置
- 为各Agent配置相应的知识库,提高专业领域问答准确性
#### 3.3.2 预期效果
- 实现Agent级别的RAG个性化配置
- 提升各Agent在专业领域的问答准确性
- 增强系统的灵活性和可扩展性
#### 3.3.3 开发步骤
1. 修改Agent模型,添加RAG配置字段
2. 更新数据库schema和初始数据
3. 修改RagService,支持Agent特定配置
4. 为各Agent创建和配置专属知识库
5. 测试RAG功能在各Agent中的表现
### 3.4 各Agent功能优化
#### 3.4.1 客服助手
- 添加订单查询工具(OrderQueryTool)
- 添加退款处理工具(RefundProcessingTool)
#### 3.4.2 技术支持
- 添加技术文档检索工具
- 添加代码解释工具
#### 3.4.3 数据分析师
- 添加图表生成工具
- 添加统计计算工具
#### 3.4.4 内容创作助手
- 添加创作风格参考工具
- 添加文档模板工具
#### 3.4.5 学习导师
- 添加学习计划制定工具
- 添加课程资料检索工具
#### 3.4.6 预期效果
- 各Agent具备更强的专业服务能力
- 提升用户满意度和问题解决率
- 增强系统的实用性和商业价值
#### 3.4.7 开发步骤
1. 为每个Agent分析所需工具类型
2. 创建相应的工具实现类
3. 更新Agent配置,关联相应工具
4. 测试各Agent的新功能
## 4. 实施计划
| 阶段 | 任务 | 预期完成时间 |
|------|------|--------------|
| 第一阶段 | 增强记忆持久化机制 | 3天 |
| 第二阶段 | 优化历史消息管理策略 | 2天 |
| 第三阶段 | 完善RAG集成功能 | 3天 |
| 第四阶段 | 各Agent功能优化 | 5天 |
## 5. 风险评估与应对措施
1. **Redis性能瓶颈**:监控Redis使用情况,必要时进行集群部署
2. **摘要算法效果不佳**:持续优化算法,引入机器学习模型
3. **RAG检索准确性不足**:优化向量索引参数,调整相似度阈值
4. **Agent工具集成问题**:建立完善的测试机制,确保工具稳定性
## 6. 验收标准
1. 所有对话历史能够持久化存储,系统重启后可恢复
2. 长对话场景下系统响应时间无明显增长
3. 各Agent在专业领域问答准确率提升30%以上
4. 用户满意度调查得分提升20%以上
\ No newline at end of file
This diff is collapsed.
-- 插入默认数据
INSERT INTO sys_user (id, username, password, email, nickname, status, role) VALUES
('default-user-id', 'admin', '$2a$10$N.zmdr9k7uOCQb0bta/OauRxaOKSr.QhqyD2R5FKvMQjmHoLkm5Sy', 'admin@hiagent.com', 'Admin', 'active', 'admin');
INSERT INTO agent (id, name, description, status, default_model, owner) VALUES
('default-agent-1', '客服助手', '处理客户咨询的AI助手', 'active', 'deepseek-chat', 'default-user-id'),
('default-agent-2', '技术支持', '提供技术支持服务的AI助手', 'active', 'gpt-3.5-turbo', 'default-user-id');
INSERT INTO tool (id, name, display_name, description, category, status, timeout, http_method) VALUES
('default-tool-1', 'search', '搜索工具', '进行网络搜索查询', 'API', 'active', 30000, 'GET'),
('default-tool-2', 'calculator', '计算器', '进行数学计算', 'FUNCTION', 'active', 5000, 'POST'),
('default-tool-3', 'weather', '天气查询', '查询天气信息', 'API', 'active', 10000, 'GET'),
('default-tool-4', 'get_current_time', '获取当前时间', '获取当前系统时间', 'FUNCTION', 'active', 1000, 'GET');
\ No newline at end of file
-- 插入默认用户数据
MERGE INTO sys_user (id, username, password, email, nickname, status, role) VALUES
('user-001', 'admin', '$2a$10$N.zmdr9k7uOCQb0bta/OauRxaOKSr.QhqyD2R5FKvMQjmHoLkm5Sy', 'admin@hiagent.com', 'Admin', 'active', 'admin');
-- 插入默认LLM配置数据
MERGE INTO llm_config (id, name, description, provider, model_name, api_key, base_url, temperature, max_tokens, top_p, enabled, owner) VALUES
('deepseek-default', 'deepseek-default', 'DeepSeek默认配置', 'deepseek', 'deepseek-chat', '', 'https://api.deepseek.com', 0.7, 4096, 0.9, true, 'user-001'),
('openai-default', 'openai-default', 'OpenAI默认配置', 'openai', 'gpt-3.5-turbo', '', 'https://api.openai.com/v1', 0.7, 4096, 0.9, false, 'user-001'),
('ollama-default', 'ollama-default', 'Ollama默认配置', 'ollama', 'llama2', '', 'http://localhost:11434', 0.7, 4096, 0.9, true, 'user-001'),
('hisense-default', 'hisense-default', 'Hisense默认配置', 'hisense', 'gpt-4-1', '', 'http://openai-proxy-v2-jt-higpt.cloudprd.hisense.com', 0.7, 4096, 0.9, true, 'user-001');
-- 插入默认Agent数据
MERGE INTO agent (id, name, description, status, default_model, owner, system_prompt, enable_re_act, history_length, enable_rag, rag_collection_id, rag_top_k, rag_score_threshold, tools) VALUES
('agent-1', '客服助手', '处理客户咨询的AI助手', 'active', 'deepseek-default', 'user-001', '你是一个专业的客服助手,请用友好和专业的态度回答客户的问题。', 1, 15, 1, 'customer-service-kb', 5, 0.8, '["search", "orderQuery", "refundProcessing"]'),
('agent-2', '技术支持', '提供技术支持服务的AI助手', 'active', 'openai-default', 'user-001', '你是一个技术专家,请帮助用户解决技术问题。', 1, 15, 1, 'technical-support-kb', 5, 0.8, '["search", "calculator", "technicalDocumentationRetrieval", "technicalCodeExplanation"]'),
('agent-3', '数据分析员', '专业的数据分析AI助手', 'active', 'deepseek-default', 'user-001', '你是一个数据分析专家,擅长处理和分析各种数据。', 0, 15, 1, 'data-analysis-kb', 5, 0.8, '["calculator", "chartGeneration", "statisticalCalculation"]'),
('agent-4', '内容创作助手', '帮助撰写各类文案的AI助手', 'active', 'hisense-default', 'user-001', '你是一个创意写作专家,能够帮助用户创作各种类型的文案。', 0, 15, 1, 'content-creation-kb', 5, 0.8, '["search", "writingStyleReference", "documentTemplate"]'),
('agent-5', '学习导师', '个性化学习指导AI助手', 'active', 'hisense-default', 'user-001', '你是一个教育专家,能够根据用户需求提供个性化的学习建议。', 1, 15, 1, 'learning-mentor-kb', 5, 0.8, '["search", "studyPlanGeneration", "courseMaterialRetrieval"]');
-- 插入默认工具数据
MERGE INTO tool (id, name, display_name, description, category, status, timeout, http_method) VALUES
('tool-1', 'search', '搜索工具', '进行网络搜索查询', 'API', 'active', 30000, 'GET'),
('tool-2', 'calculator', '计算器', '进行数学计算', 'FUNCTION', 'active', 5000, 'POST'),
('tool-3', 'weather', '天气查询', '查询天气信息', 'API', 'active', 10000, 'GET'),
('tool-4', 'get_current_time', '获取当前时间', '获取当前系统时间', 'FUNCTION', 'active', 1000, 'GET'),
('tool-5', 'technicalDocumentationRetrieval', '技术文档检索', '检索和查询技术文档内容', 'FUNCTION', 'active', 10000, 'GET'),
('tool-6', 'technicalCodeExplanation', '技术代码解释', '分析和解释技术代码的功能和实现逻辑', 'FUNCTION', 'active', 10000, 'GET'),
('tool-7', 'chartGeneration', '图表生成', '根据数据生成各种类型的图表', 'FUNCTION', 'active', 10000, 'GET'),
('tool-8', 'statisticalCalculation', '统计计算', '执行各种统计分析计算', 'FUNCTION', 'active', 10000, 'GET'),
('tool-9', 'writingStyleReference', '创作风格参考', '提供各种写作风格的参考和指导', 'FUNCTION', 'active', 10000, 'GET'),
('tool-10', 'documentTemplate', '文档模板', '提供各种类型的文档模板', 'FUNCTION', 'active', 10000, 'GET'),
('tool-11', 'studyPlanGeneration', '学习计划制定', '根据学习目标和时间安排制定个性化的学习计划', 'FUNCTION', 'active', 10000, 'GET'),
('tool-12', 'courseMaterialRetrieval', '课程资料检索', '检索和查询相关课程资料', 'FUNCTION', 'active', 10000, 'GET');
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<template>
<div id="app">
<!-- 如果是登录页,直接显示 -->
<div v-if="isLoginPage" class="login-container">
<router-view />
</div>
<!-- 其他页面使用新的布局 -->
<div v-else class="main-layout">
<!-- 顶部导航栏 -->
<TopNavbar />
<!-- 主要内容区域 -->
<div class="content-wrapper">
<router-view />
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import TopNavbar from '@/components/TopNavbar.vue'
const route = useRoute()
const isLoginPage = computed(() => {
return route.path === '/login' || route.path === '/register'
})
</script>
<style scoped>
#app {
height: 100vh;
display: flex;
flex-direction: column;
}
.login-container {
width: 100%;
height: 100%;
}
.main-layout {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.content-wrapper {
flex: 1;
overflow: hidden;
padding: 0;
background-color: var(--bg-secondary);
}
/* 响应式设计 */
@media (max-width: 768px) {
.main-layout {
height: 100vh;
}
.content-wrapper {
padding: 0;
}
}
@media (max-width: 576px) {
#app {
height: 100vh;
}
.main-layout {
height: 100vh;
}
}
</style>
\ 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.
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
\ 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.
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
\ 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.
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