feat(channel): add HappyHorse (百炼快乐马) DashScope video generation channel + playground UI - #5124
feat(channel): add HappyHorse (百炼快乐马) DashScope video generation channel + playground UI#5124tears710 wants to merge 6 commits into
Conversation
…mNous#58) - Add ChannelTypeHappyHorse = 58 with DashScope base URL - Implement TaskAdaptor: supports t2v/i2v/r2v/video-edit models - Use media[] array input format (differs from Ali's flat img_url fields) - video-edit model uses video_url type; others use image_url type - Default params: resolution=720P, duration=5s, prompt_extend=true - Billing via seconds multiplier on base model ratio - Add default model prices for 4 happyhorse-1.0-* models - Register adaptor in GetTaskAdaptor switch - Add frontend channel type 58 with Tongyi icon 🤖 Generated with [Qoder][https://qoder.com]
Task-type channel adaptors (e.g. HappyHorse, Kling) are not mapped to a standard APIType, so their GetModelList() was never called during init(), leaving them absent from the channel preset model selector. Now when ChannelType2APIType returns !success, fall back to GetTaskAdaptor and register its models into both channelId2Models and openAIModels so the frontend "填入相关模型" shortcut works correctly. 🤖 Generated with [Qoder][https://qoder.com]
Add getModelsByChannelType() API call to fetch /api/models map, and update basicModels useMemo to prefer channel-type-specific models when available, so HappyHorse (type 58) only fills its 4 models instead of all global models. 🤖 Generated with [Qoder][https://qoder.com]
- Add adaptor_test.go covering ValidateRequestAndSetAction, BuildRequestURL,
BuildRequestHeader, and ParseTaskResult for happyhorse-1.0-{t2v,i2v,r2v,video-edit}.
- Tighten adaptor request validation: reject empty media[], invalid types,
and surface DashScope error code/message via ParseTaskResult.
Verified via: go test ./relay/channel/task/happyhorse/...
…annels
- Add video task queue components (video-input-form, video-player,
video-task-item, video-task-queue) for submitting and tracking
asynchronous video generation requests.
- Add use-video-task hook to drive task lifecycle (submit, poll, retry,
cancel) and surface DashScope-style status/error to the UI.
- Wire playground entry, api.ts, constants, hooks index, and types to
expose the video task surface alongside existing image/chat modes.
Targets HappyHorse (channel type 58, happyhorse-1.0-{t2v,i2v,r2v,video-edit})
and other Task adaptors using the async video pattern.
WalkthroughAdds HappyHorse video-generation channel (type 58): backend channel registration, a full HappyHorse TaskAdaptor with tests and model discovery changes, pricing entries, and a complete frontend playground (APIs, types, hook, components, i18n) enabling video submission, polling, queueing, and preview. ChangesBackend: Channel Registration & Task Adaptor
Web Frontend: Channels & Playground
Sequence Diagram(s)sequenceDiagram
participant User
participant VideoInputForm
participant API
participant useVideoTask
participant HappyHorseBackend
User->>VideoInputForm: choose model, prompt, media, token
VideoInputForm->>API: fetchTokenKey(tokenId)
API-->>VideoInputForm: unmasked API key
VideoInputForm->>useVideoTask: submitTask(request, apiKey)
useVideoTask->>API: submitVideoGeneration(request, apiKey)
API->>HappyHorseBackend: POST /tasks (Bearer)
HappyHorseBackend-->>API: { task_id, ... }
API-->>useVideoTask: VideoTaskResponse
useVideoTask->>useVideoTask: create task, store apiKey, start polling
loop polling
useVideoTask->>API: fetchVideoTaskStatus(taskId, apiKey)
API->>HappyHorseBackend: GET /tasks/{task_id}
HappyHorseBackend-->>API: status/progress/(videoUrl)
API-->>useVideoTask: VideoTaskResponse
useVideoTask->>useVideoTask: updateTask(status, progress, videoUrl)
end
useVideoTask-->>VideoInputForm: task completed (videoUrl available)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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: 13
🤖 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/model.go`:
- Around line 100-119: The code appends task-adaptor models into openAIModels
and channelId2Models after openAIModelsMap is initialized, so lookups using
openAIModelsMap (e.g., RetrieveModel) will miss these new entries; update the
code that handles task adaptor models (taskAdaptor.Init,
taskAdaptor.GetModelList, channelId2Models population and openAIModels appends)
to also insert or refresh entries in openAIModelsMap (the map created at
initialization) for each modelName (use the same model object shape used when
building openAIModelsMap) so subsequent calls like RetrieveModel see the new
task-adaptor models.
In `@relay/channel/task/happyhorse/adaptor.go`:
- Around line 110-156: Validate duration and resolution locally inside
ValidateRequestAndSetAction: inspect taskReq (returned by
relaycommon.GetTaskRequest) for duration and resolution fields (e.g., in
taskReq.Metadata or explicit fields) and verify they fall within allowed
ranges/formats (e.g., numeric, positive, and within agreed min/max values); if
invalid, return a dto.TaskError with an appropriate Code (e.g.,
"invalid_duration" / "invalid_resolution"), Message and http.StatusBadRequest
instead of forwarding to upstream; ensure the same validation logic covers the
code paths for image/video models (those checked by strings.Contains(model,
...)) so both the video-edit and i2v/r2v branches reject out-of-range values
locally.
- Around line 302-313: EstimateBilling currently reads taskReq.Duration directly
(via relaycommon.GetTaskRequest) and ignores the metadata-based override applied
during request assembly; update TaskAdaptor.EstimateBilling to compute the
effective duration the same way the request assembly does (i.e., apply the
metadata override logic used elsewhere when building the final task request) and
use that effective duration (falling back to the existing default of 5 seconds)
when returning the "seconds" billing map so billing matches the assembled
request.
In
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`:
- Around line 417-424: The early-return for empty allModelsList in the useMemo
for basicModels prevents using server-provided channel-type-specific models;
reorder the logic in the useMemo so you first check channelTypeModelsData
(channelTypeMap) and return specific = channelTypeMap[String(currentType)] if
present and non-empty, and only then fall back to return [] when allModelsList
is empty; apply the same reorder/fix to the other similar guard occurrence that
mirrors this pattern (the other useMemo/guard around related-model fill).
In `@web/default/src/features/playground/components/video-input-form.tsx`:
- Around line 202-207: The placeholder currently uses a nested ternary
(isLoadingTokens ? ... : tokens.length === 0 ? ... : ...) which violates the
nested-ternary rule; replace it with a small helper function or an if-else
inside the VideoInputForm component (e.g., add a computePlaceholder or
getPlaceholderText function) that uses isLoadingTokens, tokens (tokens.length),
and t to return the correct string ("Loading...", "No API keys available", or
"Select API key"), then use that helper for the placeholder prop.
- Around line 70-72: selectedModel can stay set to a fallback value that isn't
present in availableModels when models arrive asynchronously; add a useEffect
that watches availableModels and resets selectedModel via setSelectedModel
whenever the current selectedModel is not found in availableModels (set it to
availableModels[0].model if present, otherwise to HAPPYHORSE_MODELS[0].model).
Implement the same pattern for the other similar useState initialized around the
107–112 region so both state values stay in sync with async model availability.
- Around line 157-171: The submit flow allows requests for models with
modelConfig.requiresVideo or modelConfig.requiresImage to proceed when videoUrl
or imageUrls are empty; add explicit client-side validation in the submit
handler and form state so the submit is blocked and the submit button disabled
when required references are missing. Concretely, check
modelConfig.requiresVideo && videoUrl.trim() and modelConfig.requiresImage &&
imageUrls.filter(u => u.trim() !== '').length > 0 before building req (the block
around req.input_reference / req.images) and return/early-fail the submit if
these conditions are not met; also set the submit button's disabled prop (and
any form-level isValid flag) based on the same checks so users cannot click
submit (apply the same validation where the other related code exists around the
image/video handling at the other occurrence referenced by lines 416-418).
Ensure error state messages are set so the UI can show why submit is disabled.
In `@web/default/src/features/playground/components/video-player.tsx`:
- Around line 64-100: The three icon-only Button components (the copy button
using copied/handleCopy with CopyIcon/CheckIcon, the fullscreen button using
handleFullscreen with Maximize2Icon, and the close button using onClose with
XIcon) rely only on title for accessible names; add explicit aria-label
attributes to each Button (e.g., aria-label="Copy URL" on the copy button,
aria-label="Fullscreen" on the fullscreen button, aria-label="Close" on the
close button) so screen readers receive a reliable name while preserving
existing title, size, variant and event handlers.
In `@web/default/src/features/playground/components/video-task-queue.tsx`:
- Around line 145-157: The icon-only clear button (the <Button> with props
title={t('Clear finished tasks')} and onClick calling onClearFinished, rendering
<Trash2Icon>) needs an explicit accessible name: add an aria-label prop (e.g.,
aria-label={t('Clear finished tasks')}) to the Button so screen readers get a
clear name instead of relying on title; keep the existing title if desired but
ensure aria-label is present for accessibility.
In `@web/default/src/features/playground/constants.ts`:
- Around line 110-154: HAPPYHORSE_MODELS currently contains hardcoded
user-facing labels (label fields) and VIDEO_MODEL_TYPE_LABELS maps types to
hardcoded strings; change those label values to i18n keys (e.g.
'playground.model.text_to_video', 'playground.model.label.t2v', etc.) and update
VIDEO_MODEL_TYPE_LABELS to return i18n keys instead of plain text, then ensure
consumers render with the i18n function (e.g. use t(config.label) and
t(VIDEO_MODEL_TYPE_LABELS[type])). Update references to the symbols
HAPPYHORSE_MODELS, VIDEO_MODEL_TYPE_LABELS and any usages that render
config.label to call t(...) so all user-facing strings go through i18n.
In `@web/default/src/features/playground/hooks/use-video-task.ts`:
- Around line 103-106: Replace hardcoded English toast strings with i18next
translations: import { t } from 'i18next' at the top of use-video-task.ts, then
change calls like toast.success('Video generation completed') and
toast.error(errorMsg ?? 'Video generation failed') to use t('...') keys (and
fall back to errorMsg when appropriate), and update the other hardcoded
instances noted (around lines 137-147, 173, 193) to use t(...) as well; ensure
keys are descriptive (e.g., 'video.generationCompleted',
'video.generationFailed') and preserve the conditional use of errorMsg when
present.
- Around line 81-116: The poll function can start overlapping
fetchVideoTaskStatus calls; add an in-flight guard (e.g., a per-id boolean like
pollingInFlight.current[id]) to skip starting a new poll while a previous one is
running in poll inside use-video-task.ts: before calling fetchVideoTaskStatus
check and set the guard, and clear it in finally (or after stopPolling) so no
concurrent requests run; ensure stopPolling clears both
pollingTimers.current[id] and the in-flight flag and keep existing updateTask,
toast, and stopPolling logic unchanged.
In `@web/default/src/features/playground/index.tsx`:
- Around line 279-283: The previewTask state can point to a task that gets
deleted or cleared, leaving VideoPlayer showing stale content; update the
task-deletion and "clear finished" handlers (the functions that remove tasks and
the handler that clears finished tasks) to call setPreviewTask(null) when the
affected task matches previewTask (e.g., compare IDs) or when clearing finished
tasks remove the currently previewed task; ensure this logic runs inside the
existing delete/clear handlers so previewTask is unset and VideoPlayer is closed
when its task is removed.
🪄 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: 87780884-7436-4b77-957e-f8f5991b4b4b
📒 Files selected for processing (21)
constant/channel.gocontroller/model.gorelay/channel/task/happyhorse/adaptor.gorelay/channel/task/happyhorse/adaptor_test.gorelay/channel/task/happyhorse/constants.gorelay/relay_adaptor.gosetting/ratio_setting/model_ratio.goweb/default/src/features/channels/api.tsweb/default/src/features/channels/components/drawers/channel-mutate-drawer.tsxweb/default/src/features/channels/constants.tsweb/default/src/features/channels/lib/channel-utils.tsweb/default/src/features/playground/api.tsweb/default/src/features/playground/components/video-input-form.tsxweb/default/src/features/playground/components/video-player.tsxweb/default/src/features/playground/components/video-task-item.tsxweb/default/src/features/playground/components/video-task-queue.tsxweb/default/src/features/playground/constants.tsweb/default/src/features/playground/hooks/index.tsweb/default/src/features/playground/hooks/use-video-task.tsweb/default/src/features/playground/index.tsxweb/default/src/features/playground/types.ts
| export const HAPPYHORSE_MODELS: VideoModelConfig[] = [ | ||
| { | ||
| model: 'happyhorse-1.0-t2v', | ||
| label: '文生视频 (Text-to-Video)', | ||
| type: 'text-to-video', | ||
| requiresImage: false, | ||
| requiresVideo: false, | ||
| supportedSizes: ['720P', '1080P'], | ||
| durationRange: [2, 15], | ||
| }, | ||
| { | ||
| model: 'happyhorse-1.0-i2v', | ||
| label: '图生视频 (Image-to-Video)', | ||
| type: 'image-to-video', | ||
| requiresImage: true, | ||
| requiresVideo: false, | ||
| supportedSizes: ['720P', '1080P'], | ||
| durationRange: [2, 15], | ||
| }, | ||
| { | ||
| model: 'happyhorse-1.0-r2v', | ||
| label: '首尾帧生视频 (Reference-to-Video)', | ||
| type: 'reference-to-video', | ||
| requiresImage: true, | ||
| requiresVideo: false, | ||
| supportedSizes: ['720P', '1080P'], | ||
| durationRange: [2, 15], | ||
| }, | ||
| { | ||
| model: 'happyhorse-1.0-video-edit', | ||
| label: '视频编辑 (Video Edit)', | ||
| type: 'video-edit', | ||
| requiresImage: false, | ||
| requiresVideo: true, | ||
| supportedSizes: ['720P', '1080P'], | ||
| durationRange: [2, 15], | ||
| }, | ||
| ] | ||
|
|
||
| export const VIDEO_MODEL_TYPE_LABELS: Record<VideoModelType, string> = { | ||
| 'text-to-video': 'T2V', | ||
| 'image-to-video': 'I2V', | ||
| 'reference-to-video': 'R2V', | ||
| 'video-edit': 'Edit', | ||
| } |
There was a problem hiding this comment.
New video labels are hardcoded instead of i18n keys.
Lines 113, 122, 131, 140 and Lines 150-153 introduce user-facing text directly in constants. This bypasses translation flow and makes localization inconsistent.
Suggested direction
export const HAPPYHORSE_MODELS: VideoModelConfig[] = [
{
model: 'happyhorse-1.0-t2v',
- label: '文生视频 (Text-to-Video)',
+ label: 'playground.video.model.text_to_video',
...
export const VIDEO_MODEL_TYPE_LABELS: Record<VideoModelType, string> = {
- 'text-to-video': 'T2V',
+ 'text-to-video': 'playground.video.type.t2v',Then render with t(config.label) / t(VIDEO_MODEL_TYPE_LABELS[type]).
As per coding guidelines, “All user-facing text content must support i18n using the t() function” and in constants.ts labels should be i18n keys rendered with t(...).
🤖 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/default/src/features/playground/constants.ts` around lines 110 - 154,
HAPPYHORSE_MODELS currently contains hardcoded user-facing labels (label fields)
and VIDEO_MODEL_TYPE_LABELS maps types to hardcoded strings; change those label
values to i18n keys (e.g. 'playground.model.text_to_video',
'playground.model.label.t2v', etc.) and update VIDEO_MODEL_TYPE_LABELS to return
i18n keys instead of plain text, then ensure consumers render with the i18n
function (e.g. use t(config.label) and t(VIDEO_MODEL_TYPE_LABELS[type])). Update
references to the symbols HAPPYHORSE_MODELS, VIDEO_MODEL_TYPE_LABELS and any
usages that render config.label to call t(...) so all user-facing strings go
through i18n.
- Move openAIModelsMap rebuild after UniqBy so task-adaptor models are findable - Add duration (2-15s) and resolution (720P/1080P) local validation in adaptor - Add metadata duration validation to prevent billing override abuse - Normalize resolution value back to taskReq.Size after validation - Align EstimateBilling with metadata override precedence - Fix basicModels useMemo: check channelTypeModelsData before allModelsList guard - Sync selectedModel with async availableModels via useEffect - Add isSubmitting guard to handleSubmit to prevent double submission - Add hasRequiredMedia client-side validation before submit - Add aria-label to all icon-only Buttons (video-player, video-task-queue) - Fix stale previewTask when task is removed or cleared - Add in-flight guard to polling loop to prevent concurrent requests - Convert all hardcoded toast strings to i18n t() calls - Convert HAPPYHORSE_MODELS labels to i18n keys - Add media-drop-zone.tsx component (drag/paste/URL upload) - Fix URL.createObjectURL memory leak with prevUrlRef + cleanup - Add 15 new i18n keys to all 6 locale files (en/zh/fr/ja/ru/vi)
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/default/src/features/playground/components/video-player.tsx (1)
77-80:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAdd
aria-hidden='true'to decorative icons inside labeled buttons.The button icons are decorative because each button already has an
aria-label. Screen readers should skip these icons to avoid redundant announcements.Add aria-hidden to all button icons
{copied ? ( - <CheckIcon className='size-3.5' /> + <CheckIcon className='size-3.5' aria-hidden='true' /> ) : ( - <CopyIcon className='size-3.5' /> + <CopyIcon className='size-3.5' aria-hidden='true' /> )}Apply the same to Maximize2Icon (line 91) and XIcon (line 102):
- <Maximize2Icon className='size-3.5' /> + <Maximize2Icon className='size-3.5' aria-hidden='true' />- <XIcon className='size-3.5' /> + <XIcon className='size-3.5' aria-hidden='true' />As per coding guidelines: "add
aria-hidden='true'to decorative icons."Also applies to: 91-91, 102-102
🤖 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/default/src/features/playground/components/video-player.tsx` around lines 77 - 80, The button icons are decorative and should be hidden from screen readers; update the JSX for the icon components used inside labeled buttons—CheckIcon, CopyIcon, Maximize2Icon, and XIcon—to include aria-hidden='true' (e.g., <CheckIcon aria-hidden='true' />) so screen readers do not redundantly announce them while preserving the existing aria-label on the buttons.
🧹 Nitpick comments (2)
web/default/src/features/playground/components/media-drop-zone.tsx (1)
44-54: ⚡ Quick winConsider validating file type matches the
acceptprop.When users drop or paste files, the component does not verify that the file's MIME type matches the
acceptprop ('image' or 'video'). A user could drop a video file whenaccept='image'is set, leading to unexpected behavior or errors downstream.Add file type validation
const handleFile = useCallback( (file: File) => { + const expectedType = isImage ? 'image' : 'video' + if (!file.type.startsWith(`${expectedType}/`)) { + return // or show an error message + } if (prevUrlRef.current?.startsWith('blob:')) { URL.revokeObjectURL(prevUrlRef.current) }Also applies to: 65-73, 75-96
🤖 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/default/src/features/playground/components/media-drop-zone.tsx` around lines 44 - 54, The file handlers (e.g., handleFile) currently create object URLs for any dropped/pasted File without checking the component's accept prop; add MIME validation so the File's type matches accept ('image' => type.startsWith('image/'), 'video' => type.startsWith('video/')) before creating the URL and calling onChange. If the file does not match, revoke any created URL, do not call onChange, and surface the rejection (e.g., call an onError/onInvalid callback or return early) in the handlers that process user input (handleFile and the corresponding drop/paste handlers) so only accepted file types are accepted.web/default/src/i18n/locales/en.json (1)
2013-2013: ⚡ Quick winAvoid introducing parallel key variants for the same concept.
Line 2013 (
Image-to-Video), Line 3222 (Reference-to-Video), and Line 3906 (Text-to-Video) introduce hyphenated labels while non-hyphen variants already exist, which increases translation drift and lookup inconsistency. Please standardize on one canonical key pattern for each label.As per coding guidelines: “Use hierarchical and semantically clear translation key names such as
dashboard.overview.titleand maintain naming consistency.”Also applies to: 3222-3222, 3906-3906
🤖 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/default/src/i18n/locales/en.json` at line 2013, The JSON introduces hyphenated duplicate keys ("Image-to-Video", "Reference-to-Video", "Text-to-Video"); remove these hyphenated entries and consolidate to the existing canonical keys (the non-hyphen variants already present) so there is a single translation key per concept, then update any code or lookup references that use "Image-to-Video", "Reference-to-Video", or "Text-to-Video" to use the canonical non-hyphen keys and run your i18n validation/extraction to ensure no missing keys remain.
🤖 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/task/happyhorse/adaptor.go`:
- Around line 129-166: The metadata resolution (HappyHorseMetadata.Resolution)
is not validated, allowing overrides to bypass local checks in
convertToHappyHorseRequest; after unmarshaling metadata with
taskcommon.UnmarshalMetadata(&meta) in the validation block, normalize
meta.Resolution (strings.TrimSpace + strings.ToUpper, append "P" if missing) and
ensure it equals "720P" or "1080P"; if invalid, return a dto.TaskError with Code
"invalid_resolution" and the same StatusCode/LocalError semantics used for
taskReq.Size, and update the metadata/resolution in taskReq or meta so
downstream sees the normalized value.
In `@web/default/src/features/playground/components/media-drop-zone.tsx`:
- Line 164: The decorative icons XIcon, ImageIcon, and VideoIcon are missing
aria-hidden and should be ignored by screen readers; update the JSX where XIcon,
ImageIcon, and VideoIcon are rendered in media-drop-zone.tsx to include
aria-hidden="true" on each icon element (ensure the attribute is added to the
icon props/element so screen readers skip these decorative icons while leaving
surrounding button/label text unchanged).
- Line 180: Remove the unsafe "as any" assertion on the ref usage (ref={inputRef
as any}) and pass the properly typed ref to the Input component (use
ref={inputRef}); if TypeScript reports an incompatibility, update the Input
component's ref prop type to accept React.RefObject<HTMLInputElement> (or
appropriate element) or switch to a callback ref that forwards the inputRef
value, ensuring inputRef remains typed as React.RefObject<HTMLInputElement>.
- Around line 126-140: The drop zone is focusable (role='button', tabIndex={0})
but missing keyboard handlers; add an onKeyDown on the same element to listen
for Enter and Space and call inputRef.current?.click() to open the file picker
(for Space also preventDefault to avoid page scroll), and keep existing
onClick/onDrop behavior; use the existing identifiers inputRef, setIsDragging,
handleDrop and value when locating the element to update.
In `@web/default/src/i18n/locales/fr.json`:
- Around line 3867-3868: The French translations for the keys "Task failed: API
Key invalid" and "Task failed: cannot restore key" include an undeclared
interpolation `{{prompt}}...` which can render unresolved placeholders; update
the values for those keys in fr.json to remove the `{{prompt}}...` fragment so
they match the source keys (e.g., change "Tâche \"{{prompt}}...\" échouée : clé
API invalide" and "Tâche \"{{prompt}}...\" échouée : impossible de restaurer la
clé" to versions without `{{prompt}}...`), ensuring no undeclared placeholders
remain.
In `@web/default/src/i18n/locales/ru.json`:
- Around line 3867-3868: The translations for the keys "Task failed: API Key
invalid" and "Task failed: cannot restore key" include an unresolved
interpolation placeholder `{{prompt}}...`; remove that placeholder and replace
the values with plain static Russian messages (e.g., 'Задача не выполнена:
недействительный ключ API' and 'Задача не выполнена: не удалось восстановить
ключ') so no raw interpolation tokens remain in the ru.json entries for those
keys.
In `@web/default/src/i18n/locales/vi.json`:
- Around line 3867-3868: Remove the unmatched interpolation token from the
Vietnamese translations for the keys "Task failed: API Key invalid" and "Task
failed: cannot restore key" in vi.json: replace the values that include
'{{prompt}}...' with plain messages that match the source (no interpolation
placeholders) so the locale stays parity with other languages and no raw
placeholders render in the UI.
---
Outside diff comments:
In `@web/default/src/features/playground/components/video-player.tsx`:
- Around line 77-80: The button icons are decorative and should be hidden from
screen readers; update the JSX for the icon components used inside labeled
buttons—CheckIcon, CopyIcon, Maximize2Icon, and XIcon—to include
aria-hidden='true' (e.g., <CheckIcon aria-hidden='true' />) so screen readers do
not redundantly announce them while preserving the existing aria-label on the
buttons.
---
Nitpick comments:
In `@web/default/src/features/playground/components/media-drop-zone.tsx`:
- Around line 44-54: The file handlers (e.g., handleFile) currently create
object URLs for any dropped/pasted File without checking the component's accept
prop; add MIME validation so the File's type matches accept ('image' =>
type.startsWith('image/'), 'video' => type.startsWith('video/')) before creating
the URL and calling onChange. If the file does not match, revoke any created
URL, do not call onChange, and surface the rejection (e.g., call an
onError/onInvalid callback or return early) in the handlers that process user
input (handleFile and the corresponding drop/paste handlers) so only accepted
file types are accepted.
In `@web/default/src/i18n/locales/en.json`:
- Line 2013: The JSON introduces hyphenated duplicate keys ("Image-to-Video",
"Reference-to-Video", "Text-to-Video"); remove these hyphenated entries and
consolidate to the existing canonical keys (the non-hyphen variants already
present) so there is a single translation key per concept, then update any code
or lookup references that use "Image-to-Video", "Reference-to-Video", or
"Text-to-Video" to use the canonical non-hyphen keys and run your i18n
validation/extraction to ensure no missing keys remain.
🪄 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: a490422d-73bf-4ca6-a004-3f66eda260d2
⛔ Files ignored due to path filters (1)
web/default/bun.lockis excluded by!**/*.lock
📒 Files selected for processing (16)
controller/model.gorelay/channel/task/happyhorse/adaptor.goweb/default/src/features/channels/components/drawers/channel-mutate-drawer.tsxweb/default/src/features/playground/components/media-drop-zone.tsxweb/default/src/features/playground/components/video-input-form.tsxweb/default/src/features/playground/components/video-player.tsxweb/default/src/features/playground/components/video-task-queue.tsxweb/default/src/features/playground/constants.tsweb/default/src/features/playground/hooks/use-video-task.tsweb/default/src/features/playground/index.tsxweb/default/src/i18n/locales/en.jsonweb/default/src/i18n/locales/fr.jsonweb/default/src/i18n/locales/ja.jsonweb/default/src/i18n/locales/ru.jsonweb/default/src/i18n/locales/vi.jsonweb/default/src/i18n/locales/zh.json
✅ Files skipped from review due to trivial changes (1)
- web/default/src/i18n/locales/ja.json
🚧 Files skipped from review as they are similar to previous changes (7)
- web/default/src/features/playground/constants.ts
- web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
- controller/model.go
- web/default/src/features/playground/index.tsx
- web/default/src/features/playground/components/video-input-form.tsx
- web/default/src/features/playground/hooks/use-video-task.ts
- web/default/src/features/playground/components/video-task-queue.tsx
| // Validate duration and resolution locally before forwarding to upstream | ||
| if taskReq.Duration > 0 && (taskReq.Duration < 2 || taskReq.Duration > 15) { | ||
| return &dto.TaskError{ | ||
| Code: "invalid_duration", | ||
| Message: "duration must be between 2 and 15 seconds", | ||
| StatusCode: http.StatusBadRequest, | ||
| LocalError: true, | ||
| } | ||
| } | ||
| if taskReq.Size != "" { | ||
| resolution := strings.ToUpper(strings.TrimSpace(taskReq.Size)) | ||
| if !strings.HasSuffix(resolution, "P") { | ||
| resolution += "P" | ||
| } | ||
| if resolution != "720P" && resolution != "1080P" { | ||
| return &dto.TaskError{ | ||
| Code: "invalid_resolution", | ||
| Message: "resolution must be 720P or 1080P", | ||
| StatusCode: http.StatusBadRequest, | ||
| LocalError: true, | ||
| } | ||
| } | ||
| taskReq.Size = resolution // normalize for downstream consumers | ||
| } | ||
| // Validate metadata duration (same override path as EstimateBilling / request assembly) | ||
| if taskReq.Metadata != nil { | ||
| var meta HappyHorseMetadata | ||
| if err := taskcommon.UnmarshalMetadata(taskReq.Metadata, &meta); err == nil { | ||
| if meta.Duration != nil && *meta.Duration > 0 && (*meta.Duration < 2 || *meta.Duration > 15) { | ||
| return &dto.TaskError{ | ||
| Code: "invalid_duration", | ||
| Message: "metadata duration must be between 2 and 15 seconds", | ||
| StatusCode: http.StatusBadRequest, | ||
| LocalError: true, | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing validation for meta.Resolution in metadata.
The validation logic validates taskReq.Size (lines 138-152) and meta.Duration (lines 157-164), but meta.Resolution is not validated here. However, convertToHappyHorseRequest (lines 317-323) applies meta.Resolution as an override. This allows invalid resolutions like "480P" to bypass local validation when passed via metadata.
For consistency with the stated goal of local validation before forwarding to upstream:
🛡️ Proposed fix to validate metadata resolution
// Validate metadata duration (same override path as EstimateBilling / request assembly)
if taskReq.Metadata != nil {
var meta HappyHorseMetadata
if err := taskcommon.UnmarshalMetadata(taskReq.Metadata, &meta); err == nil {
+ if meta.Resolution != nil {
+ resolution := strings.ToUpper(strings.TrimSpace(*meta.Resolution))
+ if !strings.HasSuffix(resolution, "P") {
+ resolution += "P"
+ }
+ if resolution != "720P" && resolution != "1080P" {
+ return &dto.TaskError{
+ Code: "invalid_resolution",
+ Message: "metadata resolution must be 720P or 1080P",
+ StatusCode: http.StatusBadRequest,
+ LocalError: true,
+ }
+ }
+ }
if meta.Duration != nil && *meta.Duration > 0 && (*meta.Duration < 2 || *meta.Duration > 15) {
return &dto.TaskError{
Code: "invalid_duration",
Message: "metadata duration must be between 2 and 15 seconds",
StatusCode: http.StatusBadRequest,
LocalError: true,
}
}
}
}📝 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.
| // Validate duration and resolution locally before forwarding to upstream | |
| if taskReq.Duration > 0 && (taskReq.Duration < 2 || taskReq.Duration > 15) { | |
| return &dto.TaskError{ | |
| Code: "invalid_duration", | |
| Message: "duration must be between 2 and 15 seconds", | |
| StatusCode: http.StatusBadRequest, | |
| LocalError: true, | |
| } | |
| } | |
| if taskReq.Size != "" { | |
| resolution := strings.ToUpper(strings.TrimSpace(taskReq.Size)) | |
| if !strings.HasSuffix(resolution, "P") { | |
| resolution += "P" | |
| } | |
| if resolution != "720P" && resolution != "1080P" { | |
| return &dto.TaskError{ | |
| Code: "invalid_resolution", | |
| Message: "resolution must be 720P or 1080P", | |
| StatusCode: http.StatusBadRequest, | |
| LocalError: true, | |
| } | |
| } | |
| taskReq.Size = resolution // normalize for downstream consumers | |
| } | |
| // Validate metadata duration (same override path as EstimateBilling / request assembly) | |
| if taskReq.Metadata != nil { | |
| var meta HappyHorseMetadata | |
| if err := taskcommon.UnmarshalMetadata(taskReq.Metadata, &meta); err == nil { | |
| if meta.Duration != nil && *meta.Duration > 0 && (*meta.Duration < 2 || *meta.Duration > 15) { | |
| return &dto.TaskError{ | |
| Code: "invalid_duration", | |
| Message: "metadata duration must be between 2 and 15 seconds", | |
| StatusCode: http.StatusBadRequest, | |
| LocalError: true, | |
| } | |
| } | |
| } | |
| } | |
| // Validate duration and resolution locally before forwarding to upstream | |
| if taskReq.Duration > 0 && (taskReq.Duration < 2 || taskReq.Duration > 15) { | |
| return &dto.TaskError{ | |
| Code: "invalid_duration", | |
| Message: "duration must be between 2 and 15 seconds", | |
| StatusCode: http.StatusBadRequest, | |
| LocalError: true, | |
| } | |
| } | |
| if taskReq.Size != "" { | |
| resolution := strings.ToUpper(strings.TrimSpace(taskReq.Size)) | |
| if !strings.HasSuffix(resolution, "P") { | |
| resolution += "P" | |
| } | |
| if resolution != "720P" && resolution != "1080P" { | |
| return &dto.TaskError{ | |
| Code: "invalid_resolution", | |
| Message: "resolution must be 720P or 1080P", | |
| StatusCode: http.StatusBadRequest, | |
| LocalError: true, | |
| } | |
| } | |
| taskReq.Size = resolution // normalize for downstream consumers | |
| } | |
| // Validate metadata duration (same override path as EstimateBilling / request assembly) | |
| if taskReq.Metadata != nil { | |
| var meta HappyHorseMetadata | |
| if err := taskcommon.UnmarshalMetadata(taskReq.Metadata, &meta); err == nil { | |
| if meta.Resolution != nil { | |
| resolution := strings.ToUpper(strings.TrimSpace(*meta.Resolution)) | |
| if !strings.HasSuffix(resolution, "P") { | |
| resolution += "P" | |
| } | |
| if resolution != "720P" && resolution != "1080P" { | |
| return &dto.TaskError{ | |
| Code: "invalid_resolution", | |
| Message: "metadata resolution must be 720P or 1080P", | |
| StatusCode: http.StatusBadRequest, | |
| LocalError: true, | |
| } | |
| } | |
| } | |
| if meta.Duration != nil && *meta.Duration > 0 && (*meta.Duration < 2 || *meta.Duration > 15) { | |
| return &dto.TaskError{ | |
| Code: "invalid_duration", | |
| Message: "metadata duration must be between 2 and 15 seconds", | |
| StatusCode: http.StatusBadRequest, | |
| LocalError: true, | |
| } | |
| } | |
| } | |
| } |
🤖 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/happyhorse/adaptor.go` around lines 129 - 166, The
metadata resolution (HappyHorseMetadata.Resolution) is not validated, allowing
overrides to bypass local checks in convertToHappyHorseRequest; after
unmarshaling metadata with taskcommon.UnmarshalMetadata(&meta) in the validation
block, normalize meta.Resolution (strings.TrimSpace + strings.ToUpper, append
"P" if missing) and ensure it equals "720P" or "1080P"; if invalid, return a
dto.TaskError with Code "invalid_resolution" and the same StatusCode/LocalError
semantics used for taskReq.Size, and update the metadata/resolution in taskReq
or meta so downstream sees the normalized value.
| role='button' | ||
| tabIndex={0} | ||
| onClick={() => inputRef.current?.click()} | ||
| onDragOver={(e) => { | ||
| e.preventDefault() | ||
| setIsDragging(true) | ||
| }} | ||
| onDragLeave={() => setIsDragging(false)} | ||
| onDrop={handleDrop} | ||
| className={cn( | ||
| 'border-muted flex cursor-pointer flex-col items-center justify-center gap-1.5 rounded-md border border-dashed p-4 transition-colors', | ||
| isDragging && 'border-primary bg-primary/5', | ||
| !value && 'hover:bg-muted/50', | ||
| )} | ||
| > |
There was a problem hiding this comment.
Add keyboard event handlers for the drop zone.
The drop zone uses role='button' and tabIndex={0} to make it focusable, but it lacks keyboard event handlers. Users navigating via keyboard cannot activate the file picker with Enter or Space keys.
Add onKeyDown handler
<div
role='button'
tabIndex={0}
onClick={() => inputRef.current?.click()}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault()
+ inputRef.current?.click()
+ }
+ }}
onDragOver={(e) => {As per coding guidelines: "Ensure keyboard operability and logical focus order; use ARIA attributes when necessary."
📝 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.
| role='button' | |
| tabIndex={0} | |
| onClick={() => inputRef.current?.click()} | |
| onDragOver={(e) => { | |
| e.preventDefault() | |
| setIsDragging(true) | |
| }} | |
| onDragLeave={() => setIsDragging(false)} | |
| onDrop={handleDrop} | |
| className={cn( | |
| 'border-muted flex cursor-pointer flex-col items-center justify-center gap-1.5 rounded-md border border-dashed p-4 transition-colors', | |
| isDragging && 'border-primary bg-primary/5', | |
| !value && 'hover:bg-muted/50', | |
| )} | |
| > | |
| role='button' | |
| tabIndex={0} | |
| onClick={() => inputRef.current?.click()} | |
| onKeyDown={(e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault() | |
| inputRef.current?.click() | |
| } | |
| }} | |
| onDragOver={(e) => { | |
| e.preventDefault() | |
| setIsDragging(true) | |
| }} | |
| onDragLeave={() => setIsDragging(false)} | |
| onDrop={handleDrop} | |
| className={cn( | |
| 'border-muted flex cursor-pointer flex-col items-center justify-center gap-1.5 rounded-md border border-dashed p-4 transition-colors', | |
| isDragging && 'border-primary bg-primary/5', | |
| !value && 'hover:bg-muted/50', | |
| )} |
🤖 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/default/src/features/playground/components/media-drop-zone.tsx` around
lines 126 - 140, The drop zone is focusable (role='button', tabIndex={0}) but
missing keyboard handlers; add an onKeyDown on the same element to listen for
Enter and Space and call inputRef.current?.click() to open the file picker (for
Space also preventDefault to avoid page scroll), and keep existing
onClick/onDrop behavior; use the existing identifiers inputRef, setIsDragging,
handleDrop and value when locating the element to update.
| onClick={clearValue} | ||
| className='bg-destructive text-destructive-foreground absolute top-1 right-1 flex size-5 items-center justify-center rounded-full text-xs' | ||
| > | ||
| <XIcon className='size-3' /> |
There was a problem hiding this comment.
Add aria-hidden='true' to decorative icons.
The icons (XIcon, ImageIcon, VideoIcon) are decorative because their context is already conveyed by surrounding text or button labels. Screen readers should skip them.
Add aria-hidden to icons
<button
type='button'
onClick={clearValue}
className='bg-destructive text-destructive-foreground absolute top-1 right-1 flex size-5 items-center justify-center rounded-full text-xs'
>
- <XIcon className='size-3' />
+ <XIcon className='size-3' aria-hidden='true' />
</button>Apply the same change to ImageIcon (line 170) and VideoIcon (line 172):
{isImage ? (
- <ImageIcon className='text-muted-foreground size-6' />
+ <ImageIcon className='text-muted-foreground size-6' aria-hidden='true' />
) : (
- <VideoIcon className='text-muted-foreground size-6' />
+ <VideoIcon className='text-muted-foreground size-6' aria-hidden='true' />
)}As per coding guidelines: "add aria-hidden='true' to decorative icons."
Also applies to: 170-170, 172-172
🤖 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/default/src/features/playground/components/media-drop-zone.tsx` at line
164, The decorative icons XIcon, ImageIcon, and VideoIcon are missing
aria-hidden and should be ignored by screen readers; update the JSX where XIcon,
ImageIcon, and VideoIcon are rendered in media-drop-zone.tsx to include
aria-hidden="true" on each icon element (ensure the attribute is added to the
icon props/element so screen readers skip these decorative icons while leaving
surrounding button/label text unchanged).
| </div> | ||
|
|
||
| <Input | ||
| ref={inputRef as any} |
There was a problem hiding this comment.
Avoid using any type for ref.
The as any type assertion bypasses TypeScript's type checking. The Input component likely expects a compatible ref type for HTMLInputElement, which inputRef already provides.
Fix by removing the type assertion
<Input
- ref={inputRef as any}
+ ref={inputRef}
type='file'
accept={acceptAttr}If TypeScript reports an incompatibility, explicitly type the Input component's ref prop or use a callback ref instead.
As per coding guidelines: "Avoid any type in TypeScript; prefer specific types or unknown; explicitly annotate parameter and return value types."
📝 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.
| ref={inputRef as any} | |
| <Input | |
| ref={inputRef} | |
| type='file' | |
| accept={acceptAttr} |
🤖 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/default/src/features/playground/components/media-drop-zone.tsx` at line
180, Remove the unsafe "as any" assertion on the ref usage (ref={inputRef as
any}) and pass the properly typed ref to the Input component (use
ref={inputRef}); if TypeScript reports an incompatibility, update the Input
component's ref prop type to accept React.RefObject<HTMLInputElement> (or
appropriate element) or switch to a callback ref that forwards the inputRef
value, ensuring inputRef remains typed as React.RefObject<HTMLInputElement>.
| "Task failed: API Key invalid": "Tâche \"{{prompt}}...\" échouée : clé API invalide", | ||
| "Task failed: cannot restore key": "Tâche \"{{prompt}}...\" échouée : impossible de restaurer la clé", |
There was a problem hiding this comment.
Remove undeclared {{prompt}} interpolation from task-failure translations.
Line 3867 and Line 3868 add {{prompt}}... in FR values even though the source keys don’t contain that variable. This can surface unresolved placeholders in UI when prompt isn’t provided.
💡 Suggested fix
- "Task failed: API Key invalid": "Tâche \"{{prompt}}...\" échouée : clé API invalide",
- "Task failed: cannot restore key": "Tâche \"{{prompt}}...\" échouée : impossible de restaurer la clé",
+ "Task failed: API Key invalid": "Échec de la tâche : clé API invalide",
+ "Task failed: cannot restore key": "Échec de la tâche : impossible de restaurer la clé",📝 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.
| "Task failed: API Key invalid": "Tâche \"{{prompt}}...\" échouée : clé API invalide", | |
| "Task failed: cannot restore key": "Tâche \"{{prompt}}...\" échouée : impossible de restaurer la clé", | |
| "Task failed: API Key invalid": "Échec de la tâche : clé API invalide", | |
| "Task failed: cannot restore key": "Échec de la tâche : impossible de restaurer la clé", |
🤖 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/default/src/i18n/locales/fr.json` around lines 3867 - 3868, The French
translations for the keys "Task failed: API Key invalid" and "Task failed:
cannot restore key" include an undeclared interpolation `{{prompt}}...` which
can render unresolved placeholders; update the values for those keys in fr.json
to remove the `{{prompt}}...` fragment so they match the source keys (e.g.,
change "Tâche \"{{prompt}}...\" échouée : clé API invalide" and "Tâche
\"{{prompt}}...\" échouée : impossible de restaurer la clé" to versions without
`{{prompt}}...`), ensuring no undeclared placeholders remain.
| "Task failed: API Key invalid": "Задача \"{{prompt}}...\" не выполнена: недействительный ключ API", | ||
| "Task failed: cannot restore key": "Задача \"{{prompt}}...\" не выполнена: не удалось восстановить ключ", |
There was a problem hiding this comment.
Remove unresolved interpolation placeholders from task-failure messages.
These translations inject {{prompt}}..., but the key patterns here don’t indicate interpolation input. This can render raw placeholders to users.
💡 Suggested fix
- "Task failed: API Key invalid": "Задача \"{{prompt}}...\" не выполнена: недействительный ключ API",
- "Task failed: cannot restore key": "Задача \"{{prompt}}...\" не выполнена: не удалось восстановить ключ",
+ "Task failed: API Key invalid": "Задача не выполнена: недействительный ключ API",
+ "Task failed: cannot restore key": "Задача не выполнена: не удалось восстановить ключ",📝 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.
| "Task failed: API Key invalid": "Задача \"{{prompt}}...\" не выполнена: недействительный ключ API", | |
| "Task failed: cannot restore key": "Задача \"{{prompt}}...\" не выполнена: не удалось восстановить ключ", | |
| "Task failed: API Key invalid": "Задача не выполнена: недействительный ключ API", | |
| "Task failed: cannot restore key": "Задача не выполнена: не удалось восстановить ключ", |
🤖 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/default/src/i18n/locales/ru.json` around lines 3867 - 3868, The
translations for the keys "Task failed: API Key invalid" and "Task failed:
cannot restore key" include an unresolved interpolation placeholder
`{{prompt}}...`; remove that placeholder and replace the values with plain
static Russian messages (e.g., 'Задача не выполнена: недействительный ключ API'
and 'Задача не выполнена: не удалось восстановить ключ') so no raw interpolation
tokens remain in the ru.json entries for those keys.
| "Task failed: API Key invalid": "Nhiệm vụ \"{{prompt}}...\" thất bại: khóa API không hợp lệ", | ||
| "Task failed: cannot restore key": "Nhiệm vụ \"{{prompt}}...\" thất bại: không thể khôi phục khóa", |
There was a problem hiding this comment.
Remove unmatched interpolation placeholder from task-failure translations.
These Vietnamese values introduce {{prompt}}..., but the source keys have no interpolation token. This can render raw placeholders in UI and break message parity across locales.
💡 Suggested fix
- "Task failed: API Key invalid": "Nhiệm vụ \"{{prompt}}...\" thất bại: khóa API không hợp lệ",
- "Task failed: cannot restore key": "Nhiệm vụ \"{{prompt}}...\" thất bại: không thể khôi phục khóa",
+ "Task failed: API Key invalid": "Nhiệm vụ thất bại: khóa API không hợp lệ",
+ "Task failed: cannot restore key": "Nhiệm vụ thất bại: không thể khôi phục khóa",📝 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.
| "Task failed: API Key invalid": "Nhiệm vụ \"{{prompt}}...\" thất bại: khóa API không hợp lệ", | |
| "Task failed: cannot restore key": "Nhiệm vụ \"{{prompt}}...\" thất bại: không thể khôi phục khóa", | |
| "Task failed: API Key invalid": "Nhiệm vụ thất bại: khóa API không hợp lệ", | |
| "Task failed: cannot restore key": "Nhiệm vụ thất bại: không thể khôi phục khóa", |
🤖 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/default/src/i18n/locales/vi.json` around lines 3867 - 3868, Remove the
unmatched interpolation token from the Vietnamese translations for the keys
"Task failed: API Key invalid" and "Task failed: cannot restore key" in vi.json:
replace the values that include '{{prompt}}...' with plain messages that match
the source (no interpolation placeholders) so the locale stays parity with other
languages and no raw placeholders render in the UI.
…yground UI Assign HappyHorse channel type 59 so it does not collide with Advanced Custom (58). Co-authored-by: Cursor <cursoragent@cursor.com>
移植上游 PR QuantumNous#5124 的 playground 视频 UI 部分。 只有存在 happyhorse-* 模型时才出现 Chat/Video 分页,否则原样早返回 单栏聊天布局,现有 PlaygroundChat/PlaygroundInput 的 props 一字未改。 左栏为参数表单(模型类型、分辨率、时长、媒体输入、高级设置),右栏为 任务队列与播放器,提交后 5 秒轮询、任务列表持久化到 localStorage。 未采纳该 PR 的独立渠道方案:它的上游地址与既有 ali 渠道完全相同 (DashScope video-synthesis),happyhorse 已由 QuantumNous#4810 作为 ali 渠道下的 模型接入,再开一个渠道号只是重复——渠道号不可回收。前端改用模型名 前缀 happyhorse- 识别,同时覆盖仓库里的 1.0 与 1.1 两个系列。 在上游实现上修正三处: 1. 拖拽上传实际不可用 原实现对拖入文件调 URL.createObjectURL 生成 blob: URL 直接当 input_reference 提交。blob: 是仅在本标签页有效的句柄,上游服务器 取不到,而本地预览却能正常显示,用户要到任务失败才发现。改为只接受 http(s) URL(text/uri-list 或 text/plain)。真正的文件上传需要后端 对象存储接口,不在本次范围。 2. 明文 API Key 的多余留存 删除了写入后从未被读取的 taskApiKeys 引用与 TokenOption.key 字段。 密钥现仅在提交前按需拉取,存活于轮询闭包内。仍存在的残留面:未完成 任务会在每次进入 playground 时重新拉取明文密钥,根治需要后端会话 鉴权代理,已记录为后续项。 3. localStorage 无上限 任务历史无限追加,最终撑爆配额后静默吞掉 QuotaExceededError。现限制 50 条,写入与读取两侧都截断;并为 loadTasksFromStorage 补 Array 守卫——原实现遇到非数组值会整页崩溃。 补齐 32 个 i18n key × 7 种语言(上游 patch 只登记了其代码实际调用的 一部分,其余回退显示英文原串,且完全没有 zh-TW)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
上一批移植(QuantumNous#4810 + QuantumNous#5124)引入的三处缺陷,合起来使 happyhorse 图生视频 端到端不可用。三处单看都不像问题,是组合出来的。 1. input_reference 被静默丢弃 QuantumNous#4810 把 ali 的校验入口从 ValidateMultipartDirect 换成 ValidateBasicTaskRequest,而后者没有 InputReference→Images 的归一化。 旧模型与 wan2.7-i2v 因为走 firstTaskImage 兜底而幸免,happyhorse 全系 与 wan2.7-r2v/videoedit 的 isNewFormatModel 分支只读 req.Images,于是 收不到任何媒体——而 QuantumNous#5124 的新前端发的正是 input_reference。 归一化补在通用校验器里:input_reference 是 OpenAI 视频接口的标准字段, 其余用该校验器的渠道(xai/gemini/vertex)此前同样收不到它。 2. 刚提交的任务返回非协议状态值 "unknown" QuantumNous#4810 把状态源从 convertAliStatus(存档响应)换成 task.ToOpenAIVideo() (DB 行),而 InitTask 建行时是 NOT_START,ToVideoStatus 没有该分支,落到 default 返回 "unknown"。轮询 worker 15 秒才接手,这期间下游按四态解析 会整条落空——前端表现为任务提交后从队列里消失十几秒。 NOT_START 语义就是排队中,补进 queued 分支。 3. 首尾帧语义丢失 imageMediaType 对整个数组只返回一种类型,happyhorse-i2v 传两张图会被 打成两个 first_frame。改为按模型分型:参考类(r2v/videoedit)同型追加, 图生视频首张 first_frame、次张 last_frame,与 wan2.7-i2v 的 normalizeWan27I2VInput 对齐。 另修两处既有缺陷: - 时长兜底原本在 metadata 合并之前执行,而 metadata 能把 Duration 写回 0。 0 一头被 omitempty 从上游请求里抹掉(阿里改用自己的默认时长照常出片), 另一头让 seconds 倍率在 AddOtherRatio 的 ratio>0 守卫处被静默丢弃(按 1 秒计价)——净效果是付 1 秒的钱拿到默认时长的视频,稳定可复现。兜底移到 metadata 合并之后,使计费口径与上游下单口径一致。 - 双图走首尾帧字段的分支原本对所有旧模型生效,但只有 kf2v 系列真正支持 首尾帧,其余旧版图生视频只认 img_url,发 first_frame_url/last_frame_url 会被上游拒绝。收窄到 kf2v。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
review 查出,均为 QuantumNous#5124 移植带入。 1. 切换分页销毁输入 TabsContent 默认不保留挂载,切走再切回会清掉聊天草稿与滚动位置,视频侧的 提示词/分辨率/时长/图片 URL 也全部重置,且每次重挂都重新拉一遍密钥列表。 两个面板都加 keepMounted。 2. 轮询无上限 状态查询的异常被整体吞掉并注明继续重试,只有 completed/failed 才停表。 上游任务被清理(404)或密钥被删(401)时,只要标签页开着就以 12 次/分钟 永远轮询下去,任务也永远停在进行中。加 240 次(20 分钟)上限,超时置失败。 3. 非四态状态导致任务从队列消失 后端在任务刚入库时可能返回四态之外的值,原样存下来会让任务既不算进行中 也不算已完成,在队列里整条消失。加归一化兜底(后端侧的根因已另行修复)。 4. 取密钥失败时完全静默 fetchTokenKey 走 skipErrorHandler,全局拦截器不弹提示,而 handleSubmit 没有 try/catch——密钥被删或无权限时点「生成视频」毫无反应,只在控制台 留一条未捕获拒绝。 5. 提交失败仍清空提示词 onSubmit 捕获异常后不再抛出,await 正常返回,于是失败也会执行 setPrompt('') ——用户刚写的长提示词随报错一起消失且找不回。onSubmit 改为返回是否成功, 仅成功时清空。 另加多标签页同步:任务列表是整体覆写持久化,不监听 storage 事件的话,本标签页 的下一次写入会抹掉另一个标签页刚提交的任务(已付费任务失联)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
📝 变更描述 / Description
新增 HappyHorse(百炼快乐马) 渠道,对接阿里云百炼 DashScope 异步视频合成 API(Channel Type 58),并配套前端 Playground 视频任务 UI 与单元测试。
后端(Task adaptor)
前端(Playground 视频任务 UI)
测试
🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
📸 运行证明 / Proof of Work
提交列表(5 commits ahead of upstream/main):
Summary by CodeRabbit