Files
1818web-hoduan/RUNNINGHUB_IMPLEMENTATION_TODO.md
2025-11-14 17:41:15 +08:00

296 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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. 返回ProviderTaskResponsestatus=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() {
// 初始化providerMapkey为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新功能
- ✅ 用户无感切换,根据模型自动选择服务商
- ✅ 统一的任务管理和状态追踪