Conversation
- Provider 列表页添加搜索框,支持按名称和显示信息过滤 - Client Routes 页面 (/routes/claude 等) 添加搜索框 - 搜索同时过滤已配置路由和可用 Provider - 添加 Custom Provider 模板的 Model Mapping 支持 - 添加 NVIDIA 模板 (OpenAI 兼容,默认模型映射) - Provider 创建页面增加 Model Mapping 编辑功能 - Provider 新增 Logo 字段,支持模板自动设置 - 添加 88code、aicodemirror、nvidia 图标资源 - 补充 i18n 翻译
📝 Walkthrough总体概览此拉取请求为提供商(Provider)实体添加了徽标字段支持,实现了客户端路由和提供商管理页面的搜索功能,新增了NVIDIA提供商模板,并实现了提供商自定义流程中的模型映射管理UI及关联创建逻辑。 变更明细
序列图sequenceDiagram
actor User
participant UI as 自定义步骤UI
participant Service as 提供商服务
participant MappingAPI as 模型映射API
User->>UI: 填写提供商信息和模型映射
User->>UI: 点击保存
UI->>Service: 创建提供商 (含logo)
Service-->>UI: 返回提供商ID
alt 存在模型映射
loop 对每个映射
UI->>MappingAPI: 创建模型映射 (pattern→target)
MappingAPI-->>UI: 映射创建成功
end
end
UI->>UI: 标记保存成功
UI-->>User: 显示成功提示
代码审查工作量评估🎯 3 (中等复杂度) | ⏱️ ~25 分钟 相关联的拉取请求
建议审查者
庆祝诗
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
web/src/components/routes/ClientTypeRoutesContent.tsx (1)
104-124: 过滤状态下允许拖拽会导致路由顺序错乱。
searchQuery 非空时只对可见子集排序更新,隐藏项保留旧 position,容易出现重复/错序。建议在过滤时禁用拖拽,或基于全量列表计算新顺序。🛠️ 建议修复(示例:过滤时禁用拖拽)
- const loading = routesLoading || providersLoading; + const loading = routesLoading || providersLoading; + const isFiltering = !!searchQuery.trim();- const handleDragStart = (event: DragStartEvent) => { - setActiveId(event.active.id as string); - }; + const handleDragStart = (event: DragStartEvent) => { + if (isFiltering) return; + setActiveId(event.active.id as string); + };- const handleDragEnd = (event: DragEndEvent) => { + const handleDragEnd = (event: DragEndEvent) => { + if (isFiltering) return; const { active, over } = event;web/src/pages/providers/components/custom-config-step.tsx (1)
34-60: 模型映射创建失败会导致“已创建 Provider 但页面报错”。
Provider 已创建后若映射创建失败会进入 error 状态,用户可能重复创建;同时空 pattern/target 也可能触发无效请求。建议过滤空映射并用 allSettled 处理失败(必要时提示用户或引导到编辑页重试)。🛠️ 建议修复(示例)
- // Create model mappings if template has any - if (formData.modelMappings && formData.modelMappings.length > 0) { - for (const mapping of formData.modelMappings) { - await createModelMapping.mutateAsync({ - scope: 'provider', - providerID: provider.id, - pattern: mapping.pattern, - target: mapping.target, - }); - } - } + const mappings = (formData.modelMappings ?? []).filter( + (m) => m.pattern.trim() && m.target.trim() + ); + if (mappings.length > 0) { + await Promise.allSettled( + mappings.map((mapping) => + createModelMapping.mutateAsync({ + scope: 'provider', + providerID: provider.id, + pattern: mapping.pattern, + target: mapping.target, + }) + ) + ); + }
🤖 Fix all issues with AI agents
In `@internal/domain/model.go`:
- Around line 91-92: The Provider GORM model is missing the Logo field added to
the domain model; update the Provider struct in
internal/repository/sqlite/models.go to include a Logo string field mapped to
the JSON/tag and GORM column (e.g., Logo string `json:"logo,omitempty"
gorm:"column:logo;size:2048"`), adding a size limit (suggest ~2048) to prevent
oversized data URIs and ensure AutoMigrate creates the logo column; keep the
JSON tag consistent with the domain model.
In `@web/src/pages/client-routes/index.tsx`:
- Around line 38-48: 为搜索输入补充无障碍标签:在使用的 Input 组件(在这段代码块中引用的 Input)上添加 aria-label
或为其生成关联的 <label>(使用相同的翻译文本 t('common.searchProviders') 以保持一致),确保 Input 保留现有的
placeholder、value 和 onChange;如果添加 <label>,给 Input 一个唯一 id 并将 <label
htmlFor={id}> 包裹或放在相应位置;同时保留 Search 图标不影响可访问性(图标保持装饰性,确保其 role 或 aria-hidden 设为
true 如有必要)。
In `@web/src/pages/providers/components/custom-config-step.tsx`:
- Around line 172-245: The delete Button that renders only the Trash2 icon in
the modelMappings list lacks accessible text—update the Button (the one with
variant="ghost" size="icon" wrapping <Trash2 /> inside the map iteration) to
include an aria-label and title (e.g.
aria-label={t('routes.modelMapping.removeMapping')}
title={t('routes.modelMapping.removeMapping')}) and mark the <Trash2 /> icon
aria-hidden="true" to avoid duplicate announcements; ensure the i-th removal
onClick logic remains unchanged so accessibility is added without affecting
behavior.
In `@web/src/pages/providers/components/select-type-step.tsx`:
- Around line 44-45: The template’s modelMappings reference is being assigned
directly (modelMappings: template.modelMappings) which can mutate the original
template if the form allows edits; in the SelectTypeStep component, replace the
direct reference with a copy of template.modelMappings (use a shallow copy for
arrays/objects via spread or a deep copy via JSON.parse(JSON.stringify(...)) or
structuredClone) so the form state owns its own data and does not mutate
template.modelMappings; keep logo: template.logoUrl as-is.
In `@web/src/pages/providers/index.tsx`:
- Around line 123-134: The search input lacks an explicit accessibility label;
update the Input used in this block (the Input component bound to searchQuery
with onChange={(e) => setSearchQuery(e.target.value)}) to include an aria-label
(or associate a visible <label>) such as aria-label={t('common.search')} so
screen readers can identify the field; ensure the Search icon remains decorative
(e.g., aria-hidden="true" or role="presentation") if applicable.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
web/src/assets/icons/88code.svgis excluded by!**/*.svgweb/src/assets/icons/aicodemirror.pngis excluded by!**/*.pngweb/src/assets/icons/nvidia.svgis excluded by!**/*.svg
📒 Files selected for processing (11)
internal/domain/model.goweb/src/components/routes/ClientTypeRoutesContent.tsxweb/src/components/ui/model-input.tsxweb/src/lib/transport/types.tsweb/src/locales/en.jsonweb/src/locales/zh.jsonweb/src/pages/client-routes/index.tsxweb/src/pages/providers/components/custom-config-step.tsxweb/src/pages/providers/components/select-type-step.tsxweb/src/pages/providers/index.tsxweb/src/pages/providers/types.ts
🧰 Additional context used
🧬 Code graph analysis (3)
web/src/components/routes/ClientTypeRoutesContent.tsx (3)
internal/domain/model.go (1)
Provider(76-99)web/src/lib/transport/types.ts (1)
Provider(43-52)internal/repository/sqlite/models.go (2)
Provider(115-121)Provider(123-123)
web/src/pages/client-routes/index.tsx (4)
web/src/lib/transport/types.ts (1)
ClientType(8-8)web/src/components/ui/index.ts (1)
Input(23-23)web/src/components/ui/input.tsx (1)
Input(20-20)web/src/components/routes/ClientTypeRoutesContent.tsx (1)
ClientTypeRoutesContent(54-371)
web/src/pages/providers/index.tsx (3)
web/src/pages/providers/types.ts (2)
PROVIDER_TYPE_CONFIGS(30-63)ProviderTypeKey(14-14)web/src/components/ui/index.ts (1)
Input(23-23)web/src/components/ui/input.tsx (1)
Input(20-20)
🔇 Additional comments (14)
web/src/components/ui/model-input.tsx (1)
51-72: 新增 NVIDIA/Meta 常见模型清单 OK
扩展模型列表与现有过滤/分组逻辑兼容,整体看起来合理。web/src/locales/en.json (2)
39-41: 新增搜索相关文案 OK
与新增搜索功能匹配。
574-576: 新增 NVIDIA 模板文案 OK
与模板扩展保持一致。web/src/lib/transport/types.ts (1)
49-49: 类型扩展与后端对齐
新增logo?: string与后端字段一致。web/src/locales/zh.json (2)
39-41: 新增搜索相关中文文案 OK
与新增搜索功能匹配。
573-576: 新增 NVIDIA 模板中文文案 OK
与模板扩展保持一致。web/src/pages/client-routes/index.tsx (2)
6-19: 搜索状态与 i18n 接入清晰。
54-58: searchQuery 下传到内容组件很清晰。web/src/components/routes/ClientTypeRoutesContent.tsx (2)
48-58: searchQuery 作为可选 prop 设计合理。
128-143: Available Providers 的搜索过滤符合预期。web/src/pages/providers/index.tsx (1)
36-59: 按名称与显示信息过滤后再分组逻辑正确。web/src/pages/providers/types.ts (3)
80-98: 模板模型映射类型与字段扩展清晰。
108-172: 模板 Logo 与 NVIDIA 模板补充到位。
198-200: ProviderFormData 新字段与创建流程一致。
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| // Logo URL 或 data URI | ||
| Logo string `json:"logo,omitempty"` |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f \( -name "*migration*" -o -name "*migrations*" -o -name "*schema*" \) | head -20Repository: awsl-project/maxx
Length of output: 188
🏁 Script executed:
rg -l "migrations|migration" --type go --type sql | head -20Repository: awsl-project/maxx
Length of output: 171
🏁 Script executed:
rg -n "logo" -i -t go -t sql . 2>/dev/null | head -40Repository: awsl-project/maxx
Length of output: 181
🏁 Script executed:
rg -n "type Provider struct" -A 50 internal/domain/model.goRepository: awsl-project/maxx
Length of output: 1409
🏁 Script executed:
cat -n internal/repository/sqlite/migrations.goRepository: awsl-project/maxx
Length of output: 5457
🏁 Script executed:
cat -n internal/repository/sqlite/models.go | head -100Repository: awsl-project/maxx
Length of output: 2771
🏁 Script executed:
rg -n "validate|Validate" internal/domain/model.goRepository: awsl-project/maxx
Length of output: 43
🏁 Script executed:
cat -n internal/repository/sqlite/models.go | grep -A 30 "type Provider struct"Repository: awsl-project/maxx
Length of output: 1135
🏁 Script executed:
rg -n "Logo" internal/repository/sqlite/models.goRepository: awsl-project/maxx
Length of output: 43
🏁 Script executed:
cat -n docs/database-migrations.mdRepository: awsl-project/maxx
Length of output: 6890
Logo 字段缺失 GORM 模型定义,导致数据无法持久化
Logo 字段已添加到域模型 (internal/domain/model.go:91-92),但在 GORM 模型 (internal/repository/sqlite/models.go:115-121) 中缺失。由于 AutoMigrate 仅处理 GORM 模型中定义的字段,缺少该定义会导致数据库列不被创建,Logo 数据将被丢弃。
必须在 Provider 结构体中补充 Logo 字段定义,建议添加长度限制以防止超长 data URI 导致存储膨胀。
🤖 Prompt for AI Agents
In `@internal/domain/model.go` around lines 91 - 92, The Provider GORM model is
missing the Logo field added to the domain model; update the Provider struct in
internal/repository/sqlite/models.go to include a Logo string field mapped to
the JSON/tag and GORM column (e.g., Logo string `json:"logo,omitempty"
gorm:"column:logo;size:2048"`), adding a size limit (suggest ~2048) to prevent
oversized data URIs and ensure AutoMigrate creates the logo column; keep the
JSON tag consistent with the domain model.
| <div className="relative"> | ||
| <Search | ||
| size={14} | ||
| className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" | ||
| /> | ||
| <Input | ||
| placeholder={t('common.searchProviders')} | ||
| value={searchQuery} | ||
| onChange={(e) => setSearchQuery(e.target.value)} | ||
| className="pl-9 w-48" | ||
| /> |
There was a problem hiding this comment.
为搜索输入补充可访问性标签。
仅依赖 placeholder 不利于读屏器识别,建议添加 aria-label 或显式 label。
🧩 建议修复
- <Input
- placeholder={t('common.searchProviders')}
- value={searchQuery}
- onChange={(e) => setSearchQuery(e.target.value)}
- className="pl-9 w-48"
- />
+ <Input
+ type="search"
+ aria-label={t('common.searchProviders')}
+ placeholder={t('common.searchProviders')}
+ value={searchQuery}
+ onChange={(e) => setSearchQuery(e.target.value)}
+ className="pl-9 w-48"
+ />📝 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.
| <div className="relative"> | |
| <Search | |
| size={14} | |
| className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" | |
| /> | |
| <Input | |
| placeholder={t('common.searchProviders')} | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| className="pl-9 w-48" | |
| /> | |
| <div className="relative"> | |
| <Search | |
| size={14} | |
| className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" | |
| /> | |
| <Input | |
| type="search" | |
| aria-label={t('common.searchProviders')} | |
| placeholder={t('common.searchProviders')} | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| className="pl-9 w-48" | |
| /> |
🤖 Prompt for AI Agents
In `@web/src/pages/client-routes/index.tsx` around lines 38 - 48,
为搜索输入补充无障碍标签:在使用的 Input 组件(在这段代码块中引用的 Input)上添加 aria-label 或为其生成关联的
<label>(使用相同的翻译文本 t('common.searchProviders') 以保持一致),确保 Input 保留现有的
placeholder、value 和 onChange;如果添加 <label>,给 Input 一个唯一 id 并将 <label
htmlFor={id}> 包裹或放在相应位置;同时保留 Search 图标不影响可访问性(图标保持装饰性,确保其 role 或 aria-hidden 设为
true 如有必要)。
| {/* Model Mapping Section */} | ||
| <div className="space-y-6"> | ||
| <div className="flex items-center justify-between border-b border-border pb-2"> | ||
| <h3 className="text-lg font-semibold text-text-primary"> | ||
| {t('modelMappings.title')} | ||
| </h3> | ||
| <Button | ||
| variant="outline" | ||
| size="sm" | ||
| onClick={() => { | ||
| const newMappings = [...(formData.modelMappings || []), { pattern: '', target: '' }]; | ||
| updateFormData({ modelMappings: newMappings }); | ||
| }} | ||
| > | ||
| <Plus size={14} /> | ||
| {t('routes.modelMapping.addMapping')} | ||
| </Button> | ||
| </div> | ||
|
|
||
| {formData.modelMappings && formData.modelMappings.length > 0 ? ( | ||
| <div className="space-y-3"> | ||
| {formData.modelMappings.map((mapping, index) => ( | ||
| <div key={index} className="flex items-center gap-3 p-3 bg-muted/50 rounded-lg"> | ||
| <div className="flex-1"> | ||
| <label className="text-xs text-muted-foreground mb-1 block"> | ||
| {t('settings.matchPattern')} | ||
| </label> | ||
| <Input | ||
| type="text" | ||
| value={mapping.pattern} | ||
| onChange={(e) => { | ||
| const newMappings = [...(formData.modelMappings || [])]; | ||
| newMappings[index] = { ...newMappings[index], pattern: e.target.value }; | ||
| updateFormData({ modelMappings: newMappings }); | ||
| }} | ||
| placeholder="*claude*, *sonnet*, *" | ||
| className="font-mono text-sm" | ||
| /> | ||
| </div> | ||
| <ArrowRight size={16} className="text-muted-foreground shrink-0 mt-5" /> | ||
| <div className="flex-1"> | ||
| <label className="text-xs text-muted-foreground mb-1 block"> | ||
| {t('settings.targetModel')} | ||
| </label> | ||
| <ModelInput | ||
| value={mapping.target} | ||
| onChange={(value) => { | ||
| const newMappings = [...(formData.modelMappings || [])]; | ||
| newMappings[index] = { ...newMappings[index], target: value }; | ||
| updateFormData({ modelMappings: newMappings }); | ||
| }} | ||
| placeholder={t('modelInput.selectOrEnter')} | ||
| /> | ||
| </div> | ||
| <Button | ||
| variant="ghost" | ||
| size="icon" | ||
| className="shrink-0 mt-5 text-muted-foreground hover:text-destructive" | ||
| onClick={() => { | ||
| const newMappings = (formData.modelMappings || []).filter((_, i) => i !== index); | ||
| updateFormData({ modelMappings: newMappings }); | ||
| }} | ||
| > | ||
| <Trash2 size={14} /> | ||
| </Button> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| ) : ( | ||
| <div className="text-sm text-muted-foreground p-4 bg-muted/30 rounded-lg text-center"> | ||
| {t('modelMappings.noMappings')} | ||
| </div> | ||
| )} | ||
| </div> |
There was a problem hiding this comment.
删除按钮为纯图标,建议补充可访问性文本。
为图标按钮添加 aria-label/title 可提升可访问性与可用性。
🧩 建议修复
- <Button
+ <Button
variant="ghost"
size="icon"
className="shrink-0 mt-5 text-muted-foreground hover:text-destructive"
+ aria-label={t('common.delete')}
+ title={t('common.delete')}
onClick={() => {
const newMappings = (formData.modelMappings || []).filter((_, i) => i !== index);
updateFormData({ modelMappings: newMappings });
}}
>🤖 Prompt for AI Agents
In `@web/src/pages/providers/components/custom-config-step.tsx` around lines 172 -
245, The delete Button that renders only the Trash2 icon in the modelMappings
list lacks accessible text—update the Button (the one with variant="ghost"
size="icon" wrapping <Trash2 /> inside the map iteration) to include an
aria-label and title (e.g. aria-label={t('routes.modelMapping.removeMapping')}
title={t('routes.modelMapping.removeMapping')}) and mark the <Trash2 /> icon
aria-hidden="true" to avoid duplicate announcements; ensure the i-th removal
onClick logic remains unchanged so accessibility is added without affecting
behavior.
| modelMappings: template.modelMappings, | ||
| logo: template.logoUrl, |
There was a problem hiding this comment.
避免直接复用模板的 modelMappings 引用
如果后续允许在表单中编辑映射,直接复用模板数组/对象可能污染模板数据,影响再次选择模板的初始值。建议做浅/深拷贝后再写入表单状态。
🛠 建议修复
- modelMappings: template.modelMappings,
+ modelMappings: template.modelMappings?.map((m) => ({ ...m })),📝 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.
| modelMappings: template.modelMappings, | |
| logo: template.logoUrl, | |
| modelMappings: template.modelMappings?.map((m) => ({ ...m })), | |
| logo: template.logoUrl, |
🤖 Prompt for AI Agents
In `@web/src/pages/providers/components/select-type-step.tsx` around lines 44 -
45, The template’s modelMappings reference is being assigned directly
(modelMappings: template.modelMappings) which can mutate the original template
if the form allows edits; in the SelectTypeStep component, replace the direct
reference with a copy of template.modelMappings (use a shallow copy for
arrays/objects via spread or a deep copy via JSON.parse(JSON.stringify(...)) or
structuredClone) so the form state owns its own data and does not mutate
template.modelMappings; keep logo: template.logoUrl as-is.
| <div className="relative"> | ||
| <Search | ||
| size={14} | ||
| className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" | ||
| /> | ||
| <Input | ||
| placeholder={t('common.search')} | ||
| value={searchQuery} | ||
| onChange={(e) => setSearchQuery(e.target.value)} | ||
| className="pl-9 w-48" | ||
| /> | ||
| </div> |
There was a problem hiding this comment.
搜索框缺少显式可访问性标签。
建议补充 aria-label(或 label)以提升读屏器可用性。
🧩 建议修复
- <Input
- placeholder={t('common.search')}
- value={searchQuery}
- onChange={(e) => setSearchQuery(e.target.value)}
- className="pl-9 w-48"
- />
+ <Input
+ type="search"
+ aria-label={t('common.search')}
+ placeholder={t('common.search')}
+ value={searchQuery}
+ onChange={(e) => setSearchQuery(e.target.value)}
+ className="pl-9 w-48"
+ />📝 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.
| <div className="relative"> | |
| <Search | |
| size={14} | |
| className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" | |
| /> | |
| <Input | |
| placeholder={t('common.search')} | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| className="pl-9 w-48" | |
| /> | |
| </div> | |
| <div className="relative"> | |
| <Search | |
| size={14} | |
| className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" | |
| /> | |
| <Input | |
| type="search" | |
| aria-label={t('common.search')} | |
| placeholder={t('common.search')} | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| className="pl-9 w-48" | |
| /> | |
| </div> |
🤖 Prompt for AI Agents
In `@web/src/pages/providers/index.tsx` around lines 123 - 134, The search input
lacks an explicit accessibility label; update the Input used in this block (the
Input component bound to searchQuery with onChange={(e) =>
setSearchQuery(e.target.value)}) to include an aria-label (or associate a
visible <label>) such as aria-label={t('common.search')} so screen readers can
identify the field; ensure the Search icon remains decorative (e.g.,
aria-hidden="true" or role="presentation") if applicable.
Summary
/routes/claude等) 添加搜索框,同时过滤已配置路由和可用 Provider* -> minimaxai/minimax-m2.1)Test plan
/routes/claude页面,验证搜索框同时过滤路由列表和 Available ProvidersSummary by CodeRabbit
发版说明
✏️ Tip: You can customize this high-level summary in your review settings.