feat(channel): 使用表单配置获取上游模型 - #5947
Conversation
WalkthroughAdds column-resizing support to the shared DataTable component (persisted via localStorage) and applies it to the Channels table with layout adjustments to the Name column. Reworks the channel model-fetch payload builder on the frontend and simplifies the backend FetchModels endpoint to use a unified upstream-ID fetch helper. ChangesColumn Resizing Feature
Estimated code review effort: 3 (Moderate) | ~25 minutes FetchModels Payload and Backend Refactor
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Frontend
participant FetchModelsAPI
participant fetchChannelUpstreamModelIDs
Frontend->>FetchModelsAPI: POST channel JSON (base_url, type, key, setting, settings, header_override)
FetchModelsAPI->>FetchModelsAPI: validate channel.Key non-empty
FetchModelsAPI->>fetchChannelUpstreamModelIDs: fetchChannelUpstreamModelIDs(&channel)
fetchChannelUpstreamModelIDs-->>FetchModelsAPI: ids
FetchModelsAPI-->>Frontend: { ids }
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ 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 |
|
按维护成本和 review 范围拆分为两个独立 PR,本合并版先关闭。拆分后的 PR 会分别覆盖渠道列表列宽调整和获取上游模型体验优化。 |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
web/default/src/features/channels/components/channels-columns.tsx (1)
562-563: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winConsider disabling resize on other fixed-width utility columns too.
The
selectcolumn now explicitly setsenableResizing: false, which makes sense for a fixed-width checkbox column. Theactionscolumn (pinned right, icon-only) is similarly fixed-content but isn't marked non-resizable, so it will show a resize handle onceenableColumnResizing: trueis set table-wide — resizing a pinned action column could produce an odd/broken layout.🤖 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/channels/components/channels-columns.tsx` around lines 562 - 563, The fixed-width utility columns should behave consistently: `select` is already marked non-resizable, but `actions` in `channels-columns.tsx` should also disable resizing so the pinned icon-only column does not show a resize handle when table-wide resizing is enabled. Update the `actions` column definition alongside `select` to set `enableResizing: false`, using the existing column config in the channels columns setup.web/default/src/components/data-table/hooks/use-data-table.ts (1)
241-251: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winColumn-visibility and column-sizing hydration/persistence logic is now duplicated.
The refs,
resolvedInitial*memo, hydration effect, and persistence effect forcolumnSizingclosely mirror the pre-existingcolumnVisibilitymachinery. Consider extracting a shared generic helper (e.g.usePersistedTableState<T>) to avoid drift between the two nearly-identical implementations as more persisted table state (e.g. column ordering) is added later.🤖 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/components/data-table/hooks/use-data-table.ts` around lines 241 - 251, Extract the duplicated hydration/persistence flow in use-data-table from the columnVisibility and columnSizing paths into a shared generic helper such as usePersistedTableState<T>, reusing the same patterns for resolvedInitial state, hydrated storage key ref, skip-next-persist ref, and the hydration/persistence effects. Wire both existing columnVisibility and columnSizing state through the new helper so the behavior stays identical while reducing drift for future persisted table state like column ordering.controller/channel.go (1)
1159-1167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider a dedicated request DTO instead of binding directly into
model.Channel.Binding the client payload straight into the persistence model couples the public request contract to internal storage schema (e.g., any future field added to
model.Channelbecomes implicitly bindable here). Since this handler doesn't persist the struct, the risk is currently low, but a small request struct mirroring onlytype/key/base_url/setting/settings/header_overridewould be safer and self-documenting.♻️ Example of a narrower request DTO
+type fetchModelsRequest struct { + Type int `json:"type"` + Key string `json:"key"` + BaseURL *string `json:"base_url"` + Setting *string `json:"setting"` + OtherSettings string `json:"settings"` + HeaderOverride *string `json:"header_override"` +} + func FetchModels(c *gin.Context) { - var channel model.Channel - - if err := c.ShouldBindJSON(&channel); err != nil { + var req fetchModelsRequest + if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "Invalid request", }) return } + channel := model.Channel{ + Type: req.Type, + Key: req.Key, + BaseURL: req.BaseURL, + Setting: req.Setting, + OtherSettings: req.OtherSettings, + HeaderOverride: req.HeaderOverride, + }🤖 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/channel.go` around lines 1159 - 1167, The request binding in the channel handler currently uses model.Channel directly, which couples the API payload to the persistence model. Update the handler to bind into a dedicated request DTO in the same flow instead of model.Channel, and have it include only the fields this endpoint accepts (such as type, key, base_url, setting/settings, and header_override). Keep the existing validation/error handling, but reference the new DTO in the binding logic so the public contract stays narrow and explicit.
🤖 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/channel.go`:
- Around line 1159-1188: Populate ChannelInfo before calling
fetchChannelUpstreamModelIDs in the form-based handler so GetNextEnabledKey can
detect multi-key input correctly. After ShouldBindJSON on model.Channel,
construct or fill channel.ChannelInfo with IsMultiKey set from the request
context/params as needed, and ensure the existing key-splitting logic is used
for pasted multi-line API keys instead of treating channel.Key as a single
value.
In `@web/default/src/components/data-table/core/data-table-header.tsx`:
- Around line 58-79: The column resizer in data-table-header is mouse/touch-only
and lacks keyboard accessibility. Update the resizer element in DataTableHeader
to be focusable and operable via keyboard by adding a tab stop and an onKeyDown
handler that adjusts the column size using the table/header sizing APIs. Also
expose the current sizing state with appropriate ARIA attributes on the
separator element so keyboard-only users can resize columns.
- Line 69: The new aria-label in DataTableHeader uses the literal “Resize
column” without a localized entry, so add this key to the locale JSON files
under web/default/src/i18n/locales and wire it into the existing translation
set. Update the relevant i18n resources used by data-table-header.tsx so the
t('Resize column') lookup resolves in every locale instead of falling back to
English.
In `@web/default/src/components/data-table/hooks/use-data-table.ts`:
- Around line 294-301: The current use-data-table setup keeps columnSizing
updating on every drag tick via useDataTable with columnResizeMode set to
onChange, and the persistence effect then writes to localStorage synchronously
on every change. Update the column sizing persistence path in use-data-table so
it is debounced or deferred until resizing settles, instead of writing during
the resize hot path. Keep the change focused around the
columnSizing/localStorage effect and the column resizing handlers so drag
updates remain smooth.
---
Nitpick comments:
In `@controller/channel.go`:
- Around line 1159-1167: The request binding in the channel handler currently
uses model.Channel directly, which couples the API payload to the persistence
model. Update the handler to bind into a dedicated request DTO in the same flow
instead of model.Channel, and have it include only the fields this endpoint
accepts (such as type, key, base_url, setting/settings, and header_override).
Keep the existing validation/error handling, but reference the new DTO in the
binding logic so the public contract stays narrow and explicit.
In `@web/default/src/components/data-table/hooks/use-data-table.ts`:
- Around line 241-251: Extract the duplicated hydration/persistence flow in
use-data-table from the columnVisibility and columnSizing paths into a shared
generic helper such as usePersistedTableState<T>, reusing the same patterns for
resolvedInitial state, hydrated storage key ref, skip-next-persist ref, and the
hydration/persistence effects. Wire both existing columnVisibility and
columnSizing state through the new helper so the behavior stays identical while
reducing drift for future persisted table state like column ordering.
In `@web/default/src/features/channels/components/channels-columns.tsx`:
- Around line 562-563: The fixed-width utility columns should behave
consistently: `select` is already marked non-resizable, but `actions` in
`channels-columns.tsx` should also disable resizing so the pinned icon-only
column does not show a resize handle when table-wide resizing is enabled. Update
the `actions` column definition alongside `select` to set `enableResizing:
false`, using the existing column config in the channels columns setup.
🪄 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: d6cd74fe-838a-4de1-a1c2-b4a08db22864
📒 Files selected for processing (9)
controller/channel.goweb/default/src/components/data-table/core/data-table-colgroup.tsxweb/default/src/components/data-table/core/data-table-header.tsxweb/default/src/components/data-table/hooks/use-data-table.tsweb/default/src/features/channels/api.tsweb/default/src/features/channels/components/channels-columns.tsxweb/default/src/features/channels/components/channels-table.tsxweb/default/src/features/channels/components/drawers/channel-mutate-drawer.tsxweb/default/src/features/channels/lib/channel-form.ts
| var channel model.Channel | ||
|
|
||
| if err := c.ShouldBindJSON(&req); err != nil { | ||
| if err := c.ShouldBindJSON(&channel); err != nil { | ||
| c.JSON(http.StatusBadRequest, gin.H{ | ||
| "success": false, | ||
| "message": "Invalid request", | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| baseURL := req.BaseURL | ||
| if baseURL == "" { | ||
| baseURL = constant.ChannelBaseURLs[req.Type] | ||
| } | ||
|
|
||
| // remove line breaks and extra spaces. | ||
| key := strings.TrimSpace(req.Key) | ||
| key = strings.Split(key, "\n")[0] | ||
|
|
||
| if req.Type == constant.ChannelTypeOllama { | ||
| models, err := ollama.FetchOllamaModels(baseURL, key) | ||
| if err != nil { | ||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": false, | ||
| "message": fmt.Sprintf("获取Ollama模型失败: %s", err.Error()), | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| names := make([]string, 0, len(models)) | ||
| for _, modelInfo := range models { | ||
| names = append(names, modelInfo.Name) | ||
| } | ||
|
|
||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": true, | ||
| "data": names, | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| if req.Type == constant.ChannelTypeGemini { | ||
| models, err := gemini.FetchGeminiModels(baseURL, key, "") | ||
| if err != nil { | ||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": false, | ||
| "message": fmt.Sprintf("获取Gemini模型失败: %s", err.Error()), | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| if strings.TrimSpace(channel.Key) == "" { | ||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": true, | ||
| "data": models, | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| client := &http.Client{} | ||
| url := fmt.Sprintf("%s/v1/models", baseURL) | ||
|
|
||
| request, err := http.NewRequest("GET", url, nil) | ||
| if err != nil { | ||
| c.JSON(http.StatusInternalServerError, gin.H{ | ||
| "success": false, | ||
| "message": err.Error(), | ||
| "message": "Please enter API key first", | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| request.Header.Set("Authorization", "Bearer "+key) | ||
|
|
||
| response, err := client.Do(request) | ||
| ids, err := fetchChannelUpstreamModelIDs(&channel) | ||
| if err != nil { | ||
| c.JSON(http.StatusInternalServerError, gin.H{ | ||
| "success": false, | ||
| "message": err.Error(), | ||
| }) | ||
| return | ||
| } | ||
| //check status code | ||
| if response.StatusCode != http.StatusOK { | ||
| c.JSON(http.StatusInternalServerError, gin.H{ | ||
| "success": false, | ||
| "message": "Failed to fetch models", | ||
| }) | ||
| return | ||
| } | ||
| defer response.Body.Close() | ||
|
|
||
| var result struct { | ||
| Data []struct { | ||
| ID string `json:"id"` | ||
| } `json:"data"` | ||
| } | ||
|
|
||
| if err := json.NewDecoder(response.Body).Decode(&result); err != nil { | ||
| c.JSON(http.StatusInternalServerError, gin.H{ | ||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": false, | ||
| "message": err.Error(), | ||
| "message": fmt.Sprintf("获取模型列表失败: %s", err.Error()), | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| var models []string | ||
| for _, model := range result.Data { | ||
| models = append(models, model.ID) | ||
| } | ||
|
|
||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": true, | ||
| "data": models, | ||
| "data": ids, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Locate GetNextEnabledKey and inspect its dependency on Status/ChannelInfo
ast-grep run --pattern 'func (channel *Channel) GetNextEnabledKey($$$) $$$' --lang go model
rg -n -A 40 'func \(channel \*Channel\) GetNextEnabledKey' modelRepository: QuantumNous/new-api
Length of output: 7220
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect Channel shape, the fetch-models controller path, and any normalization before GetNextEnabledKey.
ast-grep outline model/channel.go --view expanded
printf '\n--- controller path ---\n'
ast-grep outline controller/channel_upstream_update.go --view expanded
printf '\n--- fetch payload source ---\n'
rg -n -A 40 -B 20 'transformFormDataToFetchModelsPayload|fetchChannelUpstreamModelIDs|GetNextEnabledKey\(' controller modelRepository: QuantumNous/new-api
Length of output: 35307
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the Channel struct, JSON tags, and the fetch-models request transformation.
sed -n '1,260p' model/channel.go | cat -n
printf '\n--- fetch-models transformation ---\n'
rg -n -A 80 -B 30 'transformFormDataToFetchModelsPayload|FetchModels|fetchChannelUpstreamModelIDs' controllerRepository: QuantumNous/new-api
Length of output: 39500
Populate ChannelInfo for this form-based fetch path. GetNextEnabledKey() ignores Status, but it does require ChannelInfo.IsMultiKey; with the current raw ShouldBindJSON payload, pasted multi-line API keys stay unsplit and are treated as one 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 `@controller/channel.go` around lines 1159 - 1188, Populate ChannelInfo before
calling fetchChannelUpstreamModelIDs in the form-based handler so
GetNextEnabledKey can detect multi-key input correctly. After ShouldBindJSON on
model.Channel, construct or fill channel.ChannelInfo with IsMultiKey set from
the request context/params as needed, and ensure the existing key-splitting
logic is used for pasted multi-line API keys instead of treating channel.Key as
a single value.
| className={cn( | ||
| 'relative', | ||
| getColumnClassName?.(header.column.id, 'header') | ||
| )} | ||
| style={getHeaderSizeStyle(header, applyHeaderSize)} | ||
| > | ||
| {renderHeaderContent(header)} | ||
| {shouldRenderColumnResizer(table, header) && ( | ||
| <div | ||
| role='separator' | ||
| aria-orientation='vertical' | ||
| aria-label={t('Resize column')} | ||
| onDoubleClick={() => header.column.resetSize()} | ||
| onMouseDown={header.getResizeHandler()} | ||
| onTouchStart={header.getResizeHandler()} | ||
| className={cn( | ||
| 'absolute top-0 right-0 h-full w-2 cursor-col-resize touch-none select-none', | ||
| 'after:bg-border hover:after:bg-primary after:absolute after:top-2 after:right-0 after:h-[calc(100%-1rem)] after:w-px after:transition-colors', | ||
| header.column.getIsResizing() && 'after:bg-primary' | ||
| )} | ||
| /> | ||
| )} |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Resize handle is mouse/touch-only; no keyboard support.
The resizer div (Lines 65-79) uses role='separator' with onMouseDown/onTouchStart only — there's no tabIndex, no onKeyDown, and no aria-valuenow/aria-valuemin/aria-valuemax. Keyboard-only users cannot resize columns at all with this implementation, which is a common headless-table pitfall since TanStack Table intentionally leaves all accessibility to the consumer.
As per coding guidelines, web/default/**/*.{ts,tsx} components must "ensure keyboard operability and sensible focus order, add ARIA attributes when needed."
Consider adding tabIndex={0} plus an onKeyDown handler that nudges header.column.getSize() via arrow keys (calling the table's setColumnSizing or equivalent), and exposing current/min/max via ARIA attributes.
Also applies to: 88-99
🤖 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/components/data-table/core/data-table-header.tsx` around
lines 58 - 79, The column resizer in data-table-header is mouse/touch-only and
lacks keyboard accessibility. Update the resizer element in DataTableHeader to
be focusable and operable via keyboard by adding a tab stop and an onKeyDown
handler that adjusts the column size using the table/header sizing APIs. Also
expose the current sizing state with appropriate ARIA attributes on the
separator element so keyboard-only users can resize columns.
Source: Coding guidelines
| <div | ||
| role='separator' | ||
| aria-orientation='vertical' | ||
| aria-label={t('Resize column')} |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
fd -e json . web/default/src/i18n/locales --exec rg -l '"Resize column"' {}Repository: QuantumNous/new-api
Length of output: 157
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== locale files ==\n'
fd -a -t f '.*\.json$' web/default/src/i18n/locales
printf '\n== search for Resize column (case-sensitive) ==\n'
rg -n '"Resize column"|Resize column' web/default/src/i18n/locales || true
printf '\n== inspect i18n config ==\n'
fd -a -t f 'i18n' web/default/src | sed -n '1,20p'
rg -n "fallbackLng|lng|keySeparator|nsSeparator|returnNull|returnEmptyString|fallbackNS" web/default/src/i18n web/default/src --glob '!**/*.map' || trueRepository: QuantumNous/new-api
Length of output: 1000
Add Resize column to the locale files. The new aria-label key is missing from web/default/src/i18n/locales/*.json, so it will fall back to the English key in every locale.
🤖 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/components/data-table/core/data-table-header.tsx` at line 69,
The new aria-label in DataTableHeader uses the literal “Resize column” without a
localized entry, so add this key to the locale JSON files under
web/default/src/i18n/locales and wire it into the existing translation set.
Update the relevant i18n resources used by data-table-header.tsx so the
t('Resize column') lookup resolves in every locale instead of falling back to
English.
Source: Coding guidelines
| manualFiltering, | ||
| manualPagination, | ||
| manualSorting, | ||
| enableColumnResizing: options.enableColumnResizing, | ||
| columnResizeMode: 'onChange', | ||
| onSortingChange, | ||
| onColumnVisibilityChange, | ||
| onColumnSizingChange, |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
sed -n '240,430p' web/default/src/components/data-table/hooks/use-data-table.tsRepository: QuantumNous/new-api
Length of output: 5038
Defer columnSizing persistence off the resize hot path. columnResizeMode: 'onChange' keeps columnSizing updating during every drag tick, and the effect at lines 384-401 synchronously writes to localStorage on each change. That can cause visible resize jank; debounce the write or flush it only after the drag settles.
🤖 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/components/data-table/hooks/use-data-table.ts` around lines
294 - 301, The current use-data-table setup keeps columnSizing updating on every
drag tick via useDataTable with columnResizeMode set to onChange, and the
persistence effect then writes to localStorage synchronously on every change.
Update the column sizing persistence path in use-data-table so it is debounced
or deferred until resizing settles, instead of writing during the resize hot
path. Keep the change focused around the columnSizing/localStorage effect and
the column resizing handlers so drag updates remain smooth.
Important
📝 变更描述 / Description
当前在创建或编辑渠道时,获取上游模型的行为没有完全使用表单里的未保存配置:创建模式只提交 type/key/base_url,后端单独手写了一套模型列表请求逻辑;编辑模式在未保存新 key 时仍使用已保存渠道配置。这会导致表单里刚填的代理设置、header override 等配置无法参与获取模型,也容易和已保存渠道的正式获取逻辑不一致。
本 PR 将
POST /api/channel/fetch_models改为接收渠道表单配置并复用现有fetchChannelUpstreamModelIDs。这样 provider 特殊路径、代理、header override、Ollama/Gemini 等逻辑都和已保存渠道获取上游模型保持一致。前端新增从渠道表单生成获取模型 payload 的转换函数。创建渠道时会直接用当前表单中的 key、Base URL、代理设置和 header override 拉取模型;编辑已有渠道时,只有用户在表单中填入新 key 才会走表单配置请求,未填写新 key 时仍保持原有按已保存渠道获取的行为,避免把已保存密钥发送到任意未保存的 Base URL。
🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
Bug fix。📸 运行证明 / Proof of Work
已执行:
说明:当前本地 Windows 环境未安装
go/gofmt,因此 Go 编译与格式检查由 CI 继续验证。