296 lines
9.3 KiB
Markdown
296 lines
9.3 KiB
Markdown
|
|
# RunningHub集成实现清单
|
|||
|
|
|
|||
|
|
## ✅ 已完成
|
|||
|
|
|
|||
|
|
1. ✅ 创建Provider接口和DTO
|
|||
|
|
2. ✅ 数据库表扩展(V5迁移脚本)
|
|||
|
|
3. ✅ 配置文件扩展
|
|||
|
|
4. ✅ 实体类更新
|
|||
|
|
|
|||
|
|
## 🔨 待实现(按优先级)
|
|||
|
|
|
|||
|
|
### 1. 实现OpenAIProvider适配器
|
|||
|
|
**文件:** `src/main/java/com/dora/service/provider/impl/OpenAIProviderImpl.java`
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Service
|
|||
|
|
@Slf4j
|
|||
|
|
@RequiredArgsConstructor
|
|||
|
|
public class OpenAIProviderImpl implements AIProvider {
|
|||
|
|
|
|||
|
|
private final ThirdPartyApiService thirdPartyApiService;
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public ProviderTaskResponse submitTask(ProviderTaskRequest request) {
|
|||
|
|
// 调用现有的 thirdPartyApiService
|
|||
|
|
// 同步返回结果
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public String getProviderName() {
|
|||
|
|
return "openai";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public boolean isAsyncProvider() {
|
|||
|
|
return false; // OpenAI是同步API
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 实现RunningHubProvider适配器
|
|||
|
|
**文件:** `src/main/java/com/dora/service/provider/impl/RunningHubProviderImpl.java`
|
|||
|
|
|
|||
|
|
**关键逻辑:**
|
|||
|
|
```java
|
|||
|
|
@Override
|
|||
|
|
public ProviderTaskResponse submitTask(ProviderTaskRequest request) {
|
|||
|
|
// 1. 从providerConfig中获取webappId
|
|||
|
|
// 2. 构建nodeInfoList
|
|||
|
|
// - prompt节点
|
|||
|
|
// - model节点(portrait/landscape等)
|
|||
|
|
// - duration_seconds节点
|
|||
|
|
// 3. POST到 /task/openapi/ai-app/run
|
|||
|
|
// 4. 解析响应获取taskId
|
|||
|
|
// 5. 返回ProviderTaskResponse,status=PROCESSING
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public ProviderTaskStatus queryTaskStatus(String providerTaskId) {
|
|||
|
|
// POST到 /task/openapi/status
|
|||
|
|
// 解析响应:QUEUED/RUNNING/FAILED/SUCCESS
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public ProviderTaskResult getTaskResult(String providerTaskId) {
|
|||
|
|
// POST到 /task/openapi/outputs
|
|||
|
|
// 解析data数组,获取fileUrl
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**RunningHub请求DTO:**
|
|||
|
|
```java
|
|||
|
|
@Data
|
|||
|
|
class RunningHubSubmitRequest {
|
|||
|
|
private String webappId;
|
|||
|
|
private String apiKey;
|
|||
|
|
private List<RunningHubNodeInfo> nodeInfoList;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Data
|
|||
|
|
class RunningHubNodeInfo {
|
|||
|
|
private String nodeId;
|
|||
|
|
private String fieldName;
|
|||
|
|
private String fieldValue;
|
|||
|
|
private String fieldData; // 可选
|
|||
|
|
private String description;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 创建AIProviderService路由服务
|
|||
|
|
**文件:** `src/main/java/com/dora/service/AIProviderService.java`
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Service
|
|||
|
|
@Slf4j
|
|||
|
|
@RequiredArgsConstructor
|
|||
|
|
public class AIProviderService {
|
|||
|
|
|
|||
|
|
private final Map<String, AIProvider> providerMap;
|
|||
|
|
private final PointsConfigMapper pointsConfigMapper;
|
|||
|
|
|
|||
|
|
@PostConstruct
|
|||
|
|
public void init() {
|
|||
|
|
// 初始化providerMap,key为providerType
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public AIProvider getProvider(String modelName) {
|
|||
|
|
// 1. 从points_config表查询模型配置
|
|||
|
|
// 2. 获取providerType
|
|||
|
|
// 3. 从providerMap中获取对应的Provider
|
|||
|
|
PointsConfig config = pointsConfigMapper.findByModelName(modelName);
|
|||
|
|
String providerType = config.getProviderType();
|
|||
|
|
return providerMap.get(providerType);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. 添加RunningHub轮询定时器
|
|||
|
|
**文件:** `src/main/java/com/dora/scheduler/RunningHubPollingScheduler.java`
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Component
|
|||
|
|
@Slf4j
|
|||
|
|
@RequiredArgsConstructor
|
|||
|
|
public class RunningHubPollingScheduler {
|
|||
|
|
|
|||
|
|
private final AiTaskMapper aiTaskMapper;
|
|||
|
|
private final AIProviderService providerService;
|
|||
|
|
private final AiTaskService aiTaskService;
|
|||
|
|
|
|||
|
|
@Scheduled(fixedDelay = 5000) // 每5秒执行一次
|
|||
|
|
public void pollRunningHubTasks() {
|
|||
|
|
// 1. 查询 status='processing' 且 provider_type='runninghub' 的任务
|
|||
|
|
List<AiTask> tasks = aiTaskMapper.findProcessingTasksByProvider("runninghub");
|
|||
|
|
|
|||
|
|
for (AiTask task : tasks) {
|
|||
|
|
try {
|
|||
|
|
AIProvider provider = providerService.getProvider(task.getModelName());
|
|||
|
|
|
|||
|
|
// 2. 查询任务状态
|
|||
|
|
ProviderTaskStatus status = provider.queryTaskStatus(task.getProviderTaskId());
|
|||
|
|
|
|||
|
|
// 3. 根据状态更新
|
|||
|
|
if (status.getStatus() == Status.SUCCESS) {
|
|||
|
|
// 获取结果
|
|||
|
|
ProviderTaskResult result = provider.getTaskResult(task.getProviderTaskId());
|
|||
|
|
// 更新任务为completed
|
|||
|
|
aiTaskService.markTaskCompleted(task.getTaskNo(), result.getFiles().get(0).getFileUrl());
|
|||
|
|
} else if (status.getStatus() == Status.FAILED) {
|
|||
|
|
// 标记为失败
|
|||
|
|
aiTaskService.markTaskFailed(task.getTaskNo(), status.getErrorMessage());
|
|||
|
|
}
|
|||
|
|
} catch (Exception e) {
|
|||
|
|
log.error("轮询RunningHub任务失败: {}", task.getTaskNo(), e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5. 更新AiTaskMapper
|
|||
|
|
**文件:** `src/main/resources/mapper/AiTaskMapper.xml`
|
|||
|
|
|
|||
|
|
```xml
|
|||
|
|
<!-- 新增:查询指定provider的processing任务 -->
|
|||
|
|
<select id="findProcessingTasksByProvider" parameterType="string" resultType="com.dora.entity.AiTask">
|
|||
|
|
SELECT * FROM ai_task
|
|||
|
|
WHERE status = 'processing'
|
|||
|
|
AND provider_type = #{providerType}
|
|||
|
|
AND is_deleted = 0
|
|||
|
|
ORDER BY update_time ASC
|
|||
|
|
LIMIT 100
|
|||
|
|
</select>
|
|||
|
|
|
|||
|
|
<!-- 更新insert语句,添加provider字段 -->
|
|||
|
|
<insert id="insert" parameterType="com.dora.entity.AiTask" useGeneratedKeys="true" keyProperty="id">
|
|||
|
|
INSERT INTO ai_task (
|
|||
|
|
task_no, user_id, model_name, task_type, provider_type, provider_task_id, provider_response,
|
|||
|
|
prompt, image_url, image_base64, aspect_ratio,
|
|||
|
|
status, progress, progress_message, points_frozen, points_consumed, result_url,
|
|||
|
|
error_message, queue_time, start_time, complete_time, expire_time
|
|||
|
|
)
|
|||
|
|
VALUES (
|
|||
|
|
#{taskNo}, #{userId}, #{modelName}, #{taskType}, #{providerType}, #{providerTaskId}, #{providerResponse},
|
|||
|
|
#{prompt}, #{imageUrl}, #{imageBase64}, #{aspectRatio},
|
|||
|
|
#{status}, #{progress}, #{progressMessage}, #{pointsFrozen}, #{pointsConsumed}, #{resultUrl},
|
|||
|
|
#{errorMessage}, #{queueTime}, #{startTime}, #{completeTime}, #{expireTime}
|
|||
|
|
)
|
|||
|
|
</insert>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6. 更新AiTaskServiceImpl
|
|||
|
|
**文件:** `src/main/java/com/dora/service/impl/AiTaskServiceImpl.java`
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
@Override
|
|||
|
|
@Transactional(rollbackFor = Exception.class)
|
|||
|
|
public AiTask createTask(CreateTaskDto createTaskDto) {
|
|||
|
|
// 1. 验证模型并获取价格
|
|||
|
|
PointsConfig pointsConfig = pointsConfigMapper.findByModelName(createTaskDto.getModelName());
|
|||
|
|
|
|||
|
|
// 2. 扣除积分
|
|||
|
|
// ...
|
|||
|
|
|
|||
|
|
// 3. 创建任务
|
|||
|
|
AiTask task = new AiTask();
|
|||
|
|
// ... 设置基本字段
|
|||
|
|
task.setProviderType(pointsConfig.getProviderType()); // 设置provider类型
|
|||
|
|
aiTaskMapper.insert(task);
|
|||
|
|
|
|||
|
|
// 4. 判断provider类型
|
|||
|
|
if ("runninghub".equals(pointsConfig.getProviderType())) {
|
|||
|
|
// RunningHub:直接提交到服务商
|
|||
|
|
submitToRunningHub(task, pointsConfig);
|
|||
|
|
} else {
|
|||
|
|
// OpenAI:加入队列,由原有流程处理
|
|||
|
|
queueService.enqueue(task.getModelName(), task.getTaskNo());
|
|||
|
|
updateTaskStatus(task.getTaskNo(), "queued", "任务已进入等待队列");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return task;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void submitToRunningHub(AiTask task, PointsConfig config) {
|
|||
|
|
try {
|
|||
|
|
AIProvider provider = aiProviderService.getProvider(task.getModelName());
|
|||
|
|
|
|||
|
|
// 构建请求
|
|||
|
|
ProviderTaskRequest request = ProviderTaskRequest.builder()
|
|||
|
|
.modelName(task.getModelName())
|
|||
|
|
.prompt(task.getPrompt())
|
|||
|
|
.imageUrl(task.getImageUrl())
|
|||
|
|
.imageBase64(task.getImageBase64())
|
|||
|
|
.providerConfig(parseProviderConfig(config.getProviderConfig()))
|
|||
|
|
.build();
|
|||
|
|
|
|||
|
|
// 提交任务
|
|||
|
|
ProviderTaskResponse response = provider.submitTask(request);
|
|||
|
|
|
|||
|
|
// 更新任务
|
|||
|
|
task.setProviderTaskId(response.getProviderTaskId());
|
|||
|
|
task.setProviderResponse(JSON.toJSONString(response.getRawResponse()));
|
|||
|
|
task.setStatus("processing");
|
|||
|
|
task.setStartTime(LocalDateTime.now());
|
|||
|
|
aiTaskMapper.update(task);
|
|||
|
|
|
|||
|
|
} catch (Exception e) {
|
|||
|
|
log.error("提交RunningHub任务失败: {}", task.getTaskNo(), e);
|
|||
|
|
markTaskFailed(task.getTaskNo(), "提交失败: " + e.getMessage());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 🧪 测试步骤
|
|||
|
|
|
|||
|
|
### 1. 数据库迁移
|
|||
|
|
```bash
|
|||
|
|
mysql -u root -p your_database < V5__add_provider_support.sql
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 配置RunningHub API Key
|
|||
|
|
修改`application.yml`中的`ai.providers.runninghub.api-key`
|
|||
|
|
|
|||
|
|
### 3. 测试RunningHub模型
|
|||
|
|
```bash
|
|||
|
|
curl -X POST "http://localhost:8081/user/ai/tasks/submit" \
|
|||
|
|
-H "Authorization: Bearer YOUR_JWT_OR_API_KEY" \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{
|
|||
|
|
"modelName": "rh_sora2_portrait",
|
|||
|
|
"prompt": "测试视频生成"
|
|||
|
|
}'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. 查看轮询日志
|
|||
|
|
```bash
|
|||
|
|
tail -f logs/application.log | grep "RunningHub"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 📝 注意事项
|
|||
|
|
|
|||
|
|
1. **配置管理**:RunningHub的webappId需要从数据库的provider_config中读取
|
|||
|
|
2. **错误处理**:RunningHub API可能返回各种错误,需要完善异常处理
|
|||
|
|
3. **超时控制**:设置最大轮询次数,防止任务无限轮询
|
|||
|
|
4. **并发限制**:轮询时要控制并发数,避免给RunningHub造成压力
|
|||
|
|
5. **日志记录**:详细记录每次API调用的请求和响应,便于排查问题
|
|||
|
|
|
|||
|
|
## 🎯 预期效果
|
|||
|
|
|
|||
|
|
完成后,系统将支持:
|
|||
|
|
- ✅ OpenAI格式的同步API(原有功能,无影响)
|
|||
|
|
- ✅ RunningHub的异步API(新功能)
|
|||
|
|
- ✅ 用户无感切换,根据模型自动选择服务商
|
|||
|
|
- ✅ 统一的任务管理和状态追踪
|
|||
|
|
|