mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-05-04 14:03:59 +00:00
Compare commits
26 Commits
v3.0.0
...
260503-fea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
410cb0b6f2 | ||
|
|
ec092a11c3 | ||
|
|
9a7b727413 | ||
|
|
b8d16b7669 | ||
|
|
058a4aee2a | ||
|
|
1b50c7f9f1 | ||
|
|
e7f53fd55f | ||
|
|
07bdc5e585 | ||
|
|
e1b8a5f011 | ||
|
|
80ca76ea37 | ||
|
|
2c6ff66830 | ||
|
|
4f79a66559 | ||
|
|
22883b4334 | ||
|
|
081da6d18d | ||
|
|
74eb5b2530 | ||
|
|
b0328fe0ef | ||
|
|
2ee0aae57e | ||
|
|
d9c3de660a | ||
|
|
ccbf5c9520 | ||
|
|
c4f7c1f5d0 | ||
|
|
1208c46cca | ||
|
|
06a63c377e | ||
|
|
0fa25032a3 | ||
|
|
28ad29d6ed | ||
|
|
b9097b4989 | ||
|
|
5d14eb20af |
63
.monkeycode/MEMORY.md
Normal file
63
.monkeycode/MEMORY.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# 用户指令记忆
|
||||
|
||||
本文件记录了用户的指令、偏好和教导,用于在未来的交互中提供参考。
|
||||
|
||||
## 格式
|
||||
|
||||
### 用户指令条目
|
||||
用户指令条目应遵循以下格式:
|
||||
|
||||
[用户指令摘要]
|
||||
- Date: [YYYY-MM-DD]
|
||||
- Context: [提及的场景或时间]
|
||||
- Instructions:
|
||||
- [用户教导或指示的内容,逐行描述]
|
||||
|
||||
### 项目知识条目
|
||||
Agent 在任务执行过程中发现的条目应遵循以下格式:
|
||||
|
||||
[项目知识摘要]
|
||||
- Date: [YYYY-MM-DD]
|
||||
- Context: Agent 在执行 [具体任务描述] 时发现
|
||||
- Category: [代码结构|代码模式|代码生成|构建方法|测试方法|依赖关系|环境配置]
|
||||
- Instructions:
|
||||
- [具体的知识点,逐行描述]
|
||||
|
||||
## 去重策略
|
||||
- 添加新条目前,检查是否存在相似或相同的指令
|
||||
- 若发现重复,跳过新条目或与已有条目合并
|
||||
- 合并时,更新上下文或日期信息
|
||||
- 这有助于避免冗余条目,保持记忆文件整洁
|
||||
|
||||
## 条目
|
||||
|
||||
[项目技术栈偏好:LangChain4j]
|
||||
- Date: 2026-05-03
|
||||
- Context: 用户在讨论“集成 AI 编程能力”方案时说明
|
||||
- Category: 依赖关系
|
||||
- Instructions:
|
||||
- 项目基于 LangChain4j 设计仓库级理解与自动化动作能力
|
||||
|
||||
[需求澄清:构建通用 Coding Agent]
|
||||
- Date: 2026-05-03
|
||||
- Context: 用户纠正方案范围时说明
|
||||
- Category: 代码结构
|
||||
- Instructions:
|
||||
- 目标是基于 LangChain4j 构建通用 coding agent,而非仅项目内问答助手
|
||||
- Agent 需要可操作文件、调用工具、跨前后端完成任务(如新建前端页面并对接现有后端)
|
||||
|
||||
[内置工具自动注册机制]
|
||||
- Date: 2026-05-03
|
||||
- Context: Agent 在执行 coding agent 工具扩展时发现
|
||||
- Category: 代码结构
|
||||
- Instructions:
|
||||
- ruoyi-chat 模块通过 BuiltinToolProvider + @Component 自动发现并注册内置工具
|
||||
- 新增工具无需手工改注册表,BuiltinToolRegistry 会在启动时扫描并创建可供 Agent 调用的实例
|
||||
|
||||
[新增能力方向:AI 报表 Agent]
|
||||
- Date: 2026-05-03
|
||||
- Context: 用户提出新的产品化需求
|
||||
- Category: 代码模式
|
||||
- Instructions:
|
||||
- 用户希望通过自然语言生成报表,包含数据库查询和 HTML 报表生成
|
||||
- 用户希望在报表页面内通过提示词继续动态编辑页面
|
||||
27
README.md
27
README.md
@@ -31,8 +31,8 @@
|
||||
|
||||
| 模块 | 现有能力
|
||||
|:----------:|---
|
||||
| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱)、多模态理解、Coze/DIFY/FastGPT平台集成
|
||||
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
|
||||
| **模型管理** | 多模型接入(OpenAI/DeepSeek/通义/智谱/MiniMax)、多模态理解、Coze/DIFY/FastGPT平台集成
|
||||
| **知识管理** | 本地RAG + 向量库(Milvus/Weaviate/Qdrant) + 文档解析
|
||||
| **工具管理** | Mcp协议集成、Skills能力 + 可扩展工具生态
|
||||
| **流程编排** | 可视化工作流设计器、节点拖拽编排、SSE流式执行,目前已经支持模型调用,邮件发送,人工审核等节点
|
||||
| **多智能体** | 基于Langchain4j的Agent框架、Supervisor模式编排,支持多种决策模型
|
||||
@@ -72,6 +72,29 @@
|
||||
- **实时通信**:WebSocket 实时通信,SSE 流式响应
|
||||
- **系统监控**:完善的日志体系、性能监控、服务健康检查
|
||||
|
||||
## 使用web coding急速部署
|
||||
#### 在线体验: https://monkeycode-ai.com/?ic=019d9e9f-edc3-7a4b-8987-11b028751a1e
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px;">
|
||||
<img src="docs/image/01.png" alt="web code" width="660" height="400"><br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px;">
|
||||
<img src="docs/image/02.png" alt="web code" width="660" height="400"><br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px;">
|
||||
<img src="docs/image/03.png" alt="web code" width="660" height="400"><br>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
#### 等待10分钟左右即可完成
|
||||
|
||||
## 🐳 Docker 部署
|
||||
|
||||
本项目提供两种 Docker 部署方式:
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
| Module | Current Capabilities |
|
||||
|:---:|---|
|
||||
| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu), multi-modal understanding, Coze/DIFY/FastGPT platform integration |
|
||||
| **Model Management** | Multi-model integration (OpenAI/DeepSeek/Tongyi/Zhipu/MiniMax), multi-modal understanding, Coze/DIFY/FastGPT platform integration |
|
||||
| **Knowledge Base** | Local RAG + Vector DB (Milvus/Weaviate/Qdrant) + Document parsing |
|
||||
| **Tool Management** | MCP protocol integration, Skills capability + Extensible tool ecosystem |
|
||||
| **Workflow Orchestration** | Visual workflow designer, drag-and-drop node orchestration, SSE streaming execution, currently supports model calls, email sending, manual review nodes |
|
||||
|
||||
@@ -65,7 +65,7 @@ services:
|
||||
- "28080:8080"
|
||||
environment:
|
||||
QUERY_DEFAULTS_LIMIT: 25
|
||||
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: true
|
||||
AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: "true"
|
||||
PERSISTENCE_DATA_PATH: /var/lib/weaviate
|
||||
DEFAULT_VECTORIZER_MODULE: none
|
||||
ENABLE_MODULES: text2vec-cohere,text2vec-huggingface,text2vec-palm,text2vec-openai,generative-openai,generative-cohere,generative-palm,ref2vec-centroid,reranker-cohere,qna-openai
|
||||
|
||||
BIN
docs/image/01.png
Normal file
BIN
docs/image/01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 298 KiB |
BIN
docs/image/02.png
Normal file
BIN
docs/image/02.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 185 KiB |
BIN
docs/image/03.png
Normal file
BIN
docs/image/03.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 316 KiB |
@@ -72,8 +72,9 @@ CREATE TABLE `chat_model` (
|
||||
-- ----------------------------
|
||||
-- Records of chat_model
|
||||
-- ----------------------------
|
||||
INSERT INTO `chat_model` VALUES (2000585866022060033, 'chat', 'deepseek/deepseek-v3.2', 'ppio', 'deepseek', NULL, 'Y', 'https://api.ppinfra.com/openai', 'sk_xx', 103, 1, '2025-12-15 23:16:54', 1, '2026-03-15 19:18:48', 'DeepSeek-V3.2 是一款在高效推理、复杂推理能力与智能体场景中表现突出的领先模型。其基于 DeepSeek Sparse Attention(DSA)稀疏注意力机制,在显著降低计算开销的同时优化长上下文性能;通过可扩展强化学习框架,整体能力达到 GPT-5 同级,高算力版本 V3.2-Speciale 更在推理表现上接近 Gemini-3.0-Pro;同时,模型依托大型智能体任务合成管线,具备更强的工具调用与多步骤决策能力,并在 2025 年 IMO 与 IOI 中取得金牌级表现。作为 MaaS 平台,我们已对 DeepSeek-V3.2 完成深度适配,通过动态调度、批处理加速、低延迟推理与企业级 SLA 保障,进一步增强其在企业生产环境中的稳定性、性价比与可控性,适用于搜索、问答、智能体、代码、数据处理等多类高价值场景。', 0);
|
||||
INSERT INTO `chat_model` VALUES (2007528268536287233, 'vector', 'baai/bge-m3', 'ppio', 'bge-m3', 1024, 'N', 'https://api.ppinfra.com/openai', 'sk_xx', 103, 1, '2026-01-04 03:03:32', 1, '2026-03-15 19:18:51', 'BGE-M3 是一款具备多维度能力的文本嵌入模型,可同时实现密集检索、多向量检索和稀疏检索三大核心功能。该模型设计上兼容超过100种语言,并支持从短句到长达8192词元的长文本等多种输入形式。在跨语言检索任务中,BGE-M3展现出显著优势,其性能在MIRACL、MKQA等国际基准测试中位居前列。此外,针对长文档检索场景,该模型在MLDR、NarritiveQA等数据集上的表现同样达到行业领先水平。', 0);
|
||||
INSERT INTO `chat_model` VALUES (2000585866022060033, 'chat', 'zai-org/glm-5', 'ppio', 'zai-org/glm-5', NULL, 'Y', 'https://api.ppio.com/openai', 'sk_xx', 103, 1, '2025-12-15 23:16:54', 1, '2026-03-15 19:18:48', 'DeepSeek-V3.2 是一款在高效推理、复杂推理能力与智能体场景中表现突出的领先模型。其基于 DeepSeek Sparse Attention(DSA)稀疏注意力机制,在显著降低计算开销的同时优化长上下文性能;通过可扩展强化学习框架,整体能力达到 GPT-5 同级,高算力版本 V3.2-Speciale 更在推理表现上接近 Gemini-3.0-Pro;同时,模型依托大型智能体任务合成管线,具备更强的工具调用与多步骤决策能力,并在 2025 年 IMO 与 IOI 中取得金牌级表现。作为 MaaS 平台,我们已对 DeepSeek-V3.2 完成深度适配,通过动态调度、批处理加速、低延迟推理与企业级 SLA 保障,进一步增强其在企业生产环境中的稳定性、性价比与可控性,适用于搜索、问答、智能体、代码、数据处理等多类高价值场景。', 0);
|
||||
INSERT INTO `chat_model` VALUES (2007528268536287233, 'vector', 'baai/bge-m3', 'ppio', 'bge-m3', 1024, 'N', 'https://api.ppio.com/openai', 'sk_xx', 103, 1, '2026-01-04 03:03:32', 1, '2026-03-15 19:18:51', 'BGE-M3 是一款具备多维度能力的文本嵌入模型,可同时实现密集检索、多向量检索和稀疏检索三大核心功能。该模型设计上兼容超过100种语言,并支持从短句到长达8192词元的长文本等多种输入形式。在跨语言检索任务中,BGE-M3展现出显著优势,其性能在MIRACL、MKQA等国际基准测试中位居前列。此外,针对长文档检索场景,该模型在MLDR、NarritiveQA等数据集上的表现同样达到行业领先水平。', 0);
|
||||
INSERT INTO `chat_model` VALUES (2045735140488847361, 'chat', 'deepseek-chat', 'custom_api', 'deepseek-chat', NULL, NULL, 'https://api.deepseek.com', 'sk_xx', 103, 1, '2026-04-19 13:24:00', 1, '2026-04-19 13:24:00', 'deepseek对话模型', 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for chat_provider
|
||||
@@ -95,22 +96,26 @@ CREATE TABLE `chat_provider` (
|
||||
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
|
||||
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
|
||||
`version` int NULL DEFAULT NULL COMMENT '版本',
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '删除标志',
|
||||
`update_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '更新IP',
|
||||
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `unique_provider_code`(`provider_code` ASC, `tenant_id` ASC) USING BTREE,
|
||||
UNIQUE INDEX `unique_provider_code`(`provider_code` ASC, `tenant_id` ASC, `del_flag` ASC) USING BTREE,
|
||||
INDEX `idx_status`(`status` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2008460994477690882 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '厂商管理表' ROW_FORMAT = DYNAMIC;
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2045727230803255298 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '厂商管理表' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of chat_provider
|
||||
-- ----------------------------
|
||||
INSERT INTO `chat_provider` VALUES (1, 'OpenAI', 'openai', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/01091be272334383a1efd9bc22b73ee6.png', 'OpenAI官方API服务商', 'https://api.openai.com', '0', 1, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:46:59', 'OpenAI厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (2, '阿里云百炼', 'qianwen', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/de2aa7e649de44f3ba5c6380ac6acd04.png', '阿里云百炼大模型服务', 'https://dashscope.aliyuncs.com', '0', 2, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:49:13', '阿里云厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (3, '智谱AI', 'zhipu', 'https://ruoyi-ai-1254149996.cos.ap-guangzhou.myqcloud.com/2025/12/15/a43e98fb7b3b4861b8caa6184e6fa40a.png', '智谱AI大模型服务', 'https://open.bigmodel.cn', '0', 3, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-06 00:49:14', '智谱AI厂商', NULL, '1', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (5, 'ollama', 'ollama', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/afecabebc8014d80b0f06b4796a74c5d.png', 'ollama大模型', 'http://127.0.0.1:11434', '0', 5, NULL, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:48:48', 'ollama厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (2000585060904435714, 'PPIO', 'ppio', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/049bb6a507174f73bba4b8d8b9e55b8a.png', 'api聚合厂商', 'https://api.ppinfra.com/openai', '0', 5, 103, '2025-12-15 23:13:42', '1', '1', '2026-02-25 20:49:01', 'api聚合厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (1, 'OpenAI', 'openai', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/01091be272334383a1efd9bc22b73ee6.png', 'OpenAI官方API服务商', 'https://api.openai.com', '0', 1, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:46:59', 'OpenAI厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (11, '深度求索', 'deepseek', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/5ba8c30f153246898a4d7dc7b846de8d.png', 'DeepSeek官方API', 'https://api.deepseek.com', '0', 0, 103, '2026-04-19 12:52:34', '1', '1', '2026-04-19 13:13:25', 'DeepSeek官方API', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (12, '智谱AI', 'zhipu', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/da071783c9284fdd9ed1ce1b57b3c75c.png', '智谱AI大模型服务', 'https://open.bigmodel.cn', '0', 4, 103, '2025-12-14 21:48:11', '1', '1', '2026-04-19 13:14:00', '智谱AI厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (13, '小米MIMO', 'xiaomi', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/18dd39365ce244e3ae5e030da036760e.png', '小米官方API', 'https://api.xiaomimimo.com/anthropic/v1/messages', '0', 3, 103, '2026-04-19 12:48:24', '1', '1', '2026-04-19 13:14:22', '小米官方API', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (14, '阿里云百炼', 'qianwen', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/de2aa7e649de44f3ba5c6380ac6acd04.png', '阿里云百炼大模型服务', 'https://dashscope.aliyuncs.com', '0', 2, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:49:13', '阿里云厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (15, 'PPIO', 'ppio', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/049bb6a507174f73bba4b8d8b9e55b8a.png', 'api聚合厂商', 'https://api.ppinfra.com/openai', '0', 5, 103, '2025-12-15 23:13:42', '1', '1', '2026-02-25 20:49:01', 'api聚合厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (16, 'MiniMax', 'minimax', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/fdc712e90e0e4d78b05862ad230884e5.png', 'MiniMax大模型服务,支持M2.7、M2.5等模型', 'https://api.minimax.io/v1', '0', 6, 103, '2026-04-19 12:50:12', '1', '1', '2026-04-19 13:14:59', 'MiniMax厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (17, 'ollama', 'ollama', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/afecabebc8014d80b0f06b4796a74c5d.png', 'ollama大模型', 'http://127.0.0.1:11434', '0', 7, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:48:48', 'ollama厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (18, '自定义厂商', 'custom_api', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/c1a8e122510f4e2f90deb36958af710b.png', 'OPENAI兼容格式', '自定义', '0', 8, 103, '2026-04-19 12:35:57', '1', '1', '2026-04-19 13:17:20', 'OPENAI兼容格式', NULL, '0', NULL, 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for chat_session
|
||||
|
||||
92
docs/script/sql/update/updat-0419.sql
Normal file
92
docs/script/sql/update/updat-0419.sql
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
Navicat Premium Dump SQL
|
||||
|
||||
Source Server : localhost-mysql
|
||||
Source Server Type : MySQL
|
||||
Source Server Version : 80045 (8.0.45)
|
||||
Source Host : localhost:3306
|
||||
Source Schema : ruoyi-ai
|
||||
|
||||
Target Server Type : MySQL
|
||||
Target Server Version : 80045 (8.0.45)
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 19/04/2026 13:36:41
|
||||
*/
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for chat_model
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `chat_model`;
|
||||
CREATE TABLE `chat_model` (
|
||||
`id` bigint NOT NULL COMMENT '主键',
|
||||
`category` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型分类',
|
||||
`model_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型名称',
|
||||
`provider_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型供应商',
|
||||
`model_describe` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型描述',
|
||||
`model_dimension` int NULL DEFAULT NULL COMMENT '模型维度',
|
||||
`model_show` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '是否显示',
|
||||
`api_host` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '请求地址',
|
||||
`api_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密钥',
|
||||
`create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门',
|
||||
`create_by` bigint NULL DEFAULT NULL COMMENT '创建者',
|
||||
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
|
||||
`update_by` bigint NULL DEFAULT NULL COMMENT '更新者',
|
||||
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
|
||||
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
|
||||
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '模型管理' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of chat_model
|
||||
-- ----------------------------
|
||||
INSERT INTO `chat_model` VALUES (2000585866022060033, 'chat', 'zai-org/glm-5', 'ppio', 'zai-org/glm-5', NULL, 'Y', 'https://api.ppio.com/openai', 'sk_xx', 103, 1, '2025-12-15 23:16:54', 1, '2026-03-15 19:18:48', 'DeepSeek-V3.2 是一款在高效推理、复杂推理能力与智能体场景中表现突出的领先模型。其基于 DeepSeek Sparse Attention(DSA)稀疏注意力机制,在显著降低计算开销的同时优化长上下文性能;通过可扩展强化学习框架,整体能力达到 GPT-5 同级,高算力版本 V3.2-Speciale 更在推理表现上接近 Gemini-3.0-Pro;同时,模型依托大型智能体任务合成管线,具备更强的工具调用与多步骤决策能力,并在 2025 年 IMO 与 IOI 中取得金牌级表现。作为 MaaS 平台,我们已对 DeepSeek-V3.2 完成深度适配,通过动态调度、批处理加速、低延迟推理与企业级 SLA 保障,进一步增强其在企业生产环境中的稳定性、性价比与可控性,适用于搜索、问答、智能体、代码、数据处理等多类高价值场景。', 0);
|
||||
INSERT INTO `chat_model` VALUES (2007528268536287233, 'vector', 'baai/bge-m3', 'ppio', 'bge-m3', 1024, 'N', 'https://api.ppio.com/openai', 'sk_xx', 103, 1, '2026-01-04 03:03:32', 1, '2026-03-15 19:18:51', 'BGE-M3 是一款具备多维度能力的文本嵌入模型,可同时实现密集检索、多向量检索和稀疏检索三大核心功能。该模型设计上兼容超过100种语言,并支持从短句到长达8192词元的长文本等多种输入形式。在跨语言检索任务中,BGE-M3展现出显著优势,其性能在MIRACL、MKQA等国际基准测试中位居前列。此外,针对长文档检索场景,该模型在MLDR、NarritiveQA等数据集上的表现同样达到行业领先水平。', 0);
|
||||
INSERT INTO `chat_model` VALUES (2045735140488847361, 'chat', 'deepseek-chat', 'custom_api', 'deepseek-chat', NULL, NULL, 'https://api.deepseek.com', 'sk_xx', 103, 1, '2026-04-19 13:24:00', 1, '2026-04-19 13:24:00', 'deepseek对话模型', 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for chat_provider
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `chat_provider`;
|
||||
CREATE TABLE `chat_provider` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`provider_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '厂商名称',
|
||||
`provider_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '厂商编码',
|
||||
`provider_icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '厂商图标',
|
||||
`provider_desc` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '厂商描述',
|
||||
`api_host` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'API地址',
|
||||
`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '状态(0正常 1停用)',
|
||||
`sort_order` int NULL DEFAULT 0 COMMENT '排序',
|
||||
`create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门',
|
||||
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
|
||||
`create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '创建者',
|
||||
`update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '更新者',
|
||||
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
|
||||
`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
|
||||
`version` int NULL DEFAULT NULL COMMENT '版本',
|
||||
`del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '删除标志',
|
||||
`update_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '更新IP',
|
||||
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户Id',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `unique_provider_code`(`provider_code` ASC, `tenant_id` ASC, `del_flag` ASC) USING BTREE,
|
||||
INDEX `idx_status`(`status` ASC) USING BTREE
|
||||
) ENGINE = InnoDB AUTO_INCREMENT = 2045727230803255298 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '厂商管理表' ROW_FORMAT = DYNAMIC;
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of chat_provider
|
||||
-- ----------------------------
|
||||
INSERT INTO `chat_provider` VALUES (1, 'OpenAI', 'openai', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/01091be272334383a1efd9bc22b73ee6.png', 'OpenAI官方API服务商', 'https://api.openai.com', '0', 1, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:46:59', 'OpenAI厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (11, '深度求索', 'deepseek', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/5ba8c30f153246898a4d7dc7b846de8d.png', 'DeepSeek官方API', 'https://api.deepseek.com', '0', 0, 103, '2026-04-19 12:52:34', '1', '1', '2026-04-19 13:13:25', 'DeepSeek官方API', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (12, '智谱AI', 'zhipu', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/da071783c9284fdd9ed1ce1b57b3c75c.png', '智谱AI大模型服务', 'https://open.bigmodel.cn', '0', 4, 103, '2025-12-14 21:48:11', '1', '1', '2026-04-19 13:14:00', '智谱AI厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (13, '小米MIMO', 'xiaomi', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/18dd39365ce244e3ae5e030da036760e.png', '小米官方API', 'https://api.xiaomimimo.com/anthropic/v1/messages', '0', 3, 103, '2026-04-19 12:48:24', '1', '1', '2026-04-19 13:14:22', '小米官方API', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (14, '阿里云百炼', 'qianwen', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/de2aa7e649de44f3ba5c6380ac6acd04.png', '阿里云百炼大模型服务', 'https://dashscope.aliyuncs.com', '0', 2, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:49:13', '阿里云厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (15, 'PPIO', 'ppio', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/049bb6a507174f73bba4b8d8b9e55b8a.png', 'api聚合厂商', 'https://api.ppinfra.com/openai', '0', 5, 103, '2025-12-15 23:13:42', '1', '1', '2026-02-25 20:49:01', 'api聚合厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (16, 'MiniMax', 'minimax', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/fdc712e90e0e4d78b05862ad230884e5.png', 'MiniMax大模型服务,支持M2.7、M2.5等模型', 'https://api.minimax.io/v1', '0', 6, 103, '2026-04-19 12:50:12', '1', '1', '2026-04-19 13:14:59', 'MiniMax厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (17, 'ollama', 'ollama', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/02/25/afecabebc8014d80b0f06b4796a74c5d.png', 'ollama大模型', 'http://127.0.0.1:11434', '0', 7, 103, '2025-12-14 21:48:11', '1', '1', '2026-02-25 20:48:48', 'ollama厂商', NULL, '0', NULL, 0);
|
||||
INSERT INTO `chat_provider` VALUES (18, '自定义厂商', 'custom_api', 'https://ruoyiai-1254149996.cos.ap-guangzhou.myqcloud.com/2026/04/19/c1a8e122510f4e2f90deb36958af710b.png', 'OPENAI兼容格式', '自定义', '0', 8, 103, '2026-04-19 12:35:57', '1', '1', '2026-04-19 13:17:20', 'OPENAI兼容格式', NULL, '0', NULL, 0);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
46
docs/script/sql/update/updat-0420.sql
Normal file
46
docs/script/sql/update/updat-0420.sql
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
Navicat Premium Dump SQL
|
||||
|
||||
Source Server : localhost-mysql
|
||||
Source Server Type : MySQL
|
||||
Source Server Version : 80045 (8.0.45)
|
||||
Source Host : localhost:3306
|
||||
Source Schema : ruoyi-ai
|
||||
|
||||
Target Server Type : MySQL
|
||||
Target Server Version : 80045 (8.0.45)
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 20/04/2026 15:30:00
|
||||
*/
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ----------------------------
|
||||
-- 新增:重排序模型(chat_model)
|
||||
-- ----------------------------
|
||||
INSERT INTO `chat_model`
|
||||
(id, category, model_name, provider_code, model_describe, model_dimension, model_show, api_host, api_key, create_dept, create_by, create_time, update_by, update_time, remark, tenant_id)
|
||||
VALUES(2045071617578237953, 'rerank', 'rerank', 'zhipu', '智谱重排序', NULL, 'Y', 'https://open.bigmodel.cn', 'e9xx', 103, 1, '2026-04-17 17:27:24', 1, '2026-04-20 15:21:48', '智谱重排序', 0);
|
||||
|
||||
INSERT INTO `chat_model`
|
||||
(id, category, model_name, provider_code, model_describe, model_dimension, model_show, api_host, api_key, create_dept, create_by, create_time, update_by, update_time, remark, tenant_id)
|
||||
VALUES(2046119803482902530, 'rerank', 'qwen3-rerank', 'qianwen', '千问3重排序', NULL, NULL, 'https://dashscope.aliyuncs.com', 'sk-xx', 103, 1, '2026-04-20 14:52:31', 1, '2026-04-20 15:03:13', '千问3文本重排序', 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- 新增:字典类型 - 重排序模型分类
|
||||
-- ----------------------------
|
||||
INSERT INTO `sys_dict_data`
|
||||
(dict_code, tenant_id, dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, create_dept, create_by, create_time, update_by, update_time, remark)
|
||||
VALUES(2045070879435259905, '000000', 4, '重排序', 'rerank', 'chat_model_category', NULL, '#000000', 'N', 103, 1, '2026-04-17 17:24:28', 1, '2026-04-19 01:02:20', '重排序模型');
|
||||
|
||||
-- ----------------------------
|
||||
-- 修改表:knowledge_info 增加重排序相关字段
|
||||
-- ----------------------------
|
||||
ALTER TABLE `knowledge_info` ADD COLUMN `enable_rerank` tinyint DEFAULT 0 NULL COMMENT '是否启用重排序(0否 1是)';
|
||||
ALTER TABLE `knowledge_info` ADD COLUMN `rerank_score_threshold` double NULL COMMENT '重排序相关性分数阈值';
|
||||
ALTER TABLE `knowledge_info` ADD COLUMN `rerank_top_n` int NULL COMMENT '重排序后返回的文档数量';
|
||||
ALTER TABLE `knowledge_info` ADD COLUMN `rerank_model` varchar(100) NULL COMMENT '重排序模型名称';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
14
docs/script/sql/update/updat-0423.sql
Normal file
14
docs/script/sql/update/updat-0423.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- 为知识库信息表新增检索配置字段 (剔除了已存在的重排字段)
|
||||
ALTER TABLE knowledge_info
|
||||
ADD COLUMN similarity_threshold DOUBLE DEFAULT 0.5 COMMENT '相似度阈值'
|
||||
AFTER retrieve_limit;
|
||||
|
||||
ALTER TABLE knowledge_info ADD COLUMN enable_hybrid tinyint(1) DEFAULT 0 COMMENT '是否启用混合检索';
|
||||
ALTER TABLE knowledge_info ADD COLUMN hybrid_alpha double DEFAULT 0.5 COMMENT '混合检索权重比例 (0.0=纯向量, 1.0=纯关键词)';
|
||||
|
||||
-- 为知识片段表增加全文索引及关联ID
|
||||
ALTER TABLE knowledge_fragment ADD COLUMN knowledge_id bigint COMMENT '知识库ID';
|
||||
ALTER TABLE knowledge_fragment ADD FULLTEXT INDEX ft_content (content) WITH PARSER ngram;
|
||||
|
||||
-- 为知识库附件表增加解析状态字段
|
||||
ALTER TABLE `knowledge_attach` ADD COLUMN `status` TINYINT DEFAULT 0 COMMENT '解析状态: 0待解析, 1解析中, 2已解析, 3解析失败';
|
||||
@@ -265,7 +265,7 @@ demo:
|
||||
# 是否开启演示模式(开启后所有写操作将被拦截)
|
||||
enabled: false
|
||||
# 提示消息
|
||||
message: "演示模式,不允许进行写操作"
|
||||
message: "演示模式,不允许操作"
|
||||
# 排除的路径(这些路径不受演示模式限制)
|
||||
excludes:
|
||||
- /login
|
||||
@@ -276,7 +276,9 @@ demo:
|
||||
- /chat/send
|
||||
- /system/session/**
|
||||
- /system/message/**
|
||||
|
||||
- /system/attach/**
|
||||
- /system/fragment/**
|
||||
- /system/info/**
|
||||
--- # warm-flow工作流配置
|
||||
warm-flow:
|
||||
# 是否开启工作流,默认true
|
||||
|
||||
320
ruoyi-admin/src/main/resources/static/report/index.html
Normal file
320
ruoyi-admin/src/main/resources/static/report/index.html
Normal file
@@ -0,0 +1,320 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Report Agent</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{--primary:#6366f1;--primary-hover:#4f46e5;--bg:#f8f9fa;--card:#fff;--border:#e2e8f0;--text:#1e293b;--text-muted:#64748b;--success:#22c55e;--warning:#f59e0b;--error:#ef4444;--shadow:0 1px 3px rgba(0,0,0,0.1);--radius:8px}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;min-height:100vh}
|
||||
.app{max-width:1200px;margin:0 auto;padding:20px}
|
||||
.header{text-align:center;margin-bottom:32px}
|
||||
.header h1{font-size:28px;background:linear-gradient(135deg,var(--primary),#a855f7);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||
.header p{color:var(--text-muted);margin-top:4px}
|
||||
.steps{display:flex;gap:8px;margin-bottom:24px;align-items:center}
|
||||
.step{flex:1;padding:12px 16px;background:var(--card);border:2px solid var(--border);border-radius:var(--radius);text-align:center;transition:all 0.3s}
|
||||
.step.active{border-color:var(--primary);background:#eef2ff}
|
||||
.step.done{border-color:var(--success);background:#f0fdf4}
|
||||
.step h3{font-size:14px;color:var(--text-muted);margin-bottom:2px}
|
||||
.step .num{font-size:24px;font-weight:700}
|
||||
.card{background:var(--card);border-radius:var(--radius);box-shadow:var(--shadow);padding:24px;margin-bottom:20px}
|
||||
.card h2{font-size:18px;margin-bottom:16px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||||
textarea{width:100%;padding:12px;border:1px solid var(--border);border-radius:var(--radius);resize:vertical;font-size:14px;line-height:1.5;font-family:inherit}
|
||||
textarea:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 3px rgba(99,102,241,0.1)}
|
||||
.model-select{display:flex;align-items:center;gap:12px;margin-bottom:16px}
|
||||
.model-select label{font-weight:500}
|
||||
.model-select select{padding:8px 12px;border:1px solid var(--border);border-radius:var(--radius);font-size:14px}
|
||||
.model-select input[type=number]{width:80px}
|
||||
.btn{padding:10px 24px;border:none;border-radius:var(--radius);font-size:14px;font-weight:600;cursor:pointer;transition:all 0.2s;display:inline-flex;align-items:center;gap:8px}
|
||||
.btn-primary{background:var(--primary);color:#fff}
|
||||
.btn-primary:hover{background:var(--primary-hover)}
|
||||
.btn-success{background:var(--success);color:#fff}
|
||||
.btn-success:hover{background:#16a34a}
|
||||
.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)}
|
||||
.btn-outline:hover{border-color:var(--primary);color:var(--primary)}
|
||||
.btn:disabled{opacity:0.6;cursor:not-allowed}
|
||||
.btn-group{display:flex;gap:8px;margin-top:16px}
|
||||
.sql-preview{background:#1e293b;color:#e2e8f0;padding:16px;border-radius:var(--radius);font-family:monospace;white-space:pre-wrap;overflow:auto;max-height:300px;line-height:1.5}
|
||||
.sql-block{background:#1e293b;color:#e2e8f0;padding:12px 16px;border-radius:var(--radius);margin:12px 0;font-family:monospace;overflow-x:auto}
|
||||
.sql-block .label{color:#94a3b8;font-size:12px;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px}
|
||||
.status-msg{padding:12px 16px;border-radius:var(--radius);margin:12px 0;font-size:14px;display:flex;align-items:center;gap:8px}
|
||||
.status-info{background:#e0f2fe;color:#0369a1}
|
||||
.status-success{background:#dcfce7;color:#15803d}
|
||||
.status-error{background:#fee2e2;color:#dc2626}
|
||||
.loader{display:inline-block;width:16px;height:16px;border:2px solid currentColor;border-right-color:transparent;border-radius:50%;animation:spin 0.6s linear infinite}
|
||||
@keyframes spin{to{transform:rotate(360deg)}}
|
||||
#report-container{display:none}
|
||||
#report-container iframe{width:100%;height:70vh;border:1px solid var(--border);border-radius:var(--radius)}
|
||||
.section{display:none}
|
||||
.section.active{display:block}
|
||||
.edit-bar{display:flex;gap:8px;margin-top:16px;align-items:center}
|
||||
.edit-bar input{flex:1;padding:10px 12px;border:1px solid var(--border);border-radius:var(--radius);font-size:14px}
|
||||
.edit-bar input:focus{outline:none;border-color:var(--primary)}
|
||||
.fade-in{animation:fadeIn 0.3s ease-in}
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<div class="header">
|
||||
<h1>AI Report Agent</h1>
|
||||
<p>自然语言描述需求,自动生成数据报表</p>
|
||||
</div>
|
||||
|
||||
<div class="steps">
|
||||
<div class="step active" id="step-1"><h3>Step 1</h3><div class="num" data-step="1">描述需求</div></div>
|
||||
<div class="step" id="step-2"><h3>Step 2</h3><div class="num" data-step="2">确认SQL</div></div>
|
||||
<div class="step" id="step-3"><h3>Step 3</h3><div class="num" data-step="3">查看报表</div></div>
|
||||
</div>
|
||||
|
||||
<div class="section active fade-in" id="section-prompt">
|
||||
<div class="card">
|
||||
<h2>输入报表需求</h2>
|
||||
<div class="model-select">
|
||||
<label>模型:</label>
|
||||
<select id="model-name" required>
|
||||
<option value="">-- 选择模型 --</option>
|
||||
</select>
|
||||
<label>最大行数:</label>
|
||||
<input type="number" id="max-rows" value="100" min="1" max="1000">
|
||||
</div>
|
||||
<textarea id="user-prompt" rows="4" placeholder="例如:查询最近30天注册的用户数量,按日期分组统计"></textarea>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary" id="btn-generate"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section fade-in" id="section-preview">
|
||||
<div class="card">
|
||||
<h2>SQL 预览与确认</h2>
|
||||
<div id="preview-info"></div>
|
||||
<div class="sql-preview" id="sql-code"></div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-success" id="btn-execute" disabled></button>
|
||||
<button class="btn btn-outline" id="btn-modify" disabled></button>
|
||||
<button class="btn btn-outline" id="btn-cancel"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section fade-in" id="section-report">
|
||||
<div id="report-container">
|
||||
<iframe id="report-frame" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>
|
||||
</div>
|
||||
<div class="edit-bar">
|
||||
<input type="text" id="edit-prompt" placeholder="输入修改要求,例如:把表格改成柱状图展示">
|
||||
<button class="btn btn-primary" id="btn-refine"></button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-outline" id="btn-back"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="status-area"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE = '';
|
||||
let currentReport = {
|
||||
model: '',
|
||||
title: '',
|
||||
summary: '',
|
||||
sql: '',
|
||||
html: '',
|
||||
queryResult: ''
|
||||
};
|
||||
|
||||
async function loadModels() {
|
||||
try {
|
||||
const res = await fetch(API_BASE + '/chat/model/list');
|
||||
const data = await res.json();
|
||||
const select = document.getElementById('model-name');
|
||||
if (data.code === 200 && data.rows) {
|
||||
data.rows.forEach(m => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = m.modelName || m.name;
|
||||
opt.textContent = m.modelName || m.name;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('无法加载模型列表,请手动填写', e);
|
||||
}
|
||||
}
|
||||
|
||||
function showStatus(msg, type) {
|
||||
const area = document.getElementById('status-area');
|
||||
area.innerHTML = '<div class="status-msg status-' + (type || 'info') + '">' + msg + '</div>';
|
||||
setTimeout(() => area.innerHTML = '', 5000);
|
||||
}
|
||||
|
||||
function setLoading(btn, loading) {
|
||||
if (loading) {
|
||||
btn.disabled = true;
|
||||
btn._orig = btn.innerHTML;
|
||||
btn.innerHTML = '<span class="loader"></span> 处理中...';
|
||||
} else {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = btn._orig || '';
|
||||
}
|
||||
}
|
||||
|
||||
function setStep(s) {
|
||||
document.querySelectorAll('.step').forEach(el => {
|
||||
const n = parseInt(el.dataset.step || el.querySelector('.num').dataset.step);
|
||||
el.classList.remove('active', 'done');
|
||||
if (n === s) el.classList.add('active');
|
||||
else if (n < s) el.classList.add('done');
|
||||
});
|
||||
document.querySelectorAll('.section').forEach(el => el.classList.remove('active'));
|
||||
document.getElementById(['section-prompt','section-preview','section-report'][s - 1]).classList.add('active');
|
||||
}
|
||||
|
||||
// Step 1: Generate SQL
|
||||
document.getElementById('btn-generate').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('btn-generate');
|
||||
const model = document.getElementById('model-name').value;
|
||||
const prompt = document.getElementById('user-prompt').value.trim();
|
||||
const maxRows = parseInt(document.getElementById('max-rows').value) || 100;
|
||||
|
||||
if (!model) { showStatus('请选择或填写模型名称', 'error'); return; }
|
||||
if (!prompt) { showStatus('请输入报表需求', 'error'); return; }
|
||||
|
||||
setLoading(btn, true);
|
||||
showStatus('正在生成 SQL 预览...', 'info');
|
||||
|
||||
try {
|
||||
const res = await fetch(API_BASE + '/chat/report/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model, prompt, maxRows })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.code !== 200 || !data.data) throw new Error(data.msg || '生成失败');
|
||||
|
||||
currentReport.model = model;
|
||||
currentReport.title = data.data.title;
|
||||
currentReport.summary = data.data.summary;
|
||||
currentReport.sql = data.data.sql;
|
||||
currentReport.html = data.data.html || '';
|
||||
currentReport.queryResult = data.data.queryResult || '';
|
||||
|
||||
document.getElementById('preview-info').innerHTML =
|
||||
'<p><strong>报表标题:</strong> ' + escapeHtml(currentReport.title) + '</p>' +
|
||||
'<p><strong>摘要:</strong> ' + escapeHtml(currentReport.summary) + '</p>';
|
||||
document.getElementById('sql-code').textContent = currentReport.sql;
|
||||
document.getElementById('btn-execute').disabled = false;
|
||||
document.getElementById('btn-modify').disabled = false;
|
||||
setStep(2);
|
||||
showStatus('SQL 生成成功,请确认后执行', 'success');
|
||||
} catch (e) {
|
||||
showStatus('生成失败: ' + e.message, 'error');
|
||||
} finally {
|
||||
setLoading(btn, false);
|
||||
}
|
||||
});
|
||||
|
||||
// Step 2: Execute
|
||||
document.getElementById('btn-execute').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('btn-execute');
|
||||
setLoading(btn, true);
|
||||
showStatus('正在执行查询并生成报表...', 'info');
|
||||
|
||||
try {
|
||||
const res = await fetch(API_BASE + '/chat/report/execute', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: currentReport.model,
|
||||
title: currentReport.title,
|
||||
summary: currentReport.summary,
|
||||
sql: currentReport.sql
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.code !== 200 || !data.data) throw new Error(data.msg || '执行失败');
|
||||
|
||||
currentReport.html = data.data.html;
|
||||
currentReport.queryResult = data.data.queryResult;
|
||||
|
||||
const container = document.getElementById('report-container');
|
||||
const frame = document.getElementById('report-frame');
|
||||
container.style.display = 'block';
|
||||
frame.contentWindow.document.open();
|
||||
frame.contentWindow.document.write(currentReport.html);
|
||||
frame.contentWindow.document.close();
|
||||
|
||||
setStep(3);
|
||||
showStatus('报表生成成功', 'success');
|
||||
} catch (e) {
|
||||
showStatus('执行失败: ' + e.message, 'error');
|
||||
} finally {
|
||||
setLoading(btn, false);
|
||||
}
|
||||
});
|
||||
|
||||
// Step 3: Refine
|
||||
document.getElementById('btn-refine').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('btn-refine');
|
||||
const prompt = document.getElementById('edit-prompt').value.trim();
|
||||
if (!prompt) { showStatus('请输入修改要求', 'error'); return; }
|
||||
|
||||
setLoading(btn, true);
|
||||
showStatus('正在优化报表...', 'info');
|
||||
|
||||
try {
|
||||
const res = await fetch(API_BASE + '/chat/report/refine', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: currentReport.model,
|
||||
prompt,
|
||||
html: currentReport.html,
|
||||
dataContext: currentReport.queryResult
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.code !== 200 || !data.data) throw new Error(data.msg || '修改失败');
|
||||
|
||||
currentReport.html = data.data.html;
|
||||
currentReport.summary = data.data.summary;
|
||||
|
||||
const frame = document.getElementById('report-frame');
|
||||
frame.contentWindow.document.open();
|
||||
frame.contentWindow.document.write(currentReport.html);
|
||||
frame.contentWindow.document.close();
|
||||
|
||||
document.getElementById('edit-prompt').value = '';
|
||||
showStatus('报表已更新', 'success');
|
||||
} catch (e) {
|
||||
showStatus('修改失败: ' + e.message, 'error');
|
||||
} finally {
|
||||
setLoading(btn, false);
|
||||
}
|
||||
});
|
||||
|
||||
// Back button
|
||||
document.getElementById('btn-cancel').addEventListener('click', () => { setStep(1); showStatus('已取消', 'info'); });
|
||||
document.getElementById('btn-back').addEventListener('click', () => { setStep(2); });
|
||||
document.getElementById('btn-modify').addEventListener('click', () => { setStep(1); document.getElementById('user-prompt').value = document.getElementById('edit-prompt').value || ''; });
|
||||
|
||||
function escapeHtml(s) {
|
||||
if (!s) return '';
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// Init
|
||||
loadModels();
|
||||
document.getElementById('btn-generate').textContent = '生成 SQL 预览';
|
||||
document.getElementById('btn-execute').textContent = '确认执行并生成报表';
|
||||
document.getElementById('btn-cancel').textContent = '返回修改';
|
||||
document.getElementById('btn-modify').textContent = '重新编辑需求';
|
||||
document.getElementById('btn-refine').textContent = '执行修改';
|
||||
document.getElementById('btn-back').textContent = '返回查看 SQL';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -10,6 +10,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.task.VirtualThreadTaskExecutor;
|
||||
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
@@ -22,6 +23,12 @@ import java.util.concurrent.*;
|
||||
@EnableConfigurationProperties(ThreadPoolProperties.class)
|
||||
public class ThreadPoolConfig {
|
||||
|
||||
private final ThreadPoolProperties properties;
|
||||
|
||||
public ThreadPoolConfig(ThreadPoolProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心线程数 = cpu 核心数 + 1
|
||||
*/
|
||||
@@ -54,6 +61,22 @@ public class ThreadPoolConfig {
|
||||
return scheduledThreadPoolExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 知识库解析专用异步线程池
|
||||
*/
|
||||
@Bean(name = "knowledgeParseExecutor")
|
||||
public ThreadPoolTaskExecutor knowledgeParseExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(core);
|
||||
executor.setMaxPoolSize(core * 2);
|
||||
executor.setQueueCapacity(properties.getQueueCapacity());
|
||||
executor.setKeepAliveSeconds(properties.getKeepAliveSeconds());
|
||||
executor.setThreadNamePrefix("knowledge-parse-pool-");
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁事件
|
||||
* 停止线程池
|
||||
|
||||
@@ -174,6 +174,23 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.ruoyi.agent;
|
||||
|
||||
import dev.langchain4j.agentic.Agent;
|
||||
import dev.langchain4j.service.SystemMessage;
|
||||
import dev.langchain4j.service.UserMessage;
|
||||
import dev.langchain4j.service.V;
|
||||
|
||||
/**
|
||||
* 通用 Coding Agent
|
||||
* 使用任务规划、文件操作与受控命令执行能力完成开发任务
|
||||
*/
|
||||
public interface CodingAgent {
|
||||
|
||||
@SystemMessage("""
|
||||
你是一个通用 coding agent,负责把用户的软件开发请求落地为可执行改动。
|
||||
|
||||
必须遵循:
|
||||
1. 先调用 task_planner 输出结构化计划与 approval token
|
||||
2. 获得用户确认后再执行 create_file、edit_file、run_command
|
||||
3. run_command 必须使用 task_planner 返回的 approval token 和 approval scope
|
||||
4. 优先最小改动,保持可验证、可回滚
|
||||
5. 输出最终变更摘要、验证结果与下一步建议
|
||||
""")
|
||||
@UserMessage("{{query}}")
|
||||
@Agent("通用 Coding Agent,支持任务规划、文件操作与受控命令执行")
|
||||
String execute(@V("query") String query);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.ruoyi.controller.chat;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.ruoyi.common.core.domain.R;
|
||||
import org.ruoyi.domain.dto.request.AiReportExecuteRequest;
|
||||
import org.ruoyi.domain.dto.request.AiReportGenerateRequest;
|
||||
import org.ruoyi.domain.dto.request.AiReportRefineRequest;
|
||||
import org.ruoyi.domain.dto.response.AiReportResponse;
|
||||
import org.ruoyi.service.report.IAiReportService;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/chat/report")
|
||||
public class AiReportController {
|
||||
|
||||
private final IAiReportService aiReportService;
|
||||
|
||||
@PostMapping("/generate")
|
||||
public R<AiReportResponse> generate(@RequestBody @Valid AiReportGenerateRequest request) {
|
||||
return R.ok(aiReportService.generate(request));
|
||||
}
|
||||
|
||||
@PostMapping("/execute")
|
||||
public R<AiReportResponse> execute(@RequestBody @Valid AiReportExecuteRequest request) {
|
||||
return R.ok(aiReportService.execute(request));
|
||||
}
|
||||
|
||||
@PostMapping("/refine")
|
||||
public R<AiReportResponse> refine(@RequestBody @Valid AiReportRefineRequest request) {
|
||||
return R.ok(aiReportService.refine(request));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.enums.ChatModeType;
|
||||
import org.ruoyi.enums.ModelType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@@ -23,6 +24,8 @@ import org.ruoyi.common.log.enums.BusinessType;
|
||||
import org.ruoyi.common.excel.utils.ExcelUtil;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
/**
|
||||
* 模型管理
|
||||
*
|
||||
@@ -55,6 +58,21 @@ public class ChatModelController extends BaseController {
|
||||
return R.ok(chatModelService.queryList(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型供应商枚举
|
||||
*/
|
||||
@GetMapping("/providerOptions")
|
||||
public R<List<LinkedHashMap<String, String>>> providerOptions() {
|
||||
List<LinkedHashMap<String, String>> options = new java.util.ArrayList<>();
|
||||
for (ChatModeType type : ChatModeType.values()) {
|
||||
LinkedHashMap<String, String> item = new LinkedHashMap<>();
|
||||
item.put("label", type.getDescription());
|
||||
item.put("value", type.getCode());
|
||||
options.add(item);
|
||||
}
|
||||
return R.ok(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出模型管理列表
|
||||
*/
|
||||
|
||||
@@ -110,6 +110,17 @@ public class KnowledgeAttachController extends BaseController {
|
||||
@PostMapping(value = "/upload")
|
||||
public R<String> upload(KnowledgeInfoUploadBo bo){
|
||||
knowledgeAttachService.upload(bo);
|
||||
return R.ok("上传知识库附件成功!");
|
||||
return R.ok("上传成功!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动解析附件内容
|
||||
*
|
||||
* @param id 附件ID
|
||||
*/
|
||||
@PostMapping("/parse/{id}")
|
||||
public R<Void> parse(@PathVariable Long id) {
|
||||
knowledgeAttachService.parse(id);
|
||||
return R.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import jakarta.validation.constraints.*;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.ruoyi.domain.bo.knowledge.KnowledgeFragmentBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeFragmentVo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeRetrievalVo;
|
||||
import org.ruoyi.service.knowledge.IKnowledgeFragmentService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
@@ -102,4 +103,12 @@ public class KnowledgeFragmentController extends BaseController {
|
||||
@PathVariable Long[] ids) {
|
||||
return toAjax(knowledgeFragmentService.deleteWithValidByIds(List.of(ids), true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检索测试
|
||||
*/
|
||||
@PostMapping("/retrieval")
|
||||
public R<List<KnowledgeRetrievalVo>> retrieval(@RequestBody KnowledgeFragmentBo bo) {
|
||||
return R.ok(knowledgeFragmentService.retrieval(bo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
package org.ruoyi.controller.knowledge;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.constraints.*;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.ruoyi.domain.bo.knowledge.KnowledgeGraphInstanceBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeGraphInstanceVo;
|
||||
import org.ruoyi.service.knowledge.IKnowledgeGraphInstanceService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
|
||||
import org.ruoyi.common.log.annotation.Log;
|
||||
import org.ruoyi.common.web.core.BaseController;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.common.core.domain.R;
|
||||
import org.ruoyi.common.core.validate.AddGroup;
|
||||
import org.ruoyi.common.core.validate.EditGroup;
|
||||
import org.ruoyi.common.log.enums.BusinessType;
|
||||
import org.ruoyi.common.excel.utils.ExcelUtil;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
|
||||
/**
|
||||
* 知识图谱实例
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-17
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/system/graphInstance")
|
||||
public class KnowledgeGraphInstanceController extends BaseController {
|
||||
|
||||
private final IKnowledgeGraphInstanceService knowledgeGraphInstanceService;
|
||||
|
||||
/**
|
||||
* 查询知识图谱实例列表
|
||||
*/
|
||||
@SaCheckPermission("system:graphInstance:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<KnowledgeGraphInstanceVo> list(KnowledgeGraphInstanceBo bo, PageQuery pageQuery) {
|
||||
return knowledgeGraphInstanceService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出知识图谱实例列表
|
||||
*/
|
||||
@SaCheckPermission("system:graphInstance:export")
|
||||
@Log(title = "知识图谱实例", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(KnowledgeGraphInstanceBo bo, HttpServletResponse response) {
|
||||
List<KnowledgeGraphInstanceVo> list = knowledgeGraphInstanceService.queryList(bo);
|
||||
ExcelUtil.exportExcel(list, "知识图谱实例", KnowledgeGraphInstanceVo.class, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识图谱实例详细信息
|
||||
*
|
||||
* @param id 主键
|
||||
*/
|
||||
@SaCheckPermission("system:graphInstance:query")
|
||||
@GetMapping("/{id}")
|
||||
public R<KnowledgeGraphInstanceVo> getInfo(@NotNull(message = "主键不能为空")
|
||||
@PathVariable Long id) {
|
||||
return R.ok(knowledgeGraphInstanceService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增知识图谱实例
|
||||
*/
|
||||
@SaCheckPermission("system:graphInstance:add")
|
||||
@Log(title = "知识图谱实例", businessType = BusinessType.INSERT)
|
||||
@RepeatSubmit()
|
||||
@PostMapping()
|
||||
public R<Void> add(@Validated(AddGroup.class) @RequestBody KnowledgeGraphInstanceBo bo) {
|
||||
return toAjax(knowledgeGraphInstanceService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改知识图谱实例
|
||||
*/
|
||||
@SaCheckPermission("system:graphInstance:edit")
|
||||
@Log(title = "知识图谱实例", businessType = BusinessType.UPDATE)
|
||||
@RepeatSubmit()
|
||||
@PutMapping()
|
||||
public R<Void> edit(@Validated(EditGroup.class) @RequestBody KnowledgeGraphInstanceBo bo) {
|
||||
return toAjax(knowledgeGraphInstanceService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除知识图谱实例
|
||||
*
|
||||
* @param ids 主键串
|
||||
*/
|
||||
@SaCheckPermission("system:graphInstance:remove")
|
||||
@Log(title = "知识图谱实例", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@NotEmpty(message = "主键不能为空")
|
||||
@PathVariable Long[] ids) {
|
||||
return toAjax(knowledgeGraphInstanceService.deleteWithValidByIds(List.of(ids), true));
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package org.ruoyi.controller.knowledge;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.constraints.*;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.ruoyi.domain.bo.knowledge.KnowledgeGraphSegmentBo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeGraphSegmentVo;
|
||||
import org.ruoyi.service.knowledge.IKnowledgeGraphSegmentService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.ruoyi.common.idempotent.annotation.RepeatSubmit;
|
||||
import org.ruoyi.common.log.annotation.Log;
|
||||
import org.ruoyi.common.web.core.BaseController;
|
||||
import org.ruoyi.common.mybatis.core.page.PageQuery;
|
||||
import org.ruoyi.common.core.domain.R;
|
||||
import org.ruoyi.common.core.validate.AddGroup;
|
||||
import org.ruoyi.common.core.validate.EditGroup;
|
||||
import org.ruoyi.common.log.enums.BusinessType;
|
||||
import org.ruoyi.common.excel.utils.ExcelUtil;
|
||||
import org.ruoyi.common.mybatis.core.page.TableDataInfo;
|
||||
|
||||
/**
|
||||
* 知识图谱片段
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-17
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/system/graphSegment")
|
||||
public class KnowledgeGraphSegmentController extends BaseController {
|
||||
|
||||
private final IKnowledgeGraphSegmentService knowledgeGraphSegmentService;
|
||||
|
||||
/**
|
||||
* 查询知识图谱片段列表
|
||||
*/
|
||||
@SaCheckPermission("system:graphSegment:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<KnowledgeGraphSegmentVo> list(KnowledgeGraphSegmentBo bo, PageQuery pageQuery) {
|
||||
return knowledgeGraphSegmentService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出知识图谱片段列表
|
||||
*/
|
||||
@SaCheckPermission("system:graphSegment:export")
|
||||
@Log(title = "知识图谱片段", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(KnowledgeGraphSegmentBo bo, HttpServletResponse response) {
|
||||
List<KnowledgeGraphSegmentVo> list = knowledgeGraphSegmentService.queryList(bo);
|
||||
ExcelUtil.exportExcel(list, "知识图谱片段", KnowledgeGraphSegmentVo.class, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识图谱片段详细信息
|
||||
*
|
||||
* @param id 主键
|
||||
*/
|
||||
@SaCheckPermission("system:graphSegment:query")
|
||||
@GetMapping("/{id}")
|
||||
public R<KnowledgeGraphSegmentVo> getInfo(@NotNull(message = "主键不能为空")
|
||||
@PathVariable Long id) {
|
||||
return R.ok(knowledgeGraphSegmentService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增知识图谱片段
|
||||
*/
|
||||
@SaCheckPermission("system:graphSegment:add")
|
||||
@Log(title = "知识图谱片段", businessType = BusinessType.INSERT)
|
||||
@RepeatSubmit()
|
||||
@PostMapping()
|
||||
public R<Void> add(@Validated(AddGroup.class) @RequestBody KnowledgeGraphSegmentBo bo) {
|
||||
return toAjax(knowledgeGraphSegmentService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改知识图谱片段
|
||||
*/
|
||||
@SaCheckPermission("system:graphSegment:edit")
|
||||
@Log(title = "知识图谱片段", businessType = BusinessType.UPDATE)
|
||||
@RepeatSubmit()
|
||||
@PutMapping()
|
||||
public R<Void> edit(@Validated(EditGroup.class) @RequestBody KnowledgeGraphSegmentBo bo) {
|
||||
return toAjax(knowledgeGraphSegmentService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除知识图谱片段
|
||||
*
|
||||
* @param ids 主键串
|
||||
*/
|
||||
@SaCheckPermission("system:graphSegment:remove")
|
||||
@Log(title = "知识图谱片段", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@NotEmpty(message = "主键不能为空")
|
||||
@PathVariable Long[] ids) {
|
||||
return toAjax(knowledgeGraphSegmentService.deleteWithValidByIds(List.of(ids), true));
|
||||
}
|
||||
}
|
||||
@@ -49,5 +49,44 @@ public class KnowledgeFragmentBo extends BaseEntity {
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 知识库ID
|
||||
*/
|
||||
private Long knowledgeId;
|
||||
|
||||
/**
|
||||
* 检索内容
|
||||
*/
|
||||
private String query;
|
||||
|
||||
/**
|
||||
* 返回条数
|
||||
*/
|
||||
private Integer topK;
|
||||
|
||||
/**
|
||||
* 相似度阈值
|
||||
*/
|
||||
private Double threshold;
|
||||
|
||||
/**
|
||||
* 是否启用重排
|
||||
*/
|
||||
private Boolean enableRerank;
|
||||
|
||||
/**
|
||||
* 重排模型名称
|
||||
*/
|
||||
private String rerankModel;
|
||||
|
||||
/**
|
||||
* 是否启用混合检索
|
||||
*/
|
||||
private Boolean enableHybrid;
|
||||
|
||||
/**
|
||||
* 混合检索权重 (0.0-1.0)
|
||||
*/
|
||||
private Double hybridAlpha;
|
||||
|
||||
}
|
||||
|
||||
@@ -62,6 +62,11 @@ public class KnowledgeInfoBo extends BaseEntity {
|
||||
*/
|
||||
private Long retrieveLimit;
|
||||
|
||||
/**
|
||||
* 相似度阈值
|
||||
*/
|
||||
private Double similarityThreshold;
|
||||
|
||||
/**
|
||||
* 文本块大小
|
||||
*/
|
||||
@@ -77,10 +82,40 @@ public class KnowledgeInfoBo extends BaseEntity {
|
||||
*/
|
||||
private String embeddingModel;
|
||||
|
||||
/**
|
||||
* 是否启用重排序(0 否 1是)
|
||||
*/
|
||||
private Integer enableRerank;
|
||||
|
||||
/**
|
||||
* 重排序模型名称
|
||||
*/
|
||||
private String rerankModel;
|
||||
|
||||
/**
|
||||
* 重排序后返回的文档数量
|
||||
*/
|
||||
private Integer rerankTopN;
|
||||
|
||||
/**
|
||||
* 重排序相关性分数阈值
|
||||
*/
|
||||
private Double rerankScoreThreshold;
|
||||
|
||||
|
||||
/**
|
||||
* 是否启用混合检索(0 否 1是)
|
||||
*/
|
||||
private Integer enableHybrid;
|
||||
|
||||
/**
|
||||
* 混合检索权重 (0.0-1.0)
|
||||
*/
|
||||
private Double hybridAlpha;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ public class KnowledgeInfoUploadBo {
|
||||
|
||||
private MultipartFile file;
|
||||
|
||||
/**
|
||||
* 是否自动解析 (true: 立即解析, false: 仅上传)
|
||||
*/
|
||||
private Boolean autoParse;
|
||||
|
||||
/**
|
||||
* 生效时间, 为空则立即生效
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.ruoyi.domain.bo.rerank;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 重排序请求参数
|
||||
*
|
||||
* @author yang
|
||||
* @date 2026-04-19
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RerankRequest {
|
||||
|
||||
/**
|
||||
* 查询文本
|
||||
*/
|
||||
private String query;
|
||||
|
||||
/**
|
||||
* 候选文档列表
|
||||
*/
|
||||
private List<String> documents;
|
||||
|
||||
/**
|
||||
* 返回的文档数量(topN)
|
||||
* 如果不指定,默认返回所有文档
|
||||
*/
|
||||
private Integer topN;
|
||||
|
||||
/**
|
||||
* 是否返回原始文档内容
|
||||
* 默认为 true
|
||||
*/
|
||||
@Builder.Default
|
||||
private Boolean returnDocuments = true;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.ruoyi.domain.bo.rerank;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 重排序结果
|
||||
*
|
||||
* @author yang
|
||||
* @date 2026-04-19
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RerankResult {
|
||||
|
||||
/**
|
||||
* 重排序后的文档结果列表
|
||||
*/
|
||||
private List<RerankDocument> documents;
|
||||
|
||||
/**
|
||||
* 原始请求中的文档总数
|
||||
*/
|
||||
private Integer totalDocuments;
|
||||
|
||||
/**
|
||||
* 重排序耗时(毫秒)
|
||||
*/
|
||||
private Long durationMs;
|
||||
|
||||
/**
|
||||
* 单个重排序文档结果
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class RerankDocument {
|
||||
|
||||
/**
|
||||
* 文档在原始列表中的索引位置
|
||||
*/
|
||||
private Integer index;
|
||||
|
||||
/**
|
||||
* 相关性分数(通常 0-1 之间,越高越相关)
|
||||
*/
|
||||
private Double relevanceScore;
|
||||
|
||||
/**
|
||||
* 文档内容
|
||||
*/
|
||||
private String document;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建空结果
|
||||
*/
|
||||
public static RerankResult empty() {
|
||||
return RerankResult.builder()
|
||||
.documents(List.of())
|
||||
.totalDocuments(0)
|
||||
.durationMs(0L)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -51,4 +51,48 @@ public class QueryVectorBo {
|
||||
*/
|
||||
private String baseUrl;
|
||||
|
||||
|
||||
// ========== 重排序相关参数 ==========
|
||||
|
||||
/**
|
||||
* 是否启用重排序
|
||||
* 默认为 false
|
||||
*/
|
||||
private Boolean enableRerank = false;
|
||||
|
||||
/**
|
||||
* 重排序模型名称
|
||||
*/
|
||||
private String rerankModelName;
|
||||
|
||||
/**
|
||||
* 重排序后返回的文档数量(topN)
|
||||
* 如果不指定,默认与 maxResults 相同
|
||||
*/
|
||||
private Integer rerankTopN;
|
||||
|
||||
/**
|
||||
* 重排序相关性分数阈值
|
||||
* 低于此阈值的文档将被过滤
|
||||
*/
|
||||
private Double rerankScoreThreshold;
|
||||
|
||||
// ========== 混合检索与阈值相关参数 ==========
|
||||
|
||||
/**
|
||||
* 相似度阈值 (0.0-1.0)
|
||||
* 应用于向量搜索阶段
|
||||
*/
|
||||
private Double similarityThreshold;
|
||||
|
||||
/**
|
||||
* 是否启用混合检索
|
||||
*/
|
||||
private Boolean enableHybrid = false;
|
||||
|
||||
/**
|
||||
* 混合检索权重 (0.0-1.0)
|
||||
*/
|
||||
private Double hybridAlpha;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.ruoyi.domain.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AiReportExecuteRequest {
|
||||
|
||||
@NotBlank(message = "模型不能为空")
|
||||
private String model;
|
||||
|
||||
@NotBlank(message = "报表标题不能为空")
|
||||
private String title;
|
||||
|
||||
@NotBlank(message = "报表摘要不能为空")
|
||||
private String summary;
|
||||
|
||||
@NotBlank(message = "SQL 不能为空")
|
||||
private String sql;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package org.ruoyi.domain.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AiReportGenerateRequest {
|
||||
|
||||
@NotBlank(message = "模型不能为空")
|
||||
private String model;
|
||||
|
||||
@NotBlank(message = "报表需求不能为空")
|
||||
private String prompt;
|
||||
|
||||
private Integer maxRows;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.ruoyi.domain.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AiReportRefineRequest {
|
||||
|
||||
@NotBlank(message = "模型不能为空")
|
||||
private String model;
|
||||
|
||||
@NotBlank(message = "编辑提示词不能为空")
|
||||
private String prompt;
|
||||
|
||||
@NotBlank(message = "当前报表 HTML 不能为空")
|
||||
private String html;
|
||||
|
||||
private String dataContext;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package org.ruoyi.domain.dto.request;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 阿里百炼重排序请求DTO(OpenAI兼容格式)
|
||||
*
|
||||
* @author yang
|
||||
* @date 2026-04-20
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public record AliBaiLianRerankRequest(
|
||||
String model,
|
||||
List<String> documents,
|
||||
String query,
|
||||
@JsonProperty("top_n")
|
||||
Integer topN,
|
||||
String instruct,
|
||||
@JsonProperty("return_documents")
|
||||
Boolean returnDocuments
|
||||
) {
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 创建文本重排序请求
|
||||
*/
|
||||
public static AliBaiLianRerankRequest create(String modelName, String query,
|
||||
List<String> documents, Integer topN,
|
||||
Boolean returnDocuments) {
|
||||
return new AliBaiLianRerankRequest(
|
||||
modelName,
|
||||
documents,
|
||||
query,
|
||||
topN != null ? topN : documents.size(),
|
||||
null,
|
||||
returnDocuments != null ? returnDocuments : true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为JSON字符串
|
||||
*/
|
||||
public String toJson() {
|
||||
try {
|
||||
return OBJECT_MAPPER.writeValueAsString(this);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("序列化阿里百炼重排序请求失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.ruoyi.domain.dto.request;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 智谱AI重排序请求DTO
|
||||
*
|
||||
* @author yang
|
||||
* @date 2026-04-19
|
||||
*/
|
||||
public record ZhipuRerankRequest(
|
||||
String model,
|
||||
String query,
|
||||
List<String> documents,
|
||||
Integer top_n,
|
||||
Boolean return_documents
|
||||
) {
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 创建智谱重排序请求
|
||||
*/
|
||||
public static ZhipuRerankRequest create(String modelName, String query,
|
||||
List<String> documents, Integer topN,
|
||||
Boolean returnDocuments) {
|
||||
return new ZhipuRerankRequest(
|
||||
modelName,
|
||||
query,
|
||||
documents,
|
||||
topN != null ? topN : documents.size(),
|
||||
returnDocuments != null ? returnDocuments : true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为JSON字符串
|
||||
*/
|
||||
public String toJson() {
|
||||
try {
|
||||
return OBJECT_MAPPER.writeValueAsString(this);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalArgumentException("序列化智谱重排序请求失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.ruoyi.domain.dto.response;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class AiReportResponse {
|
||||
|
||||
private String sql;
|
||||
|
||||
private String title;
|
||||
|
||||
private String summary;
|
||||
|
||||
private String queryResult;
|
||||
|
||||
private String html;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.ruoyi.domain.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.ruoyi.domain.bo.rerank.RerankResult;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 阿里百炼重排序响应DTO(OpenAI兼容格式)
|
||||
*
|
||||
* @author yang
|
||||
* @date 2026-04-20
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record AliBaiLianRerankResponse(
|
||||
String id,
|
||||
String object,
|
||||
List<ResultItem> results,
|
||||
UsageInfo usage
|
||||
) {
|
||||
/**
|
||||
* 单个重排序结果项
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record ResultItem(
|
||||
Integer index,
|
||||
@JsonProperty("relevance_score")
|
||||
Double relevanceScore,
|
||||
Object document
|
||||
) {
|
||||
/**
|
||||
* 获取文档文本内容
|
||||
*/
|
||||
public String getDocumentText() {
|
||||
if (document == null) return null;
|
||||
if (document instanceof String) return (String) document;
|
||||
if (document instanceof Map) {
|
||||
Object text = ((Map<?, ?>) document).get("text");
|
||||
return text != null ? text.toString() : null;
|
||||
}
|
||||
return document.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Token使用信息
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record UsageInfo(
|
||||
@JsonProperty("total_tokens")
|
||||
Integer totalTokens,
|
||||
@JsonProperty("prompt_tokens")
|
||||
Integer promptTokens
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 转换为通用RerankResult
|
||||
*/
|
||||
public RerankResult toRerankResult(int totalDocs, long durationMs) {
|
||||
if (results == null || results.isEmpty()) {
|
||||
return RerankResult.empty();
|
||||
}
|
||||
|
||||
List<RerankResult.RerankDocument> documents = results.stream()
|
||||
.map(item -> RerankResult.RerankDocument.builder()
|
||||
.index(item.index())
|
||||
.relevanceScore(item.relevanceScore())
|
||||
.document(item.getDocumentText())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return RerankResult.builder()
|
||||
.documents(documents)
|
||||
.totalDocuments(totalDocs)
|
||||
.durationMs(durationMs)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.ruoyi.domain.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.ruoyi.domain.bo.rerank.RerankResult;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 智谱AI重排序响应DTO
|
||||
*
|
||||
* @author yang
|
||||
* @date 2026-04-19
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record ZhipuRerankResponse(
|
||||
String model,
|
||||
String object,
|
||||
List<ResultItem> results,
|
||||
UsageInfo usage
|
||||
) {
|
||||
/**
|
||||
* 单个重排序结果项
|
||||
*/
|
||||
public record ResultItem(
|
||||
Integer index,
|
||||
@JsonProperty("relevance_score")
|
||||
Double relevanceScore,
|
||||
String document
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Token使用信息
|
||||
*/
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record UsageInfo(
|
||||
@JsonProperty("total_tokens")
|
||||
Integer totalTokens,
|
||||
@JsonProperty("input_tokens")
|
||||
Integer inputTokens,
|
||||
@JsonProperty("output_tokens")
|
||||
Integer outputTokens
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 转换为通用RerankResult
|
||||
*/
|
||||
public RerankResult toRerankResult(int totalDocs, long durationMs) {
|
||||
if (results == null || results.isEmpty()) {
|
||||
return RerankResult.empty();
|
||||
}
|
||||
|
||||
List<RerankResult.RerankDocument> documents = results.stream()
|
||||
.map(item -> RerankResult.RerankDocument.builder()
|
||||
.index(item.index())
|
||||
.relevanceScore(item.relevanceScore())
|
||||
.document(item.document())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return RerankResult.builder()
|
||||
.documents(documents)
|
||||
.totalDocuments(totalDocs)
|
||||
.durationMs(durationMs)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -57,5 +57,10 @@ public class KnowledgeAttach extends BaseEntity {
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 解析状态: 0待解析, 1解析中, 2已解析, 3解析失败
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -47,5 +47,10 @@ public class KnowledgeFragment extends BaseEntity {
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 知识库ID
|
||||
*/
|
||||
private Long knowledgeId;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -63,6 +63,11 @@ public class KnowledgeInfo extends BaseEntity {
|
||||
*/
|
||||
private Long retrieveLimit;
|
||||
|
||||
/**
|
||||
* 相似度阈值
|
||||
*/
|
||||
private Double similarityThreshold;
|
||||
|
||||
/**
|
||||
* 文本块大小
|
||||
*/
|
||||
@@ -78,6 +83,36 @@ public class KnowledgeInfo extends BaseEntity {
|
||||
*/
|
||||
private String embeddingModel;
|
||||
|
||||
/**
|
||||
* 是否启用重排序(0 否 1是)
|
||||
*/
|
||||
private Integer enableRerank;
|
||||
|
||||
/**
|
||||
* 重排序模型名称
|
||||
*/
|
||||
private String rerankModel;
|
||||
|
||||
/**
|
||||
* 重排序后返回的文档数量
|
||||
*/
|
||||
private Integer rerankTopN;
|
||||
|
||||
/**
|
||||
* 重排序相关性分数阈值
|
||||
*/
|
||||
private Double rerankScoreThreshold;
|
||||
|
||||
/**
|
||||
* 是否启用混合检索(0 否 1是)
|
||||
*/
|
||||
private Integer enableHybrid;
|
||||
|
||||
/**
|
||||
* 混合检索权重 (0.0-1.0)
|
||||
*/
|
||||
private Double hybridAlpha;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.ruoyi.domain.vo.knowledge;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 文档分块数统计 VO(用于 GROUP BY 查询结果接收)
|
||||
*/
|
||||
@Data
|
||||
public class DocFragmentCountVo {
|
||||
|
||||
/**
|
||||
* 文档ID(关联 knowledge_attach.doc_id)
|
||||
*/
|
||||
private String docId;
|
||||
|
||||
/**
|
||||
* 该文档下的分块数量
|
||||
*/
|
||||
private Integer fragmentCount;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import org.ruoyi.domain.entity.knowledge.KnowledgeAttach;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
|
||||
@@ -68,5 +69,22 @@ public class KnowledgeAttachVo implements Serializable {
|
||||
@ExcelProperty(value = "备注")
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 上传时间(来自 BaseEntity.createTime)
|
||||
*/
|
||||
@ExcelProperty(value = "上传时间")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 解析状态: 0待解析, 1解析中, 2已解析, 3解析失败
|
||||
*/
|
||||
@ExcelProperty(value = "解析状态")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 分块数(统计字段,非数据库列)
|
||||
*/
|
||||
private Integer fragmentCount;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ public class KnowledgeFragmentVo implements Serializable {
|
||||
* 片段索引下标
|
||||
*/
|
||||
@ExcelProperty(value = "片段索引下标")
|
||||
private Long idx;
|
||||
private Integer idx;
|
||||
|
||||
/**
|
||||
* 文档内容
|
||||
@@ -53,5 +53,10 @@ public class KnowledgeFragmentVo implements Serializable {
|
||||
@ExcelProperty(value = "备注")
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 知识库ID
|
||||
*/
|
||||
private Long knowledgeId;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -76,6 +76,12 @@ public class KnowledgeInfoVo implements Serializable {
|
||||
@ExcelProperty(value = "知识库中检索的条数")
|
||||
private Integer retrieveLimit;
|
||||
|
||||
/**
|
||||
* 相似度阈值
|
||||
*/
|
||||
@ExcelProperty(value = "相似度阈值")
|
||||
private Double similarityThreshold;
|
||||
|
||||
/**
|
||||
* 文本块大小
|
||||
*/
|
||||
@@ -94,6 +100,48 @@ public class KnowledgeInfoVo implements Serializable {
|
||||
@ExcelProperty(value = "向量模型")
|
||||
private String embeddingModel;
|
||||
|
||||
/**
|
||||
* 是否启用重排序(0 否 1是)
|
||||
*/
|
||||
@ExcelProperty(value = "是否启用重排序")
|
||||
private Integer enableRerank;
|
||||
|
||||
/**
|
||||
* 重排序模型名称
|
||||
*/
|
||||
@ExcelProperty(value = "重排序模型")
|
||||
private String rerankModel;
|
||||
|
||||
/**
|
||||
* 重排序后返回的文档数量
|
||||
*/
|
||||
@ExcelProperty(value = "重排序返回数量")
|
||||
private Integer rerankTopN;
|
||||
|
||||
/**
|
||||
* 重排序相关性分数阈值
|
||||
*/
|
||||
@ExcelProperty(value = "重排序分数阈值")
|
||||
private Double rerankScoreThreshold;
|
||||
|
||||
/**
|
||||
* 是否启用混合检索(0 否 1是)
|
||||
*/
|
||||
@ExcelProperty(value = "是否启用混合检索")
|
||||
private Integer enableHybrid;
|
||||
|
||||
/**
|
||||
* 混合检索权重 (0.0-1.0)
|
||||
*/
|
||||
@ExcelProperty(value = "混合检索权重")
|
||||
private Double hybridAlpha;
|
||||
|
||||
/**
|
||||
* 文档数量
|
||||
*/
|
||||
@ExcelProperty(value = "文档数量")
|
||||
private Integer documentCount;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.ruoyi.domain.vo.knowledge;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 知识检索测试结果视图对象
|
||||
*
|
||||
* @author RobustH
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class KnowledgeRetrievalVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 片段ID
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 文档ID
|
||||
*/
|
||||
private String docId;
|
||||
|
||||
/**
|
||||
* 知识库ID
|
||||
*/
|
||||
private Long knowledgeId;
|
||||
|
||||
/**
|
||||
* 分片索引
|
||||
*/
|
||||
private Integer idx;
|
||||
|
||||
/**
|
||||
* 片段内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 相似度得分
|
||||
*/
|
||||
private Double score;
|
||||
|
||||
/**
|
||||
* 原始检索排名 (重排前)
|
||||
*/
|
||||
private Integer originalIndex;
|
||||
|
||||
/**
|
||||
* 原始检索得分 (重排前)
|
||||
*/
|
||||
private Double rawScore;
|
||||
|
||||
/**
|
||||
* 来源文档名称
|
||||
*/
|
||||
private String sourceName;
|
||||
}
|
||||
@@ -15,7 +15,10 @@ public enum ChatModeType {
|
||||
DEEP_SEEK("deepseek", "深度求索"),
|
||||
QIAN_WEN("qianwen", "通义千问"),
|
||||
OPEN_AI("openai", "openai"),
|
||||
PPIO("ppio", "ppio");
|
||||
PPIO("ppio", "ppio"),
|
||||
CUSTOM_API("custom_api", "自定义API"),
|
||||
MINIMAX("minimax", "MiniMax"),
|
||||
XIAOMI("xiaomi", "小米MiMo");
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.ruoyi.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 知识库附件解析状态枚举
|
||||
*
|
||||
* @author RobustH
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum KnowledgeAttachStatus {
|
||||
|
||||
/**
|
||||
* 待解析
|
||||
*/
|
||||
WAITING(0, "待解析"),
|
||||
|
||||
/**
|
||||
* 解析中
|
||||
*/
|
||||
PARSING(1, "解析中"),
|
||||
|
||||
/**
|
||||
* 已解析
|
||||
*/
|
||||
COMPLETED(2, "已解析"),
|
||||
|
||||
/**
|
||||
* 解析失败
|
||||
*/
|
||||
FAILED(3, "解析失败");
|
||||
|
||||
private final Integer code;
|
||||
private final String info;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package org.ruoyi.factory;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
|
||||
import org.ruoyi.common.chat.service.chat.IChatModelService;
|
||||
import org.ruoyi.service.rerank.RerankModelService;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 重排序模型工厂服务类
|
||||
* 参考设计模式:EmbeddingModelFactory
|
||||
* 负责创建和管理重排序模型实例
|
||||
*
|
||||
* @author yang
|
||||
* @date 2026-04-19
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class RerankModelFactory {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private final IChatModelService chatModelService;
|
||||
|
||||
/**
|
||||
* 模型缓存,使用ConcurrentHashMap保证线程安全
|
||||
*/
|
||||
private final Map<String, RerankModelService> modelCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 创建重排序模型实例
|
||||
* 如果模型已存在于缓存中,则直接返回;否则创建新的实例
|
||||
*
|
||||
* @param rerankModelName 重排序模型名称
|
||||
*/
|
||||
public RerankModelService createModel(String rerankModelName) {
|
||||
return modelCache.computeIfAbsent(rerankModelName, name -> {
|
||||
ChatModelVo modelConfig = chatModelService.selectModelByName(rerankModelName);
|
||||
|
||||
if (modelConfig == null) {
|
||||
throw new IllegalArgumentException("未找到重排序模型配置,name=" + name);
|
||||
}
|
||||
return createModelInstance(modelConfig.getProviderCode(), modelConfig);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新模型缓存
|
||||
* 根据给定的模型ID从缓存中移除对应的模型
|
||||
*
|
||||
* @param modelId 模型的唯一标识ID
|
||||
*/
|
||||
public void refreshModel(Long modelId) {
|
||||
modelCache.remove(modelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有支持模型工厂的列表
|
||||
*
|
||||
* @return 支持的模型工厂名称列表
|
||||
*/
|
||||
public List<String> getSupportedFactories() {
|
||||
return new ArrayList<>(applicationContext.getBeansOfType(RerankModelService.class)
|
||||
.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建具体的模型实例
|
||||
* 根据提供的工厂名称和配置信息创建并配置模型实例
|
||||
*
|
||||
* @param factory 工厂名称,用于标识模型类型(providerCode)
|
||||
* @param config 模型配置信息
|
||||
* @return RerankModelService 配置好的模型实例
|
||||
* @throws IllegalArgumentException 当无法获取指定的模型实例时抛出
|
||||
*/
|
||||
private RerankModelService createModelInstance(String factory, ChatModelVo config) {
|
||||
try {
|
||||
// 优先尝试使用 providerCode + "Rerank" 作为 Bean 名称
|
||||
// 例如:zhipu -> zhipuRerank,jina -> jinaRerank
|
||||
String rerankBeanName = factory + "Rerank";
|
||||
RerankModelService model = applicationContext.getBean(rerankBeanName, RerankModelService.class);
|
||||
model.configure(config);
|
||||
log.info("成功创建重排序模型: factory={}, modelName={}", rerankBeanName, config.getModelName());
|
||||
return model;
|
||||
} catch (NoSuchBeanDefinitionException e) {
|
||||
// 如果找不到,尝试使用原始的 providerCode
|
||||
try {
|
||||
RerankModelService model = applicationContext.getBean(factory, RerankModelService.class);
|
||||
model.configure(config);
|
||||
log.info("成功创建重排序模型: factory={}, modelName={}", factory, config.getModelName());
|
||||
return model;
|
||||
} catch (NoSuchBeanDefinitionException ex) {
|
||||
throw new IllegalArgumentException("获取不到重排序模型: " + factory + " 或 " + factory + "Rerank", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.ruoyi.mapper.knowledge;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.ruoyi.domain.entity.knowledge.KnowledgeAttach;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeAttachVo;
|
||||
import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
@@ -10,6 +13,12 @@ import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
* @author ageerle
|
||||
* @date 2025-12-17
|
||||
*/
|
||||
@Mapper
|
||||
public interface KnowledgeAttachMapper extends BaseMapperPlus<KnowledgeAttach, KnowledgeAttachVo> {
|
||||
|
||||
/**
|
||||
* 统计指定知识库下的文档数量
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM knowledge_attach WHERE knowledge_id = #{knowledgeId}")
|
||||
int countByKnowledgeId(@Param("knowledgeId") Long knowledgeId);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,45 @@
|
||||
package org.ruoyi.mapper.knowledge;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.ruoyi.domain.entity.knowledge.KnowledgeFragment;
|
||||
import org.ruoyi.domain.vo.knowledge.DocFragmentCountVo;
|
||||
import org.ruoyi.domain.vo.knowledge.KnowledgeFragmentVo;
|
||||
import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 知识片段Mapper接口
|
||||
*
|
||||
* @author ageerle
|
||||
* @date 2025-12-17
|
||||
*/
|
||||
@Mapper
|
||||
public interface KnowledgeFragmentMapper extends BaseMapperPlus<KnowledgeFragment, KnowledgeFragmentVo> {
|
||||
|
||||
/**
|
||||
* 批量统计各文档的分块数(强类型接收,避免 Map key 大小写问题)
|
||||
*
|
||||
* @param docIds 文档 ID 列表
|
||||
* @return 每个 docId 对应的分块数列表
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT doc_id AS docId, COUNT(*) AS fragmentCount " +
|
||||
"FROM knowledge_fragment " +
|
||||
"WHERE doc_id IN " +
|
||||
"<foreach collection='docIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
"GROUP BY doc_id" +
|
||||
"</script>")
|
||||
List<DocFragmentCountVo> selectFragmentCountByDocIds(@Param("docIds") List<String> docIds);
|
||||
@Select("<script>" +
|
||||
"SELECT id, doc_id AS docId, content, idx, knowledge_id AS knowledgeId " +
|
||||
"FROM knowledge_fragment " +
|
||||
"WHERE knowledge_id = #{knowledgeId} " +
|
||||
"AND MATCH (content) AGAINST (#{query} IN NATURAL LANGUAGE MODE) " +
|
||||
"ORDER BY MATCH (content) AGAINST (#{query} IN NATURAL LANGUAGE MODE) DESC " +
|
||||
"LIMIT #{limit}" +
|
||||
"</script>")
|
||||
List<KnowledgeFragmentVo> searchByKeyword(@Param("knowledgeId") Long knowledgeId, @Param("query") String query, @Param("limit") Integer limit);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.ruoyi.mcp.tools;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 执行审批令牌存储
|
||||
* 使用内存存储短期有效令牌,用于“先计划后执行”闭环
|
||||
*/
|
||||
public final class ApprovalTokenStore {
|
||||
|
||||
private static final Map<String, TokenRecord> TOKENS = new ConcurrentHashMap<>();
|
||||
|
||||
private ApprovalTokenStore() {
|
||||
}
|
||||
|
||||
public static String issue(String goal, long ttlSeconds) {
|
||||
cleanupExpired();
|
||||
String token = UUID.randomUUID().toString();
|
||||
long expireAt = Instant.now().getEpochSecond() + Math.max(60, ttlSeconds);
|
||||
TOKENS.put(token, new TokenRecord(goal == null ? "" : goal, expireAt));
|
||||
return token;
|
||||
}
|
||||
|
||||
public static boolean consume(String token, String scope) {
|
||||
if (token == null || token.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
TokenRecord record = TOKENS.remove(token.trim());
|
||||
if (record == null) {
|
||||
return false;
|
||||
}
|
||||
if (record.expireAtEpochSeconds() < Instant.now().getEpochSecond()) {
|
||||
return false;
|
||||
}
|
||||
String normalizedScope = scope == null ? "" : scope.trim();
|
||||
return record.goal().equals(normalizedScope);
|
||||
}
|
||||
|
||||
private static void cleanupExpired() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
TOKENS.entrySet().removeIf(entry -> entry.getValue().expireAtEpochSeconds() < now);
|
||||
}
|
||||
|
||||
private record TokenRecord(String goal, long expireAtEpochSeconds) {
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user