Skip to content

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Jan 13, 2026

Summary

Add vendor-endpoint architecture enabling providers to have multiple endpoints with intelligent routing, health probing, and automatic failover via circuit breakers.

Problem

Currently, providers are limited to a single endpoint configuration. This creates limitations for:

  • High availability scenarios requiring multiple endpoints per provider
  • Load balancing across different API endpoints
  • Automatic failover when endpoints become unhealthy

Related:

Solution

Implement a vendor-endpoint architecture that allows:

  • Multiple endpoints per provider type (e.g., multiple Claude API endpoints)
  • Intelligent endpoint selection based on health status and priority
  • Circuit breaker pattern at both endpoint and vendor-type levels
  • Real-time health monitoring with probe scheduling and history

Changes

Database Schema (Migration 0055)

  • Added provider_vendors table for vendor management (grouped by domain)
  • Added provider_endpoints table with probe state tracking
  • Added provider_endpoint_probe_logs table for health monitoring history
  • Added provider_vendor_id foreign key to providers table (auto-migrated from existing URL domains)

Core Services

  • Endpoint Circuit Breaker (src/lib/endpoint-circuit-breaker.ts): Per-endpoint failure tracking with Redis state
  • Vendor-Type Circuit Breaker (src/lib/vendor-type-circuit-breaker.ts): Aggregate circuit breaker for vendor+type combinations
  • Endpoint Selector (src/lib/provider-endpoints/endpoint-selector.ts): Smart endpoint selection based on availability
  • Endpoint Probe (src/lib/provider-endpoints/probe.ts): Health check implementation with logging

Proxy Pipeline Integration

  • Updated forwarder.ts to support endpoint-level routing with failover
  • Updated provider-selector.ts with vendor-type circuit integration

Management API

  • New server actions in src/actions/provider-endpoints.ts for endpoint CRUD, probing, and circuit management
  • New API routes for endpoint availability and probe logs

Dashboard UI

  • New endpoint probe history view in availability dashboard
  • New provider vendor view mode in settings with endpoint management
  • Full i18n support (5 languages)

Breaking Changes

Change Impact Migration
New provider_vendor_id column on providers table Existing providers auto-migrated based on URL domain Run bun run db:migrate or enable AUTO_MIGRATE=true
New database tables Requires migration Automatic with migration

Testing

Automated Tests

  • Unit tests for endpoint-circuit-breaker
  • Unit tests for vendor-type-circuit-breaker
  • Unit tests for endpoint-selector
  • Unit tests for probe functionality

Manual Testing

  1. Navigate to Settings > Providers and switch to Vendor view mode
  2. Expand a vendor to see/manage endpoints
  3. Add multiple endpoints for a vendor
  4. Trigger manual probe and verify status updates
  5. Check Dashboard > Availability for endpoint probe history

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally
  • i18n strings added for all 5 languages
  • Database migration generated via bun run db:generate

Description enhanced by Claude AI

@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

新增完整的提供商端点子系统:数据库迁移与快照、端点探测与记录、端点/供应商类型电路断路器、端点选择与代理重试集成、后端 actions/API、管理端 UI 多语言扩展及大量测试与调度/清理工具(新增表、仓库、调度器、前后端组件与路由)。

Changes

Cohort / File(s) 变化摘要
数据库迁移与元数据
drizzle/0054_mixed_eternity.sql, drizzle/0054_tidy_winter_soldier.sql, drizzle/0056_colorful_nightshade.sql, drizzle/meta/0054_snapshot.json, drizzle/meta/0055_snapshot.json, drizzle/meta/0056_snapshot.json, drizzle/meta/_journal.json
新增/调整 migration 与 snapshots:引入/回退 provider_vendorsprovider_endpointsprovider_endpoint_probe_logs 表及索引、FK,并更新 journal 顺序与快照。
Drizzle schema 与类型
src/drizzle/schema.ts, src/types/provider.ts, src/types/message.ts
在 schema 中声明 provider_* 表与 relations;为 providers 增加 providerVendorId、priority、costMultiplier、groupTag;新增 ProviderVendor/ProviderEndpoint/ProbeLog 类型并扩展 ProviderChainItem。
仓库层(provider endpoints)
src/repository/provider-endpoints.ts, src/repository/provider.ts, src/repository/index.ts, src/repository/_shared/transformers.ts
新增完整仓库实现(vendor/endpoint CRUD、软删、回填、probe 记录、事务/冲突处理);provider 仓库集成 providerVendorId 并在 create/update 时尝试回填端点;transformer 返回包含 providerVendorId;对外导出新接口。
后端 Actions 与 API 路由
src/actions/provider-endpoints.ts, src/actions/providers.ts, src/app/api/actions/[...route]/route.ts, src/app/api/availability/endpoints/route.ts, src/app/api/availability/endpoints/probe-logs/route.ts, src/app/api/availability/endpoints/probe-logs/batch/route.ts
新增 admin 受限 actions 与 API:供应商/端点 CRUD、探测触发与日志读取、电路信息查询/控制;getProviders 暴露 providerVendorId;删除 provider 后尝试清理空 vendor。
端点探测、选择与电路断路器
src/lib/provider-endpoints/probe.ts, src/lib/provider-endpoints/endpoint-selector.ts, src/lib/endpoint-circuit-breaker.ts, src/lib/vendor-type-circuit-breaker.ts, src/lib/redis/*.ts
实现 HTTP 探测(HEAD→GET 回退)、错误归一化、探测结果持久化;端点排序/选择逻辑;新增端点级与 vendor-type 级电路断路器并提供 Redis 持久化接口与相关状态模块。
Leader lock / 调度 / 清理
src/lib/provider-endpoints/leader-lock.ts, src/lib/provider-endpoints/probe-scheduler.ts, src/lib/provider-endpoints/probe-log-cleanup.ts
新增分布式 leader-lock(Redis 优先、内存回退)、端点探测调度器(leader 驱动、并发/抖动/超时配置)和探测日志定期清理(start/stop API)。
代理转发与提供者选择集成
src/app/v1/_lib/proxy/forwarder.ts, src/app/v1/_lib/proxy/provider-selector.ts, src/app/v1/_lib/proxy/session.ts, src/app/v1/_lib/url.ts
将端点候选纳入转发/重试策略,轮换重试并记录每次 endpoint 审计信息;扩展 doForward 签名以携带 endpoint 上下文;改进 buildProxyUrl 行为与预览键映射。
前端 - 仪表板与设置 UI
src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx, .../availability-view.tsx, src/app/[locale]/settings/providers/_components/provider-manager.tsx, provider-vendor-view.tsx, provider-manager-loader.tsx, forms/url-preview.tsx, provider-rich-list-item.tsx, endpoint-latency-sparkline.tsx, vendor-keys-compact-list.tsx, endpoint-probe-row.tsx, endpoint-url-display.tsx, probe-cell-tooltip.tsx
新增 EndpointProbeHistory 及行/显示/tooltip 组件并集成到可用性仪表板;新增 ProviderVendorView 与视图切换;表单/URL 预览、延迟 sparkline、vendor keys 列表等 UI 组件。
本地化 / 文案扩展
messages/*/dashboard.json, messages/*/settings/providers/*, messages/*/settings/index.ts, messages/*/settings/providers/form/tabs.json, messages/*/settings/providers/form/urlPreview.json
为 probeHistory 与 providers 设置添加/扩展多语言翻译(en/ja/ru/zh-CN/zh-TW 等),新增大量 providers 相关翻译键与 tabs、url preview endpoints 映射。
启动、回填与配置
src/instrumentation.ts, .env.example, src/lib/config/env.schema.ts
启动时尝试幂等回填 provider_endpoints、启动探测调度与日志清理(动态导入、降级容错);删除/可选化 ENABLE_MULTI_PROVIDER_TYPES 配置并更新示例。
测试套件
tests/unit/*, tests/unit/repository/provider-endpoints.test.ts, tests/unit/lib/*, tests/unit/proxy/*, tests/unit/actions/provider-endpoints.test.ts, tests/unit/app/v1/url.test.ts, tests/api/api-openapi-spec.test.ts
大量新增/扩展单元测试:端点探测、selector、电路断路器、forwarder endpoint 审计、仓库回填、actions、buildProxyUrl、probe-scheduler、probe-cleanup 等,并调整 OpenAPI paths 上界。
轻微改动与补充
src/repository/_shared/transformers.ts, src/actions/providers.ts, src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx, 若干小文件
transformer 返回包含 providerVendorId;removeProvider 调用 vendor 清理并失效 provider-vendors 缓存;切换 ProviderForm → ProviderFormV2 及相关 UI 布局调整。

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.98% 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 标题准确简洁地总结了主要变更:添加供应商端点架构以支持多端点、健康探测和故障转移。
Description check ✅ Passed 描述与变更集相关,详细解释了问题、解决方案、数据库架构、核心服务、代理集成、管理API、UI和测试等方面的改动。

✏️ 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
  • Commit unit tests in branch feat/provider-multi-endpoint-multi-key-multi-type

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

Summary of Changes

Hello @ding113, 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!

This pull request significantly enhances the reliability and management of AI model providers by introducing a robust system for endpoint health monitoring and dynamic routing. It establishes a new data architecture to group providers by vendors, implements automated probing to assess endpoint availability and latency, and integrates sophisticated circuit breaker patterns to prevent cascading failures. The changes provide administrators with granular control and visibility over individual endpoints and entire vendor services, ensuring a more resilient and performant proxy layer.

Highlights

  • New Data Model: Introduced a new Drizzle migration (0055) to establish data models for provider_vendors, provider_endpoints, and provider_endpoint_probe_logs. The providers table now includes a provider_vendor_id for vendor association.
  • Endpoint Probing and Circuit Breakers: Implemented functionality for endpoint probing, including storing probe logs and introducing both endpoint-level and vendor-type circuit breakers to manage provider health and failover.
  • Enhanced Proxy Routing: Updated the proxy routing logic to intelligently select preferred endpoints, handle failover in case of errors or timeouts, and avoid routing requests to vendors with open circuit breakers.
  • Admin Actions and UI: Exposed new admin actions and routes for comprehensive management of endpoints, including CRUD operations, manual probing, monitoring circuit status, and viewing probe history. A new 'Vendor' view mode has been added to the provider settings dashboard.
  • Localization Updates: Added new localization strings across English, Japanese, Russian, Simplified Chinese, and Traditional Chinese for the new dashboard and provider settings UI elements related to endpoint probing and vendor management.
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:provider area:UI area:i18n size/XL Extra Large PR (> 1000 lines) labels Jan 13, 2026
Copy link

@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 is a substantial and well-executed pull request that introduces a robust provider endpoint management system, complete with failover logic and multi-level circuit breaking. The database schema changes are well-designed, the new backend logic is solid, and the UI additions follow modern React patterns. The inclusion of comprehensive unit tests for the new critical logic is particularly commendable. I have a couple of suggestions to enhance UI consistency and improve maintainability, but overall, this is an excellent contribution to the project.

Comment on lines +180 to +184
return {
ok: false,
error: "无权限执行此操作",
errorCode: ERROR_CODES.PERMISSION_DENIED,
};

Choose a reason for hiding this comment

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

medium

Throughout this file, error messages returned in the ActionResult are hardcoded in Chinese (e.g., "无权限执行此操作"). While the frontend seems to use errorCode for translation, this approach has some drawbacks:

  1. Inconsistency: It's inconsistent with the i18n pattern used elsewhere in the application.
  2. Maintainability: If a new error is added, developers must remember to add a corresponding translation case in the frontend.
  3. Fallback Issue: If an errorCode is not handled by the frontend, the user will see a hardcoded Chinese message, which is not ideal for non-Chinese users.

A better practice would be for server actions to return only errorCodes and let the client be fully responsible for localization. The hardcoded strings should be removed, or replaced with English as a developer-facing fallback.

Suggested change
return {
ok: false,
error: "无权限执行此操作",
errorCode: ERROR_CODES.PERMISSION_DENIED,
};
return {
ok: false,
error: "Permission denied",
errorCode: ERROR_CODES.PERMISSION_DENIED,
};

Comment on lines +505 to +509
onClick={() => {
if (confirm(t("confirmDeleteEndpoint"))) {
deleteMutation.mutate();
}
}}

Choose a reason for hiding this comment

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

medium

The delete confirmation uses the native window.confirm(). For a better user experience and UI consistency with the rest of the application, it's recommended to use a custom dialog component, such as Shadcn's AlertDialog. This provides a non-blocking and stylistically consistent confirmation prompt.

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

🤖 Fix all issues with AI agents
In @drizzle/0055_sour_wallow.sql:
- Around line 1-11: The created_at column in provider_endpoint_probe_logs has
DEFAULT now() but lacks NOT NULL; update the table definition (or migration) to
add NOT NULL on the "created_at" column while preserving DEFAULT now() (i.e.,
change the column definition to "created_at timestamp with time zone DEFAULT
now() NOT NULL"); if applying to an existing DB, include a pre-migration step to
backfill NULL created_at rows (UPDATE provider_endpoint_probe_logs SET
created_at = now() WHERE created_at IS NULL) before altering the column to set
NOT NULL.

In @messages/ru/dashboard.json:
- Around line 1001-1022: Remove the unused localization key columns.method under
probeHistory (i.e., "probeHistory.columns.method") since the UI only renders
time, status, latency and error; update the Russian file by deleting that
"method" entry and mirror the same removal across other locale files so all
language bundles stay consistent.

In @src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx:
- Line 240: The providerTypes array in provider-vendor-view.tsx is missing
"claude-auth" and "gemini-cli" and the button labels are rendered raw instead of
via i18n; update the providerTypes constant to include "claude-auth" and
"gemini-cli" and change the button rendering to call the translation function
(t) with the camel-cased key (use the same getCamelCase helper used in
provider-form.tsx) for each type, e.g.,
t(`providerTypes.${getCamelCase(type)}`), while keeping the Button props (key,
variant, size, onClick, className) the same.

In @src/app/v1/_lib/proxy/provider-selector.ts:
- Around line 879-885: The hard-coded Chinese string assigned to details in the
context.filteredProviders push (details: "供应商类型临时熔断") should be replaced with an
i18n lookup so user-visible messages honor localization and are safe when
verboseProviderError exposes them; update the push call in provider-selector
(where context.filteredProviders?.push({ id: p.id, name: p.name, reason:
"circuit_open", details: ... })) to call the i18n translation function or a
resource key (e.g., t('provider.circuit_open')) and ensure the same translation
is applied before sending errors to users (see verboseProviderError handling
around verboseProviderError / lines ~485-488) so no raw Chinese strings are
returned.
🧹 Nitpick comments (27)
src/lib/provider-endpoints/endpoint-selector.ts (1)

30-54: 实现正确,存在轻微冗余。

整体逻辑合理:排除指定端点 → 过滤启用状态 → 并行检查熔断状态 → 排序返回。

Line 38 的 !e.deletedAt 检查与 findProviderEndpointsByVendorAndType 仓库函数中的 isNull(providerEndpoints.deletedAt) 条件重复。这是防御性编码,不影响正确性,但如果确认仓库层保证不返回已删除记录,可以移除此检查以简化代码。

可选:移除冗余的 deletedAt 检查
- const filtered = endpoints.filter((e) => e.isEnabled && !e.deletedAt && !excludeSet.has(e.id));
+ const filtered = endpoints.filter((e) => e.isEnabled && !excludeSet.has(e.id));
src/lib/redis/endpoint-circuit-breaker-state.ts (2)

30-48: 反序列化时缺少 circuitState 值的验证。

deserializeState 函数直接将 data.circuitState 强制转换为 EndpointCircuitState,但未验证该值是否为有效的枚举值("closed" | "open" | "half-open")。如果 Redis 中存储了无效值,可能导致意外行为。

建议添加验证逻辑
+const VALID_CIRCUIT_STATES: EndpointCircuitState[] = ["closed", "open", "half-open"];
+
+function isValidCircuitState(value: string): value is EndpointCircuitState {
+  return VALID_CIRCUIT_STATES.includes(value as EndpointCircuitState);
+}
+
 function deserializeState(data: Record<string, string>): EndpointCircuitBreakerState {
+  const circuitState = isValidCircuitState(data.circuitState) ? data.circuitState : "closed";
   return {
     failureCount: Number.parseInt(data.failureCount || "0", 10),
     lastFailureTime: data.lastFailureTime ? Number.parseInt(data.lastFailureTime, 10) : null,
-    circuitState: (data.circuitState as EndpointCircuitState) || "closed",
+    circuitState,
     circuitOpenUntil: data.circuitOpenUntil ? Number.parseInt(data.circuitOpenUntil, 10) : null,
     halfOpenSuccessCount: Number.parseInt(data.halfOpenSuccessCount || "0", 10),
   };
 }

89-93: hsetexpire 非原子操作,存在潜在的状态不一致风险。

当前实现先调用 hset 再调用 expire,如果在两次调用之间发生错误或进程崩溃,可能导致 key 没有设置 TTL 而永不过期。

可考虑使用 pipeline 或 Lua 脚本保证原子性
   try {
     const key = getStateKey(endpointId);
     const data = serializeState(state);
-    await redis.hset(key, data);
-    await redis.expire(key, STATE_TTL_SECONDS);
+    const pipeline = redis.pipeline();
+    pipeline.hset(key, data);
+    pipeline.expire(key, STATE_TTL_SECONDS);
+    await pipeline.exec();
   } catch (error) {
src/lib/endpoint-circuit-breaker.ts (2)

32-33: 内存缓存无淘汰机制,可能导致内存增长。

healthMaploadedFromRedis 会随着端点数量增加而无限增长。在长时间运行的进程中,如果端点被删除或不再使用,相关条目不会被清理。

可以考虑:

  1. resetEndpointCircuit 中从 healthMap 删除条目
  2. 或使用 LRU 缓存替代 Map

187-196: resetEndpointCircuit 未清理内存缓存条目。

虽然函数重置了健康状态的所有字段并删除了 Redis 条目,但 healthMap 中的条目仍然保留。此外,loadedFromRedis 中的条目也未清除,这意味着后续调用不会再从 Redis 加载(尽管对于重置场景这可能是期望的行为)。

考虑清理缓存条目
 export async function resetEndpointCircuit(endpointId: number): Promise<void> {
-  const health = getOrCreateHealthSync(endpointId);
-  health.circuitState = "closed";
-  health.failureCount = 0;
-  health.lastFailureTime = null;
-  health.circuitOpenUntil = null;
-  health.halfOpenSuccessCount = 0;
-
+  healthMap.delete(endpointId);
+  loadedFromRedis.delete(endpointId);
   await deleteEndpointCircuitState(endpointId);
 }
tests/unit/lib/endpoint-circuit-breaker.test.ts (2)

3-9: 类型定义与主模块重复。

SavedEndpointCircuitState 类型与 src/lib/redis/endpoint-circuit-breaker-state.ts 中的 EndpointCircuitBreakerState 相同。建议从主模块导入类型以避免不一致。

从主模块导入类型
 import { afterEach, describe, expect, test, vi } from "vitest";
+import type { EndpointCircuitBreakerState } from "@/lib/redis/endpoint-circuit-breaker-state";

-type SavedEndpointCircuitState = {
-  failureCount: number;
-  lastFailureTime: number | null;
-  circuitState: "closed" | "open" | "half-open";
-  circuitOpenUntil: number | null;
-  halfOpenSuccessCount: number;
-};
+type SavedEndpointCircuitState = EndpointCircuitBreakerState;

26-123: 建议增加边界情况测试。

当前测试覆盖了主要流程,但缺少以下场景的测试:

  • Redis 不可用时的降级行为
  • half-open 状态下失败时重新打开断路器
  • 阈值为 0 时禁用断路器

建议后续补充这些边界情况的测试以提高覆盖率。

src/lib/redis/vendor-type-circuit-breaker-state.ts (2)

38-45: 与端点模块相同:反序列化缺少 circuitState 验证。

建议添加对 circuitState 值的验证,确保只接受 "closed" 或 "open"。

建议添加验证逻辑
+const VALID_VENDOR_CIRCUIT_STATES: VendorTypeCircuitStateValue[] = ["closed", "open"];
+
+function isValidVendorCircuitState(value: string): value is VendorTypeCircuitStateValue {
+  return VALID_VENDOR_CIRCUIT_STATES.includes(value as VendorTypeCircuitStateValue);
+}
+
 function deserializeState(data: Record<string, string>): VendorTypeCircuitBreakerState {
+  const circuitState = isValidVendorCircuitState(data.circuitState) ? data.circuitState : "closed";
   return {
-    circuitState: (data.circuitState as VendorTypeCircuitStateValue) || "closed",
+    circuitState,
     circuitOpenUntil: data.circuitOpenUntil ? Number.parseInt(data.circuitOpenUntil, 10) : null,
     lastFailureTime: data.lastFailureTime ? Number.parseInt(data.lastFailureTime, 10) : null,
     manualOpen: data.manualOpen === "1",
   };
 }

87-91: 与端点模块相同:hsetexpire 非原子操作。

建议使用 pipeline 保证原子性,与端点模块保持一致的改进方案。

src/app/api/availability/endpoints/route.ts (2)

6-13: PROVIDER_TYPES 常量在多处重复定义

该常量在以下位置重复定义:

  • src/app/api/availability/endpoints/route.ts(此处)
  • src/app/api/actions/[...route]/route.tsProviderTypeSchema
  • src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx

建议将其提取到 src/types/provider.ts 或创建一个共享常量文件,以保持单一数据源。

建议:在 types/provider.ts 中导出常量
// src/types/provider.ts
export const PROVIDER_TYPES = [
  "claude",
  "claude-auth",
  "codex",
  "gemini",
  "gemini-cli",
  "openai-compatible",
] as const;

export type ProviderType = (typeof PROVIDER_TYPES)[number];

然后在其他文件中直接导入使用。


22-45: API 路由未使用 Hono 框架

根据编码规范,src/app/api/**/*.{ts,tsx} 路径下的 API 路由应使用 Hono 框架。当前实现直接使用 NextResponse

如果这是有意为之的简单路由设计,可以忽略此建议。否则建议考虑与项目中其他 API 路由保持一致的风格。

src/app/api/actions/[...route]/route.ts (1)

344-351: ProviderTypeSchema 与其他位置的定义重复

此处的 ProviderTypeSchemasrc/app/api/availability/endpoints/route.ts 中的 PROVIDER_TYPESsrc/types/provider.ts 中的 ProviderType 类型定义重复。

建议在类型定义文件中导出一个共享的 Zod schema 或常量数组,然后在此处引用。

建议:创建共享的 Zod schema
// src/types/provider.ts 或 src/lib/validation/schemas.ts
import { z } from "zod";

export const PROVIDER_TYPES = [
  "claude",
  "claude-auth",
  "codex",
  "gemini",
  "gemini-cli",
  "openai-compatible",
] as const;

export const ProviderTypeSchema = z.enum(PROVIDER_TYPES);
export type ProviderType = z.infer<typeof ProviderTypeSchema>;
src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx (3)

35-42: PROVIDER_TYPES 第三处重复定义

此常量在项目中已有多处定义,建议从共享位置导入以保持一致性。


61-63: 加载供应商失败时用户无反馈

getProviderVendors() 失败时,错误仅记录到控制台,用户界面无任何提示。建议添加 toast 通知以改善用户体验。

建议:添加错误提示
 useEffect(() => {
-  getProviderVendors().then(setVendors).catch(console.error);
+  getProviderVendors()
+    .then(setVendors)
+    .catch((error) => {
+      console.error(error);
+      toast.error(tErrors("fetchFailed"));
+    });
 }, []);

78-91: 加载端点失败时缺少用户反馈

与上述问题类似,fetch 端点失败时仅 console.error,建议添加 toast 通知。

建议:添加错误处理
     fetch(`/api/availability/endpoints?${params.toString()}`)
       .then((res) => res.json())
       .then((data) => {
         if (data.endpoints) {
           setEndpoints(data.endpoints);
           setSelectedEndpointId((prev) =>
             prev && !data.endpoints.some((e: ProviderEndpoint) => e.id.toString() === prev)
               ? ""
               : prev
           );
         }
       })
-      .catch(console.error)
+      .catch((error) => {
+        console.error(error);
+        toast.error(tErrors("fetchFailed"));
+      })
       .finally(() => setLoadingEndpoints(false));
tests/unit/lib/provider-endpoints/probe.test.ts (1)

35-42: 考虑提取重复的 mock 对象。

logger mock 对象在每个测试中重复定义。可以考虑在 describe 块顶部提取为共享 helper 函数,减少重复代码。

♻️ 建议的重构
function createLoggerMock() {
  return {
    debug: vi.fn(),
    info: vi.fn(),
    warn: vi.fn(),
    trace: vi.fn(),
    error: vi.fn(),
    fatal: vi.fn(),
  };
}

然后在每个测试中使用 const logger = createLoggerMock();

src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (2)

73-77: 建议为 healthStatusstatistics 使用更具体的类型。

当前 Record<number, any> 类型过于宽泛,失去了类型安全的优势。建议定义具体的接口类型。

♻️ 建议的类型定义
interface ProviderHealthStatus {
  isHealthy: boolean;
  lastCheckedAt?: Date;
  circuitState?: "open" | "closed" | "half-open";
}

interface ProviderStatistics {
  todayCostUsd?: string;
  todayCallCount?: number;
  lastCallTime?: Date | null;
}

interface ProviderVendorViewProps {
  // ...
  healthStatus: Record<number, ProviderHealthStatus>;
  statistics: Record<number, ProviderStatistics>;
  // ...
}

503-509: 考虑使用自定义 Dialog 替代 confirm()

使用原生 confirm() 对话框与应用其他部分的 UI 风格不一致。建议使用已导入的 Dialog 组件创建确认对话框,以保持一致的用户体验。

src/app/v1/_lib/proxy/forwarder.ts (2)

220-257: 端点候选列表构建逻辑清晰,但建议处理 getPreferredProviderEndpoints 返回空数组的场景

当前实现逻辑合理:

  • MCP 请求直接使用 provider URL(不经过端点池)
  • 非 MCP 请求且有 vendorId 时从端点池获取候选
  • 异常时回退到 provider URL

getPreferredProviderEndpoints 返回空数组(所有端点都被熔断)与抛出异常是不同的场景。当前代码在空数组时会在 Line 250-252 补充 fallback,这是正确的处理。

建议增加日志区分这两种情况,便于运维排查:

建议添加空数组场景的日志
         try {
           const preferred = await getPreferredProviderEndpoints({
             vendorId: currentProvider.providerVendorId,
             providerType: currentProvider.providerType,
           });
+          if (preferred.length === 0) {
+            logger.debug(
+              "[ProxyForwarder] No available endpoints from pool, fallback to provider.url",
+              {
+                providerId: currentProvider.id,
+                vendorId: currentProvider.providerVendorId,
+                providerType: currentProvider.providerType,
+              }
+            );
+          }
           endpointCandidates.push(...preferred.map((e) => ({ endpointId: e.id, baseUrl: e.url })));
         } catch (error) {

2201-2217: toUndiciBody 类型转换辅助函数可能丢失不支持的 body 类型

当前实现对不支持的类型返回 undefined,这可能导致请求体静默丢失。虽然在当前上下文中 body 通常是 JSON 字符串,但建议添加日志或抛出错误以便调试:

建议添加未知类型的警告日志
     if (ArrayBuffer.isView(body)) {
       return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
     }
+    logger.warn("ProxyForwarder: Unsupported body type for undici request", {
+      bodyType: body?.constructor?.name ?? typeof body,
+    });
     return undefined;
src/lib/vendor-type-circuit-breaker.ts (1)

45-91: getOrCreateState Redis 同步逻辑存在潜在的状态不一致风险

当前逻辑:

  • Line 51-53: needsRedisCheck 在首次访问或状态非 closed/manualOpen 时触发 Redis 同步
  • Line 65-70: 如果 Redis 无数据但本地有非 closed 状态,则重置本地状态

潜在问题:

  1. 竞态条件:多个并发请求可能同时触发 Redis 加载,导致状态覆盖
  2. Line 65-70 逻辑:当 Redis 无数据但本地有状态时,强制重置为 closed 可能丢失有效的熔断状态(例如 Redis 数据过期但熔断仍应生效)

建议考虑添加互斥锁或使用 loadedFromRedis 更严格地控制同步:

建议增加状态同步的原子性保护
+const syncLocks = new Map<string, Promise<VendorTypeCircuitBreakerState>>();

 async function getOrCreateState(
   vendorId: number,
   providerType: ProviderType
 ): Promise<VendorTypeCircuitBreakerState> {
   const key = getKey(vendorId, providerType);
+
+  // 如果已有同步进行中,等待其完成
+  const existingSync = syncLocks.get(key);
+  if (existingSync) {
+    return existingSync;
+  }
+
   let state = stateMap.get(key);
   // ... rest of the logic
src/actions/provider-endpoints.ts (3)

36-43: ProviderTypeSchema 顺序与源类型定义不一致

当前 schema 中 "gemini-cli""gemini" 之前,而 src/types/provider.ts 中的 ProviderType 顺序是 "gemini" 在前。虽然功能上没有影响,但建议保持一致以便维护:

建议调整顺序与类型定义一致
 const ProviderTypeSchema = z.enum([
   "claude",
   "claude-auth",
   "codex",
-  "gemini-cli",
   "gemini",
+  "gemini-cli",
   "openai-compatible",
 ]);

327-348: 双重端点存在性检查可简化

findProviderEndpointByIdprobeProviderEndpointAndRecord 内部都会检查端点是否存在。当前的双重检查虽然提供了更清晰的错误定位,但略显冗余。

如果保留当前实现,建议在第二个检查(Line 342-348)返回更具体的错误信息,例如"端点在探测过程中被删除",以区分两种场景:

可选:区分错误场景
     if (!result) {
       return {
         ok: false,
-        error: "端点不存在",
+        error: "端点探测失败:端点可能已被删除",
         errorCode: ERROR_CODES.NOT_FOUND,
       };
     }

421-428: Schema 复用命名可能造成混淆

DeleteProviderEndpointSchema 被复用于 getEndpointCircuitInforesetEndpointCircuit,虽然功能正确(都只需要 endpointId),但命名暗示"删除"操作可能误导读者。

建议提取通用 schema 或添加注释说明复用原因:

建议:提取通用 EndpointIdInputSchema
+const EndpointIdInputSchema = z.object({
+  endpointId: EndpointIdSchema,
+});
+
 const DeleteProviderEndpointSchema = z.object({
   endpointId: EndpointIdSchema,
 });
 
 // ... later in getEndpointCircuitInfo:
-    const parsed = DeleteProviderEndpointSchema.safeParse(input);
+    const parsed = EndpointIdInputSchema.safeParse(input);
src/repository/provider-endpoints.ts (3)

49-97: 建议使用 Drizzle 推断类型替代 any

当前使用 eslint-disable 来绕过 any 类型检查。可以考虑使用 Drizzle 的 $inferSelect 类型来获得更好的类型安全性。

♻️ 可选的类型改进示例
// 使用 Drizzle 推断类型
type ProviderVendorRow = typeof providerVendors.$inferSelect;

function toProviderVendor(row: ProviderVendorRow): ProviderVendor {
  return {
    id: row.id,
    websiteDomain: row.websiteDomain,
    // ...
  };
}

330-355: 空 payload 处理可以复用已有函数

payload 为空时,直接调用 findProviderEndpointById 会更简洁,避免重复 select 投影代码。

♻️ 简化空 payload 处理
 export async function updateProviderEndpoint(
   endpointId: number,
   payload: { url?: string; label?: string | null; sortOrder?: number; isEnabled?: boolean }
 ): Promise<ProviderEndpoint | null> {
   if (Object.keys(payload).length === 0) {
-    const existing = await db
-      .select({
-        id: providerEndpoints.id,
-        vendorId: providerEndpoints.vendorId,
-        // ... 省略重复的字段投影
-      })
-      .from(providerEndpoints)
-      .where(and(eq(providerEndpoints.id, endpointId), isNull(providerEndpoints.deletedAt)))
-      .limit(1);
-
-    return existing[0] ? toProviderEndpoint(existing[0]) : null;
+    return findProviderEndpointById(endpointId);
   }

444-468: 建议为分页参数添加边界验证

limit 默认值 200 且无上限检查,可能导致性能问题。建议添加最大值限制。

♻️ 添加参数边界验证
 export async function findProviderEndpointProbeLogs(
   endpointId: number,
   limit: number = 200,
   offset: number = 0
 ): Promise<ProviderEndpointProbeLog[]> {
+  const safeLimit = Math.min(Math.max(1, limit), 500);
+  const safeOffset = Math.max(0, offset);
+
   const rows = await db
     .select({
       // ...
     })
     .from(providerEndpointProbeLogs)
     .where(eq(providerEndpointProbeLogs.endpointId, endpointId))
     .orderBy(desc(providerEndpointProbeLogs.createdAt))
-    .limit(limit)
-    .offset(offset);
+    .limit(safeLimit)
+    .offset(safeOffset);

   return rows.map(toProviderEndpointProbeLog);
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 84c8ce6 and 3106ec4.

📒 Files selected for processing (43)
  • drizzle/0055_sour_wallow.sql
  • drizzle/meta/0055_snapshot.json
  • drizzle/meta/_journal.json
  • messages/en/dashboard.json
  • messages/en/settings/providers/strings.json
  • messages/ja/dashboard.json
  • messages/ja/settings/providers/strings.json
  • messages/ru/dashboard.json
  • messages/ru/settings/providers/strings.json
  • messages/zh-CN/dashboard.json
  • messages/zh-CN/settings/providers/strings.json
  • messages/zh-TW/dashboard.json
  • messages/zh-TW/settings/providers/strings.json
  • src/actions/provider-endpoints.ts
  • src/actions/providers.ts
  • src/app/[locale]/dashboard/availability/_components/availability-view.tsx
  • src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx
  • src/app/[locale]/settings/providers/_components/provider-manager.tsx
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  • src/app/api/actions/[...route]/route.ts
  • src/app/api/availability/endpoints/probe-logs/route.ts
  • src/app/api/availability/endpoints/route.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/drizzle/schema.ts
  • src/lib/endpoint-circuit-breaker.ts
  • src/lib/provider-endpoints/endpoint-selector.ts
  • src/lib/provider-endpoints/probe.ts
  • src/lib/redis/endpoint-circuit-breaker-state.ts
  • src/lib/redis/vendor-type-circuit-breaker-state.ts
  • src/lib/vendor-type-circuit-breaker.ts
  • src/repository/_shared/transformers.ts
  • src/repository/index.ts
  • src/repository/provider-endpoints.ts
  • src/repository/provider.ts
  • src/types/provider.ts
  • tests/api/api-openapi-spec.test.ts
  • tests/unit/lib/endpoint-circuit-breaker.test.ts
  • tests/unit/lib/provider-endpoints/endpoint-selector.test.ts
  • tests/unit/lib/provider-endpoints/probe.test.ts
  • tests/unit/lib/vendor-type-circuit-breaker.test.ts
  • tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
  • tests/unit/user-dialogs.test.tsx
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use emoji characters in any code, comments, or string literals

Files:

  • src/actions/providers.ts
  • src/app/api/availability/endpoints/route.ts
  • tests/unit/lib/vendor-type-circuit-breaker.test.ts
  • src/app/api/availability/endpoints/probe-logs/route.ts
  • src/lib/provider-endpoints/endpoint-selector.ts
  • src/lib/provider-endpoints/probe.ts
  • src/app/[locale]/settings/providers/_components/provider-manager.tsx
  • tests/unit/user-dialogs.test.tsx
  • tests/api/api-openapi-spec.test.ts
  • src/lib/endpoint-circuit-breaker.ts
  • src/lib/vendor-type-circuit-breaker.ts
  • tests/unit/lib/endpoint-circuit-breaker.test.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/types/provider.ts
  • src/app/[locale]/dashboard/availability/_components/availability-view.tsx
  • tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
  • tests/unit/lib/provider-endpoints/probe.test.ts
  • src/lib/redis/vendor-type-circuit-breaker-state.ts
  • src/repository/index.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • tests/unit/lib/provider-endpoints/endpoint-selector.test.ts
  • src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx
  • src/actions/provider-endpoints.ts
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  • src/drizzle/schema.ts
  • src/app/api/actions/[...route]/route.ts
  • src/repository/provider-endpoints.ts
  • src/lib/redis/endpoint-circuit-breaker-state.ts
  • src/repository/provider.ts
  • src/repository/_shared/transformers.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias @/ to reference files in ./src/ directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width

Files:

  • src/actions/providers.ts
  • src/app/api/availability/endpoints/route.ts
  • tests/unit/lib/vendor-type-circuit-breaker.test.ts
  • src/app/api/availability/endpoints/probe-logs/route.ts
  • src/lib/provider-endpoints/endpoint-selector.ts
  • src/lib/provider-endpoints/probe.ts
  • src/app/[locale]/settings/providers/_components/provider-manager.tsx
  • tests/unit/user-dialogs.test.tsx
  • tests/api/api-openapi-spec.test.ts
  • src/lib/endpoint-circuit-breaker.ts
  • src/lib/vendor-type-circuit-breaker.ts
  • tests/unit/lib/endpoint-circuit-breaker.test.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/types/provider.ts
  • src/app/[locale]/dashboard/availability/_components/availability-view.tsx
  • tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
  • tests/unit/lib/provider-endpoints/probe.test.ts
  • src/lib/redis/vendor-type-circuit-breaker-state.ts
  • src/repository/index.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • tests/unit/lib/provider-endpoints/endpoint-selector.test.ts
  • src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx
  • src/actions/provider-endpoints.ts
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  • src/drizzle/schema.ts
  • src/app/api/actions/[...route]/route.ts
  • src/repository/provider-endpoints.ts
  • src/lib/redis/endpoint-circuit-breaker-state.ts
  • src/repository/provider.ts
  • src/repository/_shared/transformers.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer named exports over default exports

Files:

  • src/actions/providers.ts
  • src/app/api/availability/endpoints/route.ts
  • tests/unit/lib/vendor-type-circuit-breaker.test.ts
  • src/app/api/availability/endpoints/probe-logs/route.ts
  • src/lib/provider-endpoints/endpoint-selector.ts
  • src/lib/provider-endpoints/probe.ts
  • src/app/[locale]/settings/providers/_components/provider-manager.tsx
  • tests/unit/user-dialogs.test.tsx
  • tests/api/api-openapi-spec.test.ts
  • src/lib/endpoint-circuit-breaker.ts
  • src/lib/vendor-type-circuit-breaker.ts
  • tests/unit/lib/endpoint-circuit-breaker.test.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/types/provider.ts
  • src/app/[locale]/dashboard/availability/_components/availability-view.tsx
  • tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
  • tests/unit/lib/provider-endpoints/probe.test.ts
  • src/lib/redis/vendor-type-circuit-breaker-state.ts
  • src/repository/index.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • tests/unit/lib/provider-endpoints/endpoint-selector.test.ts
  • src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx
  • src/actions/provider-endpoints.ts
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  • src/drizzle/schema.ts
  • src/app/api/actions/[...route]/route.ts
  • src/repository/provider-endpoints.ts
  • src/lib/redis/endpoint-circuit-breaker-state.ts
  • src/repository/provider.ts
  • src/repository/_shared/transformers.ts
src/app/api/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

API routes should use Hono framework and follow Next.js 16 App Router conventions

Files:

  • src/app/api/availability/endpoints/route.ts
  • src/app/api/availability/endpoints/probe-logs/route.ts
  • src/app/api/actions/[...route]/route.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

All new features must have unit test coverage of at least 80%

Files:

  • tests/unit/lib/vendor-type-circuit-breaker.test.ts
  • tests/unit/user-dialogs.test.tsx
  • tests/api/api-openapi-spec.test.ts
  • tests/unit/lib/endpoint-circuit-breaker.test.ts
  • tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
  • tests/unit/lib/provider-endpoints/probe.test.ts
  • tests/unit/lib/provider-endpoints/endpoint-selector.test.ts
tests/**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Vitest for unit testing and happy-dom for DOM testing

Files:

  • tests/unit/lib/vendor-type-circuit-breaker.test.ts
  • tests/unit/user-dialogs.test.tsx
  • tests/api/api-openapi-spec.test.ts
  • tests/unit/lib/endpoint-circuit-breaker.test.ts
  • tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
  • tests/unit/lib/provider-endpoints/probe.test.ts
  • tests/unit/lib/provider-endpoints/endpoint-selector.test.ts
src/app/v1/_lib/proxy/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

The proxy pipeline processes requests through a GuardPipeline with sequential guards: auth, sensitive, client, model, version, probe, session, warmup, requestFilter, rateLimit, provider, providerRequestFilter, messageContext

Files:

  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
src/repository/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Drizzle ORM for data access in the repository layer

Files:

  • src/repository/index.ts
  • src/repository/provider-endpoints.ts
  • src/repository/provider.ts
  • src/repository/_shared/transformers.ts
src/drizzle/schema.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Modify database schema in src/drizzle/schema.ts, then run bun run db:generate to generate migrations. Never create SQL migration files manually

Files:

  • src/drizzle/schema.ts
🧠 Learnings (10)
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: 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.

Applied to files:

  • src/actions/providers.ts
  • src/app/api/availability/endpoints/route.ts
  • src/app/api/availability/endpoints/probe-logs/route.ts
  • src/lib/provider-endpoints/endpoint-selector.ts
  • src/lib/provider-endpoints/probe.ts
  • src/lib/endpoint-circuit-breaker.ts
  • src/lib/vendor-type-circuit-breaker.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/types/provider.ts
  • src/lib/redis/vendor-type-circuit-breaker-state.ts
  • src/repository/index.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/actions/provider-endpoints.ts
  • src/drizzle/schema.ts
  • src/app/api/actions/[...route]/route.ts
  • src/repository/provider-endpoints.ts
  • src/lib/redis/endpoint-circuit-breaker-state.ts
  • src/repository/provider.ts
  • src/repository/_shared/transformers.ts
📚 Learning: 2026-01-10T06:19:58.167Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:19:58.167Z
Learning: Do not modify hardcoded Chinese error messages in Server Actions under src/actions/*.ts as part of piecemeal changes. This is a repo-wide architectural decision that requires a coordinated i18n refactor across all Server Action files (e.g., model-prices.ts, users.ts, system-config.ts). Treat i18n refactor as a separate unified task rather than per-PR changes, and plan a project-wide approach for replacing hardcoded strings with localized resources.

Applied to files:

  • src/actions/providers.ts
  • src/actions/provider-endpoints.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/app/api/**/*.{ts,tsx} : API routes should use Hono framework and follow Next.js 16 App Router conventions

Applied to files:

  • src/app/api/availability/endpoints/route.ts
  • src/app/api/actions/[...route]/route.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : All new features must have unit test coverage of at least 80%

Applied to files:

  • tests/unit/lib/vendor-type-circuit-breaker.test.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/app/v1/_lib/proxy/**/*.{ts,tsx} : The proxy pipeline processes requests through a GuardPipeline with sequential guards: auth, sensitive, client, model, version, probe, session, warmup, requestFilter, rateLimit, provider, providerRequestFilter, messageContext

Applied to files:

  • src/app/api/availability/endpoints/probe-logs/route.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
📚 Learning: 2026-01-05T03:02:14.502Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx:66-66
Timestamp: 2026-01-05T03:02:14.502Z
Learning: In the claude-code-hub project, the translations.actions.addKey field in UserKeyTableRowProps is defined as optional for backward compatibility, but all actual callers in the codebase provide the complete translations object. The field has been added to all 5 locale files (messages/{locale}/dashboard.json).

Applied to files:

  • tests/unit/user-dialogs.test.tsx
  • messages/zh-CN/dashboard.json
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text

Applied to files:

  • tests/unit/user-dialogs.test.tsx
  • tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
📚 Learning: 2026-01-10T06:20:04.478Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:20:04.478Z
Learning: In the `ding113/claude-code-hub` repository, Server Actions (files under `src/actions/*.ts`) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.

Applied to files:

  • tests/unit/user-dialogs.test.tsx
  • tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/drizzle/schema.ts : Modify database schema in `src/drizzle/schema.ts`, then run `bun run db:generate` to generate migrations. Never create SQL migration files manually

Applied to files:

  • drizzle/meta/0055_snapshot.json
  • src/drizzle/schema.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/repository/**/*.{ts,tsx} : Use Drizzle ORM for data access in the repository layer

Applied to files:

  • src/drizzle/schema.ts
  • src/repository/provider-endpoints.ts
🧬 Code graph analysis (20)
src/app/api/availability/endpoints/route.ts (4)
src/types/provider.ts (1)
  • ProviderType (6-12)
src/lib/auth.ts (1)
  • getSession (116-128)
src/repository/index.ts (1)
  • findProviderEndpointsByVendorAndType (52-52)
src/repository/provider-endpoints.ts (1)
  • findProviderEndpointsByVendorAndType (248-282)
tests/unit/lib/vendor-type-circuit-breaker.test.ts (2)
src/types/provider.ts (1)
  • ProviderType (6-12)
src/lib/vendor-type-circuit-breaker.ts (5)
  • setVendorTypeCircuitManualOpen (157-177)
  • getVendorTypeCircuitInfo (107-113)
  • isVendorTypeCircuitOpen (115-137)
  • recordVendorTypeAllEndpointsTimeout (139-155)
  • resetVendorTypeCircuit (179-187)
src/app/api/availability/endpoints/probe-logs/route.ts (4)
src/app/api/actions/[...route]/route.ts (1)
  • GET (1809-1809)
src/app/api/availability/endpoints/route.ts (1)
  • GET (22-45)
src/lib/auth.ts (1)
  • getSession (116-128)
src/repository/provider-endpoints.ts (2)
  • findProviderEndpointById (188-215)
  • findProviderEndpointProbeLogs (444-468)
src/lib/provider-endpoints/endpoint-selector.ts (3)
src/types/provider.ts (2)
  • ProviderEndpoint (387-404)
  • ProviderType (6-12)
src/repository/provider-endpoints.ts (1)
  • findProviderEndpointsByVendorAndType (248-282)
src/lib/endpoint-circuit-breaker.ts (1)
  • isEndpointCircuitOpen (116-135)
src/lib/provider-endpoints/probe.ts (4)
src/lib/logger.ts (1)
  • logger (168-187)
src/types/provider.ts (1)
  • ProviderEndpointProbeSource (385-385)
src/repository/index.ts (2)
  • findProviderEndpointById (50-50)
  • recordProviderEndpointProbeResult (55-55)
src/repository/provider-endpoints.ts (2)
  • findProviderEndpointById (188-215)
  • recordProviderEndpointProbeResult (405-442)
src/app/[locale]/settings/providers/_components/provider-manager.tsx (5)
src/types/provider.ts (1)
  • ProviderType (6-12)
src/app/[locale]/settings/providers/_components/provider-sort-dropdown.tsx (1)
  • SortKey (13-13)
src/components/ui/button.tsx (1)
  • Button (58-58)
src/app/[locale]/settings/providers/_components/provider-list.tsx (1)
  • ProviderList (28-67)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (1)
  • ProviderVendorView (79-145)
src/lib/vendor-type-circuit-breaker.ts (3)
src/types/provider.ts (1)
  • ProviderType (6-12)
src/lib/redis/vendor-type-circuit-breaker-state.ts (4)
  • VendorTypeCircuitBreakerState (9-14)
  • loadVendorTypeCircuitState (47-74)
  • saveVendorTypeCircuitState (76-99)
  • deleteVendorTypeCircuitState (101-121)
src/lib/logger.ts (1)
  • logger (168-187)
tests/unit/lib/endpoint-circuit-breaker.test.ts (1)
src/lib/endpoint-circuit-breaker.ts (5)
  • recordEndpointFailure (137-158)
  • isEndpointCircuitOpen (116-135)
  • recordEndpointSuccess (160-185)
  • resetEndpointCircuit (187-196)
  • getEndpointHealthInfo (109-114)
src/app/v1/_lib/proxy/forwarder.ts (7)
src/lib/provider-endpoints/endpoint-selector.ts (1)
  • getPreferredProviderEndpoints (30-54)
src/lib/logger.ts (1)
  • logger (168-187)
src/lib/vendor-type-circuit-breaker.ts (2)
  • isVendorTypeCircuitOpen (115-137)
  • recordVendorTypeAllEndpointsTimeout (139-155)
src/lib/endpoint-circuit-breaker.ts (2)
  • recordEndpointSuccess (160-185)
  • recordEndpointFailure (137-158)
src/app/v1/_lib/proxy/errors.ts (1)
  • ProxyError (14-450)
src/app/v1/_lib/gemini/protocol.ts (1)
  • GEMINI_PROTOCOL (1-20)
src/app/v1/_lib/url.ts (1)
  • buildProxyUrl (25-84)
src/app/[locale]/dashboard/availability/_components/availability-view.tsx (1)
src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx (1)
  • EndpointProbeHistory (44-301)
tests/unit/lib/provider-endpoints/probe.test.ts (2)
src/types/provider.ts (1)
  • ProviderEndpoint (387-404)
src/lib/provider-endpoints/probe.ts (2)
  • probeEndpointUrl (93-102)
  • probeProviderEndpointAndRecord (104-129)
src/lib/redis/vendor-type-circuit-breaker-state.ts (2)
src/types/provider.ts (1)
  • ProviderType (6-12)
src/lib/logger.ts (1)
  • logger (168-187)
src/app/v1/_lib/proxy/provider-selector.ts (2)
src/lib/vendor-type-circuit-breaker.ts (1)
  • isVendorTypeCircuitOpen (115-137)
src/lib/logger.ts (1)
  • logger (168-187)
tests/unit/lib/provider-endpoints/endpoint-selector.test.ts (2)
src/types/provider.ts (1)
  • ProviderEndpoint (387-404)
src/lib/provider-endpoints/endpoint-selector.ts (3)
  • rankProviderEndpoints (7-28)
  • getPreferredProviderEndpoints (30-54)
  • pickBestProviderEndpoint (56-63)
src/actions/provider-endpoints.ts (7)
src/lib/auth.ts (1)
  • getSession (116-128)
src/types/provider.ts (4)
  • ProviderVendor (375-383)
  • ProviderType (6-12)
  • ProviderEndpoint (387-404)
  • ProviderEndpointProbeLog (406-416)
src/repository/provider-endpoints.ts (8)
  • findProviderVendors (148-168)
  • findProviderVendorById (170-186)
  • findProviderEndpointsByVendorAndType (248-282)
  • createProviderEndpoint (284-324)
  • updateProviderEndpoint (326-388)
  • softDeleteProviderEndpoint (390-403)
  • findProviderEndpointById (188-215)
  • findProviderEndpointProbeLogs (444-468)
src/lib/logger.ts (1)
  • logger (168-187)
src/lib/utils/zod-i18n.ts (2)
  • formatZodError (154-159)
  • extractZodErrorCode (176-190)
src/lib/provider-endpoints/probe.ts (1)
  • probeProviderEndpointAndRecord (104-129)
src/lib/endpoint-circuit-breaker.ts (2)
  • getEndpointHealthInfo (109-114)
  • resetEndpointCircuit (187-196)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (3)
src/types/provider.ts (4)
  • ProviderDisplay (147-217)
  • ProviderVendor (375-383)
  • ProviderType (6-12)
  • ProviderEndpoint (387-404)
src/actions/provider-endpoints.ts (8)
  • getProviderVendors (118-130)
  • getVendorTypeCircuitInfo (477-517)
  • setVendorTypeCircuitManualOpen (519-551)
  • getProviderEndpoints (146-172)
  • probeProviderEndpoint (295-356)
  • removeProviderEndpoint (258-293)
  • addProviderEndpoint (174-211)
  • editProviderEndpoint (213-256)
src/lib/vendor-type-circuit-breaker.ts (2)
  • getVendorTypeCircuitInfo (107-113)
  • setVendorTypeCircuitManualOpen (157-177)
src/drizzle/schema.ts (2)
src/types/provider.ts (1)
  • ProviderType (6-12)
src/lib/webhook/types.ts (1)
  • ProviderType (79-79)
src/app/api/actions/[...route]/route.ts (1)
src/lib/api/action-adapter-openapi.ts (1)
  • createActionRoute (253-419)
src/repository/provider-endpoints.ts (2)
src/types/provider.ts (5)
  • ProviderVendor (375-383)
  • ProviderEndpoint (387-404)
  • ProviderEndpointProbeLog (406-416)
  • ProviderType (6-12)
  • ProviderEndpointProbeSource (385-385)
src/drizzle/schema.ts (3)
  • providerVendors (132-145)
  • providerEndpoints (297-339)
  • providerEndpointProbeLogs (342-363)
src/lib/redis/endpoint-circuit-breaker-state.ts (2)
src/lib/logger.ts (1)
  • logger (168-187)
scripts/sync-settings-keys.js (1)
  • key (32-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: pr-review
  • GitHub Check: Greptile Review
  • GitHub Check: pr-description
  • GitHub Check: Docker Build Test
  • GitHub Check: dev-build-deploy
🔇 Additional comments (82)
drizzle/meta/_journal.json (1)

389-396: LGTM!

新增的迁移日志条目 0055_sour_wallow 格式正确,与现有条目保持一致。

tests/api/api-openapi-spec.test.ts (1)

192-198: LGTM!

上限从 60 调整为 80 是合理的,以适应本 PR 新增的 provider endpoints 相关 API 路由。注释已说明这是"合理范围"约束,而非精确断言。

tests/unit/user-dialogs.test.tsx (2)

195-203: LGTM!

新增的 ui.tagInput 翻译键为标签输入组件提供了必要的错误消息支持,符合 i18n 规范。


331-334: LGTM!

新增的 providerGroupSelect 翻译键支持供应商分组选择器组件的国际化显示。

tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx (1)

68-69: LGTM!

使用本地定义的 loadMessages() 函数直接从实际的翻译文件加载消息,确保测试使用真实的国际化内容。

messages/zh-CN/dashboard.json (1)

1128-1149: LGTM!

新增的 probeHistory 本地化条目已在全部 5 种语言(en、ja、ru、zh-CN、zh-TW)的 dashboard.json 中完整添加,所有语言的键结构保持一致,符合 i18n 多语言支持的要求。翻译准确且结构清晰。

messages/zh-TW/dashboard.json (1)

999-1020: LGTM!

新增的 probeHistory 翻译块结构完整,涵盖了端点探测历史功能所需的所有 UI 字符串。繁体中文翻译准确,与其他语言版本(en、zh-CN、ja、ru)保持一致。

messages/en/settings/providers/strings.json (1)

46-83: LGTM!

新增的本地化键完整覆盖了端点管理、探测状态、熔断器控制等功能所需的 UI 字符串。键命名遵循现有约定,JSON 语法正确。

messages/zh-CN/settings/providers/strings.json (1)

46-83: LGTM!

简体中文翻译完整且准确,与英文版本的键集保持一致。翻译用语符合中国大陆用户习惯(如"测速"、"熔断"、"端点"等)。

src/types/provider.ts (3)

375-416: LGTM!

新增的类型定义结构清晰:

  • ProviderVendor 正确定义了供应商聚合实体的字段,可空字段使用 | null 明确语义。
  • ProviderEndpointProbeSource 作为联合类型清晰定义了探测来源。
  • ProviderEndpoint 完整涵盖端点元数据,软删除字段 deletedAt 使用 Date | null
  • ProviderEndpointProbeLog 结构合理,与数据库 schema 对应。

44-45: LGTM!

Provider 接口中添加 providerVendorId: number 字段,建立了与 ProviderVendor 实体的关联。


160-161: LGTM!

ProviderDisplay 接口同步添加 providerVendorId 字段,确保前端显示类型与核心类型一致。

src/lib/provider-endpoints/endpoint-selector.ts (2)

7-28: LGTM!

rankProviderEndpoints 实现清晰:

  • 使用 slice() 避免修改原数组。
  • 优先级排序逻辑合理:探测成功 > 未探测 > 探测失败。
  • 延迟为 null 时使用 POSITIVE_INFINITY 处理,确保排序稳定性。
  • 最后按 id 排序保证确定性结果。

56-63: LGTM!

pickBestProviderEndpoint 作为便捷包装函数,实现简洁,使用 nullish coalescing (??) 正确处理空数组情况。

src/lib/redis/endpoint-circuit-breaker-state.ts (3)

1-28: 类型定义和常量设计合理。

接口和默认状态定义清晰,TTL 为 24 小时对于断路器状态是合理的设置。


50-76: 加载函数的错误处理和日志记录实现良好。

函数正确处理了 Redis 不可用、数据为空和异常情况,返回 null 的策略允许调用方优雅降级。


102-118: 删除函数实现正确。

静默处理 Redis 不可用的情况是合理的,错误日志包含足够的上下文信息便于排查问题。

src/lib/endpoint-circuit-breaker.ts (6)

1-23: 配置和接口定义清晰。

断路器配置参数(3次失败阈值、5分钟开启时长、1次成功即恢复)对于端点健康检查场景是合理的默认值。


50-90: getOrCreateHealth 中 Redis 同步逻辑存在边界情况。

第 74-80 行:当本地状态为非 "closed" 但 Redis 返回 null 时,代码会将状态重置为 "closed"。这个行为可能导致问题:

  • 如果 Redis TTL 过期但本地状态仍有效(例如刚开启断路器),会意外关闭断路器
  • 在多实例部署中,某个实例可能在另一个实例设置状态前读取到空值

建议验证此行为是否符合预期。如果 Redis 是权威数据源,当前逻辑是正确的;如果本地状态应该优先,则需要调整。


92-107: persistStateToRedis 使用 fire-and-forget 模式。

使用 .catch() 处理 Promise 拒绝是合理的,但调用方无法得知持久化是否成功。在网络问题导致 Redis 写入失败的情况下,状态可能只存在于本地内存中。

当前实现对于断路器场景可接受,因为状态最终会通过后续操作重新同步。


116-135: isEndpointCircuitOpen 状态转换逻辑正确。

从 "open" 到 "half-open" 的转换在正确的时机触发,并正确持久化到 Redis。


137-158: recordEndpointFailure 实现正确。

失败计数、阈值检查和状态转换逻辑都是正确的。日志包含足够的诊断信息。


160-185: recordEndpointSuccess 实现正确。

正确处理了 "half-open" 状态下的成功计数,以及 "closed" 状态下清零失败计数的逻辑。

tests/unit/lib/endpoint-circuit-breaker.test.ts (2)

26-91: 测试覆盖了完整的断路器生命周期。

测试验证了:

  • 3次失败后断路器打开
  • 5分钟后转为 half-open
  • 1次成功后关闭并清零
  • resetEndpointCircuit 调用删除函数

测试逻辑清晰,使用 fake timers 正确模拟时间推进。


93-123: recordEndpointSuccess 测试验证了 failureCount > 0 时的清零行为。

测试覆盖了一个重要边界情况:在 "closed" 状态下有失败记录时,成功调用应清零失败计数。

src/lib/redis/vendor-type-circuit-breaker-state.ts (3)

1-27: 模块结构与端点断路器状态模块保持一致。

类型定义清晰,30 天的 TTL 对于 vendor 级别的状态是合理的。manualOpen 字段支持管理员手动干预。


47-74: 加载函数实现正确。

错误处理和日志记录包含了 vendorIdproviderType 上下文信息,便于问题排查。


101-121: 删除函数实现正确。

与端点模块保持一致的实现模式。

messages/ja/settings/providers/strings.json (1)

46-83: 日语本地化内容完整且专业。

新增的 38 个本地化键覆盖了端点管理 UI 的所有文案,包括:

  • 视图模式切换(列表/供应商)
  • 端点 CRUD 操作
  • 探针测试和状态显示
  • 断路器状态管理

翻译质量良好,符合日语 UI 惯例。

messages/ru/settings/providers/strings.json (1)

46-83: LGTM!

俄语本地化字符串已正确添加,涵盖了新的端点管理、探针检查和电路断路器功能所需的所有 UI 文本。翻译与现有命名模式一致。

src/app/[locale]/dashboard/availability/_components/availability-view.tsx (2)

24-24: LGTM!

从同目录导入 EndpointProbeHistory 组件,遵循了项目的导入约定。


528-530: LGTM!

EndpointProbeHistory 组件在 Legend 卡片之后逻辑性地放置,为可用性仪表板增加了端点探针历史功能。组件集成简洁,符合现有的 UI 布局模式。

messages/ja/dashboard.json (1)

999-1020: LGTM!

日语本地化字符串已正确添加,包含了端点探针历史功能所需的所有 UI 文本(标题、描述、选择器标签、列标题、状态消息等)。翻译质量良好,与组件需求匹配。

src/actions/providers.ts (1)

239-239: LGTM!

providerVendorId 字段已正确添加到 getProviders 的返回数据中,与 ProviderDisplay 类型扩展保持一致。这使前端能够获取供应商的 vendor 关联信息,支持新的 vendor 视图功能。

src/repository/_shared/transformers.ts (1)

69-69: providerVendorId 的默认值 0 是正确的,无需修改

根据数据库 schema 定义,providerVendorId 被标记为 .notNull(),不允许 null 值。0 作为默认值是为了表示"未分配供应商"的哨兵值(sentinel value),这与 providerVendors 表的 serial 主键从 1 开始相匹配。整个代码库通过 providerVendorId > 0 的检查来判断是否已分配有效的供应商。此模式与其他字段默认值(如 weight: 1)的处理方式一致。

Likely an incorrect or invalid review comment.

src/app/api/availability/endpoints/probe-logs/route.ts (1)

1-46: LGTM! 路由实现清晰规范。

此 API 路由实现与现有的 src/app/api/availability/endpoints/route.ts 模式保持一致:

  • 管理员身份验证
  • 参数验证(endpointId、limit、offset)
  • 适当的错误处理和状态码

唯一的小建议:根据编码指南,API 路由应使用 Hono 框架。但观察到现有的 availability endpoints 路由也使用原生 Next.js 处理方式,为保持一致性,当前实现是可接受的。

drizzle/meta/0055_snapshot.json (1)

1-2874: 自动生成的数据库快照文件,结构合理。

此文件由 bun run db:generate 自动生成。新增的表结构设计合理:

  • provider_vendors: 唯一索引 website_domain
  • provider_endpoints: 复合唯一索引 (vendor_id, provider_type, url),适当的部分索引过滤 deleted_at IS NULL
  • provider_endpoint_probe_logs: 外键级联删除,索引支持按 endpoint_idcreated_at 查询

基于 learnings,此类文件不应手动创建或修改。

tests/unit/lib/provider-endpoints/endpoint-selector.test.ts (1)

1-156: 测试覆盖全面,实现规范。

测试用例设计合理:

  • rankProviderEndpoints: 验证禁用/删除过滤和多级排序逻辑
  • getPreferredProviderEndpoints: 验证排除逻辑(禁用、删除、显式排除、熔断)
  • pickBestProviderEndpoint: 验证空候选时返回 null

mock 设置正确使用 vi.resetModules() + vi.doMock() + 动态 import(),确保模块隔离。

src/app/[locale]/settings/providers/_components/provider-manager.tsx (2)

208-230: 视图切换 UI 实现规范。

切换按钮组实现良好:

  • 使用 i18n 获取标签文本(tStrings("viewModeList")tStrings("viewModeVendor")
  • 包含 title 属性提供无障碍支持
  • 按钮状态通过 variant 正确反映当前选中视图

348-368: 条件渲染逻辑正确。

ProviderVendorViewProviderList 接收相同的 props,确保两种视图模式下功能一致。

messages/en/dashboard.json (1)

1023-1044: probeHistory 本地化字符串已完整覆盖所有支持语言。

验证确认英文、简体中文、繁体中文、日语和俄语的 messages/*/dashboard.json 文件中均包含完整的 probeHistory 翻译。所有18个键值(包括 columns 子部分的5个键)在所有5种语言中都正确翻译,命名规范与现有文本一致。

tests/unit/lib/vendor-type-circuit-breaker.test.ts (3)

27-76: 测试用例覆盖良好

测试用例正确验证了:

  1. 手动开启时 isVendorTypeCircuitOpen 始终返回 true
  2. recordVendorTypeAllEndpointsTimeout 在手动开启状态下不会覆盖状态
  3. circuitOpenUntil 在手动开启时为 null

测试逻辑清晰,断言完整。


78-130: 自动开启和最小超时测试逻辑正确

测试正确验证了:

  1. 传入 openDurationMs: 0 时,最小值 1000ms 被强制应用
  2. 1000ms 后熔断器自动关闭
  3. 状态正确从 open 转换为 closed

132-160: 重置测试验证了缓存清理和 Redis 删除

测试正确验证了 resetVendorTypeCircuit 的行为:

  1. 调用 deleteMock 清理 Redis 状态
  2. 后续调用会重新从 Redis 加载(loadMock 被调用两次)
src/app/api/actions/[...route]/route.ts (1)

387-604: 新增的供应商端点管理路由实现良好

路由定义遵循了文件中现有的模式:

  • 所有管理员路由都正确设置了 requiredRole: "admin"
  • 请求 schema 验证合理(如 timeoutMs 限制在 1000-120000ms)
  • 路由命名清晰,职责明确
src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx (1)

149-300: 组件 UI 实现良好

  • 使用了正确的 i18n 翻译函数 t()
  • 加载状态和空状态处理完善
  • 表格渲染逻辑清晰
  • Badge 组件使用了适当的颜色区分成功/失败状态
tests/unit/lib/provider-endpoints/probe.test.ts (1)

31-65: LGTM! 测试覆盖全面。

测试用例覆盖了 probeEndpointUrl 的主要场景:HEAD 成功、HEAD 失败回退 GET、5xx/4xx 状态码处理、超时处理等。模拟设置和断言逻辑正确。

src/app/v1/_lib/proxy/provider-selector.ts (2)

532-544: LGTM! 正确实现了供应商类型熔断检查。

在会话复用路径中添加 vendor-type 熔断检查是正确的,这可以防止通过会话复用绕过故障隔离机制。检查逻辑 provider.providerVendorId > 0 确保只对有效的 vendorId 进行检查。


967-978: 熔断检查顺序合理。

将 vendor-type 熔断检查放在 provider 级别熔断检查之前是正确的设计。vendor-type 熔断是更宽范围的保护(同一供应商+类型的所有端点),应该优先于单个 provider 的熔断检查。

drizzle/0055_sour_wallow.sql (2)

113-138: 索引策略合理。

  • 使用部分索引 (WHERE deleted_at IS NULL) 优化活跃记录查询
  • uniq_provider_endpoints_vendor_type_url 唯一索引防止重复端点
  • ON DELETE restrict 用于 providers FK 防止意外删除关联的 vendor
  • ON DELETE cascade 用于 endpoints 和 probe_logs 保持数据一致性

44-66: 域名提取逻辑的技术描述有误,且无证据表明存在此问题。

对于包含认证信息的 URL(如 https://user:pass@api.example.com/v1),当前逻辑的实际提取过程是:

  1. 去掉协议后:user:pass@api.example.com/v1
  2. / 分割取第一部分:user:pass@api.example.com
  3. : 分割取第一部分:user

结果会是 user 而非 user:pass@api.example.com,这样会导致完全错误的域名提取。

但经过代码搜索,发现:

  • 现有代码库中不存在包含认证信息的提供商 URL
  • website_url 字段使用 Zod 的 .url() 验证,会限制输入格式
  • 此问题为假设性边界案例,不影响现有数据

若要增强鲁棒性,可在提取前处理 @ 符号,但基于当前数据情况,此修改不是必需的。

src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (1)

535-564: 表单处理模式正确。

表单提交逻辑正确处理了加载状态、错误处理和缓存失效。使用 try/catch 确保错误不会导致 UI 崩溃。

src/repository/provider.ts (2)

14-25: LGTM! 正确计算并存储 providerVendorId

在创建 provider 时预先计算 providerVendorId 并将其包含在插入数据中是正确的做法。getOrCreateProviderVendorIdFromUrls 会确保 vendor 记录存在。


450-471: URL 变更时正确重新计算 providerVendorId

urlwebsite_url 更新时重新计算 providerVendorId 的逻辑是正确的。代码正确处理了:

  1. 只在相关 URL 变更时触发重新计算
  2. 使用当前值作为回退,确保完整的 URL 信息传递给 vendor 查找函数
  3. 如果 provider 不存在(current 为 null),跳过 vendorId 更新,后续的 update 操作会返回空结果
src/repository/index.ts (1)

47-59: LGTM!

新增的 provider-endpoints 模块导出遵循了现有的代码组织模式,保持了 repository 层统一入口的一致性。导出的函数覆盖了端点管理所需的完整 CRUD 操作、探针记录和供应商查询功能。

src/lib/provider-endpoints/probe.ts (4)

20-39: LGTM!

fetchWithTimeout 实现正确使用了 AbortController 进行超时控制,latency 测量准确。finally 块确保了定时器在所有情况下都能被清理,避免内存泄漏。


51-91: LGTM!

tryProbe 函数逻辑正确:

  • statusCode < 500 将 4xx 视为"可达"是合理的探针策略,因为 4xx 表示端点响应正常(只是权限/请求问题)
  • 错误分类为 http_5xxtimeoutnetwork_error 等类型便于后续分析
  • debug 级别日志避免了生产环境日志过多

93-102: HEAD 优先策略的回退逻辑建议验证

当前逻辑:HEAD 请求失败且 statusCode === null 时回退到 GET。这意味着只有网络错误/超时才会触发回退。

考虑场景:某些服务器对 HEAD 返回 405 Method Not Allowed,此时 statusCode 不为 null,不会触发 GET 回退。建议确认这是否符合预期行为,或者是否需要扩展回退条件:

 export async function probeEndpointUrl(
   url: string,
   timeoutMs: number = DEFAULT_TIMEOUT_MS
 ): Promise<EndpointProbeResult> {
   const head = await tryProbe(url, "HEAD", timeoutMs);
-  if (head.statusCode === null) {
+  if (head.statusCode === null || head.statusCode === 405) {
     return tryProbe(url, "GET", timeoutMs);
   }
   return head;
 }

104-129: LGTM!

probeProviderEndpointAndRecord 函数实现清晰:

  • 先查询端点是否存在,避免无效的网络请求
  • 探针结果完整记录到数据库,包含所有必要字段
  • 返回 null 处理端点不存在的边界情况
src/app/v1/_lib/proxy/forwarder.ts (5)

266-280: LGTM!

Vendor-type 熔断检查逻辑正确:

  • 在重试循环前检查熔断状态,避免无效请求
  • 熔断时通过设置 attemptCount = maxAttemptsPerProvider 跳过内层循环
  • 正确将 providerId 加入失败列表以便后续供应商切换

286-295: 端点轮询选择逻辑正确

Round-robin 策略通过 (attemptCount - 1) % endpointCandidates.length 实现端点轮换。Line 250-252 确保了 endpointCandidates 至少有一个元素,避免了除零错误。


461-473: 端点失败记录逻辑正确,但 allEndpointAttemptsTimedOut 追踪存在边界情况

当前逻辑:

  • 只在 attemptCount <= endpointCandidates.length 时更新 endpointAttemptsEvaluated
  • 非超时错误会将 allEndpointAttemptsTimedOut 设为 false

潜在问题:如果 maxAttemptsPerProvider 被整流器等逻辑动态增加(Line 638),额外的重试不会更新 endpointAttemptsEvaluated,可能导致 allEndpointAttemptsTimedOut 判断不准确。

建议确认这是预期行为,或者在 Line 894-907 的判断中考虑此边界情况。


894-907: LGTM!

Vendor-type 全端点超时记录逻辑正确:

  • 仅在所有端点均超时时触发熔断
  • 条件检查包含 !isMcpRequest 确保 MCP 请求不受影响
  • 熔断后立即 break 进入供应商切换逻辑

1029-1033: LGTM!

doForward 签名变更正确引入了 baseUrl 参数,使得每次请求可以指定不同的端点 URL,为多端点路由提供了基础。

src/lib/vendor-type-circuit-breaker.ts (4)

115-137: LGTM!

isVendorTypeCircuitOpen 逻辑正确:

  • 手动熔断优先级最高
  • 时间窗口过期后自动关闭熔断并持久化
  • 状态字段清理完整(circuitState、circuitOpenUntil、lastFailureTime)

139-155: LGTM!

recordVendorTypeAllEndpointsTimeout 实现正确:

  • Math.max(1000, openDurationMs) 确保最小熔断窗口为 1 秒,防止配置错误
  • 手动熔断状态不被自动熔断覆盖
  • 同时更新 lastFailureTime 便于调试和监控

157-177: LGTM!

setVendorTypeCircuitManualOpen 状态管理清晰:

  • 手动开启时 circuitOpenUntil = null 确保不会自动关闭
  • 手动关闭时完全重置所有状态字段
  • 两种操作都触发 Redis 持久化

179-187: LGTM!

resetVendorTypeCircuit 清理逻辑完整:同时清除内存缓存、Redis 状态和 loadedFromRedis 标记,确保下次访问时能正确初始化。

src/actions/provider-endpoints.ts (3)

110-116: LGTM!

getAdminSession 辅助函数简洁有效,复用了现有的 getSession 并添加了管理员角色检查。


174-211: LGTM!

addProviderEndpoint 实现规范:

  • 完整的权限检查和输入验证
  • 使用 formatZodErrorextractZodErrorCode 提供结构化错误信息
  • 错误处理覆盖了 Zod 验证失败和数据库操作异常

其他 CRUD 操作(edit、remove)遵循相同模式,代码结构一致。


477-584: LGTM!

Vendor-type 熔断相关的三个 action(getVendorTypeCircuitInfo、setVendorTypeCircuitManualOpen、resetVendorTypeCircuit)实现一致,正确委托给底层状态管理函数,错误处理完善。

src/repository/provider-endpoints.ts (4)

15-24: 日期处理辅助函数设计合理

toDatetoNullableDate 函数正确处理了各种输入类型,为数据库查询结果提供了一致的日期转换。


26-47: URL 域名规范化逻辑正确

函数正确处理了多种输入格式,包括带/不带协议前缀的 URL,并规范化了 www. 前缀的移除逻辑。


99-146: Upsert 模式实现正确,处理了并发竞态条件

使用 onConflictDoNothing + 回退查询的模式正确处理了并发插入场景。当多个请求同时尝试创建相同 vendor 时,能够安全地返回已存在的记录。


405-442: 探针记录事务处理正确

使用事务确保日志插入和端点状态更新的原子性。当探针成功时清除错误字段的逻辑也是正确的。

messages/zh-TW/settings/providers/strings.json (1)

46-83: 新增的本地化键值完整且翻译质量良好

新增的端点管理、探针检测和熔断状态相关的翻译键覆盖了 UI 所需的所有场景,翻译自然流畅。

src/drizzle/schema.ts (6)

131-145: providerVendors 表设计合理

表结构简洁,使用 websiteDomain 唯一索引确保供应商域名唯一性。注意该表没有 deletedAt 列,删除是物理删除,这与 providers 表的 onDelete: 'restrict' 约束配合使用是合理的设计。


296-339: providerEndpoints 表设计完善

  • 复合唯一索引 (vendorId, providerType, url) 防止重复端点
  • 使用部分索引 WHERE deletedAt IS NULL 优化活跃端点查询
  • onDelete: 'cascade' 确保 vendor 删除时级联清理端点

341-363: providerEndpointProbeLogs 表设计正确

索引 (endpointId, createdAt DESC) 与 repository 层的查询模式匹配,支持高效的日志分页查询。

建议: 探针日志可能会快速增长,考虑添加数据保留策略或定期清理机制(可作为后续优化)。


720-746: 关系定义完整且正确

新增的关系定义正确建立了 vendors、endpoints 和 probe logs 之间的双向关联,支持 Drizzle 的关系查询功能。


154-156: 迁移脚本正确处理了 NOT NULL 约束转换

onDelete: 'restrict' 设置正确。迁移脚本通过分阶段处理妥善解决了 NOT NULL 约束问题:

  1. 先添加列为可空状态
  2. 创建 provider_vendors 记录(包括无域名情况下的 unknown-{provider_id} 后备方案)
  3. 批量更新现有 providers 的 provider_vendor_id
  4. 最后才将列设置为 NOT NULL

现有数据在添加约束前已完全填充,迁移不会失败。


18-18: 导入正确,但注意命名冲突

第 18 行的导入来自 @/types/provider,正确对应本文件中 providerType 字段使用的 API 供应商类型(claude、codex、gemini 等)。

需注意:代码库中存在同名的 ProviderType 类型定义在 src/lib/webhook/types.ts,用于 Webhook 通知类型(wechat、feishu、dingtalk、telegram、custom),与本文件的 ProviderType 完全不同。在涉及 Webhook 相关代码时应确保引入正确的类型,Webhook 代码也导出了更明确的 WebhookProviderType 以避免混淆。

Comment on lines +1 to +11
CREATE TABLE IF NOT EXISTS "provider_endpoint_probe_logs" (
"id" serial PRIMARY KEY NOT NULL,
"endpoint_id" integer NOT NULL,
"source" varchar(20) DEFAULT 'scheduled' NOT NULL,
"ok" boolean NOT NULL,
"status_code" integer,
"latency_ms" integer,
"error_type" varchar(64),
"error_message" text,
"created_at" timestamp with time zone DEFAULT now()
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

created_at 列缺少 NOT NULL 约束。

provider_endpoint_probe_logs.created_at 列默认值为 now() 但未设置 NOT NULL。如果插入时显式传入 NULL,会导致数据不完整。建议添加 NOT NULL 约束以保持数据一致性。

🔧 建议的修复
-	"created_at" timestamp with time zone DEFAULT now()
+	"created_at" timestamp with time zone DEFAULT now() NOT NULL
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE TABLE IF NOT EXISTS "provider_endpoint_probe_logs" (
"id" serial PRIMARY KEY NOT NULL,
"endpoint_id" integer NOT NULL,
"source" varchar(20) DEFAULT 'scheduled' NOT NULL,
"ok" boolean NOT NULL,
"status_code" integer,
"latency_ms" integer,
"error_type" varchar(64),
"error_message" text,
"created_at" timestamp with time zone DEFAULT now()
);
CREATE TABLE IF NOT EXISTS "provider_endpoint_probe_logs" (
"id" serial PRIMARY KEY NOT NULL,
"endpoint_id" integer NOT NULL,
"source" varchar(20) DEFAULT 'scheduled' NOT NULL,
"ok" boolean NOT NULL,
"status_code" integer,
"latency_ms" integer,
"error_type" varchar(64),
"error_message" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
🤖 Prompt for AI Agents
In @drizzle/0055_sour_wallow.sql around lines 1 - 11, The created_at column in
provider_endpoint_probe_logs has DEFAULT now() but lacks NOT NULL; update the
table definition (or migration) to add NOT NULL on the "created_at" column while
preserving DEFAULT now() (i.e., change the column definition to "created_at
timestamp with time zone DEFAULT now() NOT NULL"); if applying to an existing
DB, include a pre-migration step to backfill NULL created_at rows (UPDATE
provider_endpoint_probe_logs SET created_at = now() WHERE created_at IS NULL)
before altering the column to set NOT NULL.

Comment on lines 1001 to 1022
"probeHistory": {
"title": "История проверок эндпоинтов",
"description": "Просмотр журналов проверок и запуск ручной проверки для конкретного эндпоинта",
"selectVendor": "Выберите вендора",
"selectType": "Выберите тип провайдера",
"selectEndpoint": "Выберите эндпоинт",
"noEndpoints": "Эндпоинты не найдены",
"probeNow": "Проверить",
"probing": "Проверяем...",
"columns": {
"time": "Время",
"method": "Метод",
"status": "Статус",
"latency": "Задержка",
"error": "Ошибка"
},
"success": "Успех",
"manual": "Ручная проверка",
"auto": "Автоматическая проверка",
"probeSuccess": "Проверка успешна",
"probeFailed": "Проверка не удалась"
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# 查找 probeHistory.columns.method 或相关使用
rg -n "probeHistory.*columns.*method|columns.*method|method.*column" --type=ts --type=tsx -i

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# 查找 endpoint-probe-history 组件文件
fd -e tsx -e ts | grep -i "probe.*history\|endpoint.*probe"

Repository: ding113/claude-code-hub

Length of output: 284


🏁 Script executed:

# 搜索 probeHistory 本身的使用情况
rg -n "probeHistory" --type=ts --type=tsx -A 3 -B 1

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# Simple search for probeHistory usage
rg "probeHistory" -A 5 -B 2

Repository: ding113/claude-code-hub

Length of output: 14090


🏁 Script executed:

# Read the component file to see what columns are actually used
cat -n src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx | head -200

Repository: ding113/claude-code-hub

Length of output: 7794


🏁 Script executed:

# Search for "method" in relation to columns in the component
rg "columns\." src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx -A 2 -B 2

Repository: ding113/claude-code-hub

Length of output: 464


🏁 Script executed:

# Check if columns.method is referenced anywhere in the codebase
rg "columns\.method"

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Check if there's any mention of "method" in relation to probe history in other files
rg "probeHistory.*method|method.*probe" -i

Repository: ding113/claude-code-hub

Length of output: 200


🏁 Script executed:

# Read the probe.ts file to understand the context
cat -n src/lib/provider-endpoints/probe.ts | head -50

Repository: ding113/claude-code-hub

Length of output: 1799


🏁 Script executed:

# Check the ProviderEndpointProbeLog type to see if method is a field
rg "ProviderEndpointProbeLog|EndpointProbeMethod" -A 5 -B 2

Repository: ding113/claude-code-hub

Length of output: 11551


删除未使用的本地化键 columns.method

columns.method 在所有语言文件中定义,但组件实际只渲染 4 个表列(time、status、latency、error),未使用 method 列。虽然探针结果内部包含 method 字段(HEAD/GET),但该信息不在 UI 中显示,可以从本地化文件中删除。

🤖 Prompt for AI Agents
In @messages/ru/dashboard.json around lines 1001 - 1022, Remove the unused
localization key columns.method under probeHistory (i.e.,
"probeHistory.columns.method") since the UI only renders time, status, latency
and error; update the Russian file by deleting that "method" entry and mirror
the same removal across other locale files so all language bundles stay
consistent.

Comment on lines +879 to +885
context.filteredProviders?.push({
id: p.id,
name: p.name,
reason: "circuit_open",
details: "供应商类型临时熔断",
});
continue;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

硬编码的中文字符串应使用 i18n。

details: "供应商类型临时熔断" 是硬编码的中文字符串。根据编码规范,用户可见的字符串需要使用 i18n。当 verboseProviderError 启用时,这些详情会通过错误响应暴露给用户(参见 lines 485-488)。

建议将此类字符串迁移到 i18n 资源文件中,或者在返回给用户之前进行翻译。

🤖 Prompt for AI Agents
In @src/app/v1/_lib/proxy/provider-selector.ts around lines 879 - 885, The
hard-coded Chinese string assigned to details in the context.filteredProviders
push (details: "供应商类型临时熔断") should be replaced with an i18n lookup so
user-visible messages honor localization and are safe when verboseProviderError
exposes them; update the push call in provider-selector (where
context.filteredProviders?.push({ id: p.id, name: p.name, reason:
"circuit_open", details: ... })) to call the i18n translation function or a
resource key (e.g., t('provider.circuit_open')) and ensure the same translation
is applied before sending errors to users (see verboseProviderError handling
around verboseProviderError / lines ~485-488) so no raw Chinese strings are
returned.

Copy link

@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 adds a comprehensive provider vendor endpoint system with probing, circuit breakers, and failover capabilities. The implementation is well-structured with proper separation of concerns across repository, service, and API layers. Error handling is consistent with logging throughout, and test coverage is adequate for the new functionality.

PR Size: XL

  • Lines changed: 7707 (7672 additions, 35 deletions)
  • Files changed: 43

Split Suggestions for Future PRs: Given the XL size, consider splitting similar features into:

  1. Data model + repository layer (migration, schema, repository functions)
  2. Circuit breaker implementations (endpoint + vendor-type)
  3. Proxy integration (forwarder changes, endpoint selector)
  4. Admin API + UI components

Issues Found

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

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean (admin-only endpoints properly protected)
  • Error handling - Clean (consistent logging, no silent failures)
  • Type safety - Clean (proper TypeScript types throughout)
  • Documentation accuracy - Clean
  • Test coverage - Adequate (unit tests for circuit breakers, endpoint selector, probe logic)
  • Code clarity - Good

Notable Observations (Not Issues)

Well-implemented patterns observed:

  • Circuit breaker state properly synced between in-memory cache and Redis
  • Graceful degradation when Redis is unavailable (returns null, logs warning)
  • Proper use of persistStateToRedis with fire-and-forget pattern and error logging
  • Admin session checks consistently applied across all endpoint actions
  • Endpoint failover logic correctly tracks timeout vs non-timeout errors for vendor-type circuit breaker

Architecture notes:

  • The dual-layer circuit breaker design (endpoint-level + vendor-type-level) provides good granularity
  • Probe fallback from HEAD to GET on network error is a sensible approach
  • The allEndpointAttemptsTimedOut tracking ensures vendor-type circuit only opens when all endpoints timeout

Automated review by Claude AI

Records the selected endpoint in provider chain and seeds/backfills provider_endpoints so endpoint routing works without manual setup.
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 `@tests/unit/repository/provider-endpoints.test.ts`:
- Around line 19-253: Tests only cover two functions and ten repository
functions (getOrCreateProviderVendorIdFromUrls, findProviderVendors,
findProviderVendorById, findProviderEndpointById, updateProviderVendor,
findProviderEndpointsByVendorAndType, createProviderEndpoint,
updateProviderEndpoint, softDeleteProviderEndpoint,
findProviderEndpointProbeLogs) plus direct call coverage for
recordProviderEndpointProbeResult are missing; add unit tests for each missing
function to reach 80% coverage. For each function listed, write focused tests
that import the target (e.g., getOrCreateProviderVendorIdFromUrls,
findProviderVendors, etc.) from "@/repository/provider-endpoints", mock
"@/drizzle/db" with vi.doMock to provide controlled
select/insert/update/delete/returning behaviors, assert expected return values
and DB interaction (calls/params), include edge cases (invalid inputs,
conflicts, empty results), and add a direct unit test for
recordProviderEndpointProbeResult in this file that mocks any external probe
logger and verifies it records outcomes; ensure tests use vi.resetModules before
vi.doMock and validate coverage meets the 80% threshold.
🧹 Nitpick comments (8)
src/instrumentation.ts (1)

183-194: 建议提取重复的回填逻辑为辅助函数

此回填逻辑与开发环境路径(Lines 242-254)几乎完全相同。为了遵循 DRY 原则并与现有代码风格保持一致(如 syncErrorRulesAndInitializeDetector),建议提取为独立的辅助函数。

♻️ 建议的重构

在文件顶部添加辅助函数:

/**
 * 回填 provider_endpoints(从 providers.url/类型 生成端点池)
 * 幂等操作,失败不阻塞启动
 */
async function backfillProviderEndpoints(): Promise<void> {
  try {
    const { backfillProviderEndpointsFromProviders } = await import(
      "@/repository/provider-endpoints"
    );
    const result = await backfillProviderEndpointsFromProviders();
    logger.info("[Instrumentation] Provider endpoints backfill completed", result);
  } catch (error) {
    logger.warn("[Instrumentation] Failed to backfill provider endpoints", {
      error: error instanceof Error ? error.message : String(error),
    });
  }
}

然后在两处调用点简化为:

-      // 回填 provider_endpoints(从 providers.url/类型 生成端点池,幂等)
-      try {
-        const { backfillProviderEndpointsFromProviders } = await import(
-          "@/repository/provider-endpoints"
-        );
-        const result = await backfillProviderEndpointsFromProviders();
-        logger.info("[Instrumentation] Provider endpoints backfill completed", result);
-      } catch (error) {
-        logger.warn("[Instrumentation] Failed to backfill provider endpoints", {
-          error: error instanceof Error ? error.message : String(error),
-        });
-      }
+      await backfillProviderEndpoints();
src/repository/provider.ts (1)

469-490: 条件判断逻辑需确认一致性

provider_type 变更但 url 未变更时:

  • Line 469 的条件仅检查 urlwebsite_url,不会重新计算 providerVendorId
  • Line 553 的条件包含 provider_type,会触发端点 seed

这种设计在当前逻辑下是可行的(vendor 由 URL 域名决定,与 type 无关),但建议添加注释说明意图,避免后续维护时产生困惑。

src/repository/provider-endpoints.ts (1)

530-567: 事务内时间戳处理可优化

Line 563 在事务内创建新的 new Date() 作为 updatedAt,可能与 Line 540 的 probedAt 存在微小时间差。建议统一使用 probedAt 以保持一致性:

建议的改动
       .set({
         lastProbedAt: probedAt,
         lastProbeOk: input.ok,
         lastProbeStatusCode: input.statusCode ?? null,
         lastProbeLatencyMs: input.latencyMs ?? null,
         lastProbeErrorType: input.ok ? null : (input.errorType ?? null),
         lastProbeErrorMessage: input.ok ? null : (input.errorMessage ?? null),
-        updatedAt: new Date(),
+        updatedAt: probedAt,
       })
tests/unit/repository/provider-endpoints.test.ts (1)

3-17: 辅助函数缺少 offset 方法

createThenableQuery 未实现 offset 方法,但 backfillProviderEndpointsFromProviders 的查询链不使用 offset(使用游标分页),所以当前测试不受影响。如果后续测试需要覆盖使用 offset 的查询,需要补充此方法。

建议补充 offset 方法
   type Query = Promise<T> & {
     from: (...args: unknown[]) => Query;
     where: (...args: unknown[]) => Query;
     orderBy: (...args: unknown[]) => Query;
     limit: (...args: unknown[]) => Query;
+    offset: (...args: unknown[]) => Query;
   };

   const query = Promise.resolve(result) as unknown as Query;
   query.from = () => query;
   query.where = () => query;
   query.orderBy = () => query;
   query.limit = () => query;
+  query.offset = () => query;
   return query;
tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts (1)

149-194: 建议补充缺失的 session 字段以提高测试健壮性

createSession 辅助函数缺少 ProxySession 类中的部分字段,包括 providersSnapshotbillingModelSourcePromisecachedBillingPriceData。虽然当前测试场景可能不会访问这些字段,但如果 ProxySession 类未来发生变化,这些测试可能会意外失败。

♻️ 建议补充缺失字段
     cachedPriceData: undefined,
     cachedBillingModelSource: undefined,
+    billingModelSourcePromise: undefined,
+    cachedBillingPriceData: undefined,
+    providersSnapshot: null,
     isHeaderModified: () => false,
   });
src/app/v1/_lib/proxy/forwarder.ts (3)

231-253: providerVendorId 检查可能存在边界条件问题

Line 231 使用 currentProvider.providerVendorId > 0 进行检查。如果 providerVendorIdundefinednull,JavaScript 的比较会返回 false,这在逻辑上是正确的。但从类型安全角度,建议增加更明确的空值检查以提高代码可读性。

♻️ 建议使用更明确的空值检查
-    } else if (currentProvider.providerVendorId > 0) {
+    } else if (currentProvider.providerVendorId != null && currentProvider.providerVendorId > 0) {

267-281: 建议在 vendor-type 熔断跳过时记录到决策链

当 vendor-type 熔断器处于打开状态时,供应商被跳过但未记录到决策链中。这可能导致在调试或审计时难以追踪为何某个供应商被跳过。

♻️ 建议记录熔断跳过事件
       logger.warn("ProxyForwarder: Vendor-type circuit is open, skipping provider", {
         providerId: currentProvider.id,
         vendorId: currentProvider.providerVendorId,
         providerType: currentProvider.providerType,
       });
       failedProviderIds.push(currentProvider.id);
       attemptCount = maxAttemptsPerProvider;
+      // 记录到决策链以便审计
+      session.addProviderToChain(currentProvider, {
+        endpointId: null,
+        endpointUrl: sanitizeUrl(currentProvider.url),
+        reason: "retry_failed",
+        errorMessage: "Vendor-type circuit breaker is open",
+        circuitState: "open",
+      });
     }

2221-2237: toUndiciBody 辅助函数未处理所有 BodyInit 类型

toUndiciBody 函数处理了 nullstringUint8ArrayBufferArrayBufferArrayBufferView 类型,但 BodyInit 还包括 BlobReadableStreamFormDataURLSearchParams。对于这些类型,函数返回 undefined,可能导致请求体丢失。

根据当前代码上下文,请求体通常是 JSON 字符串(JSON.stringify(filteredMessage)),所以这个问题在实际场景中可能不会触发。但为防止未来扩展时出现问题,建议记录或处理这些边界情况。

♻️ 建议增加未处理类型的日志
     if (ArrayBuffer.isView(body)) {
       return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
     }
+    // Blob, ReadableStream, FormData, URLSearchParams are not supported
+    // Log warning if these types are encountered
+    logger.warn("[ProxyForwarder] Unsupported body type for undici request", {
+      bodyType: body?.constructor?.name,
+    });
     return undefined;
   };
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 3106ec4 and 6081e35.

📒 Files selected for processing (9)
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/instrumentation.ts
  • src/repository/provider-endpoints.ts
  • src/repository/provider.ts
  • src/types/message.ts
  • tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts
  • tests/unit/proxy/session.test.ts
  • tests/unit/repository/provider-endpoints.test.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use emoji characters in any code, comments, or string literals

Files:

  • tests/unit/repository/provider-endpoints.test.ts
  • src/types/message.ts
  • src/repository/provider.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/repository/provider-endpoints.ts
  • tests/unit/proxy/session.test.ts
  • tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts
  • src/instrumentation.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

All new features must have unit test coverage of at least 80%

Files:

  • tests/unit/repository/provider-endpoints.test.ts
  • tests/unit/proxy/session.test.ts
  • tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias @/ to reference files in ./src/ directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width

Files:

  • tests/unit/repository/provider-endpoints.test.ts
  • src/types/message.ts
  • src/repository/provider.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/repository/provider-endpoints.ts
  • tests/unit/proxy/session.test.ts
  • tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts
  • src/instrumentation.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer named exports over default exports

Files:

  • tests/unit/repository/provider-endpoints.test.ts
  • src/types/message.ts
  • src/repository/provider.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/repository/provider-endpoints.ts
  • tests/unit/proxy/session.test.ts
  • tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts
  • src/instrumentation.ts
tests/**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Vitest for unit testing and happy-dom for DOM testing

Files:

  • tests/unit/repository/provider-endpoints.test.ts
  • tests/unit/proxy/session.test.ts
  • tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts
src/repository/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Drizzle ORM for data access in the repository layer

Files:

  • src/repository/provider.ts
  • src/repository/provider-endpoints.ts
src/app/v1/_lib/proxy/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

The proxy pipeline processes requests through a GuardPipeline with sequential guards: auth, sensitive, client, model, version, probe, session, warmup, requestFilter, rateLimit, provider, providerRequestFilter, messageContext

Files:

  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/session.ts
🧠 Learnings (4)
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: 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.

Applied to files:

  • src/types/message.ts
  • src/repository/provider.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/repository/provider-endpoints.ts
  • src/instrumentation.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/app/v1/_lib/proxy/**/*.{ts,tsx} : The proxy pipeline processes requests through a GuardPipeline with sequential guards: auth, sensitive, client, model, version, probe, session, warmup, requestFilter, rateLimit, provider, providerRequestFilter, messageContext

Applied to files:

  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/session.ts
  • tests/unit/proxy/session.test.ts
  • tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/repository/**/*.{ts,tsx} : Use Drizzle ORM for data access in the repository layer

Applied to files:

  • src/repository/provider-endpoints.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to tests/**/*.test.{ts,tsx,js,jsx} : Use Vitest for unit testing and happy-dom for DOM testing

Applied to files:

  • tests/unit/proxy/session.test.ts
🧬 Code graph analysis (5)
tests/unit/repository/provider-endpoints.test.ts (1)
src/repository/provider-endpoints.ts (2)
  • ensureProviderEndpointExistsForUrl (331-365)
  • backfillProviderEndpointsFromProviders (367-449)
src/app/v1/_lib/proxy/forwarder.ts (5)
src/lib/provider-endpoints/endpoint-selector.ts (1)
  • getPreferredProviderEndpoints (30-54)
src/lib/vendor-type-circuit-breaker.ts (2)
  • isVendorTypeCircuitOpen (115-137)
  • recordVendorTypeAllEndpointsTimeout (139-155)
src/app/v1/_lib/proxy/errors.ts (1)
  • sanitizeUrl (1010-1053)
src/lib/circuit-breaker.ts (1)
  • getCircuitState (395-398)
src/app/v1/_lib/url.ts (1)
  • buildProxyUrl (25-84)
src/repository/provider-endpoints.ts (2)
src/types/provider.ts (5)
  • ProviderVendor (375-383)
  • ProviderEndpoint (387-404)
  • ProviderEndpointProbeLog (406-416)
  • ProviderType (6-12)
  • ProviderEndpointProbeSource (385-385)
src/drizzle/schema.ts (3)
  • providerVendors (132-145)
  • providerEndpoints (297-339)
  • providerEndpointProbeLogs (342-363)
tests/unit/proxy/session.test.ts (1)
src/types/provider.ts (1)
  • Provider (39-144)
src/instrumentation.ts (1)
src/repository/provider-endpoints.ts (1)
  • backfillProviderEndpointsFromProviders (367-449)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Docker Build Test
  • GitHub Check: dev-build-deploy
🔇 Additional comments (17)
src/instrumentation.ts (1)

242-254: 实现逻辑正确,位置合理

开发环境中的回填逻辑放置在 isConnected 分支内是正确的,确保只在数据库可用时执行。错误处理遵循了文件中其他非关键初始化操作的模式(catch + warn)。

如上条评论所述,建议与生产环境路径共用提取后的辅助函数。

src/types/message.ts (1)

3-3: LGTM!

类型扩展设计合理:

  • endpointId?: number | null 正确区分了 null(明确未选择端点)与 undefined(字段未填充)的语义。
  • 新增字段与 PR 中 proxy/session 层的 endpoint audit 逻辑一致。

Also applies to: 14-20

src/repository/provider.ts (1)

135-147: LGTM!

端点自动 seed 的实现采用了合理的防御性编程模式:

  • try-catch 确保 seed 失败不会阻断主流程
  • 日志包含足够的上下文信息用于排查
src/repository/provider-endpoints.ts (4)

20-52: LGTM!

日期转换辅助函数和 URL 规范化逻辑实现正确:

  • toDate 使用 new Date() 作为后备值,适用于数据库行总是包含有效时间戳的场景
  • normalizeWebsiteDomainFromUrl 正确处理了带/不带协议的 URL,并统一移除 www. 前缀

104-151: LGTM!

getOrCreateProviderVendorIdFromUrls 的 upsert 实现采用了正确的竞态条件处理模式:

  1. 先查询现有记录
  2. 尝试插入(onConflictDoNothing
  3. 若插入未返回行(冲突),再次查询获取已存在的 ID

这种模式避免了 INSERT ON CONFLICT DO UPDATE 可能带来的不必要更新。


367-449: LGTM!

批量回填实现考虑周全:

  • 游标分页(gt(providers.id, lastProviderId))适合大数据集
  • Set 去重避免单次运行内的重复插入
  • 完整的验证逻辑(vendorId > 0、URL 非空、URL 格式合法)
  • onConflictDoNothing 确保幂等性

153-329: LGTM!

CRUD 函数实现符合 Drizzle ORM 最佳实践:

  • 软删除过滤 (isNull(deletedAt)) 一致应用
  • 空 payload 检查避免无意义的数据库写入
  • 分页查询使用 limit/offset 模式

Also applies to: 451-528, 569-593

tests/unit/proxy/session.test.ts (1)

680-780: LGTM!

endpoint audit 测试覆盖了关键场景:

  1. 字段写入验证(vendorId/providerType/endpointId/endpointUrl)
  2. 无 attemptNumber 时的去重逻辑
  3. 有 attemptNumber 时保留多条记录(支持重试审计)

测试设计与 ProviderChainItem 类型扩展一致。

tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts (1)

196-373: 测试覆盖全面,安全性验证到位

三个测试用例覆盖了端点审计的关键场景:成功请求、重试、以及端点选择失败回退。每个测试都验证了 URL 脱敏逻辑,确保敏感信息(如 api_keytoken)不会泄露到审计字段中。

src/app/v1/_lib/proxy/forwarder.ts (7)

287-302: 端点轮询选择和审计字段设计合理

使用取模运算实现端点轮询,确保多次重试时能依次尝试不同端点。sanitizeUrl 用于脱敏 URL,endpointAudit 对象正确传递到 doForward 方法。


386-388: 端点级熔断器记录逻辑正确

成功时仅在 endpointId 有效时记录;失败时仅对超时(524)或系统错误进行记录,避免将客户端问题误计入端点熔断器。这种设计能准确反映端点的健康状况。

Also applies to: 469-481


910-923: LGTM! vendor-type 熔断触发条件严谨

仅当所有端点都尝试过且全部超时时才触发 vendor-type 熔断,避免因单个端点问题导致整个供应商被错误熔断。!isMcpRequest 条件确保 MCP 请求不影响 vendor-type 熔断状态。


1046-1051: doForward 签名扩展支持端点级路由

新增 baseUrlendpointAuditattemptNumber 参数,使方法能够正确处理端点级路由和遥测数据。设计合理。


1120-1137: Gemini 路径的 baseUrl 处理逻辑正确

effectiveBaseUrl 的回退链(baseUrlprovider.url → 官方端点)设计合理,确保在各种配置场景下都能正确构建请求 URL 和 Headers。


1335-1378: MCP 透传路径正确使用端点 URL

MCP 透传逻辑现在从 effectiveBaseUrl(可能来自端点 URL)提取基础域名,而非始终使用 provider.url。这确保了当存在端点 URL 时能正确路由 MCP 请求。


438-447: 端点审计字段在决策链中一致传播

所有 session.addProviderToChain 调用现在都通过展开运算符 ...endpointAudit 传递端点审计数据,确保决策链中每个条目都包含完整的端点级遥测信息。

src/app/v1/_lib/proxy/session.ts (1)

471-472: LGTM! 端点审计字段扩展设计合理

新增的 endpointIdendpointUrlvendorIdproviderType 字段为决策链提供了完整的端点级遥测数据。endpointId 使用 number | null 类型能正确区分"未选择端点"和"已选择端点"的场景。

ProviderChainItem 类型定义(src/types/message.ts)已同步更新,新字段在 ProviderChainItem 构造中正确赋值,实现与类型定义保持一致。代码规范符合指南要求。

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +19 to +253
describe("provider-endpoints repository", () => {
test("ensureProviderEndpointExistsForUrl: url 为空时返回 false 且不写 DB", async () => {
vi.resetModules();

const insertMock = vi.fn();
vi.doMock("@/drizzle/db", () => ({
db: {
insert: insertMock,
},
}));

const { ensureProviderEndpointExistsForUrl } = await import("@/repository/provider-endpoints");
const ok = await ensureProviderEndpointExistsForUrl({
vendorId: 1,
providerType: "claude",
url: " ",
});

expect(ok).toBe(false);
expect(insertMock).not.toHaveBeenCalled();
});

test("ensureProviderEndpointExistsForUrl: url 非法时返回 false 且不写 DB", async () => {
vi.resetModules();

const insertMock = vi.fn();
vi.doMock("@/drizzle/db", () => ({
db: {
insert: insertMock,
},
}));

const { ensureProviderEndpointExistsForUrl } = await import("@/repository/provider-endpoints");
const ok = await ensureProviderEndpointExistsForUrl({
vendorId: 1,
providerType: "claude",
url: "not a url",
});

expect(ok).toBe(false);
expect(insertMock).not.toHaveBeenCalled();
});

test("ensureProviderEndpointExistsForUrl: 插入成功时返回 true(trim + label=null)", async () => {
vi.resetModules();

const state = { values: undefined as unknown };
const returning = vi.fn(async () => [{ id: 1 }]);
const onConflictDoNothing = vi.fn(() => ({ returning }));
const values = vi.fn((payload: unknown) => {
state.values = payload;
return { onConflictDoNothing };
});
const insertMock = vi.fn(() => ({ values }));

vi.doMock("@/drizzle/db", () => ({
db: {
insert: insertMock,
},
}));

const { ensureProviderEndpointExistsForUrl } = await import("@/repository/provider-endpoints");
const ok = await ensureProviderEndpointExistsForUrl({
vendorId: 1,
providerType: "claude",
url: " https://api.example.com ",
});

expect(ok).toBe(true);
expect(insertMock).toHaveBeenCalledTimes(1);
expect(values).toHaveBeenCalledTimes(1);

expect(state.values).toEqual(
expect.objectContaining({
vendorId: 1,
providerType: "claude",
url: "https://api.example.com",
label: null,
})
);
});

test("ensureProviderEndpointExistsForUrl: 冲突不插入时返回 false", async () => {
vi.resetModules();

const returning = vi.fn(async () => []);
const onConflictDoNothing = vi.fn(() => ({ returning }));
const values = vi.fn(() => ({ onConflictDoNothing }));
const insertMock = vi.fn(() => ({ values }));

vi.doMock("@/drizzle/db", () => ({
db: {
insert: insertMock,
},
}));

const { ensureProviderEndpointExistsForUrl } = await import("@/repository/provider-endpoints");
const ok = await ensureProviderEndpointExistsForUrl({
vendorId: 1,
providerType: "claude",
url: "https://api.example.com",
});

expect(ok).toBe(false);
});

test("backfillProviderEndpointsFromProviders: 全部无效时不写 DB", async () => {
vi.resetModules();

const selectPages = [
[
{ id: 1, vendorId: 0, providerType: "claude", url: "https://ok.example.com" },
{ id: 2, vendorId: 1, providerType: "claude", url: " " },
{ id: 3, vendorId: 1, providerType: "claude", url: "not a url" },
],
[],
];

const selectMock = vi.fn(() => createThenableQuery(selectPages.shift() ?? []));
const insertMock = vi.fn();

vi.doMock("@/drizzle/db", () => ({
db: {
select: selectMock,
insert: insertMock,
},
}));

const { backfillProviderEndpointsFromProviders } = await import(
"@/repository/provider-endpoints"
);
const result = await backfillProviderEndpointsFromProviders();

expect(result).toEqual({ inserted: 0, uniqueCandidates: 0, skippedInvalid: 3 });
expect(insertMock).not.toHaveBeenCalled();
});

test("backfillProviderEndpointsFromProviders: 去重 + trim + 统计 inserted/uniqueCandidates/skippedInvalid", async () => {
vi.resetModules();

const capturedValues: unknown[] = [];

const insertState = { values: undefined as unknown };
const returning = vi.fn(async () => {
const values = insertState.values;
if (!Array.isArray(values)) return [];
return values.map((_, idx) => ({ id: idx + 1 }));
});
const onConflictDoNothing = vi.fn(() => ({ returning }));
const values = vi.fn((payload: unknown) => {
insertState.values = payload;
if (Array.isArray(payload)) capturedValues.push(...payload);
return { onConflictDoNothing };
});
const insertMock = vi.fn(() => ({ values }));

const selectPages = [
[
{ id: 1, vendorId: 1, providerType: "claude", url: " https://a.com " },
{ id: 2, vendorId: 1, providerType: "claude", url: "https://a.com" },
{ id: 3, vendorId: 1, providerType: "openai-compatible", url: "https://a.com" },
],
[
{ id: 4, vendorId: 2, providerType: "claude", url: "https://a.com" },
{ id: 5, vendorId: 0, providerType: "claude", url: "https://bad-vendor.com" },
{ id: 6, vendorId: 1, providerType: "claude", url: "not a url" },
],
[],
];

const selectMock = vi.fn(() => createThenableQuery(selectPages.shift() ?? []));

vi.doMock("@/drizzle/db", () => ({
db: {
select: selectMock,
insert: insertMock,
},
}));

const { backfillProviderEndpointsFromProviders } = await import(
"@/repository/provider-endpoints"
);
const result = await backfillProviderEndpointsFromProviders();

expect(result).toEqual({ inserted: 3, uniqueCandidates: 3, skippedInvalid: 2 });

expect(capturedValues).toEqual(
expect.arrayContaining([
expect.objectContaining({ vendorId: 1, providerType: "claude", url: "https://a.com" }),
expect.objectContaining({
vendorId: 1,
providerType: "openai-compatible",
url: "https://a.com",
}),
expect.objectContaining({ vendorId: 2, providerType: "claude", url: "https://a.com" }),
])
);
});

test("backfillProviderEndpointsFromProviders: 冲突不插入时 inserted=0 但 uniqueCandidates 仍统计", async () => {
vi.resetModules();

const insertState = { values: undefined as unknown };
const returning = vi.fn(async () => []);
const onConflictDoNothing = vi.fn(() => ({ returning }));
const values = vi.fn((payload: unknown) => {
insertState.values = payload;
return { onConflictDoNothing };
});
const insertMock = vi.fn(() => ({ values }));

const selectPages = [
[
{ id: 1, vendorId: 1, providerType: "claude", url: "https://a.com" },
{ id: 2, vendorId: 1, providerType: "openai-compatible", url: "https://a.com" },
],
[],
];

const selectMock = vi.fn(() => createThenableQuery(selectPages.shift() ?? []));

vi.doMock("@/drizzle/db", () => ({
db: {
select: selectMock,
insert: insertMock,
},
}));

const { backfillProviderEndpointsFromProviders } = await import(
"@/repository/provider-endpoints"
);
const result = await backfillProviderEndpointsFromProviders();

expect(result).toEqual({ inserted: 0, uniqueCandidates: 2, skippedInvalid: 0 });
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for the missing functions in test files to verify coverage
rg -n --type=ts -C2 'getOrCreateProviderVendorIdFromUrls|createProviderEndpoint|updateProviderEndpoint|softDeleteProviderEndpoint|recordProviderEndpointProbeResult|findProviderEndpointProbeLogs' tests/

Repository: ding113/claude-code-hub

Length of output: 2882


🏁 Script executed:

#!/bin/bash
# First, find the provider-endpoints repository file
fd -e ts -e js "provider-endpoints" src/repository/

# Then check what functions are exported
echo "=== Checking exports from provider-endpoints ==="
rg -n "export" src/repository/provider-endpoints.ts

# Search for all occurrences of the missing functions across the entire repo
echo "=== Searching for other functions across codebase ==="
rg -n 'getOrCreateProviderVendorIdFromUrls|createProviderEndpoint|updateProviderEndpoint|softDeleteProviderEndpoint|findProviderEndpointProbeLogs' . --type=ts --type=tsx

Repository: ding113/claude-code-hub

Length of output: 1111


🏁 Script executed:

#!/bin/bash
# Search more carefully for actual usage (not mocking) of these functions in tests
rg -n 'getOrCreateProviderVendorIdFromUrls|createProviderEndpoint|updateProviderEndpoint|softDeleteProviderEndpoint|findProviderEndpointProbeLogs' tests/ --type=ts -g '!*.test.ts' -A 2

# Also check if they're directly imported and tested
echo "=== Direct imports in test files ==="
rg -n 'import.*\{' tests/ --type=ts -A 5 | grep -E '(getOrCreateProviderVendorIdFromUrls|createProviderEndpoint|updateProviderEndpoint|softDeleteProviderEndpoint|findProviderEndpointProbeLogs)'

# Check the complete test file to see what's imported
echo "=== provider-endpoints.test.ts imports ==="
head -50 tests/unit/repository/provider-endpoints.test.ts

Repository: ding113/claude-code-hub

Length of output: 1487


🏁 Script executed:

#!/bin/bash
# Check the actual test file to see the complete list of tests
wc -l tests/unit/repository/provider-endpoints.test.ts

# Count how many functions are exported vs tested
echo "=== Total exported functions from provider-endpoints ==="
rg -c "^export " src/repository/provider-endpoints.ts

# List all exported functions
echo "=== All exported functions ==="
rg "^export (async )?function" src/repository/provider-endpoints.ts | sed 's/.*function //' | sed 's/(.*$//'

# Check if there are any integration or e2e tests
echo "=== Looking for other test files that might cover provider-endpoints ==="
fd -e test.ts tests/ | xargs grep -l "provider-endpoints\|provider.*endpoint" 2>/dev/null | head -20

Repository: ding113/claude-code-hub

Length of output: 1003


🏁 Script executed:

#!/bin/bash
# Fix the fd search command
echo "=== Looking for other test files that mention provider-endpoints ==="
fd . tests/ -name "*.test.ts" -o -name "*.test.js" | xargs grep -l "provider-endpoints\|from.*repository" 2>/dev/null

# Check if any integration or API tests exist
echo "=== All test files in tests/unit ==="
find tests/unit -name "*.test.ts" -o -name "*.test.js" | sort

# Check what functions are actually used/tested in the probe.test.ts
echo "=== Functions tested in probe.test.ts ==="
rg -n "await|test\(" tests/unit/lib/provider-endpoints/probe.test.ts | head -30

Repository: ding113/claude-code-hub

Length of output: 6945


🏁 Script executed:

#!/bin/bash
# Check the provider.test.ts to see if it covers provider-endpoints functions
echo "=== Checking provider.test.ts ==="
grep -n "createProviderEndpoint\|updateProviderEndpoint\|softDeleteProviderEndpoint\|getOrCreateProviderVendorIdFromUrls\|findProviderVendors\|findProviderVendorById\|findProviderEndpointProbeLogs" tests/unit/repository/provider.test.ts

# Check endpoint-selector.test.ts
echo "=== Checking endpoint-selector.test.ts ==="
grep -n "import.*from.*provider-endpoints" tests/unit/lib/provider-endpoints/endpoint-selector.test.ts

# Get a quick count of what is actually tested in the provider-endpoints.test.ts file
echo "=== Test cases in provider-endpoints.test.ts ==="
grep -n "test(" tests/unit/repository/provider-endpoints.test.ts | wc -l

echo "=== All test case names ==="
grep "test(" tests/unit/repository/provider-endpoints.test.ts | sed 's/.*test("//' | sed 's/".*//' | sort

Repository: ding113/claude-code-hub

Length of output: 687


需要补充单元测试以达到 80% 覆盖率要求

当前测试仅覆盖了 ensureProviderEndpointExistsForUrlbackfillProviderEndpointsFromProviders 两个函数。根据编码指南,新功能单元测试覆盖率需要达到 80%,但以下 10 个函数缺少直接的单元测试覆盖:

  • getOrCreateProviderVendorIdFromUrls
  • findProviderVendors
  • findProviderVendorById
  • findProviderEndpointById
  • updateProviderVendor
  • findProviderEndpointsByVendorAndType
  • createProviderEndpoint
  • updateProviderEndpoint
  • softDeleteProviderEndpoint
  • findProviderEndpointProbeLogs

recordProviderEndpointProbeResult 仅在 probe.test.ts 中被 mock,缺少在 provider-endpoints.test.ts 中的直接单元测试。

🤖 Prompt for AI Agents
In `@tests/unit/repository/provider-endpoints.test.ts` around lines 19 - 253,
Tests only cover two functions and ten repository functions
(getOrCreateProviderVendorIdFromUrls, findProviderVendors,
findProviderVendorById, findProviderEndpointById, updateProviderVendor,
findProviderEndpointsByVendorAndType, createProviderEndpoint,
updateProviderEndpoint, softDeleteProviderEndpoint,
findProviderEndpointProbeLogs) plus direct call coverage for
recordProviderEndpointProbeResult are missing; add unit tests for each missing
function to reach 80% coverage. For each function listed, write focused tests
that import the target (e.g., getOrCreateProviderVendorIdFromUrls,
findProviderVendors, etc.) from "@/repository/provider-endpoints", mock
"@/drizzle/db" with vi.doMock to provide controlled
select/insert/update/delete/returning behaviors, assert expected return values
and DB interaction (calls/params), include edge cases (invalid inputs,
conflicts, empty results), and add a direct unit test for
recordProviderEndpointProbeResult in this file that mocks any external probe
logger and verifies it records outcomes; ensure tests use vi.resetModules before
vi.doMock and validate coverage meets the 80% threshold.

Replaces 0054/0055 with a single Drizzle-generated migration and adds idempotent guards/backfill so provider_vendors schema drift won't break startup migrations.
@github-actions
Copy link

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

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

🤖 Fix all issues with AI agents
In `@drizzle/0054_motionless_ulik.sql`:
- Around line 32-40: The timestamps in the provider_vendors table lack NOT NULL
constraints; update the CREATE TABLE statement for provider_vendors to add NOT
NULL to the created_at and updated_at columns (i.e., change "created_at"
timestamp with time zone DEFAULT now() and "updated_at" timestamp with time zone
DEFAULT now() to include NOT NULL) so they match other tables' behavior.
- Line 139: The unique index uniq_provider_endpoints_vendor_type_url on table
provider_endpoints currently does not exclude soft-deleted rows; drop and
recreate that index so the uniqueness applies only to active rows by adding a
WHERE deleted_at IS NULL predicate (i.e., remove the existing
uniq_provider_endpoints_vendor_type_url and create a unique index on
("vendor_id","provider_type","url") WITH the WHERE deleted_at IS NULL condition
so soft-deleted endpoints no longer block recreating identical records).
♻️ Duplicate comments (1)
drizzle/0054_motionless_ulik.sql (1)

1-11: created_at 列缺少 NOT NULL 约束。

provider_endpoint_probe_logs.created_at 列有 DEFAULT now() 但未设置 NOT NULL。如果插入时显式传入 NULL,会导致数据不完整。

🔧 建议的修复
-	"created_at" timestamp with time zone DEFAULT now()
+	"created_at" timestamp with time zone DEFAULT now() NOT NULL
🧹 Nitpick comments (1)
drizzle/0054_motionless_ulik.sql (1)

66-117: 域名提取逻辑重复。

INSERT 语句(Lines 66-88)和 UPDATE 语句(Lines 90-117)中的域名提取逻辑完全相同。虽然在 SQL 迁移中这是可以接受的(迁移文件通常只运行一次),但如果需要修改逻辑,需要同时维护两处。

可以考虑使用 CTE 或临时表来避免重复:

-- 示例:使用 CTE 提取域名一次
WITH domain_mapping AS (
  SELECT
    id AS provider_id,
    CASE
      WHEN domain_candidate IS NULL OR domain_candidate = '' THEN 'unknown-' || id
      ELSE domain_candidate
    END AS website_domain
  FROM (
    SELECT
      id,
      lower(split_part(split_part(regexp_replace(COALESCE(NULLIF(website_url, ''), url), '^[a-zA-Z]+://', ''), '/', 1), ':', 1)) AS domain_candidate
    FROM providers
  ) n
)
-- 然后在 INSERT 和 UPDATE 中引用 domain_mapping
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 6081e35 and aa53918.

📒 Files selected for processing (4)
  • drizzle/0054_motionless_ulik.sql
  • drizzle/0054_tidy_winter_soldier.sql
  • drizzle/meta/0054_snapshot.json
  • drizzle/meta/_journal.json
💤 Files with no reviewable changes (1)
  • drizzle/0054_tidy_winter_soldier.sql
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 🌐 API Tests
  • GitHub Check: Docker Build Test
  • GitHub Check: dev-build-deploy
🔇 Additional comments (8)
drizzle/0054_motionless_ulik.sql (3)

42-60: 幂等性设计良好。

使用 ADD COLUMN IF NOT EXISTSCREATE ... IF NOT EXISTS 确保迁移可以安全地重复执行,配合 backfill 逻辑处理现有数据,设计合理。


121-156: 外键约束的幂等性处理得当。

使用 DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL; END $$ 处理外键约束已存在的情况,确保迁移可以安全重跑。


151-158: providersprovider_vendors 的外键使用 ON DELETE restrict 是正确的选择。

防止在有关联 provider 时删除 vendor,保护数据完整性。同时 idx_providers_vendor_type 索引的 WHERE deleted_at IS NULL 条件有助于优化查询。

drizzle/meta/0054_snapshot.json (4)

1153-1272: 新表 provider_endpoint_probe_logs 的 schema 定义正确。

包含端点探测日志所需的字段:endpoint_id、source、ok、status_code、latency_ms、error_type、error_message。索引设计合理,支持按 endpoint_id + created_at 的高效查询。

注意:created_atnotNull: false 与 SQL 迁移文件中的问题一致,建议在 drizzle/schema.ts 中修复后重新生成。


1273-1508: 新表 provider_endpoints 的 schema 设计合理。

包含端点管理所需的完整字段,支持软删除(deleted_at)、探测状态快照(last_probe_* 字段)和排序(sort_order)。外键 onDelete: cascade 确保删除 vendor 时级联删除关联端点。


1509-1596: 新表 provider_vendors 的 schema 定义正确。

website_domain 的唯一索引确保供应商域名不重复。结构简洁,支持后续扩展(display_name、favicon_url 等)。


1631-1636: providers 表的修改正确反映了新的 vendor 关联关系。

  • provider_vendor_id 设置为 notNull: true,确保每个 provider 必须关联一个 vendor
  • 新增的 idx_providers_vendor_type 索引配合 WHERE deleted_at IS NULL 条件,优化按 vendor+type 查询
  • 外键 onDelete: restrict 防止误删有关联的 vendor

Also applies to: 2016-2052

drizzle/meta/_journal.json (1)

383-389: 迁移日志更新正确。

Journal 条目与新的迁移文件 0054_motionless_ulik.sql 保持一致,时间戳已更新。

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +13 to +30
CREATE TABLE IF NOT EXISTS "provider_endpoints" (
"id" serial PRIMARY KEY NOT NULL,
"vendor_id" integer NOT NULL,
"provider_type" varchar(20) DEFAULT 'claude' NOT NULL,
"url" text NOT NULL,
"label" varchar(200),
"sort_order" integer DEFAULT 0 NOT NULL,
"is_enabled" boolean DEFAULT true NOT NULL,
"last_probed_at" timestamp with time zone,
"last_probe_ok" boolean,
"last_probe_status_code" integer,
"last_probe_latency_ms" integer,
"last_probe_error_type" varchar(64),
"last_probe_error_message" text,
"created_at" timestamp with time zone DEFAULT now(),
"updated_at" timestamp with time zone DEFAULT now(),
"deleted_at" timestamp with time zone
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

created_atupdated_at 列缺少 NOT NULL 约束。

provider_endpoint_probe_logs 表相同的问题,provider_endpoints 表的 created_atupdated_at 列虽然有默认值,但允许 NULL 值插入。

🔧 建议的修复
-	"created_at" timestamp with time zone DEFAULT now(),
-	"updated_at" timestamp with time zone DEFAULT now(),
+	"created_at" timestamp with time zone DEFAULT now() NOT NULL,
+	"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE TABLE IF NOT EXISTS "provider_endpoints" (
"id" serial PRIMARY KEY NOT NULL,
"vendor_id" integer NOT NULL,
"provider_type" varchar(20) DEFAULT 'claude' NOT NULL,
"url" text NOT NULL,
"label" varchar(200),
"sort_order" integer DEFAULT 0 NOT NULL,
"is_enabled" boolean DEFAULT true NOT NULL,
"last_probed_at" timestamp with time zone,
"last_probe_ok" boolean,
"last_probe_status_code" integer,
"last_probe_latency_ms" integer,
"last_probe_error_type" varchar(64),
"last_probe_error_message" text,
"created_at" timestamp with time zone DEFAULT now(),
"updated_at" timestamp with time zone DEFAULT now(),
"deleted_at" timestamp with time zone
);
CREATE TABLE IF NOT EXISTS "provider_endpoints" (
"id" serial PRIMARY KEY NOT NULL,
"vendor_id" integer NOT NULL,
"provider_type" varchar(20) DEFAULT 'claude' NOT NULL,
"url" text NOT NULL,
"label" varchar(200),
"sort_order" integer DEFAULT 0 NOT NULL,
"is_enabled" boolean DEFAULT true NOT NULL,
"last_probed_at" timestamp with time zone,
"last_probe_ok" boolean,
"last_probe_status_code" integer,
"last_probe_latency_ms" integer,
"last_probe_error_type" varchar(64),
"last_probe_error_message" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"deleted_at" timestamp with time zone
);

Comment on lines +32 to +40
CREATE TABLE IF NOT EXISTS "provider_vendors" (
"id" serial PRIMARY KEY NOT NULL,
"website_domain" varchar(255) NOT NULL,
"display_name" varchar(200),
"website_url" text,
"favicon_url" text,
"created_at" timestamp with time zone DEFAULT now(),
"updated_at" timestamp with time zone DEFAULT now()
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

provider_vendors 表的时间戳列同样缺少 NOT NULL 约束。

建议与其他表保持一致,为 created_atupdated_at 添加 NOT NULL 约束。

🔧 建议的修复
-	"created_at" timestamp with time zone DEFAULT now(),
-	"updated_at" timestamp with time zone DEFAULT now()
+	"created_at" timestamp with time zone DEFAULT now() NOT NULL,
+	"updated_at" timestamp with time zone DEFAULT now() NOT NULL
🤖 Prompt for AI Agents
In `@drizzle/0054_motionless_ulik.sql` around lines 32 - 40, The timestamps in the
provider_vendors table lack NOT NULL constraints; update the CREATE TABLE
statement for provider_vendors to add NOT NULL to the created_at and updated_at
columns (i.e., change "created_at" timestamp with time zone DEFAULT now() and
"updated_at" timestamp with time zone DEFAULT now() to include NOT NULL) so they
match other tables' behavior.

--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "idx_provider_endpoint_probe_logs_created_at" ON "provider_endpoint_probe_logs" USING btree ("created_at");
--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "uniq_provider_endpoints_vendor_type_url" ON "provider_endpoints" USING btree ("vendor_id","provider_type","url");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

唯一索引未排除软删除记录。

uniq_provider_endpoints_vendor_type_url 索引没有 WHERE deleted_at IS NULL 条件,这意味着软删除的端点仍然占用唯一约束。如果需要重新创建一个之前软删除的端点(相同 vendor_id/provider_type/url),将会违反唯一约束。

🔧 建议的修复
-CREATE UNIQUE INDEX IF NOT EXISTS "uniq_provider_endpoints_vendor_type_url" ON "provider_endpoints" USING btree ("vendor_id","provider_type","url");
+CREATE UNIQUE INDEX IF NOT EXISTS "uniq_provider_endpoints_vendor_type_url" ON "provider_endpoints" USING btree ("vendor_id","provider_type","url") WHERE "deleted_at" IS NULL;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE UNIQUE INDEX IF NOT EXISTS "uniq_provider_endpoints_vendor_type_url" ON "provider_endpoints" USING btree ("vendor_id","provider_type","url");
CREATE UNIQUE INDEX IF NOT EXISTS "uniq_provider_endpoints_vendor_type_url" ON "provider_endpoints" USING btree ("vendor_id","provider_type","url") WHERE "deleted_at" IS NULL;
🤖 Prompt for AI Agents
In `@drizzle/0054_motionless_ulik.sql` at line 139, The unique index
uniq_provider_endpoints_vendor_type_url on table provider_endpoints currently
does not exclude soft-deleted rows; drop and recreate that index so the
uniqueness applies only to active rows by adding a WHERE deleted_at IS NULL
predicate (i.e., remove the existing uniq_provider_endpoints_vendor_type_url and
create a unique index on ("vendor_id","provider_type","url") WITH the WHERE
deleted_at IS NULL condition so soft-deleted endpoints no longer block
recreating identical records).

Regenerates the 0054 migration and adds conditional backfill/insert logic so migrations succeed when provider_vendors has a NOT NULL vendor_key column.
@github-actions
Copy link

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

@github-actions
Copy link

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

@github-actions
Copy link

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

@github-actions
Copy link

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

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/provider-vendor-view.tsx:
- Around line 494-496: formatDistanceToNow is called without a locale, so
user-visible relative times always render in English; update the call in
provider-vendor-view.tsx (where formatDistanceToNow(new
Date(endpoint.lastProbedAt), ... ) is used) to pass a date-fns locale object
mapped from the app locale. Retrieve the current app locale (e.g., via the
existing useLocale/useRouter or a currentLocale prop), map it to the
corresponding date-fns locale (using your app's date-fns locale map or add one),
then call formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix:
true, locale: mappedDateFnsLocale }). Ensure you import the mapped locale
objects or mapping util and use the same locale source used elsewhere for i18n.
♻️ Duplicate comments (5)
tests/unit/repository/provider-endpoints.test.ts (1)

19-253: 测试覆盖率仍未达到 80% 要求。

当前测试仅覆盖 4 个函数(ensureProviderEndpointExistsForUrlbackfillProviderEndpointsFromProviderstryDeleteProviderVendorIfEmptydeleteProviderVendor),但 provider-endpoints.ts 导出了 10+ 个函数仍缺少测试覆盖:

  • getOrCreateProviderVendorIdFromUrls
  • findProviderVendors / findProviderVendorById / findProviderEndpointById
  • findProviderEndpointsByVendorAndType
  • createProviderEndpoint / updateProviderEndpoint / softDeleteProviderEndpoint
  • updateProviderVendor
  • findProviderEndpointProbeLogs
  • recordProviderEndpointProbeResult

根据编码指南,新功能单元测试覆盖率需要达到 80%。

src/actions/provider-endpoints.ts (2)

695-700: 同样存在英文错误消息不一致问题。

Line 698 使用英文 "Vendor not found or could not be deleted"

建议的修改
     if (!ok) {
       return {
         ok: false,
-        error: "Vendor not found or could not be deleted",
+        error: "供应商不存在或无法删除",
         errorCode: ERROR_CODES.DELETE_FAILED,
       };
     }

658-664: 错误消息语言不一致。

Line 661 使用英文 "Vendor not found",而其他错误消息使用中文(如 Line 260 "端点不存在")。建议保持一致,使用中文或都改为英文(需要统一重构)。

建议的修改(保持与其他消息一致)
     if (!vendor) {
       return {
         ok: false,
-        error: "Vendor not found",
+        error: "供应商不存在",
         errorCode: ERROR_CODES.NOT_FOUND,
       };
     }
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (2)

264-286: 缺少供应商类型且按钮文本未使用 i18n

providerTypes 数组缺少 "claude-auth""gemini-cli" 类型,且第 284 行直接渲染 {type} 字符串而未使用翻译函数,违反了 i18n 规范。


528-534: 建议使用 AlertDialog 替代 window.confirm()

当前使用原生 confirm() 对话框,与应用其他部分的 UI 风格不一致。建议使用 AlertDialog 组件以保持一致性,如同 DeleteVendorDialog 的实现方式。

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

679-684: 建议:编辑和克隆操作也应失效 vendor 缓存。

根据 PR 描述,provider_vendor_id 是根据 URL 域名自动关联的。当编辑 provider(尤其是 URL 变更)或克隆创建新 provider 时,vendor 关联可能会变化。建议在 edit 和 clone dialog 的 onSuccess 中也加上 provider-vendors 缓存失效,保持一致性。

建议的修改
 onSuccess={() => {
   setOpenEdit(false);
   queryClient.invalidateQueries({ queryKey: ["providers"] });
   queryClient.invalidateQueries({ queryKey: ["providers-health"] });
+  queryClient.invalidateQueries({ queryKey: ["provider-vendors"] });
   router.refresh();
 }}

克隆 dialog(约第 700 行)也需要类似修改。

messages/ru/settings/providers/form/urlPreview.json (1)

9-19: 建议:确认俄语端点标签是否需要翻译。

俄语本地化文件中 endpoints 的所有标签均保留英文,而 zh-CN 和 zh-TW 版本则对部分标签进行了翻译(如 "Claude 消息"、"Gemini 生成内容")。

如果这是有意为之(保持 API 术语一致性),则无需更改。否则,建议参考中文版本的翻译策略,对用户友好的标签进行俄语本地化。

src/repository/provider.ts (1)

579-581: 供应商清理调用可考虑添加防御性 try-catch

虽然 tryDeleteProviderVendorIfEmpty 内部已有 try-catch,但作为防御性编程,建议在调用处也添加错误处理,避免意外抛出影响主流程。

🔧 可选的防御性改进
  if (previousVendorId && transformed.providerVendorId !== previousVendorId) {
-   await tryDeleteProviderVendorIfEmpty(previousVendorId);
+   try {
+     await tryDeleteProviderVendorIfEmpty(previousVendorId);
+   } catch {
+     // Cleanup failure should not affect provider update result
+   }
  }
src/actions/providers.ts (1)

740-761: removeProvider 删除前获取 provider 存在潜在问题。

当前实现在 deleteProvider 之前获取 provider 信息,但如果 findProviderById 返回 null(provider 不存在),deleteProvider 仍会执行。这可能导致:

  1. 对不存在的 provider 执行删除操作(虽然不会报错,但语义上不一致)
  2. 自动清理逻辑因 providernull 而被跳过,但用户可能预期删除操作失败

建议:在 findProviderById 返回 null 时提前返回错误。

建议的修改
 export async function removeProvider(providerId: number): Promise<ActionResult> {
   try {
     const session = await getSession();
     if (!session || session.user.role !== "admin") {
       return { ok: false, error: "无权限执行此操作" };
     }

     const provider = await findProviderById(providerId);
+    if (!provider) {
+      return { ok: false, error: "供应商不存在" };
+    }
+
     await deleteProvider(providerId);

     // 清除内存缓存(无论 Redis 是否成功都要执行)
     clearConfigCache(providerId);
     await clearProviderState(providerId);
     // ... rest of the function
src/actions/provider-endpoints.ts (1)

293-312: removeProviderEndpoint 中存在重复的 NOT_FOUND 检查。

Line 293-300 先检查 endpoint 是否存在,Line 302-309 在 softDeleteProviderEndpoint 返回 false 时再次返回 NOT_FOUND。由于第一次检查已确保 endpoint 存在,第二次检查理论上不会触发(除非存在并发删除)。

代码逻辑是正确的(防御性编程),但可以考虑简化错误消息或添加注释说明。

tests/unit/actions/provider-endpoints.test.ts (1)

67-202: 考虑补充边缘情况的测试覆盖

当前测试覆盖了主要的成功路径和部分错误处理。建议补充以下场景的测试以提高覆盖率:

  1. editProviderVendor: 验证 updateProviderVendor 返回 null 时的 NOT_FOUND 响应
  2. removeProviderVendor: 验证 deleteProviderVendor 返回 false 时的 DELETE_FAILED 响应
  3. removeProviderEndpoint: 验证 findProviderEndpointById 返回 null 时的 NOT_FOUND 响应

根据编码规范,新功能需要至少 80% 的单元测试覆盖率。

src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (1)

88-89: 考虑为 healthStatusstatistics 定义具体类型

当前使用 Record<number, any> 类型,建议定义更具体的接口类型以提高类型安全性和代码可读性。

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 5859a4f and 7bbcf12.

📒 Files selected for processing (27)
  • drizzle/0055_familiar_shatterstar.sql
  • drizzle/meta/0055_snapshot.json
  • drizzle/meta/_journal.json
  • messages/en/settings/providers/form/urlPreview.json
  • messages/en/settings/providers/strings.json
  • messages/ja/settings/providers/form/urlPreview.json
  • messages/ja/settings/providers/strings.json
  • messages/ru/settings/providers/form/urlPreview.json
  • messages/ru/settings/providers/strings.json
  • messages/zh-CN/settings/providers/form/urlPreview.json
  • messages/zh-CN/settings/providers/strings.json
  • messages/zh-TW/settings/providers/form/urlPreview.json
  • messages/zh-TW/settings/providers/strings.json
  • src/actions/provider-endpoints.ts
  • src/actions/providers.ts
  • src/app/[locale]/settings/providers/_components/forms/url-preview.tsx
  • src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  • src/app/v1/_lib/url.ts
  • src/drizzle/schema.ts
  • src/repository/index.ts
  • src/repository/provider-endpoints.ts
  • src/repository/provider.ts
  • src/types/provider.ts
  • tests/unit/actions/provider-endpoints.test.ts
  • tests/unit/app/v1/url.test.ts
  • tests/unit/repository/provider-endpoints.test.ts
✅ Files skipped from review due to trivial changes (1)
  • drizzle/meta/0055_snapshot.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • drizzle/meta/_journal.json
  • src/types/provider.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use emoji characters in any code, comments, or string literals

Files:

  • src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx
  • tests/unit/app/v1/url.test.ts
  • src/app/[locale]/settings/providers/_components/forms/url-preview.tsx
  • tests/unit/actions/provider-endpoints.test.ts
  • src/repository/provider.ts
  • tests/unit/repository/provider-endpoints.test.ts
  • src/repository/index.ts
  • src/repository/provider-endpoints.ts
  • src/drizzle/schema.ts
  • src/actions/providers.ts
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  • src/app/v1/_lib/url.ts
  • src/actions/provider-endpoints.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias @/ to reference files in ./src/ directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width

Files:

  • src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx
  • tests/unit/app/v1/url.test.ts
  • src/app/[locale]/settings/providers/_components/forms/url-preview.tsx
  • tests/unit/actions/provider-endpoints.test.ts
  • src/repository/provider.ts
  • tests/unit/repository/provider-endpoints.test.ts
  • src/repository/index.ts
  • src/repository/provider-endpoints.ts
  • src/drizzle/schema.ts
  • src/actions/providers.ts
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  • src/app/v1/_lib/url.ts
  • src/actions/provider-endpoints.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer named exports over default exports

Files:

  • src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx
  • tests/unit/app/v1/url.test.ts
  • src/app/[locale]/settings/providers/_components/forms/url-preview.tsx
  • tests/unit/actions/provider-endpoints.test.ts
  • src/repository/provider.ts
  • tests/unit/repository/provider-endpoints.test.ts
  • src/repository/index.ts
  • src/repository/provider-endpoints.ts
  • src/drizzle/schema.ts
  • src/actions/providers.ts
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  • src/app/v1/_lib/url.ts
  • src/actions/provider-endpoints.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

All new features must have unit test coverage of at least 80%

Files:

  • tests/unit/app/v1/url.test.ts
  • tests/unit/actions/provider-endpoints.test.ts
  • tests/unit/repository/provider-endpoints.test.ts
tests/**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Vitest for unit testing and happy-dom for DOM testing

Files:

  • tests/unit/app/v1/url.test.ts
  • tests/unit/actions/provider-endpoints.test.ts
  • tests/unit/repository/provider-endpoints.test.ts
src/repository/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Drizzle ORM for data access in the repository layer

Files:

  • src/repository/provider.ts
  • src/repository/index.ts
  • src/repository/provider-endpoints.ts
src/drizzle/schema.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Modify database schema in src/drizzle/schema.ts, then run bun run db:generate to generate migrations. Never create SQL migration files manually

Files:

  • src/drizzle/schema.ts
🧠 Learnings (10)
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: 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.

Applied to files:

  • src/repository/provider.ts
  • src/repository/index.ts
  • src/repository/provider-endpoints.ts
  • src/drizzle/schema.ts
  • src/actions/providers.ts
  • src/app/v1/_lib/url.ts
  • src/actions/provider-endpoints.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : All new features must have unit test coverage of at least 80%

Applied to files:

  • tests/unit/repository/provider-endpoints.test.ts
📚 Learning: 2026-01-05T03:02:14.502Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx:66-66
Timestamp: 2026-01-05T03:02:14.502Z
Learning: In the claude-code-hub project, the translations.actions.addKey field in UserKeyTableRowProps is defined as optional for backward compatibility, but all actual callers in the codebase provide the complete translations object. The field has been added to all 5 locale files (messages/{locale}/dashboard.json).

Applied to files:

  • messages/zh-CN/settings/providers/form/urlPreview.json
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/repository/**/*.{ts,tsx} : Use Drizzle ORM for data access in the repository layer

Applied to files:

  • src/repository/provider-endpoints.ts
  • src/drizzle/schema.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/drizzle/schema.ts : Modify database schema in `src/drizzle/schema.ts`, then run `bun run db:generate` to generate migrations. Never create SQL migration files manually

Applied to files:

  • src/drizzle/schema.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/app/v1/_lib/proxy/**/*.{ts,tsx} : The proxy pipeline processes requests through a GuardPipeline with sequential guards: auth, sensitive, client, model, version, probe, session, warmup, requestFilter, rateLimit, provider, providerRequestFilter, messageContext

Applied to files:

  • src/actions/providers.ts
  • src/app/v1/_lib/url.ts
📚 Learning: 2026-01-10T06:19:58.167Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:19:58.167Z
Learning: Do not modify hardcoded Chinese error messages in Server Actions under src/actions/*.ts as part of piecemeal changes. This is a repo-wide architectural decision that requires a coordinated i18n refactor across all Server Action files (e.g., model-prices.ts, users.ts, system-config.ts). Treat i18n refactor as a separate unified task rather than per-PR changes, and plan a project-wide approach for replacing hardcoded strings with localized resources.

Applied to files:

  • src/actions/providers.ts
  • src/actions/provider-endpoints.ts
📚 Learning: 2026-01-10T06:20:32.687Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx:118-125
Timestamp: 2026-01-10T06:20:32.687Z
Learning: In `src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx`, the "Cancel" button in the SyncConflictDialog is intentionally designed to call `onConfirm([])`, which triggers `doSync([])` to continue the sync while skipping (not overwriting) conflicting manual prices. This is the desired product behavior to allow users to proceed with LiteLLM sync for non-conflicting models while preserving their manual price entries.

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T06:20:04.478Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:20:04.478Z
Learning: In the `ding113/claude-code-hub` repository, Server Actions (files under `src/actions/*.ts`) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  • src/actions/provider-endpoints.ts
🧬 Code graph analysis (9)
tests/unit/app/v1/url.test.ts (1)
src/app/v1/_lib/url.ts (1)
  • buildProxyUrl (25-78)
src/app/[locale]/settings/providers/_components/forms/url-preview.tsx (1)
src/lib/utils/clipboard.ts (1)
  • copyToClipboard (18-30)
tests/unit/actions/provider-endpoints.test.ts (1)
src/actions/provider-endpoints.ts (3)
  • editProviderVendor (613-672)
  • removeProviderVendor (674-718)
  • removeProviderEndpoint (273-320)
src/repository/provider.ts (4)
src/types/provider.ts (2)
  • CreateProviderData (235-303)
  • Provider (39-144)
src/repository/provider-endpoints.ts (3)
  • getOrCreateProviderVendorIdFromUrls (105-152)
  • ensureProviderEndpointExistsForUrl (412-446)
  • tryDeleteProviderVendorIfEmpty (281-332)
src/drizzle/schema.ts (1)
  • providers (149-295)
src/repository/_shared/transformers.ts (1)
  • toProvider (66-119)
tests/unit/repository/provider-endpoints.test.ts (1)
src/repository/provider-endpoints.ts (4)
  • ensureProviderEndpointExistsForUrl (412-446)
  • backfillProviderEndpointsFromProviders (448-530)
  • tryDeleteProviderVendorIfEmpty (281-332)
  • deleteProviderVendor (263-279)
src/repository/provider-endpoints.ts (2)
src/types/provider.ts (4)
  • ProviderVendor (375-384)
  • ProviderEndpoint (388-405)
  • ProviderType (6-12)
  • ProviderEndpointProbeSource (386-386)
src/drizzle/schema.ts (3)
  • providerVendors (132-146)
  • providerEndpoints (298-340)
  • providerEndpointProbeLogs (343-364)
src/drizzle/schema.ts (1)
src/types/provider.ts (1)
  • ProviderType (6-12)
src/actions/providers.ts (3)
src/repository/provider.ts (1)
  • findProviderById (312-372)
src/repository/provider-endpoints.ts (1)
  • tryDeleteProviderVendorIfEmpty (281-332)
src/app/v1/_lib/url.ts (1)
  • buildProxyUrl (25-78)
src/app/v1/_lib/url.ts (2)
src/lib/logger.ts (1)
  • logger (168-187)
scripts/sync-settings-keys.js (2)
  • path (15-15)
  • key (32-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 🌐 API Tests
  • GitHub Check: Docker Build Test
  • GitHub Check: dev-build-deploy
🔇 Additional comments (46)
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx (1)

177-180: LGTM!删除操作的缓存失效逻辑正确。

删除 provider 时同步失效 provider-vendors 缓存是合理的,因为 vendor 可能需要根据关联的 provider 变化进行更新。

drizzle/0055_familiar_shatterstar.sql (1)

1-1: LGTM!

迁移语句简洁正确。使用 DEFAULT false NOT NULL 确保现有记录自动填充默认值,且新记录必须有值。这是添加布尔标志列的标准做法。

messages/zh-CN/settings/providers/form/urlPreview.json (1)

8-19: LGTM!

新增的 endpoints 对象结构正确,翻译质量良好。保留 OpenAI、Codex 等产品名称为英文是合理的做法,有助于用户识别。

messages/zh-TW/settings/providers/form/urlPreview.json (1)

8-19: LGTM!

繁體中文翻譯準確,使用了適當的地區用語(如「訊息」、「產生」、「串流」),與簡體中文版本保持了一致的結構但有正確的詞彙差異。

messages/en/settings/providers/form/urlPreview.json (1)

8-19: LGTM!

英文版本作为基准本地化文件,endpoints 对象的标签清晰、描述性强。

src/app/[locale]/settings/providers/_components/forms/url-preview.tsx (1)

112-157: LGTM! 基于 endpoint key 的 i18n 实现正确。

使用 endpointKey 作为翻译键 (t(\endpoints.${endpointKey}`)) 并作为 React 列表的 key,符合 i18n 最佳实践。本地定义的 copyToClipboard` 函数(第 66 行)正确接受两个参数,与调用处一致。

messages/ja/settings/providers/form/urlPreview.json (1)

9-19: LGTM! 本地化条目已正确添加。

JSON 语法正确,翻译键与 url.ts 中的 endpoint key 对应。技术性 API 名称(如 "Codex Responses"、"OpenAI Chat Completions")保留英文在日语 UI 中是常见做法,可以接受。

messages/zh-CN/settings/providers/strings.json (1)

46-97: LGTM! 完整的简体中文本地化。

新增的端点管理、探测、熔断器等功能的翻译键完整,术语使用一致(服务商、端点、熔断),占位符格式正确。

messages/en/settings/providers/strings.json (1)

46-97: LGTM! 英文本地化与中文结构一致。

新增的本地化键与 zh-CN 文件对应,术语清晰(endpoints、vendor、circuit breaker),占位符格式一致。

src/app/v1/_lib/url.ts (4)

32-38: Case 1 逻辑正确。

requestPath 已经是 basePath 的扩展时(例如 base=/v1/messages,req=/v1/messages/count_tokens),直接使用 requestPath 是正确的做法,避免了路径重复。


48-66: Case 2 端点根路径检测逻辑完善。

同时检测 basePath.endsWith(endpoint)basePath.endsWith(requestRoot) 覆盖了两种配置场景(/messages/v1/messages),后缀提取使用 requestRoot.length 确保只追加子路径部分。


100-135: 端点映射重构为 key-based 结构,支持 i18n。

{ name, path } 改为 { key, path } 结构是正确的设计,key 作为翻译键由 UI 层负责国际化,与 url-preview.tsx 中的 t(\endpoints.${endpointKey}`)` 调用一致。


141-154: 默认端点回退逻辑合理。

providerType 未匹配到任何已知类型时,回退到常见端点(Claude、Codex、OpenAI),确保 UI 预览始终有内容展示。循环中的解构 { key, path } 与类型定义一致。

tests/unit/app/v1/url.test.ts (1)

1-57: 测试覆盖良好,LGTM!

测试用例覆盖了 buildProxyUrl 的核心场景:

  • 标准路径拼接
  • 避免重复路径段
  • 子路径保留
  • 带前缀的 baseUrl 处理
  • query 参数覆盖

建议考虑添加以下边缘场景测试(可后续补充):

  • 无效 URL 的降级处理(触发 catch 分支)
  • /chat/completions/models 端点的处理
src/drizzle/schema.ts (5)

131-146: providerVendors 表设计合理

表结构清晰,使用 websiteDomain 作为唯一标识符,支持供应商元数据存储。

建议:isOfficial 字段的业务语义建议在代码注释中补充说明(什么情况下标记为官方供应商)。


155-157: 外键约束设计合理

使用 onDelete: 'restrict' 可防止误删有关联 provider 的 vendor,保护数据完整性。这与 tryDeleteProviderVendorIfEmpty 的清理逻辑配合良好。


297-340: providerEndpoints 表设计良好

  • 复合唯一索引 (vendorId, providerType, url) 确保端点不重复
  • lastProbe* 字段反规范化设计合理,避免频繁关联查询探测日志表
  • 软删除支持便于数据恢复和审计

342-364: 探测日志表设计简洁有效

  • onDelete: 'cascade' 适合日志数据,端点删除时自动清理
  • (endpointId, createdAt DESC) 索引优化了按端点查询最新日志的场景
  • deletedAt 字段是正确的选择,日志作为只追加的历史记录

721-747: 关系定义完整正确

所有新增的关系定义遵循了现有模式,双向关系配置正确,支持 Drizzle 的关系查询功能。

src/repository/provider-endpoints.ts (7)

20-29: 日期处理辅助函数设计合理

toDatetoNullableDate 提供了安全的日期转换,处理了多种输入类型。

小建议:toDate 在无法解析时返回 new Date() 可能导致静默错误,考虑是否需要记录警告日志。


31-52: URL 规范化逻辑健壮

函数正确处理了多种 URL 格式:

  • 缺少协议的 URL 自动补充 https://
  • 移除 www. 前缀保证域名一致性
  • 解析失败时返回 null 而非抛出异常

105-152: 并发安全的 upsert 实现

onConflictDoNothing + 回退查询的模式正确处理了并发创建场景,避免了竞态条件导致的重复插入或失败。


263-279: 硬删除逻辑正确使用事务

事务保证了删除操作的原子性。由于 providers.providerVendorId 使用 onDelete: 'restrict',必须先删除 providers 才能删除 vendor。

注意:此函数会硬删除所有关联的 providers,使用时需谨慎。建议在调用层添加确认逻辑或仅在特定场景(如测试数据清理)使用。


281-332: 条件删除逻辑设计周全

多重检查确保只在真正"空"的情况下删除 vendor:

  1. 检查活跃 providers
  2. 检查活跃 endpoints
  3. 清理软删除的 providers(解除 FK 约束)
  4. 使用 NOT EXISTS 子句双重验证

错误处理返回 false 而非抛出异常,适合自动清理场景。


448-530: 批量回填实现高效且安全

  • 分页读取(1000)+ 批量写入(500)平衡了内存和数据库压力
  • 内存去重避免重复冲突处理
  • URL 验证确保数据质量
  • 返回统计信息便于监控迁移进度

循环在 rows.length === 0 时正确终止。


611-648: 探测结果记录逻辑正确

事务确保日志插入和端点快照更新的一致性。成功时正确清除错误字段(lastProbeErrorTypelastProbeErrorMessage 设为 null)。

src/repository/provider.ts (3)

17-23: 创建 Provider 时正确初始化 providerVendorId

在插入前通过 getOrCreateProviderVendorIdFromUrls 确保 vendor 存在,避免 FK 约束失败。使用 displayName 作为 vendor 的显示名称是合理的默认行为。


136-148: 端点种子化的容错设计合理

端点创建失败不阻塞 provider 创建是正确的选择。日志记录了足够的上下文信息便于排查问题。


471-495: URL 变更时正确更新 providerVendorId

urlwebsite_url 变更时,重新计算 providerVendorId 确保供应商关联正确。保存 previousVendorId 用于后续清理是好的设计。

src/repository/index.ts (1)

47-61: 导出列表完整且组织良好

新增的 provider-endpoints 导出涵盖了 vendor 和 endpoint 的完整 CRUD 操作,按字母顺序排列便于维护。

tests/unit/repository/provider-endpoints.test.ts (3)

3-17: createThenableQuery 辅助函数实现正确。

该辅助函数正确模拟了 Drizzle ORM 的链式查询构建器模式,使 await db.select().from().where()... 调用能够返回预设结果。


255-391: tryDeleteProviderVendorIfEmpty 测试用例全面覆盖了各种场景。

测试覆盖了:

  • 有 active provider 时不删除
  • 有 active endpoint 时不删除
  • 无 active provider/endpoint 时成功删除
  • vendor 不存在时返回 false
  • transaction 抛错时返回 false

事务模拟和级联删除逻辑的验证都很充分。


412-486: deleteProviderVendor 测试逻辑正确。

测试正确验证了级联删除的顺序(endpoints → providers → vendor)和返回值逻辑。

src/actions/providers.ts (4)

5-5: 新增 buildProxyUrl 导入用于 URL 构建。

该导入在 executeProviderApiTest 函数中使用(Line 2129),用于安全地构建代理 URL,替代简单的字符串拼接。


47-47: 新增 tryDeleteProviderVendorIfEmpty 导入支持自动清理逻辑。

该导入在 removeProvider 中使用,用于在删除 provider 后自动清理孤立的 vendor 记录。


241-241: 正确暴露 providerVendorId 字段。

该字段与 ProviderDisplay 类型定义保持一致,支持前端按 vendor 分组展示 provider。


2129-2129: 使用 buildProxyUrl 构建 URL 是正确的做法。

相比直接字符串拼接,buildProxyUrl 能正确处理路径前缀、查询参数等边界情况,避免产生无效 URL。

src/actions/provider-endpoints.ts (4)

1-38: 导入和类型定义结构清晰。

正确使用 Zod 进行输入验证,通过 @/lib/utils/zod-i18n 提供的辅助函数支持 i18n 错误消息。类型导入与 repository 层一致。


40-123: Zod Schema 定义规范。

  • ProviderTypeSchema 覆盖了所有支持的 provider 类型
  • CreateProviderEndpointSchema 正确使用 .trim().url() 验证
  • UpdateProviderEndpointSchema 使用 .refine() 确保至少有一个字段更新
  • ProbeProviderEndpointSchema 限制 timeout 范围合理(1s-120s)

125-131: getAdminSession 辅助函数减少了重复代码。

统一的 admin 权限检查逻辑,提高了代码可维护性。


703-710: 缓存失效逻辑正确处理了异常。

使用 try-catch 包裹 publishProviderCacheInvalidation 调用,确保缓存失效失败不会阻塞主流程,符合非关键路径的处理模式。

messages/ja/settings/providers/strings.json (1)

46-97: 日语本地化键添加完整。

新增的 i18n 键覆盖了 provider endpoints 和 vendor 管理 UI 所需的所有字符串,翻译质量良好,术语使用一致(如 "エンドポイント"、"ベンダー"、"回路")。

messages/ru/settings/providers/strings.json (1)

46-97: 俄语本地化键添加完整。

新增的 i18n 键与其他语言文件(en、ja、zh-CN、zh-TW)保持一致,技术术语的俄语翻译/音译合理(如 "эндпоинт"、"вендор"),符合俄语技术文档惯例。

src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (1)

811-896: 双重确认删除供应商的实现很好

DeleteVendorDialog 使用 AlertDialog 并实现了两步确认流程,这是删除敏感数据的良好 UX 实践。

messages/zh-TW/settings/providers/strings.json (1)

46-97: 本地化字符串完整覆盖了新功能

新增的 zh-TW 翻译键完整覆盖了端点管理、供应商编辑/删除、熔断状态等新功能所需的所有用户界面文本,术语使用一致。

tests/unit/actions/provider-endpoints.test.ts (1)

13-15: Mock setup is correct and sufficient

The mock properly covers the auth imports. The getAdminSession is not a separate export from @/lib/auth but a local helper function within the action file that internally calls getSession(), which is correctly mocked.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 494 to 496
<span className="text-muted-foreground text-[10px]">
{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true })}
</span>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

formatDistanceToNow 缺少 locale 参数,无法支持多语言

formatDistanceToNow 输出的时间字符串(如 "5 minutes ago")是用户可见内容,但未传入 locale 参数,导致始终显示英文。根据编码规范,所有用户可见字符串必须支持 i18n(5 种语言)。

建议修复
+import { useLocale } from "next-intl";
+import { zhCN, zhTW, ja, ru, enUS } from "date-fns/locale";

+const localeMap: Record<string, Locale> = {
+  "zh-CN": zhCN,
+  "zh-TW": zhTW,
+  ja: ja,
+  ru: ru,
+  en: enUS,
+};

// 在 EndpointRow 组件内
+const locale = useLocale();
+const dateLocale = localeMap[locale] || enUS;

-{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true })}
+{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true, locale: dateLocale })}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span className="text-muted-foreground text-[10px]">
{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true })}
</span>
<span className="text-muted-foreground text-[10px]">
{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true, locale: dateLocale })}
</span>
🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/provider-vendor-view.tsx
around lines 494 - 496, formatDistanceToNow is called without a locale, so
user-visible relative times always render in English; update the call in
provider-vendor-view.tsx (where formatDistanceToNow(new
Date(endpoint.lastProbedAt), ... ) is used) to pass a date-fns locale object
mapped from the app locale. Retrieve the current app locale (e.g., via the
existing useLocale/useRouter or a currentLocale prop), map it to the
corresponding date-fns locale (using your app's date-fns locale map or add one),
then call formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix:
true, locale: mappedDateFnsLocale }). Ensure you import the mapped locale
objects or mapping util and use the same locale source used elsewhere for i18n.

@github-actions
Copy link

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

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 `@src/actions/provider-endpoints.ts`:
- Around line 362-376: The returned endpoint is stale because
probeProviderEndpointAndRecord updates DB fields but you return the pre-probe
endpoint; after calling probeProviderEndpointAndRecord, re-fetch the endpoint by
id (e.g., call the repository/ORM method used elsewhere to load a provider
endpoint by endpoint.id or a function like getProviderEndpointById/getById) and
return that refreshed record instead (return { ok: true, data: { endpoint:
refreshedEndpoint, result } }); also handle the case where the re-fetched
endpoint is missing and return a NOT_FOUND error similarly.
♻️ Duplicate comments (3)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (3)

523-529: 使用 window.confirm 不符合 UI 一致性

删除确认使用原生 confirm() 对话框,与应用其他地方使用的 AlertDialog 组件风格不一致。参考同文件中 DeleteVendorDialog 的实现方式。

建议:替换为 AlertDialog 组件

将删除操作改为使用 AlertDialog,与 DeleteVendorDialog 保持一致的交互模式。


259-280: 供应商类型列表不完整,按钮标签未使用 i18n

providerTypes 数组缺少 "claude-auth""gemini-cli",且按钮直接渲染 {type} 字符串违反了 i18n 要求。

建议修复
-const providerTypes: ProviderType[] = ["claude", "codex", "gemini", "openai-compatible"];
+const providerTypes: ProviderType[] = ["claude", "claude-auth", "codex", "gemini", "gemini-cli", "openai-compatible"];

 {providerTypes.map((type) => (
   <Button
     key={type}
     variant={activeType === type ? "default" : "ghost"}
     size="sm"
     onClick={() => setActiveType(type)}
     className="h-7 text-xs capitalize"
   >
-    {type}
+    {t(`providerTypes.${type.replace(/-/g, "")}`)}
   </Button>
 ))}

489-491: formatDistanceToNow 缺少 locale 参数

相对时间显示(如 "5 minutes ago")始终为英文,不支持多语言。

建议修复:添加 date-fns locale 支持
+import { useLocale } from "next-intl";
+import { zhCN, zhTW, ja, ru, enUS } from "date-fns/locale";

+const localeMap: Record<string, Locale> = {
+  "zh-CN": zhCN,
+  "zh-TW": zhTW,
+  ja,
+  ru,
+  en: enUS,
+};

// 在 EndpointRow 组件内
+const locale = useLocale();
+const dateLocale = localeMap[locale] || enUS;

-{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true })}
+{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true, locale: dateLocale })}
🧹 Nitpick comments (3)
src/actions/provider-endpoints.ts (1)

447-454: Schema 复用降低可读性

getEndpointCircuitInforesetEndpointCircuit 复用了 DeleteProviderEndpointSchema,虽然功能正确(两者都只需要 endpointId),但语义上容易造成混淆。

建议:创建专用 schema 提升可读性
+const EndpointIdOnlySchema = z.object({
+  endpointId: EndpointIdSchema,
+});

// 在 getEndpointCircuitInfo 和 resetEndpointCircuit 中使用
-const parsed = DeleteProviderEndpointSchema.safeParse(input);
+const parsed = EndpointIdOnlySchema.safeParse(input);
src/repository/provider-endpoints.ts (1)

605-642: recordProviderEndpointProbeResult 事务使用正确

在同一事务中完成 probe log 插入和 endpoint 快照更新,保证数据一致性。

注意 Line 638 updatedAt: new Date() 在事务内部再次调用 new Date(),虽然时间差异极小,但为保持一致性,建议复用 probedAt

♻️ 复用 probedAt 保持时间一致
     await tx
       .update(providerEndpoints)
       .set({
         lastProbedAt: probedAt,
         lastProbeOk: input.ok,
         lastProbeStatusCode: input.statusCode ?? null,
         lastProbeLatencyMs: input.latencyMs ?? null,
         lastProbeErrorType: input.ok ? null : (input.errorType ?? null),
         lastProbeErrorMessage: input.ok ? null : (input.errorMessage ?? null),
-        updatedAt: new Date(),
+        updatedAt: probedAt,
       })
tests/unit/actions/provider-endpoints.test.ts (1)

1-199: 建议增加更多边界情况测试

当前测试覆盖了主要的成功路径,建议补充以下场景:

  1. editProviderVendor 当 vendor 不存在时返回 NOT_FOUND
  2. removeProviderEndpoint 当 endpoint 不存在时返回 NOT_FOUND
  3. Zod 验证失败的情况(如 vendorId 为负数)

根据 coding guidelines,新功能应有至少 80% 的测试覆盖率。

💡 建议补充的测试用例
it("editProviderVendor: returns NOT_FOUND when vendor does not exist", async () => {
  getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  updateProviderVendorMock.mockResolvedValue(null);

  const { editProviderVendor } = await import("@/actions/provider-endpoints");
  const res = await editProviderVendor({ vendorId: 999, displayName: "x" });

  expect(res.ok).toBe(false);
  expect(res.errorCode).toBe("NOT_FOUND");
});

it("removeProviderEndpoint: returns NOT_FOUND when endpoint does not exist", async () => {
  getSessionMock.mockResolvedValue({ user: { role: "admin" } });
  findProviderEndpointByIdMock.mockResolvedValue(null);

  const { removeProviderEndpoint } = await import("@/actions/provider-endpoints");
  const res = await removeProviderEndpoint({ endpointId: 999 });

  expect(res.ok).toBe(false);
  expect(res.errorCode).toBe("NOT_FOUND");
});
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 7bbcf12 and 650fcdc.

📒 Files selected for processing (10)
  • drizzle/0056_colorful_nightshade.sql
  • drizzle/meta/0056_snapshot.json
  • drizzle/meta/_journal.json
  • src/actions/provider-endpoints.ts
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  • src/drizzle/schema.ts
  • src/lib/circuit-breaker-probe.ts
  • src/repository/provider-endpoints.ts
  • src/types/provider.ts
  • tests/unit/actions/provider-endpoints.test.ts
✅ Files skipped from review due to trivial changes (1)
  • drizzle/0056_colorful_nightshade.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • drizzle/meta/_journal.json
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use emoji characters in any code, comments, or string literals

Files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  • tests/unit/actions/provider-endpoints.test.ts
  • src/lib/circuit-breaker-probe.ts
  • src/types/provider.ts
  • src/repository/provider-endpoints.ts
  • src/drizzle/schema.ts
  • src/actions/provider-endpoints.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias @/ to reference files in ./src/ directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width

Files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  • tests/unit/actions/provider-endpoints.test.ts
  • src/lib/circuit-breaker-probe.ts
  • src/types/provider.ts
  • src/repository/provider-endpoints.ts
  • src/drizzle/schema.ts
  • src/actions/provider-endpoints.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer named exports over default exports

Files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  • tests/unit/actions/provider-endpoints.test.ts
  • src/lib/circuit-breaker-probe.ts
  • src/types/provider.ts
  • src/repository/provider-endpoints.ts
  • src/drizzle/schema.ts
  • src/actions/provider-endpoints.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

All new features must have unit test coverage of at least 80%

Files:

  • tests/unit/actions/provider-endpoints.test.ts
tests/**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Vitest for unit testing and happy-dom for DOM testing

Files:

  • tests/unit/actions/provider-endpoints.test.ts
src/repository/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Drizzle ORM for data access in the repository layer

Files:

  • src/repository/provider-endpoints.ts
src/drizzle/schema.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Modify database schema in src/drizzle/schema.ts, then run bun run db:generate to generate migrations. Never create SQL migration files manually

Files:

  • src/drizzle/schema.ts
🧠 Learnings (9)
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/drizzle/schema.ts : Modify database schema in `src/drizzle/schema.ts`, then run `bun run db:generate` to generate migrations. Never create SQL migration files manually

Applied to files:

  • drizzle/meta/0056_snapshot.json
  • src/drizzle/schema.ts
📚 Learning: 2026-01-10T06:20:32.687Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx:118-125
Timestamp: 2026-01-10T06:20:32.687Z
Learning: In `src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx`, the "Cancel" button in the SyncConflictDialog is intentionally designed to call `onConfirm([])`, which triggers `doSync([])` to continue the sync while skipping (not overwriting) conflicting manual prices. This is the desired product behavior to allow users to proceed with LiteLLM sync for non-conflicting models while preserving their manual price entries.

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T06:20:04.478Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:20:04.478Z
Learning: In the `ding113/claude-code-hub` repository, Server Actions (files under `src/actions/*.ts`) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  • src/actions/provider-endpoints.ts
📚 Learning: 2026-01-10T06:20:19.207Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: messages/ja/dashboard.json:1342-1345
Timestamp: 2026-01-10T06:20:19.207Z
Learning: In this project, minor i18n formatting issues (such as spacing between numbers and units in Japanese/Chinese) are accepted when fixing them would require adding template complexity. The approach prioritizes code simplicity over perfect locale-specific formatting for minor cosmetic issues.

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Use `next-intl` for internationalization with support for 5 languages: zh-CN, zh-TW, en, ja, ru

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: 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.

Applied to files:

  • src/lib/circuit-breaker-probe.ts
  • src/types/provider.ts
  • src/repository/provider-endpoints.ts
  • src/drizzle/schema.ts
  • src/actions/provider-endpoints.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/repository/**/*.{ts,tsx} : Use Drizzle ORM for data access in the repository layer

Applied to files:

  • src/repository/provider-endpoints.ts
  • src/drizzle/schema.ts
📚 Learning: 2026-01-10T06:19:58.167Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:19:58.167Z
Learning: Do not modify hardcoded Chinese error messages in Server Actions under src/actions/*.ts as part of piecemeal changes. This is a repo-wide architectural decision that requires a coordinated i18n refactor across all Server Action files (e.g., model-prices.ts, users.ts, system-config.ts). Treat i18n refactor as a separate unified task rather than per-PR changes, and plan a project-wide approach for replacing hardcoded strings with localized resources.

Applied to files:

  • src/actions/provider-endpoints.ts
🧬 Code graph analysis (3)
tests/unit/actions/provider-endpoints.test.ts (1)
src/actions/provider-endpoints.ts (3)
  • editProviderVendor (612-670)
  • removeProviderVendor (672-716)
  • removeProviderEndpoint (272-319)
src/drizzle/schema.ts (1)
src/types/provider.ts (1)
  • ProviderType (6-12)
src/actions/provider-endpoints.ts (6)
src/types/provider.ts (4)
  • ProviderVendor (375-383)
  • ProviderType (6-12)
  • ProviderEndpoint (387-404)
  • ProviderEndpointProbeLog (406-416)
src/lib/logger.ts (1)
  • logger (168-187)
src/lib/utils/zod-i18n.ts (2)
  • formatZodError (154-159)
  • extractZodErrorCode (176-190)
src/lib/provider-endpoints/probe.ts (1)
  • probeProviderEndpointAndRecord (104-129)
src/lib/endpoint-circuit-breaker.ts (1)
  • getEndpointHealthInfo (109-114)
src/lib/vendor-type-circuit-breaker.ts (3)
  • getVendorTypeCircuitInfo (107-113)
  • setVendorTypeCircuitManualOpen (157-177)
  • resetVendorTypeCircuit (179-187)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 🌐 API Tests
  • GitHub Check: Docker Build Test
  • GitHub Check: dev-build-deploy
🔇 Additional comments (19)
src/lib/circuit-breaker-probe.ts (1)

9-20: 探测间隔从 30s 缩短到 10s,LGTM

更短的探测间隔可以加快熔断恢复速度,与本 PR 新增的端点探测功能配合良好。建议在高负载场景下关注探测频率对系统和上游提供商的影响。

drizzle/meta/0056_snapshot.json (1)

1-2874: 数据库快照结构正确

新增的三张表 (provider_vendorsprovider_endpointsprovider_endpoint_probe_logs) 的外键约束和索引设计合理:

  • 端点和探测日志使用 CASCADE 删除,供应商删除时自动清理
  • providers.provider_vendor_id 使用 RESTRICT,防止误删有关联的供应商
  • 复合唯一索引 uniq_provider_endpoints_vendor_type_url 防止重复端点
src/types/provider.ts (1)

374-416: 新增类型定义清晰完整

ProviderVendorProviderEndpointProviderEndpointProbeLog 接口与数据库 schema 保持一致,字段类型和可空性定义正确。

src/actions/provider-endpoints.ts (1)

634-648: favicon URL 生成逻辑清晰

根据 websiteUrl 自动生成 Google favicon 服务 URL,并在 websiteUrl 被清空时同步清除 favicon,逻辑完整。

src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (1)

796-880: DeleteVendorDialog 实现了良好的二次确认模式

使用 AlertDialog 组件配合两步确认(confirm → double-confirm),对于破坏性操作提供了良好的用户保护。

src/drizzle/schema.ts (5)

131-145: LGTM! providerVendors 表设计合理

表结构清晰,包含必要的元数据字段(displayName、websiteUrl、faviconUrl)。websiteDomain 上的唯一索引确保了供应商的唯一性标识。


154-156: FK 约束设计合理

使用 onDelete: 'restrict' 防止在有关联 providers 时误删 vendor,这是正确的数据完整性保护。注意:删除 vendor 前需要先处理关联的 providers。


296-339: providerEndpoints 表设计完善

  • 探测快照字段(lastProbe*)支持快速读取最近状态,避免每次查询 probe logs
  • 复合唯一索引 (vendorId, providerType, url) 确保端点不重复
  • 部分索引使用 deletedAt IS NULL 过滤软删除记录,优化查询性能

341-363: providerEndpointProbeLogs 表设计合理

  • 使用 onDelete: 'cascade' 确保端点删除时自动清理探测日志
  • 索引 (endpointId, createdAt DESC) 支持按端点查询最近日志的常见场景

720-746: 关系定义完整且正确

所有新表的 relations 定义正确映射了 FK 关系:

  • providersRelations 新增 vendor 关联
  • providerVendorsRelations 包含 providers 和 endpoints 的一对多关系
  • providerEndpointsRelations 包含 vendor 和 probeLogs 的关联
src/repository/provider-endpoints.ts (4)

104-151: getOrCreateProviderVendorIdFromUrls 的竞态条件处理良好

实现采用了 insert-on-conflict-do-nothing + fallback select 的模式,正确处理了并发创建同一 vendor 的竞态场景。这是标准的 upsert 模式。


275-326: tryDeleteProviderVendorIfEmpty 实现严谨

此函数实现了安全的条件删除逻辑:

  1. 检查是否存在活跃的 providers 和 endpoints
  2. 先清理软删除的 providers 以满足 FK restrict 约束
  3. 使用 NOT EXISTS 子查询确保删除时的原子性检查

这是正确的防御性实现。


442-524: backfillProviderEndpointsFromProviders 批处理设计合理

  • 使用游标分页(gt(providers.id, lastProviderId))避免大表 offset 性能问题
  • 内存中去重(seen Set)减少无效插入
  • onConflictDoNothing 确保幂等性

一个小建议:考虑在 flush() 中添加错误处理,避免部分批次失败导致整个流程中断。


1-1: 此注释不适用于当前代码架构。

src/repository/provider-endpoints.ts 中的 "use server" 指令是必要且合理的。所有调用者均来自服务端上下文:

  • Server Actions (src/actions/provider-endpoints.tssrc/actions/providers.ts) — 静态导入相关函数
  • 服务器初始化脚本 (src/instrumentation.ts) — 动态导入 backfillProviderEndpointsFromProviders
  • 测试文件 (tests/unit/repository/provider-endpoints.test.ts) — 测试环境中的动态导入

无任何客户端组件直接导入该文件。repository 层使用 "use server" 符合标准的服务端数据访问层架构。

Likely an incorrect or invalid review comment.

tests/unit/actions/provider-endpoints.test.ts (5)

67-70: beforeEach 使用 vi.clearAllMocks() 是正确做法

每个测试前清理 mocks,确保测试隔离性。


82-111: 测试验证了 favicon 自动计算逻辑

正确验证了当提供 websiteUrl 时,action 会自动计算并传递 faviconUrl 到 repository 层。


142-167: 缓存失效容错测试良好

Lines 156-167 验证了即使 publishProviderCacheInvalidation 抛出异常,removeProviderVendor 仍然返回成功。这符合 action 实现中 try-catch 的容错设计。


169-198: removeProviderEndpoint 测试覆盖了 vendor 清理流程

验证了删除端点后会触发 tryDeleteProviderVendorIfEmpty,确保孤立的 vendor 被自动清理。


72-80: 模拟设置完整,无需修改

getAdminSession 的实现只检查两个条件:会话是否存在且 session.user.role 是否为 "admin"。测试中的模拟对象 { user: { role: "user" } } 提供了这两个必要的字段,足以验证权限拒绝逻辑。模拟设置与实际实现相匹配。

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +362 to +376
const result = await probeProviderEndpointAndRecord({
endpointId: endpoint.id,
source: "manual",
timeoutMs: parsed.data.timeoutMs,
});

if (!result) {
return {
ok: false,
error: "端点不存在",
errorCode: ERROR_CODES.NOT_FOUND,
};
}

return { ok: true, data: { endpoint, result } };
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

探测后返回的 endpoint 数据可能是过时的

probeProviderEndpointAndRecord 会更新数据库中的 lastProbeOklastProbeLatencyMs 等字段,但此处返回的 endpoint 是探测前查询的数据,不包含最新的探测结果。

建议修复:探测后重新获取 endpoint
     const result = await probeProviderEndpointAndRecord({
       endpointId: endpoint.id,
       source: "manual",
       timeoutMs: parsed.data.timeoutMs,
     });

     if (!result) {
       return {
         ok: false,
         error: "端点不存在",
         errorCode: ERROR_CODES.NOT_FOUND,
       };
     }

-    return { ok: true, data: { endpoint, result } };
+    // Refetch to get updated probe fields
+    const updatedEndpoint = await findProviderEndpointById(endpoint.id);
+    return { ok: true, data: { endpoint: updatedEndpoint ?? endpoint, result } };
🤖 Prompt for AI Agents
In `@src/actions/provider-endpoints.ts` around lines 362 - 376, The returned
endpoint is stale because probeProviderEndpointAndRecord updates DB fields but
you return the pre-probe endpoint; after calling probeProviderEndpointAndRecord,
re-fetch the endpoint by id (e.g., call the repository/ORM method used elsewhere
to load a provider endpoint by endpoint.id or a function like
getProviderEndpointById/getById) and return that refreshed record instead
(return { ok: true, data: { endpoint: refreshedEndpoint, result } }); also
handle the case where the re-fetched endpoint is missing and return a NOT_FOUND
error similarly.

Comment on lines +257 to +273
export async function deleteProviderVendor(vendorId: number): Promise<boolean> {
const deleted = await db.transaction(async (tx) => {
// 1. Delete endpoints (cascade would handle this, but manual is safe)
await tx.delete(providerEndpoints).where(eq(providerEndpoints.vendorId, vendorId));
// 2. Delete providers (keys) - explicit delete required due to 'restrict'
await tx.delete(providers).where(eq(providers.providerVendorId, vendorId));
// 3. Delete vendor
const result = await tx
.delete(providerVendors)
.where(eq(providerVendors.id, vendorId))
.returning({ id: providerVendors.id });

return result.length > 0;
});

return deleted;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

deleteProviderVendor 与 schema 定义的 FK 约束不一致

Schema 中 providers.providerVendorId 定义为 onDelete: 'restrict',但此处尝试在删除 vendor 前直接 delete(providers)。这意味着会物理删除所有关联的 providers(包括未软删除的),而不是遵循 restrict 约束的语义。

如果业务意图是强制级联删除,建议:

  1. 在 schema 中将 FK 改为 onDelete: 'cascade'
  2. 或在此处改用软删除 providers

当前实现可能导致意外的数据丢失(物理删除而非软删除)。

🔧 建议改用软删除
 export async function deleteProviderVendor(vendorId: number): Promise<boolean> {
   const deleted = await db.transaction(async (tx) => {
     // 1. Delete endpoints (cascade would handle this, but manual is safe)
     await tx.delete(providerEndpoints).where(eq(providerEndpoints.vendorId, vendorId));
-    // 2. Delete providers (keys) - explicit delete required due to 'restrict'
-    await tx.delete(providers).where(eq(providers.providerVendorId, vendorId));
+    // 2. Soft delete providers - preserve data while removing FK constraint blocker
+    const now = new Date();
+    await tx
+      .update(providers)
+      .set({ deletedAt: now, isEnabled: false, updatedAt: now })
+      .where(and(eq(providers.providerVendorId, vendorId), isNull(providers.deletedAt)));
+    // 3. Hard delete already soft-deleted providers to satisfy FK restrict
+    await tx
+      .delete(providers)
+      .where(and(eq(providers.providerVendorId, vendorId), isNotNull(providers.deletedAt)));
     // 3. Delete vendor
     const result = await tx
       .delete(providerVendors)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function deleteProviderVendor(vendorId: number): Promise<boolean> {
const deleted = await db.transaction(async (tx) => {
// 1. Delete endpoints (cascade would handle this, but manual is safe)
await tx.delete(providerEndpoints).where(eq(providerEndpoints.vendorId, vendorId));
// 2. Delete providers (keys) - explicit delete required due to 'restrict'
await tx.delete(providers).where(eq(providers.providerVendorId, vendorId));
// 3. Delete vendor
const result = await tx
.delete(providerVendors)
.where(eq(providerVendors.id, vendorId))
.returning({ id: providerVendors.id });
return result.length > 0;
});
return deleted;
}
export async function deleteProviderVendor(vendorId: number): Promise<boolean> {
const deleted = await db.transaction(async (tx) => {
// 1. Delete endpoints (cascade would handle this, but manual is safe)
await tx.delete(providerEndpoints).where(eq(providerEndpoints.vendorId, vendorId));
// 2. Soft delete providers - preserve data while removing FK constraint blocker
const now = new Date();
await tx
.update(providers)
.set({ deletedAt: now, isEnabled: false, updatedAt: now })
.where(and(eq(providers.providerVendorId, vendorId), isNull(providers.deletedAt)));
// 3. Hard delete already soft-deleted providers to satisfy FK restrict
await tx
.delete(providers)
.where(and(eq(providers.providerVendorId, vendorId), isNotNull(providers.deletedAt)));
// 3. Delete vendor
const result = await tx
.delete(providerVendors)
.where(eq(providerVendors.id, vendorId))
.returning({ id: providerVendors.id });
return result.length > 0;
});
return deleted;
}

@github-actions
Copy link

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

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/lib/provider-endpoints/probe.ts`:
- Around line 129-138: The probeEndpointUrl function only falls back to GET when
tryProbe(url, "HEAD") returns statusCode === null; update probeEndpointUrl to
also treat a 405 response as a need to retry with GET by checking
head.statusCode === 405 (or null) and then returning tryProbe(url, "GET",
timeoutMs); keep using the existing tryProbe and EndpointProbeResult types so
behavior and return shape remain unchanged.
♻️ Duplicate comments (1)
src/repository/provider-endpoints.ts (1)

334-350: deleteProviderVendor 与 schema FK 约束不一致(已知问题)

此问题已在之前的审查中指出:schema 中 providers.providerVendorId 定义为 onDelete: 'restrict',但此处物理删除所有关联 providers,可能导致意外数据丢失。

请参考之前的审查建议考虑软删除或调整 FK 约束。

🧹 Nitpick comments (7)
.env.example (1)

127-129: 可选:调整变量顺序以符合 linter 规范

dotenv-linter 提示这些配置变量的顺序与字母序不符。虽然不影响功能,但如果团队有代码规范要求,可考虑调整顺序。

♻️ 建议的调整

将以下变量按字母序重新排列:

  • ENDPOINT_PROBE_CONCURRENCY 应在 ENDPOINT_PROBE_INTERVAL_MS 之前
  • ENDPOINT_PROBE_CYCLE_JITTER_MS 应在 ENDPOINT_PROBE_INTERVAL_MS 之前
  • ENDPOINT_PROBE_LOCK_TTL_MS 应在 ENDPOINT_PROBE_TIMEOUT_MS 之前
  • ENDPOINT_PROBE_LOG_CLEANUP_BATCH_SIZE 应在 ENDPOINT_PROBE_LOG_RETENTION_DAYS 之前

不过考虑到这是示例配置文件,当前按逻辑分组的排列方式可能更易读,可根据团队偏好决定是否调整。

Also applies to: 138-138

src/lib/provider-endpoints/probe.ts (1)

68-80: TypeError 判断可能过于宽泛

TypeError 在 fetch 中不仅用于 URL 解析失败,还可能用于其他场景(如无效的 header、CORS 问题等)。将所有 TypeError 归类为 invalid_url 可能导致错误分类。

建议检查 error message 内容以更准确地判断错误类型。

♻️ 可选的改进方案
 function toErrorInfo(error: unknown): { type: string; message: string } {
   if (error instanceof Error) {
     if (error.name === "AbortError") {
       return { type: "timeout", message: error.message || "timeout" };
     }
     if (error instanceof TypeError) {
-      // Fetch URL parsing failures should not leak the original URL.
-      return { type: "invalid_url", message: "invalid_url" };
+      // TypeError can indicate various issues; use a generic type to avoid misclassification
+      return { type: "fetch_error", message: "fetch_error" };
     }
     return { type: "network_error", message: error.message };
   }
   return { type: "unknown_error", message: String(error) };
 }
tests/unit/lib/provider-endpoints/probe.test.ts (1)

185-222: 建议补充 HEAD 405 回退测试

当前测试覆盖了 AbortError(timeout)场景,但缺少 HEAD 返回 405 Method Not Allowed 时回退到 GET 的测试(如果实现中添加此回退逻辑)。

src/repository/provider-endpoints.ts (1)

57-105: 建议使用更具体的类型替代 any

三个 mapper 函数使用 eslint-disable-next-line @typescript-eslint/no-explicit-any`` 来处理 row 参数。虽然对于内部 mapper 可接受,但可以考虑使用 Drizzle 的推断类型或定义一个宽松的 row 类型以提高类型安全性。

src/lib/provider-endpoints/probe-log-cleanup.ts (2)

1-14: parseIntWithDefault 重复定义

此函数与 src/lib/provider-endpoints/probe.ts 中的实现相同。建议提取到共享工具模块(如 src/lib/provider-endpoints/utils.ts)以避免代码重复。

♻️ 建议提取共享工具
// src/lib/provider-endpoints/utils.ts
export function parseIntWithDefault(value: string | undefined, fallback: number): number {
  const n = value ? Number.parseInt(value, 10) : Number.NaN;
  return Number.isFinite(n) ? n : fallback;
}

然后在 probe.tsprobe-log-cleanup.ts 中导入使用。


111-126: stopEndpointProbeLogCleanup 与运行中的清理存在潜在竞态

如果 runCleanupOnce 正在执行中调用 stopEndpointProbeLogCleanup,第 119 行会将 RUNNING 设为 false,但异步删除操作仍在进行。虽然锁机制可防止其他实例启动,但在同一进程中,若立即调用 startEndpointProbeLogCleanup,可能导致两个清理流程重叠。

当前风险较低,因为这种调用模式不太常见,但建议在注释中说明此行为或考虑等待当前执行完成。

tests/unit/lib/provider-endpoints/probe-scheduler.test.ts (1)

97-170: 并发控制测试设计巧妙

使用 pending 数组手动控制 Promise 解析的方式能够精确验证并发限制。测试正确断言了:

  • 最大并发数不超过配置值 (2)
  • 发现操作仅执行一次
  • 所有端点最终都被探测

建议后续可增加以下边缘场景测试:

  • 探测周期中途失去 leadership
  • 探测过程中请求停止调度器
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 650fcdc and 65c9160.

📒 Files selected for processing (10)
  • .env.example
  • src/instrumentation.ts
  • src/lib/provider-endpoints/leader-lock.ts
  • src/lib/provider-endpoints/probe-log-cleanup.ts
  • src/lib/provider-endpoints/probe-scheduler.ts
  • src/lib/provider-endpoints/probe.ts
  • src/repository/index.ts
  • src/repository/provider-endpoints.ts
  • tests/unit/lib/provider-endpoints/probe-scheduler.test.ts
  • tests/unit/lib/provider-endpoints/probe.test.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use emoji characters in any code, comments, or string literals

Files:

  • tests/unit/lib/provider-endpoints/probe.test.ts
  • src/lib/provider-endpoints/probe-log-cleanup.ts
  • src/lib/provider-endpoints/probe-scheduler.ts
  • src/repository/index.ts
  • tests/unit/lib/provider-endpoints/probe-scheduler.test.ts
  • src/lib/provider-endpoints/probe.ts
  • src/lib/provider-endpoints/leader-lock.ts
  • src/repository/provider-endpoints.ts
  • src/instrumentation.ts
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

All new features must have unit test coverage of at least 80%

Files:

  • tests/unit/lib/provider-endpoints/probe.test.ts
  • tests/unit/lib/provider-endpoints/probe-scheduler.test.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias @/ to reference files in ./src/ directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width

Files:

  • tests/unit/lib/provider-endpoints/probe.test.ts
  • src/lib/provider-endpoints/probe-log-cleanup.ts
  • src/lib/provider-endpoints/probe-scheduler.ts
  • src/repository/index.ts
  • tests/unit/lib/provider-endpoints/probe-scheduler.test.ts
  • src/lib/provider-endpoints/probe.ts
  • src/lib/provider-endpoints/leader-lock.ts
  • src/repository/provider-endpoints.ts
  • src/instrumentation.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer named exports over default exports

Files:

  • tests/unit/lib/provider-endpoints/probe.test.ts
  • src/lib/provider-endpoints/probe-log-cleanup.ts
  • src/lib/provider-endpoints/probe-scheduler.ts
  • src/repository/index.ts
  • tests/unit/lib/provider-endpoints/probe-scheduler.test.ts
  • src/lib/provider-endpoints/probe.ts
  • src/lib/provider-endpoints/leader-lock.ts
  • src/repository/provider-endpoints.ts
  • src/instrumentation.ts
tests/**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Vitest for unit testing and happy-dom for DOM testing

Files:

  • tests/unit/lib/provider-endpoints/probe.test.ts
  • tests/unit/lib/provider-endpoints/probe-scheduler.test.ts
src/repository/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Drizzle ORM for data access in the repository layer

Files:

  • src/repository/index.ts
  • src/repository/provider-endpoints.ts
🧠 Learnings (2)
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: 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.

Applied to files:

  • src/lib/provider-endpoints/probe-log-cleanup.ts
  • src/lib/provider-endpoints/probe-scheduler.ts
  • src/repository/index.ts
  • src/lib/provider-endpoints/probe.ts
  • src/lib/provider-endpoints/leader-lock.ts
  • src/repository/provider-endpoints.ts
  • src/instrumentation.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/repository/**/*.{ts,tsx} : Use Drizzle ORM for data access in the repository layer

Applied to files:

  • src/repository/provider-endpoints.ts
🧬 Code graph analysis (7)
tests/unit/lib/provider-endpoints/probe.test.ts (2)
src/types/provider.ts (1)
  • ProviderEndpoint (387-404)
src/lib/provider-endpoints/probe.ts (2)
  • probeEndpointUrl (129-138)
  • probeProviderEndpointAndRecord (205-220)
src/lib/provider-endpoints/probe-scheduler.ts (3)
src/lib/provider-endpoints/leader-lock.ts (4)
  • LeaderLock (6-10)
  • renewLeaderLock (62-109)
  • releaseLeaderLock (111-141)
  • acquireLeaderLock (26-60)
src/repository/provider-endpoints.ts (1)
  • findEnabledProviderEndpointsForProbing (112-132)
src/lib/provider-endpoints/probe.ts (1)
  • probeProviderEndpointAndRecordByEndpoint (155-203)
tests/unit/lib/provider-endpoints/probe-scheduler.test.ts (1)
src/lib/provider-endpoints/probe-scheduler.ts (2)
  • startEndpointProbeScheduler (213-234)
  • stopEndpointProbeScheduler (236-252)
src/lib/provider-endpoints/probe.ts (4)
src/lib/logger.ts (1)
  • logger (168-187)
src/types/provider.ts (2)
  • ProviderEndpoint (387-404)
  • ProviderEndpointProbeSource (385-385)
src/lib/endpoint-circuit-breaker.ts (1)
  • recordEndpointFailure (137-158)
src/repository/provider-endpoints.ts (3)
  • recordProviderEndpointProbeResult (682-719)
  • updateProviderEndpointProbeSnapshot (134-157)
  • findProviderEndpointById (270-297)
src/lib/provider-endpoints/leader-lock.ts (3)
scripts/sync-settings-keys.js (2)
  • key (32-32)
  • result (40-40)
src/lib/rate-limit/service.ts (1)
  • redis (102-104)
src/lib/logger.ts (1)
  • logger (168-187)
src/repository/provider-endpoints.ts (4)
src/types/provider.ts (5)
  • ProviderVendor (375-383)
  • ProviderEndpoint (387-404)
  • ProviderEndpointProbeLog (406-416)
  • ProviderType (6-12)
  • ProviderEndpointProbeSource (385-385)
src/repository/index.ts (15)
  • findEnabledProviderEndpointsForProbing (52-52)
  • updateProviderEndpointProbeSnapshot (62-62)
  • deleteProviderEndpointProbeLogsBeforeDateBatch (50-50)
  • findProviderVendors (57-57)
  • findProviderVendorById (56-56)
  • findProviderEndpointById (53-53)
  • updateProviderVendor (63-63)
  • deleteProviderVendor (51-51)
  • tryDeleteProviderVendorIfEmpty (60-60)
  • findProviderEndpointsByVendorAndType (55-55)
  • createProviderEndpoint (49-49)
  • updateProviderEndpoint (61-61)
  • softDeleteProviderEndpoint (59-59)
  • recordProviderEndpointProbeResult (58-58)
  • findProviderEndpointProbeLogs (54-54)
src/drizzle/db.ts (1)
  • db (37-44)
src/drizzle/schema.ts (4)
  • providerEndpoints (297-339)
  • providerVendors (132-145)
  • providers (148-294)
  • providerEndpointProbeLogs (342-363)
src/instrumentation.ts (3)
src/lib/provider-endpoints/probe-scheduler.ts (2)
  • stopEndpointProbeScheduler (236-252)
  • startEndpointProbeScheduler (213-234)
src/lib/provider-endpoints/probe-log-cleanup.ts (2)
  • stopEndpointProbeLogCleanup (111-126)
  • startEndpointProbeLogCleanup (93-109)
src/repository/provider-endpoints.ts (1)
  • backfillProviderEndpointsFromProviders (519-601)
🪛 dotenv-linter (4.0.0)
.env.example

[warning] 127-127: [UnorderedKey] The ENDPOINT_PROBE_CONCURRENCY key should go before the ENDPOINT_PROBE_INTERVAL_MS key

(UnorderedKey)


[warning] 128-128: [UnorderedKey] The ENDPOINT_PROBE_CYCLE_JITTER_MS key should go before the ENDPOINT_PROBE_INTERVAL_MS key

(UnorderedKey)


[warning] 129-129: [UnorderedKey] The ENDPOINT_PROBE_LOCK_TTL_MS key should go before the ENDPOINT_PROBE_TIMEOUT_MS key

(UnorderedKey)


[warning] 138-138: [UnorderedKey] The ENDPOINT_PROBE_LOG_CLEANUP_BATCH_SIZE key should go before the ENDPOINT_PROBE_LOG_RETENTION_DAYS key

(UnorderedKey)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 🌐 API Tests
  • GitHub Check: Docker Build Test
  • GitHub Check: dev-build-deploy
🔇 Additional comments (41)
.env.example (1)

122-138: 配置设计合理,参数选择得当!

新增的端点探测配置整体设计良好:

  • 超时(5秒)小于探测间隔(10秒),避免探测重叠
  • 锁 TTL(30秒)是探测间隔的 3 倍,适合分布式领导者选举
  • 并发度(10)和抖动(1秒)设置合理,可防止探测风暴
  • 成功日志采样间隔(60秒)能有效减少日志量
  • 日志保留期(7天)和清理批次大小(10000)均为典型生产配置

配置说明也很清晰,默认启用符合该功能的核心定位。

src/lib/provider-endpoints/probe.ts (4)

1-11: LGTM!

导入和模块设置正确。使用 server-only 确保仅在服务端执行,导入了必要的依赖。


12-26: LGTM!

类型定义清晰,配置解析逻辑合理,带有默认值回退。


155-203: LGTM!

probeProviderEndpointAndRecordByEndpoint 实现完整:

  • 正确处理失败时的熔断器记录
  • 根据 source 和采样间隔决定是写完整日志还是仅更新快照
  • 逻辑清晰,分支处理合理

205-220: LGTM!

probeProviderEndpointAndRecord 正确处理端点不存在的情况,返回 null 而非抛出异常。

tests/unit/lib/provider-endpoints/probe.test.ts (6)

1-30: LGTM!

测试辅助函数 makeEndpoint 设置合理,afterEach 清理逻辑确保测试隔离。


31-69: LGTM!

HEAD 成功测试覆盖了正常路径,验证了不触发 GET 的预期行为。


71-111: LGTM!

HEAD 网络错误回退 GET 的测试验证了容错逻辑。


113-183: LGTM!

5xx 和 4xx 响应的处理测试覆盖了边界情况,验证了 ok 状态的判断逻辑。


224-330: LGTM!

端点不存在和记录入库的测试完整,验证了:

  • findProviderEndpointById 返回 null 时正确处理
  • 记录字段包含所有必要信息(source、ok、statusCode、latency、probedAt)
  • 时间戳使用 fake timers 确保确定性

332-423: LGTM!

scheduled 采样和失败熔断计数的测试覆盖了核心业务逻辑:

  • 验证了 scheduled 成功时仅更新快照而非写日志
  • 验证了失败时 recordEndpointFailure 被正确调用
src/repository/provider-endpoints.ts (8)

1-19: LGTM!

模块设置正确,使用 "use server" 指令并导入了必要的 Drizzle ORM 组件和 schema 定义。按照 coding guidelines,repository 层使用 Drizzle ORM 进行数据访问。


20-29: LGTM!

日期转换辅助函数处理了多种输入类型,toNullableDate 正确区分 null/undefined。


31-55: LGTM!

normalizeWebsiteDomainFromUrl 实现健壮:

  • 支持带/不带协议的 URL
  • 正确移除 www. 前缀
  • 解析失败时记录调试日志而不泄露完整 URL

159-179: LGTM!

批量删除使用 FOR UPDATE SKIP LOCKED 是正确的并发安全模式,避免了锁竞争和死锁。


181-228: LGTM!

getOrCreateProviderVendorIdFromUrls 使用 onConflictDoNothing 处理并发插入竞态,并有回退查询确保最终获取 ID。


352-403: LGTM!

tryDeleteProviderVendorIfEmpty 实现完善:

  • 事务内检查无活跃 providers 和 endpoints
  • 先清理软删除的 providers 满足 FK restrict 约束
  • 使用 NOT EXISTS 子查询确保原子性
  • 异常捕获并记录警告日志

519-601: LGTM!

backfillProviderEndpointsFromProviders 实现高效:

  • 分页读取避免内存溢出
  • 使用 Set 去重避免重复插入
  • 批量写入减少数据库往返
  • URL 验证跳过无效数据

682-719: LGTM!

recordProviderEndpointProbeResult 使用事务确保日志插入和端点更新的原子性,符合数据一致性要求。

src/repository/index.ts (1)

47-64: LGTM!

新增的 provider-endpoints 导出遵循现有模式,函数按字母顺序排列,为上层 actions 和 API 提供统一入口。

src/lib/provider-endpoints/probe-log-cleanup.ts (2)

27-32: LGTM!

使用 globalThis 管理单例状态是 Next.js 中处理模块级状态的常见模式,避免热重载时状态丢失。


34-91: LGTM!

runCleanupOnce 实现健壮:

  • 使用 RUNNING 标志防止并发执行
  • 分布式锁确保多实例环境下单点执行
  • 批量删除直到无更多数据或批次不满
  • finally 块确保状态和锁正确释放
tests/unit/lib/provider-endpoints/probe-scheduler.test.ts (3)

1-41: 类型定义和辅助函数设计良好

类型定义 ProbeTargetProbeResult 与实际被测模块保持一致,辅助工厂函数 makeEndpointmakeOkResult 简洁实用。flushMicrotasks 是处理异步测试的常见模式。


43-68: Mock 设置结构合理

使用模块级 mock 变量配合 vi.mock 工厂函数的模式,能够在保持 mock 提升行为的同时允许每个测试自定义 mock 实现。afterEach 中正确清理了定时器和环境变量存根。


70-95: 非 Leader 场景测试正确

测试正确验证了当无法获取 leader 锁时,调度器不会执行端点发现和探测操作。断言清晰且覆盖了关键行为。

src/instrumentation.ts (5)

124-144: 关闭处理逻辑完善

新增的 stopEndpointProbeSchedulerstopEndpointProbeLogCleanup 调用遵循了现有的错误处理模式,使用 try/catch 包裹并记录警告日志。这确保了即使某个组件停止失败,也不会阻塞其他组件的清理。


202-220: 生产环境初始化增强合理

AUTO_MIGRATE=false 检查允许外部管理迁移的生产部署跳过自动迁移,并清晰记录日志。端点回填使用 try/catch 包裹,失败不会阻塞应用启动。


257-277: 生产环境探测调度器启动逻辑一致

探测调度器和日志清理的启动遵循了与其他后台任务相同的模式:try/catch 包裹、失败仅记录警告、不阻塞应用启动。


290-302: 开发环境回填逻辑正确

开发环境在迁移完成后执行端点回填,与生产环境保持一致。错误处理模式相同。


343-369: 开发环境数据库可用性检查完善

正确地在 isConnected 条件内启动探测调度器和日志清理,避免本地开发无数据库时的错误。当数据库不可用时的警告信息清晰明确。

src/lib/provider-endpoints/probe-scheduler.ts (7)

1-31: 导入和配置解析设计良好

配置解析使用 Math.max 确保最小边界值,parseIntWithDefault 辅助函数处理了无效输入的回退。环境变量命名清晰,默认值合理。


41-55: 工具函数实现正确

sleep 函数正确处理了非正数的边缘情况。shuffleInPlace 使用 Fisher-Yates 算法实现无偏随机打乱。


57-76: Leader 锁获取逻辑正确

先尝试续期现有锁,失败后释放并重新获取的模式是合理的。当续期失败时(无论是过期还是所有权丢失),先释放再重新获取能确保状态一致性。


78-125: Keep-alive 机制设计稳健

续期间隔设置为 TTL/2 确保在锁过期前完成续期。timer.unref() 调用避免定时器阻塞进程退出。stoppedrenewing 标志防止了竞态条件。


127-174: 探测周期主逻辑结构清晰

周期运行前的守卫检查(防止并发和响应停止请求)、leadership 确保、keep-alive 启动、随机抖动、容量预警计算都设计合理。


178-202: 并发 Worker 模式实现正确

使用共享 index 变量的 work-stealing 模式在 JavaScript 单线程模型下是安全的。每个 worker 在获取端点后立即递增索引,确保端点不会被重复处理。Promise.all 等待所有 worker 完成后才退出周期。


213-272: 公共 API 函数设计完善

  • startEndpointProbeScheduler: 幂等启动,立即执行首次探测周期
  • stopEndpointProbeScheduler: 正确设置停止标志、清理定时器、释放锁
  • getEndpointProbeSchedulerStatus: 提供有用的调试信息

所有函数使用命名导出,符合编码规范。

src/lib/provider-endpoints/leader-lock.ts (4)

1-24: 类型定义和辅助函数清晰

LeaderLock 接口明确区分了 redismemory 锁类型。内存锁清理函数正确处理过期条目。generateLockId 使用时间戳和随机字符串组合确保唯一性。


26-60: Redis 错误回退行为需注意

当 Redis 调用失败时(非不可用,而是实际错误如网络超时),代码会回退到内存锁。在多实例部署中,如果 Redis 间歇性不稳定,可能导致不同实例分别持有 Redis 锁和内存锁。

当前设计优先考虑优雅降级而非严格一致性,这在单实例部署或可以容忍短暂多 leader 的场景下是合理的。建议在部署文档中明确说明此行为。


62-82: 内存锁续期时的 Redis 可用性检查设计巧妙

第 65-69 行的逻辑:当持有内存锁但 Redis 恢复可用时返回 false,强制调用方重新获取分布式锁。这有效缓解了 Redis 恢复后可能出现的脑裂问题。


111-141: 锁释放逻辑正确

内存锁和 Redis 锁的释放都正确验证了所有权。Redis 不可用时静默处理,错误时仅记录警告不抛出异常,符合"尽力而为"的释放语义。

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +129 to +138
export async function probeEndpointUrl(
url: string,
timeoutMs: number = DEFAULT_TIMEOUT_MS
): Promise<EndpointProbeResult> {
const head = await tryProbe(url, "HEAD", timeoutMs);
if (head.statusCode === null) {
return tryProbe(url, "GET", timeoutMs);
}
return head;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

HEAD 返回 405 时未回退到 GET

当前逻辑仅在 statusCode === null(网络错误)时回退到 GET。但某些服务器不支持 HEAD 方法会返回 405 Method Not Allowed,此时 statusCode 为 405,不会触发 GET 回退,可能导致误判健康端点为不可用。

🔧 建议增加 405 回退
 export async function probeEndpointUrl(
   url: string,
   timeoutMs: number = DEFAULT_TIMEOUT_MS
 ): Promise<EndpointProbeResult> {
   const head = await tryProbe(url, "HEAD", timeoutMs);
-  if (head.statusCode === null) {
+  if (head.statusCode === null || head.statusCode === 405) {
     return tryProbe(url, "GET", timeoutMs);
   }
   return head;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function probeEndpointUrl(
url: string,
timeoutMs: number = DEFAULT_TIMEOUT_MS
): Promise<EndpointProbeResult> {
const head = await tryProbe(url, "HEAD", timeoutMs);
if (head.statusCode === null) {
return tryProbe(url, "GET", timeoutMs);
}
return head;
}
export async function probeEndpointUrl(
url: string,
timeoutMs: number = DEFAULT_TIMEOUT_MS
): Promise<EndpointProbeResult> {
const head = await tryProbe(url, "HEAD", timeoutMs);
if (head.statusCode === null || head.statusCode === 405) {
return tryProbe(url, "GET", timeoutMs);
}
return head;
}
🤖 Prompt for AI Agents
In `@src/lib/provider-endpoints/probe.ts` around lines 129 - 138, The
probeEndpointUrl function only falls back to GET when tryProbe(url, "HEAD")
returns statusCode === null; update probeEndpointUrl to also treat a 405
response as a need to retry with GET by checking head.statusCode === 405 (or
null) and then returning tryProbe(url, "GET", timeoutMs); keep using the
existing tryProbe and EndpointProbeResult types so behavior and return shape
remain unchanged.

@github-actions
Copy link

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

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 `@tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx`:
- Around line 131-143: The JSX for the test render is broken because the type
assertion `as any` was split onto its own line and treated as JSX props; wrap
the entire JSX expression passed to renderWithProviders (the ProviderVendorView
element) in parentheses and apply the `as any` type assertion to that
parenthesized expression so the assertion attaches to the JSX value rather than
creating stray props (fix the call that uses renderWithProviders and the
ProviderVendorView props/currentUser).
- Around line 166-178: The JSX has a broken type assertion split across lines
for the currentUser prop; fix the syntax by combining the assertion so
currentUser is passed as a single expression (e.g., currentUser={{ role: "admin"
} as any}) and remove the stray standalone "as" and "any" tokens after the
component props; verify the ProviderVendorView(...) call contains no broken
tokens and compiles.
♻️ Duplicate comments (3)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (3)

484-496: 删除确认使用原生 window.confirm()

此问题在之前的评审中已被标记。建议使用 AlertDialog 组件替代原生 confirm() 以保持 UI 一致性,类似于 DeleteVendorDialog 的实现方式。


221-260: providerTypes 数组不完整且按钮标签缺少 i18n 翻译。

此问题在之前的评审中已被标记。当前 providerTypes 数组缺少 "claude-auth""gemini-cli",且第 245 行直接渲染 {type} 违反了 i18n 要求。

根据 src/types/provider.ts 中的 ProviderType 定义,应包含所有 6 种类型,并使用翻译函数显示按钮文本。

建议修复
-  const providerTypes: ProviderType[] = ["claude", "codex", "gemini", "openai-compatible"];
+  const providerTypes: ProviderType[] = ["claude", "claude-auth", "codex", "gemini", "gemini-cli", "openai-compatible"];

按钮渲染部分:

-                >
-                  {type}
-                </Button>
+                >
+                  {t(`providerTypes.${type.replace(/-([a-z])/g, (_, c) => c.toUpperCase())}`)}
+                </Button>

450-457: formatDistanceToNow 缺少 locale 参数。

此问题在之前的评审中已被标记。第 452 行的 formatDistanceToNow 调用没有传入 locale 参数,导致相对时间始终显示为英文,违反了 i18n 要求。

建议修复
+import { useLocale } from "next-intl";
+import { zhCN, zhTW, ja, ru, enUS } from "date-fns/locale";

+const localeMap: Record<string, Locale> = {
+  "zh-CN": zhCN,
+  "zh-TW": zhTW,
+  ja: ja,
+  ru: ru,
+  en: enUS,
+};

// 在 EndpointRow 组件内
+const locale = useLocale();
+const dateLocale = localeMap[locale] || enUS;

-{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true })}
+{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true, locale: dateLocale })}
🧹 Nitpick comments (5)
tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsx (2)

40-56: 建议添加 afterEach 清理以提高测试隔离性。

当前 renderWithProviders 返回 unmount 函数由每个测试手动调用。如果测试中途失败抛出异常,unmount() 可能不会被执行,导致 DOM 残留影响后续测试。

♻️ 建议的改进
 let queryClient: QueryClient;
+let cleanupFns: Array<() => void> = [];

 function renderWithProviders(node: ReactNode) {
   const container = document.createElement("div");
   document.body.appendChild(container);
   const root = createRoot(container);

   act(() => {
     root.render(<QueryClientProvider client={queryClient}>{node}</QueryClientProvider>);
   });

+  const cleanup = () => {
+    act(() => root.unmount());
+    container.remove();
+  };
+  cleanupFns.push(cleanup);
+
   return {
     container,
-    unmount: () => {
-      act(() => root.unmount());
-      container.remove();
-    },
+    unmount: cleanup,
   };
 }

然后在 beforeEach 中添加清理逻辑:

 beforeEach(() => {
+  cleanupFns.forEach((fn) => fn());
+  cleanupFns = [];
   queryClient = new QueryClient({

或者使用 afterEach 钩子进行清理。


78-148: 测试覆盖良好,涵盖了主要场景。

三个测试用例覆盖了:

  • 空数据状态渲染占位符
  • 成功状态(绿色线条)
  • 失败状态(红色线条)

建议考虑补充以下边界场景以提高覆盖率:

  • res.ok === false 时的错误处理
  • latencyMsnull 的情况
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx (2)

162-162: 考虑将 providerTypeItems 提取为共享常量。

providerTypeItems 数组在组件内部硬编码,如果其他地方也需要使用相同的类型列表,可能导致不一致。建议将其提取到共享的常量文件或类型定义中。

建议的重构方案
// 在 src/types/provider.ts 或 src/lib/provider-type-utils.tsx 中定义
export const PROVIDER_TYPE_OPTIONS: ProviderType[] = ["claude", "codex", "gemini", "openai-compatible"];

然后在组件中导入使用:

-  const providerTypeItems: ProviderType[] = ["claude", "codex", "gemini", "openai-compatible"];
+  // 从共享常量导入 PROVIDER_TYPE_OPTIONS

116-118: 使用占位符文本作为错误消息不太合适。

当 API 密钥为空时,抛出的错误消息使用了 tForm("key.placeholder")(输入框占位符文本),这在语义上不正确。建议使用专门的验证错误消息。

建议的修改
      if (!apiKey) {
-        throw new Error(tForm("key.placeholder"));
+        throw new Error(tForm("key.required") || "API key is required");
      }

需要在翻译文件中添加对应的 key.required 键值。

src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (1)

83-91: Props 接口中 any 类型可以考虑改进。

healthStatusstatistics 使用 Record<number, any> 类型,虽然可以工作,但会丢失类型安全性。建议在后续迭代中定义具体的类型接口。

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between 65c9160 and d96db4c.

📒 Files selected for processing (11)
  • messages/en/settings/providers/strings.json
  • messages/ja/settings/providers/strings.json
  • messages/ru/settings/providers/strings.json
  • messages/zh-CN/settings/providers/strings.json
  • messages/zh-TW/settings/providers/strings.json
  • src/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  • src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
  • tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsx
  • tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
  • tests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • messages/en/settings/providers/strings.json
  • messages/ja/settings/providers/strings.json
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use emoji characters in any code, comments, or string literals

Files:

  • tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsx
  • tests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx
  • src/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx
  • src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
  • tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

All new features must have unit test coverage of at least 80%

Files:

  • tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsx
  • tests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx
  • tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias @/ to reference files in ./src/ directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width

Files:

  • tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsx
  • tests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx
  • src/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx
  • src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
  • tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer named exports over default exports

Files:

  • tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsx
  • tests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx
  • src/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx
  • src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
  • tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
tests/**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Vitest for unit testing and happy-dom for DOM testing

Files:

  • tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsx
  • tests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx
  • tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
🧠 Learnings (8)
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to tests/**/*.test.{ts,tsx,js,jsx} : Use Vitest for unit testing and happy-dom for DOM testing

Applied to files:

  • tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsx
  • tests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx
  • tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : All new features must have unit test coverage of at least 80%

Applied to files:

  • tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsx
📚 Learning: 2026-01-05T03:02:14.502Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx:66-66
Timestamp: 2026-01-05T03:02:14.502Z
Learning: In the claude-code-hub project, the translations.actions.addKey field in UserKeyTableRowProps is defined as optional for backward compatibility, but all actual callers in the codebase provide the complete translations object. The field has been added to all 5 locale files (messages/{locale}/dashboard.json).

Applied to files:

  • tests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx
  • src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
📚 Learning: 2026-01-10T06:20:32.687Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx:118-125
Timestamp: 2026-01-10T06:20:32.687Z
Learning: In `src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx`, the "Cancel" button in the SyncConflictDialog is intentionally designed to call `onConfirm([])`, which triggers `doSync([])` to continue the sync while skipping (not overwriting) conflicting manual prices. This is the desired product behavior to allow users to proceed with LiteLLM sync for non-conflicting models while preserving their manual price entries.

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T06:20:04.478Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:20:04.478Z
Learning: In the `ding113/claude-code-hub` repository, Server Actions (files under `src/actions/*.ts`) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T06:20:19.207Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: messages/ja/dashboard.json:1342-1345
Timestamp: 2026-01-10T06:20:19.207Z
Learning: In this project, minor i18n formatting issues (such as spacing between numbers and units in Japanese/Chinese) are accepted when fixing them would require adding template complexity. The approach prioritizes code simplicity over perfect locale-specific formatting for minor cosmetic issues.

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Use `next-intl` for internationalization with support for 5 languages: zh-CN, zh-TW, en, ja, ru

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
🧬 Code graph analysis (6)
tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsx (1)
src/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx (1)
  • EndpointLatencySparkline (13-63)
tests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx (1)
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx (1)
  • VendorKeysCompactList (70-266)
src/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx (1)
src/actions/provider-endpoints.ts (1)
  • getProviderEndpointProbeLogs (384-418)
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx (4)
src/actions/provider-endpoints.ts (1)
  • getProviderEndpoints (160-186)
src/actions/providers.ts (4)
  • addProvider (433-596)
  • editProvider (599-730)
  • removeProvider (733-772)
  • getUnmaskedProviderKey (1354-1381)
src/lib/provider-type-utils.tsx (1)
  • getProviderTypeTranslationKey (65-67)
src/lib/utils/clipboard.ts (2)
  • isClipboardSupported (5-12)
  • copyToClipboard (18-30)
tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx (1)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (1)
  • ProviderVendorView (93-149)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (3)
src/types/provider.ts (4)
  • ProviderDisplay (147-217)
  • ProviderVendor (375-383)
  • ProviderType (6-12)
  • ProviderEndpoint (387-404)
src/actions/provider-endpoints.ts (8)
  • getProviderVendors (132-144)
  • getVendorTypeCircuitInfo (503-543)
  • resetVendorTypeCircuit (579-610)
  • getProviderEndpoints (160-186)
  • probeProviderEndpoint (321-382)
  • removeProviderEndpoint (272-319)
  • addProviderEndpoint (188-225)
  • editProviderEndpoint (227-270)
src/drizzle/schema.ts (1)
  • providers (148-294)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 🌐 API Tests
  • GitHub Check: Docker Build Test
  • GitHub Check: dev-build-deploy
🔇 Additional comments (16)
src/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx (1)

1-62: LGTM!

组件实现清晰:

  • 使用 react-query 正确配置了缓存策略
  • 数据转换逻辑合理,通过 reverse 保证时间顺序
  • 空状态处理得当
  • 颜色根据最近探测状态动态切换
messages/zh-CN/settings/providers/strings.json (1)

46-101: 本地化键值完整且结构正确。

新增的翻译键值覆盖了端点管理、服务商管理、熔断控制等新功能的全部 UI 文案。JSON 语法正确,与其他语言文件(en、ja、ru、zh-TW)的键名保持一致。

src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx (2)

58-68: LGTM!

辅助函数逻辑清晰,名称构建规则合理:使用域名、类型和密钥后4位组合,并限制在64字符内。


268-469: LGTM!

VendorKeyRow 组件实现良好:

  • 正确处理了敏感数据(密钥查看后及时清除)
  • 剪贴板功能有完善的错误处理
  • 删除操作使用了确认对话框
  • 状态切换和删除后正确刷新缓存
messages/zh-TW/settings/providers/strings.json (1)

46-101: LGTM!

繁体中文翻译完整,键名与简体中文及其他语言文件保持一致。JSON 结构正确。

tests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx (3)

62-101: LGTM!

测试工具函数实现正确:

  • renderWithProviders 正确设置了 QueryClient 和 NextIntl 上下文
  • flushTicks 用于等待异步操作完成
  • setNativeValue 正确处理了 React 受控输入的值设置

115-156: LGTM!

第一个测试用例正确验证了回归点:新增密钥对话框中不应显示 URL 输入框和 URL 拼接预览。断言清晰明确。


158-229: LGTM!

第二个测试用例全面验证了提交流程:

  • 验证了 getProviderEndpoints 被正确调用
  • 验证了 addProvider 的调用参数包含正确的 URL、密钥、类型等
  • 验证了成功提示 toast 的显示
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (7)

1-17: 导入和客户端声明结构正确。

导入组织清晰,使用了 @/ 路径别名,符合编码规范。React Query、next-intl 和 UI 组件的导入都是必要的。


151-219: VendorCard 组件实现良好。

组件正确使用 i18n 翻译函数处理所有用户可见文本,Avatar 和 Card 组件组合合理,条件渲染逻辑清晰。


262-327: VendorTypeCircuitControl 实现正确。

React Query 的 useQuery 和 useMutation 使用模式正确,错误处理通过 toast 通知用户,成功后正确调用 invalidateQueries 刷新缓存。


329-378: EndpointsTable 组件结构清晰。

查询使用了正确的 queryKey 模式 ["provider-endpoints", vendorId, providerType],加载状态和空状态都有适当的 UI 反馈。


503-596: AddEndpointButton 实现良好。

Dialog 模式使用正确,表单处理通过 FormData 实现,useEffect 在对话框关闭时重置状态。值得注意的是第 541 行的查询失效使用了精确的 queryKey ["provider-endpoints", vendorId, providerType],这是比较好的实践。


598-672: EditEndpointDialog 实现正确。

表单处理和 Switch 组件的使用方式正确。第 624 行的 invalidateQueries({ queryKey: ["provider-endpoints"] }) 会刷新所有端点查询,这在编辑场景下是合理的,因为用户可能会切换到其他类型查看。


674-759: DeleteVendorDialog 实现优秀。

两步确认流程(confirm → double-confirm)为破坏性操作提供了额外的安全保障。正确使用 AlertDialog 组件而非原生 confirm(),保持了 UI 一致性。错误处理使用 getErrorMessage(tErrors, res.errorCode) 进行国际化翻译。

这是删除确认对话框的良好实现范例,建议将 EndpointRow 中的删除逻辑也改为类似模式。

messages/ru/settings/providers/strings.json (1)

46-101: 俄语翻译完整且格式正确。

新增的翻译键覆盖了所有端点管理、供应商管理和熔断器控制相关的 UI 文本。占位符变量({name}{id}{providerType})保留正确,JSON 结构有效。

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@github-actions
Copy link

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

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: 8

🤖 Fix all issues with AI agents
In `@src/app/`[locale]/settings/providers/_components/forms/provider-form.tsx:
- Around line 1135-1160: Replace the hardcoded SelectValue placeholder text with
the i18n lookup: use the page's translation function t(...) instead of literal
"inherit" for the SelectValue placeholder and for other SelectValue occurrences
(e.g., the Select using cacheTtlPreference / SelectValue placeholder and the
Context1m/Codex SelectValue placeholders mentioned); locate the Select
components (e.g., the Select with value={cacheTtlPreference} and its
SelectValue) and set
placeholder={t("sections.routing.cacheTtl.options.inherit")} (and equivalent
i18n keys for the other SelectValue placeholders) so all user-visible strings
flow through t(...).
- Around line 156-167: The default cloned name uses a hardcoded suffix
(`${cloneProvider.name}_Copy`) in the name useState initialization; change it to
use the app i18n instead by obtaining the translation function (e.g.,
useTranslation or t) and building the default as
`${cloneProvider.name}${t('provider.copySuffix')}` (or another existing i18n
key) so the user-visible suffix is localized; update the name useState
initializer in provider-form (and import/use the translation hook at the top of
the component) and ensure the chosen i18n key is added to translation files.
- Around line 860-887: The options mapping can omit the current providerType,
causing stale/invalid selections to be submitted; update the options generation
so that if providerType is set but not included in allowedProviderTypes you
insert it into the list (preferably as the first or last item and marked
disabled or annotated) before mapping; adjust the disabled logic that uses
enableMultiProviderTypes accordingly so the inserted fallback item uses the same
disabled rules, and keep using getProviderTypeConfig and renderProviderTypeLabel
to render its icon/label so the SelectItem shows a visible, non-crashing
fallback for providerType.
- Around line 286-314: The auto-resolve effect overwrites an existing
preset/website URL; update the useEffect in provider-form.tsx to skip calling
urlResolver when a preset or current websiteUrl exists (i.e., only run when
websiteUrl is empty and preset?.websiteUrl is falsy), and make sure to call the
correct state setter (setWebsiteUrl) instead of setUrl in the then/catch/finally
branches; keep the cancelled guard and autoUrlPending handling as-is.

In `@src/app/`[locale]/settings/providers/_components/provider-vendor-view.tsx:
- Line 246: providerTypes array is missing the "openai-compatible" member from
the ProviderType union; update the const providerTypes (in
provider-vendor-view.tsx) to include "openai-compatible" or add a brief inline
comment on why it's intentionally excluded so callers expecting ProviderType
variants (e.g., other endpoint handlers) remain consistent; modify the array
declaration for providerTypes to add "openai-compatible" (or add a clear comment
next to that declaration explaining its intentional omission).

In
`@src/app/`[locale]/settings/providers/_components/vendor-keys-compact-list.tsx:
- Around line 487-507: Add accessible aria-labels to the interactive elements
that lack visible text: for the Switch (checked={props.provider.isEnabled} /
onCheckedChange calling toggleMutation.mutate) and for the icon-only Button
inside AlertDialogTrigger (which shows Trash2 and uses deleteMutation and
deleteDialogOpen/setDeleteDialogOpen). Use i18n strings (translated messages)
that include the provider name (props.provider.name) to form labels like "Enable
{provider}" / "Disable {provider}" or "Delete {provider} key" as appropriate,
and pass them via aria-label on the Switch and the Button so screen readers get
descriptive text.
- Around line 50-55: The hardcoded fallback "vendor" in buildDefaultProviderName
(used with vendorWebsiteDomain and providerType) must be localized and not
embedded; change buildDefaultProviderName to accept a fallbackName parameter (or
an i18n function) and use that instead of the literal "vendor", then update
callers (including the other usage around the block at lines ~112-118) to pass a
localized string from the component via the i18n hook (e.g., t('...')) so all
user-visible defaults come from localized resources.
- Around line 413-425: The UI currently hardcodes the multiplier suffix "x" in
the InlineEditPopover suffix prop and in the read-only span; replace both
occurrences to use the i18n helper (e.g., tInline) instead of the literal "x".
Update InlineEditPopover's suffix argument to tInline("costMultiplierSuffix")
and render the read-only value using tInline("costMultiplierSuffix") so the
suffix is translatable; locate these in the cost multiplier cell around
InlineEditPopover and the span that renders props.provider.costMultiplier.
♻️ Duplicate comments (2)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (2)

516-527: 删除端点使用原生 confirm() 与 UI 风格不一致

删除供应商使用 AlertDialog 组件(DeleteVendorDialog),但删除端点却使用原生 window.confirm(),导致用户体验不一致。建议统一使用 AlertDialog 组件。

建议提取为独立的 DeleteEndpointDialog 组件
function DeleteEndpointDialog({ 
  endpoint, 
  onDelete 
}: { 
  endpoint: ProviderEndpoint; 
  onDelete: () => void;
}) {
  const t = useTranslations("settings.providers");
  const tCommon = useTranslations("settings.common");
  
  return (
    <AlertDialog>
      <AlertDialogTrigger asChild>
        <DropdownMenuItem
          className="text-destructive focus:text-destructive"
          onSelect={(e) => e.preventDefault()}
        >
          <Trash2 className="mr-2 h-4 w-4" />
          {tCommon("delete")}
        </DropdownMenuItem>
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>{t("confirmDeleteEndpoint")}</AlertDialogTitle>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>{tCommon("cancel")}</AlertDialogCancel>
          <AlertDialogAction onClick={onDelete}>
            {tCommon("confirm")}
          </AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  );
}

482-488: formatDistanceToNow 缺少 locale 参数,不符合 i18n 规范

相对时间字符串(如 "5 minutes ago")是用户可见内容,但未传入 locale 参数,导致始终显示英文。根据编码规范,所有用户可见字符串必须支持 i18n(5 种语言)。

建议的修复
+import { useLocale } from "next-intl";
+import { zhCN, zhTW, ja, ru, enUS } from "date-fns/locale";
+import type { Locale } from "date-fns";

+const DATE_LOCALE_MAP: Record<string, Locale> = {
+  "zh-CN": zhCN,
+  "zh-TW": zhTW,
+  ja: ja,
+  ru: ru,
+  en: enUS,
+};

// 在 EndpointRow 组件内
function EndpointRow({ endpoint }: { endpoint: ProviderEndpoint }) {
  const t = useTranslations("settings.providers");
  const tCommon = useTranslations("settings.common");
+ const locale = useLocale();
+ const dateLocale = DATE_LOCALE_MAP[locale] || enUS;
  // ...
  
  // 在渲染时
- {formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true })}
+ {formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true, locale: dateLocale })}
🧹 Nitpick comments (5)
tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx (2)

13-13: 使用路径别名导入消息文件

根据编码规范,应使用 @/ 路径别名引用 ./src/ 目录下的文件。虽然 messages/ 目录在 src/ 外部,但建议确认项目中是否配置了对应的别名(如 @messages/),以保持导入风格一致性。


184-185: CSS 类选择器断言较为脆弱

使用 th[class*="w-[220px]"] 依赖具体的 Tailwind 类名,若样式重构会导致测试失败。建议改用 data-testid 属性或基于文本内容查询(如 getByText)来提高测试稳定性。

建议的改进
-    const latencyHeader = document.querySelector('th[class*="w-[220px]"]');
-    expect(latencyHeader?.textContent || "").toContain("Latency");
+    // 在组件中添加 data-testid="latency-header"
+    // 或直接使用文本断言
+    expect(document.body.textContent || "").toContain("Latency");
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (2)

84-92: healthStatus 属性未被使用

ProviderVendorViewProps 定义了 healthStatus 属性,但在组件实现中从未使用该属性。如果暂不需要,建议移除以避免混淆;如果是后续功能预留,建议添加 TODO 注释说明。

如果暂不需要,建议移除
 interface ProviderVendorViewProps {
   providers: ProviderDisplay[];
   currentUser?: User;
   enableMultiProviderTypes: boolean;
-  healthStatus: Record<number, any>;
   statistics: Record<number, any>;
   statisticsLoading: boolean;
   currencyCode: CurrencyCode;
 }

306-310: 错误处理可能产生不明确的错误信息

res.okfalse 时,res.error 可能为 undefined,导致抛出 Error: undefined。建议添加回退消息。

建议的改进
     queryFn: async () => {
       const res = await getVendorTypeCircuitInfo({ vendorId, providerType });
-      if (!res.ok) throw new Error(res.error);
+      if (!res.ok) throw new Error(res.error || "Failed to fetch circuit info");
       return res.data;
     },
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx (1)

7-7: 复制提示的定时器缺少清理

在组件卸载或重复点击时可能触发卸载后的 setState。建议用 useRef 保存计时器并在 unmount 时清理。

Proposed refactor
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
@@
   const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
   const [editOpen, setEditOpen] = useState(false);
+  const copyResetTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
@@
   useEffect(() => {
     setClipboardAvailable(isClipboardSupported());
   }, []);
+
+  useEffect(() => {
+    return () => {
+      if (copyResetTimerRef.current) {
+        clearTimeout(copyResetTimerRef.current);
+      }
+    };
+  }, []);
@@
     setCopied(true);
     toast.success(tList("keyCopied"));
-    setTimeout(() => setCopied(false), 3000);
+    if (copyResetTimerRef.current) {
+      clearTimeout(copyResetTimerRef.current);
+    }
+    copyResetTimerRef.current = setTimeout(() => setCopied(false), 3000);
   };

Also applies to: 342-345

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to Reviews > Disable Cache setting

📥 Commits

Reviewing files that changed from the base of the PR and between d96db4c and fbd5084.

📒 Files selected for processing (6)
  • src/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx
  • src/app/[locale]/settings/providers/_components/forms/provider-form.tsx
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
  • src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
  • tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
  • tests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx
  • src/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Never use emoji characters in any code, comments, or string literals

Files:

  • src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
  • src/app/[locale]/settings/providers/_components/forms/provider-form.tsx
  • tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias @/ to reference files in ./src/ directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width

Files:

  • src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
  • src/app/[locale]/settings/providers/_components/forms/provider-form.tsx
  • tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer named exports over default exports

Files:

  • src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
  • src/app/[locale]/settings/providers/_components/forms/provider-form.tsx
  • tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

All new features must have unit test coverage of at least 80%

Files:

  • tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
tests/**/*.test.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Vitest for unit testing and happy-dom for DOM testing

Files:

  • tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
🧠 Learnings (7)
📚 Learning: 2026-01-05T03:02:14.502Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx:66-66
Timestamp: 2026-01-05T03:02:14.502Z
Learning: In the claude-code-hub project, the translations.actions.addKey field in UserKeyTableRowProps is defined as optional for backward compatibility, but all actual callers in the codebase provide the complete translations object. The field has been added to all 5 locale files (messages/{locale}/dashboard.json).

Applied to files:

  • src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to tests/**/*.test.{ts,tsx,js,jsx} : Use Vitest for unit testing and happy-dom for DOM testing

Applied to files:

  • tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
📚 Learning: 2026-01-10T06:20:32.687Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx:118-125
Timestamp: 2026-01-10T06:20:32.687Z
Learning: In `src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx`, the "Cancel" button in the SyncConflictDialog is intentionally designed to call `onConfirm([])`, which triggers `doSync([])` to continue the sync while skipping (not overwriting) conflicting manual prices. This is the desired product behavior to allow users to proceed with LiteLLM sync for non-conflicting models while preserving their manual price entries.

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T06:20:04.478Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:20:04.478Z
Learning: In the `ding113/claude-code-hub` repository, Server Actions (files under `src/actions/*.ts`) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T06:20:19.207Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: messages/ja/dashboard.json:1342-1345
Timestamp: 2026-01-10T06:20:19.207Z
Learning: In this project, minor i18n formatting issues (such as spacing between numbers and units in Japanese/Chinese) are accepted when fixing them would require adding template complexity. The approach prioritizes code simplicity over perfect locale-specific formatting for minor cosmetic issues.

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Use `next-intl` for internationalization with support for 5 languages: zh-CN, zh-TW, en, ja, ru

Applied to files:

  • src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
🧬 Code graph analysis (4)
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx (6)
src/app/[locale]/settings/providers/_components/forms/provider-form.tsx (1)
  • ProviderForm (111-2215)
src/actions/provider-endpoints.ts (1)
  • getProviderEndpoints (160-186)
src/actions/providers.ts (3)
  • editProvider (599-730)
  • removeProvider (733-772)
  • getUnmaskedProviderKey (1354-1381)
src/lib/provider-type-utils.tsx (2)
  • getProviderTypeConfig (55-57)
  • getProviderTypeTranslationKey (65-67)
src/lib/utils/clipboard.ts (2)
  • isClipboardSupported (5-12)
  • copyToClipboard (18-30)
src/app/[locale]/settings/providers/_components/inline-edit-popover.tsx (1)
  • InlineEditPopover (22-185)
src/app/[locale]/settings/providers/_components/forms/provider-form.tsx (3)
src/types/provider.ts (5)
  • ProviderType (6-12)
  • CodexReasoningEffortPreference (17-24)
  • CodexReasoningSummaryPreference (26-26)
  • CodexTextVerbosityPreference (28-28)
  • CodexParallelToolCallsPreference (31-31)
src/app/[locale]/settings/providers/_components/forms/url-preview.tsx (1)
  • UrlPreview (31-162)
src/lib/provider-type-utils.tsx (1)
  • getProviderTypeConfig (55-57)
tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx (2)
src/types/user.ts (1)
  • User (4-32)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (1)
  • ProviderVendorView (94-160)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (4)
src/types/provider.ts (4)
  • ProviderDisplay (147-217)
  • ProviderVendor (375-383)
  • ProviderType (6-12)
  • ProviderEndpoint (387-404)
src/lib/provider-type-utils.tsx (2)
  • getProviderTypeConfig (55-57)
  • getProviderTypeTranslationKey (65-67)
src/app/[locale]/settings/providers/_components/forms/url-preview.tsx (1)
  • UrlPreview (31-162)
src/lib/utils/error-messages.ts (1)
  • getErrorMessage (157-169)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: 🌐 API Tests
  • GitHub Check: Docker Build Test
  • GitHub Check: dev-build-deploy
🔇 Additional comments (12)
tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx (3)

80-94: 测试用户常量定义正确

ADMIN_USER 包含了 User 类型所需的所有必填字段,且类型安全,无需使用 as any 类型断言。


139-149: 测试设置结构良好

beforeEach 正确地重置 QueryClient、清理 DOM 并重置 mocks。使用 vi.hoisted() 确保 mocks 在模块加载前初始化。


151-188: 测试用例覆盖基本场景

当前测试验证了电路断开(open)和关闭(closed)两种核心状态的 UI 表现。建议后续补充以下场景的测试:

  • half-open 状态
  • manualOpen: true 时的 UI 差异
  • 加载状态和错误状态
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (4)

535-628: 添加端点对话框实现正确

表单处理、加载状态管理和查询失效逻辑都符合最佳实践。使用 useEffect 在对话框关闭时重置表单状态是好的做法。


706-791: 删除供应商对话框实现优秀

两步确认流程、加载状态处理、i18n 错误消息以及对话框关闭时重置状态,都是很好的 UX 实践。


94-160: 主组件结构清晰

ProviderVendorView 使用命名导出、正确处理加载状态、使用 useMemo 优化派生数据计算。组件分解合理,各子组件职责明确。


344-356: 当前实现已正确处理所有可能的电路断路器状态

VendorTypeCircuitInfo 只定义了两种状态:"closed""open",不包含 "half-open" 状态。当前实现在 circuitState === "open" 时显示重置按钮,其他情况(即 "closed" 状态)返回 null,这已经正确处理了所有可能的状态。无需额外处理 "half-open" 状态。

(注:"half-open" 状态用于其他电路断路器实现如 EndpointCircuit,但不适用于此处的 VendorTypeCircuit)

src/app/[locale]/settings/providers/_components/forms/provider-form.tsx (5)

36-36: ProviderForm 扩展 API 与类型标签渲染集成良好
新增 props、类型配置读取与标签渲染逻辑清晰,能支撑多端点/多类型的表单需求。

Also applies to: 69-78, 125-148


420-429: 提交前的 no-endpoints 保护合理
在隐藏 URL 场景下补充提示,逻辑清晰。


718-744: 隐藏 URL 时的空态与加载态提示清晰
空态与自动解析中的提示对用户友好。


767-782: 网站地址可选渲染逻辑 OK
开关式展示和校验保持一致。


1071-1134: 路由参数输入的校验与提示保持一致
数值处理与说明文本一致,整体 OK。

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 156 to 167
const [name, setName] = useState(
isEdit ? (provider?.name ?? "") : cloneProvider ? `${cloneProvider.name}_Copy` : ""
isEdit
? (provider?.name ?? "")
: cloneProvider
? `${cloneProvider.name}_Copy`
: (preset?.name ?? "")
);
const [url, setUrl] = useState(sourceProvider?.url ?? "");
const [url, setUrl] = useState(sourceProvider?.url ?? preset?.url ?? "");
const [key, setKey] = useState(""); // 编辑时留空代表不更新
const [providerType, setProviderType] = useState<ProviderType>(
sourceProvider?.providerType ?? "claude"
sourceProvider?.providerType ?? preset?.providerType ?? "claude"
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

克隆名称默认值使用了硬编码字符串
${cloneProvider.name}_Copy 会直接出现在输入框中,应走 i18n。

Proposed fix
-      : cloneProvider
-        ? `${cloneProvider.name}_Copy`
-        : (preset?.name ?? "")
+      : cloneProvider
+        ? t("name.cloneDefault", { name: cloneProvider.name })
+        : (preset?.name ?? "")
根据编码规范,用户可见字符串必须走 i18n。
🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/forms/provider-form.tsx
around lines 156 - 167, The default cloned name uses a hardcoded suffix
(`${cloneProvider.name}_Copy`) in the name useState initialization; change it to
use the app i18n instead by obtaining the translation function (e.g.,
useTranslation or t) and building the default as
`${cloneProvider.name}${t('provider.copySuffix')}` (or another existing i18n
key) so the user-visible suffix is localized; update the name useState
initializer in provider-form (and import/use the translation hook at the top of
the component) and ensure the chosen i18n key is added to translation files.

Comment on lines +286 to +314
const [websiteUrl, setWebsiteUrl] = useState<string>(
sourceProvider?.websiteUrl ?? preset?.websiteUrl ?? ""
);
const [autoUrlPending, setAutoUrlPending] = useState(false);

useEffect(() => {
if (isEdit) return;
if (!hideUrl || !urlResolver) return;

let cancelled = false;
setAutoUrlPending(true);
urlResolver(providerType)
.then((resolved) => {
if (cancelled) return;
setUrl(resolved?.trim() ? resolved.trim() : "");
})
.catch(() => {
if (cancelled) return;
setUrl("");
})
.finally(() => {
if (cancelled) return;
setAutoUrlPending(false);
});

return () => {
cancelled = true;
};
}, [isEdit, hideUrl, urlResolver, providerType]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

自动解析可能覆盖已有 preset URL
hideUrl + urlResolver 下会在 providerType 变化时重写 url,即使已有 preset/url 也会被覆盖。建议在已有值时跳过解析。

Proposed fix
   useEffect(() => {
     if (isEdit) return;
     if (!hideUrl || !urlResolver) return;
+    if (url.trim()) return;

     let cancelled = false;
     setAutoUrlPending(true);
@@
-  }, [isEdit, hideUrl, urlResolver, providerType]);
+  }, [isEdit, hideUrl, urlResolver, providerType, url]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [websiteUrl, setWebsiteUrl] = useState<string>(
sourceProvider?.websiteUrl ?? preset?.websiteUrl ?? ""
);
const [autoUrlPending, setAutoUrlPending] = useState(false);
useEffect(() => {
if (isEdit) return;
if (!hideUrl || !urlResolver) return;
let cancelled = false;
setAutoUrlPending(true);
urlResolver(providerType)
.then((resolved) => {
if (cancelled) return;
setUrl(resolved?.trim() ? resolved.trim() : "");
})
.catch(() => {
if (cancelled) return;
setUrl("");
})
.finally(() => {
if (cancelled) return;
setAutoUrlPending(false);
});
return () => {
cancelled = true;
};
}, [isEdit, hideUrl, urlResolver, providerType]);
const [websiteUrl, setWebsiteUrl] = useState<string>(
sourceProvider?.websiteUrl ?? preset?.websiteUrl ?? ""
);
const [autoUrlPending, setAutoUrlPending] = useState(false);
useEffect(() => {
if (isEdit) return;
if (!hideUrl || !urlResolver) return;
if (url.trim()) return;
let cancelled = false;
setAutoUrlPending(true);
urlResolver(providerType)
.then((resolved) => {
if (cancelled) return;
setUrl(resolved?.trim() ? resolved.trim() : "");
})
.catch(() => {
if (cancelled) return;
setUrl("");
})
.finally(() => {
if (cancelled) return;
setAutoUrlPending(false);
});
return () => {
cancelled = true;
};
}, [isEdit, hideUrl, urlResolver, providerType, url]);
🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/forms/provider-form.tsx
around lines 286 - 314, The auto-resolve effect overwrites an existing
preset/website URL; update the useEffect in provider-form.tsx to skip calling
urlResolver when a preset or current websiteUrl exists (i.e., only run when
websiteUrl is empty and preset?.websiteUrl is falsy), and make sure to call the
correct state setter (setWebsiteUrl) instead of setUrl in the then/catch/finally
branches; keep the cancelled guard and autoUrlPending handling as-is.

Comment on lines +860 to +887
{(
allowedProviderTypes ?? [
"claude",
"claude-auth",
"codex",
"gemini",
"gemini-cli",
"openai-compatible",
]
).map((type) => {
const typeConfig = getProviderTypeConfig(type);
const TypeIcon = typeConfig.icon;
const label = renderProviderTypeLabel(type);
const disabled =
type === "openai-compatible" ? !enableMultiProviderTypes : false;
return (
<SelectItem key={type} value={type} disabled={disabled}>
<div className="flex items-center gap-2">
<span
className={`inline-flex h-5 w-5 items-center justify-center rounded ${typeConfig.bgColor}`}
>
<TypeIcon className={`h-3.5 w-3.5 ${typeConfig.iconColor}`} />
</span>
<span>{label}</span>
</div>
</SelectItem>
);
})}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

allowedProviderTypes 可能与当前 providerType 不一致
当 preset/providerType 不在 allowedProviderTypes 中时,Select 没有对应项但仍可提交旧值,易导致 UI/校验异常。建议在本地进行兜底纠正或上层保证包含当前类型。

Proposed fix
   const [providerType, setProviderType] = useState<ProviderType>(
     sourceProvider?.providerType ?? preset?.providerType ?? "claude"
   );

+  useEffect(() => {
+    if (!allowedProviderTypes?.length) return;
+    if (!allowedProviderTypes.includes(providerType)) {
+      setProviderType(allowedProviderTypes[0]);
+    }
+  }, [allowedProviderTypes, providerType]);
🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/forms/provider-form.tsx
around lines 860 - 887, The options mapping can omit the current providerType,
causing stale/invalid selections to be submitted; update the options generation
so that if providerType is set but not included in allowedProviderTypes you
insert it into the list (preferably as the first or last item and marked
disabled or annotated) before mapping; adjust the disabled logic that uses
enableMultiProviderTypes accordingly so the inserted fallback item uses the same
disabled rules, and keep using getProviderTypeConfig and renderProviderTypeLabel
to render its icon/label so the SelectItem shows a visible, non-crashing
fallback for providerType.

Comment on lines +1135 to +1160
<div className="space-y-2">
<Label>{t("sections.routing.cacheTtl.label")}</Label>
<Select
value={cacheTtlPreference}
onValueChange={(val) => setCacheTtlPreference(val as "inherit" | "5m" | "1h")}
disabled={isPending}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="inherit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="inherit">
{t("sections.routing.cacheTtl.options.inherit")}
</SelectItem>
<SelectItem value="5m">
{t("sections.routing.cacheTtl.options.5m")}
</SelectItem>
<SelectItem value="1h">
{t("sections.routing.cacheTtl.options.1h")}
</SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-muted-foreground">
{t("sections.routing.cacheTtl.desc")}
</p>
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

SelectValue 的 placeholder 存在硬编码
多处 placeholder="inherit" 属于用户可见文本,需使用 i18n。

Proposed fix
-  <SelectValue placeholder="inherit" />
+  <SelectValue placeholder={t("sections.routing.cacheTtl.options.inherit")} />
其余 Context1m/Codex 相关 SelectValue 也应使用对应的 i18n key。根据编码规范,用户可见字符串必须走 i18n。

Also applies to: 1162-1192, 1194-1330

🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/forms/provider-form.tsx
around lines 1135 - 1160, Replace the hardcoded SelectValue placeholder text
with the i18n lookup: use the page's translation function t(...) instead of
literal "inherit" for the SelectValue placeholder and for other SelectValue
occurrences (e.g., the Select using cacheTtlPreference / SelectValue placeholder
and the Context1m/Codex SelectValue placeholders mentioned); locate the Select
components (e.g., the Select with value={cacheTtlPreference} and its
SelectValue) and set
placeholder={t("sections.routing.cacheTtl.options.inherit")} (and equivalent
i18n keys for the other SelectValue placeholders) so all user-visible strings
flow through t(...).

Comment on lines +50 to +55
function buildDefaultProviderName(input: {
vendorWebsiteDomain: string;
providerType: ProviderType;
}): string {
const base = input.vendorWebsiteDomain.trim() || "vendor";
return `${base}-${input.providerType}`.slice(0, 64);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

默认名称使用硬编码文案,需本地化

Line 54 的 "vendor" 会出现在用户可见的默认名称中,应通过 i18n 提供本地化文案。建议将回退名称作为参数从组件中传入。As per coding guidelines, 所有用户可见文本需使用 i18n。

Proposed fix
-function buildDefaultProviderName(input: {
-  vendorWebsiteDomain: string;
-  providerType: ProviderType;
-}): string {
-  const base = input.vendorWebsiteDomain.trim() || "vendor";
-  return `${base}-${input.providerType}`.slice(0, 64);
-}
+function buildDefaultProviderName(input: {
+  vendorWebsiteDomain: string;
+  providerType: ProviderType;
+  fallbackName: string;
+}): string {
+  const base = input.vendorWebsiteDomain.trim() || input.fallbackName;
+  return `${base}-${input.providerType}`.slice(0, 64);
+}
-                  preset={{
-                    name: buildDefaultProviderName({
-                      vendorWebsiteDomain: props.vendorWebsiteDomain,
-                      providerType: defaultProviderType,
-                    }),
-                    providerType: defaultProviderType,
-                    websiteUrl: props.vendorWebsiteUrl ?? "",
-                  }}
+                  preset={{
+                    name: buildDefaultProviderName({
+                      vendorWebsiteDomain: props.vendorWebsiteDomain,
+                      providerType: defaultProviderType,
+                      fallbackName: t("vendorFallbackName"),
+                    }),
+                    providerType: defaultProviderType,
+                    websiteUrl: props.vendorWebsiteUrl ?? "",
+                  }}

Also applies to: 112-118

🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
around lines 50 - 55, The hardcoded fallback "vendor" in
buildDefaultProviderName (used with vendorWebsiteDomain and providerType) must
be localized and not embedded; change buildDefaultProviderName to accept a
fallbackName parameter (or an i18n function) and use that instead of the literal
"vendor", then update callers (including the other usage around the block at
lines ~112-118) to pass a localized string from the component via the i18n hook
(e.g., t('...')) so all user-visible defaults come from localized resources.

Comment on lines +413 to +425
<TableCell className="hidden md:table-cell py-1 text-right text-sm tabular-nums">
{props.canEdit ? (
<InlineEditPopover
value={props.provider.costMultiplier}
label={tInline("costMultiplierLabel")}
validator={validateCostMultiplier}
onSave={handleSaveCostMultiplier}
suffix="x"
type="number"
/>
) : (
<span className="text-xs">{props.provider.costMultiplier}x</span>
)}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

倍率后缀 "x" 为硬编码展示文本

Line 420 和 Line 424 的 "x" 会直接展示在 UI 中,应通过 i18n 提供可翻译后缀。As per coding guidelines, 所有用户可见文本需使用 i18n。

Proposed fix
   const tInline = useTranslations("settings.providers.inlineEdit");
   const tTypes = useTranslations("settings.providers.types");
+  const multiplierSuffix = tList("costMultiplierSuffix");
@@
             <InlineEditPopover
               value={props.provider.costMultiplier}
               label={tInline("costMultiplierLabel")}
               validator={validateCostMultiplier}
               onSave={handleSaveCostMultiplier}
-              suffix="x"
+              suffix={multiplierSuffix}
               type="number"
             />
           ) : (
-            <span className="text-xs">{props.provider.costMultiplier}x</span>
+            <span className="text-xs">
+              {props.provider.costMultiplier}
+              {multiplierSuffix}
+            </span>
           )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<TableCell className="hidden md:table-cell py-1 text-right text-sm tabular-nums">
{props.canEdit ? (
<InlineEditPopover
value={props.provider.costMultiplier}
label={tInline("costMultiplierLabel")}
validator={validateCostMultiplier}
onSave={handleSaveCostMultiplier}
suffix="x"
type="number"
/>
) : (
<span className="text-xs">{props.provider.costMultiplier}x</span>
)}
<TableCell className="hidden md:table-cell py-1 text-right text-sm tabular-nums">
{props.canEdit ? (
<InlineEditPopover
value={props.provider.costMultiplier}
label={tInline("costMultiplierLabel")}
validator={validateCostMultiplier}
onSave={handleSaveCostMultiplier}
suffix={tInline("costMultiplierSuffix")}
type="number"
/>
) : (
<span className="text-xs">
{props.provider.costMultiplier}
{tInline("costMultiplierSuffix")}
</span>
)}
🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
around lines 413 - 425, The UI currently hardcodes the multiplier suffix "x" in
the InlineEditPopover suffix prop and in the read-only span; replace both
occurrences to use the i18n helper (e.g., tInline) instead of the literal "x".
Update InlineEditPopover's suffix argument to tInline("costMultiplierSuffix")
and render the read-only value using tInline("costMultiplierSuffix") so the
suffix is translatable; locate these in the cost multiplier cell around
InlineEditPopover and the span that renders props.provider.costMultiplier.

Comment on lines +487 to +507
<Switch
checked={props.provider.isEnabled}
onCheckedChange={(checked) => toggleMutation.mutate(checked)}
className="scale-75 data-[state=checked]:bg-green-500"
/>
)}
{props.canEdit && (
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Trash2 className="h-4 w-4" />
)}
</Button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

图标按钮与开关缺少可访问性标签

Switch 与图标按钮在无可见文本时应提供 aria-label,建议使用 i18n 文案并包含 provider 名称以提升可访问性。As per coding guidelines, 用户可见文本需使用 i18n。

Proposed fix
             {props.canEdit && (
               <Switch
                 checked={props.provider.isEnabled}
                 onCheckedChange={(checked) => toggleMutation.mutate(checked)}
                 className="scale-75 data-[state=checked]:bg-green-500"
+                aria-label={tList("toggleProvider", { name: props.provider.name })}
               />
             )}
@@
                 <AlertDialogTrigger asChild>
                   <Button
                     variant="ghost"
                     size="icon"
                     className="h-7 w-7 text-destructive hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
                     disabled={deleteMutation.isPending}
+                    aria-label={tList("deleteButton")}
                   >
@@
               {clipboardAvailable && (
-                <Button onClick={handleCopy} disabled={!unmaskedKey} size="icon" variant="outline">
+                <Button
+                  onClick={handleCopy}
+                  disabled={!unmaskedKey}
+                  size="icon"
+                  variant="outline"
+                  aria-label={tList("copyButton")}
+                >

Also applies to: 558-563

🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
around lines 487 - 507, Add accessible aria-labels to the interactive elements
that lack visible text: for the Switch (checked={props.provider.isEnabled} /
onCheckedChange calling toggleMutation.mutate) and for the icon-only Button
inside AlertDialogTrigger (which shows Trash2 and uses deleteMutation and
deleteDialogOpen/setDeleteDialogOpen). Use i18n strings (translated messages)
that include the provider name (props.provider.name) to form labels like "Enable
{provider}" / "Disable {provider}" or "Delete {provider} key" as appropriate,
and pass them via aria-label on the Switch and the Button so screen readers get
descriptive text.

ding113 and others added 2 commits January 18, 2026 21:13
…ap UI

- Add tabs layout to availability page (Provider Availability | Endpoint Probe History)
- Create heatmap grid for endpoint probes grouped by provider type (claude, gemini, codex, openai-compatible)
- Add batch probe logs API endpoint for efficient data fetching
- Create reusable components: endpoint-url-display, probe-cell-tooltip, endpoint-probe-row
- Add i18n translations for all 5 languages (en, zh-CN, zh-TW, ja, ru)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create new provider form v2 with tabbed layout (General, Routing, Rate Limits, Resilience, Advanced, Test)
- Add provider form context for shared state management
- Refactor form sections into dedicated tab components
- Update i18n translations for form tabs and sections (all 5 languages)
- Minor fixes to existing provider components

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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: 12

Caution

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

⚠️ Outside diff range comments (5)
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx (1)

673-686: 在编辑和克隆提供商后需要同时刷新 provider-vendors 缓存。

当前编辑和克隆的 onSuccess 回调中缺少 queryClient.invalidateQueries({ queryKey: ["provider-vendors"] }) 调用。删除提供商时已包含此缓存失效操作,而编辑/克隆应遵循相同模式以保持缓存一致性。

建议修改
               onSuccess={() => {
                 setOpenEdit(false);
                 queryClient.invalidateQueries({ queryKey: ["providers"] });
                 queryClient.invalidateQueries({ queryKey: ["providers-health"] });
+                queryClient.invalidateQueries({ queryKey: ["provider-vendors"] });
                 router.refresh();
               }}
               onSuccess={() => {
                 setOpenClone(false);
                 queryClient.invalidateQueries({ queryKey: ["providers"] });
                 queryClient.invalidateQueries({ queryKey: ["providers-health"] });
+                queryClient.invalidateQueries({ queryKey: ["provider-vendors"] });
                 router.refresh();
               }}
src/app/[locale]/settings/providers/_components/provider-list-item.legacy.tsx (3)

128-143: 硬编码的中文字符串违反 i18n 规范。

根据编码规范,所有面向用户的字符串必须使用 i18n(支持 5 种语言:zh-CN、zh-TW、en、ja、ru)。此处的 toast 消息应使用翻译函数。

示例:

  • "熔断器已重置"t("circuitBreaker.resetSuccess")
  • "供应商 "{item.name}" 的熔断状态已解除"t("circuitBreaker.resetSuccessDesc", { name: item.name })

210-217: 禁止在代码中使用 emoji 字符。

根据编码规范,不得在代码、注释或字符串字面量中使用 emoji 字符。🔴 应替换为非 emoji 的状态指示器(如图标组件或 CSS 样式)。

建议修复
-                <Badge variant="destructive" className="text-xs h-5 px-2">
-                  🔴 熔断中
+                <Badge variant="destructive" className="text-xs h-5 px-2 gap-1">
+                  <span className="h-2 w-2 rounded-full bg-destructive-foreground" />
+                  {t("circuitBreaker.open")}

256-263: 同样存在 emoji 和硬编码字符串问题。

🟡 恢复中 同样违反了禁止使用 emoji 和必须使用 i18n 的规范。

messages/en/settings/providers/form/sections.json (1)

252-254: 使用了 emoji 字符 "✓",违反编码规范。

根据编码规范,不应在代码、注释或字符串字面量中使用 emoji 字符。Line 253 的 "allowAll": "✓ Allow all models (recommended)" 包含了 emoji 勾号。

建议的修复方案
-      "allowAll": "✓ Allow all models (recommended)",
+      "allowAll": "Allow all models (recommended)",

或者使用文本标记:

-      "allowAll": "✓ Allow all models (recommended)",
+      "allowAll": "[Default] Allow all models (recommended)",
🤖 Fix all issues with AI agents
In `@src/app/`[locale]/dashboard/availability/_components/endpoint-probe-row.tsx:
- Around line 119-128: The Probe Button in the EndpointProbeRow component is
missing an accessibility label; add aria-label={t("probeNow")} to the Button
element (the one using onClick={handleProbe}, disabled={probing} and rendering
<Play .../>), and ensure the translation function t is imported or available in
the component scope (e.g., via useTranslation or passed props) so the aria-label
resolves correctly for screen readers and keyboard users.
- Around line 67-89: Summary: Hard-coded badge texts ("N/A", "OK", "ERR") and
tooltip strings ("OK", "Error") must be localized. Update the JSX that renders
the Badge when probeStatus (variable) is null/true/false in the
endpoint-probe-row component to use the i18n translation function (e.g.,
t('availability.na'), t('availability.ok'), t('availability.err')) instead of
raw strings, and similarly replace the literal "OK" and "Error" in
probe-cell-tooltip (referenced symbol: the tooltip render at line ~55) with
corresponding translation keys; ensure the component imports/uses the project's
translation hook (useTranslation or t) and add the new keys to the locale files.

In
`@src/app/`[locale]/dashboard/availability/_components/endpoint-url-display.tsx:
- Around line 110-112: The truncation math in endpoint-url-display.tsx can
produce a negative slice end when availableForPath < 3, causing incorrect
outputs; update the logic around truncatedSegment and lastSegment so you clamp
the slice length to a safe non-negative (and preferably at least 1) value before
slicing (e.g., compute const sliceLen = Math.max(0, availableForPath - 3) or use
Math.max(1, ...) if you want at least one visible char), then slice lastSegment
with that clamped value and append "..." only when truncation actually occurred;
ensure the final returned string still respects the intended maxLength by
checking if lastSegment.length <= sliceLen and return host/ellipsis/lastSegment
unmodified in that case.

In `@src/app/`[locale]/dashboard/availability/_components/probe-cell-tooltip.tsx:
- Around line 58-62: The latency display currently uses a truthy check
({probe.latencyMs && ...}) which hides 0ms; update the conditional in
probe-cell-tooltip.tsx to render when latency is present even if zero by using
an explicit null/undefined check (e.g., probe.latencyMs !== undefined &&
probe.latencyMs !== null or probe.latencyMs != null) around the JSX that renders
probe.latencyMs so 0 is shown as "0ms".
- Around line 20-27: The date formatting uses toLocaleString(undefined, ...)
which follows the browser locale; update formatTime in probe-cell-tooltip.tsx to
use the app locale from next-intl by importing and calling useLocale() and
passing that locale into toLocaleString (e.g. toLocaleString(locale, {...})); if
formatTime is defined outside the component, change it to accept a locale
parameter or move it inside the component so you can call useLocale() directly;
ensure you add the import { useLocale } from "next-intl" and propagate the
locale into formatTime to guarantee the app's language is used.
- Around line 53-56: Replace the hardcoded visible strings "OK" and "Error" in
the status span with i18n translation calls; in the probe-cell-tooltip component
(the span that currently renders {probe.statusCode ?? (probe.ok ? "OK" :
"Error")}) use the translation helper t(...) for the fallback values (e.g.
t("status.ok") and t("status.error") or your project's equivalent keys) so the
user-visible status text is localized.

In
`@src/app/`[locale]/settings/providers/_components/forms/v2/provider-form-v2.tsx:
- Around line 148-171: The state initializers for rate-limit fields (limit5hUsd,
limitDailyUsd, dailyResetMode, dailyResetTime, limitWeeklyUsd, limitMonthlyUsd,
limitTotalUsd, limitConcurrentSessions) use `isEdit ? provider?.field || null :
null`, which treats 0 as falsy and converts valid zeros to null; change these
initializations to use nullish coalescing or an explicit undefined check (e.g.
`isEdit ? (provider?.limit5hUsd ?? null) : null` or `isEdit ?
(provider?.limit5hUsd !== undefined ? provider.limit5hUsd : null) : null`) so
zero values from provider are preserved; apply the same fix for each referenced
state variable and keep dailyResetMode/dailyResetTime defaulting to
"rolling"/"00:00" only when provider values are truly undefined.

In
`@src/app/`[locale]/settings/providers/_components/forms/v2/tabs/advanced-tab.tsx:
- Around line 123-142: The Select currently emits "true"/"false" which
mismatches the CodexReasoningSummaryPreference type (allowed: "inherit" | "auto"
| "detailed"); update the SelectItems in the component that uses
codexReasoningSummaryPreference and setCodexReasoningSummaryPreference to use
values "auto" and "detailed" (keep "inherit") and swap the displayed labels to
the correct translation keys (replace tCommon("enabled")/("disabled") with the
appropriate strings for "auto" and "detailed"); ensure the Select value types
match CodexReasoningSummaryPreference so serialization/save logic works
correctly.

In
`@src/app/`[locale]/settings/providers/_components/forms/v2/tabs/general-tab.tsx:
- Around line 66-71: The default providerTypes array is missing "claude-auth"
and "gemini-cli" causing inconsistency with ProviderType and other components;
update the fallback used to compute providerTypes (the expression using
allowedProviderTypes and providerTypes) to include all six types: "claude",
"claude-auth", "gemini", "gemini-cli", "codex", and "openai-compatible" so it
matches the ProviderType definition, renderProviderTypeLabel logic, and the
PROVIDER_TYPES constant in provider-form-v2.tsx.

In
`@src/app/`[locale]/settings/providers/_components/forms/v2/tabs/routing-tab.tsx:
- Around line 119-126: The onChange handler for the Input in routing-tab.tsx
uses parseFloat(e.target.value) || 1 which converts a valid 0 to 1; change the
handler to parse the value and only fall back to 1 when the parsed result is NaN
(or empty), e.g. compute const v = parseFloat(e.target.value); then call
setCostMultiplier(Number.isFinite(v) ? v : 1) (reference the costMultiplier
state and setCostMultiplier function used in the Input onChange).

In `@src/app/api/availability/endpoints/probe-logs/batch/route.ts`:
- Around line 1-52: The GET handler currently uses NextRequest/NextResponse and
must be refactored to Hono: replace the exported async GET(request: NextRequest)
with a Hono handler that accepts ctx (Context) and uses ctx.req and ctx.json for
responses; keep and reuse getSession(), findProviderEndpointProbeLogsBatch(),
and the same validation logic for endpointIds and limit, but return errors with
ctx.json(..., status) instead of NextResponse.json and log errors via
console.error before returning a 500. Follow the Hono + OpenAPIHono pattern used
in the project (e.g. use ctx.getQuery or new URL(ctx.req.url).searchParams for
params), ensure the response body shape ({ logs }) is preserved and convert the
Map to a plain object as before, and export the Hono-compatible handler so the
route conforms to the repository standard.

In `@src/app/api/availability/endpoints/route.ts`:
- Around line 1-61: The route currently implements an exported GET using
NextRequest/NextResponse (functions: GET, isProviderType, PROVIDER_TYPES,
getSession, findAllProviderEndpointsWithVendorInfo,
findProviderEndpointsByVendorAndType) but must be converted to a Hono-style
handler per project conventions; replace the exported GET with a Hono
app/handler (use OpenAPIHono and app.handle or the pattern used in
src/app/api/actions/[...route]/route.ts) and adapt request/session handling to
Hono's Context (ctx.req, ctx.json, ctx.text) while keeping the same validation
logic (vendorId/vendorIdRaw, providerTypeRaw, isProviderType) and error
branches, and ensure you export the Hono handler via handle() so the route
registers under the OpenAPIHono pattern.
♻️ Duplicate comments (6)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (2)

246-246: providerTypes 缺少部分类型。 当前列表未包含 claude-authgemini-cli,与 ProviderType 取值不一致。

Proposed fix
-  const providerTypes: ProviderType[] = ["claude", "gemini", "codex", "openai-compatible"];
+  const providerTypes: ProviderType[] = [
+    "claude",
+    "claude-auth",
+    "gemini",
+    "gemini-cli",
+    "codex",
+    "openai-compatible",
+  ];

483-485: 相对时间未传 locale 导致多语言失效。 formatDistanceToNow 默认输出英文,需按当前应用语言映射 date-fns locale。依据编码规范。

src/repository/provider-endpoints.ts (1)

334-349: deleteProviderVendor 与 FK restrict 语义不一致,存在误删风险。
providers.providerVendorId 为 onDelete: restrict,这里直接物理删除 providers 可能绕过软删除语义并造成数据丢失。建议改为软删除 providers,或将 FK 调整为 cascade 并与业务语义一致。

建议修改
 export async function deleteProviderVendor(vendorId: number): Promise<boolean> {
   const deleted = await db.transaction(async (tx) => {
     // 1. Delete endpoints (cascade would handle this, but manual is safe)
     await tx.delete(providerEndpoints).where(eq(providerEndpoints.vendorId, vendorId));
-    // 2. Delete providers (keys) - explicit delete required due to 'restrict'
-    await tx.delete(providers).where(eq(providers.providerVendorId, vendorId));
+    // 2. Soft delete providers to respect FK restrict semantics
+    const now = new Date();
+    await tx
+      .update(providers)
+      .set({ deletedAt: now, isEnabled: false, updatedAt: now })
+      .where(and(eq(providers.providerVendorId, vendorId), isNull(providers.deletedAt)));
+    // 3. Hard delete already soft-deleted providers
+    await tx
+      .delete(providers)
+      .where(and(eq(providers.providerVendorId, vendorId), isNotNull(providers.deletedAt)));
     // 3. Delete vendor
     const result = await tx
       .delete(providerVendors)
       .where(eq(providerVendors.id, vendorId))
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx (3)

50-56: 默认名称回退值仍为硬编码。
回退值 "vendor" 会进入用户可见默认名称,应通过 i18n 提供并由调用方传入。As per coding guidelines, 所有用户可见文本需使用 i18n。


413-425: 倍率后缀仍为硬编码。
suffix="x" 与展示文本需要可翻译。As per coding guidelines, 所有用户可见文本需使用 i18n。


487-507: 图标按钮与开关缺少 aria-label。
Switch、删除按钮、复制按钮在无可见文本时需提供 aria-label,并使用 i18n 文案。As per coding guidelines, 所有用户可见文本需使用 i18n。

Also applies to: 558-563

🧹 Nitpick comments (10)
src/app/[locale]/settings/providers/_components/forms/v2/tabs/routing-tab.tsx (1)

9-11: 建议使用 @/ 路径别名替代深层相对路径。
当前相对路径较深,可读性与重构成本偏高,建议按规范改为 @/ 引用。As per coding guidelines, ...

建议修改
-import { ModelMultiSelect } from "../../../model-multi-select";
-import { ModelRedirectEditor } from "../../../model-redirect-editor";
-import { useProviderForm } from "../provider-form-context";
+import { ModelMultiSelect } from "@/app/[locale]/settings/providers/_components/model-multi-select";
+import { ModelRedirectEditor } from "@/app/[locale]/settings/providers/_components/model-redirect-editor";
+import { useProviderForm } from "@/app/[locale]/settings/providers/_components/forms/v2/provider-form-context";
src/app/[locale]/settings/providers/_components/forms/v2/tabs/test-tab.tsx (1)

32-41: 避免直接断言 providerType 为 ProviderType。
ProviderFormState 允许 string,直接断言可能把空值/未知值传入 ApiTestButton,导致测试请求参数不合法。建议在此处加类型守卫或在上下文层收窄类型,并在无效时禁用按钮。

src/repository/provider-endpoints.ts (1)

603-665: 更新 URL 时建议做 trim + 校验,避免无效值或隐性重复。
目前 update 直接写入 payload.url,和 create/ensure 的校验策略不一致,可能引入无效 URL 或前后空格导致的重复记录。建议在更新时统一 trim 并校验。

建议修改
   const now = new Date();
   const updates: Partial<typeof providerEndpoints.$inferInsert> = { updatedAt: now };
-  if (payload.url !== undefined) updates.url = payload.url;
+  if (payload.url !== undefined) {
+    const trimmedUrl = payload.url.trim();
+    // eslint-disable-next-line no-new
+    new URL(trimmedUrl);
+    updates.url = trimmedUrl;
+  }
messages/zh-TW/settings/providers/form/sections.json (1)

97-106: mcpPassthrough.type 与 mcpPassthrough.select 存在重复。

mcpPassthrough.type (lines 97-103) 的内容与 mcpPassthrough.select (lines 70-89) 中的标签高度重复。建议确认这是否为不同 UI 组件的需要,如果可以复用,考虑简化翻译结构。

src/app/[locale]/settings/providers/_components/forms/v2/provider-form-layout.tsx (1)

55-78: "Resilience" 和 "Test" 标签页使用了相同的 Activity 图标。

第 60 行和第 76 行都使用了 <Activity /> 图标。考虑为 "Test" 标签页使用不同的图标以增强视觉区分度,例如 FlaskConicalTestTube2

建议修复
-import { Activity, Banknote, Bot, Settings2, Sliders } from "lucide-react";
+import { Activity, Banknote, Bot, FlaskConical, Settings2, Sliders } from "lucide-react";
               <TabsTrigger
                 id="provider-form-tab-test"
                 value="test"
                 className="gap-2 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm"
               >
-                <Activity className="h-4 w-4" />
+                <FlaskConical className="h-4 w-4" />
                 <span className="hidden sm:inline">{t("tabs.test")}</span>
               </TabsTrigger>
src/app/[locale]/settings/providers/_components/forms/v2/tabs/advanced-tab.tsx (1)

58-89: 避免使用 any 以保留类型约束。
cacheTtlPreference/context1mPreference 的取值域已固定,建议显式联合类型(或导入类型别名)以恢复类型校验。请确认实际类型定义后调整。

建议修改
-            <Select value={cacheTtlPreference} onValueChange={(v: any) => setCacheTtlPreference(v)}>
+            <Select
+              value={cacheTtlPreference}
+              onValueChange={(v: "inherit" | "5m" | "1h") => setCacheTtlPreference(v)}
+            >
-            <Select
-              value={context1mPreference}
-              onValueChange={(v: any) => setContext1mPreference(v)}
-            >
+            <Select
+              value={context1mPreference}
+              onValueChange={(v: "inherit" | "force_enable" | "disabled") =>
+                setContext1mPreference(v)
+              }
+            >
src/app/[locale]/settings/providers/_components/forms/v2/provider-form-v2.tsx (3)

335-336: proxyUrl 缺少 URL 格式验证。

url 字段使用了 isValidUrl 进行验证(line 281-284),但 proxyUrl(line 335)在提交时未进行格式验证。虽然代理 URL 支持多种协议(HTTP、HTTPS、SOCKS4、SOCKS5),如果用户输入无效格式可能会导致运行时错误。

建议添加代理 URL 格式验证,或至少验证其为有效的 URL 结构。


436-449: 删除确认使用原生 confirm() 对话框。

使用浏览器原生 confirm() 对话框可以工作,但与应用的整体 UI 风格可能不一致。考虑使用自定义的确认对话框组件以保持 UX 一致性。


20-31: 使用 satisfies 确保 PROVIDER_TYPESProviderType 类型定义保持同步。

PROVIDER_TYPES 数组与 ProviderType 类型定义目前保持同步,但建议添加 satisfies ProviderType[] 注解,以实现编译时类型检查。这样,如果 ProviderType 类型定义中添加或删除了提供商类型,TypeScript 将强制要求同步更新此数组,防止运行时不匹配。

messages/en/settings/providers/form/sections.json (1)

97-106: mcpPassthrough.type 与现有 mcpPassthrough.select 存在重复。

与俄语文件相同的问题:新增的 mcpPassthrough.type 对象与现有的 mcpPassthrough.select 对象存在重复的选项定义。请确认是否需要两组键,或者可以合并以减少维护成本。

Comment on lines +67 to +89
{probeStatus === null ? (
<Badge
variant="outline"
className="text-[10px] px-1.5 py-0.5 bg-gray-100 text-gray-500 border-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-700"
>
N/A
</Badge>
) : probeStatus ? (
<Badge
variant="outline"
className="text-[10px] px-1.5 py-0.5 bg-green-100 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800"
>
<CheckCircle2 className="w-3 h-3 mr-0.5" />
OK
</Badge>
) : (
<Badge
variant="outline"
className="text-[10px] px-1.5 py-0.5 bg-red-100 text-red-700 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800"
>
<XCircle className="w-3 h-3 mr-0.5" />
ERR
</Badge>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the file and check its existence
fd -t f "endpoint-probe-row.tsx" --exec wc -l {}

Repository: ding113/claude-code-hub

Length of output: 147


🏁 Script executed:

# Read the actual file to see the code at specified lines
fd -t f "endpoint-probe-row.tsx" -x cat -n {} | head -100

Repository: ding113/claude-code-hub

Length of output: 4357


🏁 Script executed:

# Check i18n file structure
fd -t f -e json "\.json$" | grep -E "(en|zh-CN|ja)" | head -20

Repository: ding113/claude-code-hub

Length of output: 626


🏁 Script executed:

# Search for existing translation keys related to status and legend
rg -t json "status|legend" --max-count 5

Repository: ding113/claude-code-hub

Length of output: 26837


🏁 Script executed:

# Read the complete file since it's 131 lines
cat -n src/app/[locale]/dashboard/availability/_components/endpoint-probe-row.tsx

Repository: ding113/claude-code-hub

Length of output: 5437


🏁 Script executed:

# Search for the specific i18n namespace and keys in dashboard.json files
rg -t json "probeHistory|status\.na|legend\.(success|failure)|tooltip\.noData" messages/ -A 2

Repository: ding113/claude-code-hub

Length of output: 1117


🏁 Script executed:

# Check the dashboard.json structure for availability/probeHistory namespace
fd -t f "dashboard.json" -x grep -l "probeHistory" {}

Repository: ding113/claude-code-hub

Length of output: 217


🏁 Script executed:

# Search for the "-" placeholder mentioned in the review in the probe-related components
rg '"-"' src/app/[locale]/dashboard/availability/_components/ -A 2 -B 2

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Check probe-cell-tooltip to see if it has hardcoded strings
cat -n src/app/[locale]/dashboard/availability/_components/probe-cell-tooltip.tsx | head -150

Repository: ding113/claude-code-hub

Length of output: 3158


🏁 Script executed:

# Search for all hardcoded strings in the availability component directory
rg "N/A|OK|ERR" src/app/[locale]/dashboard/availability/_components/

Repository: ding113/claude-code-hub

Length of output: 473


🏁 Script executed:

# Check what keys exist in the probeHistory namespace for all languages
for file in messages/*/dashboard.json; do echo "=== $file ==="; rg -A 20 '"probeHistory"' "$file" | head -25; done

Repository: ding113/claude-code-hub

Length of output: 3418


🏁 Script executed:

# Search for tooltip.noData key in all i18n files
rg "tooltip\.noData" messages/ -B 2 -A 2

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Get the full probeHistory namespace from one file to see all available keys
cat messages/en/dashboard.json | jq '.availability.probeHistory' 2>/dev/null || rg -A 50 '"probeHistory"' messages/en/dashboard.json | head -60

Repository: ding113/claude-code-hub

Length of output: 1139


徽章状态文本需 i18nN/AOKERR 为硬编码用户可见文本,需替换为 i18n 翻译键。同时 probe-cell-tooltip.tsx 第 55 行的 OKError 也需本地化。

修复方案

endpoint-probe-row.tsx 第 67-90 行:

-            N/A
+            {t("legend.noData")}
-            OK
+            {t("legend.success")}
-            ERR
+            {t("legend.failure")}

probe-cell-tooltip.tsx 第 55 行:

-          {probe.statusCode ?? (probe.ok ? "OK" : "Error")}
+          {probe.statusCode ?? (probe.ok ? t("legend.success") : t("legend.failure"))}
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/availability/_components/endpoint-probe-row.tsx
around lines 67 - 89, Summary: Hard-coded badge texts ("N/A", "OK", "ERR") and
tooltip strings ("OK", "Error") must be localized. Update the JSX that renders
the Badge when probeStatus (variable) is null/true/false in the
endpoint-probe-row component to use the i18n translation function (e.g.,
t('availability.na'), t('availability.ok'), t('availability.err')) instead of
raw strings, and similarly replace the literal "OK" and "Error" in
probe-cell-tooltip (referenced symbol: the tooltip render at line ~55) with
corresponding translation keys; ensure the component imports/uses the project's
translation hook (useTranslation or t) and add the new keys to the locale files.

Comment on lines +119 to +128
{/* Probe Button */}
<Button
variant="ghost"
size="icon"
className="shrink-0 h-7 w-7"
onClick={handleProbe}
disabled={probing}
>
<Play className={cn("h-3.5 w-3.5", probing && "animate-pulse")} />
</Button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, verify the file exists and examine the content at the specified lines
cat -n src/app/[locale]/dashboard/availability/_components/endpoint-probe-row.tsx | sed -n '115,135p'

Repository: ding113/claude-code-hub

Length of output: 520


🏁 Script executed:

# Check the file structure around the Button component
head -130 src/app/[locale]/dashboard/availability/_components/endpoint-probe-row.tsx | tail -30

Repository: ding113/claude-code-hub

Length of output: 927


🏁 Script executed:

# Look for other Button components with aria-label in similar files
fd -e tsx -e ts . src/app/[locale]/dashboard/availability/_components --exec grep -l "aria-label" {} \;

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Check for i18n translation patterns and the "probeNow" key
rg "probeNow|aria-label.*t\(" src/app/[locale]/dashboard/availability/ -A 2

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Check the i18n configuration and available translations
fd -e json . --path "*locale*" --path "*translation*" --path "*i18n*" | head -20

Repository: ding113/claude-code-hub

Length of output: 300


🏁 Script executed:

# Search for aria-label usage more broadly across the codebase
rg "aria-label" src/ -A 1 -B 1 | head -40

Repository: ding113/claude-code-hub

Length of output: 3800


🏁 Script executed:

# Check the imports in the target file to understand i18n setup
head -30 src/app/[locale]/dashboard/availability/_components/endpoint-probe-row.tsx

Repository: ding113/claude-code-hub

Length of output: 1352


🏁 Script executed:

# Find translation/i18n files
find src -type f -name "*message*" -o -name "*translation*" -o -name "*locale*" | grep -E "\.(json|ts|tsx)$" | head -20

Repository: ding113/claude-code-hub

Length of output: 1081


🏁 Script executed:

# Look for the t() function usage pattern in the codebase
rg "t\(" src/ -A 1 | head -50

Repository: ding113/claude-code-hub

Length of output: 2822


🏁 Script executed:

# Search for similar icon buttons to see accessibility patterns
rg "Play|Icon.*Button|icon.*size" src/app/[locale]/dashboard/ -B 2 -A 2 | head -60

Repository: ding113/claude-code-hub

Length of output: 3304


🏁 Script executed:

# Find translation/messages files
find src -type f \( -name "*.json" -o -name "*message*" -o -name "*locale*" \) | grep -v node_modules | head -30

Repository: ding113/claude-code-hub

Length of output: 1113


🏁 Script executed:

# Look for probe-related translations and keys
rg "probe|Probe" src -l | grep -E "\.(json|ts)$" | head -20

Repository: ding113/claude-code-hub

Length of output: 777


🏁 Script executed:

# Check the messages directory structure
find src -type d -name "messages" -o -name "locales" -o -name "i18n" 2>/dev/null

Repository: ding113/claude-code-hub

Length of output: 132


🏁 Script executed:

# Search for existing translation patterns in the dashboard.availability namespace
rg "dashboard\.availability" src -A 2 -B 2 | head -60

Repository: ding113/claude-code-hub

Length of output: 2407


🏁 Script executed:

# Examine i18n directory structure
ls -la src/i18n/

Repository: ding113/claude-code-hub

Length of output: 481


🏁 Script executed:

# Look for messages directory and JSON files
find src/i18n -type f | head -30

Repository: ding113/claude-code-hub

Length of output: 162


🏁 Script executed:

# Check English and Chinese translation files for dashboard.availability
find src/i18n -name "*.json" -exec grep -l "dashboard\|availability" {} \;

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Look for dashboard availability translations specifically
find src/i18n -type f \( -name "*.json" -o -name "*.ts" -o -name "*.js" \) -exec grep -l "probeHistory\|probeNow\|probe" {} \;

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Check the i18n config to understand how translations are structured
cat src/i18n/config.ts

Repository: ding113/claude-code-hub

Length of output: 860


🏁 Script executed:

# Look for messages directory anywhere in the codebase
find . -type d -name "messages" 2>/dev/null | grep -v node_modules

Repository: ding113/claude-code-hub

Length of output: 136


🏁 Script executed:

# Search for where translations are actually stored
rg "probeHistory|dashboard" --type json | head -20

Repository: ding113/claude-code-hub

Length of output: 1634


🏁 Script executed:

# Check if translations are embedded in the code or external
grep -r "useTranslations\|next-intl" src/i18n/ src/app/[locale]/ | head -10

Repository: ding113/claude-code-hub

Length of output: 808


🏁 Script executed:

# Look for JSON files that might contain translations
find . -name "*.json" -path "*/messages/*" -o -name "*.json" -path "*/locales/*" -o -name "*.json" -path "*/translations/*" 2>/dev/null | grep -v node_modules | head -20

Repository: ding113/claude-code-hub

Length of output: 706


🏁 Script executed:

# Check the English dashboard.json for probeHistory keys
cat messages/en/dashboard.json | grep -A 50 "probeHistory"

Repository: ding113/claude-code-hub

Length of output: 1638


🏁 Script executed:

# Check Chinese simplified (zh-CN) dashboard.json for probeHistory
cat messages/zh-CN/dashboard.json | grep -A 50 "probeHistory"

Repository: ding113/claude-code-hub

Length of output: 1290


🏁 Script executed:

# Search for "probeNow" key across all message files
rg "probeNow" messages/

Repository: ding113/claude-code-hub

Length of output: 350


🏁 Script executed:

# Look for probe-related keys in the dashboard files
rg "probe" messages/en/dashboard.json messages/zh-CN/dashboard.json -A 1 -B 1

Repository: ding113/claude-code-hub

Length of output: 1893


为探测按钮添加无障碍标签 aria-label属性缺失。应为这个图标按钮补充aria-label={t("probeNow")},便于屏幕阅读器用户和键盘导航。

修改建议
       <Button
         variant="ghost"
         size="icon"
         className="shrink-0 h-7 w-7"
         onClick={handleProbe}
         disabled={probing}
+        aria-label={t("probeNow")}
       >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{/* Probe Button */}
<Button
variant="ghost"
size="icon"
className="shrink-0 h-7 w-7"
onClick={handleProbe}
disabled={probing}
>
<Play className={cn("h-3.5 w-3.5", probing && "animate-pulse")} />
</Button>
{/* Probe Button */}
<Button
variant="ghost"
size="icon"
className="shrink-0 h-7 w-7"
onClick={handleProbe}
disabled={probing}
aria-label={t("probeNow")}
>
<Play className={cn("h-3.5 w-3.5", probing && "animate-pulse")} />
</Button>
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/availability/_components/endpoint-probe-row.tsx
around lines 119 - 128, The Probe Button in the EndpointProbeRow component is
missing an accessibility label; add aria-label={t("probeNow")} to the Button
element (the one using onClick={handleProbe}, disabled={probing} and rendering
<Play .../>), and ensure the translation function t is imported or available in
the component scope (e.g., via useTranslation or passed props) so the aria-label
resolves correctly for screen readers and keyboard users.

Comment on lines +110 to +112
// Truncate the last segment
const truncatedSegment = `${lastSegment.slice(0, availableForPath - 3)}...`;
return `${host}${ellipsis}/${truncatedSegment}`;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

availableForPath 值较小时,截断逻辑存在边界问题

availableForPath 小于 3 时,availableForPath - 3 会产生负数。JavaScript 的 slice(0, -n) 会保留除最后 n 个字符以外的所有内容,这会导致:

  1. 截断结果可能超出 maxLength 限制
  2. 对于极短的 segment,可能产生空字符串加 ... 的无意义输出
建议修复
    // Truncate the last segment
-   const truncatedSegment = `${lastSegment.slice(0, availableForPath - 3)}...`;
-   return `${host}${ellipsis}/${truncatedSegment}`;
+   if (availableForPath < 4) {
+     // Not enough space for meaningful truncation, just show host with ellipsis
+     return `${host}${ellipsis}`;
+   }
+   const truncatedSegment = `${lastSegment.slice(0, availableForPath - 3)}...`;
+   return `${host}${ellipsis}/${truncatedSegment}`;
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/availability/_components/endpoint-url-display.tsx
around lines 110 - 112, The truncation math in endpoint-url-display.tsx can
produce a negative slice end when availableForPath < 3, causing incorrect
outputs; update the logic around truncatedSegment and lastSegment so you clamp
the slice length to a safe non-negative (and preferably at least 1) value before
slicing (e.g., compute const sliceLen = Math.max(0, availableForPath - 3) or use
Math.max(1, ...) if you want at least one visible char), then slice lastSegment
with that clamped value and append "..." only when truncation actually occurred;
ensure the final returned string still respects the intended maxLength by
checking if lastSegment.length <= sliceLen and return host/ellipsis/lastSegment
unmodified in that case.

Comment on lines +20 to +27
const formatTime = (date: Date) => {
return new Date(date).toLocaleString(undefined, {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and read the relevant lines
fd "probe-cell-tooltip.tsx" && echo "File found" || echo "File not found"

Repository: ding113/claude-code-hub

Length of output: 152


🏁 Script executed:

# Read the file to verify the code snippet
cat -n src/app/\[locale\]/dashboard/availability/_components/probe-cell-tooltip.tsx | head -40

Repository: ding113/claude-code-hub

Length of output: 1407


🏁 Script executed:

# Check for next-intl imports and useLocale usage patterns in the project
rg "useLocale" --type ts --type tsx -A 2 -B 2 | head -50

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# Search for useLocale usage patterns without explicit type flags
rg "useLocale" -A 2 -B 2 | head -60

Repository: ding113/claude-code-hub

Length of output: 4752


🏁 Script executed:

# Check other dashboard components for locale usage patterns
rg "toLocaleString\|useLocale\|useFormatter" src/app/\[locale\]/dashboard -A 1 -B 1 | head -40

Repository: ding113/claude-code-hub

Length of output: 49


时间格式应使用当前应用语言环境。 toLocaleString(undefined, …) 会跟随浏览器语言,可能与应用语言不一致。建议使用 next-intluseLocale() 获取应用当前语言环境。

建议的修复
-import { useTranslations } from "next-intl";
+import { useLocale, useTranslations } from "next-intl";
@@
 export function ProbeCellTooltip({ probe }: ProbeCellTooltipProps) {
   const t = useTranslations("dashboard.availability.probeHistory");
+  const locale = useLocale();
@@
   const formatTime = (date: Date) => {
     return new Date(date).toLocaleString(locale, {
       month: "short",
       day: "numeric",
       hour: "2-digit",
       minute: "2-digit",
       second: "2-digit",
     });
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const formatTime = (date: Date) => {
return new Date(date).toLocaleString(undefined, {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
const formatTime = (date: Date) => {
return new Date(date).toLocaleString(locale, {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
};
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/availability/_components/probe-cell-tooltip.tsx
around lines 20 - 27, The date formatting uses toLocaleString(undefined, ...)
which follows the browser locale; update formatTime in probe-cell-tooltip.tsx to
use the app locale from next-intl by importing and calling useLocale() and
passing that locale into toLocaleString (e.g. toLocaleString(locale, {...})); if
formatTime is defined outside the component, change it to accept a locale
parameter or move it inside the component so you can call useLocale() directly;
ensure you add the import { useLocale } from "next-intl" and propagate the
locale into formatTime to guarantee the app's language is used.

Comment on lines +53 to +56
<span className="text-muted-foreground">{t("columns.status")}:</span>
<span className={probe.ok ? "text-green-500" : "text-red-500"}>
{probe.statusCode ?? (probe.ok ? "OK" : "Error")}
</span>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and read the probe-cell-tooltip.tsx file
fd "probe-cell-tooltip.tsx" -t f

Repository: ding113/claude-code-hub

Length of output: 190


🏁 Script executed:

# Search for the file and get its full content
find . -name "probe-cell-tooltip.tsx" -type f | head -1 | xargs wc -l

Repository: ding113/claude-code-hub

Length of output: 146


🏁 Script executed:

# Read the file to understand the context
cat -n src/app/*/dashboard/availability/_components/probe-cell-tooltip.tsx 2>/dev/null || cat -n src/app/dashboard/availability/_components/probe-cell-tooltip.tsx 2>/dev/null || find . -name "probe-cell-tooltip.tsx" -type f | head -1 | xargs cat -n

Repository: ding113/claude-code-hub

Length of output: 3158


🏁 Script executed:

# Search for translation files to check if "success" and "columns.error" keys exist
find . -name "*.json" -path "*/locales/*" -o -name "*.json" -path "*/i18n/*" -o -name "*.json" -path "*/translations/*" | head -20

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Look for i18n configuration and usage patterns
fd -e "ts" -e "tsx" -e "js" -e "jsx" | xargs grep -l "useTranslation\|i18n" | head -10

Repository: ding113/claude-code-hub

Length of output: 569


🏁 Script executed:

# Search for translation JSON files
find . -type f \( -name "*.json" -o -name "*.ts" -o -name "*.js" \) | grep -iE "(locale|i18n|translation)" | head -20

Repository: ding113/claude-code-hub

Length of output: 1280


🏁 Script executed:

# Look for locales directory structure
find . -type d -name "locales" -o -type d -name "i18n" -o -type d -name "messages" | head -10

Repository: ding113/claude-code-hub

Length of output: 165


🏁 Script executed:

# Search for where "columns.status" and related keys are defined
rg '"columns\.' -A 2 -B 2 | head -50

Repository: ding113/claude-code-hub

Length of output: 5714


🏁 Script executed:

# Check for "OK" or "Error" translations in the codebase
rg '"OK"|"Error"' -t ts -t tsx -t json | head -20

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# Look for existing status-related translation keys
rg 'success|error' --type json -i | grep -E '(status|ok|error)' | head -30

Repository: ding113/claude-code-hub

Length of output: 2325


🏁 Script executed:

# Let's look at the messages directory structure and find locale files
find ./messages -type f -name "*.json" | head -20

Repository: ding113/claude-code-hub

Length of output: 706


🏁 Script executed:

# Check the main dashboard translation file
cat ./messages/en/dashboard.json | grep -A 5 -B 5 "columns\|success\|status" | head -100

Repository: ding113/claude-code-hub

Length of output: 3223


🏁 Script executed:

# Look for what translation namespace this component uses - "dashboard.availability.probeHistory"
find ./messages -type f -name "*.json" -exec grep -l "probeHistory\|probe" {} \;

Repository: ding113/claude-code-hub

Length of output: 488


🏁 Script executed:

# Check if there's a availability-specific translation file
find ./messages -type f \( -name "*availability*" -o -name "*probe*" \)

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Look at the structure of dashboard.json to understand translation hierarchy
python3 << 'EOF'
import json
import os

# Find all message files
message_files = []
for root, dirs, files in os.walk('./messages'):
    for file in files:
        if file.endswith('.json'):
            message_files.append(os.path.join(root, file))

# Check en/dashboard.json specifically
en_dashboard = './messages/en/dashboard.json'
if os.path.exists(en_dashboard):
    with open(en_dashboard, 'r') as f:
        data = json.load(f)
    
    # Look for the probeHistory key
    if 'availability' in data:
        if 'probeHistory' in data['availability']:
            print("Found probeHistory:")
            print(json.dumps(data['availability']['probeHistory'], indent=2))
        else:
            print("No probeHistory found. Available availability keys:")
            print(json.dumps(list(data['availability'].keys())))
    else:
        print("No availability found. Available top-level keys:")
        print(json.dumps(list(data.keys()), indent=2))
EOF

Repository: ding113/claude-code-hub

Length of output: 1159


🏁 Script executed:

# Search for existing "columns.error" usage in the codebase
rg 'columns\.error|"error"' ./messages -t json -C 2

Repository: ding113/claude-code-hub

Length of output: 44905


用户可见状态文本需要使用 i18n 翻译。 行 55 中的 "OK""Error" 是硬编码的用户可见字符串,违反了国际化规范。应使用对应的翻译键替代。

修复方案
-        <span className={probe.ok ? "text-green-500" : "text-red-500"}>
-          {probe.statusCode ?? (probe.ok ? "OK" : "Error")}
-        </span>
+        <span className={probe.ok ? "text-green-500" : "text-red-500"}>
+          {probe.statusCode ?? (probe.ok ? t("success") : t("columns.error"))}
+        </span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span className="text-muted-foreground">{t("columns.status")}:</span>
<span className={probe.ok ? "text-green-500" : "text-red-500"}>
{probe.statusCode ?? (probe.ok ? "OK" : "Error")}
</span>
<span className="text-muted-foreground">{t("columns.status")}:</span>
<span className={probe.ok ? "text-green-500" : "text-red-500"}>
{probe.statusCode ?? (probe.ok ? t("success") : t("columns.error"))}
</span>
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/availability/_components/probe-cell-tooltip.tsx
around lines 53 - 56, Replace the hardcoded visible strings "OK" and "Error" in
the status span with i18n translation calls; in the probe-cell-tooltip component
(the span that currently renders {probe.statusCode ?? (probe.ok ? "OK" :
"Error")}) use the translation helper t(...) for the fallback values (e.g.
t("status.ok") and t("status.error") or your project's equivalent keys) so the
user-visible status text is localized.

Comment on lines +123 to +142
<div className="space-y-2">
<Label>{t("routing.codexOverrides.reasoningSummary.label")}</Label>
<Select
value={codexReasoningSummaryPreference}
onValueChange={(v: CodexReasoningSummaryPreference) =>
setCodexReasoningSummaryPreference(v)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="inherit">
{t("routing.codexOverrides.reasoningSummary.options.inherit")}
</SelectItem>
<SelectItem value="true">{tCommon("enabled")}</SelectItem>
<SelectItem value="false">{tCommon("disabled")}</SelectItem>
</SelectContent>
</Select>
</div>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

reasoningSummary 选项值与类型定义不一致。
CodexReasoningSummaryPreferencesrc/types/provider.ts 仅允许 inherit | auto | detailed,当前写入 true/false 会落入非法值并影响保存/序列化。建议改为 auto/detailed 选项并使用对应文案。

建议修改
-                <SelectItem value="true">{tCommon("enabled")}</SelectItem>
-                <SelectItem value="false">{tCommon("disabled")}</SelectItem>
+                <SelectItem value="auto">
+                  {t("routing.codexOverrides.reasoningSummary.options.auto")}
+                </SelectItem>
+                <SelectItem value="detailed">
+                  {t("routing.codexOverrides.reasoningSummary.options.detailed")}
+                </SelectItem>
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/forms/v2/tabs/advanced-tab.tsx
around lines 123 - 142, The Select currently emits "true"/"false" which
mismatches the CodexReasoningSummaryPreference type (allowed: "inherit" | "auto"
| "detailed"); update the SelectItems in the component that uses
codexReasoningSummaryPreference and setCodexReasoningSummaryPreference to use
values "auto" and "detailed" (keep "inherit") and swap the displayed labels to
the correct translation keys (replace tCommon("enabled")/("disabled") with the
appropriate strings for "auto" and "detailed"); ensure the Select value types
match CodexReasoningSummaryPreference so serialization/save logic works
correctly.

Comment on lines +66 to +71
const providerTypes = (allowedProviderTypes || [
"claude",
"gemini",
"codex",
"openai-compatible",
]) as ProviderType[];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/app/[locale]/settings/providers/_components/forms/v2/tabs/general-tab.tsx | head -80

Repository: ding113/claude-code-hub

Length of output: 2742


🏁 Script executed:

rg "ProviderType" --type=ts --type=tsx -B 2 -A 5 | head -100

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

rg "renderProviderTypeLabel" --type=ts --type=tsx -B 2 -A 15

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

rg "claude-auth|gemini-cli" --type=ts --type=tsx

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

cat src/types/provider.ts 2>/dev/null || cat src/types/provider.tsx 2>/dev/null || find src -name "provider.ts" -o -name "provider.tsx"

Repository: ding113/claude-code-hub

Length of output: 11834


🏁 Script executed:

rg "type ProviderType|enum ProviderType" -A 15

Repository: ding113/claude-code-hub

Length of output: 3481


🏁 Script executed:

rg "allowedProviderTypes" -B 3 -A 3

Repository: ding113/claude-code-hub

Length of output: 10279


🏁 Script executed:

rg "claude-auth|gemini-cli" -B 2 -A 2

Repository: ding113/claude-code-hub

Length of output: 50379


🏁 Script executed:

rg "allowedProviderTypes\s*\?" -B 5 -A 5 | head -50

Repository: ding113/claude-code-hub

Length of output: 4718


🏁 Script executed:

rg "PROVIDER_TYPES\|providerTypes\s*=" src/app/[locale]/settings/providers/_components/forms/v2/ -A 10

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

cat src/app/[locale]/settings/providers/_components/forms/v2/provider-form-v2.tsx | grep -A 10 "const PROVIDER_TYPES"

Repository: ding113/claude-code-hub

Length of output: 338


🏁 Script executed:

grep -n "const providerTypes\|const PROVIDER_TYPES" src/app/[locale]/settings/providers/_components/forms/v2/tabs/general-tab.tsx

Repository: ding113/claude-code-hub

Length of output: 120


默认 provider 类型列表不完整,应与其他组件保持一致。

默认数组 ["claude", "gemini", "codex", "openai-compatible"] 缺少 "claude-auth""gemini-cli"。这两个类型在 ProviderType 定义、renderProviderTypeLabel 处理函数和其他组件(如 provider-form-v2.tsx 中的 PROVIDER_TYPES 常量)中都被完整支持,应保持一致。建议改为包含全部 6 种类型。

🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/forms/v2/tabs/general-tab.tsx
around lines 66 - 71, The default providerTypes array is missing "claude-auth"
and "gemini-cli" causing inconsistency with ProviderType and other components;
update the fallback used to compute providerTypes (the expression using
allowedProviderTypes and providerTypes) to include all six types: "claude",
"claude-auth", "gemini", "gemini-cli", "codex", and "openai-compatible" so it
matches the ProviderType definition, renderProviderTypeLabel logic, and the
PROVIDER_TYPES constant in provider-form-v2.tsx.

Comment on lines +119 to +126
<Input
id="costMultiplier"
type="number"
step="0.01"
min={0}
value={costMultiplier}
onChange={(e) => setCostMultiplier(parseFloat(e.target.value) || 1)}
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "routing-tab.tsx" -type f

Repository: ding113/claude-code-hub

Length of output: 146


🏁 Script executed:

cat -n ./src/app/[locale]/settings/providers/_components/forms/v2/tabs/routing-tab.tsx | sed -n '110,135p'

Repository: ding113/claude-code-hub

Length of output: 1123


🏁 Script executed:

cat -n ./src/app/[locale]/settings/providers/_components/forms/v2/tabs/routing-tab.tsx | head -30

Repository: ding113/claude-code-hub

Length of output: 1163


🏁 Script executed:

cat -n ./src/app/[locale]/settings/providers/_components/forms/v2/tabs/routing-tab.tsx | sed -n '30,70p'

Repository: ding113/claude-code-hub

Length of output: 1568


costMultiplier 当前无法设置为 0。

parseFloat(...) || 1 会把合法的 0 变成 1,导致最小值 0 无法输入。

建议修改
-                onChange={(e) => setCostMultiplier(parseFloat(e.target.value) || 1)}
+                onChange={(e) => {
+                  const value = Number.parseFloat(e.target.value);
+                  setCostMultiplier(Number.isNaN(value) ? 1 : value);
+                }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Input
id="costMultiplier"
type="number"
step="0.01"
min={0}
value={costMultiplier}
onChange={(e) => setCostMultiplier(parseFloat(e.target.value) || 1)}
/>
<Input
id="costMultiplier"
type="number"
step="0.01"
min={0}
value={costMultiplier}
onChange={(e) => {
const value = Number.parseFloat(e.target.value);
setCostMultiplier(Number.isNaN(value) ? 1 : value);
}}
/>
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/forms/v2/tabs/routing-tab.tsx
around lines 119 - 126, The onChange handler for the Input in routing-tab.tsx
uses parseFloat(e.target.value) || 1 which converts a valid 0 to 1; change the
handler to parse the value and only fall back to 1 when the parsed result is NaN
(or empty), e.g. compute const v = parseFloat(e.target.value); then call
setCostMultiplier(Number.isFinite(v) ? v : 1) (reference the costMultiplier
state and setCostMultiplier function used in the Input onChange).

Comment on lines +1 to +52
import { type NextRequest, NextResponse } from "next/server";
import { getSession } from "@/lib/auth";
import { findProviderEndpointProbeLogsBatch } from "@/repository";

export async function GET(request: NextRequest) {
const session = await getSession();
if (!session || session.user.role !== "admin") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { searchParams } = new URL(request.url);

const endpointIdsRaw = searchParams.get("endpointIds");
const limitRaw = searchParams.get("limit");

if (!endpointIdsRaw) {
return NextResponse.json({ error: "endpointIds is required" }, { status: 400 });
}

const endpointIds = endpointIdsRaw
.split(",")
.map((id) => Number.parseInt(id.trim(), 10))
.filter((id) => Number.isFinite(id) && id > 0);

if (endpointIds.length === 0) {
return NextResponse.json({ error: "No valid endpoint IDs provided" }, { status: 400 });
}

// Limit to 100 endpoints per request
if (endpointIds.length > 100) {
return NextResponse.json({ error: "Too many endpoint IDs (max 100)" }, { status: 400 });
}

const limit = limitRaw ? Number.parseInt(limitRaw, 10) : 30;
if (!Number.isFinite(limit) || limit <= 0 || limit > 100) {
return NextResponse.json({ error: "Invalid limit (1-100)" }, { status: 400 });
}

try {
const logsMap = await findProviderEndpointProbeLogsBatch(endpointIds, limit);

// Convert Map to object for JSON serialization
const logs: Record<string, Array<unknown>> = {};
for (const [endpointId, probeLogs] of logsMap) {
logs[endpointId.toString()] = probeLogs;
}

return NextResponse.json({ logs });
} catch (error) {
console.error("Batch probe logs API error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find all API route files in src/app/api
fd -t f 'route\.(ts|tsx)$' src/app/api

Repository: ding113/claude-code-hub

Length of output: 906


🏁 Script executed:

# Search for Hono imports and usage patterns in API routes
rg -n -A 3 'from ["'"'"']hono|import.*Hono' src/app/api

Repository: ding113/claude-code-hub

Length of output: 608


🏁 Script executed:

# Check the specific file content and surrounding API routes for context
cat -n src/app/api/availability/endpoints/probe-logs/batch/route.ts

Repository: ding113/claude-code-hub

Length of output: 2276


🏁 Script executed:

# Check implementation of a few other API routes to see the pattern
head -20 src/app/api/availability/endpoints/probe-logs/route.ts

Repository: ding113/claude-code-hub

Length of output: 893


🏁 Script executed:

head -20 src/app/api/availability/endpoints/route.ts

Repository: ding113/claude-code-hub

Length of output: 579


🏁 Script executed:

head -20 src/app/api/admin/database/status/route.ts

Repository: ding113/claude-code-hub

Length of output: 606


🏁 Script executed:

# Search for all NextResponse usage in API routes
rg -n 'from "next/server"|NextResponse' src/app/api --type ts --type tsx | head -20

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# Search for NextResponse usage across API routes with proper type handling
rg -n 'NextResponse' src/app/api --glob '*.ts' --glob '*.tsx' | head -30

Repository: ding113/claude-code-hub

Length of output: 2794


🏁 Script executed:

# Count how many API routes use Hono vs NextResponse
echo "=== Hono usage ===" && \
rg -l 'from.*hono|import.*Hono' src/app/api --glob '*.ts' --glob '*.tsx' | wc -l && \
echo "=== NextResponse usage ===" && \
rg -l 'NextResponse' src/app/api --glob '*.ts' --glob '*.tsx' | wc -l

Repository: ding113/claude-code-hub

Length of output: 117


🏁 Script executed:

# Check the pattern in actions route which uses Hono
head -40 src/app/api/actions/[...route]/route.ts

Repository: ding113/claude-code-hub

Length of output: 1738


API 路由应使用 Hono 框架而非 NextResponse。

该路由不符合编码规范要求。根据仓库标准,src/app/api 下的所有路由需要使用 Hono 框架以保持一致性。参考 src/app/api/actions/[...route]/route.ts 中的 Hono + OpenAPIHono 实现模式进行重构。

🤖 Prompt for AI Agents
In `@src/app/api/availability/endpoints/probe-logs/batch/route.ts` around lines 1
- 52, The GET handler currently uses NextRequest/NextResponse and must be
refactored to Hono: replace the exported async GET(request: NextRequest) with a
Hono handler that accepts ctx (Context) and uses ctx.req and ctx.json for
responses; keep and reuse getSession(), findProviderEndpointProbeLogsBatch(),
and the same validation logic for endpointIds and limit, but return errors with
ctx.json(..., status) instead of NextResponse.json and log errors via
console.error before returning a 500. Follow the Hono + OpenAPIHono pattern used
in the project (e.g. use ctx.getQuery or new URL(ctx.req.url).searchParams for
params), ensure the response body shape ({ logs }) is preserved and convert the
Map to a plain object as before, and export the Hono-compatible handler so the
route conforms to the repository standard.

Comment on lines +1 to +61
import { type NextRequest, NextResponse } from "next/server";
import { getSession } from "@/lib/auth";
import {
findAllProviderEndpointsWithVendorInfo,
findProviderEndpointsByVendorAndType,
} from "@/repository";
import type { ProviderType } from "@/types/provider";

const PROVIDER_TYPES: ProviderType[] = [
"claude",
"claude-auth",
"codex",
"gemini-cli",
"gemini",
"openai-compatible",
];

function isProviderType(value: string | null): value is ProviderType {
if (!value) {
return false;
}
return PROVIDER_TYPES.includes(value as ProviderType);
}

export async function GET(request: NextRequest) {
const session = await getSession();
if (!session || session.user.role !== "admin") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { searchParams } = new URL(request.url);

const vendorIdRaw = searchParams.get("vendorId");
const providerTypeRaw = searchParams.get("providerType");

// If no filters provided, return all endpoints with vendor info
if (!vendorIdRaw && !providerTypeRaw) {
try {
const endpoints = await findAllProviderEndpointsWithVendorInfo();
return NextResponse.json({ endpoints });
} catch (error) {
console.error("Endpoint availability API error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}

// Original filtered query
const vendorId = vendorIdRaw ? Number.parseInt(vendorIdRaw, 10) : Number.NaN;

if (!Number.isFinite(vendorId) || vendorId <= 0 || !isProviderType(providerTypeRaw)) {
return NextResponse.json({ error: "Invalid query" }, { status: 400 });
}

try {
const endpoints = await findProviderEndpointsByVendorAndType(vendorId, providerTypeRaw);
return NextResponse.json({ vendorId, providerType: providerTypeRaw, endpoints });
} catch (error) {
console.error("Endpoint availability API error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for Hono usage in API routes
rg -n 'from "hono"|from \'hono\'|Hono|hono' src/app/api --type ts --type tsx

Repository: ding113/claude-code-hub

Length of output: 190


🏁 Script executed:

# Check if Hono is listed as a dependency
cat package.json | grep -i hono

Repository: ding113/claude-code-hub

Length of output: 189


🏁 Script executed:

# Look for other API route examples in the codebase
fd -e 'route.ts' -e 'route.tsx' src/app/api

Repository: ding113/claude-code-hub

Length of output: 416


🏁 Script executed:

# Check the structure of API directory
find src/app/api -type f -name '*.ts' -o -name '*.tsx' | head -20

Repository: ding113/claude-code-hub

Length of output: 877


🏁 Script executed:

# Check one of the API routes to see the actual pattern being used
cat -n src/app/api/availability/endpoints/route.ts | head -40

Repository: ding113/claude-code-hub

Length of output: 1542


🏁 Script executed:

# Search for Hono framework usage across all API routes
rg 'from "hono"|from \'hono\'|new Hono' src/app/api --type ts --type tsx -A 2

Repository: ding113/claude-code-hub

Length of output: 135


🏁 Script executed:

# Check a few other API route files to see the pattern
cat -n src/app/api/auth/login/route.ts | head -30

Repository: ding113/claude-code-hub

Length of output: 1114


🏁 Script executed:

# Search for NextResponse usage across API routes
rg 'NextResponse|NextRequest' src/app/api --type ts --type tsx | head -20

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# Search for actual Hono usage in the codebase (try different pattern)
rg 'Hono|from "hono"|from .hono.' --type ts --type tsx

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# Check if there are any route handlers using Hono pattern
rg 'new Hono|Hono()' --type ts --type tsx

Repository: ding113/claude-code-hub

Length of output: 94


🏁 Script executed:

# Look at the structure - check if there's a different API pattern used
cat -n src/app/api/actions/\[...\route\]/route.ts | head -40

Repository: ding113/claude-code-hub

Length of output: 2018


🏁 Script executed:

# Check which API routes use Hono pattern
rg 'from "@hono|from .hono' src/app/api -l

Repository: ding113/claude-code-hub

Length of output: 106


🏁 Script executed:

# Check which use NextResponse pattern
rg 'from "next/server"' src/app/api -l

Repository: ding113/claude-code-hub

Length of output: 659


🏁 Script executed:

# Look at the full actions route to understand the Hono pattern
cat -n src/app/api/actions/\[...\route\]/route.ts | tail -80

Repository: ding113/claude-code-hub

Length of output: 2220


API 路由应使用 Hono 框架以遵循统一规范。

根据编码规范,src/app/api/**/*.{ts,tsx} 的 API 路由应使用 Hono 框架。当前代码使用 NextResponse,应改为 Hono 模式。参考 src/app/api/actions/[...route]/route.ts 中使用 OpenAPIHonohandle() 的实现方式。

🤖 Prompt for AI Agents
In `@src/app/api/availability/endpoints/route.ts` around lines 1 - 61, The route
currently implements an exported GET using NextRequest/NextResponse (functions:
GET, isProviderType, PROVIDER_TYPES, getSession,
findAllProviderEndpointsWithVendorInfo, findProviderEndpointsByVendorAndType)
but must be converted to a Hono-style handler per project conventions; replace
the exported GET with a Hono app/handler (use OpenAPIHono and app.handle or the
pattern used in src/app/api/actions/[...route]/route.ts) and adapt
request/session handling to Hono's Context (ctx.req, ctx.json, ctx.text) while
keeping the same validation logic (vendorId/vendorIdRaw, providerTypeRaw,
isProviderType) and error branches, and ensure you export the Hono handler via
handle() so the route registers under the OpenAPIHono pattern.

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: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants