增加豆包视频渠道 - #1951
Conversation
WalkthroughAdds Doubao Video channel (type 54) with base URL and UI entries, introduces a Doubao task adaptor for video generation, extends TaskInfo with token fields, updates relay adaptor imports, marks Doubao video test as unsupported, and implements post-success token-based billing/refund in video task handling. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant C as Client
participant API as Server (Task API)
participant DA as DoubaoAdaptor
participant DB as Doubao API
rect rgb(245,248,255)
C->>API: Submit video task
API->>DA: Build request (URL, headers, body)
DA->>DB: POST /ark/v1/tasks (generate)
DB-->>DA: { task_id, status }
DA-->>API: task_id, raw response
API-->>C: 202 Accepted with task_id
end
loop Poll status
API->>DA: FetchTask(task_id)
DA->>DB: GET /ark/v1/tasks/{task_id}
DB-->>DA: { status, progress, url?, tokens? }
DA-->>API: ParseTaskResult(TaskInfo)
end
alt Success
API->>API: UpdateVideoSingleTask<br/>- compute actualQuota via model/group ratios<br/>- charge/refund delta
API-->>C: Final result (url, status)
else Failure
API-->>C: Error status/reason
end
note over API: TaskInfo now carries completion_tokens / total_tokens for billing
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
constant/channel.go(2 hunks)controller/channel-test.go(1 hunks)controller/task_video.go(2 hunks)relay/channel/task/doubao/adaptor.go(1 hunks)relay/channel/task/doubao/constants.go(1 hunks)relay/common/relay_info.go(1 hunks)relay/relay_adaptor.go(3 hunks)web/src/constants/channel.constants.js(1 hunks)web/src/helpers/render.jsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
controller/channel-test.go (1)
constant/channel.go (1)
ChannelTypeDoubaoVideo(54-54)
relay/relay_adaptor.go (3)
constant/channel.go (1)
ChannelTypeDoubaoVideo(54-54)relay/channel/task/doubao/adaptor.go (1)
TaskAdaptor(67-71)relay/channel/adapter.go (1)
TaskAdaptor(32-51)
relay/channel/task/doubao/adaptor.go (7)
relay/common/relay_info.go (3)
RelayInfo(74-121)TaskSubmitReq(483-492)TaskInfo(502-511)relay/common/relay_utils.go (2)
ValidateBasicTaskRequest(67-96)HasImage(18-20)relay/channel/api_request.go (1)
DoTaskApiRequest(280-302)service/error.go (1)
TaskErrorWrapper(139-155)service/http_client.go (1)
GetHttpClient(32-34)relay/channel/task/doubao/constants.go (2)
ModelList(3-7)ChannelName(9-9)model/task.go (4)
TaskStatusQueued(16-16)TaskStatusInProgress(17-17)TaskStatusSuccess(19-19)TaskStatusFailure(18-18)
controller/task_video.go (6)
setting/ratio_setting/model_ratio.go (1)
GetModelRatio(430-441)model/user.go (4)
GetUserById(287-299)DecreaseUserQuota(777-792)UpdateUserUsedQuotaAndRequestCount(823-830)IncreaseUserQuota(752-767)setting/ratio_setting/group_ratio.go (2)
GetGroupRatio(64-74)GetGroupGroupRatio(76-89)logger/logger.go (3)
LogInfo(55-57)LogQuota(94-100)LogError(63-65)model/channel.go (1)
UpdateChannelUsedQuota(740-746)model/log.go (2)
RecordLog(78-94)LogTypeSystem(46-46)
🔇 Additional comments (7)
relay/common/relay_info.go (1)
502-511: LGTM! Token fields added for task billing.The two new fields
CompletionTokensandTotalTokensappropriately support token-based billing for video generation tasks. TheomitemptyJSON tags ensure backward compatibility, and the Chinese comments clearly indicate their purpose for multiplier-based billing.constant/channel.go (2)
54-54: LGTM! New channel type constant added.The new
ChannelTypeDoubaoVideo = 54constant follows the established pattern and is properly sequenced.
114-114: Confirm DoubaoVideo base URL: constant/channel.go:114 duplicates the VolcEngine entry at index 45. Ensure Doubao Video is intended to use the same base endpoint or update to its correct URL.web/src/helpers/render.jsx (1)
340-341: LGTM! Icon mapping added for Doubao Video channel.The new case for channel type 54 correctly maps to
Doubao.Coloricon, consistent with the existing Doubao/VolcEngine iconography. The bilingual comment and placement are appropriate.controller/channel-test.go (1)
73-78: LGTM! Test exemption added for Doubao Video channel.The early return for
ChannelTypeDoubaoVideofollows the established pattern for async task-based channels (Midjourney, Kling, Jimeng, etc.). The error message is clear and the placement is logical.relay/relay_adaptor.go (2)
4-4: LGTM! Imports updated for new Doubao task adaptor.The new imports for
submodelandtaskdoubaoare properly added to support the new channel integration. The gin import consolidation improves code organization.Also applies to: 28-29
138-139: LGTM! Task adaptor routing added for Doubao Video.The new case for
ChannelTypeDoubaoVideocorrectly returns ataskdoubao.TaskAdaptorinstance, following the established pattern for other task-based channels.
| model.UpdateUserUsedQuotaAndRequestCount(task.UserId, quotaDelta) | ||
| model.UpdateChannelUsedQuota(task.ChannelId, quotaDelta) |
There was a problem hiding this comment.
Stop incrementing request_count during post-settlement charges
UpdateUserUsedQuotaAndRequestCount always bumps the user’s request_count by 1. Calling it here makes every post-settlement adjustment look like an extra request, which corrupts usage statistics. Please switch to a helper that only adjusts the used quota (or add one) so this reconciliation doesn’t double-count requests.
🤖 Prompt for AI Agents
In controller/task_video.go around lines 168-169, the code calls
model.UpdateUserUsedQuotaAndRequestCount which increments the user request_count
and thereby double-counts post-settlement adjustments; replace this call with a
helper that only adjusts the user's used quota (e.g.,
model.UpdateUserUsedQuota(userId, quotaDelta)) or add such a function in the
model layer if it doesn't exist, then call that instead while keeping
model.UpdateChannelUsedQuota(task.ChannelId, quotaDelta) as-is so reconciliation
only updates quota and not request_count.
| if err := model.IncreaseUserQuota(task.UserId, refundQuota, false); err != nil { | ||
| logger.LogError(ctx, fmt.Sprintf("退还预扣费失败: %s", err.Error())) | ||
| } else { | ||
| task.Quota = actualQuota // 更新任务记录的实际扣费额度 | ||
|
|
||
| // 记录退款日志 | ||
| logContent := fmt.Sprintf("视频任务成功退还多扣费用,模型倍率 %.2f,分组倍率 %.2f,tokens %d,预扣费 %s,实际扣费 %s,退还 %s", | ||
| modelRatio, finalGroupRatio, taskResult.TotalTokens, | ||
| logger.LogQuota(preConsumedQuota), logger.LogQuota(actualQuota), logger.LogQuota(refundQuota)) | ||
| model.RecordLog(task.UserId, model.LogTypeSystem, logContent) |
There was a problem hiding this comment.
Refund must also rewind channel/user usage counters
When we refund refundQuota we only credit the user balance. The channel’s used quota and the user’s recorded used quota stay at the higher pre-charge value, so dashboards and reconciliation drift permanently. Mirror the charge path by subtracting refundQuota from the channel/user usage counters (e.g. UpdateChannelUsedQuota with a negative delta and an equivalent used-quota adjustment) so accounting remains consistent.
🤖 Prompt for AI Agents
In controller/task_video.go around lines 188 to 197, the refund only credits the
user balance but does not rewind the channel and user usage counters; update the
accounting by mirroring the original charge path — subtract refundQuota from the
channel's used quota (e.g., call the same UpdateChannelUsedQuota function with a
negative delta or equivalent) and decrement the user's recorded used quota by
refundQuota as well, perform these updates alongside IncreaseUserQuota so state
remains consistent, handle and log any errors from those updates and ensure the
operations are applied atomically or rolled back on failure.
| func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) { | ||
| responseBody, err := io.ReadAll(resp.Body) | ||
| if err != nil { | ||
| taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) | ||
| return | ||
| } | ||
| _ = resp.Body.Close() | ||
|
|
||
| // Parse Doubao response | ||
| var dResp responsePayload | ||
| if err := json.Unmarshal(responseBody, &dResp); err != nil { | ||
| taskErr = service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| if dResp.ID == "" { | ||
| taskErr = service.TaskErrorWrapper(fmt.Errorf("task_id is empty"), "invalid_response", http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| c.JSON(http.StatusOK, gin.H{"task_id": dResp.ID}) | ||
| return dResp.ID, responseBody, nil | ||
| } |
There was a problem hiding this comment.
Handle non-2xx Doubao responses before parsing success payload
When Doubao returns a 4xx/5xx (e.g., auth failure or validation error), we still unmarshal into responsePayload. Because id is empty, this path always bubbles up as our own 500/"task_id is empty", masking the original failure and misreporting the status. Please gate the success flow on resp.StatusCode so we surface upstream errors accurately and keep retries/backoffs sane.
@@
- // Parse Doubao response
+ if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
+ taskErr = service.TaskErrorWrapper(
+ fmt.Errorf("upstream status %d: %s", resp.StatusCode, string(responseBody)),
+ "upstream_request_failed",
+ resp.StatusCode,
+ )
+ return
+ }
+
+ // Parse Doubao response
var dResp responsePayload📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| func (a *TaskAdaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (taskID string, taskData []byte, taskErr *dto.TaskError) { | |
| responseBody, err := io.ReadAll(resp.Body) | |
| if err != nil { | |
| taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) | |
| return | |
| } | |
| _ = resp.Body.Close() | |
| // Parse Doubao response | |
| var dResp responsePayload | |
| if err := json.Unmarshal(responseBody, &dResp); err != nil { | |
| taskErr = service.TaskErrorWrapper(errors.Wrapf(err, "body: %s", responseBody), "unmarshal_response_body_failed", http.StatusInternalServerError) | |
| return | |
| } | |
| if dResp.ID == "" { | |
| taskErr = service.TaskErrorWrapper(fmt.Errorf("task_id is empty"), "invalid_response", http.StatusInternalServerError) | |
| return | |
| } | |
| c.JSON(http.StatusOK, gin.H{"task_id": dResp.ID}) | |
| return dResp.ID, responseBody, nil | |
| } | |
| func (a *TaskAdaptor) DoResponse( | |
| c *gin.Context, | |
| resp *http.Response, | |
| info *relaycommon.RelayInfo, | |
| ) (taskID string, taskData []byte, taskErr *dto.TaskError) { | |
| responseBody, err := io.ReadAll(resp.Body) | |
| if err != nil { | |
| taskErr = service.TaskErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError) | |
| return | |
| } | |
| _ = resp.Body.Close() | |
| // Handle non-2xx upstream responses | |
| if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { | |
| taskErr = service.TaskErrorWrapper( | |
| fmt.Errorf("upstream status %d: %s", resp.StatusCode, string(responseBody)), | |
| "upstream_request_failed", | |
| resp.StatusCode, | |
| ) | |
| return | |
| } | |
| // Parse Doubao response | |
| var dResp responsePayload | |
| if err := json.Unmarshal(responseBody, &dResp); err != nil { | |
| taskErr = service.TaskErrorWrapper( | |
| errors.Wrapf(err, "body: %s", responseBody), | |
| "unmarshal_response_body_failed", | |
| http.StatusInternalServerError, | |
| ) | |
| return | |
| } | |
| if dResp.ID == "" { | |
| taskErr = service.TaskErrorWrapper( | |
| fmt.Errorf("task_id is empty"), | |
| "invalid_response", | |
| http.StatusInternalServerError, | |
| ) | |
| return | |
| } | |
| c.JSON(http.StatusOK, gin.H{"task_id": dResp.ID}) | |
| return dResp.ID, responseBody, nil | |
| } |
🤖 Prompt for AI Agents
In relay/channel/task/doubao/adaptor.go around lines 123 to 145, the code
currently unmarshals the response body and treats missing dResp.ID as an
internal 500 error even when Doubao returned a non-2xx (4xx/5xx) error; change
the flow to first check resp.StatusCode and, for non-2xx responses, wrap and
return the original status and body as a TaskError (including status code and
response body text) without treating it as a missing task_id; only on 2xx should
you unmarshal into responsePayload and proceed to return the task ID and body,
ensuring resp.Body is closed and errors from ReadAll/Unmarshal are still wrapped
appropriately.
Uh oh!
There was an error while loading. Please reload this page.