From 8850a06feaaab384398afc75fedf1120dcb18d70 Mon Sep 17 00:00:00 2001
From: wangys <3401275564@qq.com>
Date: Tue, 4 Nov 2025 18:49:37 +0800
Subject: [PATCH] dify
---
.../.bin/mysql/sql/createTableAI.sql | 139 +-
.../sql/createTablePermissionControl.sql | 3 +-
.../.bin/mysql/sql/initMenuData.sql | 2 +-
schoolNewsServ/admin/pom.xml | 5 +
schoolNewsServ/ai/AI模块实现进度.md | 353 +++++
schoolNewsServ/ai/Dify知识库指定方案.md | 588 +++++++
schoolNewsServ/ai/pom.xml | 4 +
.../org/xyzh/ai/client/DifyApiClient.java | 794 ++++++++++
.../ai/client/callback/StreamCallback.java | 35 +
.../org/xyzh/ai/client/dto/ChatRequest.java | 98 ++
.../org/xyzh/ai/client/dto/ChatResponse.java | 121 ++
.../client/dto/ConversationListResponse.java | 49 +
.../ai/client/dto/DatasetCreateRequest.java | 43 +
.../ai/client/dto/DatasetCreateResponse.java | 55 +
.../ai/client/dto/DatasetDetailResponse.java | 82 +
.../ai/client/dto/DatasetListResponse.java | 54 +
.../ai/client/dto/DatasetUpdateRequest.java | 25 +
.../ai/client/dto/DocumentListResponse.java | 85 ++
.../ai/client/dto/DocumentStatusResponse.java | 95 ++
.../ai/client/dto/DocumentUploadRequest.java | 95 ++
.../ai/client/dto/DocumentUploadResponse.java | 60 +
.../ai/client/dto/MessageHistoryResponse.java | 94 ++
.../xyzh/ai/client/dto/RetrievalRequest.java | 33 +
.../xyzh/ai/client/dto/RetrievalResponse.java | 88 ++
.../java/org/xyzh/ai/config/DifyConfig.java | 175 +++
.../controller/AiAgentConfigController.java | 172 +++
.../xyzh/ai/controller/AiChatController.java | 386 +++++
.../ai/controller/AiFileUploadController.java | 158 ++
.../ai/controller/AiKnowledgeController.java | 180 +++
.../ai/controller/DifyProxyController.java | 201 +++
.../ai/exception/AiKnowledgeException.java | 19 +
.../org/xyzh/ai/exception/ChatException.java | 19 +
.../org/xyzh/ai/exception/DifyException.java | 42 +
.../ai/exception/FileProcessException.java | 19 +
.../xyzh/ai/mapper/AiAgentConfigMapper.java | 50 +-
.../xyzh/ai/mapper/AiConversationMapper.java | 104 ++
.../org/xyzh/ai/mapper/AiKnowledgeMapper.java | 97 +-
.../org/xyzh/ai/mapper/AiMessageMapper.java | 61 +
.../xyzh/ai/mapper/AiUploadFileMapper.java | 44 +
.../impl/AiAgentConfigServiceImpl.java | 431 ++++++
.../impl/AiChatHistoryServiceImpl.java | 727 +++++++++
.../ai/service/impl/AiChatServiceImpl.java | 851 +++++++++++
.../service/impl/AiKnowledgeServiceImpl.java | 565 +++++++
.../service/impl/AiUploadFileServiceImpl.java | 578 +++++++
.../main/resources/application-ai.yml.example | 68 +
.../resources/mapper/AiAgentConfigMapper.xml | 133 +-
.../resources/mapper/AiConversationMapper.xml | 202 ++-
.../resources/mapper/AiKnowledgeMapper.xml | 242 ++-
.../main/resources/mapper/AiMessageMapper.xml | 129 +-
.../resources/mapper/AiUploadFileMapper.xml | 170 ++-
.../mapper/AiUsageStatisticsMapper.xml | 9 +-
schoolNewsServ/ai/前端API接口文档.md | 392 +++++
schoolNewsServ/ai/数据同步检查报告.md | 127 ++
schoolNewsServ/ai/知识库隔离方案.md | 484 ++++++
.../api/ai/agent/AiAgentConfigService.java | 118 +-
.../org/xyzh/api/ai/chat/AiChatService.java | 131 ++
.../xyzh/api/ai/file/AiUploadFileService.java | 155 +-
.../api/ai/history/AiChatHistoryService.java | 138 ++
.../api/ai/knowledge/AiKnowledgeService.java | 156 +-
.../api/system/config/SysConfigService.java | 14 +
.../xyzh/common/core/enums/ResourceType.java | 3 +-
.../xyzh/common/dto/ai/TbAiAgentConfig.java | 39 +
.../xyzh/common/dto/ai/TbAiConversation.java | 78 +
.../org/xyzh/common/dto/ai/TbAiKnowledge.java | 99 +-
.../org/xyzh/common/dto/ai/TbAiMessage.java | 62 +
.../xyzh/common/dto/ai/TbAiUploadFile.java | 71 +-
.../common/dto/ai/TbAiUsageStatistics.java | 26 +
.../config/impl/SysConfigServiceImpl.java | 13 +
.../service/SysDepartmentService.java | 2 +-
.../impl/SysDepartmentServiceImpl.java | 4 +-
.../log}/impl/LoginLogServiceImpl.java | 2 +-
.../log}/impl/SysOperationLogServiceImpl.java | 2 +-
.../menu/service/SysMenuService.java | 2 +-
.../menu/service/impl/SysMenuServiceImpl.java | 4 +-
.../module/SysModuleService.java | 2 +-
.../module/impl/ModuleServiceImpl.java | 2 +-
.../service/SysPermissionService.java | 2 +-
.../impl/SysPermissionServiceImpl.java | 4 +-
.../SysResourcePermissionServiceImpl.java | 2 +-
.../role/service/SysRoleService.java | 2 +-
.../role/service/impl/SysRoleServiceImpl.java | 4 +-
.../user/service/SysUserService.java | 2 +-
.../user/service/impl/SysUserServiceImpl.java | 4 +-
schoolNewsWeb/src/apis/ai/agent-config.ts | 113 +-
schoolNewsWeb/src/apis/ai/chat-history.ts | 198 +++
schoolNewsWeb/src/apis/ai/chat.ts | 238 +++
schoolNewsWeb/src/apis/ai/conversation.ts | 53 -
schoolNewsWeb/src/apis/ai/document-segment.ts | 168 ++
schoolNewsWeb/src/apis/ai/file-upload.ts | 109 +-
schoolNewsWeb/src/apis/ai/index.ts | 15 +-
schoolNewsWeb/src/apis/ai/knowledge.ts | 136 +-
schoolNewsWeb/src/apis/ai/message.ts | 73 -
schoolNewsWeb/src/assets/imgs/assisstent.svg | 10 +
schoolNewsWeb/src/assets/imgs/chat-ball.svg | 5 +
schoolNewsWeb/src/assets/imgs/link.svg | 4 +
schoolNewsWeb/src/assets/imgs/send.svg | 5 +
.../src/assets/imgs/trashbin-grey.svg | 4 +
schoolNewsWeb/src/types/ai/index.ts | 387 ++++-
.../views/admin/manage/ai/AIConfigView.vue | 487 ++++--
.../manage/ai/KnowledgeManagementView.vue | 1033 ++++++++++++-
.../ai/components/DocumentSegmentDialog.vue | 742 +++++++++
schoolNewsWeb/src/views/public/ai/AIAgent.vue | 1349 +++++++++++++++++
schoolNewsWeb/src/views/public/ai/index.ts | 1 +
103 files changed, 15337 insertions(+), 771 deletions(-)
create mode 100644 schoolNewsServ/ai/AI模块实现进度.md
create mode 100644 schoolNewsServ/ai/Dify知识库指定方案.md
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/callback/StreamCallback.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/ChatRequest.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/ChatResponse.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/ConversationListResponse.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetCreateRequest.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetCreateResponse.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetDetailResponse.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetListResponse.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetUpdateRequest.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentListResponse.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentStatusResponse.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentUploadRequest.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentUploadResponse.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/MessageHistoryResponse.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/RetrievalRequest.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/RetrievalResponse.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/config/DifyConfig.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/AiAgentConfigController.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/AiChatController.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/AiFileUploadController.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/AiKnowledgeController.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/controller/DifyProxyController.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/exception/AiKnowledgeException.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/exception/ChatException.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/exception/DifyException.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/exception/FileProcessException.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiAgentConfigServiceImpl.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiChatHistoryServiceImpl.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiChatServiceImpl.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiKnowledgeServiceImpl.java
create mode 100644 schoolNewsServ/ai/src/main/java/org/xyzh/ai/service/impl/AiUploadFileServiceImpl.java
create mode 100644 schoolNewsServ/ai/src/main/resources/application-ai.yml.example
create mode 100644 schoolNewsServ/ai/前端API接口文档.md
create mode 100644 schoolNewsServ/ai/数据同步检查报告.md
create mode 100644 schoolNewsServ/ai/知识库隔离方案.md
create mode 100644 schoolNewsServ/api/api-ai/src/main/java/org/xyzh/api/ai/chat/AiChatService.java
create mode 100644 schoolNewsServ/api/api-ai/src/main/java/org/xyzh/api/ai/history/AiChatHistoryService.java
create mode 100644 schoolNewsServ/api/api-system/src/main/java/org/xyzh/api/system/config/SysConfigService.java
create mode 100644 schoolNewsServ/system/src/main/java/org/xyzh/system/service/config/impl/SysConfigServiceImpl.java
rename schoolNewsServ/system/src/main/java/org/xyzh/system/{ => service}/department/service/SysDepartmentService.java (85%)
rename schoolNewsServ/system/src/main/java/org/xyzh/system/{ => service}/department/service/impl/SysDepartmentServiceImpl.java (99%)
rename schoolNewsServ/system/src/main/java/org/xyzh/system/{log/service => service/log}/impl/LoginLogServiceImpl.java (98%)
rename schoolNewsServ/system/src/main/java/org/xyzh/system/{log/service => service/log}/impl/SysOperationLogServiceImpl.java (98%)
rename schoolNewsServ/system/src/main/java/org/xyzh/system/{ => service}/menu/service/SysMenuService.java (85%)
rename schoolNewsServ/system/src/main/java/org/xyzh/system/{ => service}/menu/service/impl/SysMenuServiceImpl.java (99%)
rename schoolNewsServ/system/src/main/java/org/xyzh/system/{ => service}/module/SysModuleService.java (86%)
rename schoolNewsServ/system/src/main/java/org/xyzh/system/{ => service}/module/impl/ModuleServiceImpl.java (99%)
rename schoolNewsServ/system/src/main/java/org/xyzh/system/{ => service}/permission/service/SysPermissionService.java (89%)
rename schoolNewsServ/system/src/main/java/org/xyzh/system/{ => service}/permission/service/impl/SysPermissionServiceImpl.java (99%)
rename schoolNewsServ/system/src/main/java/org/xyzh/system/{ => service}/permission/service/impl/SysResourcePermissionServiceImpl.java (99%)
rename schoolNewsServ/system/src/main/java/org/xyzh/system/{ => service}/role/service/SysRoleService.java (85%)
rename schoolNewsServ/system/src/main/java/org/xyzh/system/{ => service}/role/service/impl/SysRoleServiceImpl.java (99%)
rename schoolNewsServ/system/src/main/java/org/xyzh/system/{ => service}/user/service/SysUserService.java (85%)
rename schoolNewsServ/system/src/main/java/org/xyzh/system/{ => service}/user/service/impl/SysUserServiceImpl.java (99%)
create mode 100644 schoolNewsWeb/src/apis/ai/chat-history.ts
create mode 100644 schoolNewsWeb/src/apis/ai/chat.ts
delete mode 100644 schoolNewsWeb/src/apis/ai/conversation.ts
create mode 100644 schoolNewsWeb/src/apis/ai/document-segment.ts
delete mode 100644 schoolNewsWeb/src/apis/ai/message.ts
create mode 100644 schoolNewsWeb/src/assets/imgs/assisstent.svg
create mode 100644 schoolNewsWeb/src/assets/imgs/chat-ball.svg
create mode 100644 schoolNewsWeb/src/assets/imgs/link.svg
create mode 100644 schoolNewsWeb/src/assets/imgs/send.svg
create mode 100644 schoolNewsWeb/src/assets/imgs/trashbin-grey.svg
create mode 100644 schoolNewsWeb/src/views/admin/manage/ai/components/DocumentSegmentDialog.vue
create mode 100644 schoolNewsWeb/src/views/public/ai/AIAgent.vue
create mode 100644 schoolNewsWeb/src/views/public/ai/index.ts
diff --git a/schoolNewsServ/.bin/mysql/sql/createTableAI.sql b/schoolNewsServ/.bin/mysql/sql/createTableAI.sql
index c400e92..8457ba7 100644
--- a/schoolNewsServ/.bin/mysql/sql/createTableAI.sql
+++ b/schoolNewsServ/.bin/mysql/sql/createTableAI.sql
@@ -5,12 +5,15 @@ CREATE TABLE `tb_ai_agent_config` (
`id` VARCHAR(50) NOT NULL COMMENT '配置ID',
`name` VARCHAR(100) NOT NULL COMMENT '智能体名称',
`avatar` VARCHAR(255) DEFAULT NULL COMMENT '智能体头像',
+ `description` VARCHAR(500) DEFAULT NULL COMMENT '智能体描述',
`system_prompt` TEXT COMMENT '系统提示词',
`model_name` VARCHAR(100) DEFAULT NULL COMMENT '模型名称',
`model_provider` VARCHAR(50) DEFAULT NULL COMMENT '模型提供商',
`temperature` DECIMAL(3,2) DEFAULT 0.70 COMMENT '温度值',
`max_tokens` INT(11) DEFAULT 2000 COMMENT '最大tokens',
`top_p` DECIMAL(3,2) DEFAULT 1.00 COMMENT 'Top P值',
+ `dify_app_id` VARCHAR(100) DEFAULT NULL COMMENT 'Dify应用ID',
+ `dify_api_key` VARCHAR(255) DEFAULT NULL COMMENT 'Dify应用API密钥',
`status` INT(4) DEFAULT 1 COMMENT '状态(0禁用 1启用)',
`creator` VARCHAR(50) DEFAULT NULL COMMENT '创建者',
`updater` VARCHAR(50) DEFAULT NULL COMMENT '更新者',
@@ -18,53 +21,76 @@ CREATE TABLE `tb_ai_agent_config` (
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` TIMESTAMP NULL DEFAULT NULL COMMENT '删除时间',
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
- PRIMARY KEY (`id`)
+ PRIMARY KEY (`id`),
+ KEY `idx_status` (`status`, `deleted`),
+ KEY `idx_dify_app` (`dify_app_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='智能体配置表';
--- 知识库表
+-- 知识库表(支持资源权限控制)
DROP TABLE IF EXISTS `tb_ai_knowledge`;
CREATE TABLE `tb_ai_knowledge` (
- `id` VARCHAR(50) NOT NULL COMMENT '知识ID',
- `title` VARCHAR(255) NOT NULL COMMENT '知识标题',
- `content` LONGTEXT NOT NULL COMMENT '知识内容',
+ `id` VARCHAR(50) NOT NULL COMMENT '知识库ID',
+ `title` VARCHAR(255) NOT NULL COMMENT '知识库标题',
+ `description` VARCHAR(500) DEFAULT NULL COMMENT '知识库描述',
+ `content` LONGTEXT COMMENT '知识内容(手动添加时使用)',
`source_type` INT(4) DEFAULT 1 COMMENT '来源类型(1手动添加 2文件导入 3资源同步)',
`source_id` VARCHAR(50) DEFAULT NULL COMMENT '来源ID',
`file_name` VARCHAR(255) DEFAULT NULL COMMENT '文件名',
`file_path` VARCHAR(500) DEFAULT NULL COMMENT '文件路径',
`category` VARCHAR(100) DEFAULT NULL COMMENT '分类',
`tags` VARCHAR(500) DEFAULT NULL COMMENT '标签(JSON数组)',
+ `dify_dataset_id` VARCHAR(100) DEFAULT NULL COMMENT 'Dify知识库ID(Dataset ID)',
+ `dify_indexing_technique` VARCHAR(50) DEFAULT 'high_quality' COMMENT 'Dify索引方式(high_quality/economy)',
+ `embedding_model` VARCHAR(100) DEFAULT NULL COMMENT '向量模型名称',
`vector_id` VARCHAR(100) DEFAULT NULL COMMENT '向量ID(用于向量检索)',
- `status` INT(4) DEFAULT 1 COMMENT '状态(0禁用 1启用)',
- `creator` VARCHAR(50) DEFAULT NULL COMMENT '创建者',
+ `document_count` INT(11) DEFAULT 0 COMMENT '文档数量',
+ `total_chunks` INT(11) DEFAULT 0 COMMENT '总分段数',
+ `status` INT(4) DEFAULT 1 COMMENT '状态(0禁用 1启用 2处理中)',
+ `creator` VARCHAR(50) NOT NULL COMMENT '创建者(用户ID)',
+ `creator_dept` VARCHAR(50) DEFAULT NULL COMMENT '创建者部门ID',
`updater` VARCHAR(50) DEFAULT NULL COMMENT '更新者',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` TIMESTAMP NULL DEFAULT NULL COMMENT '删除时间',
`deleted` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`id`),
+ KEY `idx_creator` (`creator`, `deleted`),
+ KEY `idx_creator_dept` (`creator_dept`),
KEY `idx_source` (`source_type`, `source_id`),
KEY `idx_category` (`category`),
- KEY `idx_status` (`status`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='知识库表';
+ KEY `idx_status` (`status`, `deleted`),
+ KEY `idx_dify_dataset` (`dify_dataset_id`),
+ KEY `idx_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='知识库表(resource_type=10,支持权限控制)';
-- 对话会话表
DROP TABLE IF EXISTS `tb_ai_conversation`;
CREATE TABLE `tb_ai_conversation` (
`id` VARCHAR(50) NOT NULL COMMENT '会话ID',
`user_id` VARCHAR(50) NOT NULL COMMENT '用户ID',
- `title` VARCHAR(255) DEFAULT NULL COMMENT '会话标题',
+ `agent_id` VARCHAR(50) DEFAULT NULL COMMENT '智能体ID',
+ `title` VARCHAR(255) DEFAULT '新对话' COMMENT '会话标题',
+ `summary` VARCHAR(500) DEFAULT NULL COMMENT '对话摘要(AI自动生成)',
+ `dify_conversation_id` VARCHAR(100) DEFAULT NULL COMMENT 'Dify会话ID',
`status` INT(4) DEFAULT 1 COMMENT '状态(0已结束 1进行中)',
+ `is_favorite` TINYINT(1) DEFAULT 0 COMMENT '是否收藏(0否 1是)',
+ `is_pinned` TINYINT(1) DEFAULT 0 COMMENT '是否置顶(0否 1是)',
`message_count` INT(11) DEFAULT 0 COMMENT '消息数量',
+ `total_tokens` INT(11) DEFAULT 0 COMMENT '总Token消耗',
`last_message_time` TIMESTAMP NULL DEFAULT NULL COMMENT '最后消息时间',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
- KEY `idx_user` (`user_id`),
+ KEY `idx_user_createtime` (`user_id`, `create_time` DESC),
+ KEY `idx_user_favorite` (`user_id`, `is_favorite`),
+ KEY `idx_user_pinned` (`user_id`, `is_pinned`),
+ KEY `idx_agent` (`agent_id`),
+ KEY `idx_dify_conversation` (`dify_conversation_id`),
KEY `idx_last_message_time` (`last_message_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='对话会话表';
--- 对话记录表
+-- 对话消息表
DROP TABLE IF EXISTS `tb_ai_message`;
CREATE TABLE `tb_ai_message` (
`id` VARCHAR(50) NOT NULL COMMENT '消息ID',
@@ -74,32 +100,44 @@ CREATE TABLE `tb_ai_message` (
`content` LONGTEXT NOT NULL COMMENT '消息内容',
`file_ids` VARCHAR(500) DEFAULT NULL COMMENT '关联文件ID(JSON数组)',
`knowledge_ids` VARCHAR(500) DEFAULT NULL COMMENT '引用知识ID(JSON数组)',
+ `knowledge_refs` TEXT DEFAULT NULL COMMENT '知识库引用详情(JSON数组,包含title/snippet/score)',
`token_count` INT(11) DEFAULT 0 COMMENT 'Token数量',
+ `dify_message_id` VARCHAR(100) DEFAULT NULL COMMENT 'Dify消息ID',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
- KEY `idx_conversation` (`conversation_id`),
+ KEY `idx_conversation_createtime` (`conversation_id`, `create_time` ASC),
KEY `idx_user` (`user_id`),
- KEY `idx_create_time` (`create_time`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='对话消息表';
+ KEY `idx_role` (`role`),
+ KEY `idx_create_time` (`create_time`),
+ FULLTEXT KEY `ft_content` (`content`) WITH PARSER ngram
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='对话消息表(支持全文检索)';
-- 上传文件表
DROP TABLE IF EXISTS `tb_ai_upload_file`;
CREATE TABLE `tb_ai_upload_file` (
`id` VARCHAR(50) NOT NULL COMMENT '文件ID',
`user_id` VARCHAR(50) NOT NULL COMMENT '用户ID',
- `conversation_id` VARCHAR(50) DEFAULT NULL COMMENT '会话ID',
+ `knowledge_id` VARCHAR(50) DEFAULT NULL COMMENT '所属知识库ID',
+ `conversation_id` VARCHAR(50) DEFAULT NULL COMMENT '关联会话ID(对话中上传)',
`file_name` VARCHAR(255) NOT NULL COMMENT '文件名',
`file_path` VARCHAR(500) NOT NULL COMMENT '文件路径',
`file_size` BIGINT(20) DEFAULT 0 COMMENT '文件大小(字节)',
- `file_type` VARCHAR(50) DEFAULT NULL COMMENT '文件类型',
+ `file_type` VARCHAR(50) DEFAULT NULL COMMENT '文件类型(pdf/txt/docx/md等)',
`mime_type` VARCHAR(100) DEFAULT NULL COMMENT 'MIME类型',
`extracted_text` LONGTEXT COMMENT '提取的文本内容',
- `status` INT(4) DEFAULT 1 COMMENT '状态(0处理中 1已完成 2失败)',
+ `dify_document_id` VARCHAR(100) DEFAULT NULL COMMENT 'Dify文档ID',
+ `dify_batch_id` VARCHAR(100) DEFAULT NULL COMMENT 'Dify批次ID',
+ `chunk_count` INT(11) DEFAULT 0 COMMENT '分段数量',
+ `status` INT(4) DEFAULT 0 COMMENT '状态(0上传中 1处理中 2已完成 3失败)',
+ `error_message` VARCHAR(500) DEFAULT NULL COMMENT '错误信息',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间',
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_user` (`user_id`),
+ KEY `idx_knowledge` (`knowledge_id`),
KEY `idx_conversation` (`conversation_id`),
+ KEY `idx_dify_document` (`dify_document_id`),
+ KEY `idx_status` (`status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='上传文件表';
@@ -108,16 +146,79 @@ DROP TABLE IF EXISTS `tb_ai_usage_statistics`;
CREATE TABLE `tb_ai_usage_statistics` (
`id` VARCHAR(50) NOT NULL COMMENT '统计ID',
`user_id` VARCHAR(50) NOT NULL COMMENT '用户ID',
+ `agent_id` VARCHAR(50) DEFAULT NULL COMMENT '智能体ID(为空表示全局统计)',
`stat_date` DATE NOT NULL COMMENT '统计日期',
`conversation_count` INT(11) DEFAULT 0 COMMENT '会话数量',
`message_count` INT(11) DEFAULT 0 COMMENT '消息数量',
`total_tokens` INT(11) DEFAULT 0 COMMENT '总Token数',
`file_count` INT(11) DEFAULT 0 COMMENT '上传文件数',
+ `knowledge_query_count` INT(11) DEFAULT 0 COMMENT '知识库查询次数',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
- UNIQUE KEY `uk_user_date` (`user_id`, `stat_date`),
+ UNIQUE KEY `uk_user_agent_date` (`user_id`, `agent_id`, `stat_date`),
KEY `idx_user` (`user_id`),
+ KEY `idx_agent` (`agent_id`),
KEY `idx_date` (`stat_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='AI使用统计表';
+
+-- ========================================
+-- 知识库权限控制说明
+-- ========================================
+-- 知识库使用统一权限表 tb_resource_permission 进行权限控制
+-- resource_type = 10 (AI_KNOWLEDGE)
+--
+-- 权限逻辑:
+-- 1. 用户创建知识库时,自动创建权限记录:
+-- - 为创建者所在部门创建全权限(can_read=1, can_write=1, can_execute=1)
+-- - 为root_department的superadmin创建全权限
+--
+-- 2. 权限查询示例(Java Service层):
+-- SELECT k.* FROM tb_ai_knowledge k
+-- INNER JOIN tb_resource_permission rp
+-- ON rp.resource_type = 10
+-- AND rp.resource_id = k.id
+-- AND rp.deleted = 0
+-- WHERE k.deleted = 0
+-- AND (
+-- (rp.dept_id IS NULL AND rp.role_id IS NULL) -- 公开
+-- OR (rp.dept_id = '用户部门' AND rp.role_id IS NULL)
+-- OR (rp.dept_id IS NULL AND rp.role_id = '用户角色')
+-- OR (rp.dept_id = '用户部门' AND rp.role_id = '用户角色')
+-- )
+-- AND rp.can_read = 1;
+--
+-- 3. 知识库分类权限示例:
+-- - 公开知识库:dept_id=NULL, role_id=NULL(所有人可读)
+-- - 部门知识库:dept_id='xxx', role_id=NULL(部门内可读写)
+-- - 角色知识库:dept_id=NULL, role_id='teacher'(所有教师可读)
+-- - 私有知识库:仅创建者部门+特定角色可访问
+--
+-- ========================================
+-- 初始化示例数据
+-- ========================================
+
+-- 插入默认智能体配置
+INSERT INTO `tb_ai_agent_config`
+ (`id`, `name`, `avatar`, `description`, `system_prompt`, `model_name`, `model_provider`, `status`, `creator`, `create_time`)
+VALUES
+ ('agent_default_001', '校园助手', '/img/agent/default.png', '我是您的智能校园助手,可以帮助您解答校园相关问题',
+ '你是一个友好、专业的校园助手。你需要基于校园知识库回答用户问题,语气亲切自然。如果知识库中没有相关信息,请诚实告知用户。',
+ 'gpt-3.5-turbo', 'openai', 1, '1', NOW());
+
+-- 插入示例知识库(需要配合权限表使用)
+INSERT INTO `tb_ai_knowledge`
+ (`id`, `title`, `description`, `category`, `status`, `creator`, `creator_dept`, `create_time`)
+VALUES
+ ('knowledge_demo_001', '校园规章制度', '学校各项规章制度汇总', '规章制度', 1, '1', 'root_department', NOW()),
+ ('knowledge_demo_002', '新生入学指南', '新生入学相关事项说明', '入学指导', 1, '1', 'root_department', NOW());
+
+-- 为示例知识库创建权限(公开可读)
+-- 注意:实际使用时应该在应用层通过权限服务自动创建
+-- INSERT INTO `tb_resource_permission`
+-- (`id`, `resource_type`, `resource_id`, `dept_id`, `role_id`, `can_read`, `can_write`, `can_execute`, `creator`, `create_time`)
+-- VALUES
+-- ('perm_ai_kb_001', 10, 'knowledge_demo_001', NULL, NULL, 1, 0, 0, '1', NOW()),
+-- ('perm_ai_kb_002', 10, 'knowledge_demo_002', NULL, NULL, 1, 0, 0, '1', NOW());
+
diff --git a/schoolNewsServ/.bin/mysql/sql/createTablePermissionControl.sql b/schoolNewsServ/.bin/mysql/sql/createTablePermissionControl.sql
index 506125e..9184c9f 100644
--- a/schoolNewsServ/.bin/mysql/sql/createTablePermissionControl.sql
+++ b/schoolNewsServ/.bin/mysql/sql/createTablePermissionControl.sql
@@ -3,7 +3,7 @@ use school_news;
DROP TABLE IF EXISTS `tb_resource_permission`;
CREATE TABLE `tb_resource_permission` (
`id` VARCHAR(50) NOT NULL COMMENT '权限ID',
- `resource_type` INT(4) NOT NULL COMMENT '资源类型(1新闻 2课程 3学习任务 4部门 5角色 6成就 7定时任务 8轮播图 9标签)',
+ `resource_type` INT(4) NOT NULL COMMENT '资源类型(1新闻 2课程 3学习任务 4部门 5角色 6成就 7定时任务 8轮播图 9标签 10AI知识库)',
`resource_id` VARCHAR(50) NOT NULL COMMENT '资源ID',
`dept_id` VARCHAR(50) DEFAULT NULL COMMENT '部门ID(NULL表示不限制部门)',
`role_id` VARCHAR(50) DEFAULT NULL COMMENT '角色ID(NULL表示不限制角色)',
@@ -35,6 +35,7 @@ CREATE TABLE `tb_resource_permission` (
-- 7 - CRONTAB_TASK (定时任务)
-- 8 - BANNER (轮播图)
-- 9 - TAG (标签)
+-- 10 - AI_KNOWLEDGE (AI知识库)
-- 注意:这些值必须与 common-core/enums/ResourceType.java 中的枚举定义完全一致
--
-- 2. dept_id 和 role_id 组合使用:
diff --git a/schoolNewsServ/.bin/mysql/sql/initMenuData.sql b/schoolNewsServ/.bin/mysql/sql/initMenuData.sql
index 3c9c797..12c4e63 100644
--- a/schoolNewsServ/.bin/mysql/sql/initMenuData.sql
+++ b/schoolNewsServ/.bin/mysql/sql/initMenuData.sql
@@ -2,7 +2,7 @@ use school_news;
-- 插入默认超级管理员用户(必须最先创建,因为后续数据的creator都需要引用此用户)
INSERT INTO `tb_sys_user` (id, username, password, email, status, create_time) VALUES
-('1', 'superadmin', '$2a$10$/Bo2SXboVUpYfR6EA.y8puYQaMGBcuNYFY/EkQRY3w27IH56EuEcS', 'superadmin@example.com', 1, now());
+('1', 'superadmin', '$2a$10$/Bo2SXboVUpYfR6EA.y8puYQaMGBcuNYFY/EkQRY3w27IH56EuEcS', 'superadmin@example.com', 0, now());
-- 插入默认用户信息数据
INSERT INTO `tb_sys_user_info` (id, user_id, full_name, avatar, create_time) VALUES
diff --git a/schoolNewsServ/admin/pom.xml b/schoolNewsServ/admin/pom.xml
index 06e9f77..429dc74 100644
--- a/schoolNewsServ/admin/pom.xml
+++ b/schoolNewsServ/admin/pom.xml
@@ -34,6 +34,11 @@
api-all
${school-news.version}
+
+ org.xyzh
+ ai
+ ${school-news.version}
+
org.xyzh
system
diff --git a/schoolNewsServ/ai/AI模块实现进度.md b/schoolNewsServ/ai/AI模块实现进度.md
new file mode 100644
index 0000000..591b94d
--- /dev/null
+++ b/schoolNewsServ/ai/AI模块实现进度.md
@@ -0,0 +1,353 @@
+# AI模块实现进度
+
+## ✅ 已完成部分 (26/26) - 完成度: 100%(核心功能)
+
+### 1. ✅ 数据库表结构设计与创建
+- 创建了6张AI相关表(智能体、知识库、对话、消息、文件、统计)
+- 集成Dify字段(dify_app_id、dify_dataset_id等)
+- 添加权限控制字段(creator_dept等)
+- 添加示例数据和详细注释
+
+### 2. ✅ 更新common-dto实体类
+- TbAiAgentConfig:添加description、difyAppId、difyApiKey
+- TbAiKnowledge:添加完整Dify集成字段和权限字段
+- TbAiConversation:添加agentID、summary、difyConversationId等
+- TbAiMessage:添加knowledgeRefs、difyMessageId
+- TbAiUploadFile:添加knowledgeId、difyDocumentId等
+- TbAiUsageStatistics:添加agentID、knowledgeQueryCount
+
+### 3. ✅ 更新Mapper XML文件
+- 所有6个Mapper XML已更新字段映射
+- **知识库Mapper添加权限过滤**:
+ - `selectAiKnowledges`:带权限的列表查询
+ - `selectByIdWithPermission`:带权限的单条查询
+ - `checkKnowledgePermission`:权限检查方法
+ - 使用`UserDeptRoleVO`和dept_path支持部门继承
+
+### 4. ✅ 配置文件管理
+- **DifyConfig.java**:完整的配置类
+ - API配置(baseUrl、apiKey、timeout)
+ - 上传配置(文件类型、大小限制)
+ - 知识库配置(索引方式、Embedding模型)
+ - 对话配置(温度、Token、流式)
+- **application-ai.yml.example**:配置示例文件
+
+### 5. ✅ 异常处理
+- DifyException:Dify API调用异常
+- AiKnowledgeException:知识库异常
+- FileProcessException:文件处理异常
+- ChatException:对话异常
+
+### 6. ✅ Dify API Client
+- **DifyApiClient.java**:完整的Dify API封装
+ - HTTP Client封装(OkHttp,支持普通和流式请求)
+ - 知识库管理API(创建、查询、删除)
+ - 文档管理API(上传、查询状态、删除)
+ - 知识库检索API(用于RAG)
+ - 对话API(流式SSE、阻塞式、停止生成)
+ - 对话历史API(消息历史、对话列表)
+- **完整的DTO体系**(15个类):
+ - 知识库相关:DatasetCreateRequest/Response、DatasetListResponse、DatasetDetailResponse
+ - 文档相关:DocumentUploadRequest/Response、DocumentStatusResponse、DocumentListResponse
+ - 检索相关:RetrievalRequest/Response
+ - 对话相关:ChatRequest/Response、MessageHistoryResponse、ConversationListResponse
+- **StreamCallback接口**:流式响应回调
+
+### 7. ✅ 实现Service层 - 智能体管理服务
+- **AiAgentConfigService接口**:定义10个核心方法
+ - createAgent():创建智能体(带参数验证、默认值设置)
+ - updateAgent():更新智能体(动态更新非null字段)
+ - deleteAgent():删除智能体(逻辑删除)
+ - getAgentById():根据ID查询
+ - listEnabledAgents():查询启用的智能体
+ - listAgents():查询智能体列表(支持过滤)
+ - pageAgents():分页查询
+ - updateAgentStatus():更新状态
+ - updateDifyConfig():更新Dify配置
+ - checkNameExists():检查名称是否存在
+- **AiAgentConfigServiceImpl实现类**:
+ - 完整的CRUD操作
+ - 使用LoginUtil获取当前用户
+ - 参数验证和业务逻辑
+ - 异常处理和日志记录
+- **AiAgentConfigMapper**:按BannerMapper规范命名
+ - insertAgentConfig()、updateAgentConfig()
+ - deleteAgentConfig()、selectAgentConfigById()
+ - selectAgentConfigs()、selectAgentConfigsPage()
+ - countAgentConfigs()、countAgentConfigByName()
+
+### 8. ✅ 实现Service层 - 知识库管理服务
+- **AiKnowledgeService接口**:定义10个核心方法
+ - createKnowledge():创建知识库(同步到Dify + 权限创建)
+ - updateKnowledge():更新知识库(带权限检查)
+ - deleteKnowledge():删除知识库(同时删除Dify)
+ - getKnowledgeById():根据ID查询(带权限校验)
+ - listKnowledges():查询知识库列表(权限过滤)
+ - pageKnowledges():分页查询(权限过滤)
+ - syncFromDify():同步Dify知识库信息
+ - updateKnowledgePermission():更新知识库权限
+ - checkKnowledgePermission():检查权限
+ - getKnowledgeStats():查询统计信息
+- **AiKnowledgeServiceImpl实现类**:
+ - Dify集成:创建知识库、删除知识库、同步信息
+ - 权限控制:集成ResourcePermissionService
+ - 事务管理:@Transactional保证数据一致性
+ - 异常处理:Dify异常和本地异常分离
+- **AiKnowledgeMapper**:完整的CRUD + 权限查询
+ - insertKnowledge()、updateKnowledge()、deleteKnowledge()
+ - selectKnowledgeById()(不带权限)
+ - selectAiKnowledges()(带权限过滤)
+ - selectByIdWithPermission()(带权限单条查询)
+ - checkKnowledgePermission()(权限检查)
+ - selectKnowledgesPage()、countKnowledges()
+
+### 9. ✅ 实现Service层 - 文件上传服务
+- **AiUploadFileServiceImpl实现类**:
+ - 文件上传到本地和Dify
+ - 文件状态管理和查询
+ - 支持批量上传
+ - 异步向量化处理
+
+### 10. ✅ 实现Service层 - 对话服务
+- **AiChatServiceImpl实现类**:
+ - 流式对话(SSE)
+ - 阻塞式对话
+ - 创建/获取/更新/删除会话
+ - 消息历史查询
+ - 停止对话生成
+ - 重新生成回答
+ - 消息评价
+
+### 11. ✅ 实现Service层 - 对话历史服务
+- **AiChatHistoryServiceImpl实现类**:
+ - 对话历史分页查询
+ - 对话搜索(全文搜索)
+ - 收藏/置顶对话
+ - 批量删除对话
+ - 对话统计信息
+ - 导出对话(Markdown/JSON)
+ - 清理过期对话
+
+### 12. ✅ Controller层 - Dify代理控制器
+- **DifyProxyController**:文档分段管理代理接口
+ - GET /datasets/{datasetId}/documents/{documentId}/segments:获取文档分段
+ - GET /datasets/{datasetId}/documents/{documentId}/segments/{segmentId}/child_chunks:获取子块
+ - PATCH /datasets/{datasetId}/documents/{documentId}/segments/{segmentId}/child_chunks/{childChunkId}:更新子块
+ - POST /datasets/{datasetId}/documents/{documentId}/segments/{segmentId}/child_chunks:创建子块
+ - DELETE /datasets/{datasetId}/documents/{documentId}/segments/{segmentId}/child_chunks/{childChunkId}:删除子块
+
+### 15. ✅ Controller层 - 核心业务Controller
+- **AiAgentConfigController**:智能体管理接口
+ - POST /ai/agent:创建智能体
+ - PUT /ai/agent:更新智能体
+ - DELETE /ai/agent/{id}:删除智能体
+ - GET /ai/agent/{id}:获取智能体
+ - GET /ai/agent:查询智能体列表
+ - POST /ai/agent/page:分页查询
+ - PUT /ai/agent/{id}/status:更新状态
+ - PUT /ai/agent/{id}/dify:更新Dify配置
+ - GET /ai/agent/check-name:检查名称是否存在
+
+- **AiKnowledgeController**:知识库管理接口
+ - POST /ai/knowledge:创建知识库
+ - PUT /ai/knowledge:更新知识库
+ - DELETE /ai/knowledge/{id}:删除知识库
+ - GET /ai/knowledge/{id}:获取知识库
+ - GET /ai/knowledge:查询知识库列表
+ - POST /ai/knowledge/page:分页查询
+ - POST /ai/knowledge/{id}/sync:同步Dify信息
+ - GET /ai/knowledge/{id}/permission:检查权限
+ - GET /ai/knowledge/{id}/stats:获取统计信息
+
+- **AiFileUploadController**:文件上传接口
+ - POST /ai/file/upload:上传文件
+ - POST /ai/file/upload/batch:批量上传
+ - GET /ai/file/{fileId}:获取文件状态
+ - GET /ai/file/list:查询文件列表
+ - POST /ai/file/page:分页查询
+ - DELETE /ai/file/{fileId}:删除文件
+ - POST /ai/file/{fileId}/reindex:重新索引
+ - GET /ai/file/{fileId}/progress:查询处理进度
+
+- **AiChatController**:对话管理接口
+ - GET /ai/chat/stream:流式对话(SSE)
+ - POST /ai/chat/blocking:阻塞式对话
+ - POST /ai/chat/stop/{messageId}:停止对话
+ - POST /ai/chat/regenerate/{messageId}:重新生成
+ - POST /ai/chat/conversation:创建会话
+ - GET /ai/chat/conversation/{id}:获取会话
+ - PUT /ai/chat/conversation:更新会话
+ - DELETE /ai/chat/conversation/{id}:删除会话
+ - GET /ai/chat/conversations:获取会话列表
+ - GET /ai/chat/conversation/{id}/messages:获取消息列表
+ - POST /ai/chat/message/{id}/rate:评价消息
+ - POST /ai/chat/conversation/{id}/summary:生成摘要
+ - POST /ai/chat/history/conversations/page:分页查询历史
+ - POST /ai/chat/history/search:搜索会话
+ - PUT /ai/chat/history/conversation/{id}/favorite:收藏/取消收藏
+ - PUT /ai/chat/history/conversation/{id}/pin:置顶/取消置顶
+ - GET /ai/chat/history/export/markdown/{id}:导出Markdown
+ - GET /ai/chat/history/export/json/{id}:导出JSON
+ - GET /ai/chat/history/recent:获取最近对话
+
+### 13. ✅ 前端API对接
+- **agent-config.ts**:智能体配置API(CRUD、列表查询、状态更新)
+- **knowledge.ts**:知识库API(CRUD、权限检查、统计)
+- **file-upload.ts**:文件上传API(上传、查询状态)
+- **chat.ts**:对话API(流式/阻塞对话、会话管理、消息评价)
+- **chat-history.ts**:对话历史API(分页查询、搜索、收藏、导出)
+- **document-segment.ts**:文档分段API(查询、更新、创建、删除分段)
+
+### 14. ✅ 前端组件实现
+- **AIConfigView.vue**:智能体配置管理页面
+- **KnowledgeManagementView.vue**:知识库管理页面(卡片式布局、三步骤上传)
+- **DocumentSegmentDialog.vue**:文档分段管理对话框(查看、编辑、删除、添加分段)
+- **AIAgent.vue**:AI助手组件(悬浮球拖动、对话界面、流式对话)
+
+---
+
+## 📋 待实现部分 (0/26) - 核心功能已全部完成!
+
+### 可选优化项(不计入核心功能)
+- [ ] AI使用统计服务实现(已有接口定义,需要时再实现)
+- [ ] 性能优化与压力测试(后续根据实际需求优化)
+
+---
+
+## 📊 完成度统计
+
+| 类别 | 已完成 | 待完成 | 合计 |
+|------|--------|--------|------|
+| 基础设施 | 6 | 0 | 6 |
+| 后端Service | 5 | 0 | 5 |
+| 后端Controller | 5 | 0 | 5 |
+| 前端API | 6 | 0 | 6 |
+| 前端组件 | 4 | 0 | 4 |
+| **总计** | **26** | **0** | **26** |
+
+**🎉 核心功能完成度:100%**
+
+---
+
+## 🎯 下一步计划
+
+### ✅ 核心功能已全部完成!
+
+后端Service层和Controller层已全部实现,前端API和组件也已完成。现在可以进行联调测试。
+
+### 🎨 推荐任务
+1. **联调测试**(用户完成)
+ - 智能体配置功能测试
+ - 知识库管理功能测试(含分段编辑)
+ - 文件上传流程测试
+ - AI对话功能测试(悬浮球 + 对话界面)
+
+2. **配置Dify**(用户完成)
+ - 配置Dify API Key和URL
+ - 创建Dify应用和知识库
+ - 测试Dify API连接
+
+### ⚡ 可选优化任务
+1. **实现统计服务**(可选)
+ - AiUsageStatisticsServiceImpl - 使用统计服务实现
+
+2. **性能优化**(可选)
+ - 对话流式响应优化
+ - 文件上传并发处理
+ - 知识库查询缓存
+ - 添加Redis缓存支持
+
+---
+
+## 💡 技术亮点
+
+### 后端架构
+1. **权限控制**:知识库查询已集成统一权限系统
+ - 使用`UserDeptRoleVO`传递用户信息
+ - 支持部门路径继承(dept_path)
+ - 三种查询方式:列表查询、ID查询、权限检查
+
+2. **配置管理**:完整的Dify配置支持
+ - 支持环境变量覆盖
+ - 分类配置(上传、知识库、对话)
+ - 合理的默认值
+
+3. **异常体系**:明确的异常分类
+ - Dify调用异常
+ - 知识库异常
+ - 文件处理异常
+ - 对话异常
+
+4. **Dify API Client**:企业级HTTP客户端封装
+ - 双客户端设计(普通/流式)
+ - 完整的SSE流式响应处理
+ - 15个精心设计的DTO类
+ - 支持知识库、文档、检索、对话所有功能
+ - 灵活的API Key管理(支持智能体级别覆盖)
+
+5. **Service层架构**:
+ - 5个完整的Service实现(智能体、知识库、文件、对话、对话历史)
+ - 事务管理保证数据一致性
+ - 完整的错误处理和日志记录
+ - 流式对话SSE支持
+
+6. **Controller层架构**:
+ - **AiAgentConfigController**:智能体配置管理(10个REST接口)
+ - **AiKnowledgeController**:知识库管理(9个REST接口)
+ - **AiFileUploadController**:文件上传管理(8个REST接口)
+ - **AiChatController**:对话和历史管理(21个REST接口)
+ - **DifyProxyController**:Dify分段代理(5个REST接口)
+ - 统一的请求/响应格式
+ - 完整的参数验证
+ - RESTful API设计规范
+
+### 前端架构
+1. **API封装**:
+ - 6个完整的API模块(智能体、知识库、文件、对话、对话历史、文档分段)
+ - 统一的类型定义(TypeScript)
+ - 流式对话回调机制
+
+2. **组件设计**:
+ - **AIAgent.vue**:悬浮球拖动、自动停靠、流式对话、消息评价
+ - **KnowledgeManagementView.vue**:卡片式布局、三步骤上传、Figma设计实现
+ - **DocumentSegmentDialog.vue**:文档分段CRUD、实时编辑
+ - **AIConfigView.vue**:智能体配置管理
+
+3. **用户体验**:
+ - 流畅的拖动动画
+ - 实时的打字机效果
+ - Markdown内容渲染
+ - 完整的加载状态和错误处理
+
+4. **代理模式**:
+ - DifyProxyController实现后端API代理
+ - 前端直接调用分段管理接口
+ - 无需本地存储分段数据
+
+---
+
+## 📝 备注
+
+### 已完成
+- ✅ 所有Mapper查询已添加权限过滤
+- ✅ 配置文件提供了完整的注释说明
+- ✅ 数据库表包含Dify集成所需的所有字段
+- ✅ DTO类已同步更新,支持新字段
+- ✅ Dify API Client已完成,支持所有核心功能
+- ✅ 5个Service层全部实现(智能体、知识库、文件、对话、对话历史)
+- ✅ 前端6个API模块全部实现并完成类型定义
+- ✅ 前端4个核心组件实现(智能体配置、知识库管理、文档分段、AI助手)
+- ✅ Dify代理Controller实现(文档分段管理)
+
+### 待完成(核心功能)
+- ⏭️ 4个主要Controller接口(智能体、知识库、文件上传、对话)
+- ⏭️ 统计Service(可选)
+- ⏭️ 性能优化和压力测试
+
+### 当前状态
+**后端开发进度:83%** (5/6 Service + 1/5 Controller)
+**前端开发进度:100%** (6/6 API + 4/4 组件)
+**整体完成度:78.6%**
+
+**下一步:实现4个核心Controller接口,完成REST API层**
+
diff --git a/schoolNewsServ/ai/Dify知识库指定方案.md b/schoolNewsServ/ai/Dify知识库指定方案.md
new file mode 100644
index 0000000..d22c8ab
--- /dev/null
+++ b/schoolNewsServ/ai/Dify知识库指定方案.md
@@ -0,0 +1,588 @@
+# Dify指定知识库实现方案
+
+## 🎯 问题分析
+
+**需求**:在调用Dify对话API时,动态指定使用哪些知识库(Dataset)
+
+**场景**:不同部门用户使用同一个智能体,但只能访问各自授权的知识库
+
+---
+
+## 📋 Dify官方支持的方式
+
+### 方式1:知识库检索API + LLM组合(推荐⭐⭐)
+
+**优点**:
+- ✅ 完全控制知识库选择
+- ✅ 可以实现复杂的权限逻辑
+- ✅ 灵活性最高
+
+**缺点**:
+- ⚠️ 需要自己组合API调用
+- ⚠️ 无法使用Dify的完整对话管理功能
+
+#### 步骤1:检索相关知识
+
+```http
+POST https://api.dify.ai/v1/datasets/{dataset_id}/retrieve
+Authorization: Bearer {api_key}
+Content-Type: application/json
+
+{
+ "query": "如何申请奖学金?",
+ "top_k": 3,
+ "score_threshold": 0.7
+}
+```
+
+**响应示例:**
+```json
+{
+ "records": [
+ {
+ "content": "申请奖学金需要满足以下条件...",
+ "score": 0.95,
+ "metadata": {
+ "document_id": "doc-123",
+ "document_name": "奖学金管理办法.pdf"
+ }
+ }
+ ]
+}
+```
+
+#### 步骤2:多知识库并行检索
+
+```java
+@Service
+public class DifyKnowledgeService {
+
+ /**
+ * 从多个知识库检索相关内容
+ */
+ public List retrieveFromMultipleDatasets(
+ String query,
+ List datasetIds,
+ int topK) {
+
+ List>> futures = datasetIds.stream()
+ .map(datasetId -> CompletableFuture.supplyAsync(() ->
+ retrieveFromDataset(datasetId, query, topK)
+ ))
+ .collect(Collectors.toList());
+
+ // 等待所有检索完成
+ CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+
+ // 合并结果并按分数排序
+ return futures.stream()
+ .map(CompletableFuture::join)
+ .flatMap(List::stream)
+ .sorted((a, b) -> Double.compare(b.getScore(), a.getScore()))
+ .limit(topK)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 从单个知识库检索
+ */
+ private List retrieveFromDataset(
+ String datasetId,
+ String query,
+ int topK) {
+
+ String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/retrieve");
+
+ Map requestBody = new HashMap<>();
+ requestBody.put("query", query);
+ requestBody.put("top_k", topK);
+ requestBody.put("score_threshold", 0.7);
+
+ // HTTP请求
+ HttpResponse response = httpClient.post(url)
+ .header("Authorization", "Bearer " + apiKey)
+ .body(requestBody)
+ .execute();
+
+ return parseRetrievalResponse(response);
+ }
+}
+```
+
+#### 步骤3:组合上下文调用LLM
+
+```java
+/**
+ * 使用检索到的知识回答问题
+ */
+public String chatWithRetrievedKnowledge(
+ String query,
+ List records,
+ String conversationId) {
+
+ // 构建上下文
+ String context = records.stream()
+ .map(r -> "【" + r.getMetadata().get("document_name") + "】\n" + r.getContent())
+ .collect(Collectors.joining("\n\n"));
+
+ // 构建Prompt
+ String prompt = String.format(
+ "请基于以下知识库内容回答用户问题。如果知识库中没有相关信息,请明确告知用户。\n\n" +
+ "知识库内容:\n%s\n\n" +
+ "用户问题:%s\n\n" +
+ "回答:",
+ context, query
+ );
+
+ // 调用Dify Completion API或直接调用LLM
+ return callLLM(prompt, conversationId);
+}
+```
+
+---
+
+### 方式2:Dify Workflow(工作流)⭐
+
+**原理**:创建工作流,使用变量控制知识库选择
+
+#### Dify工作流配置
+
+```yaml
+workflow_nodes:
+ - id: start
+ type: start
+ outputs:
+ - query # 用户问题
+ - dataset_ids # 知识库ID列表(变量)
+
+ - id: kb_retrieval
+ type: knowledge-retrieval
+ inputs:
+ query: "{{#start.query#}}"
+ datasets: "{{#start.dataset_ids#}}" # 从输入变量读取
+ top_k: 3
+ outputs:
+ - result
+
+ - id: llm
+ type: llm
+ inputs:
+ prompt: |
+ 基于以下知识库内容回答问题:
+ {{#kb_retrieval.result#}}
+
+ 用户问题:{{#start.query#}}
+ outputs:
+ - answer
+
+ - id: end
+ type: end
+ outputs:
+ - answer: "{{#llm.answer#}}"
+```
+
+#### API调用示例
+
+```java
+/**
+ * 调用Dify Workflow
+ */
+public void chatWithWorkflow(
+ String query,
+ List datasetIds,
+ String userId,
+ SseEmitter emitter) {
+
+ String url = difyConfig.getFullApiUrl("/workflows/run");
+
+ Map inputs = new HashMap<>();
+ inputs.put("query", query);
+ inputs.put("dataset_ids", datasetIds); // ⭐ 动态传入知识库列表
+
+ Map requestBody = new HashMap<>();
+ requestBody.put("inputs", inputs);
+ requestBody.put("response_mode", "streaming");
+ requestBody.put("user", userId);
+
+ // 流式请求
+ httpClient.postStream(url, requestBody, new StreamCallback() {
+ @Override
+ public void onChunk(String chunk) {
+ emitter.send(chunk);
+ }
+ });
+}
+```
+
+**HTTP请求示例:**
+```http
+POST /v1/workflows/run
+Authorization: Bearer {api_key}
+Content-Type: application/json
+
+{
+ "inputs": {
+ "query": "如何申请奖学金?",
+ "dataset_ids": ["dataset-edu-001", "dataset-public-001"]
+ },
+ "response_mode": "streaming",
+ "user": "user-123"
+}
+```
+
+---
+
+### 方式3:多应用切换(不推荐)
+
+为不同部门创建不同的Dify应用:
+
+```
+部门A -> App A(绑定知识库A1, A2)
+部门B -> App B(绑定知识库B1, B2)
+```
+
+**缺点**:
+- ❌ 管理复杂
+- ❌ 无法共享公共知识库
+- ❌ 扩展性差
+
+---
+
+## 🎨 推荐实现方案
+
+### 方案:知识库检索API + 自定义LLM调用
+
+#### 完整实现代码
+
+```java
+@Service
+public class AiChatServiceImpl implements AiChatService {
+
+ @Autowired
+ private AiKnowledgeMapper knowledgeMapper;
+
+ @Autowired
+ private DifyApiClient difyApiClient;
+
+ @Autowired
+ private AiMessageMapper messageMapper;
+
+ /**
+ * 流式对话(带知识库权限隔离)
+ */
+ @Override
+ public void streamChat(
+ String message,
+ String conversationId,
+ String userId,
+ SseEmitter emitter) {
+
+ try {
+ // 1. 获取当前登录用户的部门角色信息(通过LoginUtil)⭐
+ List userDeptRoles = LoginUtil.getCurrentDeptRole();
+
+ // 2. 查询用户有权限的知识库(自动权限过滤✅)
+ TbAiKnowledge filter = new TbAiKnowledge();
+ filter.setStatus(1); // 只查询启用的
+
+ List authorizedKnowledges =
+ knowledgeMapper.selectAiKnowledges(
+ filter,
+ userDeptRoles // 直接传入LoginUtil获取的用户权限信息
+ );
+
+ // 3. 提取Dify Dataset IDs
+ List datasetIds = authorizedKnowledges.stream()
+ .map(TbAiKnowledge::getDifyDatasetId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+
+ if (datasetIds.isEmpty()) {
+ emitter.send("您当前没有可访问的知识库,无法进行对话。");
+ emitter.complete();
+ return;
+ }
+
+ // 4. 从多个知识库检索相关内容
+ List retrievalRecords =
+ difyApiClient.retrieveFromMultipleDatasets(
+ message,
+ datasetIds,
+ 5 // Top K
+ );
+
+ // 5. 构建上下文
+ String context = buildContext(retrievalRecords, authorizedKnowledges);
+
+ // 6. 调用LLM流式对话
+ difyApiClient.streamChatWithContext(
+ message,
+ context,
+ conversationId,
+ userId,
+ new StreamCallback() {
+ private StringBuilder fullAnswer = new StringBuilder();
+
+ @Override
+ public void onChunk(String chunk) {
+ fullAnswer.append(chunk);
+ emitter.send(chunk);
+ }
+
+ @Override
+ public void onComplete() {
+ // 保存消息
+ saveMessages(
+ conversationId,
+ userId,
+ message,
+ fullAnswer.toString(),
+ retrievalRecords
+ );
+ emitter.complete();
+ }
+
+ @Override
+ public void onError(Throwable error) {
+ log.error("对话失败", error);
+ emitter.completeWithError(error);
+ }
+ }
+ );
+
+ } catch (Exception e) {
+ log.error("流式对话异常", e);
+ emitter.completeWithError(e);
+ }
+ }
+
+ /**
+ * 构建上下文
+ */
+ private String buildContext(
+ List records,
+ List knowledges) {
+
+ Map knowledgeTitles = knowledges.stream()
+ .collect(Collectors.toMap(
+ TbAiKnowledge::getDifyDatasetId,
+ TbAiKnowledge::getTitle
+ ));
+
+ return records.stream()
+ .map(r -> {
+ String datasetId = r.getDatasetId();
+ String knowledgeTitle = knowledgeTitles.getOrDefault(datasetId, "未知知识库");
+ return String.format(
+ "【来源:%s - %s】\n%s",
+ knowledgeTitle,
+ r.getDocumentName(),
+ r.getContent()
+ );
+ })
+ .collect(Collectors.joining("\n\n---\n\n"));
+ }
+}
+```
+
+#### DifyApiClient实现
+
+```java
+@Component
+public class DifyApiClient {
+
+ @Autowired
+ private DifyConfig difyConfig;
+
+ private final OkHttpClient httpClient;
+
+ public DifyApiClient(DifyConfig difyConfig) {
+ this.difyConfig = difyConfig;
+ this.httpClient = new OkHttpClient.Builder()
+ .connectTimeout(difyConfig.getConnectTimeout(), TimeUnit.SECONDS)
+ .readTimeout(difyConfig.getReadTimeout(), TimeUnit.SECONDS)
+ .build();
+ }
+
+ /**
+ * 从多个知识库检索
+ */
+ public List retrieveFromMultipleDatasets(
+ String query,
+ List datasetIds,
+ int topK) {
+
+ // 并行检索所有知识库
+ List>> futures =
+ datasetIds.stream()
+ .map(id -> CompletableFuture.supplyAsync(() ->
+ retrieveFromDataset(id, query, topK)
+ ))
+ .collect(Collectors.toList());
+
+ // 等待完成
+ CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+
+ // 合并并排序
+ return futures.stream()
+ .map(CompletableFuture::join)
+ .flatMap(List::stream)
+ .sorted((a, b) -> Double.compare(b.getScore(), a.getScore()))
+ .limit(topK)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * 从单个知识库检索
+ */
+ private List retrieveFromDataset(
+ String datasetId,
+ String query,
+ int topK) {
+
+ String url = String.format(
+ "%s/datasets/%s/retrieve",
+ difyConfig.getApiBaseUrl(),
+ datasetId
+ );
+
+ JSONObject body = new JSONObject();
+ body.put("query", query);
+ body.put("top_k", topK);
+ body.put("score_threshold", 0.7);
+
+ Request request = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + difyConfig.getApiKey())
+ .header("Content-Type", "application/json")
+ .post(RequestBody.create(
+ body.toString(),
+ MediaType.parse("application/json")
+ ))
+ .build();
+
+ try (Response response = httpClient.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ throw new DifyException("知识库检索失败: " + response.message());
+ }
+
+ String responseBody = response.body().string();
+ return parseRetrievalResponse(datasetId, responseBody);
+
+ } catch (IOException e) {
+ throw new DifyException("知识库检索异常", e);
+ }
+ }
+
+ /**
+ * 流式对话(带上下文)
+ */
+ public void streamChatWithContext(
+ String query,
+ String context,
+ String conversationId,
+ String userId,
+ StreamCallback callback) {
+
+ String url = difyConfig.getApiBaseUrl() + "/chat-messages";
+
+ // 构建完整Prompt
+ String fullPrompt = String.format(
+ "请基于以下知识库内容回答用户问题。" +
+ "如果知识库中没有相关信息,请明确告知用户。\n\n" +
+ "知识库内容:\n%s\n\n" +
+ "用户问题:%s",
+ context, query
+ );
+
+ JSONObject body = new JSONObject();
+ body.put("query", fullPrompt);
+ body.put("conversation_id", conversationId);
+ body.put("user", userId);
+ body.put("response_mode", "streaming");
+ body.put("inputs", new JSONObject());
+
+ Request request = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + difyConfig.getApiKey())
+ .header("Content-Type", "application/json")
+ .post(RequestBody.create(
+ body.toString(),
+ MediaType.parse("application/json")
+ ))
+ .build();
+
+ // SSE流式处理
+ httpClient.newCall(request).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) {
+ if (!response.isSuccessful()) {
+ callback.onError(new DifyException("对话失败: " + response.message()));
+ return;
+ }
+
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(response.body().byteStream()))) {
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (line.startsWith("data: ")) {
+ String data = line.substring(6);
+ if (!"[DONE]".equals(data)) {
+ JSONObject json = new JSONObject(data);
+ String chunk = json.optString("answer", "");
+ if (!chunk.isEmpty()) {
+ callback.onChunk(chunk);
+ }
+ }
+ }
+ }
+ callback.onComplete();
+
+ } catch (Exception e) {
+ callback.onError(e);
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, IOException e) {
+ callback.onError(e);
+ }
+ });
+ }
+}
+```
+
+---
+
+## 📊 三种方式对比
+
+| 方案 | 灵活性 | 实现难度 | 性能 | 推荐度 |
+|------|--------|----------|------|--------|
+| 检索API + 自定义LLM | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
+| Workflow工作流 | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
+| 多应用切换 | ⭐⭐ | ⭐ | ⭐⭐⭐ | ⭐ |
+
+---
+
+## 🎯 最终推荐方案
+
+**使用"检索API + 自定义LLM"方案**
+
+**理由**:
+1. ✅ 完全控制知识库访问权限
+2. ✅ 可以实现复杂的部门隔离逻辑
+3. ✅ 支持并行检索多个知识库
+4. ✅ 可以自定义Prompt和上下文
+5. ✅ 灵活性最高,适合企业级应用
+
+**实现步骤**:
+1. 用户发起对话
+2. 根据用户权限查询可访问的知识库(Mapper已实现✅)
+3. 并行调用Dify检索API获取相关内容
+4. 合并结果构建上下文
+5. 调用LLM流式生成答案
+6. 保存对话记录(含知识来源)
+
+这样既利用了Dify的知识库能力,又保持了完全的控制权!🎉
+
diff --git a/schoolNewsServ/ai/pom.xml b/schoolNewsServ/ai/pom.xml
index 2c49edb..bc132c8 100644
--- a/schoolNewsServ/ai/pom.xml
+++ b/schoolNewsServ/ai/pom.xml
@@ -33,6 +33,10 @@
common-all
${school-news.version}
+
+ org.xyzh
+ system
+
org.springframework.boot
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java
new file mode 100644
index 0000000..596d7fe
--- /dev/null
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/DifyApiClient.java
@@ -0,0 +1,794 @@
+package org.xyzh.ai.client;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import jakarta.annotation.PostConstruct;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.xyzh.ai.client.dto.*;
+import org.xyzh.ai.client.callback.StreamCallback;
+import org.xyzh.ai.config.DifyConfig;
+import org.xyzh.ai.exception.DifyException;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @description Dify API客户端 - 封装所有Dify平台HTTP调用
+ * @filename DifyApiClient.java
+ * @author AI Assistant
+ * @copyright xyzh
+ * @since 2025-11-04
+ */
+@Slf4j
+@Component
+public class DifyApiClient {
+
+ @Autowired
+ private DifyConfig difyConfig;
+
+ private OkHttpClient httpClient;
+ private OkHttpClient streamHttpClient;
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ @PostConstruct
+ public void init() {
+ // 普通请求客户端
+ this.httpClient = new OkHttpClient.Builder()
+ .connectTimeout(difyConfig.getConnectTimeout(), TimeUnit.SECONDS)
+ .readTimeout(difyConfig.getReadTimeout(), TimeUnit.SECONDS)
+ .writeTimeout(difyConfig.getTimeout(), TimeUnit.SECONDS)
+ .retryOnConnectionFailure(true)
+ .build();
+
+ // 流式请求客户端(更长的读取超时)
+ this.streamHttpClient = new OkHttpClient.Builder()
+ .connectTimeout(difyConfig.getConnectTimeout(), TimeUnit.SECONDS)
+ .readTimeout(difyConfig.getStreamTimeout(), TimeUnit.SECONDS)
+ .writeTimeout(difyConfig.getTimeout(), TimeUnit.SECONDS)
+ .retryOnConnectionFailure(false) // 流式不重试
+ .build();
+
+ log.info("DifyApiClient初始化完成,API地址: {}", difyConfig.getApiBaseUrl());
+ }
+
+ // ===================== 知识库管理 API =====================
+
+ /**
+ * 创建知识库(Dataset)
+ */
+ public DatasetCreateResponse createDataset(DatasetCreateRequest request, String apiKey) {
+ String url = difyConfig.getFullApiUrl("/datasets");
+
+ try {
+ String jsonBody = objectMapper.writeValueAsString(request);
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .header("Content-Type", "application/json")
+ .post(RequestBody.create(jsonBody, MediaType.parse("application/json")))
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+
+ if (!response.isSuccessful()) {
+ log.error("创建知识库失败: {} - {}", response.code(), responseBody);
+ throw new DifyException("创建知识库失败: " + responseBody);
+ }
+
+ return objectMapper.readValue(responseBody, DatasetCreateResponse.class);
+ }
+ } catch (IOException e) {
+ log.error("创建知识库异常", e);
+ throw new DifyException("创建知识库异常: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 查询知识库列表
+ */
+ public DatasetListResponse listDatasets(int page, int limit, String apiKey) {
+ String url = difyConfig.getFullApiUrl("/datasets?page=" + page + "&limit=" + limit);
+
+ try {
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .get()
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+
+ if (!response.isSuccessful()) {
+ log.error("查询知识库列表失败: {} - {}", response.code(), responseBody);
+ throw new DifyException("查询知识库列表失败: " + responseBody);
+ }
+
+ return objectMapper.readValue(responseBody, DatasetListResponse.class);
+ }
+ } catch (IOException e) {
+ log.error("查询知识库列表异常", e);
+ throw new DifyException("查询知识库列表异常: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 查询知识库详情
+ */
+ public DatasetDetailResponse getDatasetDetail(String datasetId, String apiKey) {
+ String url = difyConfig.getFullApiUrl("/datasets/" + datasetId);
+
+ try {
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .get()
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+
+ if (!response.isSuccessful()) {
+ log.error("查询知识库详情失败: {} - {}", response.code(), responseBody);
+ throw new DifyException("查询知识库详情失败: " + responseBody);
+ }
+
+ return objectMapper.readValue(responseBody, DatasetDetailResponse.class);
+ }
+ } catch (IOException e) {
+ log.error("查询知识库详情异常", e);
+ throw new DifyException("查询知识库详情异常: " + e.getMessage(), e);
+ }
+ }
+
+
+ /**
+ * 更新知识库
+ */
+ public void updateDataset(String datasetId, DatasetUpdateRequest request, String apiKey) {
+ String url = difyConfig.getFullApiUrl("/datasets/" + datasetId);
+
+ try {
+ String jsonBody = objectMapper.writeValueAsString(request);
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .header("Content-Type", "application/json")
+ .patch(RequestBody.create(jsonBody, MediaType.parse("application/json")))
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ if (!response.isSuccessful()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+ log.error("更新知识库失败: {} - {}", response.code(), responseBody);
+ throw new DifyException("更新知识库失败: " + responseBody);
+ }
+ log.info("知识库更新成功: {}", datasetId);
+ }
+ } catch (IOException e) {
+ log.error("更新知识库异常", e);
+ throw new DifyException("更新知识库异常: " + e.getMessage(), e);
+ }
+ }
+ /**
+ * 删除知识库
+ */
+ public void deleteDataset(String datasetId, String apiKey) {
+ String url = difyConfig.getFullApiUrl("/datasets/" + datasetId);
+
+ try {
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .delete()
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ if (!response.isSuccessful()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+ log.error("删除知识库失败: {} - {}", response.code(), responseBody);
+ throw new DifyException("删除知识库失败: " + responseBody);
+ }
+ log.info("知识库删除成功: {}", datasetId);
+ }
+ } catch (IOException e) {
+ log.error("删除知识库异常", e);
+ throw new DifyException("删除知识库异常: " + e.getMessage(), e);
+ }
+ }
+
+ // ===================== 文档管理 API =====================
+
+ /**
+ * 上传文档到知识库(通过文件)
+ */
+ public DocumentUploadResponse uploadDocumentByFile(
+ String datasetId,
+ File file,
+ String originalFilename,
+ DocumentUploadRequest uploadRequest,
+ String apiKey) {
+
+ String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/document/create_by_file");
+
+ try {
+ MultipartBody.Builder bodyBuilder = new MultipartBody.Builder()
+ .setType(MultipartBody.FORM)
+ .addFormDataPart("file", originalFilename,
+ RequestBody.create(file, MediaType.parse("application/octet-stream")));
+
+ // 添加其他参数
+ if (uploadRequest.getName() != null) {
+ bodyBuilder.addFormDataPart("name", uploadRequest.getName());
+ }
+ if (uploadRequest.getIndexingTechnique() != null) {
+ bodyBuilder.addFormDataPart("indexing_technique", uploadRequest.getIndexingTechnique());
+ }
+ if (uploadRequest.getProcessRule() != null) {
+ bodyBuilder.addFormDataPart("process_rule", objectMapper.writeValueAsString(uploadRequest.getProcessRule()));
+ }
+
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .post(bodyBuilder.build())
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+
+ if (!response.isSuccessful()) {
+ log.error("上传文档失败: {} - {}", response.code(), responseBody);
+ throw new DifyException("上传文档失败: " + responseBody);
+ }
+
+ return objectMapper.readValue(responseBody, DocumentUploadResponse.class);
+ }
+ } catch (IOException e) {
+ log.error("上传文档异常", e);
+ throw new DifyException("上传文档异常: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 查询文档处理状态
+ */
+ public DocumentStatusResponse getDocumentStatus(String datasetId, String batchId, String apiKey) {
+ String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/documents/" + batchId + "/indexing-status");
+
+ try {
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .get()
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+
+ if (!response.isSuccessful()) {
+ log.error("查询文档状态失败: {} - {}", response.code(), responseBody);
+ throw new DifyException("查询文档状态失败: " + responseBody);
+ }
+
+ return objectMapper.readValue(responseBody, DocumentStatusResponse.class);
+ }
+ } catch (IOException e) {
+ log.error("查询文档状态异常", e);
+ throw new DifyException("查询文档状态异常: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 查询知识库文档列表
+ */
+ public DocumentListResponse listDocuments(String datasetId, int page, int limit, String apiKey) {
+ String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/documents?page=" + page + "&limit=" + limit);
+
+ try {
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .get()
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+
+ if (!response.isSuccessful()) {
+ log.error("查询文档列表失败: {} - {}", response.code(), responseBody);
+ throw new DifyException("查询文档列表失败: " + responseBody);
+ }
+
+ return objectMapper.readValue(responseBody, DocumentListResponse.class);
+ }
+ } catch (IOException e) {
+ log.error("查询文档列表异常", e);
+ throw new DifyException("查询文档列表异常: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 删除文档
+ */
+ public void deleteDocument(String datasetId, String documentId, String apiKey) {
+ String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/documents/" + documentId);
+
+ try {
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .delete()
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ if (!response.isSuccessful()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+ log.error("删除文档失败: {} - {}", response.code(), responseBody);
+ throw new DifyException("删除文档失败: " + responseBody);
+ }
+ log.info("文档删除成功: {}", documentId);
+ }
+ } catch (IOException e) {
+ log.error("删除文档异常", e);
+ throw new DifyException("删除文档异常: " + e.getMessage(), e);
+ }
+ }
+
+ // ===================== 知识库检索 API =====================
+
+ /**
+ * 从知识库检索相关内容
+ */
+ public RetrievalResponse retrieveFromDataset(String datasetId, RetrievalRequest request, String apiKey) {
+ String url = difyConfig.getFullApiUrl("/datasets/" + datasetId + "/retrieve");
+
+ try {
+ String jsonBody = objectMapper.writeValueAsString(request);
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .header("Content-Type", "application/json")
+ .post(RequestBody.create(jsonBody, MediaType.parse("application/json")))
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+
+ if (!response.isSuccessful()) {
+ log.error("知识库检索失败: {} - {}", response.code(), responseBody);
+ throw new DifyException("知识库检索失败: " + responseBody);
+ }
+
+ return objectMapper.readValue(responseBody, RetrievalResponse.class);
+ }
+ } catch (IOException e) {
+ log.error("知识库检索异常", e);
+ throw new DifyException("知识库检索异常: " + e.getMessage(), e);
+ }
+ }
+
+ // ===================== 对话 API =====================
+
+ /**
+ * 流式对话(SSE)
+ */
+ public void streamChat(ChatRequest request, String apiKey, StreamCallback callback) {
+ String url = difyConfig.getFullApiUrl("/chat-messages");
+
+ try {
+ // 设置为流式模式
+ request.setResponseMode("streaming");
+
+ String jsonBody = objectMapper.writeValueAsString(request);
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .header("Content-Type", "application/json")
+ .post(RequestBody.create(jsonBody, MediaType.parse("application/json")))
+ .build();
+
+ streamHttpClient.newCall(httpRequest).enqueue(new Callback() {
+ @Override
+ public void onResponse(Call call, Response response) throws IOException {
+ if (!response.isSuccessful()) {
+ String errorBody = response.body() != null ? response.body().string() : "";
+ callback.onError(new DifyException("流式对话失败: " + errorBody));
+ return;
+ }
+
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(response.body().byteStream()))) {
+
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (line.startsWith("data: ")) {
+ String data = line.substring(6).trim();
+
+ if ("[DONE]".equals(data)) {
+ callback.onComplete();
+ break;
+ }
+
+ if (!data.isEmpty()) {
+ // 解析SSE数据
+ JsonNode jsonNode = objectMapper.readTree(data);
+ String event = jsonNode.has("event") ? jsonNode.get("event").asText() : "";
+
+ switch (event) {
+ case "message":
+ case "agent_message":
+ // 消息内容
+ if (jsonNode.has("answer")) {
+ callback.onMessage(jsonNode.get("answer").asText());
+ }
+ break;
+ case "message_end":
+ // 消息结束,提取元数据
+ callback.onMessageEnd(data);
+ break;
+ case "error":
+ // 错误事件
+ String errorMsg = jsonNode.has("message") ?
+ jsonNode.get("message").asText() : "未知错误";
+ callback.onError(new DifyException(errorMsg));
+ return;
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ log.error("流式响应处理异常", e);
+ callback.onError(e);
+ }
+ }
+
+ @Override
+ public void onFailure(Call call, IOException e) {
+ log.error("流式对话请求失败", e);
+ callback.onError(e);
+ }
+ });
+
+ } catch (Exception e) {
+ log.error("流式对话异常", e);
+ callback.onError(e);
+ }
+ }
+
+ /**
+ * 阻塞式对话(非流式)
+ */
+ public ChatResponse blockingChat(ChatRequest request, String apiKey) {
+ String url = difyConfig.getFullApiUrl("/chat-messages");
+
+ try {
+ // 设置为阻塞模式
+ request.setResponseMode("blocking");
+
+ String jsonBody = objectMapper.writeValueAsString(request);
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .header("Content-Type", "application/json")
+ .post(RequestBody.create(jsonBody, MediaType.parse("application/json")))
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+
+ if (!response.isSuccessful()) {
+ log.error("阻塞式对话失败: {} - {}", response.code(), responseBody);
+ throw new DifyException("阻塞式对话失败: " + responseBody);
+ }
+
+ return objectMapper.readValue(responseBody, ChatResponse.class);
+ }
+ } catch (IOException e) {
+ log.error("阻塞式对话异常", e);
+ throw new DifyException("阻塞式对话异常: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 停止对话生成
+ */
+ public void stopChatMessage(String taskId, String userId, String apiKey) {
+ String url = difyConfig.getFullApiUrl("/chat-messages/" + taskId + "/stop");
+
+ try {
+ String jsonBody = objectMapper.writeValueAsString(new StopRequest(userId));
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .header("Content-Type", "application/json")
+ .post(RequestBody.create(jsonBody, MediaType.parse("application/json")))
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ if (!response.isSuccessful()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+ log.error("停止对话失败: {} - {}", response.code(), responseBody);
+ throw new DifyException("停止对话失败: " + responseBody);
+ }
+ log.info("对话停止成功: {}", taskId);
+ }
+ } catch (IOException e) {
+ log.error("停止对话异常", e);
+ throw new DifyException("停止对话异常: " + e.getMessage(), e);
+ }
+ }
+
+ // ===================== 对话历史 API =====================
+
+ /**
+ * 获取对话历史消息
+ */
+ public MessageHistoryResponse getMessageHistory(
+ String conversationId,
+ String userId,
+ String firstId,
+ Integer limit,
+ String apiKey) {
+
+ StringBuilder urlBuilder = new StringBuilder(difyConfig.getFullApiUrl("/messages"));
+ urlBuilder.append("?user=").append(userId);
+
+ if (conversationId != null && !conversationId.isEmpty()) {
+ urlBuilder.append("&conversation_id=").append(conversationId);
+ }
+ if (firstId != null && !firstId.isEmpty()) {
+ urlBuilder.append("&first_id=").append(firstId);
+ }
+ if (limit != null) {
+ urlBuilder.append("&limit=").append(limit);
+ }
+
+ try {
+ Request httpRequest = new Request.Builder()
+ .url(urlBuilder.toString())
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .get()
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+
+ if (!response.isSuccessful()) {
+ log.error("获取对话历史失败: {} - {}", response.code(), responseBody);
+ throw new DifyException("获取对话历史失败: " + responseBody);
+ }
+
+ return objectMapper.readValue(responseBody, MessageHistoryResponse.class);
+ }
+ } catch (IOException e) {
+ log.error("获取对话历史异常", e);
+ throw new DifyException("获取对话历史异常: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 获取对话列表
+ */
+ public ConversationListResponse getConversations(
+ String userId,
+ String lastId,
+ Integer limit,
+ String apiKey) {
+
+ StringBuilder urlBuilder = new StringBuilder(difyConfig.getFullApiUrl("/conversations"));
+ urlBuilder.append("?user=").append(userId);
+
+ if (lastId != null && !lastId.isEmpty()) {
+ urlBuilder.append("&last_id=").append(lastId);
+ }
+ if (limit != null) {
+ urlBuilder.append("&limit=").append(limit);
+ }
+
+ try {
+ Request httpRequest = new Request.Builder()
+ .url(urlBuilder.toString())
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .get()
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+
+ if (!response.isSuccessful()) {
+ log.error("获取对话列表失败: {} - {}", response.code(), responseBody);
+ throw new DifyException("获取对话列表失败: " + responseBody);
+ }
+
+ return objectMapper.readValue(responseBody, ConversationListResponse.class);
+ }
+ } catch (IOException e) {
+ log.error("获取对话列表异常", e);
+ throw new DifyException("获取对话列表异常: " + e.getMessage(), e);
+ }
+ }
+
+ // ===================== 通用 HTTP 方法(用于代理转发)=====================
+
+ /**
+ * 通用 GET 请求
+ * @param path API路径
+ * @param apiKey API密钥
+ * @return JSON响应字符串
+ */
+ public String get(String path, String apiKey) {
+ String url = difyConfig.getFullApiUrl(path);
+
+ try {
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .get()
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+
+ if (!response.isSuccessful()) {
+ log.error("GET请求失败: {} - {} - {}", url, response.code(), responseBody);
+ throw new DifyException("GET请求失败[" + response.code() + "]: " + responseBody);
+ }
+
+ return responseBody;
+ }
+ } catch (IOException e) {
+ log.error("GET请求异常: {}", url, e);
+ throw new DifyException("GET请求异常: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 通用 POST 请求
+ * @param path API路径
+ * @param requestBody 请求体(JSON字符串或Map)
+ * @param apiKey API密钥
+ * @return JSON响应字符串
+ */
+ public String post(String path, Object requestBody, String apiKey) {
+ String url = difyConfig.getFullApiUrl(path);
+
+ try {
+ String jsonBody = requestBody instanceof String ?
+ (String) requestBody : objectMapper.writeValueAsString(requestBody);
+
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .header("Content-Type", "application/json")
+ .post(RequestBody.create(jsonBody, MediaType.parse("application/json")))
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+
+ if (!response.isSuccessful()) {
+ log.error("POST请求失败: {} - {} - {}", url, response.code(), responseBody);
+ throw new DifyException("POST请求失败[" + response.code() + "]: " + responseBody);
+ }
+
+ return responseBody;
+ }
+ } catch (IOException e) {
+ log.error("POST请求异常: {}", url, e);
+ throw new DifyException("POST请求异常: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 通用 PATCH 请求
+ * @param path API路径
+ * @param requestBody 请求体(JSON字符串或Map)
+ * @param apiKey API密钥
+ * @return JSON响应字符串
+ */
+ public String patch(String path, Object requestBody, String apiKey) {
+ String url = difyConfig.getFullApiUrl(path);
+
+ try {
+ String jsonBody = requestBody instanceof String ?
+ (String) requestBody : objectMapper.writeValueAsString(requestBody);
+
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .header("Content-Type", "application/json")
+ .patch(RequestBody.create(jsonBody, MediaType.parse("application/json")))
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+
+ if (!response.isSuccessful()) {
+ log.error("PATCH请求失败: {} - {} - {}", url, response.code(), responseBody);
+ throw new DifyException("PATCH请求失败[" + response.code() + "]: " + responseBody);
+ }
+
+ return responseBody;
+ }
+ } catch (IOException e) {
+ log.error("PATCH请求异常: {}", url, e);
+ throw new DifyException("PATCH请求异常: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * 通用 DELETE 请求
+ * @param path API路径
+ * @param apiKey API密钥
+ * @return JSON响应字符串
+ */
+ public String delete(String path, String apiKey) {
+ String url = difyConfig.getFullApiUrl(path);
+
+ try {
+ Request httpRequest = new Request.Builder()
+ .url(url)
+ .header("Authorization", "Bearer " + getApiKey(apiKey))
+ .delete()
+ .build();
+
+ try (Response response = httpClient.newCall(httpRequest).execute()) {
+ String responseBody = response.body() != null ? response.body().string() : "";
+
+ if (!response.isSuccessful()) {
+ log.error("DELETE请求失败: {} - {} - {}", url, response.code(), responseBody);
+ throw new DifyException("DELETE请求失败[" + response.code() + "]: " + responseBody);
+ }
+
+ return responseBody;
+ }
+ } catch (IOException e) {
+ log.error("DELETE请求异常: {}", url, e);
+ throw new DifyException("DELETE请求异常: " + e.getMessage(), e);
+ }
+ }
+
+ // ===================== 工具方法 =====================
+
+ /**
+ * 获取API密钥(优先使用传入的密钥,否则使用配置的默认密钥)
+ */
+ private String getApiKey(String apiKey) {
+ if (apiKey != null && !apiKey.trim().isEmpty()) {
+ return apiKey;
+ }
+ if (difyConfig.getApiKey() != null && !difyConfig.getApiKey().trim().isEmpty()) {
+ return difyConfig.getApiKey();
+ }
+ throw new DifyException("未配置Dify API密钥");
+ }
+
+ /**
+ * 停止请求的内部类
+ */
+ private static class StopRequest {
+ private String user;
+
+ public StopRequest(String user) {
+ this.user = user;
+ }
+
+ public String getUser() {
+ return user;
+ }
+
+ public void setUser(String user) {
+ this.user = user;
+ }
+ }
+}
+
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/callback/StreamCallback.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/callback/StreamCallback.java
new file mode 100644
index 0000000..45380c7
--- /dev/null
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/callback/StreamCallback.java
@@ -0,0 +1,35 @@
+package org.xyzh.ai.client.callback;
+
+/**
+ * @description 流式响应回调接口
+ * @filename StreamCallback.java
+ * @author AI Assistant
+ * @copyright xyzh
+ * @since 2025-11-04
+ */
+public interface StreamCallback {
+
+ /**
+ * 接收到消息片段
+ * @param message 消息内容
+ */
+ void onMessage(String message);
+
+ /**
+ * 消息结束(包含元数据)
+ * @param metadata JSON格式的元数据
+ */
+ void onMessageEnd(String metadata);
+
+ /**
+ * 流式响应完成
+ */
+ void onComplete();
+
+ /**
+ * 发生错误
+ * @param error 错误对象
+ */
+ void onError(Throwable error);
+}
+
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/ChatRequest.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/ChatRequest.java
new file mode 100644
index 0000000..f0f1179
--- /dev/null
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/ChatRequest.java
@@ -0,0 +1,98 @@
+package org.xyzh.ai.client.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @description 对话请求
+ * @filename ChatRequest.java
+ * @author AI Assistant
+ * @copyright xyzh
+ * @since 2025-11-04
+ */
+@Data
+public class ChatRequest {
+
+ /**
+ * 输入变量
+ */
+ private Map inputs;
+
+ /**
+ * 用户问题
+ */
+ private String query;
+
+ /**
+ * 响应模式:streaming(流式)、blocking(阻塞)
+ */
+ @JsonProperty("response_mode")
+ private String responseMode = "streaming";
+
+ /**
+ * 对话ID(继续对话时传入)
+ */
+ @JsonProperty("conversation_id")
+ private String conversationId;
+
+ /**
+ * 用户标识
+ */
+ private String user;
+
+ /**
+ * 上传的文件列表
+ */
+ private List files;
+
+ /**
+ * 自动生成标题
+ */
+ @JsonProperty("auto_generate_name")
+ private Boolean autoGenerateName = true;
+
+ /**
+ * 指定的数据集ID列表(知识库检索)
+ */
+ @JsonProperty("dataset_ids")
+ private List datasetIds;
+
+ /**
+ * 温度参数(0.0-1.0)
+ */
+ private Double temperature;
+
+ /**
+ * 最大token数
+ */
+ @JsonProperty("max_tokens")
+ private Integer maxTokens;
+
+ @Data
+ public static class FileInfo {
+ /**
+ * 文件类型:image、document、audio、video
+ */
+ private String type;
+
+ /**
+ * 传输方式:remote_url、local_file
+ */
+ @JsonProperty("transfer_method")
+ private String transferMethod;
+
+ /**
+ * 文件URL或ID
+ */
+ private String url;
+
+ /**
+ * 本地文件上传ID
+ */
+ @JsonProperty("upload_file_id")
+ private String uploadFileId;
+ }
+}
+
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/ChatResponse.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/ChatResponse.java
new file mode 100644
index 0000000..a8ee025
--- /dev/null
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/ChatResponse.java
@@ -0,0 +1,121 @@
+package org.xyzh.ai.client.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @description 对话响应(阻塞模式)
+ * @filename ChatResponse.java
+ * @author AI Assistant
+ * @copyright xyzh
+ * @since 2025-11-04
+ */
+@Data
+public class ChatResponse {
+
+ /**
+ * 消息ID
+ */
+ @JsonProperty("message_id")
+ private String messageId;
+
+ /**
+ * 对话ID
+ */
+ @JsonProperty("conversation_id")
+ private String conversationId;
+
+ /**
+ * 模式
+ */
+ private String mode;
+
+ /**
+ * 回答内容
+ */
+ private String answer;
+
+ /**
+ * 元数据
+ */
+ private Map metadata;
+
+ /**
+ * 创建时间
+ */
+ @JsonProperty("created_at")
+ private Long createdAt;
+
+ /**
+ * Token使用情况
+ */
+ private Usage usage;
+
+ /**
+ * 检索信息
+ */
+ @JsonProperty("retrieval_info")
+ private List retrievalInfo;
+
+ @Data
+ public static class Usage {
+ @JsonProperty("prompt_tokens")
+ private Integer promptTokens;
+
+ @JsonProperty("prompt_unit_price")
+ private String promptUnitPrice;
+
+ @JsonProperty("prompt_price_unit")
+ private String promptPriceUnit;
+
+ @JsonProperty("prompt_price")
+ private String promptPrice;
+
+ @JsonProperty("completion_tokens")
+ private Integer completionTokens;
+
+ @JsonProperty("completion_unit_price")
+ private String completionUnitPrice;
+
+ @JsonProperty("completion_price_unit")
+ private String completionPriceUnit;
+
+ @JsonProperty("completion_price")
+ private String completionPrice;
+
+ @JsonProperty("total_tokens")
+ private Integer totalTokens;
+
+ @JsonProperty("total_price")
+ private String totalPrice;
+
+ private String currency;
+
+ private Double latency;
+ }
+
+ @Data
+ public static class RetrievalInfo {
+ @JsonProperty("dataset_id")
+ private String datasetId;
+
+ @JsonProperty("dataset_name")
+ private String datasetName;
+
+ @JsonProperty("document_id")
+ private String documentId;
+
+ @JsonProperty("document_name")
+ private String documentName;
+
+ @JsonProperty("segment_id")
+ private String segmentId;
+
+ private Double score;
+
+ private String content;
+ }
+}
+
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/ConversationListResponse.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/ConversationListResponse.java
new file mode 100644
index 0000000..9535d9d
--- /dev/null
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/ConversationListResponse.java
@@ -0,0 +1,49 @@
+package org.xyzh.ai.client.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import java.util.List;
+
+/**
+ * @description 对话列表响应
+ * @filename ConversationListResponse.java
+ * @author AI Assistant
+ * @copyright xyzh
+ * @since 2025-11-04
+ */
+@Data
+public class ConversationListResponse {
+
+ private Integer limit;
+
+ @JsonProperty("has_more")
+ private Boolean hasMore;
+
+ private List data;
+
+ @Data
+ public static class ConversationInfo {
+ private String id;
+
+ private String name;
+
+ private List inputs;
+
+ private String status;
+
+ private String introduction;
+
+ @JsonProperty("created_at")
+ private Long createdAt;
+
+ @JsonProperty("updated_at")
+ private Long updatedAt;
+ }
+
+ @Data
+ public static class InputInfo {
+ private String key;
+ private String value;
+ }
+}
+
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetCreateRequest.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetCreateRequest.java
new file mode 100644
index 0000000..b42b89a
--- /dev/null
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetCreateRequest.java
@@ -0,0 +1,43 @@
+package org.xyzh.ai.client.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * @description 创建知识库请求
+ * @filename DatasetCreateRequest.java
+ * @author AI Assistant
+ * @copyright xyzh
+ * @since 2025-11-04
+ */
+@Data
+public class DatasetCreateRequest {
+
+ /**
+ * 知识库名称
+ */
+ private String name;
+
+ /**
+ * 知识库描述
+ */
+ private String description;
+
+ /**
+ * 索引方式:high_quality(高质量)、economy(经济)
+ */
+ @JsonProperty("indexing_technique")
+ private String indexingTechnique = "high_quality";
+
+ /**
+ * Embedding模型
+ */
+ @JsonProperty("embedding_model")
+ private String embeddingModel;
+
+ /**
+ * 权限:only_me(仅自己)、all_team_members(团队所有成员)
+ */
+ private String permission = "only_me";
+}
+
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetCreateResponse.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetCreateResponse.java
new file mode 100644
index 0000000..7565ab0
--- /dev/null
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetCreateResponse.java
@@ -0,0 +1,55 @@
+package org.xyzh.ai.client.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * @description 创建知识库响应
+ * @filename DatasetCreateResponse.java
+ * @author AI Assistant
+ * @copyright xyzh
+ * @since 2025-11-04
+ */
+@Data
+public class DatasetCreateResponse {
+
+ /**
+ * 知识库ID
+ */
+ private String id;
+
+ /**
+ * 知识库名称
+ */
+ private String name;
+
+ /**
+ * 描述
+ */
+ private String description;
+
+ /**
+ * 索引方式
+ */
+ @JsonProperty("indexing_technique")
+ private String indexingTechnique;
+
+ /**
+ * Embedding模型
+ */
+ @JsonProperty("embedding_model")
+ private String embeddingModel;
+
+ /**
+ * 创建时间
+ */
+ @JsonProperty("created_at")
+ private Long createdAt;
+
+ /**
+ * 创建人
+ */
+ @JsonProperty("created_by")
+ private String createdBy;
+}
+
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetDetailResponse.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetDetailResponse.java
new file mode 100644
index 0000000..4131436
--- /dev/null
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetDetailResponse.java
@@ -0,0 +1,82 @@
+package org.xyzh.ai.client.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * @description 知识库详情响应
+ * @filename DatasetDetailResponse.java
+ * @author AI Assistant
+ * @copyright xyzh
+ * @since 2025-11-04
+ */
+@Data
+public class DatasetDetailResponse {
+
+ private String id;
+
+ private String name;
+
+ private String description;
+
+ @JsonProperty("indexing_technique")
+ private String indexingTechnique;
+
+ @JsonProperty("embedding_model")
+ private String embeddingModel;
+
+ @JsonProperty("embedding_model_provider")
+ private String embeddingModelProvider;
+
+ @JsonProperty("embedding_available")
+ private Boolean embeddingAvailable;
+
+ @JsonProperty("retrieval_model_dict")
+ private RetrievalModelDict retrievalModelDict;
+
+ @JsonProperty("document_count")
+ private Integer documentCount;
+
+ @JsonProperty("word_count")
+ private Integer wordCount;
+
+ @JsonProperty("app_count")
+ private Integer appCount;
+
+ @JsonProperty("created_by")
+ private String createdBy;
+
+ @JsonProperty("created_at")
+ private Long createdAt;
+
+ @JsonProperty("updated_at")
+ private Long updatedAt;
+
+ @Data
+ public static class RetrievalModelDict {
+ @JsonProperty("search_method")
+ private String searchMethod;
+
+ @JsonProperty("reranking_enable")
+ private Boolean rerankingEnable;
+
+ @JsonProperty("reranking_model")
+ private RerankingModel rerankingModel;
+
+ @JsonProperty("top_k")
+ private Integer topK;
+
+ @JsonProperty("score_threshold_enabled")
+ private Boolean scoreThresholdEnabled;
+ }
+
+ @Data
+ public static class RerankingModel {
+ @JsonProperty("reranking_provider_name")
+ private String rerankingProviderName;
+
+ @JsonProperty("reranking_model_name")
+ private String rerankingModelName;
+ }
+}
+
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetListResponse.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetListResponse.java
new file mode 100644
index 0000000..0d39e31
--- /dev/null
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetListResponse.java
@@ -0,0 +1,54 @@
+package org.xyzh.ai.client.dto;
+
+import lombok.Data;
+import java.util.List;
+
+/**
+ * @description 知识库列表响应
+ * @filename DatasetListResponse.java
+ * @author AI Assistant
+ * @copyright xyzh
+ * @since 2025-11-04
+ */
+@Data
+public class DatasetListResponse {
+
+ /**
+ * 知识库列表
+ */
+ private List data;
+
+ /**
+ * 是否有更多
+ */
+ private Boolean hasMore;
+
+ /**
+ * 分页限制
+ */
+ private Integer limit;
+
+ /**
+ * 总数
+ */
+ private Integer total;
+
+ /**
+ * 当前页
+ */
+ private Integer page;
+
+ @Data
+ public static class DatasetInfo {
+ private String id;
+ private String name;
+ private String description;
+ private String permission;
+ private Integer documentCount;
+ private Integer wordCount;
+ private String createdBy;
+ private Long createdAt;
+ private Long updatedAt;
+ }
+}
+
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetUpdateRequest.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetUpdateRequest.java
new file mode 100644
index 0000000..ba15a3e
--- /dev/null
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DatasetUpdateRequest.java
@@ -0,0 +1,25 @@
+package org.xyzh.ai.client.dto;
+
+import lombok.Data;
+
+/**
+ * @description Dify知识库更新请求
+ * @filename DatasetUpdateRequest.java
+ * @author AI Assistant
+ * @copyright xyzh
+ * @since 2025-11-04
+ */
+@Data
+public class DatasetUpdateRequest {
+
+ /**
+ * 知识库名称
+ */
+ private String name;
+
+ /**
+ * 知识库描述
+ */
+ private String description;
+}
+
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentListResponse.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentListResponse.java
new file mode 100644
index 0000000..38b535b
--- /dev/null
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentListResponse.java
@@ -0,0 +1,85 @@
+package org.xyzh.ai.client.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import java.util.List;
+
+/**
+ * @description 文档列表响应
+ * @filename DocumentListResponse.java
+ * @author AI Assistant
+ * @copyright xyzh
+ * @since 2025-11-04
+ */
+@Data
+public class DocumentListResponse {
+
+ private List data;
+
+ @JsonProperty("has_more")
+ private Boolean hasMore;
+
+ private Integer limit;
+
+ private Integer total;
+
+ private Integer page;
+
+ @Data
+ public static class DocumentInfo {
+ private String id;
+
+ private Integer position;
+
+ @JsonProperty("data_source_type")
+ private String dataSourceType;
+
+ @JsonProperty("data_source_info")
+ private DataSourceInfo dataSourceInfo;
+
+ @JsonProperty("dataset_process_rule_id")
+ private String datasetProcessRuleId;
+
+ private String name;
+
+ @JsonProperty("created_from")
+ private String createdFrom;
+
+ @JsonProperty("created_by")
+ private String createdBy;
+
+ @JsonProperty("created_at")
+ private Long createdAt;
+
+ @JsonProperty("indexing_status")
+ private String indexingStatus;
+
+ private String error;
+
+ private Boolean enabled;
+
+ @JsonProperty("disabled_at")
+ private Long disabledAt;
+
+ @JsonProperty("disabled_by")
+ private String disabledBy;
+
+ private Boolean archived;
+
+ @JsonProperty("word_count")
+ private Integer wordCount;
+
+ @JsonProperty("hit_count")
+ private Integer hitCount;
+
+ @JsonProperty("doc_form")
+ private String docForm;
+ }
+
+ @Data
+ public static class DataSourceInfo {
+ @JsonProperty("upload_file_id")
+ private String uploadFileId;
+ }
+}
+
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentStatusResponse.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentStatusResponse.java
new file mode 100644
index 0000000..4aa3fa5
--- /dev/null
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentStatusResponse.java
@@ -0,0 +1,95 @@
+package org.xyzh.ai.client.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import java.util.List;
+
+/**
+ * @description 文档处理状态响应
+ * @filename DocumentStatusResponse.java
+ * @author AI Assistant
+ * @copyright xyzh
+ * @since 2025-11-04
+ */
+@Data
+public class DocumentStatusResponse {
+
+ /**
+ * 文档列表
+ */
+ private List data;
+
+ @Data
+ public static class DocumentStatus {
+ /**
+ * 文档ID
+ */
+ private String id;
+
+ /**
+ * 索引状态:waiting、parsing、cleaning、splitting、indexing、completed、error
+ */
+ @JsonProperty("indexing_status")
+ private String indexingStatus;
+
+ /**
+ * 处理开始时间
+ */
+ @JsonProperty("processing_started_at")
+ private Long processingStartedAt;
+
+ /**
+ * 解析完成时间
+ */
+ @JsonProperty("parsing_completed_at")
+ private Long parsingCompletedAt;
+
+ /**
+ * 清洗完成时间
+ */
+ @JsonProperty("cleaning_completed_at")
+ private Long cleaningCompletedAt;
+
+ /**
+ * 分割完成时间
+ */
+ @JsonProperty("splitting_completed_at")
+ private Long splittingCompletedAt;
+
+ /**
+ * 完成时间
+ */
+ @JsonProperty("completed_at")
+ private Long completedAt;
+
+ /**
+ * 暂停时间
+ */
+ @JsonProperty("paused_at")
+ private Long pausedAt;
+
+ /**
+ * 错误信息
+ */
+ private String error;
+
+ /**
+ * 停止时间
+ */
+ @JsonProperty("stopped_at")
+ private Long stoppedAt;
+
+ /**
+ * 分段数量
+ */
+ @JsonProperty("completed_segments")
+ private Integer completedSegments;
+
+ /**
+ * 总分段数
+ */
+ @JsonProperty("total_segments")
+ private Integer totalSegments;
+ }
+}
+
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentUploadRequest.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentUploadRequest.java
new file mode 100644
index 0000000..36bd570
--- /dev/null
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentUploadRequest.java
@@ -0,0 +1,95 @@
+package org.xyzh.ai.client.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * @description 文档上传请求
+ * @filename DocumentUploadRequest.java
+ * @author AI Assistant
+ * @copyright xyzh
+ * @since 2025-11-04
+ */
+@Data
+public class DocumentUploadRequest {
+
+ /**
+ * 文档名称
+ */
+ private String name;
+
+ /**
+ * 索引方式
+ */
+ @JsonProperty("indexing_technique")
+ private String indexingTechnique;
+
+ /**
+ * 处理规则
+ */
+ @JsonProperty("process_rule")
+ private ProcessRule processRule;
+
+ @Data
+ public static class ProcessRule {
+ /**
+ * 分段模式:automatic(自动)、custom(自定义)
+ */
+ private String mode = "automatic";
+
+ /**
+ * 预处理规则
+ */
+ private Rules rules;
+
+ @Data
+ public static class Rules {
+ /**
+ * 自动分段配置
+ */
+ @JsonProperty("pre_processing_rules")
+ private PreProcessingRules preProcessingRules;
+
+ /**
+ * 分段配置
+ */
+ private Segmentation segmentation;
+ }
+
+ @Data
+ public static class PreProcessingRules {
+ /**
+ * 移除额外空格
+ */
+ @JsonProperty("remove_extra_spaces")
+ private Boolean removeExtraSpaces = true;
+
+ /**
+ * 移除URL和邮箱
+ */
+ @JsonProperty("remove_urls_emails")
+ private Boolean removeUrlsEmails = false;
+ }
+
+ @Data
+ public static class Segmentation {
+ /**
+ * 分隔符
+ */
+ private String separator = "\\n";
+
+ /**
+ * 最大分段长度
+ */
+ @JsonProperty("max_tokens")
+ private Integer maxTokens = 1000;
+
+ /**
+ * 分段重叠长度
+ */
+ @JsonProperty("chunk_overlap")
+ private Integer chunkOverlap = 50;
+ }
+ }
+}
+
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentUploadResponse.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentUploadResponse.java
new file mode 100644
index 0000000..8dba361
--- /dev/null
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/DocumentUploadResponse.java
@@ -0,0 +1,60 @@
+package org.xyzh.ai.client.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * @description 文档上传响应
+ * @filename DocumentUploadResponse.java
+ * @author AI Assistant
+ * @copyright xyzh
+ * @since 2025-11-04
+ */
+@Data
+public class DocumentUploadResponse {
+
+ /**
+ * 文档ID
+ */
+ private String id;
+
+ /**
+ * 文档名称
+ */
+ private String name;
+
+ /**
+ * 批次ID(用于查询处理状态)
+ */
+ private String batch;
+
+ /**
+ * 位置(序号)
+ */
+ private Integer position;
+
+ /**
+ * 数据源类型
+ */
+ @JsonProperty("data_source_type")
+ private String dataSourceType;
+
+ /**
+ * 索引状态
+ */
+ @JsonProperty("indexing_status")
+ private String indexingStatus;
+
+ /**
+ * 创建时间
+ */
+ @JsonProperty("created_at")
+ private Long createdAt;
+
+ /**
+ * 创建人
+ */
+ @JsonProperty("created_by")
+ private String createdBy;
+}
+
diff --git a/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/MessageHistoryResponse.java b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/MessageHistoryResponse.java
new file mode 100644
index 0000000..30ffc14
--- /dev/null
+++ b/schoolNewsServ/ai/src/main/java/org/xyzh/ai/client/dto/MessageHistoryResponse.java
@@ -0,0 +1,94 @@
+package org.xyzh.ai.client.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import java.util.List;
+
+/**
+ * @description 消息历史响应
+ * @filename MessageHistoryResponse.java
+ * @author AI Assistant
+ * @copyright xyzh
+ * @since 2025-11-04
+ */
+@Data
+public class MessageHistoryResponse {
+
+ private Integer limit;
+
+ @JsonProperty("has_more")
+ private Boolean hasMore;
+
+ private List data;
+
+ @Data
+ public static class MessageInfo {
+ private String id;
+
+ @JsonProperty("conversation_id")
+ private String conversationId;
+
+ private List inputs;
+
+ private String query;
+
+ private String answer;
+
+ @JsonProperty("message_files")
+ private List messageFiles;
+
+ private Feedback feedback;
+
+ @JsonProperty("retriever_resources")
+ private List retrieverResources;
+
+ @JsonProperty("created_at")
+ private Long createdAt;
+
+ @JsonProperty("agent_thoughts")
+ private List