Skip to content

对即梦2.0完美支持 - #4872

Open
xuanyustudio wants to merge 182 commits into
QuantumNous:mainfrom
xuanyustudio:main
Open

对即梦2.0完美支持#4872
xuanyustudio wants to merge 182 commits into
QuantumNous:mainfrom
xuanyustudio:main

Conversation

@xuanyustudio

@xuanyustudio xuanyustudio commented May 15, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

  • 请提供人工撰写的简洁摘要,避免直接粘贴未经整理的 AI 输出。

📝 变更描述 / Description

(简述:做了什么?为什么这样改能生效?请基于你对代码逻辑的理解来写,避免粘贴未经整理的内容)

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
  • ✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

  • Closes # (如有)

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • Bug fix 说明: 若此 PR 标记为 Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

(请在此粘贴截图、关键日志或测试报告,以证明变更生效)

Summary by CodeRabbit

  • New Features

    • Added support for VolcEngine (video + OpenSpeech v3 TTS), XAI video, ApiWenhao and ApiMart channels, and Gemini preview TTS; per-second billing mode and upstream cost multiplier in pricing UI.
    • New invitees list endpoint and invitee count included in user profile; token creation now returns full token key.
  • Bug Fixes

    • Improved task billing/reporting, token usage backfill, consumption logging timing, and acceptance of 2xx upstream task responses; fixed cost sign/display in usage logs.
  • Documentation

    • Local guide and Node example for relay TTS / OpenSpeech v3.
  • Chores

    • Adjusted Go toolchain and dependency versions.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds async video adaptors (XAI/ApiMart/ApiWenhao), VolcEngine TTS v3 and Doubao content support, Gemini preview-TTS defaults, content-capable TaskSubmitReq, expanded billing/usage logging and token backfill, affiliate invitee APIs, frontend billing UI for per-second pricing, docs/examples, and dependency adjustments.

Changes

Platform & Adaptor Support

Layer / File(s) Summary
XAI TaskAdaptor & registration
relay/channel/task/xai/adaptor.go, relay/relay_adaptor.go
New XAI TaskAdaptor handling create/query, URL normalization, auth headers, request body injection (model), response/status mapping, polling, and OpenAI-style video conversion; wired into GetTaskAdaptor.
ApiMart adaptor & tests
relay/channel/task/apimart/*, relay/channel/task/apimart/adaptor_test.go
ApiMart TaskAdaptor: payload normalization, create/poll flows, USD cost extraction, billing adjustment, and unit tests for parsing and normalization.
ApiWenhao adaptor & tests
relay/channel/task/apiwenhao/*, relay/channel/task/apiwenhao/adaptor_test.go
ApiWenhao TaskAdaptor: create/query envelope, req_key handling, result parsing across multiple shapes, and unit tests.
Doubao & VolcEngine routing
relay/channel/task/doubao/adaptor.go, relay/channel/volcengine/*
Detect VolcEngine official origins, switch endpoints, prefer content payloads, map duration/ratio/resolution/generate_audio/watermark, and add seedance models.
VolcEngine OpenSpeech v3 TTS (NDJSON)
relay/channel/volcengine/tts_v3.go, relay/channel/volcengine/adaptor.go
Implements TTS v3 request/NDJSON response parsing, base64 chunk decoding, v3 header/auth handling, and adaptor wiring for non-streaming v3 flow.
Gemini preview-TTS
relay/channel/gemini/*
Detect preview-TTS Gemini models, apply AUDIO-only generation defaults, normalize snake_case speech config into Gemini GenerationConfig, and update allowlist.

Task schema, validation, polling, and billing

Layer / File(s) Summary
Task request schema & helpers
relay/common/relay_info.go, relay/common/task_submit_image.go, relay/common/relay_utils.go
Adds content, generate_audio, ratio, aspect_ratio, resolution, watermark to TaskSubmitReq; flexible image/duration/metadata parsing; BuildTaskFetchBody; prompt validation now allows content-only requests.
Relay submit & adaptor integration
relay/relay_adaptor.go, relay/relay_task.go, controller/relay.go, controller/task_video.go
Register new adaptors, treat any 2xx create response as success, include upstream model/req_key in fetch body, and defer LogTaskConsumption until task.TaskID exists (pass publicTaskID).
Billing service & logs
service/task_billing.go, model/log.go
LogTaskConsumption now accepts publicTaskID, billing logs persist PromptTokens/CompletionTokens, add ReportTaskUsageToConsumeLog and TryPatchTaskConsumeLogTokens, RecalculateTaskQuota accepts usageForLog, and taskBillingOther records upstream cost and multiplier.
Task polling & settle order
service/task_polling.go, service/task_billing.go
Poll parsing supports nested DTOs and inner data bytes, merges nested usage into TaskInfo, applies adaptor billing adjustment before settlement, and reports/patches consume logs accordingly.
Billing tests
service/task_billing_test.go
Updated test callsites and per-call-settle expectations to reflect adaptor upstream-cost behavior and new RecalculateTaskQuota signature.

Affiliate invitee features

Layer / File(s) Summary
Invitee DTO & queries
model/user.go
Add InviteeSummary, GetInviteesByInviterId, GetInviteeCountByInviterId, AdjustUserUsedQuota, and invoke inviteUser for non-zero inviterId in compliant flows.
Controller & route
controller/user.go, router/api-router.go
Add GetAffInvitees endpoint and route; GetSelf now populates aff_count.

API response, docs, frontend, deps

Layer / File(s) Summary
Token creation response
controller/token.go
AddToken now returns the newly created token's full key in response data (one-time disclosure).
Docs & examples
docs/local-relay-tts-test.md, docs/examples/relay-tts-speech-test.mjs
Add local testing docs and Node.js example for VolcEngine OpenSpeech v3 (/v1/audio/speech).
Frontend pricing & channels
web/*, web/classic/src/pages/Setting/Ratio/*, web/default/src/features/channels/*
Add channel types 58/59, per-second billing UI and upstream cost multiplier handling, pricing preview/serialize logic, icons, and translations.
Usage logs & i18n
web/*/usage-logs/*, web/classic/src/i18n/i18n.js, web/classic/src/index.jsx
Adjust usage cost display sign for refunds, tweak subscription tooltip, and refine i18n fallback/detection and semi-locale mapping.
Go toolchain & deps
go.mod
Adjust go directive and golang.org/x dependency versions.

Sequence Diagram(s)

sequenceDiagram
  actor Client
  participant Gateway as Relay Controller
  participant Adaptor as Channel Adaptor
  participant Upstream as Upstream Provider
  participant Billing as Billing Service
  Client->>Gateway: POST /v1/task (content/video request)
  Gateway->>Adaptor: BuildRequest + credentials
  Adaptor->>Upstream: Create task (Bearer/JSON)
  Upstream-->>Adaptor: 2xx (task_id)
  Adaptor-->>Gateway: relay create response (task_id, publicTaskID)
  Gateway->>Billing: LogTaskConsumption(relayInfo, publicTaskID) [after TaskID set]
  Client->>Gateway: Poll task status
  Gateway->>Adaptor: FetchTask (task_id + upstream_model)
  Adaptor->>Upstream: Query task
  Upstream-->>Adaptor: Task result (status, url, usage)
  Adaptor-->>Gateway: TaskInfo (merged usage)
  Gateway->>Billing: ReportTaskUsageToConsumeLog(task, usage)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • seefs001
  • creamlike1024
  • Calcium-Ion

"A rabbit hops through code so wide,
Videos route and tokens bide,
NDJSON sings the TTS tune,
Invitees counted by the moon,
Hooray — the relay hums with pride!" 🥕

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@xuanyustudio

Copy link
Copy Markdown
Author

即梦2.0是视频行业领先模型,支持

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
relay/common/relay_info.go (1)

4-4: ⚠️ Potential issue | 🔴 Critical

Remove encoding/json import or add a wrapper type to common/json.go.

The encoding/json import at line 4 violates the coding guideline that states "Do NOT directly import or call encoding/json in business code." While the file correctly uses common.Marshal() and common.Unmarshal() for all JSON operations, it imports encoding/json only to use json.RawMessage as a field type (lines 700–701).

Replace json.RawMessage with []byte for those fields, or create a type alias in common/json.go (e.g., type RawMessage = encoding/json.RawMessage) and import that instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/common/relay_info.go` at line 4, This file imports encoding/json only
to use json.RawMessage for struct fields; remove the direct encoding/json import
and either (A) change those fields that reference json.RawMessage to use []byte
instead (search for occurrences of json.RawMessage in relay_info.go around the
struct field declarations) and keep using common.Marshal/common.Unmarshal for
JSON ops, or (B) add a safe alias in common/json.go (e.g., type RawMessage =
encoding/json.RawMessage) and replace references to json.RawMessage with
common.RawMessage, then remove the direct encoding/json import from
relay_info.go; ensure all references to json.RawMessage are updated and
tests/compilation pass.
service/task_polling.go (1)

552-565: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Per-call tasks skip usage backfill entirely.

The early return on PerCallBilling exits before ReportTaskUsageToConsumeLog, so any successful per-call task that reports usage never writes its prompt/completion tokens. That defeats the new “write usage regardless of quota delta” path for this billing mode.

Suggested fix
 func settleTaskBillingOnComplete(ctx context.Context, adaptor TaskPollingAdaptor, task *model.Task, taskResult *relaycommon.TaskInfo) {
 	// 0. 按次计费的任务不做差额结算
-	if bc := task.PrivateData.BillingContext; bc != nil && bc.PerCallBilling {
+	if bc := task.PrivateData.BillingContext; bc != nil && bc.PerCallBilling {
 		logger.LogInfo(ctx, fmt.Sprintf("任务 %s 按次计费,跳过差额结算", task.TaskID))
-		return
-	}
-	// 1. 优先让 adaptor 决定最终额度
-	if actualQuota := adaptor.AdjustBillingOnComplete(task, taskResult); actualQuota > 0 {
+	} else if actualQuota := adaptor.AdjustBillingOnComplete(task, taskResult); actualQuota > 0 {
 		RecalculateTaskQuota(ctx, task, actualQuota, "adaptor计费调整", taskResult)
 	} else if taskResult != nil && taskResult.TotalTokens > 0 {
 		// 2. 回退到 token 重算
 		RecalculateTaskQuotaByTokens(ctx, task, taskResult)
 	}
 	// 3. 将 usage 写回消费日志(与额度差额是否为零无关)
 	ReportTaskUsageToConsumeLog(task, taskResult)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@service/task_polling.go` around lines 552 - 565, The early return for
per-call billing in the completion billing path causes
ReportTaskUsageToConsumeLog not to run, so per-call tasks never write usage;
remove the return and instead let the PerCallBilling branch skip quota
recalculation but still call ReportTaskUsageToConsumeLog. Concretely, in the
block that checks task.PrivateData.BillingContext and bc.PerCallBilling, stop
returning immediately — call ReportTaskUsageToConsumeLog(task, taskResult) after
skipping RecalculateTaskQuota/RecalculateTaskQuotaByTokens, and keep the
existing adaptor.AdjustBillingOnComplete call path unchanged for non-per-call
cases.
controller/relay.go (1)

566-590: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Persist the task before finalizing billing/logging.

This block still settles quota and writes the consume log before task.Insert() succeeds. If the insert fails, the request leaves behind charged quota plus a public_task_id consume log, but no task row for polling or later reconciliation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/relay.go` around lines 566 - 590, The code currently calls
service.SettleBilling and service.LogTaskConsumption before persisting the task,
which can leave charged quota and a consume log if Insert() fails; change the
order so you create the task with model.InitTask, populate
task.PrivateData/.../Action, then call task.Insert() first and only after insert
succeeds call service.SettleBilling(c, relayInfo, result.Quota) and
service.LogTaskConsumption(c, relayInfo, task.TaskID); ensure that on insertErr
you do not call SettleBilling or LogTaskConsumption and return/handle the error
(or attempt appropriate rollback) so billing and logs only occur for persisted
tasks.
🧹 Nitpick comments (3)
relay/channel/task/doubao/adaptor.go (3)

345-357: ⚡ Quick win

Validate mutually exclusive duration and ratio fields.

The code allows both req.Seconds and req.Duration to be set, with Duration silently overwriting Seconds. Similarly, both req.Ratio and req.AspectRatio can overwrite each other. Users who set both fields may be confused when one is ignored.

Consider validating that only one field from each pair is set, or documenting the precedence in API documentation.

🛡️ Suggested validation
 	if sec, _ := strconv.Atoi(req.Seconds); sec > 0 {
+		if req.Duration > 0 {
+			return nil, errors.New("cannot specify both 'seconds' and 'duration' fields")
+		}
 		r.Duration = lo.ToPtr(dto.IntValue(sec))
 	}
 
 	if req.Duration > 0 {
 		r.Duration = lo.ToPtr(dto.IntValue(req.Duration))
 	}
 	if req.Ratio != "" {
+		if req.AspectRatio != "" {
+			return nil, errors.New("cannot specify both 'ratio' and 'aspect_ratio' fields")
+		}
 		r.Ratio = req.Ratio
 	}
 	if req.AspectRatio != "" {
 		r.Ratio = req.AspectRatio
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/channel/task/doubao/adaptor.go` around lines 345 - 357, The current
assignment logic silently overwrites fields: r.Duration can be set from both
req.Seconds and req.Duration, and r.Ratio from req.Ratio and req.AspectRatio;
update the code that sets r.Duration and r.Ratio to validate mutual exclusivity
and fail fast: detect when both req.Seconds and req.Duration are provided and
return an error (or validation response) instead of overwriting, and likewise
detect when both req.Ratio and req.AspectRatio are provided and return an error;
implement these checks immediately before the existing assignments around
r.Duration, r.Ratio (the block that parses strconv.Atoi(req.Seconds) and assigns
lo.ToPtr(dto.IntValue(req.Duration)) and assigns r.Ratio), so callers get a
clear validation error rather than silent precedence.

333-337: ⚡ Quick win

Clarify or remove redundant text item filtering.

Line 333 uses lo.Reject to remove all existing text-type items before appending the prompt as a new text item. In the current fallback branch, r.Content only contains image items at this point, so the reject operation is a no-op.

This pattern is confusing and could hide bugs if the code is refactored. If the intent is to ensure only one text item exists, consider documenting this or restructuring the logic.

♻️ Suggested clarification
-		r.Content = lo.Reject(r.Content, func(c ContentItem, _ int) bool { return c.Type == "text" })
+		// Ensure prompt text is the only text item (remove any existing text items)
+		r.Content = lo.Reject(r.Content, func(c ContentItem, _ int) bool { return c.Type == "text" })
 		r.Content = append(r.Content, ContentItem{
 			Type: "text",
 			Text: req.Prompt,
 		})

Or remove the reject if it's truly unnecessary:

-		r.Content = lo.Reject(r.Content, func(c ContentItem, _ int) bool { return c.Type == "text" })
 		r.Content = append(r.Content, ContentItem{
 			Type: "text",
 			Text: req.Prompt,
 		})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/channel/task/doubao/adaptor.go` around lines 333 - 337, The lo.Reject
call that removes Type=="text" from r.Content before appending a new text
ContentItem is redundant in the current fallback where r.Content contains only
images; update the code around r.Content to either (a) remove the lo.Reject line
entirely if you guarantee this branch only ever holds images, or (b) make the
intent explicit by replacing any existing text items instead of blindly
rejecting (e.g., filter and replace text items) and add a short comment
explaining "ensure exactly one text item" so future refactors don't reintroduce
confusion; target the r.Content manipulation (lo.Reject,
ContentItem{Type:"text", Text:req.Prompt}) in adaptor.go when making the change.

27-29: ⚡ Quick win

Consider more robust URL pattern matching.

The isVolcEngineOfficial function uses string prefix matching to detect official VolcEngine URLs. This approach is fragile:

  • Future official domains won't be recognized automatically
  • Unofficial domains starting with ark. or visual. would be incorrectly classified

Consider using an allowlist of complete domain names or a configurable pattern.

♻️ Suggested improvement
 func isVolcEngineOfficial(baseURL string) bool {
-	return strings.HasPrefix(baseURL, "https://ark.") || strings.HasPrefix(baseURL, "https://visual.")
+	// Check against known official VolcEngine domains
+	officialDomains := []string{
+		"https://ark.cn-beijing.volces.com",
+		"https://visual.volcengineapi.com",
+		// Add other official domains as needed
+	}
+	for _, domain := range officialDomains {
+		if strings.HasPrefix(baseURL, domain) {
+			return true
+		}
+	}
+	return false
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/channel/task/doubao/adaptor.go` around lines 27 - 29, The
isVolcEngineOfficial function currently uses naive prefix checks on baseURL;
instead parse baseURL (e.g., using url.Parse) to extract the hostname and match
it against a maintained allowlist or configurable pattern of full domain names
(or strict suffixes like ".volcengine.net") rather than prefix strings; update
isVolcEngineOfficial to validate the parsed hostname against that
allowlist/config (or a compiled regex) so only exact/approved hostnames (not any
URL starting with "https://ark." or "https://visual.") are treated as official.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.gitignore:
- Line 34: Remove the placeholder literal pattern "..." from the .gitignore
file; locate the entry that contains just three dots and delete that line so
only intentional ignore patterns remain (no code changes required beyond
removing the "..." line).
- Line 33: The .gitignore entry for token_estimator_test.go is incorrectly
hiding a real test file from version control. Remove that ignore rule from
.gitignore so the token_estimator_test.go test can be committed and picked up by
CI; verify there are no other test-related patterns in .gitignore that would
also exclude intended test files.

In `@relay/channel/task/doubao/adaptor.go`:
- Around line 289-317: The loop that builds ContentItem from req.Content
(creating ContentItem and MediaURL and appending to r.Content) must validate
each content item before appending: ensure each item is a map[string]interface{}
(or handle its actual type), and if keys like
"image_url"/"video_url"/"audio_url" exist they must be maps containing a string
"url"; if any expected nested type assertion fails, either skip the malformed
item or return an explicit error to the caller instead of appending a partially
populated ContentItem. Update the code around the ContentItem construction (the
loop over req.Content, the ContentItem and MediaURL usage, and the r.Content
append) to perform these checks and log/propagate a clear error describing which
field was invalid (e.g., include the item index and offending key) rather than
silently ignoring type assertion failures.

In `@relay/channel/task/xai/adaptor.go`:
- Line 5: Remove the direct encoding/json import and replace all usages of
json.RawMessage with []byte (e.g., change fields or local vars typed as
json.RawMessage to []byte) in relay/channel/task/xai/adaptor.go (the occurrence
around the json.RawMessage usage); keep using common.Unmarshal() for decoding
and ensure no other direct calls to encoding/json remain—delete the import line
referencing "encoding/json" and update any function signatures or struct fields
that referenced json.RawMessage to use []byte instead.

In `@web/src/components/topup/RechargeCard.jsx`:
- Around line 571-583: The hardcoded insecure URL in the RechargeCard component
should be removed and replaced with the existing configurable topUpLink and
i18n; update the anchor that currently uses 'http://83zi.com/faka.html' to use
the component's topUpLink variable (the same source used in the block around
lines 612-623) and wrap the link text with t('购买兑换码') for translation, ensure
the link uses HTTPS if provided and fall back to hiding the anchor when
topUpLink is not set so admin-configurable settings control the external
destination; locate the anchor in RechargeCard.jsx and swap to topUpLink usage
and translated text accordingly.

---

Outside diff comments:
In `@controller/relay.go`:
- Around line 566-590: The code currently calls service.SettleBilling and
service.LogTaskConsumption before persisting the task, which can leave charged
quota and a consume log if Insert() fails; change the order so you create the
task with model.InitTask, populate task.PrivateData/.../Action, then call
task.Insert() first and only after insert succeeds call service.SettleBilling(c,
relayInfo, result.Quota) and service.LogTaskConsumption(c, relayInfo,
task.TaskID); ensure that on insertErr you do not call SettleBilling or
LogTaskConsumption and return/handle the error (or attempt appropriate rollback)
so billing and logs only occur for persisted tasks.

In `@relay/common/relay_info.go`:
- Line 4: This file imports encoding/json only to use json.RawMessage for struct
fields; remove the direct encoding/json import and either (A) change those
fields that reference json.RawMessage to use []byte instead (search for
occurrences of json.RawMessage in relay_info.go around the struct field
declarations) and keep using common.Marshal/common.Unmarshal for JSON ops, or
(B) add a safe alias in common/json.go (e.g., type RawMessage =
encoding/json.RawMessage) and replace references to json.RawMessage with
common.RawMessage, then remove the direct encoding/json import from
relay_info.go; ensure all references to json.RawMessage are updated and
tests/compilation pass.

In `@service/task_polling.go`:
- Around line 552-565: The early return for per-call billing in the completion
billing path causes ReportTaskUsageToConsumeLog not to run, so per-call tasks
never write usage; remove the return and instead let the PerCallBilling branch
skip quota recalculation but still call ReportTaskUsageToConsumeLog. Concretely,
in the block that checks task.PrivateData.BillingContext and bc.PerCallBilling,
stop returning immediately — call ReportTaskUsageToConsumeLog(task, taskResult)
after skipping RecalculateTaskQuota/RecalculateTaskQuotaByTokens, and keep the
existing adaptor.AdjustBillingOnComplete call path unchanged for non-per-call
cases.

---

Nitpick comments:
In `@relay/channel/task/doubao/adaptor.go`:
- Around line 345-357: The current assignment logic silently overwrites fields:
r.Duration can be set from both req.Seconds and req.Duration, and r.Ratio from
req.Ratio and req.AspectRatio; update the code that sets r.Duration and r.Ratio
to validate mutual exclusivity and fail fast: detect when both req.Seconds and
req.Duration are provided and return an error (or validation response) instead
of overwriting, and likewise detect when both req.Ratio and req.AspectRatio are
provided and return an error; implement these checks immediately before the
existing assignments around r.Duration, r.Ratio (the block that parses
strconv.Atoi(req.Seconds) and assigns lo.ToPtr(dto.IntValue(req.Duration)) and
assigns r.Ratio), so callers get a clear validation error rather than silent
precedence.
- Around line 333-337: The lo.Reject call that removes Type=="text" from
r.Content before appending a new text ContentItem is redundant in the current
fallback where r.Content contains only images; update the code around r.Content
to either (a) remove the lo.Reject line entirely if you guarantee this branch
only ever holds images, or (b) make the intent explicit by replacing any
existing text items instead of blindly rejecting (e.g., filter and replace text
items) and add a short comment explaining "ensure exactly one text item" so
future refactors don't reintroduce confusion; target the r.Content manipulation
(lo.Reject, ContentItem{Type:"text", Text:req.Prompt}) in adaptor.go when making
the change.
- Around line 27-29: The isVolcEngineOfficial function currently uses naive
prefix checks on baseURL; instead parse baseURL (e.g., using url.Parse) to
extract the hostname and match it against a maintained allowlist or configurable
pattern of full domain names (or strict suffixes like ".volcengine.net") rather
than prefix strings; update isVolcEngineOfficial to validate the parsed hostname
against that allowlist/config (or a compiled regex) so only exact/approved
hostnames (not any URL starting with "https://ark." or "https://visual.") are
treated as official.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 162fcdfa-b02b-433e-8f61-9d7b05f7199e

📥 Commits

Reviewing files that changed from the base of the PR and between 18282e6 and e5d54b3.

⛔ Files ignored due to path filters (2)
  • go.sum is excluded by !**/*.sum
  • 视频生成对接文档_技术版.doc is excluded by !**/*.doc
📒 Files selected for processing (26)
  • .gitignore
  • controller/channel-test.go
  • controller/relay.go
  • controller/token.go
  • controller/user.go
  • go.mod
  • model/log.go
  • model/user.go
  • relay/channel/gemini/adaptor.go
  • relay/channel/gemini/constant.go
  • relay/channel/gemini/relay-gemini.go
  • relay/channel/task/doubao/adaptor.go
  • relay/channel/task/xai/adaptor.go
  • relay/channel/volcengine/constants.go
  • relay/common/relay_info.go
  • relay/common/relay_utils.go
  • relay/relay_adaptor.go
  • relay/relay_task.go
  • router/api-router.go
  • service/task_billing.go
  • service/task_billing_test.go
  • service/task_polling.go
  • web/src/components/topup/InvitationCard.jsx
  • web/src/components/topup/RechargeCard.jsx
  • web/src/components/topup/index.jsx
  • web/src/components/topup/modals/AffInviteesModal.jsx

Comment thread .gitignore
Comment thread .gitignore
Comment thread relay/channel/task/doubao/adaptor.go

import (
"bytes"
"encoding/json"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify no direct encoding/json import remains in this adaptor.
rg -n --type=go '"encoding/json"|json\.RawMessage' relay/channel/task/xai/adaptor.go

Repository: QuantumNous/new-api

Length of output: 123


🏁 Script executed:

#!/bin/bash
# Check context around line 283 and verify if there are other encoding/json usages
echo "=== Context around line 283 (20 lines) ==="
sed -n '275,295p' relay/channel/task/xai/adaptor.go

echo -e "\n=== Check for all encoding/json patterns in file ==="
rg -n --type=go 'encoding/json|json\.' relay/channel/task/xai/adaptor.go | head -20

echo -e "\n=== Check file imports ==="
head -30 relay/channel/task/xai/adaptor.go

Repository: QuantumNous/new-api

Length of output: 1590


Remove direct encoding/json import and replace json.RawMessage with []byte.

The file imports encoding/json at line 5 and uses json.RawMessage at line 283. This violates the project's Go JSON requirement: all business code must use wrapper functions from common/json.go and must not directly import or call encoding/json.

The fix is straightforward: remove the import and replace json.RawMessage with []byte (which is functionally equivalent). All JSON operations in the code already use common.Unmarshal().

Proposed patch
 import (
 	"bytes"
-	"encoding/json"
 	"fmt"
 	"io"
 	"net/http"
 	"net/url"
 	"strings"
@@
 	var wrap struct {
-		Data json.RawMessage `json:"data"`
+		Data []byte `json:"data"`
 	}
📝 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.

Suggested change
"encoding/json"
import (
"bytes"
"fmt"
"io"
"net/http"
"net/url"
"strings"
...
)
...
var wrap struct {
Data []byte `json:"data"`
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/channel/task/xai/adaptor.go` at line 5, Remove the direct encoding/json
import and replace all usages of json.RawMessage with []byte (e.g., change
fields or local vars typed as json.RawMessage to []byte) in
relay/channel/task/xai/adaptor.go (the occurrence around the json.RawMessage
usage); keep using common.Unmarshal() for decoding and ensure no other direct
calls to encoding/json remain—delete the import line referencing "encoding/json"
and update any function signatures or struct fields that referenced
json.RawMessage to use []byte instead.

Comment on lines +571 to +583
<div className='flex items-center gap-2'>
<Text type='tertiary' strong>
{t('兑换码充值')}
</Text>
<a
href='http://83zi.com/faka.html'
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 hover:text-blue-600'
>
购买兑换码
</a>
</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove hardcoded external URL or make it configurable.

Line 576 contains a hardcoded insecure external URL (http://83zi.com/faka.html). This introduces several critical issues:

  1. Security: Uses insecure HTTP instead of HTTPS
  2. Maintainability: Hardcoded URLs cannot be changed without code deployment
  3. Configuration conflict: The codebase already supports a configurable topUpLink (see lines 612-623), making this redundant and inconsistent
  4. Internationalization: Link text "购买兑换码" is not wrapped in t() for translation
  5. External dependency: Links to what appears to be a third-party commercial service without admin control

Recommended solution: Remove this hardcoded link and rely on the existing configurable topUpLink system, or introduce a new backend setting if a separate redemption purchase link is needed.

🔒 Proposed fix to remove hardcoded URL
       <Card
         className='!rounded-xl w-full'
         title={
-          <div className='flex items-center gap-2'>
-            <Text type='tertiary' strong>
-              {t('兑换码充值')}
-            </Text>
-            <a
-              href='http://83zi.com/faka.html'
-              target='_blank'
-              rel='noopener noreferrer'
-              className='text-blue-500 hover:text-blue-600'
-            >
-              购买兑换码
-            </a>
-          </div>
+          <Text type='tertiary' strong>
+            {t('兑换码充值')}
+          </Text>
         }
       >
📝 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.

Suggested change
<div className='flex items-center gap-2'>
<Text type='tertiary' strong>
{t('兑换码充值')}
</Text>
<a
href='http://83zi.com/faka.html'
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 hover:text-blue-600'
>
购买兑换码
</a>
</div>
<Text type='tertiary' strong>
{t('兑换码充值')}
</Text>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/src/components/topup/RechargeCard.jsx` around lines 571 - 583, The
hardcoded insecure URL in the RechargeCard component should be removed and
replaced with the existing configurable topUpLink and i18n; update the anchor
that currently uses 'http://83zi.com/faka.html' to use the component's topUpLink
variable (the same source used in the block around lines 612-623) and wrap the
link text with t('购买兑换码') for translation, ensure the link uses HTTPS if
provided and fall back to hiding the anchor when topUpLink is not set so
admin-configurable settings control the external destination; locate the anchor
in RechargeCard.jsx and swap to topUpLink usage and translated text accordingly.

# Conflicts:
#	.gitignore
#	model/log.go
#	web/classic/src/components/topup/InvitationCard.jsx
#	web/classic/src/components/topup/RechargeCard.jsx
#	web/classic/src/components/topup/index.jsx
#	web/classic/src/components/topup/modals/AffInviteesModal.jsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
model/log.go (1)

322-355: 🏗️ Heavy lift

Reconsider the Limit(80) logic—unnecessary fallback logs can be eliminated with a time-based window.

The function has a fallback mechanism that creates a new consume log when TryPatchTaskConsumeLogTokens returns false. However, for tasks with multi-hour delays (e.g., video processing), the hardcoded Limit(80) can cause the original submission log to fall out of the lookup window before the patch is applied. This forces an unnecessary fallback log to be created with is_task_usage_only=true, resulting in two log entries per task instead of a single patched log.

While both logs are counted in token aggregations (the is_task_usage_only flag is not filtered by SumUsedToken), eliminating unnecessary fallback logs improves log cleanliness and query efficiency.

A time-based window (e.g., 24 hours) is a straightforward improvement:

♻️ Time-bounded lookup
-	var logs []Log
-	err := LOG_DB.Where("user_id = ? AND type = ?", userId, LogTypeConsume).
-		Order("id desc").Limit(80).Find(&logs).Error
+	var logs []Log
+	// Limit lookback to last 24h to avoid unnecessary fallback logs for multi-hour tasks
+	since := time.Now().Add(-24 * time.Hour).Unix()
+	err := LOG_DB.Where("user_id = ? AND type = ? AND created_at >= ?", userId, LogTypeConsume, since).
+		Order("id desc").Limit(500).Find(&logs).Error
 	if err != nil {
 		return false
 	}

For longer-running tasks or higher confidence, consider indexing public_task_id as a dedicated column to enable direct O(1) lookup instead of scanning parsed JSON.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/log.go` around lines 322 - 355, TryPatchTaskConsumeLogTokens currently
restricts the search with Limit(80), which can miss original task logs for
long-running tasks; replace the fixed row limit with a time-bounded WHERE clause
(e.g., filter Log.CreatedAt >= now()-24h or a configurable window) when querying
LOG_DB in TryPatchTaskConsumeLogTokens so the lookup finds older matching logs,
and optionally add/document an index or dedicated column for public_task_id to
allow direct lookup instead of parsing Other JSON; ensure the query still
filters by user_id and type = LogTypeConsume and preserve the later Updates(...)
call on the matched logs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@model/log.go`:
- Around line 322-355: TryPatchTaskConsumeLogTokens currently restricts the
search with Limit(80), which can miss original task logs for long-running tasks;
replace the fixed row limit with a time-bounded WHERE clause (e.g., filter
Log.CreatedAt >= now()-24h or a configurable window) when querying LOG_DB in
TryPatchTaskConsumeLogTokens so the lookup finds older matching logs, and
optionally add/document an index or dedicated column for public_task_id to allow
direct lookup instead of parsing Other JSON; ensure the query still filters by
user_id and type = LogTypeConsume and preserve the later Updates(...) call on
the matched logs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: af8d84d3-cf25-4925-b995-36978ab3a012

📥 Commits

Reviewing files that changed from the base of the PR and between 388782b and b79e385.

⛔ Files ignored due to path filters (2)
  • web/classic/bun.lock is excluded by !**/*.lock
  • web/default/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • model/log.go
  • model/user.go
  • service/task_billing.go
  • web/classic/src/components/table/usage-logs/UsageLogsColumnDefs.jsx
  • web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • model/user.go
  • service/task_billing.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
web/classic/src/i18n/i18n.js (1)

48-53: ⚡ Quick win

Reconsider fallback order for non‑Chinese users.

For en, fr, ru, ja, vi users the default chain falls back to zh-CN before en. Practically this means an English user encountering a missing key in en.json will see Chinese text rather than English, which is worse than falling back to the most-complete English bundle. Consider keeping the Chinese-first chain only for zh-* and using an English-first chain elsewhere.

🩹 Suggested fix
     fallbackLng: {
       'zh-TW': ['zh-CN', 'en'],
       'zh-CN': ['en'],
-      default: ['zh-CN', 'en'],
+      default: ['en', 'zh-CN'],
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/classic/src/i18n/i18n.js` around lines 48 - 53, The current i18n
fallbackLng defaults to ['zh-CN','en'] causing non‑Chinese locales to fall back
to Chinese before English; update the fallbackLng mapping so Chinese-first
behavior is only for Chinese locales (e.g., keep 'zh-TW': ['zh-CN','en'] and
'zh-CN': ['en'] or similar), and make the global/default fallback English-first
(e.g., default: ['en','zh-CN']) so locales like 'en','fr','ru','ja','vi' will
prefer English before Chinese; update the fallbackLng object accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@relay/channel/volcengine/tts_v3.go`:
- Around line 66-77: The function mapEncodingToTTSV3Format currently maps
"ogg_opus" to "mp3" (making that case effectively dead code) which causes
callers requesting opus to receive MP3 streams; update mapEncodingToTTSV3Format
so that the "ogg_opus" case returns the correct upstream token ("opus")
supported by Volcengine OpenSpeech TTS v3 (or alternatively return an
error/empty string if you prefer to explicitly reject it), and ensure the switch
still falls back to "mp3" only in the default branch; refer to
mapEncodingToTTSV3Format to locate and fix the mapping.

In `@web/classic/src/index.jsx`:
- Around line 48-52: The current React.useMemo that sets semiLocale based on
i18n.language is too simplistic and forces zh_CN for any non-"en" value and
fails to handle regional codes like "en-US"; update the logic in the semiLocale
useMemo to use a lookup map (e.g., a const locales = { en: en_GB, 'en-US':
en_GB, 'zh-CN': zh_CN, 'zh-TW': zh_TW, 'fr': fr_FR, 'ja': ja_JP, 'ru': ru_RU,
'vi': vi_VN, ... } or match by normalized prefix) and select the appropriate
Semi UI locale by checking i18n.language (normalize to lower-case and prefer
exact key then prefix match like startsWith) instead of strict equality; restore
the missing locale entries (zh_TW, fr_FR, ja_JP, ru_RU, vi_VN) referenced from
`@douyinfe/semi-ui` locale source and use i18n.language as the lookup key for
semiLocale.

---

Nitpick comments:
In `@web/classic/src/i18n/i18n.js`:
- Around line 48-53: The current i18n fallbackLng defaults to ['zh-CN','en']
causing non‑Chinese locales to fall back to Chinese before English; update the
fallbackLng mapping so Chinese-first behavior is only for Chinese locales (e.g.,
keep 'zh-TW': ['zh-CN','en'] and 'zh-CN': ['en'] or similar), and make the
global/default fallback English-first (e.g., default: ['en','zh-CN']) so locales
like 'en','fr','ru','ja','vi' will prefer English before Chinese; update the
fallbackLng object accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7107a6c7-4644-4fe8-97ac-82b9d61a3e60

📥 Commits

Reviewing files that changed from the base of the PR and between b79e385 and a897e44.

📒 Files selected for processing (7)
  • constant/channel.go
  • docs/examples/relay-tts-speech-test.mjs
  • docs/local-relay-tts-test.md
  • relay/channel/volcengine/adaptor.go
  • relay/channel/volcengine/tts_v3.go
  • web/classic/src/i18n/i18n.js
  • web/classic/src/index.jsx
✅ Files skipped from review due to trivial changes (1)
  • docs/local-relay-tts-test.md

Comment on lines +66 to +77
func mapEncodingToTTSV3Format(encoding string) string {
switch encoding {
case "ogg_opus":
return "mp3"
case "pcm":
return "pcm"
case "wav":
return "wav"
default:
return "mp3"
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Volcengine OpenSpeech TTS HTTP v3 unidirectional supported audio_params format values opus

💡 Result:

For the Volcengine (BytePlus) OpenSpeech TTS HTTP v3 API, the audio_params configuration supports opus as a valid audio format [1]. In the context of Volcengine's speech synthesis services, when configuring audio output parameters (often within an audio_params object in the request body), opus is a supported encoding format [1]. Implementations typically specify this within the format field [1]. For example, configurations often use format: "opus" alongside other parameters such as sample_rate (e.g., 24000) and channels (e.g., 1) [1]. While specific documentation for the v3 HTTP interface may vary by service cluster, opus is widely recognized and used across Volcengine's speech synthesis and recognition APIs as a standard compressed audio format [2][3][1][4]. If you are integrating this into a system, ensure your request body correctly structures the audio_params object to include the format key set to "opus" [1].

Citations:


🏁 Script executed:

# Examine the file around the flagged lines and surrounding context
cat -n relay/channel/volcengine/tts_v3.go | head -150

Repository: QuantumNous/new-api

Length of output: 4863


🏁 Script executed:

# Search for calls to mapEncodingToTTSV3Format to understand how it's used
rg "mapEncodingToTTSV3Format" relay/channel/volcengine/

Repository: QuantumNous/new-api

Length of output: 327


🏁 Script executed:

# Search for mapEncoding function to understand encoding transformations
rg "mapEncoding|func.*[Ee]ncoding" relay/channel/volcengine/ -A 3

Repository: QuantumNous/new-api

Length of output: 4129


🏁 Script executed:

# Check if there are other files in the channel directory that handle encoding
fd "\.go$" relay/channel/ --exec grep -l "mapEncoding\|encoding.*format" {} \;

Repository: QuantumNous/new-api

Length of output: 166


🏁 Script executed:

# Check the responseFormatToEncodingMap and mapEncoding function in tts.go
cat -n relay/channel/volcengine/tts.go | grep -A 20 "responseFormatToEncodingMap\|func mapEncoding"

Repository: QuantumNous/new-api

Length of output: 1553


🏁 Script executed:

# Check what format is expected by Volcengine for opus in the request structure
rg "format.*opus|opus.*format" relay/channel/volcengine/ -i

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# Look at more context from the web search result about Volcengine format values
# Also check if there's any documentation in the repo about supported formats
fd "volcengine|bytedance" -t f relay/channel/ | head -20

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# Check if there are any comments or tests about format mappings
rg "mp3|pcm|wav|opus" relay/channel/volcengine/ --context 2

Repository: QuantumNous/new-api

Length of output: 2573


🏁 Script executed:

# Verify the exact format string Volcengine v3 expects for opus
# Check if there's any documentation or comments in the codebase
rg "opus|format.*value|audio_params" relay/channel/volcengine/tts_v3.go -B 2 -A 2 | head -40

Repository: QuantumNous/new-api

Length of output: 359


🏁 Script executed:

# Check if there are any issues or comments indicating this is intentional
rg "TODO|FIXME|BUG|XXX" relay/channel/volcengine/ | grep -i "opus\|format\|encoding"

Repository: QuantumNous/new-api

Length of output: 45


Fix the silent format fallback for ogg_opus in Volcengine OpenSpeech TTS v3.

Volcengine's OpenSpeech TTS v3 API supports opus as a valid audio format. Currently, mapEncodingToTTSV3Format() silently converts ogg_opus requests to mp3 (returning the same value as the default case, making the case dead code). This breaks the API contract: a caller requesting opus gets an MP3 stream with mismatched Content-Type: audio/ogg headers. Either return an error early to inform callers that this encoding is unsupported, or map ogg_opus to the correct upstream format token that Volcengine v3 accepts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/channel/volcengine/tts_v3.go` around lines 66 - 77, The function
mapEncodingToTTSV3Format currently maps "ogg_opus" to "mp3" (making that case
effectively dead code) which causes callers requesting opus to receive MP3
streams; update mapEncodingToTTSV3Format so that the "ogg_opus" case returns the
correct upstream token ("opus") supported by Volcengine OpenSpeech TTS v3 (or
alternatively return an error/empty string if you prefer to explicitly reject
it), and ensure the switch still falls back to "mp3" only in the default branch;
refer to mapEncodingToTTSV3Format to locate and fix the mapping.

Comment thread web/classic/src/index.jsx
Comment on lines +48 to +52
const semiLocale = React.useMemo(() => {
const lng = i18n.language || '';
if (lng === 'en') return en_GB;
return zh_CN;
}, [i18n.language]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Locale regression for non-Chinese / non-English UI users.

The simplified mapping forces zh_CN for every language other than en (e.g. zh-TW, fr, ru, ja, vi). Users with those UI languages will now see Semi UI components (pagination, date picker, modals, etc.) rendered in Simplified Chinese while the rest of the app is in their selected language. The earlier object-lookup form most likely covered more locales — please confirm and restore the missing mappings (e.g. zh_TW, fr_FR, ja_JP, ru_RU, vi_VN from @douyinfe/semi-ui/lib/es/locale/source/*).

Additionally, lng === 'en' is strict equality. If the language detector ever resolves to a regional code like en-US, this branch will not match. Prefer a normalized prefix check (or an explicit map).

🩹 Suggested fix
 import zh_CN from '@douyinfe/semi-ui/lib/es/locale/source/zh_CN';
 import en_GB from '@douyinfe/semi-ui/lib/es/locale/source/en_GB';
+import zh_TW from '@douyinfe/semi-ui/lib/es/locale/source/zh_TW';
+import fr_FR from '@douyinfe/semi-ui/lib/es/locale/source/fr_FR';
+import ja_JP from '@douyinfe/semi-ui/lib/es/locale/source/ja_JP';
+import ru_RU from '@douyinfe/semi-ui/lib/es/locale/source/ru_RU';
+import vi_VN from '@douyinfe/semi-ui/lib/es/locale/source/vi_VN';
@@
-  const semiLocale = React.useMemo(() => {
-    const lng = i18n.language || '';
-    if (lng === 'en') return en_GB;
-    return zh_CN;
-  }, [i18n.language]);
+  const semiLocale = React.useMemo(() => {
+    const lng = (i18n.language || '').toLowerCase();
+    if (lng.startsWith('zh-tw') || lng.startsWith('zh-hk') || lng.startsWith('zh-hant')) return zh_TW;
+    if (lng.startsWith('zh')) return zh_CN;
+    if (lng.startsWith('en')) return en_GB;
+    if (lng.startsWith('fr')) return fr_FR;
+    if (lng.startsWith('ja')) return ja_JP;
+    if (lng.startsWith('ru')) return ru_RU;
+    if (lng.startsWith('vi')) return vi_VN;
+    return en_GB;
+  }, [i18n.language]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/classic/src/index.jsx` around lines 48 - 52, The current React.useMemo
that sets semiLocale based on i18n.language is too simplistic and forces zh_CN
for any non-"en" value and fails to handle regional codes like "en-US"; update
the logic in the semiLocale useMemo to use a lookup map (e.g., a const locales =
{ en: en_GB, 'en-US': en_GB, 'zh-CN': zh_CN, 'zh-TW': zh_TW, 'fr': fr_FR, 'ja':
ja_JP, 'ru': ru_RU, 'vi': vi_VN, ... } or match by normalized prefix) and select
the appropriate Semi UI locale by checking i18n.language (normalize to
lower-case and prefer exact key then prefix match like startsWith) instead of
strict equality; restore the missing locale entries (zh_TW, fr_FR, ja_JP, ru_RU,
vi_VN) referenced from `@douyinfe/semi-ui` locale source and use i18n.language as
the lookup key for semiLocale.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
relay/channel/task/apiwenhao/adaptor.go (1)

258-273: 💤 Low value

Remove redundant extractMediaURL call.

The extractMediaURL call at line 266 is redundant—it was already called at line 259 with the same raw input. If line 259 returned an empty URL, line 266 will also return empty, making the inner block unreachable.

♻️ Suggested simplification
 	case resTypeSuccess:
 		if url := extractMediaURL(raw); url != "" {
 			taskResult.Status = model.TaskStatusSuccess
 			taskResult.Progress = "100%"
 			taskResult.Url = url
 			return &taskResult, nil
 		}
-		if status := strings.ToLower(strings.TrimSpace(gjson.Get(raw, "data.result.status").String())); status == "completed" {
-			if url := extractMediaURL(raw); url != "" {
-				taskResult.Status = model.TaskStatusSuccess
-				taskResult.Progress = "100%"
-				taskResult.Url = url
-				return &taskResult, nil
-			}
-		}
+		// res_type=success but no media URL yet - fall through to in-progress handling
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/channel/task/apiwenhao/adaptor.go` around lines 258 - 273, In the
resTypeSuccess branch replace the duplicate extractMediaURL(raw) call by calling
extractMediaURL(raw) once into a local variable (e.g., url :=
extractMediaURL(raw)) at the top of that case and reuse that url when setting
taskResult.Status, Progress and Url; update the inner status=="completed" check
to reference the same url variable instead of calling extractMediaURL again
(symbols: extractMediaURL, resTypeSuccess case, taskResult).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@controller/task_video.go`:
- Around line 83-87: The call to adaptor.FetchTask currently always passes
task.Properties.UpstreamModelName into BuildTaskFetchBody which can be empty for
older tasks; update the code around the adaptor.FetchTask / BuildTaskFetchBody
invocation to use a backward-compatible fallback: if
task.Properties.UpstreamModelName is empty, pass the saved request model (e.g.,
task.RequestModel or the persisted request-model field on the task) instead, and
if neither exists pass an empty/omitted model value so BuildTaskFetchBody
generates a payload without the model field. Ensure the conditional checks
reference task.Properties.UpstreamModelName and the saved request model symbol
you have (e.g., task.RequestModel) so the fetch payload is complete for older
queued jobs.

In `@setting/billing_setting/upstream_cost_multiplier_test.go`:
- Around line 7-15: The test mutates global map
billingSetting.UpstreamCostMultiplier and currently calls delete(...) at the end
which will be skipped on failure; fix by registering a t.Cleanup callback
immediately after setting billingSetting.UpstreamCostMultiplier["test-model"] =
7.3 (use t.Cleanup to delete that key) so the map entry is removed regardless of
test failures, then proceed to call ResolveUpstreamCostMultiplier and the
assertions as before.

---

Nitpick comments:
In `@relay/channel/task/apiwenhao/adaptor.go`:
- Around line 258-273: In the resTypeSuccess branch replace the duplicate
extractMediaURL(raw) call by calling extractMediaURL(raw) once into a local
variable (e.g., url := extractMediaURL(raw)) at the top of that case and reuse
that url when setting taskResult.Status, Progress and Url; update the inner
status=="completed" check to reference the same url variable instead of calling
extractMediaURL again (symbols: extractMediaURL, resTypeSuccess case,
taskResult).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 18031daf-b5af-413e-bc15-a192bb771489

📥 Commits

Reviewing files that changed from the base of the PR and between a897e44 and 4812ced.

📒 Files selected for processing (40)
  • constant/channel.go
  • controller/ratio_sync.go
  • controller/relay.go
  • controller/task_video.go
  • model/pricing.go
  • model/task.go
  • relay/channel/task/apimart/adaptor.go
  • relay/channel/task/apimart/adaptor_test.go
  • relay/channel/task/apiwenhao/adaptor.go
  • relay/channel/task/apiwenhao/adaptor_test.go
  • relay/channel/task/taskcommon/usd_billing.go
  • relay/channel/task/taskcommon/usd_billing_test.go
  • relay/common/relay_info.go
  • relay/common/relay_utils.go
  • relay/common/task_submit_image.go
  • relay/common/task_submit_image_test.go
  • relay/relay_adaptor.go
  • service/task_billing.go
  • service/task_billing_test.go
  • service/task_polling.go
  • setting/billing_setting/tiered_billing.go
  • setting/billing_setting/upstream_cost_multiplier_test.go
  • web/classic/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx
  • web/classic/src/components/table/model-pricing/modal/components/ModelPricingTable.jsx
  • web/classic/src/components/table/model-pricing/view/card/PricingCardView.jsx
  • web/classic/src/components/table/model-pricing/view/table/PricingTableColumns.jsx
  • web/classic/src/constants/channel.constants.js
  • web/classic/src/helpers/render.jsx
  • web/classic/src/helpers/utils.jsx
  • web/classic/src/pages/Setting/Ratio/components/ModelPricingEditor.jsx
  • web/classic/src/pages/Setting/Ratio/hooks/useModelPricingEditorState.js
  • web/default/src/features/channels/constants.ts
  • web/default/src/features/channels/lib/channel-type-config.ts
  • web/default/src/features/channels/lib/channel-utils.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh.json
✅ Files skipped from review due to trivial changes (7)
  • web/default/src/features/channels/lib/channel-utils.ts
  • web/default/src/i18n/locales/zh.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/ja.json
  • relay/channel/task/taskcommon/usd_billing_test.go
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/en.json

Comment thread controller/task_video.go
Comment on lines +83 to +87
resp, err := adaptor.FetchTask(baseURL, key, relaycommon.BuildTaskFetchBody(
task.GetUpstreamTaskID(),
task.Action,
task.Properties.UpstreamModelName,
), proxy)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a backward-compatible fallback for UpstreamModelName.

This polling path now depends on task.Properties.UpstreamModelName. If that field is empty for older queued tasks, or for any submit path that never persisted it, the post-deploy fetch payload becomes incomplete and those video jobs can stop updating. Please fall back to the saved request model, or omit the model field when it is blank.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/task_video.go` around lines 83 - 87, The call to adaptor.FetchTask
currently always passes task.Properties.UpstreamModelName into
BuildTaskFetchBody which can be empty for older tasks; update the code around
the adaptor.FetchTask / BuildTaskFetchBody invocation to use a
backward-compatible fallback: if task.Properties.UpstreamModelName is empty,
pass the saved request model (e.g., task.RequestModel or the persisted
request-model field on the task) instead, and if neither exists pass an
empty/omitted model value so BuildTaskFetchBody generates a payload without the
model field. Ensure the conditional checks reference
task.Properties.UpstreamModelName and the saved request model symbol you have
(e.g., task.RequestModel) so the fetch payload is complete for older queued
jobs.

Comment on lines +7 to +15
billingSetting.UpstreamCostMultiplier["test-model"] = 7.3
if got := ResolveUpstreamCostMultiplier("test-model"); got != 7.3 {
t.Fatalf("got %v want 7.3", got)
}
if got := ResolveUpstreamCostMultiplier("missing"); got != 1 {
t.Fatalf("missing should default to 1, got %v", got)
}
delete(billingSetting.UpstreamCostMultiplier, "test-model")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use t.Cleanup to guarantee test-state rollback.

The test mutates global state, but cleanup at Line 14 is skipped if an earlier assertion fails. Register cleanup immediately to keep tests isolated.

Suggested fix
 func TestResolveUpstreamCostMultiplier(t *testing.T) {
 	ensureBillingSettingMaps()
 	billingSetting.UpstreamCostMultiplier["test-model"] = 7.3
+	t.Cleanup(func() {
+		delete(billingSetting.UpstreamCostMultiplier, "test-model")
+	})
 	if got := ResolveUpstreamCostMultiplier("test-model"); got != 7.3 {
 		t.Fatalf("got %v want 7.3", got)
 	}
 	if got := ResolveUpstreamCostMultiplier("missing"); got != 1 {
 		t.Fatalf("missing should default to 1, got %v", got)
 	}
-	delete(billingSetting.UpstreamCostMultiplier, "test-model")
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@setting/billing_setting/upstream_cost_multiplier_test.go` around lines 7 -
15, The test mutates global map billingSetting.UpstreamCostMultiplier and
currently calls delete(...) at the end which will be skipped on failure; fix by
registering a t.Cleanup callback immediately after setting
billingSetting.UpstreamCostMultiplier["test-model"] = 7.3 (use t.Cleanup to
delete that key) so the map entry is removed regardless of test failures, then
proceed to call ResolveUpstreamCostMultiplier and the assertions as before.

xuanyustudio and others added 30 commits August 11, 2026 21:10
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Capture confirmed design for submit + polling failover across video channels with ordered list override and billing rules.

Co-authored-by: Cursor <cursoragent@cursor.com>
Break the confirmed design into sequenced tasks covering classification, settings, submit snapshot, orchestrator, cross-channel recreate, and default UI.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add TaskPrivateData failover fields and shared audit vs upstream-balance classification in taskcommon; mao same-channel path uses the shared helper.

Co-authored-by: Cursor <cursoragent@cursor.com>
Register TaskSameChannelMaxRetries, TaskCrossChannelFailoverEnabled, and TaskModelChannelOrder with getters for ordered channel overrides.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add ListSatisfiedChannelIDs and ResolveTaskFailoverChannelIDs so optional model channel order can override Priority snapshots.

Co-authored-by: Cursor <cursoragent@cursor.com>
Persist client/upstream bodies and candidate channel IDs on create; non-auto groups retry along ordered failover list when configured.

Co-authored-by: Cursor <cursoragent@cursor.com>
Route polling FAILURE through HandleAsyncTaskFailure; same-channel uses TaskAsyncFailureResubmitter; cross-channel hooks TaskFailoverRecreateFunc.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add SkipPreConsume recreate path from client_request_body and wire TaskFailoverRecreateFunc in main.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use PrivateData/SameChannelMaxRetries for same-channel caps and progress labels; keep balance blocking same-channel only.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add same-channel retry / cross-channel failover controls and JSON editor for TaskModelChannelOrder under operations settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add same-channel / cross-channel toggles in general settings and a drag-and-drop model channel order editor under operation settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant