# 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（新功能）
- ✅ 用户无感切换，根据模型自动选择服务商
- ✅ 统一的任务管理和状态追踪

