first commit

This commit is contained in:
dev
2026-02-22 12:12:02 +08:00
commit b236048a24
1467 changed files with 160403 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+549
View File
@@ -0,0 +1,549 @@
# AI模块 (ruoyi-ai)
## 模块简介
`ruoyi-ai` 是 PoJie 项目的 AI 服务中心模块,提供统一的 AI 能力接入,标准化接入主流模型与平台,支持聊天、流式对话、AI创作、智能PPT、多模态理解、代码助手、RAG增强与统一监控等功能。
## 功能特性
### 1. Provider 管理与编排 ✅
- ✅ 统一接口:`AiProviderManager` 管理所有模型提供商,支持热切换
- ✅ 智能路由:`AiOrchestrationEngine` 支持最快响应、最佳质量、成本最优、负载均衡等策略
- ✅ 自动降级:错误率>30%或响应超时自动熔断与故障转移
- ✅ 多模型接入:统一调度 OpenAI、Azure、ChatGLM、通义千问、智谱等主流模型
### 2. 聊天与流式交互 ✅
- ✅ 同步/异步聊天
- ✅ SSE 流式推送
- ✅ WebSocket 实时通道 (`/ws/ai/chat`)
- ✅ 聊天历史管理:自动保存聊天记录到Redis,支持历史查询、清除和会话统计
### 3. AI 辅助编码 ✅
- ✅ 代码分析:提供代码审查与重构建议
- ✅ 脚手架生成:自动生成项目结构与配置
### 4. Simple AI & RAG ✅
- ✅ 知识库问答:基于知识库的精准问答
- ✅ 向量检索:支持多模型向量相似度搜索
### 5. AI 创作与多模态 ✅
- ✅ 绘画调度:统一管理 DALL·E-3、MidJourney、Stable Diffusion 任务
- ✅ 智能 PPT:文本生成 PPTX,支持导出图片序列
- ✅ 多模态:文件解析(PDF/Word)、OCR识别、图像理解
### 6. 平台集成 ✅
- ✅ FastGPT:深度对接 FastGPT 平台
- ✅ Coze:深度对接 Coze 平台(SSE流式)
- ✅ DIFY:深度对接 DIFY 平台
### 7. MCP 生态 ✅
- ✅ 工具注册:`McpToolRegistry` 支持动态工具加载与调用
- ✅ 工具模板:提供多个内置工具(ProjectScaffold、DataTransform、ApiCall、AlgorithmCompute、FileParse、HttpFetch
### 8. 安全与监控 ✅
- ✅ 安全卫士:`AiSecurityManager` 提供内容过滤、IP黑白名单、速率限制
- ✅ 全链路监控:实时监控队列深度、模型健康度、响应时间与 Token 消耗
- ✅ 使用统计:记录和统计AI使用情况
## 数据库表
- `ai_provider_config` - AI Provider配置表
- `ai_provider_config_history` - AI Provider配置历史表
- `ai_chat_record` - AI聊天记录表
- `ai_usage_stats` - AI使用统计表
- `ai_rating_record` - AI评价记录表
详细表结构请参考:`script/sql/ry_ai.sql`
## 模块结构
```
org.dromara.ai
├── config/ # 模块配置类
│ ├── AiAutoConfiguration.java
│ ├── AiGatewayProperties.java
│ ├── AiProviderRegistrar.java
│ └── *ProviderConfig.java (各Provider配置)
├── controller/ # REST API控制器
│ ├── admin/ # 管理端接口
│ │ ├── AdminAiChatRecordController.java # 聊天记录管理接口
│ │ ├── AdminAiUsageStatsController.java # 使用统计管理接口
│ │ ├── AdminAiProviderController.java # Provider管理接口
│ │ └── AdminSysModelController.java # 系统模型管理接口
│ └── app/ # 用户端接口
│ ├── AiChatController.java # 聊天接口
│ ├── AiAssistantController.java # 代码助手接口
│ ├── AiPaintingController.java # 绘画接口
│ ├── AiImageController.java # DALL·E-3接口
│ ├── PptController.java # PPT生成接口
│ ├── MultimodalController.java # 多模态接口
│ ├── SimpleAiController.java # Simple AI接口
│ ├── FastGptController.java # FastGPT接口
│ ├── CozeController.java # Coze接口
│ ├── DifyController.java # DIFY接口
│ ├── McpController.java # MCP工具接口
│ └── QueueMetricsController.java # 队列指标接口
├── domain/ # 数据传输对象 (DTO) 与枚举
│ ├── dto/
│ └── enums/
│ └── AiProviderType.java
├── provider/ # 模型 Provider 实现
│ ├── AiProvider.java
│ ├── AiProviderManager.java
│ ├── OpenAiProvider.java
│ ├── AzureOpenAiProvider.java
│ ├── QwenProvider.java
│ ├── ZhipuAiProvider.java
│ └── *Provider.java
├── service/ # 核心业务服务
│ ├── AiChatService.java
│ ├── AiChatHistoryService.java
│ ├── AiCodingAssistantService.java
│ ├── AiModelService.java
│ ├── AiPaintingService.java
│ └── SimpleAiService.java
├── orchestration/ # AI 编排引擎
│ └── AiOrchestrationEngine.java
├── painting/ # 绘画服务与 Provider
│ ├── PaintingEngineType.java
│ ├── PaintingTask.java
│ ├── PaintingStatus.java
│ └── provider/
├── platform/ # 外部平台集成
│ ├── FastGptService.java
│ ├── CozeService.java
│ └── DifyClientService.java
├── mcp/ # MCP 工具链与集成
│ ├── McpToolRegistry.java
│ ├── McpTool.java
│ └── tools/
├── queue/ # 任务队列服务
│ └── TaskQueueService.java
├── security/ # 安全管理器
│ ├── AiSecurityManager.java
│ ├── AiContentFilterService.java
│ └── AiAccessControlService.java
├── monitor/ # 监控指标与度量
│ ├── AiMonitorService.java
│ └── *Metrics.java
├── websocket/ # WebSocket 处理器
│ ├── ChatWebSocketConfig.java
│ └── ChatWebSocketHandler.java
└── error/ # 全局异常处理
└── AiErrorHandler.java
```
## 配置说明
`application.yml` 中添加以下配置:
```yaml
ai:
gateway:
enabled: true
auth-required: true
base-path: /api/ai
provider:
timeout-ms: 60000
retry: 1
ruoyi:
ai:
enabled: true
external-ai:
openai:
base-url: https://api.openai.com
api-key: sk-xxxx
azure-openai:
endpoint: https://your-endpoint.openai.azure.com
api-key: your-api-key
api-version: 2023-12-01-preview
qwen:
base-url: https://dashscope.aliyuncs.com/api/v1
api-key: sk-xxxx
zhipu:
base-url: https://open.bigmodel.cn/api/paas/v4
api-key: sk-xxxx
sd:
base-url: http://localhost:7860
midjourney:
base-url: https://mj.example.com
token: mj-xxxx
ocr:
base-url: https://ocr.example.com
platform:
fastgpt:
base-url: https://fastgpt.example.com
token: fgpt-xxxx
coze:
base-url: https://coze.example.com
token: coze-xxxx
dify:
base-url: https://dify.example.com
token: dify-xxxx
```
## API接口
### 聊天接口 (`/ai/chat`)
#### 同步聊天
- `POST /ai/chat/sync` - 同步聊天(返回完整响应)
#### 异步聊天
- `POST /ai/chat/async` - 异步聊天(返回任务ID
#### 流式聊天
- `POST /ai/chat/stream` - SSE流式聊天
- `POST /ai/chat/stream-chunks` - 流式聊天(分块返回)
#### 批量聊天
- `POST /ai/chat/batch` - 批量聊天请求
#### 聊天历史
- `GET /ai/chat/history/{sessionId}` - 获取聊天历史
- `DELETE /ai/chat/history/{sessionId}` - 清除聊天历史
#### 会话统计
- `GET /ai/chat/stats/{sessionId}` - 获取会话统计
### AI代码助手接口 (`/ai/assistant`)
- `POST /ai/assistant/analyze` - 代码分析
- `POST /ai/assistant/scaffold` - 生成项目脚手架
### Provider管理接口 (`/ai/provider`)
- `GET /ai/provider/list` - 列出所有Provider
- `GET /ai/provider/models` - 列出所有模型
- `GET /ai/provider/default` - 获取默认Provider
- `POST /ai/provider/setDefault` - 设置默认Provider
- `GET /ai/provider/stats/system` - 系统统计
- `GET /ai/provider/stats/providers` - Provider统计
- `POST /ai/provider/stats/reset` - 重置统计
### Simple AI接口 (`/ai`)
- `POST /ai/ask` - 知识库问答
- `POST /ai/search` - 向量搜索
- `GET /ai/stats/{knowledgeId}` - 知识库统计
- `GET /ai/model/check/{modelName}` - 检查模型可用性
- `GET /ai/models` - 获取可用模型列表
- `GET /ai/health` - 健康检查
### 绘画接口 (`/ai/painting`)
- `POST /ai/painting/task` - 创建绘画任务
- `GET /ai/painting/status/{taskId}` - 查询任务状态
- `GET /ai/painting/stream/{taskId}` - SSE流式获取任务进度
### DALL·E-3接口
- `POST /dall3` - 文生图
- `POST /dall3/edit` - 图片编辑(image+mask
- `POST /dall3/variations` - 图片变体
### 智能PPT接口 (`/ppt`)
- `POST /ppt/generate` - 生成PPTX文件
- `POST /ppt/export/pngs` - 导出PNG序列
### 多模态接口 (`/multimodal`)
- `POST /multimodal/extract` - 文件解析(PDF/Word等)
- `POST /multimodal/ocr` - OCR识别
- `POST /multimodal/analyze/text` - 文本分析
- `POST /multimodal/analyze/image` - 图像识别(占位)
- `POST /multimodal/caption` - 图生文(占位)
### 平台集成接口
#### FastGPT (`/ai/fastgpt`)
- `POST /ai/fastgpt/knowledge/search` - 知识搜索
- `POST /ai/fastgpt/workflow/run` - 运行工作流
- `GET /ai/fastgpt/context/{sessionId}` - 获取上下文
#### Coze (`/ai/coze`)
- `POST /ai/coze/stream` - SSE流式对话
#### DIFY (`/ai/dify`)
- `GET /ai/dify/apps` - 获取应用列表
- `POST /ai/dify/apps` - 创建/更新应用
### MCP工具接口 (`/ai/mcp`)
- `GET /ai/mcp/tools` - 获取工具列表
- `POST /ai/mcp/invoke/{name}` - 调用工具
### 队列指标接口 (`/ai/queue`)
- `GET /ai/queue/metrics` - 获取队列指标
## WebSocket实时通道
### 聊天WebSocket
- **端点**: `ws://localhost:8080/ws/ai/chat`
- **协议**: JSON格式 `AiChatRequest`,支持实时双向对话
## 依赖模块
- `ruoyi-common-core` - 通用核心工具
- `ruoyi-common-web` - Web通用组件
- `ruoyi-common-redis` - Redis缓存
- `ruoyi-common-security` - 安全组件
- `ruoyi-common-log` - 日志组件
- `ruoyi-common-mybatis` - MyBatis组件
- `ruoyi-knowledge` - 知识库模块(RAG增强,可选)
## 使用示例
### 1. 聊天与流式对话
```bash
# 同步聊天
curl -X POST http://localhost:8080/api/ai/chat/sync \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"你好"}],"sessionId":"session_123"}'
# 流式聊天
curl -X POST http://localhost:8080/api/ai/chat/stream \
-H "Authorization: Bearer <token>" \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"你好"}],"stream":true,"sessionId":"session_123"}'
# 获取聊天历史
curl -X GET "http://localhost:8080/api/ai/chat/history/session_123?limit=50" \
-H "Authorization: Bearer <token>"
```
### 2. AI代码助手
```bash
# 代码分析
curl -X POST http://localhost:8080/api/ai/assistant/analyze \
-d 'code=public void test(){...}&model=gpt-4'
# 生成脚手架
curl -X POST http://localhost:8080/api/ai/assistant/scaffold \
-d 'appType=SpringBoot&options={"db":"mysql"}'
```
### 3. Simple AI (RAG)
```bash
# 知识库问答
curl -X POST "http://localhost:8080/api/ai/ask?knowledgeId=kb_123&question=合同流程是怎样的"
# 向量搜索
curl -X POST "http://localhost:8080/api/ai/search?knowledgeId=kb_123&queryText=条款&topK=3"
```
### 4. AI绘画任务
```bash
# 创建绘画任务
curl -X POST http://localhost:8080/api/ai/painting/task \
-H "Content-Type: application/json" \
-d '{"engine":"DALL_E_3","prompt":"未来城市","params":{"size":"1024x1024"}}'
# 查询任务状态
curl http://localhost:8080/api/ai/painting/status/{taskId}
```
### 5. 智能PPT生成
```bash
curl -X POST "http://localhost:8080/api/ai/ppt/generate?content=第一页标题\n内容1\n第二页标题\n内容2"
```
### 6. MCP工具调用
```bash
curl -X POST http://localhost:8080/api/ai/mcp/invoke/api_call \
-H "Content-Type: application/json" \
-d '{"url":"https://httpbin.org/get"}'
```
## 安全与合规
`AiSecurityManager` 默认开启,提供以下保护:
- **API密钥验证**:校验请求中的 `apiKey`
- **IP访问控制**:基于策略拦截非法IP
- **内容过滤**:输入/输出双向敏感词过滤(Moderate策略)
- **资源限额**:基于Token数与模型复杂度的动态配额扣除
- **速率限制**:防止API滥用
## 监控与统计
### 监控指标
- 队列深度监控
- 模型健康度监控
- 响应时间监控
- Token消耗监控
- 错误率监控
### 使用统计
- 用户使用统计(按用户、Provider、模型)
- 请求次数统计
- 成功率统计
- 成本统计
## 注意事项
1. **密钥安全**:生产环境务必通过环境变量注入 API Key,不要将密钥写入配置文件
2. **流式代理**Nginx/Gateway 需配置 `proxy_buffering off` 以支持 SSE
3. **依赖服务**:Redis 必须可用,用于存储任务状态与缓存
4. **性能优化**:建议对热点数据使用Redis缓存
5. **错误处理**:模块提供自动降级和熔断机制,但建议监控错误率
6. **并发控制**:Provider支持最大并发数配置,合理设置以避免资源耗尽
## 扩展开发
### 添加新的Provider
1. 实现 `AiProvider` 接口
2. 创建对应的配置类(如 `XxxProviderConfig.java`
3.`AiProviderRegistrar` 中注册Provider
4.`AiProviderType` 枚举中添加新的Provider类型
### 添加新的MCP工具
1. 实现 `McpTool` 接口
2.`AiProviderRegistrar` 中注册工具
3. 工具会自动出现在工具列表中
## 相关文档
- [API详细文档](docs/API.md)
- [配置说明](docs/CONFIG.md)
- [集成指南](docs/INTEGRATION.md)
- [AI创作系统开发指南](#ruoyi-ai-创作系统开发指南)
---
# RuoYi-AI 创作系统开发指南
本指南介绍 `ruoyi-ai` 模块中的 AI 创作能力,包括绘画引擎接入、智能 PPT 生成、多模态内容理解,以及统一的任务流与接口规范。
> 该文档同时覆盖平台深度集成(FastGPT/Coze/DIFY)、MCP生态扩展、RAG增强、队列与长任务监控、质量与性能保障等内容,便于端到端落地与运维。
## 模块概览
### 绘画引擎集成(DALL·E-3 / MidJourney / Stable Diffusion
- 统一引擎枚举:`painting/PaintingEngineType.java`
- 任务模型:`painting/PaintingTask.java`
- 任务状态:`painting/PaintingStatus.java`
- 引擎适配:
- DALL·E-3`painting/provider/DallE3Provider.java`
- MidJourney`painting/provider/MidJourneyProvider.java`
- Stable Diffusion`painting/provider/StableDiffusionProvider.java`
- 统一服务:`service/AiPaintingService.java`
- 控制器:`controller/AiPaintingController.java`
### 智能 PPT 生成
- 控制器:`controller/PptController.java`
- 能力:按文本生成 PPTX、导出每页 PNG 序列、主题占位与扩展点
### 多模态内容理解
- 控制器:`controller/MultimodalController.java`
- 能力:文件解析(Tika)、文本分析、OCR占位、图像识别占位、图生文占位
### DALL·E-3 扩展
- 控制器:`controller/AiImageController.java`
- 能力:文生图、编辑(image+mask)、变体(variations
### 平台接入与统一API
- FastGPT:服务 `platform/FastGptService.java`,接口 `controller/FastGptController.java`
- Coze:服务 `platform/CozeService.java`,接口 `controller/CozeController.java`SSE流式)
- DIFY:服务 `platform/DifyClientService.java`,接口 `controller/DifyController.java`
### MCP生态扩展
- 工具注册:`mcp/McpToolRegistry.java`(动态加载/版本管理/热重载)
- 工具模板:`mcp/tools/*.java`(≥5个:ProjectScaffold/DataTransform/ApiCall/AlgorithmCompute/FileParse/HttpFetch
- 控制器:`controller/McpController.java`(工具列表与调用)
### 统一模型调度与降级
- 管理器:`provider/AiProviderManager.java`(模型优先→性能优选→近似轮询、自动降级)
- 管理接口:`controller/admin/AdminAiProviderController.java`(列表/模型/默认/统计)
### 队列与长任务
- 服务:`queue/TaskQueueService.java`(优先级队列、任务状态与指标)
- 指标接口:`controller/QueueMetricsController.java`
## 配置项
### OpenAI
- `ruoyi.ai.external-ai.openai.base-url`(默认 `https://api.openai.com`
- `ruoyi.ai.external-ai.openai.api-key`
### Azure OpenAI
- `ruoyi.ai.external-ai.azure-openai.endpoint`
- `ruoyi.ai.external-ai.azure-openai.api-key`
- `ruoyi.ai.external-ai.azure-openai.api-version`
### MidJourney
- `ruoyi.ai.external-ai.midjourney.base-url`
- `ruoyi.ai.external-ai.midjourney.token`
### Stable Diffusion
- `ruoyi.ai.external-ai.sd.base-url`(例如 `http://localhost:7860`
### OCR
- `ruoyi.ai.ocr.base-url`
### 平台
- `ruoyi.ai.platform.fastgpt.base-url``ruoyi.ai.platform.fastgpt.token`
- `ruoyi.ai.platform.coze.base-url``ruoyi.ai.platform.coze.token`
- `ruoyi.ai.platform.dify.base-url``ruoyi.ai.platform.dify.token`
## API 速览
### 绘画任务统一接口(REST + SSE)
- 创建任务:`POST /ai/painting/task`
- body`{"engine":"DALL_E_3","prompt":"海边日落","params":{"size":"1024x1024"}}`
- 返回:`{"taskId":"..."}`(任务 ID
- 查询状态:`GET /ai/painting/status/{taskId}`
- 进度流:`GET /ai/painting/stream/{taskId}`SSE[DONE] 结束)
### DALL·E-3
- 文生图:`POST /dall3``prompt`,`size`
- 编辑:`POST /dall3/edit``prompt`,`size`,`image`,`mask?`
- 变体:`POST /dall3/variations``size`,`image`
### 智能 PPT
- 生成 PPTX`POST /ppt/generate`(按行拆分内容为多页)
- 导出 PNG 序列:`POST /ppt/export/pngs`(返回 `data:image/png;base64,...` 列表)
### 多模态
- 文件解析:`POST /multimodal/extract``multipart/form-data:file`
- OCR识别:`POST /multimodal/ocr`(需配置 OCR 服务)
- 文本分析:`POST /multimodal/analyze/text`(返回 `title/summary/keywords`
- 图像识别占位:`POST /multimodal/analyze/image`
- 图生文占位:`POST /multimodal/caption`
## 流式与实时
- **SSE**`/ai/painting/stream/{taskId}` 推送任务状态与进度;统一以 `[DONE]` 结束
- **WebSocket**:模块已提供聊天通道 `ws/ai/chat`;如需绘画通道,可参考该模式新增 `/ws/ai/painting`
## 任务与历史
- 任务创建后进入线程池异步执行,状态与结果存储于 Redis:键 `ai:painting:task:{taskId}`
- 可基于该结构扩展作品历史管理、检索与归档
## 扩展建议
- **引擎负载均衡**:为 Provider 增加实例列表与健康检查,依据权重分配
- **任务队列**:接入消息队列(如 Kafka/RabbitMQ)实现更大的并发与可靠性
- **权限与用量**:接入 `sa-token` 与用量计费,限制频率与配额
- **监控与审计**:统一接入日志、指标与告警,便于运维与合规
- **RAG增强**:在 `ruoyi-knowledge` 实现 `VectorDbClient` 接入层(Milvus/Weaviate/Qdrant),集成 BGE-large-zh-v1.5 批量嵌入与缓存,混合检索(关键词+向量)召回≥90%
- **队列看板**:在管理端提供队列深度/处理速率可视化与失败重试控制
## 测试与质量
- **单元测试**:建议使用 MockWebServer 模拟外部 API,覆盖率目标 ≥ 80%
- **性能**:核心 API 响应时间 < 500ms(外部调用除外),并发可通过线程池与队列扩展至 1000+
- **压测与限流**:模拟 1000+ 并发,结合令牌桶限流,P99 延迟控制在 2s 以内
- **可用性**:关键服务可用性目标 ≥ 99.9%,结合自动降级与熔断策略
+114
View File
@@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-ai</artifactId>
<description>
AI模块
</description>
<dependencies>
<!-- HTTP 客户端 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp-sse</artifactId>
</dependency>
<!-- JSON 解析 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 通用工具-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-doc</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-log</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-security</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-redis</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-excel</artifactId>
</dependency>
<!-- Spring Boot Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 配置处理 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 知识库模块依赖 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-knowledge</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.9.1</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,53 @@
package org.dromara.ai.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// springdoc group
// Use class name to avoid hard dependency if springdoc is absent at compile-time
@Configuration
@EnableConfigurationProperties(AiGatewayProperties.class)
public class AiApiConfig {
@Bean
public Object aiGroupedOpenApi() {
try {
Class<?> groupedOpenApiClass = Class.forName("org.springdoc.core.models.GroupedOpenApi");
return groupedOpenApiClass
.getMethod("builder")
.invoke(null)
.getClass()
.getMethod("group", String.class)
.invoke(
groupedOpenApiClass.getMethod("builder").invoke(null),
"AI"
)
.getClass()
.getMethod("packagesToScan", String[].class)
.invoke(
groupedOpenApiClass.getMethod("builder").invoke(null)
.getClass().getMethod("group", String.class)
.invoke(groupedOpenApiClass.getMethod("builder").invoke(null), "AI"),
(Object) new String[]{"org.dromara.ai.api"}
)
.getClass()
.getMethod("build")
.invoke(
groupedOpenApiClass.getMethod("builder").invoke(null)
.getClass().getMethod("group", String.class)
.invoke(groupedOpenApiClass.getMethod("builder").invoke(null), "AI")
.getClass().getMethod("packagesToScan", String[].class)
.invoke(
groupedOpenApiClass.getMethod("builder").invoke(null)
.getClass().getMethod("group", String.class)
.invoke(groupedOpenApiClass.getMethod("builder").invoke(null), "AI"),
(Object) new String[]{"org.dromara.ai.api"}
)
);
} catch (Throwable ignore) {
return new Object();
}
}
}
@@ -0,0 +1,23 @@
package org.dromara.ai.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* 简化的AI自动配置类
* 主要用于扫描AI模块的组件
*
* @author pengles
*/
@Slf4j
@Configuration
@ComponentScan(basePackages = "org.dromara.ai")
@ConditionalOnProperty(prefix = "ruoyi.ai", name = "enabled", havingValue = "true", matchIfMissing = true)
public class AiAutoConfiguration {
public AiAutoConfiguration() {
log.info("AI模块已启用");
}
}
@@ -0,0 +1,532 @@
package org.dromara.ai.config;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* AI配置管理服务
* 支持动态配置和热更新
*
* @author pengles
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AiConfigurationManager {
private final ApplicationEventPublisher eventPublisher;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
// 配置存储
private final Map<String, AiConfiguration> configurations = new ConcurrentHashMap<>();
// 配置监听器
private final Map<String, Consumer<AiConfiguration>> configListeners = new ConcurrentHashMap<>();
// 配置版本
private final Map<String, Long> configVersions = new ConcurrentHashMap<>();
// 配置历史
private final Map<String, Map<Long, AiConfiguration>> configHistory = new ConcurrentHashMap<>();
/**
* AI配置类
*/
public static class AiConfiguration {
private String key;
private Object value;
private String description;
private ConfigType type;
private boolean hotReloadable;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private String updatedBy;
private Map<String, Object> metadata;
// Getters and Setters
public String getKey() {
return key;
}
public AiConfiguration setKey(String key) {
this.key = key;
return this;
}
public Object getValue() {
return value;
}
public AiConfiguration setValue(Object value) {
this.value = value;
return this;
}
public String getDescription() {
return description;
}
public AiConfiguration setDescription(String description) {
this.description = description;
return this;
}
public ConfigType getType() {
return type;
}
public AiConfiguration setType(ConfigType type) {
this.type = type;
return this;
}
public boolean isHotReloadable() {
return hotReloadable;
}
public AiConfiguration setHotReloadable(boolean hotReloadable) {
this.hotReloadable = hotReloadable;
return this;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public AiConfiguration setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
return this;
}
public LocalDateTime getUpdateTime() {
return updateTime;
}
public AiConfiguration setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
return this;
}
public String getUpdatedBy() {
return updatedBy;
}
public AiConfiguration setUpdatedBy(String updatedBy) {
this.updatedBy = updatedBy;
return this;
}
public Map<String, Object> getMetadata() {
return metadata;
}
public AiConfiguration setMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
return this;
}
}
/**
* 配置类型枚举
*/
public enum ConfigType {
PROVIDER_CONFIG, // Provider配置
MODEL_CONFIG, // 模型配置
SECURITY_CONFIG, // 安全配置
MONITOR_CONFIG, // 监控配置
SYSTEM_CONFIG // 系统配置
}
/**
* 配置变更事件
*/
public static class ConfigChangeEvent {
private String key;
private Object oldValue;
private Object newValue;
private ConfigType type;
private LocalDateTime changeTime;
private String changedBy;
public ConfigChangeEvent(String key, Object oldValue, Object newValue, ConfigType type, String changedBy) {
this.key = key;
this.oldValue = oldValue;
this.newValue = newValue;
this.type = type;
this.changeTime = LocalDateTime.now();
this.changedBy = changedBy;
}
// Getters
public String getKey() {
return key;
}
public Object getOldValue() {
return oldValue;
}
public Object getNewValue() {
return newValue;
}
public ConfigType getType() {
return type;
}
public LocalDateTime getChangeTime() {
return changeTime;
}
public String getChangedBy() {
return changedBy;
}
}
/**
* 应用启动后初始化
*/
@EventListener(ApplicationReadyEvent.class)
public void initialize() {
log.info("初始化AI配置管理器");
// 加载默认配置
loadDefaultConfigurations();
// 启动配置监控
startConfigMonitoring();
log.info("AI配置管理器初始化完成,已加载{}个配置项", configurations.size());
}
/**
* 获取配置
*
* @param key 配置键
* @return 配置值
*/
@SuppressWarnings("unchecked")
public <T> T getConfig(String key) {
AiConfiguration config = configurations.get(key);
return config != null ? (T) config.getValue() : null;
}
/**
* 获取配置(带默认值)
*
* @param key 配置键
* @param defaultValue 默认值
* @return 配置值
*/
@SuppressWarnings("unchecked")
public <T> T getConfig(String key, T defaultValue) {
AiConfiguration config = configurations.get(key);
return config != null ? (T) config.getValue() : defaultValue;
}
/**
* 设置配置
*
* @param key 配置键
* @param value 配置值
* @param type 配置类型
* @param updatedBy 更新者
*/
public void setConfig(String key, Object value, ConfigType type, String updatedBy) {
setConfig(key, value, type, true, null, updatedBy);
}
/**
* 设置配置(完整参数)
*
* @param key 配置键
* @param value 配置值
* @param type 配置类型
* @param hotReloadable 是否支持热更新
* @param description 配置描述
* @param updatedBy 更新者
*/
public void setConfig(String key, Object value, ConfigType type, boolean hotReloadable,
String description, String updatedBy) {
AiConfiguration oldConfig = configurations.get(key);
Object oldValue = oldConfig != null ? oldConfig.getValue() : null;
AiConfiguration newConfig = new AiConfiguration()
.setKey(key)
.setValue(value)
.setType(type)
.setHotReloadable(hotReloadable)
.setDescription(description)
.setUpdatedBy(updatedBy)
.setUpdateTime(LocalDateTime.now());
if (oldConfig == null) {
newConfig.setCreateTime(LocalDateTime.now());
} else {
newConfig.setCreateTime(oldConfig.getCreateTime());
}
configurations.put(key, newConfig);
// 更新版本
long newVersion = configVersions.getOrDefault(key, 0L) + 1;
configVersions.put(key, newVersion);
// 保存历史
configHistory.computeIfAbsent(key, k -> new ConcurrentHashMap<>())
.put(newVersion, newConfig);
// 触发配置变更事件
ConfigChangeEvent event = new ConfigChangeEvent(key, oldValue, value, type, updatedBy);
eventPublisher.publishEvent(event);
// 通知监听器
Consumer<AiConfiguration> listener = configListeners.get(key);
if (listener != null && hotReloadable) {
try {
listener.accept(newConfig);
log.info("配置热更新成功: {} = {}", key, value);
} catch (Exception e) {
log.error("配置热更新失败: {}", key, e);
}
}
log.info("配置已更新: {} = {} (版本: {})", key, value, newVersion);
}
/**
* 删除配置
*
* @param key 配置键
* @param updatedBy 操作者
*/
public void removeConfig(String key, String updatedBy) {
AiConfiguration config = configurations.remove(key);
if (config != null) {
// 触发配置变更事件
ConfigChangeEvent event = new ConfigChangeEvent(key, config.getValue(), null, config.getType(), updatedBy);
eventPublisher.publishEvent(event);
log.info("配置已删除: {}", key);
}
}
/**
* 注册配置监听器
*
* @param key 配置键
* @param listener 监听器
*/
public void addConfigListener(String key, Consumer<AiConfiguration> listener) {
configListeners.put(key, listener);
log.debug("已注册配置监听器: {}", key);
}
/**
* 移除配置监听器
*
* @param key 配置键
*/
public void removeConfigListener(String key) {
configListeners.remove(key);
log.debug("已移除配置监听器: {}", key);
}
/**
* 获取所有配置
*
* @return 配置映射
*/
public Map<String, AiConfiguration> getAllConfigurations() {
return new ConcurrentHashMap<>(configurations);
}
/**
* 获取指定类型的配置
*
* @param type 配置类型
* @return 配置映射
*/
public Map<String, AiConfiguration> getConfigurationsByType(ConfigType type) {
Map<String, AiConfiguration> result = new ConcurrentHashMap<>();
configurations.forEach((key, config) -> {
if (config.getType() == type) {
result.put(key, config);
}
});
return result;
}
/**
* 获取配置历史
*
* @param key 配置键
* @return 配置历史
*/
public Map<Long, AiConfiguration> getConfigHistory(String key) {
return configHistory.getOrDefault(key, new ConcurrentHashMap<>());
}
/**
* 回滚配置到指定版本
*
* @param key 配置键
* @param version 版本号
* @param updatedBy 操作者
*/
public void rollbackConfig(String key, long version, String updatedBy) {
Map<Long, AiConfiguration> history = configHistory.get(key);
if (history == null || !history.containsKey(version)) {
throw new IllegalArgumentException("配置版本不存在: " + key + " v" + version);
}
AiConfiguration historicalConfig = history.get(version);
setConfig(key, historicalConfig.getValue(), historicalConfig.getType(),
historicalConfig.isHotReloadable(), historicalConfig.getDescription(), updatedBy);
log.info("配置已回滚: {} 到版本 {}", key, version);
}
/**
* 重新加载所有配置
*/
public void reloadAllConfigurations() {
log.info("开始重新加载所有配置");
configurations.forEach((key, config) -> {
if (config.isHotReloadable()) {
Consumer<AiConfiguration> listener = configListeners.get(key);
if (listener != null) {
try {
listener.accept(config);
log.debug("配置重新加载成功: {}", key);
} catch (Exception e) {
log.error("配置重新加载失败: {}", key, e);
}
}
}
});
log.info("所有配置重新加载完成");
}
/**
* 加载默认配置
*/
private void loadDefaultConfigurations() {
// Provider配置
setConfig("ai.provider.openai.enabled", true, ConfigType.PROVIDER_CONFIG, true,
"OpenAI Provider启用状态", "system");
setConfig("ai.provider.openai.timeout", 30000, ConfigType.PROVIDER_CONFIG, true,
"OpenAI Provider超时时间(ms)", "system");
setConfig("ai.provider.openai.retry.max", 3, ConfigType.PROVIDER_CONFIG, true,
"OpenAI Provider最大重试次数", "system");
// 模型配置
setConfig("ai.model.default", "gpt-3.5-turbo", ConfigType.MODEL_CONFIG, true,
"默认AI模型", "system");
setConfig("ai.model.temperature.default", 0.7, ConfigType.MODEL_CONFIG, true,
"默认温度参数", "system");
setConfig("ai.model.max_tokens.default", 2048, ConfigType.MODEL_CONFIG, true,
"默认最大token数", "system");
// 安全配置
setConfig("ai.security.content_filter.enabled", true, ConfigType.SECURITY_CONFIG, true,
"内容过滤启用状态", "system");
setConfig("ai.security.rate_limit.enabled", true, ConfigType.SECURITY_CONFIG, true,
"速率限制启用状态", "system");
setConfig("ai.security.rate_limit.requests_per_minute", 60, ConfigType.SECURITY_CONFIG, true,
"每分钟请求限制", "system");
// 监控配置
setConfig("ai.monitor.enabled", true, ConfigType.MONITOR_CONFIG, true,
"监控启用状态", "system");
setConfig("ai.monitor.metrics.retention_days", 30, ConfigType.MONITOR_CONFIG, true,
"监控数据保留天数", "system");
setConfig("ai.monitor.health_check.interval", 60, ConfigType.MONITOR_CONFIG, true,
"健康检查间隔(秒)", "system");
// 系统配置
setConfig("ai.system.thread_pool.core_size", 10, ConfigType.SYSTEM_CONFIG, true,
"线程池核心大小", "system");
setConfig("ai.system.thread_pool.max_size", 50, ConfigType.SYSTEM_CONFIG, true,
"线程池最大大小", "system");
setConfig("ai.system.cache.enabled", true, ConfigType.SYSTEM_CONFIG, true,
"缓存启用状态", "system");
}
/**
* 启动配置监控
*/
private void startConfigMonitoring() {
// 定期检查配置变更
scheduler.scheduleWithFixedDelay(() -> {
try {
// 这里可以实现从外部数据源检查配置变更
// 例如数据库、配置中心等
log.debug("配置监控检查完成");
} catch (Exception e) {
log.error("配置监控检查失败", e);
}
}, 30, 30, TimeUnit.SECONDS);
// 定期清理历史配置
scheduler.scheduleWithFixedDelay(() -> {
try {
cleanupConfigHistory();
} catch (Exception e) {
log.error("配置历史清理失败", e);
}
}, 1, 24, TimeUnit.HOURS);
}
/**
* 清理配置历史
*/
private void cleanupConfigHistory() {
int maxHistoryVersions = getConfig("ai.config.history.max_versions", 10);
configHistory.forEach((key, history) -> {
if (history.size() > maxHistoryVersions) {
// 保留最新的版本
history.entrySet().stream()
.sorted(Map.Entry.<Long, AiConfiguration>comparingByKey().reversed())
.skip(maxHistoryVersions)
.map(Map.Entry::getKey)
.forEach(history::remove);
}
});
log.debug("配置历史清理完成");
}
/**
* 销毁资源
*/
public void destroy() {
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
log.info("AI配置管理器已销毁");
}
}
@@ -0,0 +1,35 @@
package org.dromara.ai.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "ai.gateway")
public class AiGatewayProperties {
private boolean enabled = true;
private boolean authRequired = true;
private String basePath = "/api/ai";
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isAuthRequired() {
return authRequired;
}
public void setAuthRequired(boolean authRequired) {
this.authRequired = authRequired;
}
public String getBasePath() {
return basePath;
}
public void setBasePath(String basePath) {
this.basePath = basePath;
}
}
@@ -0,0 +1,44 @@
package org.dromara.ai.config;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.provider.AiProvider;
import org.dromara.ai.provider.AiProviderManager;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class AiProviderRegistrar {
private final AiProviderManager providerManager;
private final List<AiProvider> providers;
private final org.dromara.ai.mcp.McpToolRegistry registry;
@PostConstruct
public void register() {
for (AiProvider provider : providers) {
try {
provider.initialize(Collections.emptyMap());
providerManager.registerProvider(provider);
log.info("已注册AI提供商: {}", provider.getProviderName());
} catch (Exception e) {
log.error("注册AI提供商失败: {}", provider.getProviderName(), e);
}
}
try {
registry.register(new org.dromara.ai.mcp.tools.ProjectScaffoldTool());
registry.register(new org.dromara.ai.mcp.tools.DataTransformTool());
registry.register(new org.dromara.ai.mcp.tools.ApiCallTool());
registry.register(new org.dromara.ai.mcp.tools.AlgorithmComputeTool());
registry.register(new org.dromara.ai.mcp.tools.FileParseTool());
registry.register(new org.dromara.ai.mcp.tools.HttpFetchTool());
} catch (Exception e) {
log.error("注册MCP工具失败", e);
}
}
}
@@ -0,0 +1,72 @@
package org.dromara.ai.config;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.provider.AiProviderManager;
import org.dromara.ai.provider.AzureOpenAiProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Configuration
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "ruoyi.ai.external-ai.azure", name = "enabled", havingValue = "true")
public class AzureOpenAiProviderConfig {
private final AiProviderManager providerManager;
@Value("${ruoyi.ai.external-ai.azure.api-key:}")
private String apiKey;
@Value("${ruoyi.ai.external-ai.azure.endpoint:}")
private String endpoint;
@Value("${ruoyi.ai.external-ai.azure.deployment-name:}")
private String deploymentName;
@Value("${ruoyi.ai.external-ai.azure.api-version:2023-12-01-preview}")
private String apiVersion;
@Value("${ruoyi.ai.providers.azure-openai.supported-models[0]:gpt-35-turbo}")
private String supportedModel0;
@Value("${ruoyi.ai.providers.azure-openai.supported-models[1]:gpt-4}")
private String supportedModel1;
@Value("${ruoyi.ai.providers.azure-openai.default-model:gpt-35-turbo}")
private String defaultModel;
@Value("${ruoyi.ai.external-ai.openai.timeout:60s}")
private String timeout;
@PostConstruct
public void register() {
if (apiKey == null || apiKey.isBlank() || endpoint == null || endpoint.isBlank() || deploymentName == null || deploymentName.isBlank()) {
log.warn("Azure OpenAI 配置不完整,Provider 不注册");
return;
}
AzureOpenAiProvider provider = new AzureOpenAiProvider();
Map<String, Object> cfg = new HashMap<>();
cfg.put("api-key", apiKey);
cfg.put("endpoint", endpoint);
cfg.put("deployment-name", deploymentName);
cfg.put("api-version", apiVersion);
cfg.put("default-model", defaultModel);
cfg.put("timeout-ms", parseTimeoutMs(timeout));
cfg.put("supported-models", List.of(supportedModel0, supportedModel1));
provider.initialize(cfg);
providerManager.registerProvider(provider);
log.info("Azure OpenAI Provider 已注册");
}
private int parseTimeoutMs(String t) {
try {
if (t == null || t.isBlank()) return 60000;
if (t.endsWith("ms")) return Integer.parseInt(t.substring(0, t.length() - 2));
if (t.endsWith("s")) return Integer.parseInt(t.substring(0, t.length() - 1)) * 1000;
if (t.endsWith("m")) return Integer.parseInt(t.substring(0, t.length() - 1)) * 60_000;
return Integer.parseInt(t);
} catch (Exception e) {
return 60000;
}
}
}
@@ -0,0 +1,64 @@
package org.dromara.ai.config;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.provider.AiProviderManager;
import org.dromara.ai.provider.CozeProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Configuration
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "ruoyi.ai.ai-platform.coze", name = "enabled", havingValue = "true")
public class CozePlatformConfig {
private final AiProviderManager providerManager;
@Value("${ruoyi.ai.ai-platform.coze.api-key:}")
private String apiKey;
@Value("${ruoyi.ai.ai-platform.coze.base-url:}")
private String baseUrl;
@Value("${ruoyi.ai.ai-platform.coze.default-bot-id:}")
private String defaultBotId;
@Value("${ruoyi.ai.providers.coze.supported-models[0]:coze}")
private String supportedModel0;
@Value("${ruoyi.ai.external-ai.openai.timeout:60s}")
private String timeout;
@PostConstruct
public void register() {
if (apiKey == null || apiKey.isBlank() || baseUrl == null || baseUrl.isBlank() || defaultBotId == null || defaultBotId.isBlank()) {
log.warn("Coze 配置不完整,Provider 不注册");
return;
}
CozeProvider provider = new CozeProvider();
Map<String, Object> cfg = new HashMap<>();
cfg.put("api-key", apiKey);
cfg.put("base-url", baseUrl);
cfg.put("default-bot-id", defaultBotId);
cfg.put("timeout-ms", parseTimeoutMs(timeout));
cfg.put("supported-models", List.of(supportedModel0));
provider.initialize(cfg);
providerManager.registerProvider(provider);
log.info("Coze Provider 已注册");
}
private int parseTimeoutMs(String t) {
try {
if (t == null || t.isBlank()) return 60000;
if (t.endsWith("ms")) return Integer.parseInt(t.substring(0, t.length() - 2));
if (t.endsWith("s")) return Integer.parseInt(t.substring(0, t.length() - 1)) * 1000;
if (t.endsWith("m")) return Integer.parseInt(t.substring(0, t.length() - 1)) * 60_000;
return Integer.parseInt(t);
} catch (Exception e) {
return 60000;
}
}
}
@@ -0,0 +1,64 @@
package org.dromara.ai.config;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.provider.AiProviderManager;
import org.dromara.ai.provider.DifyProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Configuration
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "ruoyi.ai.ai-platform.dify", name = "enabled", havingValue = "true")
public class DifyPlatformConfig {
private final AiProviderManager providerManager;
@Value("${ruoyi.ai.ai-platform.dify.api-key:}")
private String apiKey;
@Value("${ruoyi.ai.ai-platform.dify.base-url:}")
private String baseUrl;
@Value("${ruoyi.ai.ai-platform.dify.default-app-id:}")
private String defaultAppId;
@Value("${ruoyi.ai.providers.dify.supported-models[0]:dify}")
private String supportedModel0;
@Value("${ruoyi.ai.external-ai.openai.timeout:60s}")
private String timeout;
@PostConstruct
public void register() {
if (apiKey == null || apiKey.isBlank() || baseUrl == null || baseUrl.isBlank() || defaultAppId == null || defaultAppId.isBlank()) {
log.warn("Dify 配置不完整,Provider 不注册");
return;
}
DifyProvider provider = new DifyProvider();
Map<String, Object> cfg = new HashMap<>();
cfg.put("api-key", apiKey);
cfg.put("base-url", baseUrl);
cfg.put("default-app-id", defaultAppId);
cfg.put("timeout-ms", parseTimeoutMs(timeout));
cfg.put("supported-models", List.of(supportedModel0));
provider.initialize(cfg);
providerManager.registerProvider(provider);
log.info("Dify Provider 已注册");
}
private int parseTimeoutMs(String t) {
try {
if (t == null || t.isBlank()) return 60000;
if (t.endsWith("ms")) return Integer.parseInt(t.substring(0, t.length() - 2));
if (t.endsWith("s")) return Integer.parseInt(t.substring(0, t.length() - 1)) * 1000;
if (t.endsWith("m")) return Integer.parseInt(t.substring(0, t.length() - 1)) * 60_000;
return Integer.parseInt(t);
} catch (Exception e) {
return 60000;
}
}
}
@@ -0,0 +1,64 @@
package org.dromara.ai.config;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.provider.AiProviderManager;
import org.dromara.ai.provider.FastGptProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Configuration
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "ruoyi.ai.ai-platform.fastgpt", name = "enabled", havingValue = "true")
public class FastGptPlatformConfig {
private final AiProviderManager providerManager;
@Value("${ruoyi.ai.ai-platform.fastgpt.api-key:}")
private String apiKey;
@Value("${ruoyi.ai.ai-platform.fastgpt.base-url:}")
private String baseUrl;
@Value("${ruoyi.ai.ai-platform.fastgpt.default-app-id:}")
private String defaultAppId;
@Value("${ruoyi.ai.providers.fastgpt.supported-models[0]:fastgpt}")
private String supportedModel0;
@Value("${ruoyi.ai.external-ai.openai.timeout:60s}")
private String timeout;
@PostConstruct
public void register() {
if (apiKey == null || apiKey.isBlank() || baseUrl == null || baseUrl.isBlank() || defaultAppId == null || defaultAppId.isBlank()) {
log.warn("FastGPT 配置不完整,Provider 不注册");
return;
}
FastGptProvider provider = new FastGptProvider();
Map<String, Object> cfg = new HashMap<>();
cfg.put("api-key", apiKey);
cfg.put("base-url", baseUrl);
cfg.put("default-app-id", defaultAppId);
cfg.put("timeout-ms", parseTimeoutMs(timeout));
cfg.put("supported-models", List.of(supportedModel0));
provider.initialize(cfg);
providerManager.registerProvider(provider);
log.info("FastGPT Provider 已注册");
}
private int parseTimeoutMs(String t) {
try {
if (t == null || t.isBlank()) return 60000;
if (t.endsWith("ms")) return Integer.parseInt(t.substring(0, t.length() - 2));
if (t.endsWith("s")) return Integer.parseInt(t.substring(0, t.length() - 1)) * 1000;
if (t.endsWith("m")) return Integer.parseInt(t.substring(0, t.length() - 1)) * 60_000;
return Integer.parseInt(t);
} catch (Exception e) {
return 60000;
}
}
}
@@ -0,0 +1,71 @@
package org.dromara.ai.config;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.provider.AiProviderManager;
import org.dromara.ai.provider.OpenAiProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Configuration
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "ruoyi.ai.external-ai.openai", name = "enabled", havingValue = "true")
public class OpenAiProviderConfig {
private final AiProviderManager providerManager;
@Value("${ruoyi.ai.external-ai.openai.api-key:}")
private String apiKey;
@Value("${ruoyi.ai.external-ai.openai.base-url:https://api.openai.com}")
private String baseUrl;
@Value("${ruoyi.ai.external-ai.openai.default-model:gpt-3.5-turbo}")
private String defaultModel;
@Value("${ruoyi.ai.providers.openai.supported-models[0]:gpt-3.5-turbo}")
private String supportedModel0;
@Value("${ruoyi.ai.providers.openai.supported-models[1]:gpt-4}")
private String supportedModel1;
@Value("${ruoyi.ai.external-ai.openai.timeout:60s}")
private String timeout;
@PostConstruct
public void register() {
if (apiKey == null || apiKey.isBlank()) {
log.warn("OpenAI api-key 未配置,Provider 不注册");
return;
}
OpenAiProvider provider = new OpenAiProvider();
Map<String, Object> cfg = new HashMap<>();
cfg.put("api-key", apiKey);
cfg.put("base-url", baseUrl);
cfg.put("default-model", defaultModel);
cfg.put("timeout-ms", parseTimeoutMs(timeout));
cfg.put("supported-models", List.of(supportedModel0, supportedModel1));
provider.initialize(cfg);
providerManager.registerProvider(provider);
log.info("OpenAI Provider 已注册");
}
private int parseTimeoutMs(String t) {
try {
if (t == null || t.isBlank()) return 60000;
if (t.endsWith("ms")) return Integer.parseInt(t.substring(0, t.length() - 2));
if (t.endsWith("s")) return Integer.parseInt(t.substring(0, t.length() - 1)) * 1000;
if (t.endsWith("m")) return Integer.parseInt(t.substring(0, t.length() - 1)) * 60_000;
return Integer.parseInt(t);
} catch (Exception e) {
return 60000;
}
}
}
@@ -0,0 +1,66 @@
package org.dromara.ai.config;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.provider.AiProviderManager;
import org.dromara.ai.provider.QwenProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Configuration
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "ruoyi.ai.external-ai.qwen", name = "enabled", havingValue = "true")
public class QwenProviderConfig {
private final AiProviderManager providerManager;
@Value("${ruoyi.ai.external-ai.qwen.api-key:}")
private String apiKey;
@Value("${ruoyi.ai.external-ai.qwen.base-url:https://dashscope.aliyuncs.com}")
private String baseUrl;
@Value("${ruoyi.ai.providers.qwen.default-model:qwen-turbo}")
private String defaultModel;
@Value("${ruoyi.ai.providers.qwen.supported-models[0]:qwen-turbo}")
private String supportedModel0;
@Value("${ruoyi.ai.providers.qwen.supported-models[1]:qwen-plus}")
private String supportedModel1;
@Value("${ruoyi.ai.external-ai.openai.timeout:60s}")
private String timeout;
@PostConstruct
public void register() {
if (apiKey == null || apiKey.isBlank()) {
log.warn("Qwen api-key 未配置,Provider 不注册");
return;
}
QwenProvider provider = new QwenProvider();
Map<String, Object> cfg = new HashMap<>();
cfg.put("api-key", apiKey);
cfg.put("base-url", baseUrl);
cfg.put("default-model", defaultModel);
cfg.put("timeout-ms", parseTimeoutMs(timeout));
cfg.put("supported-models", List.of(supportedModel0, supportedModel1));
provider.initialize(cfg);
providerManager.registerProvider(provider);
log.info("Qwen Provider 已注册");
}
private int parseTimeoutMs(String t) {
try {
if (t == null || t.isBlank()) return 60000;
if (t.endsWith("ms")) return Integer.parseInt(t.substring(0, t.length() - 2));
if (t.endsWith("s")) return Integer.parseInt(t.substring(0, t.length() - 1)) * 1000;
if (t.endsWith("m")) return Integer.parseInt(t.substring(0, t.length() - 1)) * 60_000;
return Integer.parseInt(t);
} catch (Exception e) {
return 60000;
}
}
}
@@ -0,0 +1,66 @@
package org.dromara.ai.config;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.provider.AiProviderManager;
import org.dromara.ai.provider.ZhipuAiProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Configuration
@RequiredArgsConstructor
@ConditionalOnProperty(prefix = "ruoyi.ai.external-ai.zhipu", name = "enabled", havingValue = "true")
public class ZhipuAiProviderConfig {
private final AiProviderManager providerManager;
@Value("${ruoyi.ai.external-ai.zhipu.api-key:}")
private String apiKey;
@Value("${ruoyi.ai.external-ai.zhipu.base-url:https://open.bigmodel.cn}")
private String baseUrl;
@Value("${ruoyi.ai.providers.zhipu.default-model:glm-4}")
private String defaultModel;
@Value("${ruoyi.ai.providers.zhipu.supported-models[0]:glm-4}")
private String supportedModel0;
@Value("${ruoyi.ai.providers.zhipu.supported-models[1]:glm-3-turbo}")
private String supportedModel1;
@Value("${ruoyi.ai.external-ai.openai.timeout:60s}")
private String timeout;
@PostConstruct
public void register() {
if (apiKey == null || apiKey.isBlank()) {
log.warn("ZhipuAI api-key 未配置,Provider 不注册");
return;
}
ZhipuAiProvider provider = new ZhipuAiProvider();
Map<String, Object> cfg = new HashMap<>();
cfg.put("api-key", apiKey);
cfg.put("base-url", baseUrl);
cfg.put("default-model", defaultModel);
cfg.put("timeout-ms", parseTimeoutMs(timeout));
cfg.put("supported-models", List.of(supportedModel0, supportedModel1));
provider.initialize(cfg);
providerManager.registerProvider(provider);
log.info("ZhipuAI Provider 已注册");
}
private int parseTimeoutMs(String t) {
try {
if (t == null || t.isBlank()) return 60000;
if (t.endsWith("ms")) return Integer.parseInt(t.substring(0, t.length() - 2));
if (t.endsWith("s")) return Integer.parseInt(t.substring(0, t.length() - 1)) * 1000;
if (t.endsWith("m")) return Integer.parseInt(t.substring(0, t.length() - 1)) * 60_000;
return Integer.parseInt(t);
} catch (Exception e) {
return 60000;
}
}
}
@@ -0,0 +1,102 @@
package org.dromara.ai.controller.admin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.ai.domain.bo.AiChatRecordBo;
import org.dromara.ai.domain.vo.AiChatRecordVo;
import org.dromara.ai.service.AiChatRecordService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
* AI聊天记录管理
*
* @author pengles
*/
@Tag(name = "管理端-AI聊天记录管理", description = "AI聊天记录管理接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/chat/record")
public class AdminAiChatRecordController extends BaseController {
private final AiChatRecordService aiChatRecordService;
/**
* 查询AI聊天记录列表
*/
@Operation(summary = "查询AI聊天记录列表", description = "分页查询AI聊天记录列表,支持多条件搜索")
@SaCheckPermission("ai:chat:record:list")
@GetMapping("/list")
public TableDataInfo<AiChatRecordVo> list(@Parameter(description = "聊天记录查询条件") AiChatRecordBo bo,
@Parameter(description = "分页参数") PageQuery pageQuery) {
return aiChatRecordService.queryPageList(bo, pageQuery);
}
/**
* 导出AI聊天记录列表
*/
@Operation(summary = "导出AI聊天记录列表", description = "导出AI聊天记录到Excel")
@SaCheckPermission("ai:chat:record:export")
@Log(title = "AI聊天记录", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(@Parameter(description = "聊天记录查询条件") AiChatRecordBo bo,
@Parameter(description = "响应对象") HttpServletResponse response) {
List<AiChatRecordVo> list = aiChatRecordService.queryList(bo);
ExcelUtil.exportExcel(list, "AI聊天记录", AiChatRecordVo.class, response);
}
/**
* 获取AI聊天记录详细信息
*/
@Operation(summary = "获取AI聊天记录详情", description = "根据记录ID获取详细信息")
@SaCheckPermission("ai:chat:record:query")
@GetMapping("/{id}")
public R<AiChatRecordVo> getInfo(@Parameter(description = "记录ID", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(aiChatRecordService.queryById(id));
}
/**
* 批量删除AI聊天记录
*/
@Operation(summary = "批量删除AI聊天记录", description = "根据ID数组批量删除AI聊天记录")
@SaCheckPermission("ai:chat:record:remove")
@Log(title = "AI聊天记录", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@Parameter(description = "记录ID数组", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(aiChatRecordService.deleteWithValidByIds(Arrays.asList(ids), true));
}
/**
* 归档聊天记录(删除指定日期之前的记录)
*/
@Operation(summary = "归档聊天记录", description = "删除指定日期之前的聊天记录")
@SaCheckPermission("ai:chat:record:remove")
@Log(title = "AI聊天记录归档", businessType = BusinessType.DELETE)
@PostMapping("/archive")
public R<Integer> archive(@Parameter(description = "归档日期(删除此日期之前的记录)", required = true)
@RequestParam java.util.Date beforeDate) {
Integer count = aiChatRecordService.archiveChatRecords(beforeDate);
return R.ok(count);
}
}
@@ -0,0 +1,89 @@
package org.dromara.ai.controller.admin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.ai.domain.bo.AiPaintingFavoriteBo;
import org.dromara.ai.domain.vo.AiPaintingFavoriteVo;
import org.dromara.ai.service.AiPaintingFavoriteService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
* AI绘画作品收藏管理
*
* @author pengles
*/
@Tag(name = "管理端-AI绘画作品收藏管理", description = "AI绘画作品收藏管理接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/painting/favorite")
public class AdminAiPaintingFavoriteController extends BaseController {
private final AiPaintingFavoriteService aiPaintingFavoriteService;
/**
* 查询AI绘画作品收藏列表
*/
@Operation(summary = "查询AI绘画作品收藏列表", description = "分页查询AI绘画作品收藏列表,支持多条件搜索")
@SaCheckPermission("ai:painting:favorite:list")
@GetMapping("/list")
public TableDataInfo<AiPaintingFavoriteVo> list(@Parameter(description = "收藏查询条件") AiPaintingFavoriteBo bo,
@Parameter(description = "分页参数") PageQuery pageQuery) {
return aiPaintingFavoriteService.queryPageList(bo, pageQuery);
}
/**
* 导出AI绘画作品收藏列表
*/
@Operation(summary = "导出AI绘画作品收藏列表", description = "导出AI绘画作品收藏到Excel")
@SaCheckPermission("ai:painting:favorite:export")
@Log(title = "AI绘画作品收藏", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(@Parameter(description = "收藏查询条件") AiPaintingFavoriteBo bo,
@Parameter(description = "响应对象") HttpServletResponse response) {
List<AiPaintingFavoriteVo> list = aiPaintingFavoriteService.queryList(bo);
ExcelUtil.exportExcel(list, "AI绘画作品收藏", AiPaintingFavoriteVo.class, response);
}
/**
* 获取AI绘画作品收藏详细信息
*/
@Operation(summary = "获取AI绘画作品收藏详情", description = "根据收藏ID获取详细信息")
@SaCheckPermission("ai:painting:favorite:query")
@GetMapping("/{id}")
public R<AiPaintingFavoriteVo> getInfo(@Parameter(description = "收藏ID", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(aiPaintingFavoriteService.queryById(id));
}
/**
* 批量删除AI绘画作品收藏
*/
@Operation(summary = "批量删除AI绘画作品收藏", description = "根据ID数组批量删除AI绘画作品收藏")
@SaCheckPermission("ai:painting:favorite:remove")
@Log(title = "AI绘画作品收藏", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@Parameter(description = "收藏ID数组", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(aiPaintingFavoriteService.deleteWithValidByIds(Arrays.asList(ids), true));
}
}
@@ -0,0 +1,129 @@
package org.dromara.ai.controller.admin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.ai.domain.bo.AiPaintingRecordBo;
import org.dromara.ai.domain.vo.AiPaintingRecordVo;
import org.dromara.ai.service.AiPaintingRecordService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* AI绘画历史记录管理
*
* @author pengles
*/
@Tag(name = "管理端-AI绘画历史记录管理", description = "AI绘画历史记录管理接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/painting/record")
public class AdminAiPaintingRecordController extends BaseController {
private final AiPaintingRecordService aiPaintingRecordService;
/**
* 查询AI绘画历史记录列表
*/
@Operation(summary = "查询AI绘画历史记录列表", description = "分页查询AI绘画历史记录列表,支持多条件搜索")
@SaCheckPermission("ai:painting:record:list")
@GetMapping("/list")
public TableDataInfo<AiPaintingRecordVo> list(@Parameter(description = "绘画历史记录查询条件") AiPaintingRecordBo bo,
@Parameter(description = "分页参数") PageQuery pageQuery) {
return aiPaintingRecordService.queryPageList(bo, pageQuery);
}
/**
* 导出AI绘画历史记录列表
*/
@Operation(summary = "导出AI绘画历史记录列表", description = "导出AI绘画历史记录到Excel")
@SaCheckPermission("ai:painting:record:export")
@Log(title = "AI绘画历史记录", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(@Parameter(description = "绘画历史记录查询条件") AiPaintingRecordBo bo,
@Parameter(description = "响应对象") HttpServletResponse response) {
List<AiPaintingRecordVo> list = aiPaintingRecordService.queryList(bo);
ExcelUtil.exportExcel(list, "AI绘画历史记录", AiPaintingRecordVo.class, response);
}
/**
* 获取AI绘画历史记录详细信息
*/
@Operation(summary = "获取AI绘画历史记录详情", description = "根据记录ID获取详细信息")
@SaCheckPermission("ai:painting:record:query")
@GetMapping("/{id}")
public R<AiPaintingRecordVo> getInfo(@Parameter(description = "记录ID", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(aiPaintingRecordService.queryById(id));
}
/**
* 根据任务ID获取记录
*/
@Operation(summary = "根据任务ID获取记录", description = "根据任务ID获取绘画记录")
@SaCheckPermission("ai:painting:record:query")
@GetMapping("/task/{taskId}")
public R<AiPaintingRecordVo> getByTaskId(@Parameter(description = "任务ID", required = true)
@NotNull(message = "任务ID不能为空")
@PathVariable String taskId) {
return R.ok(aiPaintingRecordService.queryByTaskId(taskId));
}
/**
* 批量删除AI绘画历史记录
*/
@Operation(summary = "批量删除AI绘画历史记录", description = "根据ID数组批量删除AI绘画历史记录")
@SaCheckPermission("ai:painting:record:remove")
@Log(title = "AI绘画历史记录", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@Parameter(description = "记录ID数组", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(aiPaintingRecordService.deleteWithValidByIds(Arrays.asList(ids), true));
}
/**
* 生成分享码
*/
@Operation(summary = "生成分享码", description = "为指定的绘画作品生成分享码")
@SaCheckPermission("ai:painting:record:edit")
@Log(title = "AI绘画历史记录分享", businessType = BusinessType.UPDATE)
@PostMapping("/{id}/share")
public R<Map<String, String>> generateShareCode(@Parameter(description = "记录ID", required = true)
@NotNull(message = "记录ID不能为空")
@PathVariable Long id) {
String shareCode = aiPaintingRecordService.generateShareCode(id);
return R.ok(Map.of("shareCode", shareCode));
}
/**
* 取消分享
*/
@Operation(summary = "取消分享", description = "取消指定绘画作品的分享")
@SaCheckPermission("ai:painting:record:edit")
@Log(title = "AI绘画历史记录取消分享", businessType = BusinessType.UPDATE)
@DeleteMapping("/{id}/share")
public R<Void> cancelShare(@Parameter(description = "记录ID", required = true)
@NotNull(message = "记录ID不能为空")
@PathVariable Long id) {
return toAjax(aiPaintingRecordService.cancelShare(id));
}
}
@@ -0,0 +1,112 @@
package org.dromara.ai.controller.admin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.ai.domain.bo.AiPaintingTemplateBo;
import org.dromara.ai.domain.vo.AiPaintingTemplateVo;
import org.dromara.ai.service.AiPaintingTemplateService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
* AI绘画参数模板管理
*
* @author pengles
*/
@Tag(name = "管理端-AI绘画参数模板管理", description = "AI绘画参数模板管理接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/painting/template")
public class AdminAiPaintingTemplateController extends BaseController {
private final AiPaintingTemplateService aiPaintingTemplateService;
/**
* 查询AI绘画参数模板列表
*/
@Operation(summary = "查询AI绘画参数模板列表", description = "分页查询AI绘画参数模板列表,支持多条件搜索")
@SaCheckPermission("ai:painting:template:list")
@GetMapping("/list")
public TableDataInfo<AiPaintingTemplateVo> list(@Parameter(description = "模板查询条件") AiPaintingTemplateBo bo,
@Parameter(description = "分页参数") PageQuery pageQuery) {
return aiPaintingTemplateService.queryPageList(bo, pageQuery);
}
/**
* 导出AI绘画参数模板列表
*/
@Operation(summary = "导出AI绘画参数模板列表", description = "导出AI绘画参数模板到Excel")
@SaCheckPermission("ai:painting:template:export")
@Log(title = "AI绘画参数模板", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(@Parameter(description = "模板查询条件") AiPaintingTemplateBo bo,
@Parameter(description = "响应对象") HttpServletResponse response) {
List<AiPaintingTemplateVo> list = aiPaintingTemplateService.queryList(bo);
ExcelUtil.exportExcel(list, "AI绘画参数模板", AiPaintingTemplateVo.class, response);
}
/**
* 获取AI绘画参数模板详细信息
*/
@Operation(summary = "获取AI绘画参数模板详情", description = "根据模板ID获取详细信息")
@SaCheckPermission("ai:painting:template:query")
@GetMapping("/{id}")
public R<AiPaintingTemplateVo> getInfo(@Parameter(description = "模板ID", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(aiPaintingTemplateService.queryById(id));
}
/**
* 新增AI绘画参数模板
*/
@Operation(summary = "新增AI绘画参数模板", description = "新增AI绘画参数模板")
@SaCheckPermission("ai:painting:template:add")
@Log(title = "AI绘画参数模板", businessType = BusinessType.INSERT)
@PostMapping
public R<Void> add(@Valid @RequestBody AiPaintingTemplateBo bo) {
return toAjax(aiPaintingTemplateService.insertByBo(bo));
}
/**
* 修改AI绘画参数模板
*/
@Operation(summary = "修改AI绘画参数模板", description = "修改AI绘画参数模板")
@SaCheckPermission("ai:painting:template:edit")
@Log(title = "AI绘画参数模板", businessType = BusinessType.UPDATE)
@PutMapping
public R<Void> edit(@Valid @RequestBody AiPaintingTemplateBo bo) {
return toAjax(aiPaintingTemplateService.updateByBo(bo));
}
/**
* 批量删除AI绘画参数模板
*/
@Operation(summary = "批量删除AI绘画参数模板", description = "根据ID数组批量删除AI绘画参数模板")
@SaCheckPermission("ai:painting:template:remove")
@Log(title = "AI绘画参数模板", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@Parameter(description = "模板ID数组", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(aiPaintingTemplateService.deleteWithValidByIds(Arrays.asList(ids), true));
}
}
@@ -0,0 +1,138 @@
package org.dromara.ai.controller.admin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.ai.domain.bo.AiProviderConfigBo;
import org.dromara.ai.domain.vo.AiProviderConfigVo;
import org.dromara.ai.service.AiProviderConfigService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
* AI Provider配置管理
*
* @author pengles
*/
@Tag(name = "管理端-AI Provider配置管理", description = "AI Provider配置管理接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/provider/config")
public class AdminAiProviderConfigController extends BaseController {
private final AiProviderConfigService aiProviderConfigService;
/**
* 查询AI Provider配置列表
*/
@Operation(summary = "查询AI Provider配置列表", description = "分页查询AI Provider配置列表,支持多条件搜索")
@SaCheckPermission("ai:provider:config:list")
@GetMapping("/list")
public TableDataInfo<AiProviderConfigVo> list(@Parameter(description = "Provider配置查询条件") AiProviderConfigBo bo,
@Parameter(description = "分页参数") PageQuery pageQuery) {
return aiProviderConfigService.queryPageList(bo, pageQuery);
}
/**
* 导出AI Provider配置列表
*/
@Operation(summary = "导出AI Provider配置列表", description = "导出AI Provider配置到Excel")
@SaCheckPermission("ai:provider:config:export")
@Log(title = "AI Provider配置", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(@Parameter(description = "Provider配置查询条件") AiProviderConfigBo bo,
@Parameter(description = "响应对象") HttpServletResponse response) {
List<AiProviderConfigVo> list = aiProviderConfigService.queryList(bo);
ExcelUtil.exportExcel(list, "AI Provider配置", AiProviderConfigVo.class, response);
}
/**
* 获取AI Provider配置详细信息
*/
@Operation(summary = "获取AI Provider配置详情", description = "根据配置ID获取详细信息")
@SaCheckPermission("ai:provider:config:query")
@GetMapping("/{id}")
public R<AiProviderConfigVo> getInfo(@Parameter(description = "配置ID", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(aiProviderConfigService.queryById(id));
}
/**
* 新增AI Provider配置
*/
@Operation(summary = "新增AI Provider配置", description = "新增AI Provider配置")
@SaCheckPermission("ai:provider:config:add")
@Log(title = "AI Provider配置", businessType = BusinessType.INSERT)
@PostMapping
public R<Void> add(@Validated @RequestBody AiProviderConfigBo bo) {
return toAjax(aiProviderConfigService.insertByBo(bo));
}
/**
* 修改AI Provider配置
*/
@Operation(summary = "修改AI Provider配置", description = "修改AI Provider配置")
@SaCheckPermission("ai:provider:config:edit")
@Log(title = "AI Provider配置", businessType = BusinessType.UPDATE)
@PutMapping
public R<Void> edit(@Validated @RequestBody AiProviderConfigBo bo) {
return toAjax(aiProviderConfigService.updateByBo(bo));
}
/**
* 批量删除AI Provider配置
*/
@Operation(summary = "批量删除AI Provider配置", description = "根据ID数组批量删除AI Provider配置")
@SaCheckPermission("ai:provider:config:remove")
@Log(title = "AI Provider配置", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@Parameter(description = "配置ID数组", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(aiProviderConfigService.deleteWithValidByIds(Arrays.asList(ids), true));
}
/**
* 启用/禁用Provider配置
*/
@Operation(summary = "启用/禁用Provider配置", description = "启用或禁用AI Provider配置")
@SaCheckPermission("ai:provider:config:edit")
@Log(title = "AI Provider配置", businessType = BusinessType.UPDATE)
@PutMapping("/changeEnabled")
public R<Void> changeEnabled(@Parameter(description = "配置ID", required = true)
@NotNull(message = "配置ID不能为空")
@RequestParam Long id,
@Parameter(description = "启用状态(0=禁用 1=启用)", required = true)
@NotNull(message = "启用状态不能为空")
@RequestParam Integer enabled) {
return toAjax(aiProviderConfigService.changeEnabled(id, enabled));
}
/**
* 根据Provider名称查询配置
*/
@Operation(summary = "根据Provider名称查询配置", description = "根据Provider名称获取配置信息")
@SaCheckPermission("ai:provider:config:query")
@GetMapping("/name/{providerName}")
public R<AiProviderConfigVo> getByProviderName(@Parameter(description = "Provider名称", required = true)
@PathVariable String providerName) {
return R.ok(aiProviderConfigService.queryByProviderName(providerName));
}
}
@@ -0,0 +1,101 @@
package org.dromara.ai.controller.admin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.ai.domain.bo.AiProviderConfigHistoryBo;
import org.dromara.ai.domain.vo.AiProviderConfigHistoryVo;
import org.dromara.ai.service.AiProviderConfigHistoryService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
* AI Provider配置历史管理
*
* @author pengles
*/
@Tag(name = "管理端-AI Provider配置历史管理", description = "AI Provider配置历史管理接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/provider/config/history")
public class AdminAiProviderConfigHistoryController extends BaseController {
private final AiProviderConfigHistoryService aiProviderConfigHistoryService;
/**
* 查询AI Provider配置历史列表
*/
@Operation(summary = "查询AI Provider配置历史列表", description = "分页查询AI Provider配置历史列表,支持多条件搜索")
@SaCheckPermission("ai:provider:config:history:list")
@GetMapping("/list")
public TableDataInfo<AiProviderConfigHistoryVo> list(@Parameter(description = "配置历史查询条件") AiProviderConfigHistoryBo bo,
@Parameter(description = "分页参数") PageQuery pageQuery) {
return aiProviderConfigHistoryService.queryPageList(bo, pageQuery);
}
/**
* 导出AI Provider配置历史列表
*/
@Operation(summary = "导出AI Provider配置历史列表", description = "导出AI Provider配置历史到Excel")
@SaCheckPermission("ai:provider:config:history:export")
@Log(title = "AI Provider配置历史", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(@Parameter(description = "配置历史查询条件") AiProviderConfigHistoryBo bo,
@Parameter(description = "响应对象") HttpServletResponse response) {
List<AiProviderConfigHistoryVo> list = aiProviderConfigHistoryService.queryList(bo);
ExcelUtil.exportExcel(list, "AI Provider配置历史", AiProviderConfigHistoryVo.class, response);
}
/**
* 获取AI Provider配置历史详细信息
*/
@Operation(summary = "获取AI Provider配置历史详情", description = "根据记录ID获取详细信息")
@SaCheckPermission("ai:provider:config:history:query")
@GetMapping("/{id}")
public R<AiProviderConfigHistoryVo> getInfo(@Parameter(description = "记录ID", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(aiProviderConfigHistoryService.queryById(id));
}
/**
* 根据配置ID查询历史记录列表
*/
@Operation(summary = "根据配置ID查询历史记录", description = "根据配置ID查询该配置的所有历史记录")
@SaCheckPermission("ai:provider:config:history:query")
@GetMapping("/config/{configId}")
public R<List<AiProviderConfigHistoryVo>> getHistoryByConfigId(@Parameter(description = "配置ID", required = true)
@NotNull(message = "配置ID不能为空")
@PathVariable Long configId) {
return R.ok(aiProviderConfigHistoryService.queryByConfigId(configId));
}
/**
* 批量删除AI Provider配置历史
*/
@Operation(summary = "批量删除AI Provider配置历史", description = "根据ID数组批量删除AI Provider配置历史")
@SaCheckPermission("ai:provider:config:history:remove")
@Log(title = "AI Provider配置历史", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@Parameter(description = "记录ID数组", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(aiProviderConfigHistoryService.deleteWithValidByIds(Arrays.asList(ids), true));
}
}
@@ -0,0 +1,170 @@
package org.dromara.ai.controller.admin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.monitor.AiMonitorService;
import org.dromara.ai.provider.AiProvider;
import org.dromara.ai.provider.AiProviderManager;
import org.dromara.common.core.domain.R;
import org.dromara.common.web.core.BaseController;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* AI提供商管理
*
* @author pengles
*/
@Tag(name = "管理端-AI提供商管理", description = "AI提供商管理和监控接口")
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/provider")
public class AdminAiProviderController extends BaseController {
private final AiProviderManager providerManager;
private final AiMonitorService monitorService;
/**
* 获取所有提供商列表
*/
@Operation(summary = "提供商列表", description = "获取所有已注册的AI服务提供商信息")
@SaCheckPermission("ai:provider:list")
@GetMapping("/list")
public R<List<Map<String, Object>>> list() {
try {
List<Map<String, Object>> items = providerManager.getAllProviders().stream().map(p -> Map.of(
"name", p.getProviderName(),
"type", p.getProviderType().getCode(),
"version", p.getVersion(),
"available", p.isAvailable(),
"statistics", p.getStatistics()
)).collect(Collectors.toList());
log.debug("获取提供商列表,共{}个提供商", items.size());
return R.ok(items);
} catch (Exception e) {
log.error("获取提供商列表失败", e);
return R.fail("获取提供商列表失败: " + e.getMessage());
}
}
/**
* 获取支持的模型列表
*/
@Operation(summary = "支持的模型列表", description = "获取所有提供商支持的所有模型信息")
@SaCheckPermission("ai:provider:list")
@GetMapping("/models")
public R<List<Object>> models() {
try {
List<Object> models = providerManager.getAllSupportedModels().stream().map(m -> Map.of(
"id", m.getId(),
"name", m.getName(),
"provider", m.getProvider(),
"capabilities", m.getCapabilities(),
"type", m.getType() != null ? m.getType().getCode() : null
)).collect(Collectors.toList());
log.debug("获取模型列表,共{}个模型", models.size());
return R.ok(models);
} catch (Exception e) {
log.error("获取模型列表失败", e);
return R.fail("获取模型列表失败: " + e.getMessage());
}
}
/**
* 获取默认提供商
*/
@Operation(summary = "获取默认提供商", description = "获取当前系统默认的AI服务提供商")
@SaCheckPermission("ai:provider:query")
@GetMapping("/default")
public R<Map<String, Object>> getDefault() {
try {
AiProvider p = providerManager.getDefaultProvider();
if (p == null) {
log.warn("未设置默认提供商");
return R.fail("NO_DEFAULT_PROVIDER");
}
return R.ok(Map.of("name", p.getProviderName(), "version", p.getVersion(), "type", p.getProviderType().getCode()));
} catch (Exception e) {
log.error("获取默认提供商失败", e);
return R.fail("获取默认提供商失败: " + e.getMessage());
}
}
/**
* 设置默认提供商
*/
@Operation(summary = "设置默认提供商", description = "设置系统默认的AI服务提供商")
@SaCheckPermission("ai:provider:edit")
@PostMapping("/setDefault")
public R<Void> setDefault(@Parameter(description = "提供商名称") @RequestParam String name) {
try {
providerManager.setDefaultProvider(name);
log.info("设置默认提供商: {}", name);
return R.ok();
} catch (Exception e) {
log.error("设置默认提供商失败: {}", name, e);
return R.fail("设置默认提供商失败: " + e.getMessage());
}
}
/**
* 获取系统统计信息
*/
@Operation(summary = "系统统计", description = "获取系统级别的AI使用统计信息")
@SaCheckPermission("ai:provider:query")
@GetMapping("/stats/system")
public R<Map<String, Object>> getSystemStats() {
try {
Map<String, Object> stats = monitorService.getSystemStats();
return R.ok(stats);
} catch (Exception e) {
log.error("获取系统统计失败", e);
return R.fail("获取系统统计失败: " + e.getMessage());
}
}
/**
* 获取各Provider统计信息
*/
@Operation(summary = "Provider统计", description = "获取所有Provider的使用统计信息")
@SaCheckPermission("ai:provider:query")
@GetMapping("/stats/providers")
public R<List<Map<String, Object>>> getProviderStats() {
try {
List<Map<String, Object>> stats = monitorService.getAllProviderStats();
return R.ok(stats);
} catch (Exception e) {
log.error("获取Provider统计失败", e);
return R.fail("获取Provider统计失败: " + e.getMessage());
}
}
/**
* 重置统计信息
*/
@Operation(summary = "重置统计", description = "重置所有提供商的统计信息")
@SaCheckPermission("ai:provider:edit")
@PostMapping("/stats/reset")
public R<Void> resetStats() {
try {
monitorService.resetStats();
providerManager.resetAllStatistics();
log.info("重置统计信息成功");
return R.ok();
} catch (Exception e) {
log.error("重置统计失败", e);
return R.fail("重置统计失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,102 @@
package org.dromara.ai.controller.admin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.ai.domain.bo.AiRatingRecordBo;
import org.dromara.ai.domain.vo.AiRatingRecordVo;
import org.dromara.ai.service.AiRatingRecordService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* AI评价记录管理
*
* @author pengles
*/
@Tag(name = "管理端-AI评价记录管理", description = "AI评价记录管理接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/rating/record")
public class AdminAiRatingRecordController extends BaseController {
private final AiRatingRecordService aiRatingRecordService;
/**
* 查询AI评价记录列表
*/
@Operation(summary = "查询AI评价记录列表", description = "分页查询AI评价记录列表,支持多条件搜索")
@SaCheckPermission("ai:rating:record:list")
@GetMapping("/list")
public TableDataInfo<AiRatingRecordVo> list(@Parameter(description = "评价记录查询条件") AiRatingRecordBo bo,
@Parameter(description = "分页参数") PageQuery pageQuery) {
return aiRatingRecordService.queryPageList(bo, pageQuery);
}
/**
* 导出AI评价记录列表
*/
@Operation(summary = "导出AI评价记录列表", description = "导出AI评价记录到Excel")
@SaCheckPermission("ai:rating:record:export")
@Log(title = "AI评价记录", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(@Parameter(description = "评价记录查询条件") AiRatingRecordBo bo,
@Parameter(description = "响应对象") HttpServletResponse response) {
List<AiRatingRecordVo> list = aiRatingRecordService.queryList(bo);
ExcelUtil.exportExcel(list, "AI评价记录", AiRatingRecordVo.class, response);
}
/**
* 获取AI评价记录详细信息
*/
@Operation(summary = "获取AI评价记录详情", description = "根据记录ID获取详细信息")
@SaCheckPermission("ai:rating:record:query")
@GetMapping("/{id}")
public R<AiRatingRecordVo> getInfo(@Parameter(description = "记录ID", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(aiRatingRecordService.queryById(id));
}
/**
* 批量删除AI评价记录
*/
@Operation(summary = "批量删除AI评价记录", description = "根据ID数组批量删除AI评价记录")
@SaCheckPermission("ai:rating:record:remove")
@Log(title = "AI评价记录", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@Parameter(description = "记录ID数组", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(aiRatingRecordService.deleteWithValidByIds(Arrays.asList(ids), true));
}
/**
* 获取平均评分
*/
@Operation(summary = "获取平均评分", description = "获取指定Provider的平均评分")
@SaCheckPermission("ai:rating:record:query")
@GetMapping("/average/{providerName}")
public R<Map<String, Object>> getAverageRating(@Parameter(description = "Provider名称", required = true)
@PathVariable String providerName) {
Double averageRating = aiRatingRecordService.getAverageRating(providerName);
return R.ok(Map.of("providerName", providerName, "averageRating", averageRating));
}
}
@@ -0,0 +1,128 @@
package org.dromara.ai.controller.admin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.ai.domain.bo.AiSensitiveWordBo;
import org.dromara.ai.domain.vo.AiSensitiveWordVo;
import org.dromara.ai.service.AiSensitiveWordService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
* AI敏感词管理
*
* @author pengles
*/
@Tag(name = "管理端-AI敏感词管理", description = "AI敏感词管理接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/sensitive/word")
public class AdminAiSensitiveWordController extends BaseController {
private final AiSensitiveWordService aiSensitiveWordService;
/**
* 查询AI敏感词列表
*/
@Operation(summary = "查询AI敏感词列表", description = "分页查询AI敏感词列表,支持多条件搜索")
@SaCheckPermission("ai:sensitive:word:list")
@GetMapping("/list")
public TableDataInfo<AiSensitiveWordVo> list(@Parameter(description = "敏感词查询条件") AiSensitiveWordBo bo,
@Parameter(description = "分页参数") PageQuery pageQuery) {
return aiSensitiveWordService.queryPageList(bo, pageQuery);
}
/**
* 导出AI敏感词列表
*/
@Operation(summary = "导出AI敏感词列表", description = "导出AI敏感词到Excel")
@SaCheckPermission("ai:sensitive:word:export")
@Log(title = "AI敏感词", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(@Parameter(description = "敏感词查询条件") AiSensitiveWordBo bo,
@Parameter(description = "响应对象") HttpServletResponse response) {
List<AiSensitiveWordVo> list = aiSensitiveWordService.queryList(bo);
ExcelUtil.exportExcel(list, "AI敏感词", AiSensitiveWordVo.class, response);
}
/**
* 获取AI敏感词详细信息
*/
@Operation(summary = "获取AI敏感词详情", description = "根据ID获取详细信息")
@SaCheckPermission("ai:sensitive:word:query")
@GetMapping("/{id}")
public R<AiSensitiveWordVo> getInfo(@Parameter(description = "敏感词ID", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(aiSensitiveWordService.queryById(id));
}
/**
* 新增AI敏感词
*/
@Operation(summary = "新增AI敏感词", description = "新增AI敏感词")
@SaCheckPermission("ai:sensitive:word:add")
@Log(title = "AI敏感词", businessType = BusinessType.INSERT)
@PostMapping
public R<Void> add(@Valid @RequestBody AiSensitiveWordBo bo) {
return toAjax(aiSensitiveWordService.insertByBo(bo));
}
/**
* 修改AI敏感词
*/
@Operation(summary = "修改AI敏感词", description = "修改AI敏感词")
@SaCheckPermission("ai:sensitive:word:edit")
@Log(title = "AI敏感词", businessType = BusinessType.UPDATE)
@PutMapping
public R<Void> edit(@Valid @RequestBody AiSensitiveWordBo bo) {
return toAjax(aiSensitiveWordService.updateByBo(bo));
}
/**
* 批量删除AI敏感词
*/
@Operation(summary = "批量删除AI敏感词", description = "根据ID数组批量删除AI敏感词")
@SaCheckPermission("ai:sensitive:word:remove")
@Log(title = "AI敏感词", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@Parameter(description = "敏感词ID数组", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(aiSensitiveWordService.deleteWithValidByIds(Arrays.asList(ids), true));
}
/**
* 启用/禁用敏感词
*/
@Operation(summary = "启用/禁用敏感词", description = "启用或禁用敏感词")
@SaCheckPermission("ai:sensitive:word:edit")
@Log(title = "AI敏感词", businessType = BusinessType.UPDATE)
@PutMapping("/changeEnabled")
public R<Void> changeEnabled(@Parameter(description = "敏感词ID", required = true)
@NotNull(message = "敏感词ID不能为空")
@RequestParam Long id,
@Parameter(description = "启用状态(0=禁用 1=启用)", required = true)
@NotNull(message = "启用状态不能为空")
@RequestParam Integer enabled) {
return toAjax(aiSensitiveWordService.changeEnabled(id, enabled));
}
}
@@ -0,0 +1,129 @@
package org.dromara.ai.controller.admin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.ai.domain.bo.AiUsageStatsBo;
import org.dromara.ai.domain.vo.AiUsageStatsVo;
import org.dromara.ai.service.AiUsageStatsService;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* AI使用统计管理
*
* @author pengles
*/
@Tag(name = "管理端-AI使用统计管理", description = "AI使用统计管理接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/usage/stats")
public class AdminAiUsageStatsController extends BaseController {
private final AiUsageStatsService aiUsageStatsService;
/**
* 查询AI使用统计列表
*/
@Operation(summary = "查询AI使用统计列表", description = "分页查询AI使用统计列表,支持多条件搜索")
@SaCheckPermission("ai:usage:stats:list")
@GetMapping("/list")
public TableDataInfo<AiUsageStatsVo> list(@Parameter(description = "使用统计查询条件") AiUsageStatsBo bo,
@Parameter(description = "分页参数") PageQuery pageQuery) {
return aiUsageStatsService.queryPageList(bo, pageQuery);
}
/**
* 导出AI使用统计列表
*/
@Operation(summary = "导出AI使用统计列表", description = "导出AI使用统计到Excel")
@SaCheckPermission("ai:usage:stats:export")
@Log(title = "AI使用统计", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(@Parameter(description = "使用统计查询条件") AiUsageStatsBo bo,
@Parameter(description = "响应对象") HttpServletResponse response) {
List<AiUsageStatsVo> list = aiUsageStatsService.queryList(bo);
ExcelUtil.exportExcel(list, "AI使用统计", AiUsageStatsVo.class, response);
}
/**
* 获取AI使用统计详细信息
*/
@Operation(summary = "获取AI使用统计详情", description = "根据统计ID获取详细信息")
@SaCheckPermission("ai:usage:stats:query")
@GetMapping("/{id}")
public R<AiUsageStatsVo> getInfo(@Parameter(description = "统计ID", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(aiUsageStatsService.queryById(id));
}
/**
* 获取汇总统计
*/
@Operation(summary = "获取汇总统计", description = "获取指定时间范围内的汇总统计数据")
@SaCheckPermission("ai:usage:stats:query")
@GetMapping("/summary")
public R<Map<String, Object>> getSummary(
@Parameter(description = "开始日期") @DateTimeFormat(pattern = "yyyy-MM-dd") @RequestParam(required = false) Date startDate,
@Parameter(description = "结束日期") @DateTimeFormat(pattern = "yyyy-MM-dd") @RequestParam(required = false) Date endDate) {
return R.ok(aiUsageStatsService.getSummaryStats(startDate, endDate));
}
/**
* 获取用户统计
*/
@Operation(summary = "获取用户统计", description = "获取指定用户的统计数据")
@SaCheckPermission("ai:usage:stats:query")
@GetMapping("/user/{userId}")
public R<Map<String, Object>> getUserStats(
@Parameter(description = "用户ID", required = true) @PathVariable @NotNull Long userId,
@Parameter(description = "开始日期") @DateTimeFormat(pattern = "yyyy-MM-dd") @RequestParam(required = false) Date startDate,
@Parameter(description = "结束日期") @DateTimeFormat(pattern = "yyyy-MM-dd") @RequestParam(required = false) Date endDate) {
return R.ok(aiUsageStatsService.getUserStats(userId, startDate, endDate));
}
/**
* 获取Provider统计
*/
@Operation(summary = "获取Provider统计", description = "获取指定Provider的统计数据")
@SaCheckPermission("ai:usage:stats:query")
@GetMapping("/provider/{providerName}")
public R<Map<String, Object>> getProviderStats(
@Parameter(description = "Provider名称", required = true) @PathVariable @NotNull String providerName,
@Parameter(description = "开始日期") @DateTimeFormat(pattern = "yyyy-MM-dd") @RequestParam(required = false) Date startDate,
@Parameter(description = "结束日期") @DateTimeFormat(pattern = "yyyy-MM-dd") @RequestParam(required = false) Date endDate) {
return R.ok(aiUsageStatsService.getProviderStats(providerName, startDate, endDate));
}
/**
* 获取趋势数据
*/
@Operation(summary = "获取趋势数据", description = "获取指定时间范围内的趋势数据(支持按日/周/月分组)")
@SaCheckPermission("ai:usage:stats:query")
@GetMapping("/trend")
public R<List<Map<String, Object>>> getTrend(
@Parameter(description = "开始日期") @DateTimeFormat(pattern = "yyyy-MM-dd") @RequestParam(required = false) Date startDate,
@Parameter(description = "结束日期") @DateTimeFormat(pattern = "yyyy-MM-dd") @RequestParam(required = false) Date endDate,
@Parameter(description = "分组方式(day/week/month") @RequestParam(defaultValue = "day") String groupBy) {
return R.ok(aiUsageStatsService.getTrendData(startDate, endDate, groupBy));
}
}
@@ -0,0 +1,139 @@
package org.dromara.ai.controller.admin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.ai.domain.bo.AiUserQuotaBo;
import org.dromara.ai.domain.vo.AiUserQuotaVo;
import org.dromara.ai.service.AiUserQuotaService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
* AI用户配额管理
*
* @author pengles
*/
@Tag(name = "管理端-AI用户配额管理", description = "AI用户配额管理接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/user/quota")
public class AdminAiUserQuotaController extends BaseController {
private final AiUserQuotaService aiUserQuotaService;
/**
* 查询AI用户配额列表
*/
@Operation(summary = "查询AI用户配额列表", description = "分页查询AI用户配额列表,支持多条件搜索")
@SaCheckPermission("ai:user:quota:list")
@GetMapping("/list")
public TableDataInfo<AiUserQuotaVo> list(@Parameter(description = "配额查询条件") AiUserQuotaBo bo,
@Parameter(description = "分页参数") PageQuery pageQuery) {
return aiUserQuotaService.queryPageList(bo, pageQuery);
}
/**
* 导出AI用户配额列表
*/
@Operation(summary = "导出AI用户配额列表", description = "导出AI用户配额到Excel")
@SaCheckPermission("ai:user:quota:export")
@Log(title = "AI用户配额", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(@Parameter(description = "配额查询条件") AiUserQuotaBo bo,
@Parameter(description = "响应对象") HttpServletResponse response) {
List<AiUserQuotaVo> list = aiUserQuotaService.queryList(bo);
ExcelUtil.exportExcel(list, "AI用户配额", AiUserQuotaVo.class, response);
}
/**
* 获取AI用户配额详细信息
*/
@Operation(summary = "获取AI用户配额详情", description = "根据配额ID获取详细信息")
@SaCheckPermission("ai:user:quota:query")
@GetMapping("/{id}")
public R<AiUserQuotaVo> getInfo(@Parameter(description = "配额ID", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(aiUserQuotaService.queryById(id));
}
/**
* 根据用户ID获取配额信息
*/
@Operation(summary = "根据用户ID获取配额信息", description = "根据用户ID获取配额信息")
@SaCheckPermission("ai:user:quota:query")
@GetMapping("/user/{userId}")
public R<AiUserQuotaVo> getByUserId(@Parameter(description = "用户ID", required = true)
@NotNull(message = "用户ID不能为空")
@PathVariable Long userId) {
return R.ok(aiUserQuotaService.queryByUserId(userId));
}
/**
* 新增AI用户配额
*/
@Operation(summary = "新增AI用户配额", description = "新增AI用户配额")
@SaCheckPermission("ai:user:quota:add")
@Log(title = "AI用户配额", businessType = BusinessType.INSERT)
@PostMapping
public R<Void> add(@Valid @RequestBody AiUserQuotaBo bo) {
return toAjax(aiUserQuotaService.insertByBo(bo));
}
/**
* 修改AI用户配额
*/
@Operation(summary = "修改AI用户配额", description = "修改AI用户配额")
@SaCheckPermission("ai:user:quota:edit")
@Log(title = "AI用户配额", businessType = BusinessType.UPDATE)
@PutMapping
public R<Void> edit(@Valid @RequestBody AiUserQuotaBo bo) {
return toAjax(aiUserQuotaService.updateByBo(bo));
}
/**
* 批量删除AI用户配额
*/
@Operation(summary = "批量删除AI用户配额", description = "根据ID数组批量删除AI用户配额")
@SaCheckPermission("ai:user:quota:remove")
@Log(title = "AI用户配额", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@Parameter(description = "配额ID数组", required = true)
@NotNull(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(aiUserQuotaService.deleteWithValidByIds(Arrays.asList(ids), true));
}
/**
* 重置用户配额
*/
@Operation(summary = "重置用户配额", description = "重置用户的日配额或月配额")
@SaCheckPermission("ai:user:quota:edit")
@Log(title = "AI用户配额", businessType = BusinessType.UPDATE)
@PostMapping("/reset/{userId}")
public R<Void> resetQuota(@Parameter(description = "用户ID", required = true)
@NotNull(message = "用户ID不能为空")
@PathVariable Long userId,
@Parameter(description = "重置类型(DAILY/MONTHLY)", required = true)
@RequestParam String resetType) {
return toAjax(aiUserQuotaService.resetQuota(userId, resetType));
}
}
@@ -0,0 +1,87 @@
package org.dromara.ai.controller.admin;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.dromara.ai.domain.dto.AiModelInfo;
import org.dromara.ai.service.AiModelService;
import org.dromara.common.core.domain.R;
import org.dromara.common.web.core.BaseController;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
import java.util.stream.Collectors;
/**
* 系统模型管理
*
* @author pengles
*/
@Tag(name = "管理端-系统模型管理", description = "系统模型管理接口")
@RestController
@RequiredArgsConstructor
@RequestMapping("/system/model")
public class AdminSysModelController extends BaseController {
private final AiModelService aiModelService;
/**
* 获取模型列表(用于前端下拉选择)
*/
@Operation(summary = "获取模型列表", description = "获取所有可用模型列表,用于前端下拉选择")
@GetMapping("/modelList")
public R<List<Map<String, Object>>> modelList() {
List<AiModelInfo> models = aiModelService.getAllModels();
List<Map<String, Object>> data = models.stream()
.filter(m -> Boolean.TRUE.equals(m.getAvailable()))
.filter(m -> m.getStatus() == null || m.getStatus() == AiModelInfo.ModelStatus.AVAILABLE)
.map(this::toFrontendModel)
.collect(Collectors.toList());
return R.ok(data);
}
/**
* 根据分类获取模型列表
*/
@Operation(summary = "根据分类获取模型列表", description = "根据类型或Provider分类获取模型列表")
@GetMapping("/list")
public R<List<Map<String, Object>>> listByCategory(
@Parameter(description = "分类(类型或Provider") @RequestParam(required = false) String category) {
List<AiModelInfo> models = aiModelService.getAllModels();
List<Map<String, Object>> data = models.stream()
.filter(m -> {
if (category == null || category.isBlank()) return true;
boolean byType = m.getType() != null && category.equalsIgnoreCase(m.getType().getCode());
boolean byProvider = m.getProvider() != null && category.equalsIgnoreCase(m.getProvider());
return byType || byProvider;
})
.map(this::toFrontendModel)
.collect(Collectors.toList());
return R.ok(data);
}
/**
* 转换为前端模型格式
*/
private Map<String, Object> toFrontendModel(AiModelInfo m) {
Map<String, Object> map = new LinkedHashMap<>();
map.put("modelName", m.getId());
map.put("modelDescribe", Optional.ofNullable(m.getDisplayName()).orElse(Optional.ofNullable(m.getName()).orElse(m.getId())));
List<String> caps = Optional.ofNullable(m.getCapabilities()).orElse(List.of())
.stream().map(AiModelInfo.ModelCapability::getCode).collect(Collectors.toList());
map.put("modelCapability", caps);
List<Map<String, String>> abilities = Optional.ofNullable(m.getCapabilities()).orElse(List.of())
.stream().map(c -> {
Map<String, String> a = new HashMap<>();
a.put("name", c.getCode());
a.put("description", c.getName());
return a;
}).collect(Collectors.toList());
map.put("modelAbilities", abilities);
return map;
}
}
@@ -0,0 +1,75 @@
package org.dromara.ai.controller.app;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.domain.dto.AiChatResponse;
import org.dromara.ai.service.AiCodingAssistantService;
import org.dromara.common.core.domain.R;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* AI代码助手控制器
* 提供代码分析和项目脚手架生成功能
*
* @author pengles
*/
@Slf4j
@RestController
@RequestMapping("/ai/assistant")
@RequiredArgsConstructor
public class AiAssistantController {
private final AiCodingAssistantService assistantService;
/**
* AI代码分析
*
* @param code 代码内容
* @param model 模型名称(可选)
* @param userId 用户ID(可选)
* @param params 额外参数(可选)
* @return 分析结果
*/
@PostMapping("/analyze")
@Log(title = "AI代码分析", businessType = BusinessType.OTHER)
public R<AiChatResponse> analyze(@RequestParam String code,
@RequestParam(required = false) String model,
@RequestParam(required = false) String userId,
@RequestBody(required = false) Map<String, Object> params) {
try {
AiChatResponse resp = assistantService.analyzeCode(code, model, params, userId);
return R.ok(resp);
} catch (Exception e) {
log.error("AI代码分析失败", e);
return R.fail("AI代码分析失败: " + e.getMessage());
}
}
/**
* AI项目脚手架生成
*
* @param appType 应用类型(如SpringBoot、Vue等)
* @param model 模型名称(可选)
* @param userId 用户ID(可选)
* @param options 选项参数(可选)
* @return 生成结果
*/
@PostMapping("/scaffold")
@Log(title = "AI脚手架生成", businessType = BusinessType.OTHER)
public R<AiChatResponse> scaffold(@RequestParam String appType,
@RequestParam(required = false) String model,
@RequestParam(required = false) String userId,
@RequestBody(required = false) Map<String, Object> options) {
try {
AiChatResponse resp = assistantService.generateScaffold(appType, options, model, userId);
return R.ok(resp);
} catch (Exception e) {
log.error("AI脚手架生成失败", e);
return R.fail("AI脚手架生成失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,284 @@
package org.dromara.ai.controller.app;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.domain.dto.AiChatRequest;
import org.dromara.ai.domain.dto.AiChatResponse;
import org.dromara.ai.service.AiChatService;
import org.dromara.common.core.domain.R;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.web.core.BaseController;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* AI聊天控制器
* 提供AI聊天相关的REST API接口
*
* @author pengles
*/
@Slf4j
@RestController
@RequestMapping("/ai/chat")
@RequiredArgsConstructor
public class AiChatController extends BaseController {
private final AiChatService aiChatService;
private final org.dromara.ai.service.AiChatHistoryService chatHistoryService;
/**
* 同步聊天对话
*
* @param request 聊天请求
* @return 聊天响应
*/
@PostMapping("/sync")
@Log(title = "AI聊天", businessType = BusinessType.OTHER)
public R<AiChatResponse> chat(@Validated @RequestBody AiChatRequest request) {
log.info("接收到AI聊天请求 - 用户: {}, 模型: {}", request.getUserId(), request.getModel());
try {
AiChatResponse response = aiChatService.chat(request);
return R.ok(response);
} catch (Exception e) {
log.error("AI聊天失败", e);
return R.fail("AI聊天失败: " + e.getMessage());
}
}
/**
* 异步聊天对话
*
* @param request 聊天请求
* @return 异步聊天响应
*/
@PostMapping("/async")
@Log(title = "AI异步聊天", businessType = BusinessType.OTHER)
public R<CompletableFuture<AiChatResponse>> chatAsync(@Validated @RequestBody AiChatRequest request) {
log.info("接收到AI异步聊天请求 - 用户: {}, 模型: {}", request.getUserId(), request.getModel());
try {
CompletableFuture<AiChatResponse> future = aiChatService.chatAsync(request);
return R.ok(future);
} catch (Exception e) {
log.error("AI异步聊天启动失败", e);
return R.fail("AI异步聊天启动失败: " + e.getMessage());
}
}
/**
* 流式聊天对话 (SSE)
*
* @param request 聊天请求
* @return SSE流
*/
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@Log(title = "AI流式聊天", businessType = BusinessType.OTHER)
public SseEmitter chatStream(@Validated @RequestBody AiChatRequest request) {
log.info("接收到AI流式聊天请求 - 用户: {}, 模型: {}", request.getUserId(), request.getModel());
SseEmitter emitter = new SseEmitter(TimeUnit.MINUTES.toMillis(10)); // 10分钟超时
try {
aiChatService.chatStream(request, new AiChatService.StreamCallback() {
@Override
public void onChunk(String chunk) {
try {
emitter.send(SseEmitter.event()
.name("chunk")
.data(chunk));
} catch (IOException e) {
log.error("发送SSE数据块失败", e);
emitter.completeWithError(e);
}
}
@Override
public void onComplete(AiChatResponse response) {
try {
emitter.send(SseEmitter.event()
.name("complete")
.data(response));
emitter.complete();
} catch (IOException e) {
log.error("发送SSE完成事件失败", e);
emitter.completeWithError(e);
}
}
@Override
public void onError(Throwable error) {
log.error("AI流式聊天失败", error);
try {
emitter.send(SseEmitter.event()
.name("error")
.data("AI流式聊天失败: " + error.getMessage()));
} catch (IOException e) {
log.error("发送SSE错误事件失败", e);
}
emitter.completeWithError(error);
}
});
} catch (Exception e) {
log.error("AI流式聊天启动失败", e);
emitter.completeWithError(e);
}
return emitter;
}
/**
* 流式聊天对话 (WebSocket风格的分块响应)
*
* @param request 聊天请求
* @param response HTTP响应
*/
@PostMapping("/stream-chunks")
@Log(title = "AI流式聊天分块", businessType = BusinessType.OTHER)
public void chatStreamChunks(@Validated @RequestBody AiChatRequest request,
HttpServletResponse response) {
log.info("接收到AI流式聊天分块请求 - 用户: {}, 模型: {}", request.getUserId(), request.getModel());
response.setContentType("text/plain; charset=utf-8");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Connection", "keep-alive");
try {
aiChatService.chatStream(request, new AiChatService.StreamCallback() {
@Override
public void onChunk(String chunk) {
try {
response.getWriter().write(chunk);
response.getWriter().flush();
} catch (IOException e) {
log.error("写入响应块失败", e);
}
}
@Override
public void onComplete(AiChatResponse chatResponse) {
try {
response.getWriter().write("\n[DONE]\n");
response.getWriter().flush();
response.getWriter().close();
} catch (IOException e) {
log.error("完成响应写入失败", e);
}
}
@Override
public void onError(Throwable error) {
try {
response.getWriter().write("\n[ERROR]: " + error.getMessage() + "\n");
response.getWriter().flush();
response.getWriter().close();
} catch (IOException e) {
log.error("写入错误响应失败", e);
}
}
});
} catch (Exception e) {
log.error("AI流式聊天分块启动失败", e);
try {
response.getWriter().write("\n[ERROR]: " + e.getMessage() + "\n");
response.getWriter().flush();
response.getWriter().close();
} catch (IOException ioException) {
log.error("写入启动错误响应失败", ioException);
}
}
}
/**
* 批量聊天对话
*
* @param requests 聊天请求列表
* @return 批量聊天响应
*/
@PostMapping("/batch")
@Log(title = "AI批量聊天", businessType = BusinessType.OTHER)
public R<List<AiChatResponse>> chatBatch(@Validated @RequestBody List<AiChatRequest> requests) {
log.info("接收到AI批量聊天请求 - 请求数量: {}", requests.size());
try {
List<CompletableFuture<AiChatResponse>> futures = aiChatService.chatBatch(requests);
List<AiChatResponse> responses = aiChatService.waitForBatchCompletion(futures);
return R.ok(responses);
} catch (Exception e) {
log.error("AI批量聊天失败", e);
return R.fail("AI批量聊天失败: " + e.getMessage());
}
}
/**
* 获取聊天历史
*
* @param sessionId 会话ID
* @param limit 限制数量
* @return 聊天历史
*/
@GetMapping("/history/{sessionId}")
public R<List<AiChatRequest.AiMessage>> getChatHistory(
@PathVariable String sessionId,
@RequestParam(defaultValue = "50") Integer limit) {
log.info("获取聊天历史 - 会话: {}, 限制: {}", sessionId, limit);
try {
List<AiChatRequest.AiMessage> history = chatHistoryService.getHistory(sessionId, limit);
return R.ok(history);
} catch (Exception e) {
log.error("获取聊天历史失败", e);
return R.fail("获取聊天历史失败: " + e.getMessage());
}
}
/**
* 清除聊天历史
*
* @param sessionId 会话ID
* @return 操作结果
*/
@DeleteMapping("/history/{sessionId}")
@Log(title = "清除聊天历史", businessType = BusinessType.DELETE)
public R<Void> clearChatHistory(@PathVariable String sessionId) {
log.info("清除聊天历史 - 会话: {}", sessionId);
try {
chatHistoryService.clearHistory(sessionId);
return R.ok();
} catch (Exception e) {
log.error("清除聊天历史失败", e);
return R.fail("清除聊天历史失败: " + e.getMessage());
}
}
/**
* 获取会话统计信息
*
* @param sessionId 会话ID
* @return 统计信息
*/
@GetMapping("/stats/{sessionId}")
public R<org.dromara.ai.service.AiChatHistoryService.SessionStats> getSessionStats(@PathVariable String sessionId) {
log.info("获取会话统计 - 会话: {}", sessionId);
try {
org.dromara.ai.service.AiChatHistoryService.SessionStats stats = chatHistoryService.getSessionStats(sessionId);
return R.ok(stats);
} catch (Exception e) {
log.error("获取会话统计失败", e);
return R.fail("获取会话统计失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,207 @@
package org.dromara.ai.controller.app;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.dromara.common.core.domain.R;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* AI图像控制器
* 提供DALL·E-3图像生成、编辑和变体功能
*
* @author pengles
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping
public class AiImageController {
@Value("${ruoyi.ai.external-ai.openai.base-url:https://api.openai.com}")
private String openaiBaseUrl;
@Value("${ruoyi.ai.external-ai.openai.api-key:}")
private String openaiApiKey;
private final OkHttpClient client = new OkHttpClient();
/**
* DALL·E-3 图像生成
*
* @param prompt 提示词
* @param size 分辨率 (512x512 或 1024x1024)
* @return 生成结果
*/
@Operation(summary = "DALL·E-3 生成", description = "通过OpenAI DALL·E-3模型生成图像")
@PostMapping("/dall3")
public R<Map<String, Object>> dalle3(
@Parameter(description = "提示词") @RequestParam @NotBlank String prompt,
@Parameter(description = "分辨率") @RequestParam @Pattern(regexp = "^(512x512|1024x1024)$") String size) {
try {
log.info("DALL·E-3图像生成请求 - 提示词长度: {}, 分辨率: {}", prompt.length(), size);
if (openaiApiKey == null || openaiApiKey.isBlank()) {
log.warn("OpenAI API Key未配置");
return R.fail("OpenAI API Key未配置");
}
String url = normalize(openaiBaseUrl) + "/v1/images/generations";
String body = "{\"model\":\"dall-e-3\",\"prompt\":\"" + prompt.replace("\"", "\\\"") + "\",\"size\":\"" + size + "\"}";
Request.Builder b = new Request.Builder().url(url)
.post(RequestBody.create(MediaType.parse("application/json"), body.getBytes(StandardCharsets.UTF_8)))
.header("Authorization", "Bearer " + openaiApiKey);
try (Response resp = client.newCall(b.build()).execute()) {
if (!resp.isSuccessful()) {
String errorBody = resp.body() != null ? resp.body().string() : "";
log.error("DALL·E-3 API调用失败: {} - {}", resp.code(), errorBody);
return R.fail("OpenAI调用失败: HTTP " + resp.code());
}
String respBody = resp.body() != null ? resp.body().string() : "{}";
log.info("DALL·E-3图像生成成功");
return R.ok(Map.of("raw", respBody));
}
} catch (Exception e) {
log.error("DALL·E-3图像生成失败", e);
return R.fail("OpenAI调用失败: " + e.getMessage());
}
}
/**
* DALL·E-3 图像编辑
*
* @param prompt 提示词
* @param size 分辨率 (512x512 或 1024x1024)
* @param image 原图文件
* @param mask 遮罩文件(可选)
* @return 编辑结果
*/
@Operation(summary = "DALL·E-3 编辑", description = "上传原图与遮罩进行图像编辑")
@PostMapping("/dall3/edit")
public R<Map<String, Object>> dalle3Edit(
@Parameter(description = "提示词") @RequestParam @NotBlank String prompt,
@Parameter(description = "分辨率") @RequestParam @Pattern(regexp = "^(512x512|1024x1024)$") String size,
@Parameter(description = "原图") @RequestParam("image") MultipartFile image,
@Parameter(description = "遮罩") @RequestParam(value = "mask", required = false) MultipartFile mask) {
try {
log.info("DALL·E-3图像编辑请求 - 提示词长度: {}, 分辨率: {}, 图片: {}",
prompt.length(), size, image.getOriginalFilename());
if (openaiApiKey == null || openaiApiKey.isBlank()) {
log.warn("OpenAI API Key未配置");
return R.fail("OpenAI API Key未配置");
}
String url = normalize(openaiBaseUrl) + "/v1/images/edits";
MultipartBody.Builder mb = new MultipartBody.Builder().setType(MultipartBody.FORM)
.addFormDataPart("model", "dall-e-3")
.addFormDataPart("prompt", prompt)
.addFormDataPart("size", size)
.addFormDataPart("image", image.getOriginalFilename(),
RequestBody.create(MediaType.parse("application/octet-stream"), toBytes(image)));
if (mask != null && !mask.isEmpty()) {
mb.addFormDataPart("mask", mask.getOriginalFilename(),
RequestBody.create(MediaType.parse("application/octet-stream"), toBytes(mask)));
}
Request.Builder b = new Request.Builder().url(url)
.post(mb.build())
.header("Authorization", "Bearer " + openaiApiKey);
try (Response resp = client.newCall(b.build()).execute()) {
if (!resp.isSuccessful()) {
String errorBody = resp.body() != null ? resp.body().string() : "";
log.error("DALL·E-3编辑API调用失败: {} - {}", resp.code(), errorBody);
return R.fail("OpenAI调用失败: HTTP " + resp.code());
}
String respBody = resp.body() != null ? resp.body().string() : "{}";
log.info("DALL·E-3图像编辑成功");
return R.ok(Map.of("raw", respBody));
}
} catch (Exception e) {
log.error("DALL·E-3图像编辑失败", e);
return R.fail("OpenAI调用失败: " + e.getMessage());
}
}
/**
* DALL·E-3 图像变体生成
*
* @param size 分辨率 (512x512 或 1024x1024)
* @param image 原图文件
* @return 变体生成结果
*/
@Operation(summary = "DALL·E-3 变体", description = "上传原图生成图像变体")
@PostMapping("/dall3/variations")
public R<Map<String, Object>> dalle3Variations(
@Parameter(description = "分辨率") @RequestParam @Pattern(regexp = "^(512x512|1024x1024)$") String size,
@Parameter(description = "原图") @RequestParam("image") MultipartFile image) {
try {
log.info("DALL·E-3图像变体生成请求 - 分辨率: {}, 图片: {}", size, image.getOriginalFilename());
if (openaiApiKey == null || openaiApiKey.isBlank()) {
log.warn("OpenAI API Key未配置");
return R.fail("OpenAI API Key未配置");
}
String url = normalize(openaiBaseUrl) + "/v1/images/variations";
MultipartBody.Builder mb = new MultipartBody.Builder().setType(MultipartBody.FORM)
.addFormDataPart("model", "dall-e-3")
.addFormDataPart("size", size)
.addFormDataPart("image", image.getOriginalFilename(),
RequestBody.create(MediaType.parse("application/octet-stream"), toBytes(image)));
Request.Builder b = new Request.Builder().url(url)
.post(mb.build())
.header("Authorization", "Bearer " + openaiApiKey);
try (Response resp = client.newCall(b.build()).execute()) {
if (!resp.isSuccessful()) {
String errorBody = resp.body() != null ? resp.body().string() : "";
log.error("DALL·E-3变体API调用失败: {} - {}", resp.code(), errorBody);
return R.fail("OpenAI调用失败: HTTP " + resp.code());
}
String respBody = resp.body() != null ? resp.body().string() : "{}";
log.info("DALL·E-3图像变体生成成功");
return R.ok(Map.of("raw", respBody));
}
} catch (Exception e) {
log.error("DALL·E-3图像变体生成失败", e);
return R.fail("OpenAI调用失败: " + e.getMessage());
}
}
/**
* URL标准化(去除尾部斜杠)
*/
private String normalize(String base) {
if (base == null || base.isBlank()) {
return "";
}
if (base.endsWith("/")) {
return base.substring(0, base.length() - 1);
}
return base;
}
/**
* 将MultipartFile转换为字节数组
*/
private byte[] toBytes(MultipartFile f) {
try {
return f.getBytes();
} catch (Exception e) {
log.error("转换文件为字节数组失败: {}", f.getOriginalFilename(), e);
return new byte[0];
}
}
}
@@ -0,0 +1,117 @@
package org.dromara.ai.controller.app;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.painting.PaintingEngineType;
import org.dromara.ai.painting.PaintingTask;
import org.dromara.ai.service.AiPaintingService;
import org.dromara.common.core.domain.R;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Map;
/**
* AI绘画控制器
* 提供统一的AI绘画任务管理和状态查询接口
*
* @author pengles
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/painting")
public class AiPaintingController {
private final AiPaintingService paintingService;
/**
* 创建绘画任务
*
* @param body 请求体,包含engine(引擎类型)、prompt(提示词)、params(参数)
* @return 任务ID
*/
@Operation(summary = "创建绘画任务", description = "创建一个新的AI绘画任务,支持多种绘画引擎")
@PostMapping("/task")
public R<Map<String, Object>> createTask(@RequestBody Map<String, Object> body) {
try {
String engine = String.valueOf(body.getOrDefault("engine", "DALL_E_3"));
String prompt = String.valueOf(body.getOrDefault("prompt", ""));
Map<String, Object> params = (Map<String, Object>) body.getOrDefault("params", Map.of());
log.info("创建绘画任务 - 引擎: {}, 提示词长度: {}", engine, prompt.length());
PaintingEngineType type = PaintingEngineType.valueOf(engine);
String taskId = paintingService.createTask(type, prompt, params);
log.info("绘画任务创建成功 - 任务ID: {}", taskId);
return R.ok(Map.of("taskId", taskId));
} catch (IllegalArgumentException e) {
log.error("创建绘画任务失败 - 无效的引擎类型", e);
return R.fail("无效的引擎类型: " + e.getMessage());
} catch (Exception e) {
log.error("创建绘画任务失败", e);
return R.fail("创建任务失败: " + e.getMessage());
}
}
/**
* 查询任务状态
*
* @param taskId 任务ID
* @return 任务状态信息
*/
@Operation(summary = "任务状态", description = "查询指定绘画任务的当前状态")
@GetMapping("/status/{taskId}")
public R<PaintingTask> status(
@Parameter(description = "任务ID", required = true)
@PathVariable("taskId") String taskId) {
try {
log.debug("查询绘画任务状态 - 任务ID: {}", taskId);
PaintingTask task = paintingService.getTask(taskId);
if (task == null) {
log.warn("绘画任务不存在 - 任务ID: {}", taskId);
return R.fail("TASK_NOT_FOUND");
}
return R.ok(task);
} catch (Exception e) {
log.error("查询绘画任务状态失败 - 任务ID: {}", taskId, e);
return R.fail("查询任务状态失败: " + e.getMessage());
}
}
/**
* 任务进度流(SSE
*
* @param taskId 任务ID
* @return SSE事件流
*/
@Operation(summary = "进度流", description = "通过SSE实时获取绘画任务的进度更新")
@GetMapping(value = "/stream/{taskId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter stream(
@Parameter(description = "任务ID", required = true)
@PathVariable("taskId") String taskId) {
try {
log.info("创建绘画任务进度流 - 任务ID: {}", taskId);
SseEmitter emitter = new SseEmitter(0L);
paintingService.addEmitter(taskId, emitter);
return emitter;
} catch (Exception e) {
log.error("创建绘画任务进度流失败 - 任务ID: {}", taskId, e);
SseEmitter emitter = new SseEmitter(0L);
try {
emitter.send(SseEmitter.event().name("error").data("创建进度流失败: " + e.getMessage()));
emitter.completeWithError(e);
} catch (Exception ex) {
log.error("发送错误事件失败", ex);
}
return emitter;
}
}
}
@@ -0,0 +1,82 @@
package org.dromara.ai.controller.app;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.core.BaseController;
import org.dromara.ai.domain.bo.AiPaintingFavoriteBo;
import org.dromara.ai.domain.vo.AiPaintingFavoriteVo;
import org.dromara.ai.service.AiPaintingFavoriteService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户端AI绘画作品收藏
*
* @author pengles
*/
@Tag(name = "用户端-AI绘画作品收藏", description = "用户端AI绘画作品收藏接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/app/ai/painting/favorite")
public class AppAiPaintingFavoriteController extends BaseController {
private final AiPaintingFavoriteService aiPaintingFavoriteService;
/**
* 查询当前用户的收藏列表
*/
@Operation(summary = "查询当前用户的收藏列表", description = "查询当前登录用户的绘画作品收藏列表")
@GetMapping("/list")
public R<List<AiPaintingFavoriteVo>> getMyFavorites() {
Long userId = LoginHelper.getUserId();
List<AiPaintingFavoriteVo> list = aiPaintingFavoriteService.queryByUserId(userId);
return R.ok(list);
}
/**
* 收藏绘画作品
*/
@Operation(summary = "收藏绘画作品", description = "收藏指定的绘画作品")
@PostMapping
public R<Void> addFavorite(@Valid @RequestBody AiPaintingFavoriteBo bo) {
// 自动设置用户ID
Long userId = LoginHelper.getUserId();
bo.setUserId(userId);
return toAjax(aiPaintingFavoriteService.insertByBo(bo));
}
/**
* 取消收藏
*/
@Operation(summary = "取消收藏", description = "取消收藏指定的绘画作品")
@DeleteMapping("/{paintingRecordId}")
public R<Void> cancelFavorite(@Parameter(description = "绘画记录ID", required = true)
@NotNull(message = "绘画记录ID不能为空")
@PathVariable Long paintingRecordId) {
Long userId = LoginHelper.getUserId();
return toAjax(aiPaintingFavoriteService.cancelFavorite(userId, paintingRecordId));
}
/**
* 检查是否已收藏
*/
@Operation(summary = "检查是否已收藏", description = "检查当前用户是否已收藏指定的绘画作品")
@GetMapping("/check/{paintingRecordId}")
public R<Boolean> checkFavorite(@Parameter(description = "绘画记录ID", required = true)
@NotNull(message = "绘画记录ID不能为空")
@PathVariable Long paintingRecordId) {
Long userId = LoginHelper.getUserId();
Boolean favorited = aiPaintingFavoriteService.isFavorited(userId, paintingRecordId);
return R.ok(favorited);
}
}
@@ -0,0 +1,91 @@
package org.dromara.ai.controller.app;
import cn.dev33.satoken.annotation.SaIgnore;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.core.BaseController;
import org.dromara.ai.domain.vo.AiPaintingRecordVo;
import org.dromara.ai.service.AiPaintingRecordService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 用户端AI绘画记录
*
* @author pengles
*/
@Tag(name = "用户端-AI绘画记录", description = "用户端AI绘画记录接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/app/ai/painting/record")
public class AppAiPaintingRecordController extends BaseController {
private final AiPaintingRecordService aiPaintingRecordService;
/**
* 生成分享码
*/
@Operation(summary = "生成分享码", description = "为指定的绘画作品生成分享码")
@PostMapping("/{id}/share")
public R<Map<String, String>> generateShareCode(@Parameter(description = "记录ID", required = true)
@NotNull(message = "记录ID不能为空")
@PathVariable Long id) {
// 验证记录所有权
AiPaintingRecordVo record = aiPaintingRecordService.queryById(id);
if (record == null) {
return R.fail("绘画记录不存在");
}
Long userId = LoginHelper.getUserId();
if (!userId.equals(record.getUserId())) {
return R.fail("无权操作该记录");
}
String shareCode = aiPaintingRecordService.generateShareCode(id);
return R.ok(Map.of("shareCode", shareCode));
}
/**
* 取消分享
*/
@Operation(summary = "取消分享", description = "取消指定绘画作品的分享")
@DeleteMapping("/{id}/share")
public R<Void> cancelShare(@Parameter(description = "记录ID", required = true)
@NotNull(message = "记录ID不能为空")
@PathVariable Long id) {
// 验证记录所有权
AiPaintingRecordVo record = aiPaintingRecordService.queryById(id);
if (record == null) {
return R.fail("绘画记录不存在");
}
Long userId = LoginHelper.getUserId();
if (!userId.equals(record.getUserId())) {
return R.fail("无权操作该记录");
}
return toAjax(aiPaintingRecordService.cancelShare(id));
}
/**
* 根据分享码查看作品(无需登录)
*/
@Operation(summary = "根据分享码查看作品", description = "根据分享码查看公开分享的绘画作品,无需登录")
@SaIgnore
@GetMapping("/share/{shareCode}")
public R<AiPaintingRecordVo> getByShareCode(@Parameter(description = "分享码", required = true)
@NotBlank(message = "分享码不能为空")
@PathVariable String shareCode) {
AiPaintingRecordVo record = aiPaintingRecordService.queryByShareCode(shareCode);
if (record == null) {
return R.fail("分享码无效或作品已取消分享");
}
return R.ok(record);
}
}
@@ -0,0 +1,106 @@
package org.dromara.ai.controller.app;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.core.BaseController;
import org.dromara.ai.domain.bo.AiPaintingTemplateBo;
import org.dromara.ai.domain.vo.AiPaintingTemplateVo;
import org.dromara.ai.service.AiPaintingTemplateService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
* 用户端AI绘画参数模板
*
* @author pengles
*/
@Tag(name = "用户端-AI绘画参数模板", description = "用户端AI绘画参数模板接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/app/ai/painting/template")
public class AppAiPaintingTemplateController extends BaseController {
private final AiPaintingTemplateService aiPaintingTemplateService;
/**
* 查询可用的模板列表
*/
@Operation(summary = "查询可用的模板列表", description = "根据引擎类型查询可用的模板列表(公开模板 + 用户私有模板)")
@GetMapping("/list/{engineType}")
public R<List<AiPaintingTemplateVo>> getAvailableTemplates(@Parameter(description = "引擎类型", required = true)
@PathVariable String engineType) {
Long userId = null;
try {
userId = LoginHelper.getUserId();
} catch (Exception e) {
// 未登录用户只能看到公开模板
}
List<AiPaintingTemplateVo> list = aiPaintingTemplateService.queryAvailableTemplates(engineType, userId);
return R.ok(list);
}
/**
* 获取模板详情
*/
@Operation(summary = "获取模板详情", description = "根据模板ID获取详细信息")
@GetMapping("/{id}")
public R<AiPaintingTemplateVo> getInfo(@Parameter(description = "模板ID", required = true)
@PathVariable Long id) {
return R.ok(aiPaintingTemplateService.queryById(id));
}
/**
* 新增用户私有模板
*/
@Operation(summary = "新增用户私有模板", description = "新增用户私有绘画参数模板")
@PostMapping
public R<Void> add(@Valid @RequestBody AiPaintingTemplateBo bo) {
// 自动设置用户ID
Long userId = LoginHelper.getUserId();
bo.setUserId(userId);
bo.setIsPublic(0); // 用户创建的模板默认为私有
return toAjax(aiPaintingTemplateService.insertByBo(bo));
}
/**
* 修改用户私有模板
*/
@Operation(summary = "修改用户私有模板", description = "修改用户私有绘画参数模板(只能修改自己的模板)")
@PutMapping
public R<Void> edit(@Valid @RequestBody AiPaintingTemplateBo bo) {
// 验证模板所有权
Long userId = LoginHelper.getUserId();
AiPaintingTemplateVo existing = aiPaintingTemplateService.queryById(bo.getId());
if (existing == null || !userId.equals(existing.getUserId())) {
return R.fail("无权修改该模板");
}
bo.setUserId(userId); // 确保用户ID不变
return toAjax(aiPaintingTemplateService.updateByBo(bo));
}
/**
* 删除用户私有模板
*/
@Operation(summary = "删除用户私有模板", description = "删除用户私有绘画参数模板(只能删除自己的模板)")
@DeleteMapping("/{id}")
public R<Void> remove(@Parameter(description = "模板ID", required = true)
@PathVariable Long id) {
// 验证模板所有权
Long userId = LoginHelper.getUserId();
AiPaintingTemplateVo existing = aiPaintingTemplateService.queryById(id);
if (existing == null || !userId.equals(existing.getUserId())) {
return R.fail("无权删除该模板");
}
return toAjax(aiPaintingTemplateService.deleteWithValidByIds(Arrays.asList(id), true));
}
}
@@ -0,0 +1,60 @@
package org.dromara.ai.controller.app;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.core.BaseController;
import org.dromara.ai.domain.bo.AiRatingRecordBo;
import org.dromara.ai.domain.vo.AiRatingRecordVo;
import org.dromara.ai.service.AiRatingRecordService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* AI评价记录(用户端)
*
* @author pengles
*/
@Tag(name = "用户端-AI评价记录", description = "AI评价记录接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/app/ai/rating")
public class AppAiRatingRecordController extends BaseController {
private final AiRatingRecordService aiRatingRecordService;
/**
* 提交评价
*/
@Operation(summary = "提交评价", description = "用户对AI回复进行评价")
@Log(title = "AI评价", businessType = BusinessType.OTHER)
@PostMapping("/submit")
public R<Void> submitRating(@Valid @RequestBody AiRatingRecordBo bo) {
// 设置当前用户ID
Long userId = LoginHelper.getUserId();
bo.setUserId(userId);
return toAjax(aiRatingRecordService.insertByBo(bo));
}
/**
* 查询评价记录(根据消息ID)
*/
@Operation(summary = "查询评价记录", description = "根据消息ID查询当前用户的评价记录")
@GetMapping("/{messageId}")
public R<AiRatingRecordVo> getRating(@Parameter(description = "消息ID", required = true)
@NotNull(message = "消息ID不能为空")
@PathVariable String messageId) {
Long userId = LoginHelper.getUserId();
AiRatingRecordVo vo = aiRatingRecordService.queryByMessageId(messageId, userId);
return R.ok(vo);
}
}
@@ -0,0 +1,41 @@
package org.dromara.ai.controller.app;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.ai.domain.vo.AiUserQuotaVo;
import org.dromara.ai.service.AiUserQuotaService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户端AI用户配额
*
* @author pengles
*/
@Tag(name = "用户端-AI用户配额", description = "用户端AI用户配额接口")
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/app/ai/user/quota")
public class AppAiUserQuotaController {
private final AiUserQuotaService aiUserQuotaService;
/**
* 查询当前用户的配额信息
*/
@Operation(summary = "查询当前用户的配额信息", description = "查询当前登录用户的配额信息")
@GetMapping
public R<AiUserQuotaVo> getMyQuota() {
Long userId = LoginHelper.getUserId();
AiUserQuotaVo vo = aiUserQuotaService.queryByUserId(userId);
return R.ok(vo);
}
}
@@ -0,0 +1,66 @@
package org.dromara.ai.controller.app;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.platform.CozeService;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
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;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* Coze平台控制器
* 提供Coze平台的Bot对话接口
*
* @author pengles
*/
@Slf4j
@Validated
@RestController
@RequestMapping("/ai/coze")
@RequiredArgsConstructor
public class CozeController {
private final CozeService cozeService;
/**
* Coze流式Bot对话
*
* @param body 请求体,包含botIdBot ID)和input(输入内容)
* @return SSE事件流
*/
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@Operation(summary = "Coze流式Bot对话", description = "通过SSE与Coze平台的Bot进行流式对话")
public SseEmitter stream(@RequestBody Map<String, Object> body) {
String botId = String.valueOf(body.getOrDefault("botId", ""));
String input = String.valueOf(body.getOrDefault("input", ""));
log.info("Coze流式Bot对话请求 - Bot ID: {}, 输入长度: {}", botId, input.length());
SseEmitter emitter = new SseEmitter(TimeUnit.MINUTES.toMillis(2));
try {
String raw = cozeService.stream(botId, input);
emitter.send(raw);
emitter.send("[DONE]");
emitter.complete();
log.debug("Coze流式Bot对话完成 - Bot ID: {}", botId);
} catch (Exception e) {
log.error("Coze流式Bot对话失败 - Bot ID: {}", botId, e);
try {
emitter.send(SseEmitter.event().name("error").data("ERROR: " + e.getMessage()));
} catch (Exception ignored) {
log.debug("发送错误事件失败", ignored);
}
emitter.completeWithError(e);
}
return emitter;
}
}
@@ -0,0 +1,72 @@
package org.dromara.ai.controller.app;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.platform.DifyClientService;
import org.dromara.common.core.domain.R;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* DIFY平台控制器
* 提供DIFY平台的应用管理接口
*
* @author pengles
*/
@Slf4j
@Validated
@RestController
@RequestMapping("/ai/dify")
@RequiredArgsConstructor
public class DifyController {
private final DifyClientService difyService;
/**
* 获取应用列表
*
* @return 应用列表
*/
@GetMapping("/apps")
@Operation(summary = "列表应用", description = "获取DIFY平台的所有应用列表")
public R<Map<String, Object>> apps() {
try {
log.debug("获取DIFY应用列表");
String resp = difyService.get("/api/apps");
return R.ok(Map.of("raw", resp));
} catch (Exception e) {
log.error("获取DIFY应用列表失败", e);
return R.fail("DIFY调用失败: " + e.getMessage());
}
}
/**
* 创建或更新应用
*
* @param body 应用信息
* @return 创建结果
*/
@PostMapping("/apps")
@Operation(summary = "创建应用", description = "在DIFY平台创建或更新应用")
public R<Map<String, Object>> create(@RequestBody Map<String, Object> body) {
try {
log.info("创建DIFY应用");
String resp = difyService.post("/api/apps", toJson(body));
log.info("DIFY应用创建成功");
return R.ok(Map.of("raw", resp));
} catch (Exception e) {
log.error("创建DIFY应用失败", e);
return R.fail("DIFY调用失败: " + e.getMessage());
}
}
/**
* 将Map转换为JSON字符串
*/
private String toJson(Map<String, Object> m) {
return org.dromara.common.json.utils.JsonUtils.toJsonString(m);
}
}
@@ -0,0 +1,95 @@
package org.dromara.ai.controller.app;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.platform.FastGptService;
import org.dromara.common.core.domain.R;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* FastGPT平台控制器
* 提供FastGPT平台的知识库检索、工作流编排和上下文管理接口
*
* @author pengles
*/
@Slf4j
@Validated
@RestController
@RequestMapping("/ai/fastgpt")
@RequiredArgsConstructor
public class FastGptController {
private final FastGptService fastGptService;
/**
* 知识库检索
*
* @param body 检索参数
* @return 检索结果
*/
@PostMapping("/knowledge/search")
@Operation(summary = "知识库检索", description = "在FastGPT平台的知识库中进行检索")
public R<Map<String, Object>> search(@RequestBody Map<String, Object> body) {
try {
log.debug("FastGPT知识库检索请求");
String resp = fastGptService.post("/api/knowledge/search", toJson(body));
return R.ok(Map.of("raw", resp));
} catch (Exception e) {
log.error("FastGPT知识库检索失败", e);
return R.fail("FastGPT调用失败: " + e.getMessage());
}
}
/**
* 工作流编排
*
* @param body 工作流参数
* @return 执行结果
*/
@PostMapping("/workflow/run")
@Operation(summary = "工作流编排", description = "运行FastGPT平台的工作流")
public R<Map<String, Object>> workflow(@RequestBody Map<String, Object> body) {
try {
log.info("FastGPT工作流执行请求");
String resp = fastGptService.post("/api/workflow/run", toJson(body));
log.info("FastGPT工作流执行成功");
return R.ok(Map.of("raw", resp));
} catch (Exception e) {
log.error("FastGPT工作流执行失败", e);
return R.fail("FastGPT调用失败: " + e.getMessage());
}
}
/**
* 获取上下文
*
* @param sessionId 会话ID
* @return 上下文信息
*/
@GetMapping("/context/{sessionId}")
@Operation(summary = "上下文管理", description = "获取指定会话的上下文信息")
public R<Map<String, Object>> context(
@Parameter(description = "会话ID", required = true)
@PathVariable String sessionId) {
try {
log.debug("获取FastGPT上下文 - 会话ID: {}", sessionId);
String resp = fastGptService.get("/api/context/" + sessionId);
return R.ok(Map.of("raw", resp));
} catch (Exception e) {
log.error("获取FastGPT上下文失败 - 会话ID: {}", sessionId, e);
return R.fail("FastGPT调用失败: " + e.getMessage());
}
}
/**
* 将Map转换为JSON字符串
*/
private String toJson(Map<String, Object> m) {
return org.dromara.common.json.utils.JsonUtils.toJsonString(m);
}
}
@@ -0,0 +1,82 @@
package org.dromara.ai.controller.app;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.mcp.McpIntegrationService;
import org.dromara.ai.mcp.McpTool;
import org.dromara.ai.mcp.McpToolRegistry;
import org.dromara.common.core.domain.R;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* MCP工具控制器
* 提供MCPModel Context Protocol)工具的查询和调用接口
*
* @author pengles
*/
@Slf4j
@Validated
@RestController
@RequestMapping("/ai/mcp")
@RequiredArgsConstructor
public class McpController {
private final McpToolRegistry registry;
private final McpIntegrationService mcpIntegrationService;
/**
* 获取所有已注册的MCP工具列表
*
* @return 工具列表
*/
@GetMapping("/tools")
@Operation(summary = "获取工具列表", description = "返回所有已注册的MCP工具及其信息")
public R<Map<String, McpTool>> tools() {
try {
Map<String, McpTool> allTools = registry.all();
log.debug("获取MCP工具列表,共{}个工具", allTools.size());
return R.ok(allTools);
} catch (Exception e) {
log.error("获取MCP工具列表失败", e);
return R.fail("获取工具列表失败: " + e.getMessage());
}
}
/**
* 调用指定的MCP工具
*
* @param name 工具名称
* @param input 工具输入参数
* @return 工具执行结果
*/
@PostMapping("/invoke/{name}")
@Operation(summary = "调用工具", description = "执行指定的MCP工具并返回结果")
public R<Map<String, Object>> invoke(
@Parameter(description = "工具名称", required = true)
@PathVariable @NotBlank String name,
@Parameter(description = "工具输入参数")
@RequestBody(required = false) Map<String, Object> input) {
try {
log.info("调用MCP工具: {}, 输入参数: {}", name, input != null ? input.keySet() : "");
Map<String, Object> result = mcpIntegrationService.invokeTool(name, input != null ? input : Map.of());
if (result.containsKey("error")) {
log.warn("MCP工具调用失败: {}, 错误: {}", name, result.get("error"));
return R.fail("工具调用失败: " + result.get("error"));
}
log.debug("MCP工具调用成功: {}", name);
return R.ok(result);
} catch (Exception e) {
log.error("调用MCP工具失败: {}", name, e);
return R.fail("工具调用失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,168 @@
package org.dromara.ai.controller.app;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.tika.Tika;
import org.dromara.common.core.domain.R;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
/**
* 多模态控制器
* 提供文件解析、OCR识别、图像识别、图生文等功能
*
* @author pengles
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/multimodal")
public class MultimodalController {
@Value("${ruoyi.ai.ocr.base-url:}")
private String ocrBaseUrl;
@Value("${ruoyi.ai.external-ai.openai.api-key:}")
private String openaiApiKey;
@Value("${ruoyi.ai.external-ai.openai.base-url:https://api.openai.com}")
private String openaiBaseUrl;
@Operation(summary = "文件解析")
@PostMapping(value = "/extract", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<Map<String, Object>> extract(
@Parameter(description = "文件") @RequestParam("file") @NotNull MultipartFile file) {
try {
Tika tika = new Tika();
String text = tika.parseToString(file.getInputStream());
String summary = summarize(text);
return R.ok(Map.of("text", text, "summary", summary));
} catch (Exception e) {
log.error("文件解析失败", e);
return R.fail("解析失败: " + e.getMessage());
}
}
@Operation(summary = "OCR识别")
@PostMapping(value = "/ocr", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<byte[]> ocr(
@Parameter(description = "图片") @RequestParam("file") @NotNull MultipartFile file) {
try {
if (ocrBaseUrl == null || ocrBaseUrl.isBlank()) {
String msg = "{\"code\":500,\"msg\":\"OCR未配置\",\"data\":null}";
return ResponseEntity.status(500).contentType(MediaType.APPLICATION_JSON)
.body(msg.getBytes(StandardCharsets.UTF_8));
}
// TODO: 实现OCR服务调用
String msg = "{\"code\":200,\"msg\":\"OK\",\"data\":{\"text\":\"\"}}";
return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON)
.body(msg.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) {
log.error("OCR识别失败", e);
String msg = "{\"code\":500,\"msg\":\"" + e.getMessage().replace("\"", "\\\"") + "\",\"data\":null}";
return ResponseEntity.status(500).contentType(MediaType.APPLICATION_JSON)
.body(msg.getBytes(StandardCharsets.UTF_8));
}
}
@Operation(summary = "文本分析")
@PostMapping("/analyze/text")
public R<Map<String, Object>> analyzeText(
@Parameter(description = "文本") @RequestParam("text") String text) {
try {
String t = text == null ? "" : text.trim();
String summary = summarize(t);
List<String> keywords = Arrays.stream(t.split("\\s+"))
.filter(s -> s.length() > 3)
.limit(10)
.collect(Collectors.toList());
String title = t.length() > 16 ? t.substring(0, 16) : t;
return R.ok(Map.of("title", title, "summary", summary, "keywords", keywords));
} catch (Exception e) {
log.error("文本分析失败", e);
return R.fail("文本分析失败: " + e.getMessage());
}
}
@Operation(summary = "图像识别", description = "使用AI模型识别图像内容,返回标签和描述")
@PostMapping(value = "/analyze/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<Map<String, Object>> analyzeImage(
@Parameter(description = "图片") @RequestParam("file") @NotNull MultipartFile file,
@Parameter(description = "模型名称") @RequestParam(required = false) String model) {
try {
// 检查是否配置了OpenAI API Key
if (openaiApiKey == null || openaiApiKey.isBlank()) {
return R.ok(Map.of("labels", List.of(), "description", "",
"msg", "VISION_PROVIDER_NOT_CONFIGURED",
"error", "OpenAI API Key未配置"));
}
// 由于当前 AiMessage.content 是 String 类型,Vision API 的多模态支持需要在 Provider 层面实现
// 这里提供一个占位实现,提示用户使用支持 Vision 的模型
// 实际使用时,需要在 OpenAiProvider 中扩展支持多模态消息格式
log.warn("图像识别功能需要 Vision API 支持,当前实现为占位,请使用支持 Vision 的模型(如 gpt-4-vision-preview");
return R.ok(Map.of("labels", List.of(), "description", "",
"msg", "VISION_API_NOT_FULLY_IMPLEMENTED",
"hint", "此功能需要 Vision API 支持。当前 AiMessage 接口限制,建议直接使用 OpenAI Vision API 或扩展 Provider 接口"));
} catch (Exception e) {
log.error("图像识别失败", e);
return R.ok(Map.of("labels", List.of(), "description", "",
"msg", "VISION_ANALYSIS_ERROR",
"error", e.getMessage()));
}
}
@Operation(summary = "图生文", description = "使用AI模型为图片生成描述文本")
@PostMapping(value = "/caption", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<Map<String, Object>> caption(
@Parameter(description = "图片") @RequestParam("file") @NotNull MultipartFile file,
@Parameter(description = "模型名称") @RequestParam(required = false) String model) {
try {
// 检查是否配置了OpenAI API Key
if (openaiApiKey == null || openaiApiKey.isBlank()) {
return R.ok(Map.of("text", "", "msg", "CAPTION_PROVIDER_NOT_CONFIGURED",
"error", "OpenAI API Key未配置"));
}
// 由于当前 AiMessage.content 是 String 类型,Vision API 的多模态支持需要在 Provider 层面实现
// 这里提供一个占位实现,提示用户使用支持 Vision 的模型
log.warn("图生文功能需要 Vision API 支持,当前实现为占位,请使用支持 Vision 的模型(如 gpt-4-vision-preview");
return R.ok(Map.of("text", "", "msg", "CAPTION_API_NOT_FULLY_IMPLEMENTED",
"hint", "此功能需要 Vision API 支持。当前 AiMessage 接口限制,建议直接使用 OpenAI Vision API 或扩展 Provider 接口"));
} catch (Exception e) {
log.error("图生文失败", e);
return R.ok(Map.of("text", "", "msg", "CAPTION_GENERATION_ERROR",
"error", e.getMessage()));
}
}
/**
* 文本摘要
*/
private String summarize(String text) {
if (text == null) return "";
String s = text.trim().replaceAll("\\s+", " ");
if (s.length() <= 200) return s;
return s.substring(0, 200);
}
}
@@ -0,0 +1,133 @@
package org.dromara.ai.controller.app;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xslf.usermodel.XMLSlideShow;
import org.apache.poi.xslf.usermodel.XSLFSlide;
import org.dromara.common.core.domain.R;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
/**
* PPT控制器
* 提供智能PPT生成和导出功能
*
* @author pengles
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/ppt")
public class PptController {
/**
* 生成PPTX文件
*
* @param content 文本内容(按换行符分割为多页)
* @param theme 主题(可选,暂未实现)
* @return PPTX文件字节流
*/
@Operation(summary = "生成PPTX", description = "根据文本内容生成PowerPoint演示文稿,每行内容作为一页")
@PostMapping("/generate")
public ResponseEntity<byte[]> generate(
@Parameter(description = "文本内容") @RequestParam @NotBlank String content,
@Parameter(description = "主题") @RequestParam(required = false) String theme) {
try {
log.info("开始生成PPTX,内容长度: {}", content.length());
XMLSlideShow ppt = new XMLSlideShow();
try {
String[] parts = content.split("\\n+");
if (parts.length == 0) parts = new String[]{content};
for (String p : parts) {
XSLFSlide slide = ppt.createSlide();
slide.createTextBox().setText(p);
}
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ppt.write(bos);
byte[] bytes = bos.toByteArray();
log.info("PPTX生成成功,文件大小: {} bytes,共{}页", bytes.length, parts.length);
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"generated.pptx\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(bytes);
} finally {
ppt.close();
}
} catch (Exception e) {
log.error("PPTX生成失败", e);
String msg = "{\"code\":500,\"msg\":\"" + e.getMessage().replace("\"", "\\\"") + "\",\"data\":null}";
return ResponseEntity.status(500)
.contentType(MediaType.APPLICATION_JSON)
.body(msg.getBytes(StandardCharsets.UTF_8));
}
}
/**
* 导出PNG图片序列
*
* @param content 文本内容
* @param width 图片宽度(可选)
* @param height 图片高度(可选)
* @return Base64编码的PNG图片列表
*/
@Operation(summary = "导出PNG序列", description = "将PPT内容导出为PNG图片序列,每页一张图片,返回Base64编码")
@PostMapping("/export/pngs")
public R<List<String>> exportPngs(
@Parameter(description = "文本内容") @RequestParam @NotBlank String content,
@Parameter(description = "宽度") @RequestParam(required = false) Integer width,
@Parameter(description = "高度") @RequestParam(required = false) Integer height) {
try {
log.info("开始导出PNG序列,内容长度: {}, 尺寸: {}x{}", content.length(), width, height);
XMLSlideShow ppt = new XMLSlideShow();
try {
String[] parts = content.split("\\n+");
if (parts.length == 0) parts = new String[]{content};
for (String p : parts) {
XSLFSlide slide = ppt.createSlide();
slide.createTextBox().setText(p);
}
Dimension pgsize = ppt.getPageSize();
int w = width != null ? width : pgsize.width;
int h = height != null ? height : pgsize.height;
List<String> imgs = new ArrayList<>();
for (XSLFSlide slide : ppt.getSlides()) {
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = img.createGraphics();
graphics.setPaint(Color.WHITE);
graphics.fillRect(0, 0, w, h);
slide.draw(graphics);
graphics.dispose();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
javax.imageio.ImageIO.write(img, "png", bos);
String b64 = Base64.getEncoder().encodeToString(bos.toByteArray());
imgs.add("data:image/png;base64," + b64);
}
log.info("PNG序列导出成功,共{}张图片", imgs.size());
return R.ok(imgs);
} finally {
ppt.close();
}
} catch (Exception e) {
log.error("PNG序列导出失败", e);
return R.fail("导出失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,45 @@
package org.dromara.ai.controller.app;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.queue.TaskQueueService;
import org.dromara.common.core.domain.R;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* 队列指标控制器
* 提供任务队列的监控指标查询接口
*
* @author pengles
*/
@Slf4j
@RestController
@RequestMapping("/ai/queue")
@RequiredArgsConstructor
public class QueueMetricsController {
private final TaskQueueService queueService;
/**
* 获取队列指标
*
* @return 队列指标信息
*/
@GetMapping("/metrics")
@Operation(summary = "队列指标", description = "获取任务队列的实时监控指标,包括队列深度、处理速率等")
public R<Map<String, Object>> metrics() {
try {
Map<String, Object> metrics = queueService.metrics();
log.debug("获取队列指标成功");
return R.ok(metrics);
} catch (Exception e) {
log.error("获取队列指标失败", e);
return R.fail("获取队列指标失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,113 @@
package org.dromara.ai.controller.app;
import lombok.RequiredArgsConstructor;
import org.dromara.ai.provider.AiProviderManager;
import org.dromara.ai.service.SimpleAiService;
import org.dromara.common.core.domain.R;
import org.dromara.knowledge.domain.vo.KnowledgeSearchResultVo;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 简化的AI控制器
* 提供基于知识库的AI问答功能
*
* @author pengles
*/
@RestController
@RequestMapping("/ai")
@RequiredArgsConstructor
public class SimpleAiController {
private final SimpleAiService simpleAiService;
private final AiProviderManager aiProviderManager;
/**
* 基于知识库的问答
*
* @param knowledgeId 知识库ID
* @param question 用户问题
* @param limit 返回结果数量限制
* @return AI回答
*/
@PostMapping("/ask")
public R<String> ask(@RequestParam String knowledgeId,
@RequestParam String question,
@RequestParam(required = false, defaultValue = "5") Integer limit) {
String answer = simpleAiService.askWithKnowledge(knowledgeId, question, limit);
return R.ok(answer);
}
/**
* 向量相似度搜索
*
* @param knowledgeId 知识库ID
* @param queryText 查询文本
* @param modelName 模型名称
* @param topK 返回前K个结果
* @param threshold 相似度阈值
* @return 搜索结果
*/
@PostMapping("/search")
public R<List<KnowledgeSearchResultVo>> vectorSearch(
@RequestParam String knowledgeId,
@RequestParam String queryText,
@RequestParam(required = false, defaultValue = "default") String modelName,
@RequestParam(required = false, defaultValue = "5") Integer topK,
@RequestParam(required = false, defaultValue = "0.7") Double threshold) {
List<KnowledgeSearchResultVo> results = simpleAiService.vectorSearch(
knowledgeId, queryText, modelName, topK, threshold
);
return R.ok(results);
}
/**
* 获取知识库统计信息
*
* @param knowledgeId 知识库ID
* @return 统计信息
*/
@GetMapping("/stats/{knowledgeId}")
public R<Map<String, Object>> getKnowledgeStats(@PathVariable String knowledgeId) {
Map<String, Object> stats = simpleAiService.getKnowledgeStats(knowledgeId);
return R.ok(stats);
}
/**
* 检查模型是否可用
*
* @param modelName 模型名称
* @return 是否可用
*/
@GetMapping("/model/check/{modelName}")
public R<Boolean> checkModel(@PathVariable String modelName) {
boolean available = simpleAiService.isModelAvailable(modelName);
return R.ok(available);
}
/**
* 获取可用的模型列表
*
* @return 模型列表
*/
@GetMapping("/models")
public R<List<String>> getAvailableModels() {
List<String> models = simpleAiService.getAvailableModels();
return R.ok(models);
}
/**
* AI服务健康检查
*
* @return 健康状态与Provider统计
*/
@GetMapping("/health")
public R<Map<String, Object>> health() {
Map<String, Object> stats = aiProviderManager.getAllStatistics();
stats.put("status", "UP");
return R.ok(stats);
}
}
@@ -0,0 +1,92 @@
package org.dromara.ai.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* AI聊天记录对象 ai_chat_record
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@TableName("ai_chat_record")
public class AiChatRecord implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@TableId
private Long id;
/**
* 会话ID
*/
private String sessionId;
/**
* 用户ID
*/
private Long userId;
/**
* Provider名称
*/
private String providerName;
/**
* 模型名称
*/
private String modelName;
/**
* 消息类型(USER/ASSISTANT)
*/
private String messageType;
/**
* 消息内容
*/
private String content;
/**
* 使用的Token数
*/
private Integer tokensUsed;
/**
* 成本
*/
private BigDecimal cost;
/**
* 响应时间(毫秒)
*/
private Integer responseTime;
/**
* 是否流式响应(0否 1是)
*/
private Integer isStream;
/**
* 错误信息
*/
private String errorMessage;
/**
* 创建时间
*/
private Date createTime;
}
@@ -0,0 +1,56 @@
package org.dromara.ai.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI绘画作品收藏对象 ai_painting_favorite
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@TableName("ai_painting_favorite")
public class AiPaintingFavorite implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@TableId
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 绘画记录ID
*/
private Long paintingRecordId;
/**
* 任务ID
*/
private String taskId;
/**
* 备注
*/
private String remark;
/**
* 收藏时间
*/
private Date createTime;
}
@@ -0,0 +1,96 @@
package org.dromara.ai.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI绘画历史记录对象 ai_painting_record
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@TableName("ai_painting_record")
public class AiPaintingRecord implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@TableId
private Long id;
/**
* 任务ID
*/
private String taskId;
/**
* 用户ID
*/
private Long userId;
/**
* 绘画引擎类型(DALL_E_3/MIDJOURNEY/STABLE_DIFFUSION)
*/
private String engineType;
/**
* 提示词
*/
private String prompt;
/**
* 绘画参数(JSON字符串)
*/
private String params;
/**
* 状态(PENDING/RUNNING/SUCCESS/FAILED)
*/
private String status;
/**
* 进度(0-100)
*/
private Integer progress;
/**
* 生成的图片URL列表(JSON字符串)
*/
private String images;
/**
* 错误消息
*/
private String message;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 分享码(用于分享作品)
*/
private String shareCode;
/**
* 是否公开分享(0否 1是)
*/
private Integer isPublic;
}
@@ -0,0 +1,92 @@
package org.dromara.ai.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI绘画参数模板对象 ai_painting_template
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@TableName("ai_painting_template")
public class AiPaintingTemplate implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@TableId
private Long id;
/**
* 模板名称
*/
private String templateName;
/**
* 用户ID(为空表示系统模板)
*/
private Long userId;
/**
* 绘画引擎类型(DALL_E_3/MIDJOURNEY/STABLE_DIFFUSION)
*/
private String engineType;
/**
* 绘画参数(JSON字符串)
*/
private String params;
/**
* 模板描述
*/
private String description;
/**
* 是否公开(0=私有 1=公开)
*/
private Integer isPublic;
/**
* 排序
*/
private Integer sort;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private String createBy;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
}
@@ -0,0 +1,137 @@
package org.dromara.ai.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI Provider配置对象 ai_provider_config
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@TableName("ai_provider_config")
public class AiProviderConfig implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@TableId
private Long id;
/**
* Provider名称
*/
private String providerName;
/**
* Provider类型
*/
private String providerType;
/**
* 是否启用(0=禁用 1=启用)
*/
private Integer enabled;
/**
* 优先级(数字越小优先级越高)
*/
private Integer priority;
/**
* 权重(用于负载均衡)
*/
private Integer weight;
/**
* 最大并发数
*/
private Integer maxConcurrency;
/**
* 超时时间(秒)
*/
private Integer timeoutSeconds;
/**
* 最大重试次数
*/
private Integer maxRetries;
/**
* 熔断器配置(JSON字符串)
*/
private String circuitBreakerConfig;
/**
* 限流配置(JSON字符串)
*/
private String rateLimitConfig;
/**
* API配置(JSON字符串)
*/
private String apiConfig;
/**
* 模型配置(JSON字符串)
*/
private String modelConfig;
/**
* 扩展配置(JSON字符串)
*/
private String extensionConfig;
/**
* 健康检查URL
*/
private String healthCheckUrl;
/**
* 健康检查间隔(秒)
*/
private Integer healthCheckInterval;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private String createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/**
* 备注
*/
private String remark;
}
@@ -0,0 +1,66 @@
package org.dromara.ai.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI Provider配置历史对象 ai_provider_config_history
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@TableName("ai_provider_config_history")
public class AiProviderConfigHistory implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@TableId
private Long id;
/**
* 配置ID
*/
private Long configId;
/**
* Provider名称
*/
private String providerName;
/**
* 配置数据(JSON字符串)
*/
private String configData;
/**
* 操作类型(CREATE/UPDATE/DELETE)
*/
private String operationType;
/**
* 操作时间
*/
private Date operationTime;
/**
* 操作者
*/
private String operationBy;
/**
* 备注
*/
private String remark;
}
@@ -0,0 +1,71 @@
package org.dromara.ai.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI评价记录对象 ai_rating_record
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@TableName("ai_rating_record")
public class AiRatingRecord implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@TableId
private Long id;
/**
* 消息ID
*/
private String messageId;
/**
* 用户ID
*/
private Long userId;
/**
* Provider名称
*/
private String providerName;
/**
* 评分(1-5)
*/
private Integer rating;
/**
* 反馈内容
*/
private String feedback;
/**
* 评价维度
*/
private String dimension;
/**
* 是否匿名(0否 1是)
*/
private Integer anonymous;
/**
* 创建时间
*/
private Date createTime;
}
@@ -0,0 +1,82 @@
package org.dromara.ai.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI敏感词对象 ai_sensitive_word
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@TableName("ai_sensitive_word")
public class AiSensitiveWord implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@TableId
private Long id;
/**
* 敏感词
*/
private String word;
/**
* 词类型(PROFANITY/HATE_SPEECH/VIOLENCE/SEXUAL_CONTENT/ILLEGAL_ACTIVITY/PERSONAL_INFO/SPAM/MALICIOUS_CODE/POLITICAL_SENSITIVE/COMMERCIAL_AD)
*/
private String wordType;
/**
* 是否启用(0=禁用 1=启用)
*/
private Integer enabled;
/**
* 优先级(数字越大优先级越高)
*/
private Integer priority;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private String createBy;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
}
@@ -0,0 +1,92 @@
package org.dromara.ai.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* AI使用统计对象 ai_usage_stats
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@TableName("ai_usage_stats")
public class AiUsageStats implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@TableId
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* Provider名称
*/
private String providerName;
/**
* 模型名称
*/
private String modelName;
/**
* 统计日期
*/
private Date statDate;
/**
* 请求次数
*/
private Integer requestCount;
/**
* 成功次数
*/
private Integer successCount;
/**
* 错误次数
*/
private Integer errorCount;
/**
* 总Token数
*/
private Long totalTokens;
/**
* 总成本
*/
private BigDecimal totalCost;
/**
* 平均响应时间(毫秒)
*/
private Integer avgResponseTime;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}
@@ -0,0 +1,107 @@
package org.dromara.ai.domain;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI用户配额对象 ai_user_quota
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@TableName("ai_user_quota")
public class AiUserQuota implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
@TableId
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 权限级别(GUEST/BASIC/PREMIUM/VIP/ADMIN)
*/
private String permissionLevel;
/**
* 每日配额限制(0表示无限制)
*/
private Long dailyQuota;
/**
* 每月配额限制(0表示无限制)
*/
private Long monthlyQuota;
/**
* 每日已使用配额
*/
private Long dailyUsed;
/**
* 每月已使用配额
*/
private Long monthlyUsed;
/**
* 上次重置日配额时间
*/
private Date lastResetDaily;
/**
* 上次重置月配额时间
*/
private Date lastResetMonthly;
/**
* 自定义限制(JSON字符串)
*/
private String customLimits;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private String createBy;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
}
@@ -0,0 +1,96 @@
package org.dromara.ai.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiChatRecord;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* AI聊天记录业务对象 ai_chat_record
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiChatRecord.class, reverseConvertGenerate = false)
public class AiChatRecordBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 会话ID
*/
private String sessionId;
/**
* 用户ID
*/
private Long userId;
/**
* Provider名称
*/
private String providerName;
/**
* 模型名称
*/
private String modelName;
/**
* 消息类型(USER/ASSISTANT)
*/
private String messageType;
/**
* 消息内容(用于搜索)
*/
private String content;
/**
* 使用的Token数
*/
private Integer tokensUsed;
/**
* 成本
*/
private BigDecimal cost;
/**
* 响应时间(毫秒)
*/
private Integer responseTime;
/**
* 是否流式响应(0否 1是)
*/
private Integer isStream;
/**
* 错误信息
*/
private String errorMessage;
/**
* 创建时间(开始)
*/
private Date createTimeStart;
/**
* 创建时间(结束)
*/
private Date createTimeEnd;
}
@@ -0,0 +1,60 @@
package org.dromara.ai.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiPaintingFavorite;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI绘画作品收藏业务对象 ai_painting_favorite
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiPaintingFavorite.class, reverseConvertGenerate = false)
public class AiPaintingFavoriteBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 绘画记录ID
*/
private Long paintingRecordId;
/**
* 任务ID
*/
private String taskId;
/**
* 备注
*/
private String remark;
/**
* 收藏时间开始
*/
private Date createTimeStart;
/**
* 收藏时间结束
*/
private Date createTimeEnd;
}
@@ -0,0 +1,95 @@
package org.dromara.ai.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiPaintingRecord;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI绘画历史记录业务对象 ai_painting_record
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiPaintingRecord.class, reverseConvertGenerate = false)
public class AiPaintingRecordBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 任务ID
*/
private String taskId;
/**
* 用户ID
*/
private Long userId;
/**
* 绘画引擎类型(DALL_E_3/MIDJOURNEY/STABLE_DIFFUSION)
*/
private String engineType;
/**
* 提示词
*/
private String prompt;
/**
* 绘画参数(JSON字符串)
*/
private String params;
/**
* 状态(PENDING/RUNNING/SUCCESS/FAILED)
*/
private String status;
/**
* 进度(0-100)
*/
private Integer progress;
/**
* 生成的图片URL列表(JSON字符串)
*/
private String images;
/**
* 错误消息
*/
private String message;
/**
* 创建时间开始
*/
private Date createTimeStart;
/**
* 创建时间结束
*/
private Date createTimeEnd;
/**
* 分享码(用于分享作品)
*/
private String shareCode;
/**
* 是否公开分享(0否 1是)
*/
private Integer isPublic;
}
@@ -0,0 +1,78 @@
package org.dromara.ai.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiPaintingTemplate;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI绘画参数模板业务对象 ai_painting_template
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiPaintingTemplate.class, reverseConvertGenerate = false)
public class AiPaintingTemplateBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 模板名称
*/
@NotBlank(message = "模板名称不能为空")
private String templateName;
/**
* 用户ID(为空表示系统模板)
*/
private Long userId;
/**
* 绘画引擎类型(DALL_E_3/MIDJOURNEY/STABLE_DIFFUSION)
*/
@NotBlank(message = "引擎类型不能为空")
private String engineType;
/**
* 绘画参数(JSON字符串)
*/
private String params;
/**
* 模板描述
*/
private String description;
/**
* 是否公开(0=私有 1=公开)
*/
private Integer isPublic;
/**
* 排序
*/
private Integer sort;
/**
* 创建时间开始
*/
private Date createTimeStart;
/**
* 创建时间结束
*/
private Date createTimeEnd;
}
@@ -0,0 +1,123 @@
package org.dromara.ai.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiProviderConfig;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI Provider配置业务对象 ai_provider_config
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiProviderConfig.class, reverseConvertGenerate = false)
public class AiProviderConfigBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* Provider名称
*/
@NotBlank(message = "Provider名称不能为空")
private String providerName;
/**
* Provider类型
*/
@NotBlank(message = "Provider类型不能为空")
private String providerType;
/**
* 是否启用(0=禁用 1=启用)
*/
private Integer enabled;
/**
* 优先级(数字越小优先级越高)
*/
private Integer priority;
/**
* 权重(用于负载均衡)
*/
private Integer weight;
/**
* 最大并发数
*/
private Integer maxConcurrency;
/**
* 超时时间(秒)
*/
private Integer timeoutSeconds;
/**
* 最大重试次数
*/
private Integer maxRetries;
/**
* 熔断器配置(JSON字符串)
*/
private String circuitBreakerConfig;
/**
* 限流配置(JSON字符串)
*/
private String rateLimitConfig;
/**
* API配置(JSON字符串)
*/
private String apiConfig;
/**
* 模型配置(JSON字符串)
*/
private String modelConfig;
/**
* 扩展配置(JSON字符串)
*/
private String extensionConfig;
/**
* 健康检查URL
*/
private String healthCheckUrl;
/**
* 健康检查间隔(秒)
*/
private Integer healthCheckInterval;
/**
* 备注
*/
private String remark;
/**
* 创建时间开始(用于查询)
*/
private Date createTimeStart;
/**
* 创建时间结束(用于查询)
*/
private Date createTimeEnd;
}
@@ -0,0 +1,75 @@
package org.dromara.ai.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiProviderConfigHistory;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI Provider配置历史业务对象 ai_provider_config_history
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiProviderConfigHistory.class, reverseConvertGenerate = false)
public class AiProviderConfigHistoryBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 配置ID
*/
private Long configId;
/**
* Provider名称
*/
private String providerName;
/**
* 配置数据(JSON字符串)
*/
private String configData;
/**
* 操作类型(CREATE/UPDATE/DELETE)
*/
private String operationType;
/**
* 操作时间
*/
private Date operationTime;
/**
* 操作时间开始
*/
private Date operationTimeStart;
/**
* 操作时间结束
*/
private Date operationTimeEnd;
/**
* 操作者
*/
private String operationBy;
/**
* 备注
*/
private String remark;
}
@@ -0,0 +1,75 @@
package org.dromara.ai.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiRatingRecord;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI评价记录业务对象 ai_rating_record
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiRatingRecord.class, reverseConvertGenerate = false)
public class AiRatingRecordBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 消息ID
*/
private String messageId;
/**
* 用户ID
*/
private Long userId;
/**
* Provider名称
*/
private String providerName;
/**
* 评分(1-5)
*/
private Integer rating;
/**
* 反馈内容
*/
private String feedback;
/**
* 评价维度
*/
private String dimension;
/**
* 是否匿名(0否 1是)
*/
private Integer anonymous;
/**
* 创建时间开始(用于查询)
*/
private Date createTimeStart;
/**
* 创建时间结束(用于查询)
*/
private Date createTimeEnd;
}
@@ -0,0 +1,68 @@
package org.dromara.ai.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiSensitiveWord;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI敏感词业务对象 ai_sensitive_word
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiSensitiveWord.class, reverseConvertGenerate = false)
public class AiSensitiveWordBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 敏感词
*/
@NotBlank(message = "敏感词不能为空")
private String word;
/**
* 词类型(PROFANITY/HATE_SPEECH/VIOLENCE/SEXUAL_CONTENT/ILLEGAL_ACTIVITY/PERSONAL_INFO/SPAM/MALICIOUS_CODE/POLITICAL_SENSITIVE/COMMERCIAL_AD)
*/
@NotBlank(message = "词类型不能为空")
private String wordType;
/**
* 是否启用(0=禁用 1=启用)
*/
private Integer enabled;
/**
* 优先级(数字越大优先级越高)
*/
private Integer priority;
/**
* 备注
*/
private String remark;
/**
* 创建时间开始
*/
private Date createTimeStart;
/**
* 创建时间结束
*/
private Date createTimeEnd;
}
@@ -0,0 +1,86 @@
package org.dromara.ai.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiUsageStats;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* AI使用统计业务对象 ai_usage_stats
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiUsageStats.class, reverseConvertGenerate = false)
public class AiUsageStatsBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* Provider名称
*/
private String providerName;
/**
* 模型名称
*/
private String modelName;
/**
* 统计日期(开始)
*/
private Date statDateStart;
/**
* 统计日期(结束)
*/
private Date statDateEnd;
/**
* 请求次数
*/
private Integer requestCount;
/**
* 成功次数
*/
private Integer successCount;
/**
* 错误次数
*/
private Integer errorCount;
/**
* 总Token数
*/
private Long totalTokens;
/**
* 总成本
*/
private BigDecimal totalCost;
/**
* 平均响应时间(毫秒)
*/
private Integer avgResponseTime;
}
@@ -0,0 +1,90 @@
package org.dromara.ai.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiUserQuota;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI用户配额业务对象 ai_user_quota
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiUserQuota.class, reverseConvertGenerate = false)
public class AiUserQuotaBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 权限级别(GUEST/BASIC/PREMIUM/VIP/ADMIN)
*/
private String permissionLevel;
/**
* 每日配额限制(0表示无限制)
*/
private Long dailyQuota;
/**
* 每月配额限制(0表示无限制)
*/
private Long monthlyQuota;
/**
* 每日已使用配额
*/
private Long dailyUsed;
/**
* 每月已使用配额
*/
private Long monthlyUsed;
/**
* 上次重置日配额时间
*/
private Date lastResetDaily;
/**
* 上次重置月配额时间
*/
private Date lastResetMonthly;
/**
* 自定义限制(JSON字符串)
*/
private String customLimits;
/**
* 备注
*/
private String remark;
/**
* 创建时间开始
*/
private Date createTimeStart;
/**
* 创建时间结束
*/
private Date createTimeEnd;
}
@@ -0,0 +1,165 @@
package org.dromara.ai.domain.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
* AI聊天请求DTO
*
* @author pengles
*/
@Data
@Accessors(chain = true)
public class AiChatRequest implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 模型名称
*/
private String model;
/**
* 对话消息列表
*/
private List<AiMessage> messages;
/**
* 温度参数 (0.0-2.0)
*/
private Double temperature;
/**
* 最大生成token数
*/
private Integer maxTokens;
/**
* Top-p参数 (0.0-1.0)
*/
private Double topP;
/**
* 频率惩罚 (-2.0-2.0)
*/
private Double frequencyPenalty;
/**
* 存在惩罚 (-2.0-2.0)
*/
private Double presencePenalty;
/**
* 停止词列表
*/
private List<String> stop;
/**
* 是否流式输出
*/
private Boolean stream;
/**
* 用户ID
*/
private String userId;
/**
* 会话ID
*/
private String sessionId;
/**
* 知识库ID
*/
private String knowledgeId;
/**
* 是否启用知识库检索
*/
private Boolean enableKnowledge;
/**
* 知识库检索数量
*/
private Integer knowledgeLimit;
/**
* 知识库检索阈值
*/
private Double knowledgeThreshold;
/**
* 扩展参数
*/
private Map<String, Object> extra;
private Boolean enableTools;
private java.util.List<String> requestedTools;
private Map<String, Object> toolContext;
/**
* AI消息
*/
@Data
@Accessors(chain = true)
public static class AiMessage implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 消息角色
*/
private String role;
/**
* 消息内容
*/
private String content;
/**
* 消息名称
*/
private String name;
/**
* 函数调用信息
*/
private Object functionCall;
/**
* 工具调用信息
*/
private List<Object> toolCalls;
/**
* 工具调用ID
*/
private String toolCallId;
}
/**
* 消息角色枚举
*/
public enum MessageRole {
SYSTEM("system"),
USER("user"),
ASSISTANT("assistant"),
FUNCTION("function"),
TOOL("tool");
private final String value;
MessageRole(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
}
@@ -0,0 +1,222 @@
package org.dromara.ai.domain.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* AI聊天响应DTO
*
* @author pengles
*/
@Data
@Accessors(chain = true)
public class AiChatResponse implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 响应ID
*/
private String id;
/**
* 对象类型
*/
private String object;
/**
* 创建时间
*/
private Long created;
/**
* 模型名称
*/
private String model;
/**
* 选择列表
*/
private List<Choice> choices;
/**
* 使用情况
*/
private Usage usage;
/**
* 系统指纹
*/
private String systemFingerprint;
/**
* 提供商名称
*/
private String provider;
/**
* 响应时间(毫秒)
*/
private Long responseTime;
/**
* 是否成功
*/
private Boolean success;
/**
* 错误信息
*/
private String error;
/**
* 错误代码
*/
private String errorCode;
/**
* 知识库检索结果
*/
private List<KnowledgeResult> knowledgeResults;
/**
* 扩展信息
*/
private Map<String, Object> extra;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 选择项
*/
@Data
@Accessors(chain = true)
public static class Choice implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 索引
*/
private Integer index;
/**
* 消息
*/
private AiChatRequest.AiMessage message;
/**
* 增量消息(流式)
*/
private AiChatRequest.AiMessage delta;
/**
* 结束原因
*/
private String finishReason;
/**
* 日志概率
*/
private Object logprobs;
}
/**
* 使用情况
*/
@Data
@Accessors(chain = true)
public static class Usage implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 提示token数
*/
private Integer promptTokens;
/**
* 完成token数
*/
private Integer completionTokens;
/**
* 总token数
*/
private Integer totalTokens;
/**
* 提示token详情
*/
private Object promptTokensDetails;
/**
* 完成token详情
*/
private Object completionTokensDetails;
}
/**
* 知识库检索结果
*/
@Data
@Accessors(chain = true)
public static class KnowledgeResult implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 文档ID
*/
private String documentId;
/**
* 文档标题
*/
private String title;
/**
* 文档内容
*/
private String content;
/**
* 相似度分数
*/
private Double score;
/**
* 元数据
*/
private Map<String, Object> metadata;
}
/**
* 结束原因枚举
*/
public enum FinishReason {
STOP("stop"),
LENGTH("length"),
FUNCTION_CALL("function_call"),
TOOL_CALLS("tool_calls"),
CONTENT_FILTER("content_filter");
private final String value;
FinishReason(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
}
@@ -0,0 +1,394 @@
package org.dromara.ai.domain.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* AI模型信息DTO
*
* @author pengles
*/
@Data
@Accessors(chain = true)
public class AiModelInfo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 模型ID
*/
private String id;
/**
* 模型名称
*/
private String name;
/**
* 模型显示名称
*/
private String displayName;
/**
* 模型描述
*/
private String description;
/**
* 模型版本
*/
private String version;
/**
* 提供商类型
*/
private String provider;
/**
* 模型类型
*/
private ModelType type;
/**
* 模型能力
*/
private List<ModelCapability> capabilities;
/**
* 最大上下文长度
*/
private Integer maxContextLength;
/**
* 最大输出长度
*/
private Integer maxOutputLength;
/**
* 输入价格(每1K token
*/
private Double inputPrice;
/**
* 输出价格(每1K token
*/
private Double outputPrice;
/**
* 是否支持流式输出
*/
private Boolean supportStream;
/**
* 是否支持函数调用
*/
private Boolean supportFunction;
/**
* 是否支持工具调用
*/
private Boolean supportTool;
/**
* 是否支持视觉
*/
private Boolean supportVision;
/**
* 是否支持图像生成
*/
private Boolean supportImageGeneration;
/**
* 是否支持语音
*/
private Boolean supportAudio;
/**
* 是否可用
*/
private Boolean available;
/**
* 模型状态
*/
private ModelStatus status;
/**
* 创建时间
*/
private LocalDateTime created;
/**
* 更新时间
*/
private LocalDateTime updated;
/**
* 模型参数
*/
private ModelParameters parameters;
/**
* 扩展信息
*/
private Map<String, Object> extra;
/**
* 模型类型枚举
*/
public enum ModelType {
/**
* 文本生成
*/
TEXT_GENERATION("text_generation", "文本生成"),
/**
* 聊天对话
*/
CHAT("chat", "聊天对话"),
/**
* 代码生成
*/
CODE_GENERATION("code_generation", "代码生成"),
/**
* 图像生成
*/
IMAGE_GENERATION("image_generation", "图像生成"),
/**
* 语音合成
*/
TEXT_TO_SPEECH("text_to_speech", "语音合成"),
/**
* 语音识别
*/
SPEECH_TO_TEXT("speech_to_text", "语音识别"),
/**
* 嵌入向量
*/
EMBEDDING("embedding", "嵌入向量"),
/**
* 多模态
*/
MULTIMODAL("multimodal", "多模态");
private final String code;
private final String name;
ModelType(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
}
/**
* 模型能力枚举
*/
public enum ModelCapability {
/**
* 文本理解
*/
TEXT_UNDERSTANDING("text_understanding", "文本理解"),
/**
* 文本生成
*/
TEXT_GENERATION("text_generation", "文本生成"),
/**
* 对话聊天
*/
CONVERSATION("conversation", "对话聊天"),
/**
* 代码理解
*/
CODE_UNDERSTANDING("code_understanding", "代码理解"),
/**
* 代码生成
*/
CODE_GENERATION("code_generation", "代码生成"),
/**
* 函数调用
*/
FUNCTION_CALLING("function_calling", "函数调用"),
/**
* 工具使用
*/
TOOL_USE("tool_use", "工具使用"),
/**
* 图像理解
*/
IMAGE_UNDERSTANDING("image_understanding", "图像理解"),
/**
* 图像生成
*/
IMAGE_GENERATION("image_generation", "图像生成"),
/**
* 语音识别
*/
SPEECH_RECOGNITION("speech_recognition", "语音识别"),
/**
* 语音合成
*/
SPEECH_SYNTHESIS("speech_synthesis", "语音合成"),
/**
* 文档分析
*/
DOCUMENT_ANALYSIS("document_analysis", "文档分析"),
/**
* 知识问答
*/
KNOWLEDGE_QA("knowledge_qa", "知识问答");
private final String code;
private final String name;
ModelCapability(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
}
/**
* 模型状态枚举
*/
public enum ModelStatus {
/**
* 可用
*/
AVAILABLE("available", "可用"),
/**
* 不可用
*/
UNAVAILABLE("unavailable", "不可用"),
/**
* 维护中
*/
MAINTENANCE("maintenance", "维护中"),
/**
* 已弃用
*/
DEPRECATED("deprecated", "已弃用");
private final String code;
private final String name;
ModelStatus(String code, String name) {
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
}
/**
* 模型参数
*/
@Data
@Accessors(chain = true)
public static class ModelParameters implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 默认温度
*/
private Double defaultTemperature;
/**
* 最小温度
*/
private Double minTemperature;
/**
* 最大温度
*/
private Double maxTemperature;
/**
* 默认Top-p
*/
private Double defaultTopP;
/**
* 最小Top-p
*/
private Double minTopP;
/**
* 最大Top-p
*/
private Double maxTopP;
/**
* 默认最大token数
*/
private Integer defaultMaxTokens;
/**
* 最小最大token数
*/
private Integer minMaxTokens;
/**
* 最大最大token数
*/
private Integer maxMaxTokens;
/**
* 支持的停止词数量
*/
private Integer maxStopSequences;
/**
* 其他参数
*/
private Map<String, Object> others;
}
}
@@ -0,0 +1,125 @@
package org.dromara.ai.domain.enums;
/**
* AI服务提供商类型枚举
*
* @author pengles
*/
public enum AiProviderType {
/**
* OpenAI
*/
OPENAI("openai", "OpenAI", "OpenAI官方API服务"),
/**
* Azure OpenAI
*/
AZURE_OPENAI("azure_openai", "Azure OpenAI", "微软Azure OpenAI服务"),
/**
* 百度文心一言
*/
BAIDU_WENXIN("baidu_wenxin", "百度文心一言", "百度文心一言大模型"),
/**
* 阿里通义千问
*/
ALIBABA_TONGYI("alibaba_tongyi", "阿里通义千问", "阿里巴巴通义千问大模型"),
/**
* 腾讯混元
*/
TENCENT_HUNYUAN("tencent_hunyuan", "腾讯混元", "腾讯混元大模型"),
/**
* 讯飞星火
*/
IFLYTEK_SPARK("iflytek_spark", "讯飞星火", "科大讯飞星火认知大模型"),
/**
* 智谱AI
*/
ZHIPU_AI("zhipu_ai", "智谱AI", "智谱AI大模型"),
/**
* 月之暗面Kimi
*/
MOONSHOT_KIMI("moonshot_kimi", "月之暗面Kimi", "月之暗面Kimi大模型"),
/**
* 字节豆包
*/
BYTEDANCE_DOUBAO("bytedance_doubao", "字节豆包", "字节跳动豆包大模型"),
/**
* 自定义提供商
*/
CUSTOM("custom", "自定义", "自定义AI服务提供商"),
FASTGPT("fastgpt", "FastGPT", "FastGPT平台"),
COZE("coze", "Coze", "Coze平台"),
DIFY("dify", "Dify", "Dify平台");
/**
* 提供商代码
*/
private final String code;
/**
* 提供商名称
*/
private final String name;
/**
* 提供商描述
*/
private final String description;
AiProviderType(String code, String name, String description) {
this.code = code;
this.name = name;
this.description = description;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
/**
* 根据代码获取提供商类型
*
* @param code 提供商代码
* @return 提供商类型
*/
public static AiProviderType fromCode(String code) {
for (AiProviderType type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return CUSTOM;
}
/**
* 根据名称获取提供商类型
*
* @param name 提供商名称
* @return 提供商类型
*/
public static AiProviderType fromName(String name) {
for (AiProviderType type : values()) {
if (type.getName().equals(name)) {
return type;
}
}
return CUSTOM;
}
}
@@ -0,0 +1,94 @@
package org.dromara.ai.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.dromara.ai.domain.AiChatRecord;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* AI聊天记录视图对象 ai_chat_record
*
* @author pengles
*/
@Data
@AutoMapper(target = AiChatRecord.class)
public class AiChatRecordVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 会话ID
*/
private String sessionId;
/**
* 用户ID
*/
private Long userId;
/**
* Provider名称
*/
private String providerName;
/**
* 模型名称
*/
private String modelName;
/**
* 消息类型(USER/ASSISTANT)
*/
private String messageType;
/**
* 消息内容
*/
private String content;
/**
* 使用的Token数
*/
private Integer tokensUsed;
/**
* 成本
*/
private BigDecimal cost;
/**
* 响应时间(毫秒)
*/
private Integer responseTime;
/**
* 是否流式响应(0否 1是)
*/
private Integer isStream;
/**
* 是否流式响应(显示用)
*/
private String isStreamStr;
/**
* 错误信息
*/
private String errorMessage;
/**
* 创建时间
*/
private Date createTime;
}
@@ -0,0 +1,60 @@
package org.dromara.ai.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiPaintingFavorite;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI绘画作品收藏视图对象 ai_painting_favorite
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiPaintingFavorite.class)
public class AiPaintingFavoriteVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 绘画记录ID
*/
private Long paintingRecordId;
/**
* 任务ID
*/
private String taskId;
/**
* 备注
*/
private String remark;
/**
* 收藏时间
*/
private Date createTime;
/**
* 绘画记录信息(关联查询)
*/
private AiPaintingRecordVo paintingRecord;
}
@@ -0,0 +1,110 @@
package org.dromara.ai.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiPaintingRecord;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI绘画历史记录视图对象 ai_painting_record
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiPaintingRecord.class)
public class AiPaintingRecordVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 任务ID
*/
private String taskId;
/**
* 用户ID
*/
private Long userId;
/**
* 绘画引擎类型(DALL_E_3/MIDJOURNEY/STABLE_DIFFUSION)
*/
private String engineType;
/**
* 绘画引擎类型(字符串显示)
*/
private String engineTypeStr;
/**
* 提示词
*/
private String prompt;
/**
* 绘画参数(JSON字符串)
*/
private String params;
/**
* 状态(PENDING/RUNNING/SUCCESS/FAILED)
*/
private String status;
/**
* 状态(字符串显示)
*/
private String statusStr;
/**
* 进度(0-100)
*/
private Integer progress;
/**
* 生成的图片URL列表(JSON字符串)
*/
private String images;
/**
* 错误消息
*/
private String message;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 分享码(用于分享作品)
*/
private String shareCode;
/**
* 是否公开分享(0否 1是)
*/
private Integer isPublic;
/**
* 是否公开分享(字符串显示)
*/
private String isPublicStr;
}
@@ -0,0 +1,95 @@
package org.dromara.ai.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiPaintingTemplate;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI绘画参数模板视图对象 ai_painting_template
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiPaintingTemplate.class)
public class AiPaintingTemplateVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 模板名称
*/
private String templateName;
/**
* 用户ID(为空表示系统模板)
*/
private Long userId;
/**
* 绘画引擎类型(DALL_E_3/MIDJOURNEY/STABLE_DIFFUSION)
*/
private String engineType;
/**
* 绘画引擎类型(字符串显示)
*/
private String engineTypeStr;
/**
* 绘画参数(JSON字符串)
*/
private String params;
/**
* 模板描述
*/
private String description;
/**
* 是否公开(0=私有 1=公开)
*/
private Integer isPublic;
/**
* 是否公开(字符串显示)
*/
private String isPublicStr;
/**
* 排序
*/
private Integer sort;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 创建者
*/
private String createBy;
/**
* 更新者
*/
private String updateBy;
}
@@ -0,0 +1,70 @@
package org.dromara.ai.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiProviderConfigHistory;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI Provider配置历史视图对象 ai_provider_config_history
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiProviderConfigHistory.class)
public class AiProviderConfigHistoryVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 配置ID
*/
private Long configId;
/**
* Provider名称
*/
private String providerName;
/**
* 配置数据(JSON字符串)
*/
private String configData;
/**
* 操作类型(CREATE/UPDATE/DELETE)
*/
private String operationType;
/**
* 操作类型(字符串显示)
*/
private String operationTypeStr;
/**
* 操作时间
*/
private Date operationTime;
/**
* 操作者
*/
private String operationBy;
/**
* 备注
*/
private String remark;
}
@@ -0,0 +1,135 @@
package org.dromara.ai.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiProviderConfig;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI Provider配置视图对象 ai_provider_config
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiProviderConfig.class)
public class AiProviderConfigVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* Provider名称
*/
private String providerName;
/**
* Provider类型
*/
private String providerType;
/**
* 是否启用(0=禁用 1=启用)
*/
private Integer enabled;
/**
* 是否启用(字符串显示)
*/
private String enabledStr;
/**
* 优先级(数字越小优先级越高)
*/
private Integer priority;
/**
* 权重(用于负载均衡)
*/
private Integer weight;
/**
* 最大并发数
*/
private Integer maxConcurrency;
/**
* 超时时间(秒)
*/
private Integer timeoutSeconds;
/**
* 最大重试次数
*/
private Integer maxRetries;
/**
* 熔断器配置(JSON字符串)
*/
private String circuitBreakerConfig;
/**
* 限流配置(JSON字符串)
*/
private String rateLimitConfig;
/**
* API配置(JSON字符串)
*/
private String apiConfig;
/**
* 模型配置(JSON字符串)
*/
private String modelConfig;
/**
* 扩展配置(JSON字符串)
*/
private String extensionConfig;
/**
* 健康检查URL
*/
private String healthCheckUrl;
/**
* 健康检查间隔(秒)
*/
private Integer healthCheckInterval;
/**
* 创建者
*/
private String createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新者
*/
private String updateBy;
/**
* 更新时间
*/
private Date updateTime;
/**
* 备注
*/
private String remark;
}
@@ -0,0 +1,75 @@
package org.dromara.ai.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiRatingRecord;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI评价记录视图对象 ai_rating_record
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiRatingRecord.class)
public class AiRatingRecordVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 消息ID
*/
private String messageId;
/**
* 用户ID
*/
private Long userId;
/**
* Provider名称
*/
private String providerName;
/**
* 评分(1-5)
*/
private Integer rating;
/**
* 反馈内容
*/
private String feedback;
/**
* 评价维度
*/
private String dimension;
/**
* 是否匿名(0否 1是)
*/
private Integer anonymous;
/**
* 是否匿名(字符串显示)
*/
private String anonymousStr;
/**
* 创建时间
*/
private Date createTime;
}
@@ -0,0 +1,85 @@
package org.dromara.ai.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiSensitiveWord;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI敏感词视图对象 ai_sensitive_word
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiSensitiveWord.class)
public class AiSensitiveWordVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 敏感词
*/
private String word;
/**
* 词类型(PROFANITY/HATE_SPEECH/VIOLENCE/SEXUAL_CONTENT/ILLEGAL_ACTIVITY/PERSONAL_INFO/SPAM/MALICIOUS_CODE/POLITICAL_SENSITIVE/COMMERCIAL_AD)
*/
private String wordType;
/**
* 词类型(字符串显示)
*/
private String wordTypeStr;
/**
* 是否启用(0=禁用 1=启用)
*/
private Integer enabled;
/**
* 是否启用(字符串显示)
*/
private String enabledStr;
/**
* 优先级(数字越大优先级越高)
*/
private Integer priority;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 创建者
*/
private String createBy;
/**
* 更新者
*/
private String updateBy;
}
@@ -0,0 +1,94 @@
package org.dromara.ai.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.dromara.ai.domain.AiUsageStats;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* AI使用统计视图对象 ai_usage_stats
*
* @author pengles
*/
@Data
@AutoMapper(target = AiUsageStats.class)
public class AiUsageStatsVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* Provider名称
*/
private String providerName;
/**
* 模型名称
*/
private String modelName;
/**
* 统计日期
*/
private Date statDate;
/**
* 请求次数
*/
private Integer requestCount;
/**
* 成功次数
*/
private Integer successCount;
/**
* 错误次数
*/
private Integer errorCount;
/**
* 总Token数
*/
private Long totalTokens;
/**
* 总成本
*/
private BigDecimal totalCost;
/**
* 平均响应时间(毫秒)
*/
private Integer avgResponseTime;
/**
* 成功率(计算字段)
*/
private Double successRate;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}
@@ -0,0 +1,115 @@
package org.dromara.ai.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.ai.domain.AiUserQuota;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* AI用户配额视图对象 ai_user_quota
*
* @author pengles
*/
@Data
@EqualsAndHashCode
@AutoMapper(target = AiUserQuota.class)
public class AiUserQuotaVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* ID
*/
private Long id;
/**
* 用户ID
*/
private Long userId;
/**
* 权限级别(GUEST/BASIC/PREMIUM/VIP/ADMIN)
*/
private String permissionLevel;
/**
* 权限级别(字符串显示)
*/
private String permissionLevelStr;
/**
* 每日配额限制(0表示无限制)
*/
private Long dailyQuota;
/**
* 每月配额限制(0表示无限制)
*/
private Long monthlyQuota;
/**
* 每日已使用配额
*/
private Long dailyUsed;
/**
* 每月已使用配额
*/
private Long monthlyUsed;
/**
* 每日剩余配额
*/
private Long dailyRemaining;
/**
* 每月剩余配额
*/
private Long monthlyRemaining;
/**
* 上次重置日配额时间
*/
private Date lastResetDaily;
/**
* 上次重置月配额时间
*/
private Date lastResetMonthly;
/**
* 自定义限制(JSON字符串)
*/
private String customLimits;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 创建者
*/
private String createBy;
/**
* 更新者
*/
private String updateBy;
}
@@ -0,0 +1,836 @@
package org.dromara.ai.error;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.domain.dto.AiChatRequest;
import org.dromara.ai.domain.dto.AiChatResponse;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* AI错误处理和故障恢复服务
* 提供统一的错误处理、重试机制和故障恢复功能
*
* @author pengles
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AiErrorHandler {
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
// 错误统计
private final Map<String, ErrorStatistics> errorStats = new ConcurrentHashMap<>();
// 熔断器状态
private final Map<String, CircuitBreakerState> circuitBreakers = new ConcurrentHashMap<>();
// 重试配置
private final Map<String, RetryConfig> retryConfigs = new ConcurrentHashMap<>();
/**
* 错误类型枚举
*/
public enum ErrorType {
NETWORK_ERROR, // 网络错误
TIMEOUT_ERROR, // 超时错误
RATE_LIMIT_ERROR, // 速率限制错误
QUOTA_EXCEEDED_ERROR, // 配额超限错误
AUTHENTICATION_ERROR, // 认证错误
AUTHORIZATION_ERROR, // 授权错误
VALIDATION_ERROR, // 验证错误
SERVICE_UNAVAILABLE, // 服务不可用
INTERNAL_ERROR, // 内部错误
UNKNOWN_ERROR // 未知错误
}
/**
* 错误严重级别
*/
public enum ErrorSeverity {
LOW, // 低级别
MEDIUM, // 中级别
HIGH, // 高级别
CRITICAL // 严重级别
}
/**
* 熔断器状态
*/
public enum CircuitBreakerStatus {
CLOSED, // 关闭状态(正常)
OPEN, // 开启状态(熔断)
HALF_OPEN // 半开状态(试探)
}
/**
* 错误信息类
*/
public static class AiError {
private String id;
private ErrorType type;
private ErrorSeverity severity;
private String message;
private String details;
private String provider;
private String model;
private String userId;
private LocalDateTime occurTime;
private Map<String, Object> context;
private Throwable cause;
// Getters and Setters
public String getId() {
return id;
}
public AiError setId(String id) {
this.id = id;
return this;
}
public ErrorType getType() {
return type;
}
public AiError setType(ErrorType type) {
this.type = type;
return this;
}
public ErrorSeverity getSeverity() {
return severity;
}
public AiError setSeverity(ErrorSeverity severity) {
this.severity = severity;
return this;
}
public String getMessage() {
return message;
}
public AiError setMessage(String message) {
this.message = message;
return this;
}
public String getDetails() {
return details;
}
public AiError setDetails(String details) {
this.details = details;
return this;
}
public String getProvider() {
return provider;
}
public AiError setProvider(String provider) {
this.provider = provider;
return this;
}
public String getModel() {
return model;
}
public AiError setModel(String model) {
this.model = model;
return this;
}
public String getUserId() {
return userId;
}
public AiError setUserId(String userId) {
this.userId = userId;
return this;
}
public LocalDateTime getOccurTime() {
return occurTime;
}
public AiError setOccurTime(LocalDateTime occurTime) {
this.occurTime = occurTime;
return this;
}
public Map<String, Object> getContext() {
return context;
}
public AiError setContext(Map<String, Object> context) {
this.context = context;
return this;
}
public Throwable getCause() {
return cause;
}
public AiError setCause(Throwable cause) {
this.cause = cause;
return this;
}
}
/**
* 错误统计类
*/
public static class ErrorStatistics {
private String key;
private long totalErrors;
private long networkErrors;
private long timeoutErrors;
private long rateLimitErrors;
private long quotaErrors;
private long authErrors;
private long validationErrors;
private long serviceUnavailableErrors;
private long internalErrors;
private long unknownErrors;
private LocalDateTime lastErrorTime;
private LocalDateTime firstErrorTime;
private double errorRate;
public ErrorStatistics(String key) {
this.key = key;
this.firstErrorTime = LocalDateTime.now();
}
public void incrementError(ErrorType type) {
totalErrors++;
lastErrorTime = LocalDateTime.now();
switch (type) {
case NETWORK_ERROR -> networkErrors++;
case TIMEOUT_ERROR -> timeoutErrors++;
case RATE_LIMIT_ERROR -> rateLimitErrors++;
case QUOTA_EXCEEDED_ERROR -> quotaErrors++;
case AUTHENTICATION_ERROR, AUTHORIZATION_ERROR -> authErrors++;
case VALIDATION_ERROR -> validationErrors++;
case SERVICE_UNAVAILABLE -> serviceUnavailableErrors++;
case INTERNAL_ERROR -> internalErrors++;
default -> unknownErrors++;
}
}
// Getters
public String getKey() {
return key;
}
public long getTotalErrors() {
return totalErrors;
}
public long getNetworkErrors() {
return networkErrors;
}
public long getTimeoutErrors() {
return timeoutErrors;
}
public long getRateLimitErrors() {
return rateLimitErrors;
}
public long getQuotaErrors() {
return quotaErrors;
}
public long getAuthErrors() {
return authErrors;
}
public long getValidationErrors() {
return validationErrors;
}
public long getServiceUnavailableErrors() {
return serviceUnavailableErrors;
}
public long getInternalErrors() {
return internalErrors;
}
public long getUnknownErrors() {
return unknownErrors;
}
public LocalDateTime getLastErrorTime() {
return lastErrorTime;
}
public LocalDateTime getFirstErrorTime() {
return firstErrorTime;
}
public double getErrorRate() {
return errorRate;
}
public void setErrorRate(double errorRate) {
this.errorRate = errorRate;
}
}
/**
* 熔断器状态类
*/
public static class CircuitBreakerState {
private String key;
private CircuitBreakerStatus status;
private long failureCount;
private long successCount;
private LocalDateTime lastFailureTime;
private LocalDateTime lastSuccessTime;
private LocalDateTime nextRetryTime;
private int failureThreshold;
private int successThreshold;
private long timeoutMs;
public CircuitBreakerState(String key) {
this.key = key;
this.status = CircuitBreakerStatus.CLOSED;
this.failureThreshold = 5;
this.successThreshold = 3;
this.timeoutMs = 60000; // 1分钟
}
public void recordSuccess() {
successCount++;
lastSuccessTime = LocalDateTime.now();
if (status == CircuitBreakerStatus.HALF_OPEN && successCount >= successThreshold) {
status = CircuitBreakerStatus.CLOSED;
failureCount = 0;
successCount = 0;
}
}
public void recordFailure() {
failureCount++;
lastFailureTime = LocalDateTime.now();
if (status == CircuitBreakerStatus.CLOSED && failureCount >= failureThreshold) {
status = CircuitBreakerStatus.OPEN;
nextRetryTime = LocalDateTime.now().plusNanos(timeoutMs * 1_000_000);
} else if (status == CircuitBreakerStatus.HALF_OPEN) {
status = CircuitBreakerStatus.OPEN;
nextRetryTime = LocalDateTime.now().plusNanos(timeoutMs * 1_000_000);
successCount = 0;
}
}
public boolean canExecute() {
if (status == CircuitBreakerStatus.CLOSED) {
return true;
} else if (status == CircuitBreakerStatus.OPEN) {
if (LocalDateTime.now().isAfter(nextRetryTime)) {
status = CircuitBreakerStatus.HALF_OPEN;
return true;
}
return false;
} else { // HALF_OPEN
return true;
}
}
// Getters
public String getKey() {
return key;
}
public CircuitBreakerStatus getStatus() {
return status;
}
public long getFailureCount() {
return failureCount;
}
public long getSuccessCount() {
return successCount;
}
public LocalDateTime getLastFailureTime() {
return lastFailureTime;
}
public LocalDateTime getLastSuccessTime() {
return lastSuccessTime;
}
public LocalDateTime getNextRetryTime() {
return nextRetryTime;
}
}
/**
* 重试配置类
*/
public static class RetryConfig {
private int maxAttempts;
private long initialDelayMs;
private long maxDelayMs;
private double backoffMultiplier;
private boolean exponentialBackoff;
public RetryConfig() {
this.maxAttempts = 3;
this.initialDelayMs = 1000;
this.maxDelayMs = 30000;
this.backoffMultiplier = 2.0;
this.exponentialBackoff = true;
}
// Getters and Setters
public int getMaxAttempts() {
return maxAttempts;
}
public RetryConfig setMaxAttempts(int maxAttempts) {
this.maxAttempts = maxAttempts;
return this;
}
public long getInitialDelayMs() {
return initialDelayMs;
}
public RetryConfig setInitialDelayMs(long initialDelayMs) {
this.initialDelayMs = initialDelayMs;
return this;
}
public long getMaxDelayMs() {
return maxDelayMs;
}
public RetryConfig setMaxDelayMs(long maxDelayMs) {
this.maxDelayMs = maxDelayMs;
return this;
}
public double getBackoffMultiplier() {
return backoffMultiplier;
}
public RetryConfig setBackoffMultiplier(double backoffMultiplier) {
this.backoffMultiplier = backoffMultiplier;
return this;
}
public boolean isExponentialBackoff() {
return exponentialBackoff;
}
public RetryConfig setExponentialBackoff(boolean exponentialBackoff) {
this.exponentialBackoff = exponentialBackoff;
return this;
}
}
/**
* 执行带错误处理的操作
*
* @param key 操作标识
* @param operation 操作函数
* @param request 请求对象
* @return 响应结果
*/
public AiChatResponse executeWithErrorHandling(String key,
Function<AiChatRequest, AiChatResponse> operation,
AiChatRequest request) {
CircuitBreakerState circuitBreaker = getOrCreateCircuitBreaker(key);
// 检查熔断器状态
if (!circuitBreaker.canExecute()) {
AiError error = new AiError()
.setType(ErrorType.SERVICE_UNAVAILABLE)
.setSeverity(ErrorSeverity.HIGH)
.setMessage("服务熔断中,暂时不可用")
.setProvider(key)
.setUserId(request.getUserId())
.setOccurTime(LocalDateTime.now());
recordError(key, error);
return createErrorResponse(error, request);
}
RetryConfig retryConfig = getOrCreateRetryConfig(key);
for (int attempt = 1; attempt <= retryConfig.getMaxAttempts(); attempt++) {
try {
AiChatResponse response = operation.apply(request);
if (response.getSuccess() != null && response.getSuccess()) {
circuitBreaker.recordSuccess();
return response;
} else {
// 业务错误
AiError error = createErrorFromResponse(response, request);
recordError(key, error);
if (attempt == retryConfig.getMaxAttempts() || !isRetryableError(error.getType())) {
circuitBreaker.recordFailure();
return response;
}
}
} catch (Exception e) {
AiError error = createErrorFromException(e, key, request);
recordError(key, error);
if (attempt == retryConfig.getMaxAttempts() || !isRetryableError(error.getType())) {
circuitBreaker.recordFailure();
return createErrorResponse(error, request);
}
// 等待重试
if (attempt < retryConfig.getMaxAttempts()) {
try {
long delay = calculateRetryDelay(retryConfig, attempt);
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
}
// 所有重试都失败
AiError error = new AiError()
.setType(ErrorType.INTERNAL_ERROR)
.setSeverity(ErrorSeverity.HIGH)
.setMessage("所有重试尝试都失败")
.setProvider(key)
.setUserId(request.getUserId())
.setOccurTime(LocalDateTime.now());
circuitBreaker.recordFailure();
return createErrorResponse(error, request);
}
/**
* 记录错误
*
* @param key 错误标识
* @param error 错误信息
*/
public void recordError(String key, AiError error) {
ErrorStatistics stats = errorStats.computeIfAbsent(key, ErrorStatistics::new);
stats.incrementError(error.getType());
log.error("AI错误记录 - Key: {}, Type: {}, Severity: {}, Message: {}",
key, error.getType(), error.getSeverity(), error.getMessage());
// 根据错误严重级别进行不同处理
switch (error.getSeverity()) {
case CRITICAL -> handleCriticalError(error);
case HIGH -> handleHighSeverityError(error);
case MEDIUM -> handleMediumSeverityError(error);
case LOW -> handleLowSeverityError(error);
}
}
/**
* 获取错误统计
*
* @param key 统计标识
* @return 错误统计
*/
public ErrorStatistics getErrorStatistics(String key) {
return errorStats.get(key);
}
/**
* 获取所有错误统计
*
* @return 错误统计映射
*/
public Map<String, ErrorStatistics> getAllErrorStatistics() {
return new ConcurrentHashMap<>(errorStats);
}
/**
* 获取熔断器状态
*
* @param key 熔断器标识
* @return 熔断器状态
*/
public CircuitBreakerState getCircuitBreakerState(String key) {
return circuitBreakers.get(key);
}
/**
* 重置熔断器
*
* @param key 熔断器标识
*/
public void resetCircuitBreaker(String key) {
CircuitBreakerState state = circuitBreakers.get(key);
if (state != null) {
state.status = CircuitBreakerStatus.CLOSED;
state.failureCount = 0;
state.successCount = 0;
log.info("熔断器已重置: {}", key);
}
}
/**
* 设置重试配置
*
* @param key 配置标识
* @param config 重试配置
*/
public void setRetryConfig(String key, RetryConfig config) {
retryConfigs.put(key, config);
}
/**
* 清理错误统计
*/
public void clearErrorStatistics() {
errorStats.clear();
log.info("错误统计已清理");
}
/**
* 获取或创建熔断器
*/
private CircuitBreakerState getOrCreateCircuitBreaker(String key) {
return circuitBreakers.computeIfAbsent(key, CircuitBreakerState::new);
}
/**
* 获取或创建重试配置
*/
private RetryConfig getOrCreateRetryConfig(String key) {
return retryConfigs.computeIfAbsent(key, k -> new RetryConfig());
}
/**
* 判断是否为可重试错误
*/
private boolean isRetryableError(ErrorType errorType) {
return switch (errorType) {
case NETWORK_ERROR, TIMEOUT_ERROR, SERVICE_UNAVAILABLE, RATE_LIMIT_ERROR -> true;
case AUTHENTICATION_ERROR, AUTHORIZATION_ERROR, VALIDATION_ERROR, QUOTA_EXCEEDED_ERROR -> false;
default -> true;
};
}
/**
* 计算重试延迟
*/
private long calculateRetryDelay(RetryConfig config, int attempt) {
if (!config.isExponentialBackoff()) {
return config.getInitialDelayMs();
}
long delay = (long) (config.getInitialDelayMs() * Math.pow(config.getBackoffMultiplier(), attempt - 1));
return Math.min(delay, config.getMaxDelayMs());
}
/**
* 从异常创建错误对象
*/
private AiError createErrorFromException(Exception e, String provider, AiChatRequest request) {
ErrorType type = classifyException(e);
ErrorSeverity severity = determineSeverity(type);
return new AiError()
.setId(java.util.UUID.randomUUID().toString())
.setType(type)
.setSeverity(severity)
.setMessage(e.getMessage())
.setDetails(getStackTrace(e))
.setProvider(provider)
.setModel(request.getModel())
.setUserId(request.getUserId())
.setOccurTime(LocalDateTime.now())
.setCause(e);
}
/**
* 从响应创建错误对象
*/
private AiError createErrorFromResponse(AiChatResponse response, AiChatRequest request) {
ErrorType type = classifyResponseError(response);
ErrorSeverity severity = determineSeverity(type);
return new AiError()
.setId(java.util.UUID.randomUUID().toString())
.setType(type)
.setSeverity(severity)
.setMessage(response.getError())
.setDetails(response.getErrorCode())
.setProvider(response.getProvider())
.setModel(response.getModel())
.setUserId(request.getUserId())
.setOccurTime(LocalDateTime.now());
}
/**
* 分类异常类型
*/
private ErrorType classifyException(Exception e) {
String message = e.getMessage();
if (message == null) {
return ErrorType.UNKNOWN_ERROR;
}
message = message.toLowerCase();
if (message.contains("timeout") || message.contains("timed out")) {
return ErrorType.TIMEOUT_ERROR;
} else if (message.contains("network") || message.contains("connection")) {
return ErrorType.NETWORK_ERROR;
} else if (message.contains("rate limit") || message.contains("too many requests")) {
return ErrorType.RATE_LIMIT_ERROR;
} else if (message.contains("quota") || message.contains("limit exceeded")) {
return ErrorType.QUOTA_EXCEEDED_ERROR;
} else if (message.contains("unauthorized") || message.contains("authentication")) {
return ErrorType.AUTHENTICATION_ERROR;
} else if (message.contains("forbidden") || message.contains("access denied")) {
return ErrorType.AUTHORIZATION_ERROR;
} else if (message.contains("validation") || message.contains("invalid")) {
return ErrorType.VALIDATION_ERROR;
} else if (message.contains("service unavailable") || message.contains("server error")) {
return ErrorType.SERVICE_UNAVAILABLE;
} else {
return ErrorType.INTERNAL_ERROR;
}
}
/**
* 分类响应错误类型
*/
private ErrorType classifyResponseError(AiChatResponse response) {
String errorCode = response.getErrorCode();
String errorMessage = response.getError();
if (errorCode != null) {
return switch (errorCode.toUpperCase()) {
case "TIMEOUT", "TIMEOUT_ERROR" -> ErrorType.TIMEOUT_ERROR;
case "RATE_LIMIT", "RATE_LIMIT_ERROR" -> ErrorType.RATE_LIMIT_ERROR;
case "QUOTA_EXCEEDED" -> ErrorType.QUOTA_EXCEEDED_ERROR;
case "AUTH_ERROR", "AUTHENTICATION_ERROR" -> ErrorType.AUTHENTICATION_ERROR;
case "AUTHORIZATION_ERROR" -> ErrorType.AUTHORIZATION_ERROR;
case "VALIDATION_ERROR" -> ErrorType.VALIDATION_ERROR;
case "SERVICE_UNAVAILABLE" -> ErrorType.SERVICE_UNAVAILABLE;
case "NETWORK_ERROR" -> ErrorType.NETWORK_ERROR;
default -> ErrorType.INTERNAL_ERROR;
};
}
if (errorMessage != null) {
return classifyException(new RuntimeException(errorMessage));
}
return ErrorType.UNKNOWN_ERROR;
}
/**
* 确定错误严重级别
*/
private ErrorSeverity determineSeverity(ErrorType errorType) {
return switch (errorType) {
case AUTHENTICATION_ERROR, AUTHORIZATION_ERROR -> ErrorSeverity.CRITICAL;
case SERVICE_UNAVAILABLE, QUOTA_EXCEEDED_ERROR -> ErrorSeverity.HIGH;
case NETWORK_ERROR, TIMEOUT_ERROR, RATE_LIMIT_ERROR -> ErrorSeverity.MEDIUM;
case VALIDATION_ERROR -> ErrorSeverity.LOW;
default -> ErrorSeverity.MEDIUM;
};
}
/**
* 创建错误响应
*/
private AiChatResponse createErrorResponse(AiError error, AiChatRequest request) {
return new AiChatResponse()
.setSuccess(false)
.setError(error.getMessage())
.setErrorCode(error.getType().name())
.setModel(request.getModel())
.setProvider(error.getProvider())
.setCreateTime(LocalDateTime.now());
}
/**
* 获取异常堆栈信息
*/
private String getStackTrace(Exception e) {
java.io.StringWriter sw = new java.io.StringWriter();
java.io.PrintWriter pw = new java.io.PrintWriter(sw);
e.printStackTrace(pw);
return sw.toString();
}
/**
* 处理严重错误
*/
private void handleCriticalError(AiError error) {
log.error("严重错误发生: {}", error.getMessage());
// 可以发送告警、通知等
}
/**
* 处理高级别错误
*/
private void handleHighSeverityError(AiError error) {
log.warn("高级别错误发生: {}", error.getMessage());
// 可以记录到特殊日志、发送通知等
}
/**
* 处理中级别错误
*/
private void handleMediumSeverityError(AiError error) {
log.warn("中级别错误发生: {}", error.getMessage());
}
/**
* 处理低级别错误
*/
private void handleLowSeverityError(AiError error) {
log.debug("低级别错误发生: {}", error.getMessage());
}
/**
* 销毁资源
*/
public void destroy() {
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
log.info("AI错误处理器已销毁");
}
}
@@ -0,0 +1,16 @@
package org.dromara.ai.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.ai.domain.AiChatRecord;
import org.dromara.ai.domain.vo.AiChatRecordVo;
/**
* AI聊天记录Mapper接口
*
* @author pengles
*/
@Mapper
public interface AiChatRecordMapper extends BaseMapperPlus<AiChatRecord, AiChatRecordVo> {
}
@@ -0,0 +1,16 @@
package org.dromara.ai.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.ai.domain.AiPaintingFavorite;
import org.dromara.ai.domain.vo.AiPaintingFavoriteVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
* AI绘画作品收藏Mapper接口
*
* @author pengles
*/
@Mapper
public interface AiPaintingFavoriteMapper extends BaseMapperPlus<AiPaintingFavorite, AiPaintingFavoriteVo> {
}
@@ -0,0 +1,16 @@
package org.dromara.ai.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.ai.domain.AiPaintingRecord;
import org.dromara.ai.domain.vo.AiPaintingRecordVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
* AI绘画历史记录Mapper接口
*
* @author pengles
*/
@Mapper
public interface AiPaintingRecordMapper extends BaseMapperPlus<AiPaintingRecord, AiPaintingRecordVo> {
}
@@ -0,0 +1,16 @@
package org.dromara.ai.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.ai.domain.AiPaintingTemplate;
import org.dromara.ai.domain.vo.AiPaintingTemplateVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
* AI绘画参数模板Mapper接口
*
* @author pengles
*/
@Mapper
public interface AiPaintingTemplateMapper extends BaseMapperPlus<AiPaintingTemplate, AiPaintingTemplateVo> {
}
@@ -0,0 +1,16 @@
package org.dromara.ai.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.ai.domain.AiProviderConfigHistory;
import org.dromara.ai.domain.vo.AiProviderConfigHistoryVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
* AI Provider配置历史Mapper接口
*
* @author pengles
*/
@Mapper
public interface AiProviderConfigHistoryMapper extends BaseMapperPlus<AiProviderConfigHistory, AiProviderConfigHistoryVo> {
}
@@ -0,0 +1,16 @@
package org.dromara.ai.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.ai.domain.AiProviderConfig;
import org.dromara.ai.domain.vo.AiProviderConfigVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
* AI Provider配置Mapper接口
*
* @author pengles
*/
@Mapper
public interface AiProviderConfigMapper extends BaseMapperPlus<AiProviderConfig, AiProviderConfigVo> {
}
@@ -0,0 +1,16 @@
package org.dromara.ai.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.ai.domain.AiRatingRecord;
import org.dromara.ai.domain.vo.AiRatingRecordVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
* AI评价记录Mapper接口
*
* @author pengles
*/
@Mapper
public interface AiRatingRecordMapper extends BaseMapperPlus<AiRatingRecord, AiRatingRecordVo> {
}
@@ -0,0 +1,16 @@
package org.dromara.ai.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.ai.domain.AiSensitiveWord;
import org.dromara.ai.domain.vo.AiSensitiveWordVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
* AI敏感词Mapper接口
*
* @author pengles
*/
@Mapper
public interface AiSensitiveWordMapper extends BaseMapperPlus<AiSensitiveWord, AiSensitiveWordVo> {
}
@@ -0,0 +1,16 @@
package org.dromara.ai.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.ai.domain.AiUsageStats;
import org.dromara.ai.domain.vo.AiUsageStatsVo;
/**
* AI使用统计Mapper接口
*
* @author pengles
*/
@Mapper
public interface AiUsageStatsMapper extends BaseMapperPlus<AiUsageStats, AiUsageStatsVo> {
}
@@ -0,0 +1,16 @@
package org.dromara.ai.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.ai.domain.AiUserQuota;
import org.dromara.ai.domain.vo.AiUserQuotaVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
* AI用户配额Mapper接口
*
* @author pengles
*/
@Mapper
public interface AiUserQuotaMapper extends BaseMapperPlus<AiUserQuota, AiUserQuotaVo> {
}
@@ -0,0 +1,96 @@
package org.dromara.ai.mcp;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.domain.dto.AiChatRequest;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* MCP集成服务
* 提供MCP工具与AI聊天请求的集成功能
*
* @author pengles
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class McpIntegrationService {
private final McpToolRegistry registry;
/**
* 增强消息,添加可用工具信息
*
* @param request 聊天请求
*/
public void enrichMessages(AiChatRequest request) {
if (!Boolean.TRUE.equals(request.getEnableTools())) {
return;
}
try {
List<String> names = request.getRequestedTools() != null
? request.getRequestedTools()
: new ArrayList<>(registry.all().keySet());
if (names.isEmpty()) {
log.debug("未找到可用的MCP工具");
return;
}
StringBuilder sb = new StringBuilder();
sb.append("可用工具: ").append(String.join(",", names));
AiChatRequest.AiMessage sys = new AiChatRequest.AiMessage()
.setRole("system")
.setContent(sb.toString());
List<AiChatRequest.AiMessage> msgs = new ArrayList<>();
msgs.add(sys);
if (request.getMessages() != null) {
msgs.addAll(request.getMessages());
}
request.setMessages(msgs);
log.debug("MCP工具消息增强完成 - 工具数量: {}", names.size());
} catch (Exception e) {
log.error("MCP工具消息增强失败", e);
// 增强失败不影响主流程,只记录日志
}
}
/**
* 调用MCP工具
*
* @param name 工具名称
* @param input 工具输入参数
* @return 工具执行结果
*/
public Map<String, Object> invokeTool(String name, Map<String, Object> input) {
try {
log.debug("调用MCP工具: {}, 输入参数: {}", name, input != null ? input.keySet() : "");
McpTool tool = registry.get(name);
if (tool == null) {
log.warn("MCP工具不存在: {}", name);
return Map.of("error", "tool_not_found", "toolName", name);
}
Map<String, Object> result = tool.invoke(input != null ? input : Map.of());
if (result != null && result.containsKey("error")) {
log.warn("MCP工具调用失败: {}, 错误: {}", name, result.get("error"));
} else {
log.debug("MCP工具调用成功: {}", name);
}
return result;
} catch (Exception e) {
log.error("MCP工具调用异常: {}", name, e);
return Map.of("error", "tool_invocation_error", "message", e.getMessage(), "toolName", name);
}
}
}
@@ -0,0 +1,19 @@
package org.dromara.ai.mcp;
import java.util.Map;
/**
* MCP工具接口
* 定义MCP工具的标准接口规范
*
* @author pengles
*/
public interface McpTool {
String name();
String description();
Map<String, Object> schema();
Map<String, Object> invoke(Map<String, Object> input);
}
@@ -0,0 +1,60 @@
package org.dromara.ai.mcp;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
/**
* MCP工具注册表
* 管理所有MCP工具的注册、版本控制和动态加载
*
* @author pengles
*/
@Component
public class McpToolRegistry {
private final Map<String, McpTool> tools = new ConcurrentHashMap<>();
private final Map<String, String> versions = new ConcurrentHashMap<>();
private final Map<String, Supplier<McpTool>> factories = new ConcurrentHashMap<>();
public void register(McpTool tool) {
tools.put(tool.name(), tool);
versions.put(tool.name(), "1.0.0");
}
public McpTool get(String name) {
return tools.get(name);
}
public Map<String, McpTool> all() {
return new HashMap<>(tools);
}
public void unregister(String name) {
tools.remove(name);
versions.remove(name);
factories.remove(name);
}
public void registerFactory(String name, String version, Supplier<McpTool> factory) {
factories.put(name, factory);
versions.put(name, version);
tools.put(name, factory.get());
}
public void reload(String name) {
Supplier<McpTool> f = factories.get(name);
if (f != null) {
tools.put(name, f.get());
}
}
public Map<String, Object> metadata(String name) {
McpTool t = tools.get(name);
String v = versions.get(name);
if (t == null) return Map.of();
return Map.of("name", t.name(), "version", v);
}
}
@@ -0,0 +1,34 @@
package org.dromara.ai.mcp.tools;
import org.dromara.ai.mcp.McpTool;
import java.util.HashMap;
import java.util.Map;
public class AlgorithmComputeTool implements McpTool {
@Override
public String name() {
return "algorithm_compute";
}
@Override
public String description() {
return "执行简单算法计算";
}
@Override
public Map<String, Object> schema() {
Map<String, Object> s = new HashMap<>();
s.put("type", "object");
s.put("properties", Map.of("n", Map.of("type", "integer")));
return s;
}
@Override
public Map<String, Object> invoke(Map<String, Object> input) {
int n = Integer.parseInt(String.valueOf(input.getOrDefault("n", 10)));
long sum = 0;
for (int i = 1; i <= n; i++) sum += i;
return Map.of("sum", sum);
}
}
@@ -0,0 +1,43 @@
package org.dromara.ai.mcp.tools;
import org.dromara.ai.mcp.McpTool;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.HashMap;
import java.util.Map;
public class ApiCallTool implements McpTool {
@Override
public String name() {
return "api_call";
}
@Override
public String description() {
return "调用HTTP API";
}
@Override
public Map<String, Object> schema() {
Map<String, Object> s = new HashMap<>();
s.put("type", "object");
s.put("properties", Map.of("url", Map.of("type", "string")));
return s;
}
@Override
public Map<String, Object> invoke(Map<String, Object> input) {
try {
String url = String.valueOf(input.get("url"));
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder(URI.create(url)).GET().build();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
return Map.of("status", resp.statusCode(), "body", resp.body());
} catch (Exception e) {
return Map.of("error", e.getMessage());
}
}
}
@@ -0,0 +1,33 @@
package org.dromara.ai.mcp.tools;
import org.dromara.ai.mcp.McpTool;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DataTransformTool implements McpTool {
@Override
public String name() {
return "data_transform";
}
@Override
public String description() {
return "数据格式转换";
}
@Override
public Map<String, Object> schema() {
Map<String, Object> s = new HashMap<>();
s.put("type", "object");
s.put("properties", Map.of("items", Map.of("type", "array")));
return s;
}
@Override
public Map<String, Object> invoke(Map<String, Object> input) {
Object items = input.get("items");
return Map.of("count", items instanceof List ? ((List<?>) items).size() : 0);
}
}
@@ -0,0 +1,39 @@
package org.dromara.ai.mcp.tools;
import org.apache.tika.Tika;
import org.dromara.ai.mcp.McpTool;
import java.util.HashMap;
import java.util.Map;
public class FileParseTool implements McpTool {
@Override
public String name() {
return "file_parse";
}
@Override
public String description() {
return "解析文件内容";
}
@Override
public Map<String, Object> schema() {
Map<String, Object> s = new HashMap<>();
s.put("type", "object");
s.put("properties", Map.of("path", Map.of("type", "string")));
return s;
}
@Override
public Map<String, Object> invoke(Map<String, Object> input) {
try {
String path = String.valueOf(input.get("path"));
Tika tika = new Tika();
String text = tika.parseToString(new java.io.File(path));
return Map.of("text", text);
} catch (Exception e) {
return Map.of("error", e.getMessage());
}
}
}
@@ -0,0 +1,43 @@
package org.dromara.ai.mcp.tools;
import org.dromara.ai.mcp.McpTool;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.HashMap;
import java.util.Map;
public class HttpFetchTool implements McpTool {
@Override
public String name() {
return "http_fetch";
}
@Override
public String description() {
return "HTTP抓取";
}
@Override
public Map<String, Object> schema() {
Map<String, Object> s = new HashMap<>();
s.put("type", "object");
s.put("properties", Map.of("url", Map.of("type", "string")));
return s;
}
@Override
public Map<String, Object> invoke(Map<String, Object> input) {
try {
String url = String.valueOf(input.get("url"));
HttpClient client = HttpClient.newHttpClient();
HttpRequest req = HttpRequest.newBuilder(URI.create(url)).GET().build();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
return Map.of("status", resp.statusCode(), "body", resp.body());
} catch (Exception e) {
return Map.of("error", e.getMessage());
}
}
}
@@ -0,0 +1,34 @@
package org.dromara.ai.mcp.tools;
import org.dromara.ai.mcp.McpTool;
import java.util.HashMap;
import java.util.Map;
public class ProjectScaffoldTool implements McpTool {
@Override
public String name() {
return "project_scaffold";
}
@Override
public String description() {
return "生成项目脚手架方案";
}
@Override
public Map<String, Object> schema() {
Map<String, Object> s = new HashMap<>();
s.put("type", "object");
s.put("properties", Map.of("type", Map.of("type", "string")));
return s;
}
@Override
public Map<String, Object> invoke(Map<String, Object> input) {
String type = String.valueOf(input.getOrDefault("type", "webapp"));
Map<String, Object> out = new HashMap<>();
out.put("plan", "生成 " + type + " 脚手架,包含依赖、目录结构与初始化命令");
return out;
}
}
@@ -0,0 +1,60 @@
package org.dromara.ai.model;
public class ApiResult<T> {
private boolean success;
private String message;
private String code;
private T data;
public static <T> ApiResult<T> ok(T data) {
ApiResult<T> r = new ApiResult<>();
r.success = true;
r.data = data;
return r;
}
public static <T> ApiResult<T> fail(String code, String message) {
ApiResult<T> r = new ApiResult<>();
r.success = false;
r.code = code;
r.message = message;
return r;
}
public boolean isSuccess() {
return success;
}
public String getMessage() {
return message;
}
public String getCode() {
return code;
}
public T getData() {
return data;
}
public ApiResult<T> setSuccess(boolean success) {
this.success = success;
return this;
}
public ApiResult<T> setMessage(String message) {
this.message = message;
return this;
}
public ApiResult<T> setCode(String code) {
this.code = code;
return this;
}
public ApiResult<T> setData(T data) {
this.data = data;
return this;
}
}
@@ -0,0 +1,433 @@
package org.dromara.ai.monitor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.ai.provider.AiProvider;
import org.dromara.ai.provider.AiProviderManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* AI服务监控服务
* 提供性能监控、使用统计和健康检查功能
*
* @author pengles
* @since 2024-01-01
*/
@Slf4j
@Service
public class AiMonitorService {
@Autowired
private AiProviderManager providerManager;
// 监控数据存储
private final Map<String, ProviderMetrics> providerMetrics = new ConcurrentHashMap<>();
private final Map<String, ModelMetrics> modelMetrics = new ConcurrentHashMap<>();
private final Map<String, UserMetrics> userMetrics = new ConcurrentHashMap<>();
private final SystemMetrics systemMetrics = new SystemMetrics("1.0.0", "production");
/**
* 记录请求开始
*
* @param providerName Provider名称
* @param modelName 模型名称
* @param userId 用户ID
* @return 请求ID
*/
public String recordRequestStart(String providerName, String modelName, String userId) {
String requestId = UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
// 记录Provider指标
ProviderMetrics pMetrics = providerMetrics.computeIfAbsent(providerName, k -> new ProviderMetrics(k, "AI_PROVIDER"));
pMetrics.incrementTotalRequests();
pMetrics.addActiveRequest(requestId, startTime);
// 记录模型指标
String modelKey = providerName + ":" + modelName;
ModelMetrics mMetrics = modelMetrics.computeIfAbsent(modelKey, k -> new ModelMetrics(modelName, modelName, providerName, "TEXT_GENERATION"));
mMetrics.incrementTotalRequests();
mMetrics.addActiveRequest(requestId, startTime);
// 记录用户指标
if (userId != null) {
UserMetrics uMetrics = userMetrics.computeIfAbsent(userId, k -> {
try {
return new UserMetrics(Long.parseLong(k), k, "REGULAR");
} catch (NumberFormatException e) {
// 如果userId不是数字,使用hashCode作为ID
return new UserMetrics((long) k.hashCode(), k, "REGULAR");
}
});
uMetrics.incrementTotalRequests();
uMetrics.addActiveRequest(requestId, startTime);
}
// 记录系统指标
systemMetrics.incrementTotalRequests();
systemMetrics.addActiveRequest(requestId, startTime);
log.debug("记录请求开始: requestId={}, provider={}, model={}, user={}",
requestId, providerName, modelName, userId);
return requestId;
}
/**
* 记录请求完成
*
* @param requestId 请求ID
* @param providerName Provider名称
* @param modelName 模型名称
* @param userId 用户ID
* @param success 是否成功
* @param inputTokens 输入token数
* @param outputTokens 输出token数
* @param errorCode 错误码
*/
public void recordRequestEnd(String requestId, String providerName, String modelName,
String userId, boolean success, int inputTokens, int outputTokens, String errorCode) {
long endTime = System.currentTimeMillis();
// 记录Provider指标
ProviderMetrics pMetrics = providerMetrics.get(providerName);
if (pMetrics != null) {
Long startTime = pMetrics.removeActiveRequest(requestId);
if (startTime != null) {
long responseTime = endTime - startTime;
pMetrics.addResponseTime(responseTime);
if (success) {
pMetrics.incrementSuccessRequests();
} else {
pMetrics.incrementFailedRequests();
pMetrics.incrementErrorCount(errorCode);
}
pMetrics.addTokenUsage(inputTokens, outputTokens);
}
}
// 记录模型指标
String modelKey = providerName + ":" + modelName;
ModelMetrics mMetrics = modelMetrics.get(modelKey);
if (mMetrics != null) {
Long startTime = mMetrics.removeActiveRequest(requestId);
if (startTime != null) {
long responseTime = endTime - startTime;
mMetrics.addResponseTime(responseTime);
if (success) {
mMetrics.incrementSuccessRequests();
} else {
mMetrics.incrementFailedRequests();
mMetrics.incrementErrorCount(errorCode);
}
mMetrics.addTokenUsage(inputTokens, outputTokens);
}
}
// 记录用户指标
if (userId != null) {
UserMetrics uMetrics = userMetrics.get(userId);
if (uMetrics != null) {
Long startTime = uMetrics.removeActiveRequest(requestId);
if (startTime != null) {
long responseTime = endTime - startTime;
uMetrics.addResponseTime(responseTime);
if (success) {
uMetrics.incrementSuccessRequests();
} else {
uMetrics.incrementFailedRequests();
}
uMetrics.addTokenUsage(inputTokens, outputTokens);
}
}
}
// 记录系统指标
Long startTime = systemMetrics.removeActiveRequest(requestId);
if (startTime != null) {
long responseTime = endTime - startTime;
systemMetrics.addResponseTime(responseTime);
if (success) {
systemMetrics.incrementSuccessRequests();
} else {
systemMetrics.incrementFailedRequests();
systemMetrics.incrementErrorCount(errorCode);
}
systemMetrics.addTokenUsage(inputTokens, outputTokens);
}
log.debug("记录请求完成: requestId={}, provider={}, model={}, user={}, success={}, responseTime={}ms",
requestId, providerName, modelName, userId, success, endTime - (startTime != null ? startTime : endTime));
}
/**
* 获取Provider统计信息
*
* @param providerName Provider名称
* @return 统计信息
*/
public Map<String, Object> getProviderStats(String providerName) {
ProviderMetrics metrics = providerMetrics.get(providerName);
if (metrics == null) {
return new HashMap<>();
}
Map<String, Object> stats = new HashMap<>();
stats.put("providerName", providerName);
stats.put("totalRequests", metrics.getTotalRequests());
stats.put("successRequests", metrics.getSuccessRequests());
stats.put("failedRequests", metrics.getFailedRequests());
stats.put("activeRequests", metrics.getActiveRequestCount());
stats.put("successRate", metrics.getSuccessRate());
stats.put("avgResponseTime", metrics.getAvgResponseTime());
stats.put("minResponseTime", metrics.getMinResponseTime());
stats.put("maxResponseTime", metrics.getMaxResponseTime());
stats.put("totalInputTokens", metrics.getTotalInputTokens());
stats.put("totalOutputTokens", metrics.getTotalOutputTokens());
stats.put("errorCounts", metrics.getErrorCounts());
stats.put("lastUpdateTime", metrics.getLastUpdateTime());
return stats;
}
/**
* 获取模型统计信息
*
* @param providerName Provider名称
* @param modelName 模型名称
* @return 统计信息
*/
public Map<String, Object> getModelStats(String providerName, String modelName) {
String modelKey = providerName + ":" + modelName;
ModelMetrics metrics = modelMetrics.get(modelKey);
if (metrics == null) {
return new HashMap<>();
}
Map<String, Object> stats = new HashMap<>();
stats.put("providerName", providerName);
stats.put("modelName", modelName);
stats.put("totalRequests", metrics.getTotalRequests());
stats.put("successRequests", metrics.getSuccessRequests());
stats.put("failedRequests", metrics.getFailedRequests());
stats.put("activeRequests", metrics.getActiveRequestCount());
stats.put("successRate", metrics.getSuccessRate());
stats.put("avgResponseTime", metrics.getAvgResponseTime());
stats.put("minResponseTime", metrics.getMinResponseTime());
stats.put("maxResponseTime", metrics.getMaxResponseTime());
stats.put("totalInputTokens", metrics.getTotalInputTokens());
stats.put("totalOutputTokens", metrics.getTotalOutputTokens());
stats.put("errorCounts", metrics.getErrorCounts());
stats.put("lastUpdateTime", metrics.getLastUpdateTime());
return stats;
}
/**
* 获取用户统计信息
*
* @param userId 用户ID
* @return 统计信息
*/
public Map<String, Object> getUserStats(String userId) {
UserMetrics metrics = userMetrics.get(userId);
if (metrics == null) {
return new HashMap<>();
}
Map<String, Object> stats = new HashMap<>();
stats.put("userId", userId);
stats.put("totalRequests", metrics.getTotalRequests());
stats.put("successRequests", metrics.getSuccessRequests());
stats.put("failedRequests", metrics.getFailedRequests());
stats.put("activeRequests", metrics.getActiveRequestCount());
stats.put("successRate", metrics.getSuccessRate());
stats.put("avgResponseTime", metrics.getAvgResponseTime());
stats.put("totalInputTokens", metrics.getTotalInputTokens());
stats.put("totalOutputTokens", metrics.getTotalOutputTokens());
stats.put("lastUpdateTime", metrics.getLastUpdateTime());
return stats;
}
/**
* 获取系统统计信息
*
* @return 统计信息
*/
public Map<String, Object> getSystemStats() {
Map<String, Object> stats = new HashMap<>();
stats.put("totalRequests", systemMetrics.getTotalRequests());
stats.put("successRequests", systemMetrics.getSuccessRequests());
stats.put("failedRequests", systemMetrics.getFailedRequests());
stats.put("activeRequests", systemMetrics.getActiveRequestCount());
stats.put("successRate", systemMetrics.getSuccessRate());
stats.put("avgResponseTime", systemMetrics.getAvgResponseTime());
stats.put("minResponseTime", systemMetrics.getMinResponseTime());
stats.put("maxResponseTime", systemMetrics.getMaxResponseTime());
stats.put("totalInputTokens", systemMetrics.getTotalInputTokens());
stats.put("totalOutputTokens", systemMetrics.getTotalOutputTokens());
stats.put("errorCounts", systemMetrics.getErrorCounts());
stats.put("providerCount", providerMetrics.size());
stats.put("modelCount", modelMetrics.size());
stats.put("userCount", userMetrics.size());
stats.put("lastUpdateTime", systemMetrics.getLastUpdateTime());
return stats;
}
/**
* 获取所有Provider统计信息
*
* @return Provider统计列表
*/
public List<Map<String, Object>> getAllProviderStats() {
return providerMetrics.keySet().stream()
.map(this::getProviderStats)
.collect(Collectors.toList());
}
/**
* 获取热门模型排行
*
* @param limit 限制数量
* @return 模型排行列表
*/
public List<Map<String, Object>> getPopularModels(int limit) {
return modelMetrics.entrySet().stream()
.sorted((e1, e2) -> Long.compare(e2.getValue().getTotalRequests(), e1.getValue().getTotalRequests()))
.limit(limit)
.map(entry -> {
String[] parts = entry.getKey().split(":", 2);
Map<String, Object> model = new HashMap<>();
model.put("providerName", parts[0]);
model.put("modelName", parts.length > 1 ? parts[1] : "");
model.put("totalRequests", entry.getValue().getTotalRequests());
model.put("successRate", entry.getValue().getSuccessRate());
model.put("avgResponseTime", entry.getValue().getAvgResponseTime());
return model;
})
.collect(Collectors.toList());
}
/**
* 获取活跃用户排行
*
* @param limit 限制数量
* @return 用户排行列表
*/
public List<Map<String, Object>> getActiveUsers(int limit) {
return userMetrics.entrySet().stream()
.sorted((e1, e2) -> Long.compare(e2.getValue().getTotalRequests(), e1.getValue().getTotalRequests()))
.limit(limit)
.map(entry -> {
Map<String, Object> user = new HashMap<>();
user.put("userId", entry.getKey());
user.put("totalRequests", entry.getValue().getTotalRequests());
user.put("successRate", entry.getValue().getSuccessRate());
user.put("totalTokens", entry.getValue().getTotalInputTokens() + entry.getValue().getTotalOutputTokens());
return user;
})
.collect(Collectors.toList());
}
/**
* 健康检查
*
* @return 健康状态
*/
public Map<String, Object> healthCheck() {
Map<String, Object> health = new HashMap<>();
// 检查Provider健康状态
List<Map<String, Object>> providerHealth = new ArrayList<>();
for (AiProvider provider : providerManager.getAllProviders()) {
Map<String, Object> providerStatus = new HashMap<>();
providerStatus.put("name", provider.getProviderName());
providerStatus.put("available", provider.isAvailable());
providerStatus.put("version", provider.getVersion());
ProviderMetrics metrics = providerMetrics.get(provider.getProviderName());
if (metrics != null) {
providerStatus.put("successRate", metrics.getSuccessRate());
providerStatus.put("avgResponseTime", metrics.getAvgResponseTime());
providerStatus.put("activeRequests", metrics.getActiveRequestCount());
}
providerHealth.add(providerStatus);
}
health.put("providers", providerHealth);
health.put("systemStats", getSystemStats());
health.put("timestamp", LocalDateTime.now());
// 计算整体健康分数
double healthScore = calculateHealthScore();
health.put("healthScore", healthScore);
health.put("status", healthScore >= 0.8 ? "HEALTHY" : healthScore >= 0.5 ? "WARNING" : "CRITICAL");
return health;
}
/**
* 计算健康分数
*
* @return 健康分数 (0.0-1.0)
*/
private double calculateHealthScore() {
if (systemMetrics.getTotalRequests() == 0) {
return 1.0; // 没有请求时认为是健康的
}
double successRate = systemMetrics.getSuccessRate();
double responseTimeScore = Math.max(0, 1.0 - systemMetrics.getAvgResponseTime() / 10000.0); // 10秒为基准
return (successRate * 0.7 + responseTimeScore * 0.3);
}
/**
* 重置统计数据
*/
public void resetStats() {
providerMetrics.clear();
modelMetrics.clear();
userMetrics.clear();
systemMetrics.reset();
log.info("AI监控统计数据已重置");
}
/**
* 定时清理过期数据
*/
@Scheduled(fixedRate = 3600000) // 每小时执行一次
public void cleanupExpiredData() {
long expireTime = System.currentTimeMillis() - 24 * 60 * 60 * 1000; // 24小时前
// 清理过期的活跃请求
providerMetrics.values().forEach(metrics -> metrics.cleanupExpiredRequests(expireTime));
modelMetrics.values().forEach(metrics -> metrics.cleanupExpiredRequests(expireTime));
userMetrics.values().forEach(metrics -> metrics.cleanupExpiredRequests(expireTime));
systemMetrics.cleanupExpiredRequests(expireTime);
log.debug("清理过期监控数据完成");
}
/**
* 定时输出监控报告
*/
@Scheduled(fixedRate = 300000) // 每5分钟执行一次
public void logMonitorReport() {
Map<String, Object> systemStats = getSystemStats();
log.info("AI服务监控报告: 总请求={}, 成功率={}, 平均响应时间={}ms, 活跃请求={}",
systemStats.get("totalRequests"),
systemStats.get("successRate"),
systemStats.get("avgResponseTime"),
systemStats.get("activeRequests"));
}
}

Some files were not shown because too many files have changed in this diff Show More