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
@@ -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"));
}
}
@@ -0,0 +1,252 @@
package org.dromara.ai.monitor;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
/**
* 基础监控指标类
*
* @author pengles
* @since 2024-01-01
*/
public abstract class BaseMetrics {
protected final AtomicLong totalRequests = new AtomicLong(0);
protected final AtomicLong successRequests = new AtomicLong(0);
protected final AtomicLong failedRequests = new AtomicLong(0);
protected final LongAdder totalResponseTime = new LongAdder();
protected final AtomicLong minResponseTime = new AtomicLong(Long.MAX_VALUE);
protected final AtomicLong maxResponseTime = new AtomicLong(0);
protected final AtomicLong totalInputTokens = new AtomicLong(0);
protected final AtomicLong totalOutputTokens = new AtomicLong(0);
protected final Map<String, AtomicLong> errorCounts = new ConcurrentHashMap<>();
protected final Map<String, Long> activeRequests = new ConcurrentHashMap<>();
protected volatile LocalDateTime lastUpdateTime = LocalDateTime.now();
/**
* 增加总请求数
*/
public void incrementTotalRequests() {
totalRequests.incrementAndGet();
updateLastUpdateTime();
}
/**
* 增加成功请求数
*/
public void incrementSuccessRequests() {
successRequests.incrementAndGet();
updateLastUpdateTime();
}
/**
* 增加失败请求数
*/
public void incrementFailedRequests() {
failedRequests.incrementAndGet();
updateLastUpdateTime();
}
/**
* 添加响应时间
*
* @param responseTime 响应时间(毫秒)
*/
public void addResponseTime(long responseTime) {
totalResponseTime.add(responseTime);
// 更新最小响应时间
long currentMin = minResponseTime.get();
while (responseTime < currentMin && !minResponseTime.compareAndSet(currentMin, responseTime)) {
currentMin = minResponseTime.get();
}
// 更新最大响应时间
long currentMax = maxResponseTime.get();
while (responseTime > currentMax && !maxResponseTime.compareAndSet(currentMax, responseTime)) {
currentMax = maxResponseTime.get();
}
updateLastUpdateTime();
}
/**
* 添加Token使用量
*
* @param inputTokens 输入Token数
* @param outputTokens 输出Token数
*/
public void addTokenUsage(int inputTokens, int outputTokens) {
totalInputTokens.addAndGet(inputTokens);
totalOutputTokens.addAndGet(outputTokens);
updateLastUpdateTime();
}
/**
* 增加错误计数
*
* @param errorCode 错误码
*/
public void incrementErrorCount(String errorCode) {
if (errorCode != null && !errorCode.isEmpty()) {
errorCounts.computeIfAbsent(errorCode, k -> new AtomicLong(0)).incrementAndGet();
updateLastUpdateTime();
}
}
/**
* 添加活跃请求
*
* @param requestId 请求ID
* @param startTime 开始时间
*/
public void addActiveRequest(String requestId, long startTime) {
activeRequests.put(requestId, startTime);
updateLastUpdateTime();
}
/**
* 移除活跃请求
*
* @param requestId 请求ID
* @return 开始时间
*/
public Long removeActiveRequest(String requestId) {
Long startTime = activeRequests.remove(requestId);
if (startTime != null) {
updateLastUpdateTime();
}
return startTime;
}
/**
* 清理过期的活跃请求
*
* @param expireTime 过期时间
*/
public void cleanupExpiredRequests(long expireTime) {
activeRequests.entrySet().removeIf(entry -> entry.getValue() < expireTime);
}
/**
* 获取总请求数
*/
public long getTotalRequests() {
return totalRequests.get();
}
/**
* 获取成功请求数
*/
public long getSuccessRequests() {
return successRequests.get();
}
/**
* 获取失败请求数
*/
public long getFailedRequests() {
return failedRequests.get();
}
/**
* 获取活跃请求数
*/
public int getActiveRequestCount() {
return activeRequests.size();
}
/**
* 获取成功率
*/
public double getSuccessRate() {
long total = totalRequests.get();
if (total == 0) {
return 0.0;
}
return (double) successRequests.get() / total;
}
/**
* 获取平均响应时间
*/
public double getAvgResponseTime() {
long completedRequests = successRequests.get() + failedRequests.get();
if (completedRequests == 0) {
return 0.0;
}
return (double) totalResponseTime.sum() / completedRequests;
}
/**
* 获取最小响应时间
*/
public long getMinResponseTime() {
long min = minResponseTime.get();
return min == Long.MAX_VALUE ? 0 : min;
}
/**
* 获取最大响应时间
*/
public long getMaxResponseTime() {
return maxResponseTime.get();
}
/**
* 获取总输入Token数
*/
public long getTotalInputTokens() {
return totalInputTokens.get();
}
/**
* 获取总输出Token数
*/
public long getTotalOutputTokens() {
return totalOutputTokens.get();
}
/**
* 获取错误计数
*/
public Map<String, Long> getErrorCounts() {
Map<String, Long> result = new ConcurrentHashMap<>();
errorCounts.forEach((key, value) -> result.put(key, value.get()));
return result;
}
/**
* 获取最后更新时间
*/
public LocalDateTime getLastUpdateTime() {
return lastUpdateTime;
}
/**
* 重置指标
*/
public void reset() {
totalRequests.set(0);
successRequests.set(0);
failedRequests.set(0);
totalResponseTime.reset();
minResponseTime.set(Long.MAX_VALUE);
maxResponseTime.set(0);
totalInputTokens.set(0);
totalOutputTokens.set(0);
errorCounts.clear();
activeRequests.clear();
updateLastUpdateTime();
}
/**
* 更新最后更新时间
*/
private void updateLastUpdateTime() {
lastUpdateTime = LocalDateTime.now();
}
}
@@ -0,0 +1,323 @@
package org.dromara.ai.monitor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
/**
* AI Model监控指标
* 继承基础监控指标,添加Model特有的监控数据
*
* @author pengles
* @date 2024-01-01
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ModelMetrics extends BaseMetrics {
/**
* 模型ID
*/
private String modelId;
/**
* 模型名称
*/
private String modelName;
/**
* Provider名称
*/
private String providerName;
/**
* 模型类型
*/
private String modelType;
/**
* 总成本(分)
*/
private LongAdder totalCost = new LongAdder();
/**
* 输入成本(分)
*/
private LongAdder inputCost = new LongAdder();
/**
* 输出成本(分)
*/
private LongAdder outputCost = new LongAdder();
/**
* 平均质量评分
*/
private double avgQualityScore = 0.0;
/**
* 质量评分总和
*/
private LongAdder totalQualityScore = new LongAdder();
/**
* 质量评分次数
*/
private AtomicLong qualityScoreCount = new AtomicLong(0);
/**
* 上下文长度使用统计
*/
private LongAdder totalContextLength = new LongAdder();
/**
* 最大上下文长度
*/
private AtomicLong maxContextLength = new AtomicLong(0);
/**
* 流式请求数
*/
private AtomicLong streamRequests = new AtomicLong(0);
/**
* 函数调用请求数
*/
private AtomicLong functionCallRequests = new AtomicLong(0);
/**
* 工具调用请求数
*/
private AtomicLong toolCallRequests = new AtomicLong(0);
/**
* 视觉处理请求数
*/
private AtomicLong visionRequests = new AtomicLong(0);
/**
* 图像生成请求数
*/
private AtomicLong imageGenerationRequests = new AtomicLong(0);
/**
* 音频处理请求数
*/
private AtomicLong audioRequests = new AtomicLong(0);
/**
* 最后使用时间
*/
private LocalDateTime lastUsedTime;
/**
* 模型热度(使用频率)
*/
private double popularity = 0.0;
public ModelMetrics(String modelId, String modelName, String providerName, String modelType) {
this.modelId = modelId;
this.modelName = modelName;
this.providerName = providerName;
this.modelType = modelType;
this.lastUsedTime = LocalDateTime.now();
}
/**
* 记录成本
*
* @param inputTokens 输入Token数
* @param outputTokens 输出Token数
* @param inputPrice 输入价格(每千Token
* @param outputPrice 输出价格(每千Token
*/
public void recordCost(int inputTokens, int outputTokens, double inputPrice, double outputPrice) {
long inputCostValue = Math.round(inputTokens * inputPrice / 1000.0 * 100); // 转换为分
long outputCostValue = Math.round(outputTokens * outputPrice / 1000.0 * 100); // 转换为分
inputCost.add(inputCostValue);
outputCost.add(outputCostValue);
totalCost.add(inputCostValue + outputCostValue);
lastUpdateTime = LocalDateTime.now();
}
/**
* 记录质量评分
*
* @param score 质量评分 (0-100)
*/
public void recordQualityScore(double score) {
if (score >= 0 && score <= 100) {
totalQualityScore.add(Math.round(score * 100)); // 保留两位小数精度
qualityScoreCount.incrementAndGet();
updateAvgQualityScore();
lastUpdateTime = LocalDateTime.now();
}
}
/**
* 记录上下文长度
*
* @param contextLength 上下文长度
*/
public void recordContextLength(int contextLength) {
totalContextLength.add(contextLength);
// 更新最大上下文长度
long currentMax = maxContextLength.get();
while (contextLength > currentMax && !maxContextLength.compareAndSet(currentMax, contextLength)) {
currentMax = maxContextLength.get();
}
lastUpdateTime = LocalDateTime.now();
}
/**
* 记录流式请求
*/
public void recordStreamRequest() {
streamRequests.incrementAndGet();
recordUsage();
}
/**
* 记录函数调用请求
*/
public void recordFunctionCallRequest() {
functionCallRequests.incrementAndGet();
recordUsage();
}
/**
* 记录工具调用请求
*/
public void recordToolCallRequest() {
toolCallRequests.incrementAndGet();
recordUsage();
}
/**
* 记录视觉处理请求
*/
public void recordVisionRequest() {
visionRequests.incrementAndGet();
recordUsage();
}
/**
* 记录图像生成请求
*/
public void recordImageGenerationRequest() {
imageGenerationRequests.incrementAndGet();
recordUsage();
}
/**
* 记录音频处理请求
*/
public void recordAudioRequest() {
audioRequests.incrementAndGet();
recordUsage();
}
/**
* 记录模型使用
*/
public void recordUsage() {
incrementTotalRequests();
lastUsedTime = LocalDateTime.now();
updatePopularity();
}
/**
* 更新平均质量评分
*/
private void updateAvgQualityScore() {
long count = qualityScoreCount.get();
if (count > 0) {
avgQualityScore = totalQualityScore.doubleValue() / (count * 100.0);
}
}
/**
* 更新模型热度
*/
private void updatePopularity() {
// 基于最近使用时间和总请求数计算热度
long totalReqs = getTotalRequests();
if (totalReqs > 0) {
// 简单的热度计算:总请求数的对数值
popularity = Math.log10(totalReqs + 1) * 10;
}
}
/**
* 获取平均成本(分/千Token)
*/
public double getAvgCostPerKToken() {
long totalTokens = getTotalInputTokens() + getTotalOutputTokens();
if (totalTokens > 0) {
return totalCost.doubleValue() / (totalTokens / 1000.0);
}
return 0.0;
}
/**
* 获取平均上下文长度
*/
public double getAvgContextLength() {
long totalReqs = getTotalRequests();
if (totalReqs > 0) {
return totalContextLength.doubleValue() / totalReqs;
}
return 0.0;
}
/**
* 获取流式请求比例
*/
public double getStreamRequestRatio() {
long totalReqs = getTotalRequests();
if (totalReqs > 0) {
return (double) streamRequests.get() / totalReqs * 100;
}
return 0.0;
}
/**
* 获取功能使用统计
*/
public String getFeatureUsageStats() {
return String.format("Stream: %d, Function: %d, Tool: %d, Vision: %d, Image: %d, Audio: %d",
streamRequests.get(), functionCallRequests.get(), toolCallRequests.get(),
visionRequests.get(), imageGenerationRequests.get(), audioRequests.get());
}
/**
* 重置指标
*/
@Override
public void reset() {
super.reset();
totalCost.reset();
inputCost.reset();
outputCost.reset();
totalQualityScore.reset();
qualityScoreCount.set(0);
totalContextLength.reset();
maxContextLength.set(0);
streamRequests.set(0);
functionCallRequests.set(0);
toolCallRequests.set(0);
visionRequests.set(0);
imageGenerationRequests.set(0);
audioRequests.set(0);
avgQualityScore = 0.0;
popularity = 0.0;
lastUsedTime = LocalDateTime.now();
}
}
@@ -0,0 +1,211 @@
package org.dromara.ai.monitor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
import java.util.concurrent.atomic.AtomicLong;
/**
* AI Provider监控指标
* 继承基础监控指标,添加Provider特有的监控数据
*
* @author pengles
* @date 2024-01-01
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ProviderMetrics extends BaseMetrics {
/**
* Provider名称
*/
private String providerName;
/**
* Provider类型
*/
private String providerType;
/**
* 连接超时次数
*/
private AtomicLong timeoutCount = new AtomicLong(0);
/**
* 限流次数
*/
private AtomicLong rateLimitCount = new AtomicLong(0);
/**
* 配额超限次数
*/
private AtomicLong quotaExceededCount = new AtomicLong(0);
/**
* 服务不可用次数
*/
private AtomicLong serviceUnavailableCount = new AtomicLong(0);
/**
* 最后成功请求时间
*/
private LocalDateTime lastSuccessTime;
/**
* 最后失败请求时间
*/
private LocalDateTime lastFailureTime;
/**
* 连续失败次数
*/
private AtomicLong consecutiveFailures = new AtomicLong(0);
/**
* Provider可用性状态
*/
private boolean available = true;
/**
* 健康检查最后执行时间
*/
private LocalDateTime lastHealthCheckTime;
/**
* 健康检查状态
*/
private boolean healthCheckPassed = true;
public ProviderMetrics(String providerName, String providerType) {
this.providerName = providerName;
this.providerType = providerType;
this.lastSuccessTime = LocalDateTime.now();
this.lastHealthCheckTime = LocalDateTime.now();
}
/**
* 增加超时次数
*/
public void incrementTimeout() {
timeoutCount.incrementAndGet();
incrementFailedRequests();
updateFailureInfo();
}
/**
* 增加限流次数
*/
public void incrementRateLimit() {
rateLimitCount.incrementAndGet();
incrementFailedRequests();
updateFailureInfo();
}
/**
* 增加配额超限次数
*/
public void incrementQuotaExceeded() {
quotaExceededCount.incrementAndGet();
incrementFailedRequests();
updateFailureInfo();
}
/**
* 增加服务不可用次数
*/
public void incrementServiceUnavailable() {
serviceUnavailableCount.incrementAndGet();
incrementFailedRequests();
updateFailureInfo();
}
/**
* 记录成功请求
*/
public void recordSuccess() {
incrementSuccessRequests();
lastSuccessTime = LocalDateTime.now();
consecutiveFailures.set(0);
updateAvailability();
}
/**
* 记录失败请求
*/
public void recordFailure() {
incrementFailedRequests();
updateFailureInfo();
}
/**
* 更新失败信息
*/
private void updateFailureInfo() {
lastFailureTime = LocalDateTime.now();
consecutiveFailures.incrementAndGet();
updateAvailability();
}
/**
* 更新可用性状态
*/
private void updateAvailability() {
// 连续失败超过10次则标记为不可用
if (consecutiveFailures.get() >= 10) {
available = false;
} else if (consecutiveFailures.get() == 0) {
available = true;
}
}
/**
* 更新健康检查状态
*/
public void updateHealthCheck(boolean passed) {
this.healthCheckPassed = passed;
this.lastHealthCheckTime = LocalDateTime.now();
if (!passed) {
available = false;
}
}
/**
* 获取Provider健康分数 (0-100)
*/
public double getHealthScore() {
if (!available || !healthCheckPassed) {
return 0.0;
}
double successRate = getSuccessRate();
double timeoutRate = getTotalRequests() > 0 ?
(double) timeoutCount.get() / getTotalRequests() * 100 : 0;
double rateLimitRate = getTotalRequests() > 0 ?
(double) rateLimitCount.get() / getTotalRequests() * 100 : 0;
// 综合计算健康分数
double score = successRate;
score -= timeoutRate * 0.5; // 超时影响权重0.5
score -= rateLimitRate * 0.3; // 限流影响权重0.3
return Math.max(0, Math.min(100, score));
}
/**
* 重置指标
*/
@Override
public void reset() {
super.reset();
timeoutCount.set(0);
rateLimitCount.set(0);
quotaExceededCount.set(0);
serviceUnavailableCount.set(0);
consecutiveFailures.set(0);
available = true;
healthCheckPassed = true;
lastSuccessTime = LocalDateTime.now();
lastHealthCheckTime = LocalDateTime.now();
}
}

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