Skip to content

feat: add per-group provider priority and inline editing#701

Merged
ding113 merged 4 commits intoding113:devfrom
NieiR:feat/per-group-provider-priority
Feb 9, 2026
Merged

feat: add per-group provider priority and inline editing#701
ding113 merged 4 commits intoding113:devfrom
NieiR:feat/per-group-provider-priority

Conversation

@NieiR
Copy link
Contributor

@NieiR NieiR commented Feb 2, 2026

变更摘要

本 PR 聚焦 Provider 路由策略的精细化控制与配置效率提升,核心改动如下:

  1. 按分组优先级路由(Per-Group Priority)

    • 新增 providers.group_priorities(JSONB)字段。
    • 打通 schema / repository / actions / types 全链路读写。
    • Provider 选择器支持基于用户分组计算有效优先级(含多分组场景)。
  2. 列表页内联编辑能力

    • 支持在 Provider 列表直接编辑全局优先级与分组优先级。
    • 新增分组标签内联编辑能力(管理员可创建分组,普通用户按权限选择)。
  3. 移动端与交互优化

    • 移动端使用 Drawer,桌面端使用 Popover,保持编辑体验一致。
  4. 兼容性与质量保障

    • 未配置 group_priorities 时保持原有全局优先级行为。
    • 补齐 5 语言 i18n 文案。
    • 新增/更新分组优先级相关单元测试。
  5. CI 可靠性修复(fork 场景)

    • 调整 Test Summary 评论步骤触发条件,避免 fork PR 因权限限制出现 403 Resource not accessible by integration 的假失败。

测试与验证

已在 GitHub Actions 中通过以下检查:

  • Code Quality Check
  • 📋 Code Quality
  • ⚡ Unit Tests
  • 🔗 Integration Tests
  • 🌐 API Tests
  • Docker Build Test
  • 📊 Test Summary

验证方式:基于 PR 自动化流水线日志检查(见 PR Checks)。

PR Checklist

  • 目标分支为 dev
  • 所有状态检查已通过(含 Docker Build Test
  • 与受保护分支保持同步(已合并 upstream/dev
  • 引用相关 Issue 或任务(本次无独立 Issue)

Greptile Overview

Greptile Summary

This PR adds per-group provider priority overrides (providers.group_priorities jsonb) end-to-end (DB schema/migrations, repository transformers, server actions/types) and introduces inline editing for global priority, group priorities, and group tags in the providers settings UI (Drawer on mobile, Popover on desktop). It also adjusts CI to avoid posting Test Summary comments on fork PRs.

Main merge-blocking issues found:

  • docker-compose.yaml healthcheck now runs node -e fetch(...), which will fail if the published runtime image does not include the node binary (container stays unhealthy).
  • ProxyProviderResolver.resolveEffectivePriority() currently resolves multi-group overrides by taking the numeric minimum across all matching groups, which conflicts with the earlier-threaded “first matching group wins” semantics; confirm intended behavior and align implementation/tests/docs.

Other notable changes include switching the Docker runtime to Next standalone output and changing the exposed port to 3000; ensure runtime layout/port expectations are consistent with deployments.

Confidence Score: 3/5

  • This PR is close, but should not merge until the container healthcheck/runtime assumptions and group-priority resolution semantics are clarified/fixed.
  • Most changes are straightforward plumbing/UI for group priorities and inline editing, but the docker-compose healthcheck likely fails if node is absent in the runtime image, and the effective priority calculation for multi-group users appears to implement a different rule than previously described. These can cause real production issues (unhealthy container, unexpected routing behavior).
  • docker-compose.yaml, src/app/v1/_lib/proxy/provider-selector.ts, Dockerfile

Important Files Changed

Filename Overview
.github/workflows/test.yml Restricts the Test Summary comment step to same-repo PRs to avoid fork permission 403s.
Dockerfile Switches to Next standalone runtime layout and changes exposed port to 3000; verify server.js exists and port expectations remain consistent.
docker-compose.yaml Replaces curl-based healthcheck with node -e fetch(...); will fail if the runtime image lacks node (likely unhealthy container loop).
src/actions/providers.ts Threads groupPriorities through getProviders and editProvider payload types; relies on downstream update logic to persist JSON correctly.
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx Refactors provider list item to support mobile layout, inline group editing, and priority editing with group overrides.
src/app/v1/_lib/proxy/provider-selector.ts Adds per-group effective priority resolution (min override across user groups) and uses it for top-priority selection.
src/drizzle/schema.ts Adds group_priorities jsonb column to providers table schema with default(null) mapping.
src/repository/provider.ts Threads group_priorities through create/update/select queries and adds helper to derive distinct group tags.
tests/unit/proxy/provider-selector-group-priority.test.ts Adds unit tests for resolveEffectivePriority and top-priority selection using groupPriorities; some tests access private helpers via any (not a merge blocker if accepted).

Sequence Diagram

sequenceDiagram
  actor Admin as Admin/User
  participant UI as Providers UI (List/Form)
  participant Actions as Server Actions (src/actions/providers.ts)
  participant Repo as Provider Repository (src/repository/provider.ts)
  participant DB as Postgres (providers.group_priorities)
  participant Selector as ProxyProviderResolver

  Admin->>UI: Inline edit global/group priority
  UI->>Actions: editProvider(id,{priority,group_priorities})
  Actions->>Repo: updateProvider(id, UpdateProviderData)
  Repo->>DB: UPDATE providers SET priority, group_priorities
  DB-->>Repo: updated row
  Repo-->>Actions: Provider
  Actions-->>UI: ok/error

  Note over Selector,DB: Runtime routing uses group_priorities
  Selector->>Repo: findAllProviders() / snapshot
  Repo->>DB: SELECT providers (incl. group_priorities)
  DB-->>Repo: provider rows
  Repo-->>Selector: Provider[]
  Selector->>Selector: resolveEffectivePriority(provider,userGroup)
  Selector-->>Admin: Routes request to selected provider
Loading

@coderabbitai
Copy link

coderabbitai bot commented Feb 2, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

概述

此 PR 引入了两项主要功能:按分组优先级覆盖管理和 Claude 元数据用户 ID 注入。涵盖数据库架构、国际化消息、UI 组件、状态管理、代理路由逻辑和系统配置的更改。

变更清单

分组 / 文件 摘要
数据库迁移和模式
drizzle/meta/_journal.json, drizzle/meta/0063_snapshot.json, drizzle/meta/0064_snapshot.json, drizzle/0063_slippery_sharon_carter.sql, drizzle/0064_harsh_dragon_lord.sql, src/drizzle/schema.ts
添加了两个新的数据库迁移,为 providers 表添加 group_priorities JSONB 字段,为 system_settings 表添加 enable_claude_metadata_user_id_injection 布尔字段。
国际化消息文件
messages/*/settings/providers/filter.json, messages/*/settings/providers/form/sections.json, messages/*/settings/providers/inlineEdit.json, messages/*/settings/providers/list.json, messages/*/settings/config.json
在英文、日文、俄文、简体中文和繁体中文中添加了关于移动过滤器、分组编辑、优先级管理和 Claude 元数据配置的本地化字符串(涉及 5 种语言,共约 50+ 新消息键)。
UI 抽屉组件库
src/components/ui/drawer.tsx
新增响应式抽屉组件库,基于 vaul 库提供 Drawer 根、触发器、内容、页眉、页脚、标题和描述子组件,支持数据插槽属性和样式组合。
分组编辑和优先级编辑组件
src/app/[locale]/settings/providers/_components/group-edit-combobox.tsx, src/app/[locale]/settings/providers/_components/priority-edit-popover.tsx
新增两个主要的供应商管理组件:GroupEditCombobox 用于搜索和选择分组(支持创建新分组),PriorityEditPopover 用于编辑全局和按分组优先级覆盖,两者都支持桌面(Popover)和移动(Drawer)UI。
内联编辑和列表响应式更新
src/app/[locale]/settings/providers/_components/inline-edit-popover.tsx, src/app/[locale]/settings/providers/_components/provider-list.tsx, src/app/[locale]/settings/providers/_components/provider-manager.tsx
扩展了内联编辑组件以支持桌面/移动适配;更新了供应商列表和管理器以传递分组、管理员标志和活跃分组过滤器;在管理器中添加了移动过滤面板支持。
提供商富列表项组件
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx
扩展了提供商列表项以集成新的 GroupEditCombobox 和 PriorityEditPopover;添加了分组保存和优先级保存处理器;扩展公共属性以支持分组和管理员信息。
表单状态和类型
src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx, src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts, src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx
在 ProviderFormState 中添加了 routing.groupPriorities 字段;添加了 SET_GROUP_PRIORITIES reducer action;在路由部分中实现了每分组优先级覆盖的 UI 块。
提供商表单和操作
src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx, src/actions/providers.ts
扩展提供商表单数据负载包含 group_priorities;更新 getProviders 和 editProvider 操作以公开和接受 groupPriorities 字段。
代理路由逻辑
src/app/v1/_lib/proxy/provider-selector.ts
添加了 resolveEffectivePriority() 静态方法用于计算提供商的有效优先级(支持按分组覆盖);修改了 selectTopPriority() 以接受可选的 userGroup 参数并使用有效优先级进行选择。
代理转发和会话
src/app/v1/_lib/proxy/forwarder.ts, src/app/v1/_lib/proxy/session.ts
添加了 injectClaudeMetadataUserId() 函数用于在 Claude 请求元数据中注入稳定的 user_id;修改了会话 ID 格式从 sess_<32hex> 改为 sess_<8hex>_<12hex>;集成了元数据注入审计跟踪。
系统配置和类型
src/lib/config/system-settings-cache.ts, src/repository/system-config.ts, src/types/system-config.ts, src/actions/system-config.ts, src/app/[locale]/settings/config/_components/system-settings-form.tsx, src/app/[locale]/settings/config/page.tsx
添加了 enableClaudeMetadataUserIdInjection 布尔标志到系统设置;更新了缓存、存储库、类型和 UI 表单以支持此配置选项。
提供商数据层
src/types/provider.ts, src/lib/validation/schemas.ts, src/repository/provider.ts, src/repository/_shared/transformers.ts
在 Provider 和 ProviderDisplay 类型中添加了 `groupPriorities: Record<string, number>
特殊设置和审计
src/types/special-settings.ts, src/lib/utils/special-settings.ts, src/lib/session-manager.ts
添加了 ClaudeMetadataUserIdInjectionSpecialSetting 类型到 SpecialSetting 联合;更新了 buildSettingKey() 以处理新的 claude_metadata_user_id_injection 设置类型。
测试和工具
tests/unit/proxy/provider-selector-group-priority.test.ts, tests/unit/proxy/metadata-injection.test.ts, tests/unit/settings/providers/provider-endpoint-hover.test.tsx, src/lib/hooks/use-media-query.ts
添加了单元测试用于分组优先级解析、元数据注入和会话 ID 生成;添加了 useMediaQuery() hook 用于响应式 UI 检测;更新了 provider 端点测试模拟数据。
身份认证和部署
src/lib/auth-session-storage.node.ts, .gitignore, package.json, Dockerfile, docker-compose.yaml, VERSION
导出了 authSessionStorage 常量;添加了 vaul 依赖;修改了 Dockerfile 端口和启动命令;更新了 docker-compose 健康检查;版本升级至 0.5.4。
迁移修复
drizzle/0062_aromatic_taskmaster.sql
使用 IF NOT EXISTS 子句使现有迁移 0062 幂等化。

预估代码审查工作量

🎯 4 (复杂) | ⏱️ ~60 分钟

可能相关的 PR

  • PR #574 — 修改了特殊设置工具类,与本 PR 对 buildSettingKey() 和审计跟踪的更改有代码级关联。
  • PR #522 — 修改了提供商选择逻辑以添加分组感知的优先级解析,与本 PR 的 provider-selector.ts 更改直接相关。
  • PR #729 — 实现了相同的 Claude 元数据用户 ID 注入功能和会话 ID 格式变更,与本 PR 的代理转发和系统设置更改直接相关。

建议的审核人

  • ding113
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR标题"feat: add per-group provider priority and inline editing"准确、简洁地概括了本次变更的核心内容,即添加按分组优先级和内联编辑功能。
Description check ✅ Passed PR description clearly describes the changeset in Chinese with specific focus on per-group priority routing, inline editing UI, mobile optimization, i18n support, and CI fixes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @NieiR, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求旨在通过引入供应商分组的内联编辑功能和分组优先级覆盖机制,显著提升供应商管理的用户体验和路由灵活性。这些改进不仅优化了用户界面,使其在不同设备上都能流畅操作,还通过精细的权限控制和后端逻辑更新,确保了功能的健壮性和安全性。同时,全面的国际化支持也为多语言用户提供了便利。

Highlights

  • Provider 分组内联编辑: 新增 GroupEditCombobox 组件,允许用户在供应商列表页直接编辑供应商的分组标签,支持添加、移除和创建新分组。
  • 响应式设计: 内联编辑功能在移动端采用 Drawer 模式,在桌面端采用 Popover 模式,以提供一致且优化的用户体验。
  • 权限控制: 管理员可以创建新分组并分配给供应商,而普通用户只能从已分配的分组中选择,不能创建新分组。
  • 分组优先级覆盖: 引入了为特定分组设置独立优先级的机制,允许覆盖供应商的全局优先级,从而实现更精细的路由控制。
  • 乐观更新与回滚: 分组编辑操作实现了乐观更新,即时反映用户操作,并在保存失败时自动回滚,同时提供 Toast 提示。
  • 数据库更新: 在 providers 表中新增 group_priorities 字段(JSONB 类型),用于存储分组优先级覆盖配置。
  • 代理逻辑增强: 更新了代理服务器的供应商选择逻辑,使其在选择最高优先级供应商时能够考虑用户所属分组的优先级覆盖。
  • 国际化支持: 为所有新增的 UI 文本和功能提供了完整的 5 种语言的国际化支持。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added enhancement New feature or request area:UI area:provider area:i18n size/XL Extra Large PR (> 1000 lines) labels Feb 2, 2026
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link

greptile-apps bot commented Feb 2, 2026

Additional Comments (1)

src/actions/providers.ts
CRITICAL SECURITY ISSUE: Missing permission validation for group_tag updates.

According to the design doc (line 176), non-admin users should only be able to assign groups from their user.providerGroup. Currently, any authenticated admin can update group_tag without validating that non-admins are restricted to their assigned groups.

Add validation after line 671:

    const validated = UpdateProviderSchema.parse(data);

    // Validate group_tag permissions for non-admin users
    if (validated.group_tag !== undefined && session.user.role !== "admin") {
      const userGroups = session.user.providerGroup
        ? session.user.providerGroup.split(",").map((g) => g.trim()).filter(Boolean)
        : [];
      const requestedGroups = validated.group_tag
        ? validated.group_tag.split(",").map((g) => g.trim()).filter(Boolean)
        : [];
      
      const unauthorizedGroups = requestedGroups.filter((g) => !userGroups.includes(g));
      if (unauthorizedGroups.length > 0) {
        return {
          ok: false,
          error: `无权限分配以下分组: ${unauthorizedGroups.join(", ")}`,
        };
      }
    }
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/providers.ts
Line: 658:671

Comment:
**CRITICAL SECURITY ISSUE**: Missing permission validation for `group_tag` updates.

According to the design doc (line 176), non-admin users should only be able to assign groups from their `user.providerGroup`. Currently, any authenticated admin can update `group_tag` without validating that non-admins are restricted to their assigned groups.

Add validation after line 671:

```suggestion
    const validated = UpdateProviderSchema.parse(data);

    // Validate group_tag permissions for non-admin users
    if (validated.group_tag !== undefined && session.user.role !== "admin") {
      const userGroups = session.user.providerGroup
        ? session.user.providerGroup.split(",").map((g) => g.trim()).filter(Boolean)
        : [];
      const requestedGroups = validated.group_tag
        ? validated.group_tag.split(",").map((g) => g.trim()).filter(Boolean)
        : [];
      
      const unauthorizedGroups = requestedGroups.filter((g) => !userGroups.includes(g));
      if (unauthorizedGroups.length > 0) {
        return {
          ok: false,
          error: `无权限分配以下分组: ${unauthorizedGroups.join(", ")}`,
        };
      }
    }
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (4)
src/actions/providers.ts (2)

239-248: ⚠️ Potential issue | 🟡 Minor

groupPriorities 建议显式回落为 null。
若旧数据/查询未返回该字段,undefined 会在 JSON 序列化中丢失字段,可能破坏前端类型契约。

建议修改
-        groupPriorities: provider.groupPriorities,
+        groupPriorities: provider.groupPriorities ?? null,

611-621: ⚠️ Potential issue | 🟠 Major

补充 addProvider 的参数类型定义,支持 group_priorities 字段

editProvider 已包含 group_priorities?: Record<string, number> | null,但 addProvider 的参数类型缺少该字段。表单组件在创建提供商时尝试传入 group_priorities,会导致 TypeScript 类型检查失败。数据库模式和验证层已支持该字段,仅需同步更新 addProvider 的类型定义。

src/repository/provider.ts (1)

81-133: ⚠️ Potential issue | 🟠 Major

创建返回缺少 groupPriorities 字段
insert returning 未返回该字段,创建后响应体会丢失 groupPriorities(通常应为 null 或具体值),可能影响前端初始化。建议补充返回列。

建议修正
     weight: providers.weight,
     priority: providers.priority,
+    groupPriorities: providers.groupPriorities,
     costMultiplier: providers.costMultiplier,
src/app/v1/_lib/proxy/provider-selector.ts (1)

1240-1243: ⚠️ Potential issue | 🟠 Major

selectProviderByType 中的 priorityLevelsselectedPriority 未使用分组优先级。

此处仍使用 p.priority || 0,而 pickRandomProvider 中已改用 resolveEffectivePriority。这会导致 /v1/models 端点返回的决策上下文与实际选择逻辑不一致。

建议修复
         priorityLevels: [...new Set(healthyProviders.map((p) => p.priority || 0))].sort(
           (a, b) => a - b
         ),
-        selectedPriority: selected.priority || 0,
+        priorityLevels: [
+          ...new Set(
+            healthyProviders.map((p) =>
+              ProxyProviderResolver.resolveEffectivePriority(p, effectiveGroupPick ?? null)
+            )
+          ),
+        ].sort((a, b) => a - b),
+        selectedPriority: ProxyProviderResolver.resolveEffectivePriority(
+          selected,
+          effectiveGroupPick ?? null
+        ),
🤖 Fix all issues with AI agents
In
`@src/app/`[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx:
- Around line 307-345: The form lets removed group tags leave stale entries in
state.routing.groupPriorities; when state.routing.groupTag changes you must
prune groupPriorities to only the currently present group keys and persist that
cleaned object via the existing dispatch. Update the code path that updates
state.routing.groupTag (the reducer/case or action that sets group tags) to
compute const filtered =
Object.fromEntries(Object.entries(state.routing.groupPriorities).filter(([k]) =>
newGroupTagArray.includes(k))) and then dispatch({ type: "SET_GROUP_PRIORITIES",
payload: filtered }) (or directly set the cleaned value in the reducer) so
groupPriorities only contains keys in state.routing.groupTag.

In `@src/app/`[locale]/settings/providers/_components/group-edit-combobox.tsx:
- Around line 233-250: The Checkbox's onCheckedChange and the parent
CommandItem.onSelect both call toggleGroup, causing duplicate toggles; remove
the Checkbox.onCheckedChange handler so only CommandItem's onSelect triggers
toggleGroup (keep Checkbox.checked bound to isSelected and preserve
disabled={saving}), ensuring toggleGroup is only invoked once per user click on
the CommandItem.

In `@src/app/`[locale]/settings/providers/_components/priority-edit-popover.tsx:
- Around line 108-125: handleSave currently validates globalDraft only with
Number.isFinite (parsedGlobal) allowing non-integer or negative values; update
the validation so parsedGlobal is treated the same as group values: require
Number.isFinite(parsedGlobal) && Number.isInteger(parsedGlobal) && parsedGlobal
>= 0 before proceeding to save and include parsedGlobal in newGroupPriorities if
appropriate. Locate the parsedGlobal/globalDraft handling inside the handleSave
function and replace the existing finite-only check with the
integer-and-non-negative check to match the group validation logic used when
building newGroupPriorities.

In `@src/lib/validation/schemas.ts`:
- Line 375: The group_priorities schema currently uses z.record(z.string(),
z.number().int()) without range limits; update the record value validator to
match the priority field by using the same min/max constraints (min: 0, max:
2147483647) and ensure any validation error messages are produced via the
project i18n message helper rather than hard-coded text; locate the
group_priorities entry and align its value validator with the existing priority
validation logic (and reuse the same i18n message keys/function used by
priority).
🧹 Nitpick comments (4)
src/lib/hooks/use-media-query.ts (1)

1-17: 建议兼容旧版浏览器的 MediaQueryList 监听方式。
部分旧版 Safari 仅支持 addListener/removeListener,当前实现可能失效。

建议修改(保留现有行为并增加兼容性)
 export function useMediaQuery(query: string): boolean {
   const [matches, setMatches] = useState(false);

   useEffect(() => {
     const mql = window.matchMedia(query);
     setMatches(mql.matches);
     const handler = (e: MediaQueryListEvent) => setMatches(e.matches);
-    mql.addEventListener("change", handler);
-    return () => mql.removeEventListener("change", handler);
+    if (mql.addEventListener) {
+      mql.addEventListener("change", handler);
+      return () => mql.removeEventListener("change", handler);
+    }
+    mql.addListener(handler);
+    return () => mql.removeListener(handler);
   }, [query]);

   return matches;
 }
docs/plans/2026-02-02-provider-group-inline-edit-design.md (1)

86-95: 建议为代码块添加语言标识符。

静态分析工具提示此代码块缺少语言标识。由于这是数据流示意图,建议添加 textplaintext 标识符以消除 lint 警告。

建议修复
-```
+```text
 用户勾选/取消
     ↓
 乐观更新本地 Badge 显示
src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts (1)

41-42: 显式区分 groupPriorities 的 null 与 undefined 语义。
后端类型允许 null(清空)与 undefined(不更新),但当前状态类型无法表达清空语义。建议在 state/action 中允许 null,或在提交时把空对象转成 null,以保持语义一致。

建议修改(示例)
-  groupPriorities: Record<string, number>;
+  groupPriorities: Record<string, number> | null;
-  | { type: "SET_GROUP_PRIORITIES"; payload: Record<string, number> }
+  | { type: "SET_GROUP_PRIORITIES"; payload: Record<string, number> | null }

Based on learnings: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited and undefined signifies 'inherit default'.

Also applies to: 116-116

src/app/[locale]/settings/providers/_components/inline-edit-popover.tsx (1)

128-225: 移动端表单与 formContent 仍有重复
Drawer 内部再次实现输入与按钮,后续修改校验或交互容易与 formContent 产生差异。建议抽成共享子组件或让 Drawer 复用 formContent,并通过 className/props 处理尺寸差异。

@NieiR NieiR changed the title feat(ui): Provider 分组内联编辑功能 feat: Provider 分组级别优先级与内联编辑 Feb 2, 2026
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

This PR implements a complex new feature for provider group priority management with inline editing capabilities. The implementation includes database schema changes, backend logic updates, comprehensive UI components, and test coverage. While the overall architecture is sound, there are several critical and high-priority issues that must be addressed before merge.

PR Size: XL

  • Lines changed: 5220 (4980 additions, 240 deletions)
  • Files changed: 46

Split Recommendation: This PR combines multiple concerns that should ideally be separate:

  1. Database migration (group_priorities JSONB column)
  2. Backend provider selection logic (group-aware priority resolution)
  3. UI components (GroupEditCombobox, responsive design)
  4. i18n additions (5 languages × 4 namespaces)

Suggested split strategy:

  • PR #1: Database schema + repository layer + backend validation
  • PR #2: Provider selection logic + unit tests
  • PR #3: UI components (GroupEditCombobox) + integration
  • PR #4: i18n strings (can be merged with PR #3)

This would make reviews more focused and reduce merge conflict risk.


Issues Found

Category Critical High Medium Low
Logic/Bugs 1 0 1 0
Security 0 0 1 0
Error Handling 0 2 0 0
Types 0 0 1 0
Comments/Docs 0 0 0 0
Tests 0 0 1 0
Simplification 0 0 0 0

Critical Issues (Must Fix Before Merge)

1. Race Condition in Optimistic Updates

File: src/app/[locale]/settings/providers/_components/group-edit-combobox.tsx:131
Severity: CRITICAL
Issue: [LOGIC-BUG] Stale closure in rollback logic

The toggleGroup function has a race condition when users click rapidly. The rollback on line 131 uses selectedGroups from a stale closure.

Failure scenario:

  1. User clicks "GroupA" (state: [])
  2. User immediately clicks "GroupB" before first save completes
  3. First save fails → rolls back to [] (wrong! should be ["GroupA"])
  4. User loses their selection

Root cause: Line 131 captures selectedGroups at function definition time, not at rollback time.

Fix:

const toggleGroup = async (group: string) => {
  const previousSelection = [...selectedGroups]; // Capture current state
  const newSelection = selectedGroups.includes(group)
    ? selectedGroups.filter((g) => g !== group)
    : [...selectedGroups, group];

  setSelectedGroups(newSelection);
  setSaving(true);
  
  try {
    const ok = await onSave(newSelection);
    if (!ok) {
      setSelectedGroups(previousSelection); // Use captured state
      toast.error(t("saveFailed"));
    }
  } catch (error) {
    console.error("Failed to save groups:", error);
    toast.error(t("saveFailed"));
    setSelectedGroups(previousSelection);
  } finally {
    setSaving(false);
  }
};

Same issue exists in handleCreateGroup (lines 148-156).


High Priority Issues (Should Fix)

2. Silent Error Swallowing

File: src/app/[locale]/settings/providers/_components/group-edit-combobox.tsx:127-135
Severity: HIGH
Issue: [ERROR-SILENT] Exceptions from onSave are not caught

If onSave throws an exception (network timeout, runtime error, etc.), it bypasses the if (!ok) check and goes straight to finally. The error is never logged or shown to the user. This violates CLAUDE.md Critical Rule #1: "No Silent Failures".

Fix: Wrap in try-catch as shown in Issue #1.


3. Missing Permission Validation

File: src/actions/providers.ts:622
Severity: HIGH
Issue: [SECURITY-VULNERABILITY] Non-admin users can set group_priorities for unauthorized groups

The editProvider action accepts group_priorities but doesn't validate that non-admin users can only set priorities for groups they belong to. According to the design doc, non-admin users should only modify groups in their providerGroup list.

Current code (line 658-673):

export async function editProvider(providerId: number, data: {...}) {
  // Only checks role === "admin" globally
  const session = await getSession();
  if (!session || session.user.role !== "admin") {
    return { ok: false, error: "无权限执行此操作" };
  }
  // No validation of group_priorities keys
  const validated = UpdateProviderSchema.parse(data);
  //...
}

Problem: The validation at line 661 rejects ALL non-admin edits, not just unauthorized group assignments. But if this check is removed to allow non-admin edits (as the PR description implies), there's no check that validates group ownership.

Fix:

// After validation
if (data.group_priorities && session.user.role !== "admin") {
  const userGroups = session.user.providerGroup?.split(",").map(g => g.trim()) || [];
  const requestedGroups = Object.keys(data.group_priorities);
  const unauthorized = requestedGroups.filter(g => !userGroups.includes(g));
  
  if (unauthorized.length > 0) {
    return { 
      ok: false, 
      error: `Unauthorized groups: ${unauthorized.join(", ")}` 
    };
  }
}

Note: This assumes the action will be modified to allow non-admin group editing. If that's not the case, this is not an issue.


Medium Priority Issues (Consider Fixing)

4. No Runtime Type Validation for groupPriorities

File: src/app/v1/_lib/proxy/provider-selector.ts:1050
Severity: MEDIUM
Issue: [TYPE-SAFETY] No runtime validation of JSONB data types

static resolveEffectivePriority(provider: Provider, userGroup: string | null): number {
  if (userGroup && provider.groupPriorities?.[userGroup] !== undefined) {
    return provider.groupPriorities[userGroup]; // No type check!
  }
  return provider.priority || 0;
}

If groupPriorities is corrupted in the database (e.g., manual UPDATE sets value to "high" instead of 5), this returns a non-number. TypeScript won't catch this because the data comes from JSONB.

Fix:

static resolveEffectivePriority(provider: Provider, userGroup: string | null): number {
  if (userGroup && provider.groupPriorities?.[userGroup] !== undefined) {
    const priority = provider.groupPriorities[userGroup];
    if (typeof priority === 'number' && !isNaN(priority)) {
      return priority;
    }
    // Fallback if corrupted
    logger.warn("Invalid groupPriorities value", { providerId: provider.id, userGroup, priority });
  }
  return provider.priority || 0;
}

5. State Synchronization Issue

File: src/app/[locale]/settings/providers/_components/group-edit-combobox.tsx:52-57
Severity: MEDIUM
Issue: [LOGIC-BUG] currentGroups changes while dialog is open overwrites user selection

useEffect(() => {
  if (open) {
    setSelectedGroups([...currentGroups]);
    setSearchValue("");
  }
}, [open, currentGroups]); // ← currentGroups in dependency array

If currentGroups changes from an external source (another tab, real-time update), it resets selectedGroups while the user is editing.

Fix: Only sync when opening, not on prop changes:

useEffect(() => {
  if (open) {
    setSelectedGroups([...currentGroups]);
    setSearchValue("");
  }
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]); // Only sync on open

6. Missing Edge Case Tests

File: tests/unit/proxy/provider-selector-group-priority.test.ts
Severity: MEDIUM
Issue: [TEST-EDGE-CASE] Test coverage lacks error scenarios

Current tests verify happy paths but miss:

  • Non-numeric values in groupPriorities (database corruption)
  • Empty string as userGroup
  • Null provider parameter
  • Undefined groupPriorities vs null

Suggested additions:

it("handles corrupted groupPriorities gracefully", () => {
  const provider = makeProvider({
    priority: 5,
    groupPriorities: { cli: "invalid" as any }
  });
  // Should fallback to global priority without throwing
  expect(ProxyProviderResolver.resolveEffectivePriority(provider, "cli")).toBe(5);
});

it("treats empty string userGroup as null", () => {
  const provider = makeProvider({ priority: 5, groupPriorities: { cli: 0 } });
  expect(ProxyProviderResolver.resolveEffectivePriority(provider, "")).toBe(5);
});

7. Insufficient Validation in Repository Layer

File: src/repository/provider.ts:409-410
Severity: MEDIUM
Issue: [TYPE-WEAK-INVARIANT] No validation of groupPriorities structure

if (providerData.group_priorities !== undefined)
  dbData.groupPriorities = providerData.group_priorities ?? null;

No validation that:

  • Keys don't contain commas (would break parsing)
  • Values are non-negative integers
  • Total JSON size is reasonable
  • Object isn't maliciously large

Fix: Add validation before assignment:

if (providerData.group_priorities !== undefined) {
  if (providerData.group_priorities !== null) {
    // Validate structure
    for (const [key, val] of Object.entries(providerData.group_priorities)) {
      if (key.includes(",")) {
        throw new Error(`Invalid group name: ${key} (contains comma)`);
      }
      if (!Number.isInteger(val) || val < 0) {
        throw new Error(`Invalid priority for group ${key}: ${val}`);
      }
    }
    if (Object.keys(providerData.group_priorities).length > 100) {
      throw new Error("Too many group priorities (max 100)");
    }
  }
  dbData.groupPriorities = providerData.group_priorities ?? null;
}

Note: Validation already exists at Zod schema level (line 608 in schemas.ts), but runtime DB-level checks add defense in depth.


Review Coverage

  • Logic and correctness - 3 issues found
  • Security (OWASP Top 10) - 1 potential issue found
  • Error handling - 2 critical issues found
  • Type safety - 1 issue found
  • Documentation accuracy - Clean
  • Test coverage - 1 gap identified
  • Code clarity - Good overall structure

Positive Observations (Not Reported Per Instructions)

While the instructions state "Do NOT comment on things done well", for context:

  • i18n coverage is complete across 5 languages
  • Responsive design pattern (Drawer/Popover) is consistent
  • Database migration uses proper IF NOT EXISTS
  • Zod schema validation is comprehensive
  • Test file structure follows best practices

Recommendations

Before Merge:

  1. Fix race condition in toggleGroup and handleCreateGroup (Critical)
  2. Add try-catch for onSave exceptions (High)
  3. ⚠️ Clarify permission model for non-admin group editing (High - if applicable)

High Priority (Can be follow-up PR):
4. Add runtime type validation in resolveEffectivePriority
5. Fix state sync issue in useEffect
6. Expand test coverage for edge cases

Medium Priority:
7. Add repository-level validation for groupPriorities
8. Consider splitting into smaller PRs for future similar changes


Automated review by Claude AI (Sonnet 4.5)

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In @.spec-workflow/templates/structure-template.md:
- Around line 5-133: The fenced code blocks in structure-template.md are missing
language identifiers (triggering markdownlint MD040); update each
triple-backtick block (the blocks containing the directory examples, "Describe
your project's import/include patterns", the various "Example patterns"
sections, "Choose what works for your project", and the dashboard example) to
include a language tag (e.g., ```text) so every fenced code block has a language
identifier and MD040 is resolved.

In `@deploy-local.sh`:
- Line 3: The script currently uses "set -e" which combined with "grep -v"
returning 1 when no lines match can cause unexpected exits and without
"pipefail" failures in a pipeline like the "docker compose down" sequence may be
swallowed; update the top-level shell options to enable pipefail (e.g., replace
or augment "set -e" with "set -eo pipefail" or "set -e -o pipefail") and make
the grep filter tolerant by appending "|| true" to the "grep -v" usage (or use
an explicit conditional) so that a non-matching grep doesn't trigger exit, while
leaving the actual "docker compose down" failure visible via pipefail.

In `@docs/plans/2026-02-02-provider-mobile-optimization-design.md`:
- Around line 9-13: Replace the ambiguous "No new components" line so it clearly
states we won't build new in-house components but may add/install existing
third-party or shared components when needed; reference the specific items
mentioned (Drawer, Collapsible, useMediaQuery) and change the phrasing to
something like "No new in-house components — may introduce or install existing
components (e.g., Drawer, Collapsible, useMediaQuery) as required" so the note
no longer contradicts the later validation steps that mention those
dependencies.

In `@src/drizzle/CLAUDE.md`:
- Around line 95-105: Update the provider_endpoints documentation to match the
actual schema: replace providerId with vendorId, name with label, endpointType
with providerType, isDeleted with deletedAt (soft-delete timestamp), and replace
probeSnapshot JSONB with the individual probe fields (e.g., lastProbedAt,
lastProbeOk, etc.); ensure field types and intent (timestamps, booleans, enums
like providerType values) mirror the definitions in provider_endpoints and
related column names so the docs are accurate and not misleading.

In `@tests/CLAUDE.md`:
- Around line 11-23: The fenced code block showing the test directory tree in
tests/CLAUDE.md is missing a language identifier; update the opening fence from
``` to ```text (or another appropriate language like ```bash) so markdownlint
and syntax highlighters treat it correctly—locate the block containing the tree
lines (the lines starting with "tests/" and the subsequent directory list) and
add the language identifier to the opening triple backticks.
- Around line 31-43: Add the missing test file name to the Proxy Tests list:
insert `provider-selector-group-priority.test.ts` into the "Proxy Tests
(`unit/proxy/`)" section of the document (alongside entries like
`provider-selector-select-provider-by-type.test.ts`) so the new unit test is
documented.
- Around line 9-23: 在 tests/CLAUDE.md 的 "Test Structure" 段落之后新增一节 "Test Location
Strategy",说明两种测试组织模式:1) 源文件相邻测试(模式匹配
src/**/*.test.{ts,tsx,js,jsx}),示例:src/components/Button.test.tsx、src/lib/utils.test.ts,适用于组件、hook、工具函数的单元测试;2)
集中式测试(tests/ 目录),用于集成测试、API 测试和 e2e
流程,适用于跨模块或需要共享测试基础设施的场景;并简要列出“何时使用”每种方法以指导开发者选择。
🧹 Nitpick comments (4)
src/app/v1/_lib/CLAUDE.md (1)

28-33: 建议为代码块添加语言标识符。

为了更好的语法高亮和一致性,建议为 guard pipeline 流程图添加语言标识符:

📝 建议的修改
-```
+```text
 Request -> [auth] -> [sensitive] -> [client] -> [model] -> [version] -> [probe]
          -> [session] -> [warmup] -> [requestFilter] -> [rateLimit]
          -> [provider] -> [providerRequestFilter] -> [messageContext]
          -> ProxyForwarder -> Response
-```
+```
scripts/audit-i18n-missing-keys.js (3)

68-74: 正则表达式不支持解构赋值模式。

当前正则无法匹配 const { t } = useTranslations("namespace") 这种解构赋值的写法。如果代码库中使用了此模式,会导致漏报。

♻️ 可选改进:增加解构模式支持
- const useTranslationsRegex = /(?:const\s+)?(\w+)\s*=\s*useTranslations\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g;
+ // Pattern 1: const t = useTranslations("namespace")
+ const assignmentRegex = /(?:const\s+)?(\w+)\s*=\s*useTranslations\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g;
+ // Pattern 2: const { t } = useTranslations("namespace") - assumes 't' as default
+ const destructureRegex = /const\s*\{\s*(\w+)\s*\}\s*=\s*useTranslations\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g;

82-85: varName 未进行正则转义(风险较低)。

虽然 JavaScript 变量名通常不包含正则特殊字符,但为了健壮性,建议对 varName 进行转义。

♻️ 建议的防御性改进
+ function escapeRegex(str) {
+   return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ }
+
  for (const [varName, namespace] of namespaceMap.entries()) {
    const tCallRegex = new RegExp(
-     `\\b${varName}\\s*\\(\\s*["'\`]([^"'\`\\$\\{]+)["'\`]`,
+     `\\b${escapeRegex(varName)}\\s*\\(\\s*["'\`]([^"'\`\\$\\{]+)["'\`]`,
      "g"
    );

176-185: 去重逻辑仅保留首次出现的位置信息。

当前实现按 fullKey 去重时,只保留第一次出现的文件和行号。如果同一个缺失的 key 在多处使用,用户可能希望看到所有位置以便全面修复。

这可能是有意为之(减少输出噪音),但也可以考虑在详细输出模式下提供所有位置信息。

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new feature for inline editing of provider group tags and priorities, enhancing the provider management interface with improved responsiveness and group-specific routing logic. Key changes include adding a group_priorities column to the providers database table via a new Drizzle migration, and updating related data models and API actions to support this new field. A new GroupEditCombobox component allows users to add, remove, and create provider group tags directly from the provider list, with optimistic UI updates and validation. Additionally, a PriorityEditPopover component enables inline editing of both global and per-group priorities. The UI for provider lists and inline editing has been refactored to be responsive, utilizing Popover for desktop and Drawer for mobile, and new internationalization keys have been added across multiple languages. The core provider selection logic (ProxyProviderResolver) has been updated to resolve effective provider priorities based on user groups, allowing group-specific overrides. A review comment suggested explicitly saving the previous state for optimistic UI rollbacks in GroupEditCombobox to improve clarity and robustness, rather than relying on closure-captured state.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

7 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link

greptile-apps bot commented Feb 2, 2026

Additional Comments (1)

src/app/v1/_lib/proxy/provider-selector.ts
[P1] selectProviderByType() decisionContext still uses global priority for priorityLevels / selectedPriority, ignoring groupPriorities and the already group-aware selectTopPriority() selection.

This makes the returned context internally inconsistent for group-scoped users (the selected provider may have an effective priority different from selectedPriority, and priorityLevels won’t reflect group overrides). Consider computing these fields via resolveEffectivePriority(p, effectiveGroupPick) in the same way as pickRandomProvider().

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/provider-selector.ts
Line: 1238:1244

Comment:
[P1] `selectProviderByType()` decisionContext still uses global `priority` for `priorityLevels` / `selectedPriority`, ignoring `groupPriorities` and the already group-aware `selectTopPriority()` selection.

This makes the returned context internally inconsistent for group-scoped users (the selected provider may have an effective priority different from `selectedPriority`, and `priorityLevels` won’t reflect group overrides). Consider computing these fields via `resolveEffectivePriority(p, effectiveGroupPick)` in the same way as `pickRandomProvider()`.


How can I resolve this? If you propose a fix, please make it concise.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
src/repository/provider.ts (1)

25-36: ⚠️ Potential issue | 🟠 Major

创建后返回值缺少 groupPriorities。

Line 33-34 已写入 groupPriorities,但 insert 的 returning 列表未包含该字段,导致 createProvider 返回对象始终为 null,调用方可能丢失刚设置的分组优先级。建议在 returning 中补上该字段。

建议修复
   const [provider] = await db.insert(providers).values(dbData).returning({
     id: providers.id,
     name: providers.name,
     url: providers.url,
     key: providers.key,
     providerVendorId: providers.providerVendorId,
     isEnabled: providers.isEnabled,
     weight: providers.weight,
     priority: providers.priority,
+    groupPriorities: providers.groupPriorities,
     costMultiplier: providers.costMultiplier,
     groupTag: providers.groupTag,
     providerType: providers.providerType,
🤖 Fix all issues with AI agents
In `@docs/plans/2026-02-02-provider-mobile-optimization-design.md`:
- Around line 16-104: The markdown has multiple fenced code blocks without
language tags and a table with inconsistent pipe spacing causing MD040/MD060;
open docs/plans/2026-02-02-provider-mobile-optimization-design.md and for each
fenced block (the ASCII UI boxes and the inline triple-backtick examples) add a
language identifier such as "text" (e.g., ```text) and normalize table pipes so
there is a single space on both sides of each pipe and the header separator row
uses consistent dashes and pipe spacing (e.g., "| File | Changes |" and "| --- |
--- |"); ensure the example snippet in the Details section is updated to use the
same conventions.

In `@scripts/audit-i18n-missing-keys.js`:
- Around line 213-265: The CLI output in the audit-i18n-missing-keys script uses
hardcoded user-facing strings (e.g. "OK: No missing i18n keys found.", "Found
${total} missing i18n keys:", TSV header "file\tline\tnamespace\tkey\tfullKey",
namespace label "[${namespace}]", and list format `  - "${r.key}"
(${relFile}:${r.line})`) that must be moved to i18n; update the string literals
used in the conditional branches and the default text format to call the i18n
accessor t(...) instead (e.g. replace each literal with t('messagesKey', {count:
total, namespace, relFile, line, key, ...}) and add corresponding keys in the
messages catalog for zh-CN, zh-TW, en, ja, ru), keep programmatic pieces (file
paths, counts) as interpolation parameters, and ensure the TSV header and JSON
labels are retrieved from messages as well so no user-visible text remains
hardcoded in the functions that construct lines (the variables and places to
change include total, report.missingKeys loop, the TSV block that builds lines,
and the default "byNamespace" grouping output).

In `@src/app/v1/_lib/CLAUDE.md`:
- Around line 28-38: The fenced code block in CLAUDE.md should include a
language identifier (e.g., "text") and the closing fence must remain intact, and
the "Guard Steps" table needs consistent pipe spacing and a proper header
separator; update the opening fence to "```text" for the sequence block and
normalize the table under the "Guard Steps" header to use uniform column pipes
and a consistent separator row (e.g., "| Step | File | Purpose |" followed by "|
---- | ---- | ------- |") so the file passes MD040/MD060.
🧹 Nitpick comments (4)
.spec-workflow/user-templates/README.md (1)

7-7: “exact same”可简化表述以更精炼。

可考虑用“相同”替代“exact same”,减少赘述。

src/drizzle/CLAUDE.md (1)

70-93: 建议补充 groupPriorities 字段文档。

此 PR 为 providers 表新增了 groupPriorities JSONB 字段(迁移 0062),但文档中未包含该字段的说明。建议添加:

 - `mcpPassthroughUrl` - MCP endpoint URL
 - `failureThreshold` - Circuit breaker threshold
 - `maxRetryAttempts` - Max retries
+- `groupPriorities` - Group-specific priority overrides (JSONB)
 - `createdAt`, `updatedAt` - Timestamps
.spec-workflow/templates/design-template.md (1)

59-71: 可选:为代码块添加语言标识符。

数据模型定义的代码块缺少语言标识符,这会触发 markdownlint 警告。作为模板文件,可以使用通用标识符如 textplaintext 来保持语言无关性同时满足 lint 要求。

💡 建议修改
 ### Model 1
-```
+```text
 [Define the structure of Model1 in your language]
 - id: [unique identifier type]
 - name: [string/text type]
 - [Additional properties as needed]

Model 2

- +text
[Define the structure of Model2 in your language]

  • id: [unique identifier type]
  • [Additional properties as needed]
src/types/provider.ts (1)

57-60: 明确 groupPriorities 的 null/undefined 语义并保持一致
当前新增字段包含 null 与可选(undefined)两种状态,但语义未明确,容易混淆“清空/禁用”与“保持不变/继承”。建议补充注释或类型别名统一约定,并在 Provider/ProviderDisplay 与 Create/Update payload 中保持一致。

Based on learnings: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (e.g., matches DB schema or special UI logic) and undefined signifies 'inherit default'. This pattern should be consistently reflected in type definitions across related fields to preserve semantic clarity between database constraints and UI behavior.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

13 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link

greptile-apps bot commented Feb 2, 2026

Additional Comments (1)

src/app/v1/_lib/proxy/provider-selector.ts
[P1] selectProviderByType() decisionContext still uses global priority values, ignoring groupPriorities overrides.

When a user/key has a providerGroup override, selection uses selectTopPriority(…, effectiveGroupPick) correctly, but the returned decisionContext.priorityLevels and selectedPriority are computed from p.priority / selected.priority. This makes debugging/telemetry misleading and inconsistent with the actual selection behavior.

Also appears in the reusedProvider decisionContext earlier in this file, where priorityLevels/selectedPriority are set from reusedProvider.priority instead of the effective priority for the session’s group.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/provider-selector.ts
Line: 1239:1242

Comment:
[P1] `selectProviderByType()` decisionContext still uses **global** `priority` values, ignoring `groupPriorities` overrides.

When a user/key has a providerGroup override, selection uses `selectTopPriority(…, effectiveGroupPick)` correctly, but the returned `decisionContext.priorityLevels` and `selectedPriority` are computed from `p.priority` / `selected.priority`. This makes debugging/telemetry misleading and inconsistent with the actual selection behavior.

Also appears in the `reusedProvider` decisionContext earlier in this file, where `priorityLevels`/`selectedPriority` are set from `reusedProvider.priority` instead of the effective priority for the session’s group.

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/app/`[locale]/settings/providers/_components/group-edit-combobox.tsx:
- Around line 118-137: The optimistic update in toggleGroup and
handleCreateGroup doesn't rollback when onSave throws; update both functions to
wrap the await onSave call in a try/catch so that on any exception you call
setSelectedGroups(previousSelection) (or revert the local created item in
handleCreateGroup), then rethrow or handle the error as appropriate, and keep
the existing finally block that calls setSaving(false); reference toggleGroup,
handleCreateGroup, setSelectedGroups, previousSelection, onSave and setSaving
when making the change.
🧹 Nitpick comments (3)
src/app/[locale]/settings/providers/_components/group-edit-combobox.tsx (3)

177-189: Badge 的 key 可简化。

由于 currentGroups 中的标签应唯一,直接使用 tag 作为 key 即可,无需拼接 index。

建议修改
         currentGroups.map((tag, index) => {
           const bgColor = getGroupColor(tag);
           return (
             <Badge
-              key={`${tag}-${index}`}
+              key={tag}
               className="text-xs"
               style={{ backgroundColor: bgColor, color: getContrastTextColor(bgColor) }}
             >

256-265: 建议统一异步回调的 void 处理。

第 220 行使用 void handleCreateGroup(),但第 258 行直接传递 handleCreateGroup。虽然功能正常,但建议保持一致性。

建议修改
             <CommandItem
               value={`create-${searchValue.trim()}`}
-              onSelect={handleCreateGroup}
+              onSelect={() => void handleCreateGroup()}
               className="cursor-pointer"
               disabled={saving}
             >

269-274: i18n 字符串拼接可优化。

{t("save")}... 将翻译文本与 ... 拼接,建议使用单独的 i18n 键(如 t("saving"))以更好地支持不同语言的表达习惯。

建议修改
       {saving && (
         <div className="flex items-center justify-center py-2 text-xs text-muted-foreground">
           <Loader2 className="h-3 w-3 animate-spin mr-1" />
-          {t("save")}...
+          {t("saving")}
         </div>
       )}

需要在对应的 i18n 文件中添加 saving 键。

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

7 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

@NieiR NieiR force-pushed the feat/per-group-provider-priority branch from 0f67bd1 to 82b85ce Compare February 2, 2026 13:42
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
src/app/v1/_lib/proxy/provider-selector.ts (1)

1209-1244: ⚠️ Potential issue | 🟡 Minor

selectProviderByType 的决策上下文仍基于全局优先级
选择已经按分组优先级进行,但 priorityLevels/selectedPriority 仍用 provider.priority,会让 verbose 日志/返回上下文偏差。建议改为 resolveEffectivePriority 计算。

修改建议
     const candidates = topPriorityProviders.map((p) => ({
       id: p.id,
       name: p.name,
       weight: p.weight,
       costMultiplier: p.costMultiplier,
       probability: totalWeight > 0 ? p.weight / totalWeight : 1 / topPriorityProviders.length,
     }));
+
+    const priorities = [
+      ...new Set(
+        healthyProviders.map((p) =>
+          ProxyProviderResolver.resolveEffectivePriority(p, effectiveGroupPick ?? null)
+        )
+      ),
+    ].sort((a, b) => a - b);
+    const selectedPriority = Math.min(
+      ...healthyProviders.map((p) =>
+        ProxyProviderResolver.resolveEffectivePriority(p, effectiveGroupPick ?? null)
+      )
+    );
 
     return {
       provider: selected,
       context: {
         totalProviders: visibleProviders.length,
         enabledProviders: typeFiltered.length,
@@
-        priorityLevels: [...new Set(healthyProviders.map((p) => p.priority || 0))].sort(
-          (a, b) => a - b
-        ),
-        selectedPriority: selected.priority || 0,
+        priorityLevels: priorities,
+        selectedPriority,
         candidatesAtPriority: candidates,
       },
     };
🤖 Fix all issues with AI agents
In `@src/app/`[locale]/settings/providers/_components/group-edit-combobox.tsx:
- Around line 269-273: The displayed "..." after t("save") is hardcoded; create
a new i18n string (e.g., key "saving" whose value includes the localized
ellipsis like "Saving..." / "保存中…" etc.) and replace the current concatenation
so the component uses t("saving") (the JSX area where saving && ... and Loader2
are rendered in group-edit-combobox.tsx). Update all five locale files (zh-CN,
zh-TW, en, ja, ru) with the new key or adjust the existing "save" key to include
the ellipsis, then use that translated key in the render instead of appending
"..." in code.

In `@src/app/`[locale]/settings/providers/_components/priority-edit-popover.tsx:
- Around line 108-137: handleSave builds newGroupPriorities only from the
current groups and overwrites provider.groupPriorities, which drops any existing
entries not present in groups; instead merge the existing
provider.groupPriorities with the newly collected newGroupPriorities so orphaned
keys are preserved. Locate handleSave and, before calling
onSave(parsedGlobal,...), start from the existing provider.groupPriorities (or
an empty object) and copy its entries, then overwrite only the keys present in
newGroupPriorities (ensuring they are integers >= 0) so onSave receives the
merged map; keep the existing hasGroupOverrides logic and pass null only when
there are no overrides.

In `@src/app/v1/_lib/proxy/provider-selector.ts`:
- Around line 915-931: effectiveGroupPick may be a comma-separated multi-group
string but ProxyProviderResolver.resolveEffectivePriority is being called with
the entire string, so multi-group overrides are ignored; update calls around
selectTopPriority, the computation of priorities (context.priorityLevels) and
context.selectedPriority to split effectiveGroupPick by comma, for each provider
call resolveEffectivePriority for each individual group key and take the most
specific available override (e.g., the minimal numeric priority) before using
the result; ensure healthyProviders mapping uses this per-group-minimum priority
value and pass the original effectiveGroupPick (or a normalized single-key) into
selectTopPriority if it expects a single key, and apply the same split-and-min
logic in the other occurrence referenced in the review.

In `@src/lib/validation/schemas.ts`:
- Around line 28-56: The Zod schemas ANTHROPIC_MAX_TOKENS_PREFERENCE and
ANTHROPIC_THINKING_BUDGET_PREFERENCE currently embed English error strings;
replace those hardcoded messages (the regex and refine message strings) with
calls to the project's i18n API (e.g., use the shared translator function and
i18n message keys) so validation errors are localized across the five supported
languages, and update the same pattern in the other schema instances referenced
in the comment; ensure you pass the translated string into z.string().regex(...)
and the refine error message so all user-facing text comes from i18n keys rather
than hardcoded English.
🧹 Nitpick comments (1)
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx (1)

463-494: 可选优化:提取重复的分组徽章渲染逻辑

移动端(lines 473-487)和桌面端(lines 663-679)存在几乎相同的分组徽章渲染代码。可考虑提取为独立组件或渲染函数以减少重复。

♻️ 建议的重构方案
// 可以在组件内部定义一个渲染函数
const renderGroupBadges = () => {
  if (canEdit) {
    return (
      <GroupEditCombobox
        currentGroups={providerGroups}
        allGroups={allGroups}
        userGroups={userGroups}
        isAdmin={isAdmin}
        onSave={handleSaveGroups}
      />
    );
  }
  if (providerGroups.length > 0) {
    return providerGroups.map((tag, index) => {
      const bgColor = getGroupColor(tag);
      return (
        <Badge
          key={`${tag}-${index}`}
          className="text-xs"
          style={{ backgroundColor: bgColor, color: getContrastTextColor(bgColor) }}
        >
          {tag}
        </Badge>
      );
    });
  }
  return <Badge variant="outline">{PROVIDER_GROUP.DEFAULT}</Badge>;
};

Also applies to: 654-679

@NieiR NieiR force-pushed the feat/per-group-provider-priority branch from 82b85ce to 543c9bf Compare February 2, 2026 13:57
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

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

⚠️ Outside diff range comments (1)
src/repository/provider.ts (1)

25-35: ⚠️ Potential issue | 🟡 Minor

createProvider 返回值缺少 groupPriorities 字段

目前写入了 groupPriorities,但 insert returning 没有返回该列,可能导致新建结果里 groupPriorities 为 undefined,与 Provider 类型不一致。建议补充返回字段。

修改建议
   const [provider] = await db.insert(providers).values(dbData).returning({
     id: providers.id,
     name: providers.name,
     url: providers.url,
     key: providers.key,
     providerVendorId: providers.providerVendorId,
     isEnabled: providers.isEnabled,
     weight: providers.weight,
     priority: providers.priority,
+    groupPriorities: providers.groupPriorities,
     costMultiplier: providers.costMultiplier,

Also applies to: 81-92

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

6 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

@NieiR NieiR force-pushed the feat/per-group-provider-priority branch from ba5e4e6 to 3d36a43 Compare February 4, 2026 07:25
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link

greptile-apps bot commented Feb 4, 2026

Additional Comments (2)

src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
[P1] groupPriorities is normalized to {} even though the core Provider type is Record<string, number> | null.

createInitialState() sets groupPriorities: sourceProvider?.groupPriorities ?? {} which changes null semantics into an object. This makes the UI behave, but it diverges from the DB/type contract and can hide “unset vs empty” differences. Either keep it nullable throughout the form state, or normalize everywhere consistently (types + persistence) so null can’t leak back in unexpectedly.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
Line: 45:52

Comment:
[P1] `groupPriorities` is normalized to `{}` even though the core Provider type is `Record<string, number> | null`.

`createInitialState()` sets `groupPriorities: sourceProvider?.groupPriorities ?? {}` which changes null semantics into an object. This makes the UI behave, but it diverges from the DB/type contract and can hide “unset vs empty” differences. Either keep it nullable throughout the form state, or normalize everywhere consistently (types + persistence) so null can’t leak back in unexpectedly.

How can I resolve this? If you propose a fix, please make it concise.

src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts
[P1] Form state types make groupPriorities non-nullable but Provider.groupPriorities is nullable.

RoutingState.groupPriorities: Record<string, number> conflicts with src/types/provider.ts where groupPriorities is Record<string, number> | null. This mismatch is easy to reintroduce runtime errors (e.g., spreading/indexing when null) or causes confusing coercions in the context initializer.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts
Line: 36:45

Comment:
[P1] Form state types make `groupPriorities` non-nullable but `Provider.groupPriorities` is nullable.

`RoutingState.groupPriorities: Record<string, number>` conflicts with `src/types/provider.ts` where `groupPriorities` is `Record<string, number> | null`. This mismatch is easy to reintroduce runtime errors (e.g., spreading/indexing when null) or causes confusing coercions in the context initializer.

How can I resolve this? If you propose a fix, please make it concise.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
src/repository/provider.ts (1)

82-135: ⚠️ Potential issue | 🟡 Minor

创建后返回结果缺少 groupPriorities。

createProvider 写入了 groupPriorities,但 returning 中未选出该字段,导致创建后返回的 Provider 缺少该属性。

Proposed fix
     weight: providers.weight,
     priority: providers.priority,
+    groupPriorities: providers.groupPriorities,
     costMultiplier: providers.costMultiplier,
🤖 Fix all issues with AI agents
In `@drizzle/0063_harsh_dragon_lord.sql`:
- Line 1: The migration adds "group_priorities" with a jsonb DEFAULT
'null'::jsonb which creates a JSON null value (different from SQL NULL) and can
break IS NULL checks/indexing; change the column definition in this migration so
"group_priorities" uses DEFAULT NULL (or remove the DEFAULT entirely) instead of
'null'::jsonb so the column uses SQL NULL semantics; update the ALTER TABLE
statement that mentions "providers" and "group_priorities" accordingly.

In `@src/app/`[locale]/settings/providers/_components/provider-manager.tsx:
- Around line 359-386: The "default" label is currently hardcoded in the group
filter buttons and must be localized; update the displayed text for the
default-group Button(s) to use the i18n key via the existing translator
(tFilter) while keeping the internal group value ("default") unchanged for
filtering logic (look for usages around allGroups, groupFilter, setGroupFilter
and the Button where variant is computed as groupFilter.length === 0 ? "default"
: "outline"). Also apply the same change to the other hardcoded "default"
display occurrences noted around lines 485-513 so all user-facing "default"
labels use tFilter (or the established i18n function) and internal values remain
intact.

In `@src/app/`[locale]/settings/providers/_components/provider-rich-list-item.tsx:
- Around line 474-489: The default group badge is rendering a hardcoded string
(PROVIDER_GROUP.DEFAULT) in provider-rich-list-item.tsx instead of using i18n;
update the component (ProviderRichListItem rendering logic around
providerGroups.map fallback and the other occurrence at the block mentioned
~670-672) to use the app's translation hook/function (e.g., useTranslation()/t)
and replace the hardcoded display with t('provider.group.default') (or your
project's i18n key convention), then add the corresponding translation entries
for zh-CN, zh-TW, en, ja, and ru in the i18n resource files so the badge text is
localized.
🧹 Nitpick comments (1)
.gitignore (1)

95-95: LGTM!忽略 worktrees 目录的做法很合理。

添加 .worktrees/ 到 gitignore 是标准做法,适用于使用 git worktree 功能管理多个工作目录的场景。该条目正确放置在"local scratch"部分,与 tmp/.trae/ 等类似条目保持一致。

💅 可选的样式改进:在文件末尾添加换行符

按照 POSIX 标准,文本文件应以换行符结尾。虽然这是一个非常小的样式问题,但添加末尾换行符可以避免某些工具的警告:

 .worktrees/
+

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

6 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

46 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link

greptile-apps bot commented Feb 7, 2026

Additional Comments (1)

src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
Nullability mismatch for groupPriorities
createInitialState() initializes groupPriorities with {} via sourceProvider?.groupPriorities ?? {} even though the persisted Provider.groupPriorities type is Record<string, number> | null. This makes the form state semantics diverge from the backend (and from places that may still pass null). Either make the form state nullable to match, or normalize everywhere (including submit payload) and update types consistently so you don’t reintroduce the earlier null runtime issues when wiring up save/load paths.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx
Line: 44:50

Comment:
**Nullability mismatch for groupPriorities**
`createInitialState()` initializes `groupPriorities` with `{}` via `sourceProvider?.groupPriorities ?? {}` even though the persisted `Provider.groupPriorities` type is `Record<string, number> | null`. This makes the form state semantics diverge from the backend (and from places that may still pass `null`). Either make the form state nullable to match, or normalize everywhere (including submit payload) and update types consistently so you don’t reintroduce the earlier `null` runtime issues when wiring up save/load paths.

How can I resolve this? If you propose a fix, please make it concise.

@NieiR NieiR changed the title feat: Provider 分组级别优先级与内联编辑 feat: add per-group provider priority and inline editing Feb 7, 2026
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

6 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

NieiR added a commit to NieiR/claude-code-hub that referenced this pull request Feb 9, 2026
@coderabbitai coderabbitai bot requested a review from ding113 February 9, 2026 00:56
@NieiR NieiR force-pushed the feat/per-group-provider-priority branch from 036d10b to 579c82e Compare February 9, 2026 00:58
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@drizzle/0064_harsh_dragon_lord.sql`:
- Line 1: The migration sets the "group_priorities" column default to the JSON
literal 'null'::jsonb which mismatches the Drizzle schema field groupPriorities:
jsonb('group_priorities').$type<Record<string, number> | null>().default(null);
fix by ensuring the Drizzle schema is correct (groupPriorities with
default(null)) and then regenerate the migration so the ALTER TABLE uses DEFAULT
NULL (SQL NULL) instead of 'null'::jsonb; if you cannot regenerate, manually
edit the migration ALTER TABLE statement to use DEFAULT NULL for
group_priorities and re-run tests/DB migrations to confirm behavior.

In `@src/app/`[locale]/settings/providers/_components/provider-rich-list-item.tsx:
- Around line 598-626: The AlertDialog is nested inside DropdownMenuContent
which unmounts on menu close; move the AlertDialog (including
AlertDialogTrigger/AlertDialogContent/AlertDialogAction) out of
DropdownMenuContent so it isn’t destroyed when the menu closes, and convert it
to a controlled dialog using an open state (e.g., dialogOpen / setDialogOpen)
wired from the DropdownMenuItem click handler (replace implicit
AlertDialogTrigger behavior with onClick that sets dialogOpen = true); keep
existing handlers like handleDelete and props like disabled={deletePending} on
AlertDialogAction so deletion logic remains unchanged.
🧹 Nitpick comments (6)
drizzle/0063_slippery_sharon_carter.sql (1)

1-1: 缺少 IF NOT EXISTS 幂等性保护

迁移 0064 使用了 ADD COLUMN IF NOT EXISTS 来确保幂等性,但此迁移未使用。如果在某些部署场景下该迁移被重复执行(例如 Docker 容器重启期间的竞争条件),ADD COLUMN 会因列已存在而报错。

建议保持一致:

建议的修改
-ALTER TABLE "system_settings" ADD COLUMN "enable_claude_metadata_user_id_injection" boolean DEFAULT true NOT NULL;
+ALTER TABLE "system_settings" ADD COLUMN IF NOT EXISTS "enable_claude_metadata_user_id_injection" boolean DEFAULT true NOT NULL;
src/app/v1/_lib/proxy/forwarder.ts (3)

295-410: 跳过场景的日志级别可考虑降级。

applyClaudeMetadataUserIdInjectionWithAudit 中,already_existsmissing_key_idmissing_session_id 等非命中场景均使用 logger.info。在高流量 Claude 请求场景下(例如客户端已携带 metadata.user_id),这些日志可能产生大量输出。建议将跳过场景降级为 logger.debug,仅保留 injected 场景为 logger.info

建议:将 skip 场景日志降级为 debug
   if (existingMetadata?.user_id !== undefined && existingMetadata?.user_id !== null) {
-    logger.info("[ProxyForwarder] Claude metadata.user_id injection skipped", {
+    logger.debug("[ProxyForwarder] Claude metadata.user_id injection skipped", {
       enabled,
       hit: false,
       reason: "already_exists",

missing_key_idmissing_session_id 分支做同样调整。


1828-1851: 注入逻辑在 doForward 中执行,重试时会产生重复审计记录。

doForward 每次调用都会执行注入和 persistSpecialSettings。当请求被重试时(内层循环多次调用 doForward),session.addSpecialSetting(injection.audit) 会累加多条相同的注入审计记录。这不会影响功能,但会使审计数据冗余。

对比 thinking rectifier 的做法(通过 thinkingSignatureRectifierRetried 标志控制只执行一次),如果后续发现审计噪声过大,可考虑在 send 层级做一次性注入,或在 doForward 中添加去重标志。当前实现不阻塞合并。


214-243: persistSpecialSettings 封装了通用的持久化模式,建议后续复用。

该方法很好地抽象了 "session 特殊设置存储 + 消息请求详情更新" 的逻辑,目前只用于新增的 metadata 注入。文件中 thinking signature/budget rectifier 及 Gemini/Codex/Anthropic override 的持久化代码(如 Line 833-860, 965-992 等)使用了相同的模式但以内联方式实现。可在后续 PR 中统一替换以减少重复代码。

src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx (1)

839-851: disabled={!canEdit} 冗余。

Line 839 的 canEdit && 已保证按钮只在有编辑权限时渲染,Line 847 再加 disabled={!canEdit} 永远为 false,属于无效代码。同样的模式出现在 Line 860 和 Line 899。可以移除这些冗余的 disabled 属性。

示例修改
-            <Button
-              size="icon"
-              variant="ghost"
-              onClick={(e) => {
-                e.stopPropagation();
-                handleEdit();
-              }}
-              disabled={!canEdit}
-            >
+            <Button
+              size="icon"
+              variant="ghost"
+              onClick={(e) => {
+                e.stopPropagation();
+                handleEdit();
+              }}
+            >
src/lib/validation/schemas.ts (1)

412-416: group_priorities 的 record key 缺少长度约束,与 group_tag 的 50 字符限制不一致。

group_tag(Line 418)限制了 50 个字符(整个逗号分隔字符串),但 group_priorities 的 key 使用裸 z.string() 没有长度限制。恶意或误操作的请求可以插入超长的 key。建议对 key 也加上合理的长度约束:

建议修改
     group_priorities: z
-      .record(z.string(), z.number().int().min(0).max(2147483647))
+      .record(z.string().max(50), z.number().int().min(0).max(2147483647))
       .nullable()
       .optional()
       .default(null),

同样适用于 UpdateProviderSchema(Line 618-621)。

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

9 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

9 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 70 to +77
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/api/actions/health || exit 1"]
test:
[
"CMD",
"node",
"-e",
"fetch('http://' + (process.env.HOSTNAME || '127.0.0.1') + ':3000/api/actions/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
]
Copy link

Choose a reason for hiding this comment

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

Healthcheck uses missing node

The app service healthcheck runs node -e fetch(...), but the container image is ghcr.io/ding113/claude-code-hub:latest (and in this repo’s Dockerfile the runtime command is node server.js only if Node exists in the image). If the published image is bun-based or otherwise lacks the node binary, the healthcheck will fail continuously and the service will be marked unhealthy. Consider switching back to a healthcheck that relies on tools guaranteed to exist in the image (or ensure node is present in the runtime image).

Prompt To Fix With AI
This is a comment left during a code review.
Path: docker-compose.yaml
Line: 70:77

Comment:
**Healthcheck uses missing `node`**

The `app` service healthcheck runs `node -e fetch(...)`, but the container image is `ghcr.io/ding113/claude-code-hub:latest` (and in this repo’s Dockerfile the runtime command is `node server.js` only if Node exists in the image). If the published image is bun-based or otherwise lacks the `node` binary, the healthcheck will fail continuously and the service will be marked unhealthy. Consider switching back to a healthcheck that relies on tools guaranteed to exist in the image (or ensure `node` is present in the runtime image).

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 12 to +19
ENV CI=true
RUN bun run build
RUN --mount=type=cache,target=/app/.next/cache bun run build

FROM node:20-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=8080
EXPOSE 8080
ENV PORT=3000
EXPOSE 3000
Copy link

Choose a reason for hiding this comment

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

Runtime port mismatch

This Dockerfile changes ENV PORT/EXPOSE from 8080 to 3000, but the final command is node server.js from Next standalone output. If the generated server.js is configured to bind process.env.PORT (common) this is fine, but if any deployment still expects 8080 (e.g., existing infra / docs / helm values), this change will break connectivity. At minimum ensure the app actually listens on 3000 in standalone mode and update any references that still assume 8080.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Dockerfile
Line: 12:19

Comment:
**Runtime port mismatch**

This Dockerfile changes `ENV PORT`/`EXPOSE` from `8080` to `3000`, but the final command is `node server.js` from Next standalone output. If the generated `server.js` is configured to bind `process.env.PORT` (common) this is fine, but if any deployment still expects `8080` (e.g., existing infra / docs / helm values), this change will break connectivity. At minimum ensure the app actually listens on 3000 in standalone mode and update any references that still assume 8080.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 21 to +23
# 关键:确保复制了所有必要的文件,特别是 drizzle 文件夹
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/drizzle ./drizzle
COPY --from=builder /app/.next/standalone ./
Copy link

Choose a reason for hiding this comment

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

Possible missing server.js

The runner stage copies /.next/standalone into ./ and then runs CMD ["node","server.js"]. This only works if server.js is actually present at the container workdir root after the copy. If Next’s standalone output layout changes or output: 'standalone' isn’t enabled in some build contexts, this will crash at runtime with Cannot find module '/app/server.js'. It would be safer to ensure the build always produces standalone output (and/or validate the file exists in CI) before switching the runtime command.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Dockerfile
Line: 21:23

Comment:
**Possible missing `server.js`**

The runner stage copies `/.next/standalone` into `./` and then runs `CMD ["node","server.js"]`. This only works if `server.js` is actually present at the container workdir root after the copy. If Next’s standalone output layout changes or `output: 'standalone'` isn’t enabled in some build contexts, this will crash at runtime with `Cannot find module '/app/server.js'`. It would be safer to ensure the build always produces standalone output (and/or validate the file exists in CI) before switching the runtime command.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +1039 to +1047
* 支持逗号分隔的多分组(如 "cli,admin"),取匹配到的最小优先级
*/
static resolveEffectivePriority(provider: Provider, userGroup: string | null): number {
if (userGroup && provider.groupPriorities) {
const groups = parseGroupString(userGroup);
const overrides = groups
.map((g) => provider.groupPriorities?.[g])
.filter((v): v is number => v !== undefined);
if (overrides.length > 0) {
Copy link

Choose a reason for hiding this comment

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

Priority override semantics change

resolveEffectivePriority() now parses comma-separated userGroup and returns the minimum matched override (Math.min(...overrides)). This is a behavior change vs the previously-discussed “first matching group wins” semantics from earlier threads. If userGroup can contain multiple groups (e.g. "cli,admin"), this will prefer the numerically smallest override across all matches, not the first match. Please confirm which behavior is intended and align implementation + docs/tests accordingly.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/provider-selector.ts
Line: 1039:1047

Comment:
**Priority override semantics change**

`resolveEffectivePriority()` now parses comma-separated `userGroup` and returns the *minimum* matched override (`Math.min(...overrides)`). This is a behavior change vs the previously-discussed “first matching group wins” semantics from earlier threads. If `userGroup` can contain multiple groups (e.g. `"cli,admin"`), this will prefer the numerically smallest override across all matches, not the first match. Please confirm which behavior is intended and align implementation + docs/tests accordingly.

How can I resolve this? If you propose a fix, please make it concise.

@ding113 ding113 merged commit a7bbef3 into ding113:dev Feb 9, 2026
9 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Feb 9, 2026
@NieiR NieiR deleted the feat/per-group-provider-priority branch February 9, 2026 06:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:i18n area:provider area:UI enhancement New feature or request size/XL Extra Large PR (> 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants