first commit
This commit is contained in:
@@ -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模块已启用");
|
||||
}
|
||||
}
|
||||
+532
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+89
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+129
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+112
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+138
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+101
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+170
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+128
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+129
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+139
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+87
@@ -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;
|
||||
}
|
||||
}
|
||||
+75
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+284
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+207
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+82
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+91
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+106
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
+60
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+41
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+66
@@ -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 请求体,包含botId(Bot 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;
|
||||
}
|
||||
}
|
||||
+72
@@ -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);
|
||||
}
|
||||
}
|
||||
+95
@@ -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工具控制器
|
||||
* 提供MCP(Model 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+168
@@ -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);
|
||||
}
|
||||
}
|
||||
+133
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+113
@@ -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;
|
||||
|
||||
}
|
||||
+66
@@ -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;
|
||||
|
||||
}
|
||||
+60
@@ -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;
|
||||
|
||||
}
|
||||
+78
@@ -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;
|
||||
|
||||
}
|
||||
+123
@@ -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;
|
||||
|
||||
}
|
||||
+75
@@ -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;
|
||||
|
||||
}
|
||||
+60
@@ -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;
|
||||
|
||||
}
|
||||
+110
@@ -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;
|
||||
|
||||
}
|
||||
+95
@@ -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;
|
||||
|
||||
}
|
||||
+70
@@ -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;
|
||||
|
||||
}
|
||||
+135
@@ -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> {
|
||||
|
||||
}
|
||||
+16
@@ -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> {
|
||||
|
||||
}
|
||||
+16
@@ -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> {
|
||||
|
||||
}
|
||||
+16
@@ -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> {
|
||||
|
||||
}
|
||||
+16
@@ -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> {
|
||||
|
||||
}
|
||||
+16
@@ -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);
|
||||
}
|
||||
}
|
||||
+34
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -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
Reference in New Issue
Block a user