Skip to content

feat: implement vendor-endpoint architecture for multi-endpoint provider support#605

Closed
ding113 wants to merge 1 commit intodevfrom
feat/vendor-endpoint-architecture
Closed

feat: implement vendor-endpoint architecture for multi-endpoint provider support#605
ding113 wants to merge 1 commit intodevfrom
feat/vendor-endpoint-architecture

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Jan 13, 2026

Summary

This PR introduces a comprehensive vendor-endpoint architecture that enables providers to have multiple endpoints with intelligent routing and circuit breaker support.

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

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 for automatic failover
  • Real-time health monitoring with probe scheduling

Changes

Database Schema

  • Added provider_vendors table for vendor management
  • Added provider_endpoints table with circuit breaker state tracking
  • Added provider_endpoint_probe_events table for health monitoring history
  • Added vendor_id foreign key to providers table

Core Services

  • Endpoint Availability Service (src/lib/endpoint-availability/): Health monitoring with configurable probe scheduling
  • Endpoint Circuit Breaker (src/lib/endpoint-circuit-breaker.ts): Automatic failover on endpoint failures
  • Endpoint Probe Scheduler (src/lib/endpoint-probe-scheduler.ts): Background health checks
  • Endpoint Resolver (src/app/v1/_lib/proxy/endpoint-resolver.ts): Smart endpoint selection based on availability

Proxy Pipeline Integration

  • Updated forwarder.ts to support endpoint-level routing
  • Updated provider-selector.ts with vendor fuse integration
  • Updated response-handler.ts for endpoint-aware error handling
  • Updated session.ts with endpoint context

Management UI

  • New vendors management page (/settings/providers/vendors)
  • Vendor card, edit sheet, and endpoints manager components
  • Endpoint add dialog and list item components
  • Full i18n support (en, zh-CN)

Dashboard Views

  • Split availability dashboard into provider-level and endpoint-level views
  • New endpoint availability view with health status visualization
  • Real-time SSE updates for endpoint status

API Endpoints

  • GET /api/availability/endpoints - List endpoint availability
  • GET /api/availability/endpoints/current - Current endpoint status
  • GET /api/availability/endpoints/events - SSE for real-time updates

Breaking Changes

Change Impact Migration
New database tables Requires migration Run bun run db:migrate or enable AUTO_MIGRATE=true
providers table has new vendor_id column Existing providers will have NULL vendor_id No action needed - backward compatible

Testing

Automated Tests

  • Unit tests for endpoint-resolver (tests/unit/proxy/endpoint-resolver.test.ts)
  • Unit tests for circuit-breaker (tests/unit/lib/endpoint-circuit-breaker.test.ts)
  • Unit tests for probe-scheduler (tests/unit/lib/endpoint-probe-scheduler.test.ts)
  • Unit tests for endpoint-availability (tests/unit/lib/endpoint-availability.test.ts)
  • Unit tests for vendor-key utilities (tests/unit/lib/vendor-key.test.ts)
  • Repository tests for provider-vendor (tests/unit/repository/provider-vendor.test.ts)
  • Provider selector vendor fuse tests (tests/unit/proxy/provider-selector-vendor-fuse.test.ts)

Manual Testing

  1. Create a new vendor in Settings > Providers > Vendors
  2. Add multiple endpoints to the vendor
  3. Verify endpoint health monitoring in Dashboard > Availability > Endpoints
  4. Test failover by disabling an endpoint

Files Changed

  • 57 files changed, 8073 insertions(+), 71 deletions(-)

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally
  • i18n strings added for en and zh-CN
  • Database migration generated via bun run db:generate

Description enhanced by Claude AI

…der support

- Add provider_vendors and provider_endpoints tables with circuit breaker support
- Implement endpoint-level availability monitoring and probe scheduling
- Add vendor/endpoint management UI in settings with i18n support
- Create endpoint resolver for intelligent endpoint selection
- Add availability dashboard views for both providers and endpoints
- Include comprehensive unit tests for new components
@coderabbitai
Copy link

coderabbitai bot commented Jan 13, 2026

📝 Walkthrough

Walkthrough

此PR为提供商管理系统引入端点健康监测和厂商管理功能,包括数据库架构扩展、服务端操作接口、UI组件、API路由、端点解析器、故障熔断器和端点探针调度器。

Changes

凝聚群组 / 文件 变更说明
数据库架构与迁移
drizzle/0055_bumpy_mojo.sql, drizzle/meta/0055_snapshot.json, drizzle/meta/_journal.json
新增provider_vendors、provider_endpoints、provider_endpoint_probe_events表;在providers表添加vendor_id外键;创建多个性能索引和唯一约束以优化查询和确保数据一致性
提供商管理操作
src/actions/provider-vendors.ts, src/actions/provider-endpoints.ts, src/actions/providers.ts
新增服务端操作接口:提供商CRUD管理、厂商编辑与合并分割、端点创建更新删除;集成厂商与端点数据流;实现认证鉴权、错误处理和事务一致性
设置页面UI组件
src/app/[locale]/settings/providers/_components/*
新增提供商/厂商标签导航、厂商卡片、厂商编辑表单、端点列表和添加对话框、厂商管理加载器等React组件;支持搜索、编辑、删除、计数统计
可用性监测UI
src/app/[locale]/dashboard/availability/_components/availability-*.tsx, src/app/[locale]/dashboard/availability/page.tsx
新增端点可用性视图组件;重构可用性页面为标签界面(提供商/端点两个标签);支持时间范围筛选、排序、刷新和详细统计展示
提供商设置布局
src/app/[locale]/settings/providers/layout.tsx, src/app/[locale]/settings/providers/page.tsx, src/app/[locale]/settings/providers/vendors/page.tsx
新增设置页面布局与路由;支持提供商和厂商列表页面导航
可用性API路由
src/app/api/availability/endpoints/route.ts, src/app/api/availability/endpoints/current/route.ts, src/app/api/availability/endpoints/events/route.ts
新增API端点查询、当前状态和事件历史API;实现管理员权限检查、参数验证、错误处理
代理端点解析与熔断
src/app/v1/_lib/proxy/endpoint-resolver.ts, src/app/v1/_lib/proxy/session.ts, src/lib/endpoint-circuit-breaker.ts
新增端点解析器、故障熔断机制和厂商类型保险丝;支持权重随机选择、缓存、健康检查和自适应故障隔离
代理转发增强
src/app/v1/_lib/proxy/forwarder.ts, src/app/v1/_lib/proxy/provider-selector.ts, src/app/v1/_lib/proxy/response-handler.ts
集成端点解析器;新增端点失败记录;增加厂商类型熔断检查;记录端点成功/失败指标
后端服务与调度
src/lib/endpoint-availability/endpoint-availability-service.ts, src/lib/endpoint-availability/types.ts, src/lib/endpoint-availability/index.ts, src/lib/endpoint-probe-scheduler.ts, src/instrumentation.ts
新增端点可用性查询服务、可用性指标计算、端点探针周期调度器、生命周期管理
数据层操作
src/repository/provider-vendor.ts, src/repository/provider-endpoint.ts, src/repository/provider-endpoint-probe-event.ts, src/repository/provider.ts, src/repository/_shared/transformers.ts
新增厂商、端点、探针事件的CRUD和聚合操作;实现合并分割等复杂事务;扩展数据转换器支持新类型
类型定义与工具
src/types/provider.ts, src/drizzle/schema.ts, src/lib/utils/vendor-key.ts
新增ProviderVendor、ProviderEndpoint、ProviderEndpointProbeEvent类型;扩展Provider和ProviderDisplay类型;添加厂商键规范化工具
国际化资源
messages/en/dashboard.json, messages/en/settings/providers/tabs.json, messages/en/settings/providers/vendors.json, messages/zh-CN/*
新增英文和中文本地化字符串:仪表板探针标签、设置页面标题描述、厂商管理UI文本、端点字段标签和错误消息
单元测试
tests/unit/lib/endpoint-availability.test.ts, tests/unit/lib/endpoint-circuit-breaker.test.ts, tests/unit/lib/endpoint-probe-scheduler.test.ts, tests/unit/lib/vendor-key.test.ts, tests/unit/proxy/endpoint-resolver.test.ts, tests/unit/proxy/provider-selector-vendor-fuse.test.ts, tests/unit/repository/provider-vendor.test.ts, tests/unit/actions/providers.test.ts
新增覆盖端点可用性查询、故障熔断状态转换、探针调度、厂商键规范化、端点解析器选择逻辑、厂商合并分割、提供商动作的单元测试

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed 标题清楚地总结了PR的主要变更:实现供应商-端点架构以支持多端点提供商。内容与PR的核心目标完全相关。
Description check ✅ Passed PR description clearly describes the vendor-endpoint architecture implementation with comprehensive details about problems, solutions, changes, and testing.

✏️ 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/vendor-endpoint-architecture

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

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

3 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

CREATE INDEX IF NOT EXISTS "idx_probe_events_checked_at" ON "provider_endpoint_probe_events" USING btree ("checked_at" DESC NULLS LAST);--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "idx_provider_endpoints_vendor_type" ON "provider_endpoints" USING btree ("vendor_id","provider_type") WHERE "provider_endpoints"."deleted_at" IS NULL;--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "idx_provider_endpoints_enabled_priority" ON "provider_endpoints" USING btree ("is_enabled","vendor_id","provider_type","priority","weight") WHERE "provider_endpoints"."deleted_at" IS NULL;--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "uniq_provider_endpoints_vendor_type_base_url" ON "provider_endpoints" USING btree ("vendor_id","provider_type","base_url");--> statement-breakpoint
Copy link

Choose a reason for hiding this comment

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

the unique index uniq_provider_endpoints_vendor_type_base_url does not include deleted_at in its condition, which will cause conflicts with soft-deleted records

Problem: When an endpoint is soft-deleted (deletedAt is set), the unique index will still prevent creating a new endpoint with the same vendorId+providerType+baseUrl combination. This breaks the soft-delete pattern.

Impact:

  • Users cannot recreate an endpoint after deleting it
  • The ensureProviderEndpoint function will fail with a unique constraint violation
  • The splitProviderVendor function may fail when moving providers

Fix: Add a WHERE clause to exclude soft-deleted records:

Suggested change
CREATE UNIQUE INDEX IF NOT EXISTS "uniq_provider_endpoints_vendor_type_base_url" ON "provider_endpoints" USING btree ("vendor_id","provider_type","base_url");--> statement-breakpoint
CREATE UNIQUE INDEX IF NOT EXISTS "uniq_provider_endpoints_vendor_type_base_url" ON "provider_endpoints" USING btree ("vendor_id","provider_type","base_url") WHERE "deleted_at" IS NULL;--> statement-breakpoint
Prompt To Fix With AI
This is a comment left during a code review.
Path: drizzle/0055_bumpy_mojo.sql
Line: 44:44

Comment:
the unique index `uniq_provider_endpoints_vendor_type_base_url` does not include `deleted_at` in its condition, which will cause conflicts with soft-deleted records

**Problem**: When an endpoint is soft-deleted (deletedAt is set), the unique index will still prevent creating a new endpoint with the same vendorId+providerType+baseUrl combination. This breaks the soft-delete pattern.

**Impact**: 
- Users cannot recreate an endpoint after deleting it
- The `ensureProviderEndpoint` function will fail with a unique constraint violation
- The `splitProviderVendor` function may fail when moving providers

**Fix**: Add a WHERE clause to exclude soft-deleted records:

```suggestion
CREATE UNIQUE INDEX IF NOT EXISTS "uniq_provider_endpoints_vendor_type_base_url" ON "provider_endpoints" USING btree ("vendor_id","provider_type","base_url") WHERE "deleted_at" IS NULL;--> statement-breakpoint
```

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

Comment on lines +14 to +25
CREATE TABLE IF NOT EXISTS "provider_endpoints" (
"id" serial PRIMARY KEY NOT NULL,
"vendor_id" integer NOT NULL,
"provider_type" varchar(20) NOT NULL,
"base_url" varchar(512) NOT NULL,
"is_enabled" boolean DEFAULT true NOT NULL,
"priority" integer DEFAULT 0 NOT NULL,
"weight" integer DEFAULT 1 NOT NULL,
"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.

missing foreign key constraints for referential integrity

Problem: The migration creates three tables with relationships but does not define foreign key constraints:

  • provider_endpoints.vendor_idprovider_vendors.id
  • provider_endpoint_probe_events.endpoint_idprovider_endpoints.id
  • providers.vendor_idprovider_vendors.id

Impact:

  • Orphaned records can exist if a vendor is deleted but endpoints/providers still reference it
  • Data integrity issues that are hard to debug
  • No automatic cascade deletes or restrict behavior

Fix: Add foreign key constraints with appropriate ON DELETE behavior:

Suggested change
CREATE TABLE IF NOT EXISTS "provider_endpoints" (
"id" serial PRIMARY KEY NOT NULL,
"vendor_id" integer NOT NULL,
"provider_type" varchar(20) NOT NULL,
"base_url" varchar(512) NOT NULL,
"is_enabled" boolean DEFAULT true NOT NULL,
"priority" integer DEFAULT 0 NOT NULL,
"weight" integer DEFAULT 1 NOT NULL,
"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 REFERENCES "provider_vendors"("id") ON DELETE RESTRICT,
"provider_type" varchar(20) NOT NULL,
"base_url" varchar(512) NOT NULL,
"is_enabled" boolean DEFAULT true NOT NULL,
"priority" integer DEFAULT 0 NOT NULL,
"weight" integer DEFAULT 1 NOT NULL,
"created_at" timestamp with time zone DEFAULT now(),
"updated_at" timestamp with time zone DEFAULT now(),
"deleted_at" timestamp with time zone
);

Similarly for provider_endpoint_probe_events.endpoint_id and providers.vendor_id.

Prompt To Fix With AI
This is a comment left during a code review.
Path: drizzle/0055_bumpy_mojo.sql
Line: 14:25

Comment:
missing foreign key constraints for referential integrity

**Problem**: The migration creates three tables with relationships but does not define foreign key constraints:
- `provider_endpoints.vendor_id``provider_vendors.id`
- `provider_endpoint_probe_events.endpoint_id``provider_endpoints.id`  
- `providers.vendor_id``provider_vendors.id`

**Impact**:
- Orphaned records can exist if a vendor is deleted but endpoints/providers still reference it
- Data integrity issues that are hard to debug
- No automatic cascade deletes or restrict behavior

**Fix**: Add foreign key constraints with appropriate ON DELETE behavior:

```suggestion
CREATE TABLE IF NOT EXISTS "provider_endpoints" (
	"id" serial PRIMARY KEY NOT NULL,
	"vendor_id" integer NOT NULL REFERENCES "provider_vendors"("id") ON DELETE RESTRICT,
	"provider_type" varchar(20) NOT NULL,
	"base_url" varchar(512) NOT NULL,
	"is_enabled" boolean DEFAULT true NOT NULL,
	"priority" integer DEFAULT 0 NOT NULL,
	"weight" integer DEFAULT 1 NOT NULL,
	"created_at" timestamp with time zone DEFAULT now(),
	"updated_at" timestamp with time zone DEFAULT now(),
	"deleted_at" timestamp with time zone
);
```

Similarly for `provider_endpoint_probe_events.endpoint_id` and `providers.vendor_id`.

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

})
.onConflictDoNothing();

ensuredEndpoints++;
Copy link

Choose a reason for hiding this comment

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

the ensuredEndpoints counter is incremented even when .onConflictDoNothing() skips the insert

Problem: The counter increments unconditionally, but onConflictDoNothing() means the insert might not happen if an endpoint with the same vendorId+providerType+baseUrl already exists. This makes the return value misleading.

Impact: The SplitProviderVendorResult.ensuredEndpoints count will be incorrect, reporting that endpoints were created when they actually weren't.

Fix: Only increment the counter if the endpoint was actually created:

Suggested change
ensuredEndpoints++;
const result = await tx
.insert(providerEndpoints)
.values({
vendorId: newVendor.id,
providerType: (p.providerType || "claude") as ProviderType,
baseUrl: p.url,
isEnabled: true,
priority: 0,
weight: 1,
})
.onConflictDoNothing()
.returning({ id: providerEndpoints.id });
if (result.length > 0) {
ensuredEndpoints++;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/repository/provider-vendor.ts
Line: 482:482

Comment:
the `ensuredEndpoints` counter is incremented even when `.onConflictDoNothing()` skips the insert

**Problem**: The counter increments unconditionally, but `onConflictDoNothing()` means the insert might not happen if an endpoint with the same vendorId+providerType+baseUrl already exists. This makes the return value misleading.

**Impact**: The `SplitProviderVendorResult.ensuredEndpoints` count will be incorrect, reporting that endpoints were created when they actually weren't.

**Fix**: Only increment the counter if the endpoint was actually created:

```suggestion
      const result = await tx
        .insert(providerEndpoints)
        .values({
          vendorId: newVendor.id,
          providerType: (p.providerType || "claude") as ProviderType,
          baseUrl: p.url,
          isEnabled: true,
          priority: 0,
          weight: 1,
        })
        .onConflictDoNothing()
        .returning({ id: providerEndpoints.id });

      if (result.length > 0) {
        ensuredEndpoints++;
      }
```

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

@gemini-code-assist
Copy link
Contributor

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 upgrades the system's resilience and configurability by introducing a robust vendor-endpoint architecture. It enables providers to leverage multiple endpoints for improved load balancing and fault tolerance, backed by intelligent routing, granular circuit breaking, and active health monitoring. The changes also include a comprehensive management interface and detailed availability dashboards, providing administrators with better control and visibility over their AI provider infrastructure.

Highlights

  • Multi-Endpoint Provider Support: Introduced a new architecture allowing providers to have multiple API endpoints, enhancing flexibility and reliability. This includes new database tables (provider_vendors, provider_endpoints, provider_endpoint_probe_events) to manage vendors, their endpoints, and health probe events.
  • Intelligent Endpoint Routing: Implemented an EndpointResolver that intelligently selects the best available endpoint based on priority and weight, taking into account circuit breaker states. This ensures requests are routed to healthy and optimal endpoints.
  • Endpoint-Level Circuit Breaker: Developed a new circuit breaker mechanism specifically for individual endpoints, complementing the existing provider-level circuit breaker. This allows for more granular control over endpoint health and automatic failover.
  • Endpoint Health Monitoring: Added an active endpoint probing scheduler (endpoint-probe-scheduler) that periodically checks the health and latency of configured endpoints. Probe results are stored and used to inform the circuit breaker and availability dashboard.
  • Enhanced Management UI: Integrated new UI components in the settings for managing vendors and their associated endpoints. The dashboard now features separate views for overall provider availability and detailed endpoint-level availability, complete with i18n support for English and Chinese.
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.

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: 18

🤖 Fix all issues with AI agents
In @messages/en/settings/providers/vendors.json:
- Around line 1-53: The PR is missing vendors.json translations for zh-TW, ja,
and ru; add three new files named vendors.json for locales zh-TW, ja, and ru
containing the exact same key structure as the provided en file (keys like
"title", "description", "searchPlaceholder", "empty", "stats", "actions",
"edit", "fields", "endpoints" and its nested keys such as "title", "count",
"add", "fields.type", "errors.conflict", etc.), populating the values with the
appropriate translations (or temporary copied English strings if translations
are pending) so all five locales (zh-CN, zh-TW, en, ja, ru) support the same UI
keys.

In @src/actions/providers.ts:
- Around line 723-727: The payload construction currently omits vendor_id when
vendor is null which preserves the old vendor_id; change the spread that builds
payload (the const payload block) to always include vendor_id by setting it to
vendor?.id ?? null (or using ...(vendor ? { vendor_id: vendor.id } : {
vendor_id: null })) so that vendor_id is explicitly cleared when vendor is null.

In
@src/app/[locale]/dashboard/availability/_components/availability-endpoints-view.tsx:
- Around line 324-327: The UI references t("sort.probes") inside the SelectItem
options (see SelectItem entries) but that i18n key is missing from all locale
messages; either add a "probes" key under the "sort" object in each
messages/{en,ja,ru,zh-CN,zh-TW}/dashboard.json with appropriate translations, or
change the component to use the existing key t("sort.requests") instead of
t("sort.probes"); update all 5 language files if adding the key so translations
are present across locales, or update the SelectItem value/text to consistently
use the existing "requests" key.
- Around line 274-275: In availability-endpoints-view.tsx update the i18n keys
used with the translation helper t(): replace summary.healthyEndpoints,
summary.unhealthyEndpoints, summary.unknownEndpoints with the existing keys
summary.healthyProviders, summary.unhealthyProviders, summary.unknownProviders
(used across all locales), remove or replace the nonexistent states.noEndpoints
key with a valid existing key (or add states.noEndpoints to locale files if
intended), and ensure heatmap.probes and heatmap.noProbes are added to ja, ru
and zh-TW locale files (or change calls to a key that exists in those locales);
search for all t("...") usages in this file/component and make the key
replacements or add the missing translations in the locale JSONs so keys match
across all five languages.

In @src/app/[locale]/dashboard/availability/page.tsx:
- Around line 52-53: Add the missing i18n key "tabs.endpoints" used by
t("tabs.endpoints") (TabsTrigger value="endpoints") to every locale translation
so the tab label renders; update each locale file (en, ja, ru, zh-CN, zh-TW) by
adding a "tabs": { "endpoints": "<appropriate translation>" } entry alongside
the existing "tabs.providers" key, keeping JSON structure and escaping
consistent.

In
@src/app/[locale]/settings/providers/_components/vendors/endpoint-add-dialog.tsx:
- Around line 121-135: The Select's visible strings are hardcoded in English;
replace them with i18n keys by using the project's translation hook/function
(e.g., useTranslations or t) and swap the SelectValue placeholder and each
SelectItem child with translated strings (e.g.,
t('providerType.selectPlaceholder'), t('providerType.option.openaiCompatible'),
t('providerType.option.claude'), etc.), keeping the Select
onValueChange={field.onChange}, defaultValue={field.value} and SelectTrigger
id="providerType" unchanged; add the corresponding translation keys to locale
files.
- Around line 96-100: Replace the hard-coded "CONFLICT" string comparison with
the ERROR_CODES.CONFLICT constant: update the condition that checks
result.errorCode in the endpoint-add-dialog component (where setError("baseUrl",
...) is called) to compare against ERROR_CODES.CONFLICT and add the missing
import for ERROR_CODES from "@/lib/utils/error-messages"; this keeps
result.errorCode checks consistent and avoids string typos.

In
@src/app/[locale]/settings/providers/_components/vendors/endpoint-list-item.tsx:
- Around line 16-31: The handleDelete function contains hardcoded user-facing
strings that must be internationalized: replace the confirm prompt and both
toast messages with translation keys using the app's i18n utility (e.g., obtain
a translation function like useTranslations/useIntl at top of the component and
call it inside handleDelete), update the confirm call to use
t('...confirmDeleteEndpoint') instead of the literal string, and replace
toast.success and toast.error messages with t('...endpointDeleted') and
t('...endpointDeleteFailed') respectively; also add the corresponding import for
the i18n hook and ensure the translation keys are added to locale files.

In
@src/app/[locale]/settings/providers/_components/vendors/vendor-endpoints-manager.tsx:
- Around line 20-23: Current cache invalidation in EndpointAddDialog is too
coarse compared to the query that caches endpoints by vendor; update the
invalidate step in EndpointAddDialog to target the per-vendor key by calling
queryClient.invalidateQueries with the exact queryKey ["provider-endpoints",
vendorId] (or, if vendorId is not available in that component, pass it into
EndpointAddDialog and use it) so the cached result from useQuery({ queryKey:
["provider-endpoints", vendorId] }) is properly refreshed after adding an
endpoint.

In @src/app/api/availability/endpoints/route.ts:
- Around line 65-68: The code sets options.maxBuckets from
parseInt(searchParams.get("maxBuckets"), 10) without validating NaN; update the
route handler to parse maxBuckets, check Number.isNaN(result) (or isFinite) and
only assign to options.maxBuckets when the parsed value is a valid number
(similar to how bucketSizeMinutes is validated), otherwise leave
options.maxBuckets undefined or handle the invalid input (e.g., return a 400 or
ignore the param) so NaN is never passed to the service layer.

In @src/app/v1/_lib/proxy/forwarder.ts:
- Around line 1931-1934: Tests are using the old buildHeaders signature; update
the proxy-forwarder.test.ts mocks and calls to match the new
buildHeaders(session: ProxySession, provider: Provider, baseUrl: string) =>
Headers signature used in forwarder.ts (see buildHeaders). Change the type
assertion for buildHeaders from (session: ProxySession, provider: Provider) =>
Headers to include the third baseUrl: string parameter, and add an appropriate
baseUrl argument to each of the five buildHeaders calls in the tests so they
pass the new parameter.

In @src/drizzle/schema.ts:
- Around line 323-329: The unique index
providerEndpointsVendorTypeBaseUrlUniqueIdx
(uniq_provider_endpoints_vendor_type_base_url) currently does not exclude
soft-deleted rows; update its definition in the schema builder so the
uniqueIndex on table.vendorId, table.providerType, table.baseUrl is a
partial/filtered index with WHERE table.deletedAt IS NULL (matching how
providerEndpointsVendorTypeIdx and providerEndpointsEnabledPriorityIdx are
filtered) so soft-deleted records no longer block creating the same
vendor+type+baseUrl combo.

In @src/lib/endpoint-availability/endpoint-availability-service.ts:
- Around line 307-321: The events query in getCurrentEndpointStatus is missing a
LIMIT which can cause high memory usage under load; modify the query that
selects from providerEndpointProbeEvents (using endpointIdList,
fifteenMinutesAgo, now) to add a reasonable .limit(...) (or apply the same
per-query cap used in queryEndpointAvailability) so only a bounded number of
recent probe rows are returned and memory pressure is avoided.

In @src/lib/endpoint-probe-scheduler.ts:
- Around line 259-269: The success/failure counters are inaccurate because
probeTarget swallows errors and never throws, so the catch branch in the
runWithConcurrency callback never executes; change probeTarget to return an
explicit boolean/result (e.g., true on success, false on failure) or an object
with status, then in the runWithConcurrency callback (the async function passed
to runWithConcurrency) inspect that return value to increment succeeded or
failed accordingly (instead of relying on try/catch), ensuring the callback
calls await probeTarget(target) and uses its returned status to update the
succeeded/failed counters.

In @src/repository/provider-endpoint.ts:
- Around line 130-156: ensureProviderEndpoint can race and fail when a unique
index collides with soft-deleted rows or concurrent calls; replace the
insert+onConflictDoNothing+separate-query flow with an upsert that returns the
row: use db.insert(providerEndpoints).values(...).onConflict((cp) =>
cp.constraint(/* appropriate unique constraint or columns */)).doUpdateSet({
isEnabled: true, deletedAt: null, baseUrl: input.baseUrl, priority: 0, weight: 1
}).returningAll() (or the equivalent in your query builder) to revive/update
soft-deleted rows and get the row atomically, then return that row instead of
calling createProviderEndpoint; alternatively, perform a SELECT for existing
non-deleted row first (findProviderEndpointsByVendorType) and only insert if
absent, but prefer the upsert approach to avoid concurrent duplicate inserts and
handle soft-deletes.
🧹 Nitpick comments (35)
tests/unit/lib/vendor-key.test.ts (1)

4-20: 测试覆盖范围可考虑扩展

当前测试用例覆盖了主要场景,但建议补充以下边界情况的测试:

  1. 空字符串输入 "" → 应返回 null
  2. HTTP 协议(非 HTTPS)的 URL
  3. 仅有 www. 的域名(如 https://www.
💡 建议添加的测试用例
+ test("空字符串应返回 null", () => {
+   expect(normalizeVendorKeyFromUrl("")).toBeNull();
+ });
+
+ test("应支持 http 协议的 URL", () => {
+   expect(normalizeVendorKeyFromUrl("http://api.example.com")).toBe("api.example.com");
+ });
src/instrumentation.ts (1)

227-234: 启动逻辑正确,但存在可优化的代码重复

生产环境(227-234 行)和开发环境(287-294 行)的端点探测调度器初始化逻辑完全相同。虽然这与现有的智能探测调度器(smart probing scheduler)保持了一致的模式,但可考虑将其提取为共享函数以减少重复。

Also applies to: 287-294

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

126-139: 考虑 vendorKey 空字符串默认值的影响

vendorKey 默认为空字符串可能会掩盖数据完整性问题。根据数据库 schema,vendor_keyNOT NULL 且有唯一索引约束。如果 dbVendor.vendorKey 为空或未定义,返回空字符串可能导致后续逻辑出现问题。

建议在调用层确保数据完整性,或在此处添加防御性检查/日志记录。


141-155: vendorId 和 baseUrl 默认值需要关注

  • vendorId 默认为 0 可能会导致无效的外键引用
  • baseUrl 默认为空字符串可能导致后续 URL 拼接/请求失败

这些默认值虽然防止了 null/undefined 错误,但可能会掩盖数据问题。建议确保上游数据查询时已保证这些字段有效。


157-171: probe event 默认值选择需斟酌

result 默认为 "fail" 可能在数据缺失时产生误导性的统计结果。如果无法确定探测结果,考虑是否应该抛出错误或记录警告,而不是静默地标记为失败。

当前实现保持了与其他 transformer 的一致性,作为可选改进建议。

drizzle/0055_bumpy_mojo.sql (2)

1-12: 考虑添加外键约束

endpoint_id 列引用 provider_endpoints.id,但未定义外键约束。这可能导致孤立记录(orphaned records)问题。

建议添加外键约束以确保引用完整性:

FOREIGN KEY ("endpoint_id") REFERENCES "provider_endpoints"("id") ON DELETE CASCADE

如果有意省略外键(例如出于性能考虑),建议在应用层确保数据一致性。


14-25: vendor_id 缺少外键约束

vendor_id 引用 provider_vendors.id,但未添加外键约束。同样可能导致数据不一致问题。

建议添加外键约束
FOREIGN KEY ("vendor_id") REFERENCES "provider_vendors"("id") ON DELETE RESTRICT
src/lib/endpoint-availability/endpoint-availability-service.ts (3)

90-106: 考虑为大数据量查询添加分页或流式处理

当前实现使用硬编码的 MAX_EVENTS_PER_QUERY = 100000 限制,并在达到上限时记录警告。对于高流量系统,可能需要:

  1. 在结果中标记数据是否完整(已通过日志记录)
  2. 考虑在 API 层面暴露此信息,让调用方知道数据可能不完整

当前实现可接受,作为未来优化建议。


222-222: sort() 会修改原数组

allLatencies.sort((a, b) => a - b) 会原地修改 allLatencies 数组。虽然在当前上下文中 allLatencies 不会被再次使用,但这可能导致潜在的维护问题。

建议使用 toSorted() 或创建副本
-    const sortedAllLatencies = allLatencies.sort((a, b) => a - b);
+    const sortedAllLatencies = [...allLatencies].sort((a, b) => a - b);

237-237: 类型断言可能掩盖数据问题

(endpoint.providerType || "claude") as ProviderType 在数据库值无效时会静默回退到 "claude"。考虑添加运行时验证或在数据层确保类型有效性。

当前实现作为防御性编程可接受。

tests/unit/lib/endpoint-availability.test.ts (1)

23-112: 建议增加更多测试场景以提高覆盖率

当前仅有一个测试用例覆盖了正常聚合场景。根据编码规范要求至少 80% 的测试覆盖率,建议补充以下测试场景:

  1. 空结果场景:当没有端点或探测事件时的行为
  2. 边界条件:单个端点、单个探测事件
  3. 错误处理:数据库查询失败时的异常处理
  4. 时间范围过滤:验证 startTime/endTime 参数的正确过滤
建议添加的测试用例示例
test("queryEndpointAvailability: returns empty result when no endpoints", async () => {
  // 设置 selectResults 返回空数组
  // 验证 endpoints 为空,systemAvailability 为 0
});

test("queryEndpointAvailability: handles all failed probes correctly", async () => {
  // 设置所有探测结果为 fail
  // 验证 currentStatus 为 "red",availability 为 0
});
src/app/api/availability/endpoints/events/route.ts (1)

39-41: 建议使用结构化日志替代 console.error

项目中已有 @/lib/logger 模块,建议使用统一的日志工具以保持一致性并支持日志聚合。

建议的修改
 import { type NextRequest, NextResponse } from "next/server";
 import { getSession } from "@/lib/auth";
 import { findProviderEndpointProbeEvents } from "@/repository/provider-endpoint-probe-event";
+import { logger } from "@/lib/logger";

 // ...

   } catch (error) {
-    console.error("Endpoint probe events API error:", error);
+    logger.error("Endpoint probe events API error:", error);
     return NextResponse.json({ error: "Internal server error" }, { status: 500 });
   }
src/repository/provider-endpoint-probe-event.ts (1)

53-66: 批量删除时 .returning() 可能导致大量数据加载到内存

当删除大量过期记录时,.returning() 会将所有被删除的行加载到内存。对于大批量删除,这可能导致内存压力。可改用 rowCount 获取影响行数:

优化方案
 export async function deleteProviderEndpointProbeEventsOlderThan(args: {
   days: number;
   now?: Date;
 }): Promise<number> {
   const now = args.now ?? new Date();
   const cutoff = new Date(now.getTime() - args.days * 24 * 60 * 60 * 1000);

-  const rows = await db
+  const result = await db
     .delete(providerEndpointProbeEvents)
-    .where(lt(providerEndpointProbeEvents.checkedAt, cutoff))
-    .returning({ id: providerEndpointProbeEvents.id });
+    .where(lt(providerEndpointProbeEvents.checkedAt, cutoff));

-  return rows.length;
+  return result.rowCount ?? 0;
 }
src/app/api/availability/endpoints/route.ts (1)

46-52: providerTypes 参数缺少有效值校验

providerTypes 参数被直接转换为 ProviderType[],但未验证每个值是否为有效的 ProviderType。无效值会传递给下游服务,可能导致意外行为。

建议的修复
+const VALID_PROVIDER_TYPES = new Set([
+  "claude",
+  "claude-auth",
+  "codex",
+  "gemini",
+  "gemini-cli",
+  "openai-compatible",
+]);
+
 const providerTypes = searchParams.get("providerTypes");
 if (providerTypes) {
   options.providerTypes = providerTypes
     .split(",")
     .map((t) => t.trim())
-    .filter((t) => t.length > 0) as ProviderType[];
+    .filter((t): t is ProviderType => VALID_PROVIDER_TYPES.has(t));
 }
src/app/[locale]/settings/providers/_components/vendors/endpoint-add-dialog.tsx (1)

32-45: isEnabled 字段在表单中无对应 UI 控件

Zod schema 包含 isEnabled 字段(第 44 行),但表单界面中没有提供切换控件,默认值始终为 true。如果这是预期行为(新端点始终启用),可以忽略;否则考虑添加开关控件。

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

49-50: 内存状态在多实例部署时不会共享。

endpointHealthMapvendorTypeFuseMap 使用内存存储,在以下场景可能导致问题:

  1. 服务重启后状态丢失
  2. 多实例部署时各实例状态不一致

对于生产环境,建议后续考虑使用 Redis 或数据库持久化熔断状态。当前实现适用于单实例场景。


24-29: parsePositiveInt 工具函数重复定义。

此函数在 endpoint-probe-scheduler.ts 中也有相同实现。建议抽取到共享模块(如 @/lib/utils)以符合 DRY 原则。

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

66-86: 测试用例完整度建议增强。

当前测试覆盖了主要场景,但建议补充以下边界情况测试:

  1. half-open 状态下失败导致重新打开
  2. 阈值边界(恰好达到阈值 vs 未达到)
  3. 连续成功计数在 half-open 状态下的累积

另外注意:Line 81 时间前进了 10 分钟,但 VENDOR_TYPE_FUSE_OPEN_DURATION_MS 默认为 60 秒(1 分钟),测试仍能通过但时间跨度过大。

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

100-105: 可通过类型定义避免使用 any

可以定义明确的 patch 类型来替代 any

建议修改
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const dbPatch: any = { updatedAt: new Date() };
+ const dbPatch: Partial<typeof providerEndpoints.$inferInsert> & { updatedAt: Date } = { 
+   updatedAt: new Date() 
+ };
src/lib/endpoint-probe-scheduler.ts (1)

22-27: 重复的 parsePositiveInt 工具函数。

此函数与 endpoint-circuit-breaker.ts 中的实现完全相同,建议抽取到共享模块。

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

330-331: keyOf 函数使用管道符可能与 baseUrl 内容冲突。

如果 baseUrl 包含 | 字符(虽然不常见),可能导致 key 碰撞。建议使用更安全的分隔符或 JSON 序列化。

建议修改
- const keyOf = (p: { providerType: ProviderType; baseUrl: string }) =>
-   `${p.providerType}|${p.baseUrl}`;
+ const keyOf = (p: { providerType: ProviderType; baseUrl: string }) =>
+   JSON.stringify([p.providerType, p.baseUrl]);
src/repository/provider.ts (1)

368-368: 建议:考虑显式处理 vendorId 清除场景

providerData.vendor_idnull 时,当前逻辑会正确设置 dbData.vendorId = null。但如果传入 undefined,则不会更新该字段。这符合部分更新的语义,但建议确认这是否符合预期行为——特别是当需要将 provider 与 vendor 解绑时。

src/actions/provider-vendors.ts (1)

101-107: 建议:为 sourceVendorIds 添加输入验证

当前仅验证了 targetVendorId,但 sourceVendorIds 数组可能为空或包含无效值。建议添加基本验证:

建议的验证逻辑
  if (!Number.isFinite(args.targetVendorId) || args.targetVendorId <= 0) {
    return {
      ok: false,
      error: tError("INVALID_FORMAT"),
      errorCode: ERROR_CODES.INVALID_FORMAT,
    };
  }

+  if (!Array.isArray(args.sourceVendorIds) || args.sourceVendorIds.length === 0) {
+    return {
+      ok: false,
+      error: tError("INVALID_FORMAT"),
+      errorCode: ERROR_CODES.INVALID_FORMAT,
+    };
+  }
+
+  const invalidSourceIds = args.sourceVendorIds.filter(
+    (id) => !Number.isFinite(id) || id <= 0
+  );
+  if (invalidSourceIds.length > 0) {
+    return {
+      ok: false,
+      error: tError("INVALID_FORMAT"),
+      errorCode: ERROR_CODES.INVALID_FORMAT,
+    };
+  }
src/app/[locale]/settings/providers/_components/vendors/vendor-edit-sheet.tsx (2)

27-32: 潜在问题:vendor prop 变化时状态不同步

vendor prop 变化时(如用户快速切换编辑不同的 vendor),displayNamewebsiteUrl 的 state 不会更新,因为 useState 的初始值仅在组件首次挂载时使用。

建议使用 useEffect 同步状态
+ import { useEffect, useState } from "react";

  export function VendorEditSheet({ open, onOpenChange, vendor }: VendorEditSheetProps) {
    const t = useTranslations("settings.providers.vendors");
    const [displayName, setDisplayName] = useState(vendor.displayName);
    const [websiteUrl, setWebsiteUrl] = useState(vendor.websiteUrl || "");
    const [isSaving, setIsSaving] = useState(false);
    const queryClient = useQueryClient();

+   useEffect(() => {
+     if (open) {
+       setDisplayName(vendor.displayName);
+       setWebsiteUrl(vendor.websiteUrl || "");
+     }
+   }, [open, vendor.id, vendor.displayName, vendor.websiteUrl]);

64-79: 建议:添加基本表单验证

当前没有对 displayName 进行非空验证。如果用户清空名称并保存,可能导致显示问题。

建议的验证
  const handleSave = async () => {
+   if (!displayName.trim()) {
+     toast.error(t("errors.nameRequired"));
+     return;
+   }
    setIsSaving(true);
tests/unit/proxy/provider-selector-vendor-fuse.test.ts (1)

82-83: 建议:添加更多边界测试用例

当前测试覆盖了基本场景,建议后续添加以下测试用例以提高覆盖率:

  • vendorIdnull 的 provider 不受 fuse 影响
  • 相同 vendorId 但不同 providerType 的 provider 不受影响
  • Fuse 过期后 provider 恢复可用
src/app/[locale]/settings/providers/_components/providers-tabs.tsx (1)

16-23: TabsTrigger 嵌套在 Link 中可能导致可访问性问题

当前实现将 TabsTrigger 包裹在 Link 中,这可能导致:

  1. 键盘导航行为不一致(两个可聚焦元素嵌套)
  2. 屏幕阅读器可能播报两次可点击元素

建议使用 asChild 属性让 TabsTrigger 直接渲染为 Link,或使用 onClick + router.push 的方式处理导航。

使用 asChild 的重构方案
       <TabsList>
-        <Link href="/settings/providers">
-          <TabsTrigger value="providers">{t("providers")}</TabsTrigger>
-        </Link>
-        <Link href="/settings/providers/vendors">
-          <TabsTrigger value="vendors">{t("vendors")}</TabsTrigger>
-        </Link>
+        <TabsTrigger value="providers" asChild>
+          <Link href="/settings/providers">{t("providers")}</Link>
+        </TabsTrigger>
+        <TabsTrigger value="vendors" asChild>
+          <Link href="/settings/providers/vendors">{t("vendors")}</Link>
+        </TabsTrigger>
       </TabsList>

请验证 TabsTrigger 组件是否支持 asChild 属性(基于 Radix UI 的实现通常支持)。

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

21-29: 建议为切换操作添加加载状态。

当前 handleToggle 执行异步操作时,Switch 组件没有禁用或加载指示,用户可能会重复点击。建议添加 loading 状态以提升用户体验。

♻️ 建议的改进
 export function VendorCard({ vendor }: { vendor: ProviderVendorSummary }) {
   const t = useTranslations("settings.providers.vendors");
   const [isEditOpen, setIsEditOpen] = useState(false);
+  const [isToggling, setIsToggling] = useState(false);
   const queryClient = useQueryClient();

   const handleToggle = async (enabled: boolean) => {
+    setIsToggling(true);
-    const result = await editProviderVendor(vendor.id, { isEnabled: enabled });
-    if (result.ok) {
-      queryClient.invalidateQueries({ queryKey: ["provider-vendors"] });
-      toast.success(t("actions.updated"));
-    } else {
-      toast.error(t("errors.updateFailed"));
+    try {
+      const result = await editProviderVendor(vendor.id, { isEnabled: enabled });
+      if (result.ok) {
+        queryClient.invalidateQueries({ queryKey: ["provider-vendors"] });
+        toast.success(t("actions.updated"));
+      } else {
+        toast.error(t("errors.updateFailed"));
+      }
+    } finally {
+      setIsToggling(false);
     }
   };

然后在 Switch 上添加 disabled={isToggling}:

-        <Switch checked={vendor.isEnabled} onCheckedChange={handleToggle} />
+        <Switch checked={vendor.isEnabled} onCheckedChange={handleToggle} disabled={isToggling} />
src/app/v1/_lib/proxy/endpoint-resolver.ts (3)

9-20: 缓存配置合理,但缺少缓存大小限制。

30 秒的 TTL 对于端点列表缓存是合理的选择。但模块级别的 endpointsCache Map 没有大小限制,在高并发场景下可能存在内存增长问题。

可选:添加缓存大小限制或 LRU 淘汰机制

可以考虑在设置新缓存前检查缓存大小,或使用 LRU 缓存库:

 const ENDPOINTS_CACHE_TTL_MS = 30_000;
+const ENDPOINTS_CACHE_MAX_SIZE = 1000;

 type EndpointsCacheEntry = {
   expiresAt: number;
   endpoints: ProviderEndpoint[];
 };

 const endpointsCache = new Map<string, EndpointsCacheEntry>();

+function pruneExpiredEntries(): void {
+  const now = Date.now();
+  for (const [key, entry] of endpointsCache) {
+    if (entry.expiresAt <= now) {
+      endpointsCache.delete(key);
+    }
+  }
+}

47-66: weightedRandom 函数对空数组的处理存在潜在问题。

虽然调用方确保不会传入空数组,但函数本身对空数组的防御性不足。当 endpoints.length === 0 时,会返回 undefined

建议添加防御性检查
 function weightedRandom(endpoints: ProviderEndpoint[]): ProviderEndpoint {
+  if (endpoints.length === 0) {
+    throw new Error("Cannot select from empty endpoints array");
+  }
+
   const totalWeight = endpoints.reduce((sum, e) => sum + (e.weight ?? 0), 0);

   if (totalWeight <= 0) {
     const index = Math.floor(Math.random() * endpoints.length);
-    return endpoints[Math.max(0, Math.min(index, endpoints.length - 1))];
+    return endpoints[index];
   }

另外,Line 52 中 Math.max(0, Math.min(index, endpoints.length - 1)) 的边界处理实际上是冗余的,因为 Math.floor(Math.random() * endpoints.length) 已经保证结果在 [0, length-1] 范围内。


81-143: 端点解析核心逻辑实现完善。

实现亮点:

  • 无 vendorId 时正确回退到 provider.url
  • 分层过滤:enabled → valid URL → healthy
  • 优先级+权重双维度选择算法
  • 与 circuit breaker 和 fuse 机制集成良好

一个小建议:Line 125-126 的优先级过滤可以更简洁:

可选:简化优先级过滤逻辑
-  const minPriority = Math.min(...healthyEndpoints.map((e) => e.priority ?? 0));
-  const topPriority = healthyEndpoints.filter((e) => (e.priority ?? 0) === minPriority);
+  const minPriority = Math.min(...healthyEndpoints.map((e) => e.priority));
+  const topPriority = healthyEndpoints.filter((e) => e.priority === minPriority);

根据 ProviderEndpoint 类型定义,priority 是必选字段 (priority: number),不需要 nullish coalescing。

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

95-105: 考虑移除未使用的 _formatBucketSizeDisplay 函数。

该函数以下划线前缀命名,表明未被使用。如果确实不需要,建议移除以保持代码整洁。

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

25-50: 建议将共享的工具函数提取到公共模块。

calculateBucketSizegetAvailabilityColorformatBucketTime 等函数与 availability-providers-view.tsx 中的实现完全相同。建议将这些函数提取到一个共享模块(如 @/lib/availability/utils.ts_components/availability-utils.ts)中以减少代码重复。

建议的目录结构
// 新建文件: _components/availability-utils.ts
export const TARGET_BUCKETS = 60;

export const TIME_RANGE_MAP: Record<TimeRangeOption, number> = {
  "15min": 15 * 60 * 1000,
  "1h": 60 * 60 * 1000,
  "6h": 6 * 60 * 60 * 1000,
  "24h": 24 * 60 * 60 * 1000,
  "7d": 7 * 24 * 60 * 60 * 1000,
};

export function calculateBucketSize(timeRangeMs: number): number { ... }
export function getAvailabilityColor(score: number, hasData: boolean): string { ... }
export function formatBucketTime(isoString: string, bucketSizeMinutes: number): string { ... }
export function formatLatency(ms: number): string { ... }
export function formatPercentage(value: number): string { ... }

160-199: getStatusIcongetStatusBadgeformatLatencyformatPercentage 可与 providers view 共享。

这些函数的实现与 providers view 几乎相同,建议一并提取到共享模块中。


463-488: Legend 组件可提取为独立组件复用。

Legend 部分在两个 view 中完全相同,可以提取为 <AvailabilityLegend /> 组件。

📜 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 60c5963 and 277eb5e.

📒 Files selected for processing (57)
  • drizzle/0055_bumpy_mojo.sql
  • drizzle/meta/0055_snapshot.json
  • drizzle/meta/_journal.json
  • messages/en/dashboard.json
  • messages/en/settings/providers/tabs.json
  • messages/en/settings/providers/vendors.json
  • messages/zh-CN/dashboard.json
  • messages/zh-CN/settings/providers/tabs.json
  • messages/zh-CN/settings/providers/vendors.json
  • src/actions/provider-endpoints.ts
  • src/actions/provider-vendors.ts
  • src/actions/providers.ts
  • src/app/[locale]/dashboard/availability/_components/availability-endpoints-view.tsx
  • src/app/[locale]/dashboard/availability/_components/availability-providers-view.tsx
  • src/app/[locale]/dashboard/availability/page.tsx
  • src/app/[locale]/settings/providers/_components/providers-tabs.tsx
  • src/app/[locale]/settings/providers/_components/vendors/endpoint-add-dialog.tsx
  • src/app/[locale]/settings/providers/_components/vendors/endpoint-list-item.tsx
  • src/app/[locale]/settings/providers/_components/vendors/vendor-card.tsx
  • src/app/[locale]/settings/providers/_components/vendors/vendor-edit-sheet.tsx
  • src/app/[locale]/settings/providers/_components/vendors/vendor-endpoints-manager.tsx
  • src/app/[locale]/settings/providers/_components/vendors/vendor-list.tsx
  • src/app/[locale]/settings/providers/_components/vendors/vendors-manager-loader.tsx
  • src/app/[locale]/settings/providers/_components/vendors/vendors-manager.tsx
  • src/app/[locale]/settings/providers/layout.tsx
  • src/app/[locale]/settings/providers/page.tsx
  • src/app/[locale]/settings/providers/vendors/page.tsx
  • src/app/api/availability/endpoints/current/route.ts
  • src/app/api/availability/endpoints/events/route.ts
  • src/app/api/availability/endpoints/route.ts
  • src/app/v1/_lib/proxy/endpoint-resolver.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/drizzle/schema.ts
  • src/instrumentation.ts
  • src/lib/endpoint-availability/endpoint-availability-service.ts
  • src/lib/endpoint-availability/index.ts
  • src/lib/endpoint-availability/types.ts
  • src/lib/endpoint-circuit-breaker.ts
  • src/lib/endpoint-probe-scheduler.ts
  • src/lib/utils/vendor-key.ts
  • src/repository/_shared/transformers.ts
  • src/repository/provider-endpoint-probe-event.ts
  • src/repository/provider-endpoint.ts
  • src/repository/provider-vendor.ts
  • src/repository/provider.ts
  • src/types/provider.ts
  • tests/unit/actions/providers.test.ts
  • tests/unit/lib/endpoint-availability.test.ts
  • tests/unit/lib/endpoint-circuit-breaker.test.ts
  • tests/unit/lib/endpoint-probe-scheduler.test.ts
  • tests/unit/lib/vendor-key.test.ts
  • tests/unit/proxy/endpoint-resolver.test.ts
  • tests/unit/proxy/provider-selector-vendor-fuse.test.ts
  • tests/unit/repository/provider-vendor.test.ts
🧰 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/lib/endpoint-availability/index.ts
  • src/app/[locale]/settings/providers/_components/vendors/vendor-edit-sheet.tsx
  • src/app/[locale]/settings/providers/_components/vendors/vendors-manager-loader.tsx
  • src/app/[locale]/dashboard/availability/_components/availability-providers-view.tsx
  • src/app/[locale]/settings/providers/layout.tsx
  • src/app/[locale]/settings/providers/page.tsx
  • src/app/api/availability/endpoints/route.ts
  • src/app/[locale]/settings/providers/vendors/page.tsx
  • src/app/[locale]/settings/providers/_components/providers-tabs.tsx
  • src/app/v1/_lib/proxy/session.ts
  • src/lib/endpoint-availability/types.ts
  • tests/unit/lib/endpoint-probe-scheduler.test.ts
  • src/app/[locale]/settings/providers/_components/vendors/endpoint-list-item.tsx
  • src/app/[locale]/settings/providers/_components/vendors/vendor-list.tsx
  • src/instrumentation.ts
  • src/repository/provider-endpoint-probe-event.ts
  • src/app/[locale]/settings/providers/_components/vendors/vendor-card.tsx
  • tests/unit/proxy/provider-selector-vendor-fuse.test.ts
  • src/app/api/availability/endpoints/events/route.ts
  • src/repository/_shared/transformers.ts
  • src/app/api/availability/endpoints/current/route.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/lib/endpoint-probe-scheduler.ts
  • src/app/[locale]/settings/providers/_components/vendors/vendors-manager.tsx
  • src/app/[locale]/dashboard/availability/page.tsx
  • tests/unit/proxy/endpoint-resolver.test.ts
  • tests/unit/lib/vendor-key.test.ts
  • src/app/[locale]/settings/providers/_components/vendors/endpoint-add-dialog.tsx
  • src/repository/provider-endpoint.ts
  • src/app/[locale]/dashboard/availability/_components/availability-endpoints-view.tsx
  • src/app/[locale]/settings/providers/_components/vendors/vendor-endpoints-manager.tsx
  • src/actions/provider-endpoints.ts
  • src/lib/endpoint-circuit-breaker.ts
  • src/lib/utils/vendor-key.ts
  • src/drizzle/schema.ts
  • src/actions/provider-vendors.ts
  • src/app/v1/_lib/proxy/endpoint-resolver.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/repository/provider-vendor.ts
  • tests/unit/lib/endpoint-circuit-breaker.test.ts
  • src/lib/endpoint-availability/endpoint-availability-service.ts
  • tests/unit/actions/providers.test.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/types/provider.ts
  • tests/unit/lib/endpoint-availability.test.ts
  • src/actions/providers.ts
  • tests/unit/repository/provider-vendor.test.ts
  • src/repository/provider.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/lib/endpoint-availability/index.ts
  • src/app/[locale]/settings/providers/_components/vendors/vendor-edit-sheet.tsx
  • src/app/[locale]/settings/providers/_components/vendors/vendors-manager-loader.tsx
  • src/app/[locale]/dashboard/availability/_components/availability-providers-view.tsx
  • src/app/[locale]/settings/providers/layout.tsx
  • src/app/[locale]/settings/providers/page.tsx
  • src/app/api/availability/endpoints/route.ts
  • src/app/[locale]/settings/providers/vendors/page.tsx
  • src/app/[locale]/settings/providers/_components/providers-tabs.tsx
  • src/app/v1/_lib/proxy/session.ts
  • src/lib/endpoint-availability/types.ts
  • tests/unit/lib/endpoint-probe-scheduler.test.ts
  • src/app/[locale]/settings/providers/_components/vendors/endpoint-list-item.tsx
  • src/app/[locale]/settings/providers/_components/vendors/vendor-list.tsx
  • src/instrumentation.ts
  • src/repository/provider-endpoint-probe-event.ts
  • src/app/[locale]/settings/providers/_components/vendors/vendor-card.tsx
  • tests/unit/proxy/provider-selector-vendor-fuse.test.ts
  • src/app/api/availability/endpoints/events/route.ts
  • src/repository/_shared/transformers.ts
  • src/app/api/availability/endpoints/current/route.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/lib/endpoint-probe-scheduler.ts
  • src/app/[locale]/settings/providers/_components/vendors/vendors-manager.tsx
  • src/app/[locale]/dashboard/availability/page.tsx
  • tests/unit/proxy/endpoint-resolver.test.ts
  • tests/unit/lib/vendor-key.test.ts
  • src/app/[locale]/settings/providers/_components/vendors/endpoint-add-dialog.tsx
  • src/repository/provider-endpoint.ts
  • src/app/[locale]/dashboard/availability/_components/availability-endpoints-view.tsx
  • src/app/[locale]/settings/providers/_components/vendors/vendor-endpoints-manager.tsx
  • src/actions/provider-endpoints.ts
  • src/lib/endpoint-circuit-breaker.ts
  • src/lib/utils/vendor-key.ts
  • src/drizzle/schema.ts
  • src/actions/provider-vendors.ts
  • src/app/v1/_lib/proxy/endpoint-resolver.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/repository/provider-vendor.ts
  • tests/unit/lib/endpoint-circuit-breaker.test.ts
  • src/lib/endpoint-availability/endpoint-availability-service.ts
  • tests/unit/actions/providers.test.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/types/provider.ts
  • tests/unit/lib/endpoint-availability.test.ts
  • src/actions/providers.ts
  • tests/unit/repository/provider-vendor.test.ts
  • src/repository/provider.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Prefer named exports over default exports

Files:

  • src/lib/endpoint-availability/index.ts
  • src/app/[locale]/settings/providers/_components/vendors/vendor-edit-sheet.tsx
  • src/app/[locale]/settings/providers/_components/vendors/vendors-manager-loader.tsx
  • src/app/[locale]/dashboard/availability/_components/availability-providers-view.tsx
  • src/app/[locale]/settings/providers/layout.tsx
  • src/app/[locale]/settings/providers/page.tsx
  • src/app/api/availability/endpoints/route.ts
  • src/app/[locale]/settings/providers/vendors/page.tsx
  • src/app/[locale]/settings/providers/_components/providers-tabs.tsx
  • src/app/v1/_lib/proxy/session.ts
  • src/lib/endpoint-availability/types.ts
  • tests/unit/lib/endpoint-probe-scheduler.test.ts
  • src/app/[locale]/settings/providers/_components/vendors/endpoint-list-item.tsx
  • src/app/[locale]/settings/providers/_components/vendors/vendor-list.tsx
  • src/instrumentation.ts
  • src/repository/provider-endpoint-probe-event.ts
  • src/app/[locale]/settings/providers/_components/vendors/vendor-card.tsx
  • tests/unit/proxy/provider-selector-vendor-fuse.test.ts
  • src/app/api/availability/endpoints/events/route.ts
  • src/repository/_shared/transformers.ts
  • src/app/api/availability/endpoints/current/route.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/lib/endpoint-probe-scheduler.ts
  • src/app/[locale]/settings/providers/_components/vendors/vendors-manager.tsx
  • src/app/[locale]/dashboard/availability/page.tsx
  • tests/unit/proxy/endpoint-resolver.test.ts
  • tests/unit/lib/vendor-key.test.ts
  • src/app/[locale]/settings/providers/_components/vendors/endpoint-add-dialog.tsx
  • src/repository/provider-endpoint.ts
  • src/app/[locale]/dashboard/availability/_components/availability-endpoints-view.tsx
  • src/app/[locale]/settings/providers/_components/vendors/vendor-endpoints-manager.tsx
  • src/actions/provider-endpoints.ts
  • src/lib/endpoint-circuit-breaker.ts
  • src/lib/utils/vendor-key.ts
  • src/drizzle/schema.ts
  • src/actions/provider-vendors.ts
  • src/app/v1/_lib/proxy/endpoint-resolver.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/repository/provider-vendor.ts
  • tests/unit/lib/endpoint-circuit-breaker.test.ts
  • src/lib/endpoint-availability/endpoint-availability-service.ts
  • tests/unit/actions/providers.test.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/types/provider.ts
  • tests/unit/lib/endpoint-availability.test.ts
  • src/actions/providers.ts
  • tests/unit/repository/provider-vendor.test.ts
  • src/repository/provider.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/events/route.ts
  • src/app/api/availability/endpoints/current/route.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/session.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/app/v1/_lib/proxy/endpoint-resolver.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/response-handler.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/endpoint-probe-scheduler.test.ts
  • tests/unit/proxy/provider-selector-vendor-fuse.test.ts
  • tests/unit/proxy/endpoint-resolver.test.ts
  • tests/unit/lib/vendor-key.test.ts
  • tests/unit/lib/endpoint-circuit-breaker.test.ts
  • tests/unit/actions/providers.test.ts
  • tests/unit/lib/endpoint-availability.test.ts
  • tests/unit/repository/provider-vendor.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/endpoint-probe-scheduler.test.ts
  • tests/unit/proxy/provider-selector-vendor-fuse.test.ts
  • tests/unit/proxy/endpoint-resolver.test.ts
  • tests/unit/lib/vendor-key.test.ts
  • tests/unit/lib/endpoint-circuit-breaker.test.ts
  • tests/unit/actions/providers.test.ts
  • tests/unit/lib/endpoint-availability.test.ts
  • tests/unit/repository/provider-vendor.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-endpoint-probe-event.ts
  • src/repository/_shared/transformers.ts
  • src/repository/provider-endpoint.ts
  • src/repository/provider-vendor.ts
  • src/repository/provider.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-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/endpoint-availability/index.ts
  • src/app/api/availability/endpoints/route.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/lib/endpoint-availability/types.ts
  • src/instrumentation.ts
  • src/repository/provider-endpoint-probe-event.ts
  • src/app/api/availability/endpoints/events/route.ts
  • src/repository/_shared/transformers.ts
  • src/app/api/availability/endpoints/current/route.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/lib/endpoint-probe-scheduler.ts
  • src/repository/provider-endpoint.ts
  • src/actions/provider-endpoints.ts
  • src/lib/endpoint-circuit-breaker.ts
  • src/lib/utils/vendor-key.ts
  • src/drizzle/schema.ts
  • src/actions/provider-vendors.ts
  • src/app/v1/_lib/proxy/endpoint-resolver.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/repository/provider-vendor.ts
  • src/lib/endpoint-availability/endpoint-availability-service.ts
  • src/app/v1/_lib/proxy/response-handler.ts
  • src/types/provider.ts
  • src/actions/providers.ts
  • src/repository/provider.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/availability/endpoints/current/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 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/session.ts
  • src/app/v1/_lib/proxy/provider-selector.ts
  • src/app/v1/_lib/proxy/forwarder.ts
  • src/app/v1/_lib/proxy/response-handler.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-endpoint-probe-event.ts
  • src/repository/provider-endpoint.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:

  • src/app/[locale]/dashboard/availability/page.tsx
  • messages/zh-CN/dashboard.json
  • messages/en/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 tests/**/*.test.{ts,tsx,js,jsx} : Use Vitest for unit testing and happy-dom for DOM testing

Applied to files:

  • tests/unit/lib/vendor-key.test.ts
  • tests/unit/lib/endpoint-circuit-breaker.test.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
  • src/actions/provider-vendors.ts
  • src/actions/providers.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
  • drizzle/meta/0055_snapshot.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 **/*.test.{ts,tsx,js,jsx} : All new features must have unit test coverage of at least 80%

Applied to files:

  • tests/unit/lib/endpoint-circuit-breaker.test.ts
🧬 Code graph analysis (36)
src/app/[locale]/settings/providers/_components/vendors/vendor-edit-sheet.tsx (3)
src/repository/provider-vendor.ts (1)
  • ProviderVendorSummary (75-78)
src/actions/provider-vendors.ts (1)
  • editProviderVendor (39-81)
src/app/[locale]/settings/providers/_components/vendors/vendor-endpoints-manager.tsx (1)
  • VendorEndpointsManager (15-101)
src/app/[locale]/settings/providers/_components/vendors/vendors-manager-loader.tsx (2)
src/actions/provider-vendors.ts (1)
  • getProviderVendors (23-37)
src/app/[locale]/settings/providers/_components/vendors/vendors-manager.tsx (1)
  • VendorsManager (14-41)
src/app/[locale]/settings/providers/layout.tsx (2)
scripts/sync-settings-keys.js (1)
  • t (184-184)
src/app/[locale]/settings/providers/_components/providers-tabs.tsx (1)
  • ProvidersTabs (8-27)
src/app/[locale]/settings/providers/page.tsx (4)
src/components/ui/button.tsx (1)
  • Button (58-58)
src/app/[locale]/settings/providers/_components/auto-sort-priority-dialog.tsx (1)
  • AutoSortPriorityDialog (50-250)
src/app/[locale]/settings/providers/_components/scheduling-rules-dialog.tsx (1)
  • SchedulingRulesDialog (217-275)
src/app/[locale]/settings/providers/_components/provider-manager-loader.tsx (1)
  • ProviderManagerLoader (106-108)
src/app/api/availability/endpoints/route.ts (6)
src/app/api/availability/endpoints/current/route.ts (1)
  • GET (5-18)
src/app/api/availability/endpoints/events/route.ts (1)
  • GET (5-43)
src/lib/auth.ts (1)
  • getSession (116-128)
src/lib/endpoint-availability/types.ts (1)
  • EndpointAvailabilityQueryOptions (4-13)
src/types/provider.ts (1)
  • ProviderType (6-12)
src/lib/endpoint-availability/endpoint-availability-service.ts (1)
  • queryEndpointAvailability (22-268)
src/app/[locale]/settings/providers/vendors/page.tsx (1)
src/app/[locale]/settings/providers/_components/vendors/vendors-manager-loader.tsx (1)
  • VendorsManagerLoader (8-24)
src/app/[locale]/settings/providers/_components/providers-tabs.tsx (2)
scripts/sync-settings-keys.js (1)
  • t (184-184)
src/components/ui/tabs.tsx (3)
  • Tabs (54-54)
  • TabsList (54-54)
  • TabsTrigger (54-54)
src/app/v1/_lib/proxy/session.ts (1)
src/types/provider.ts (2)
  • ProviderEndpoint (394-405)
  • ProviderType (6-12)
src/lib/endpoint-availability/types.ts (2)
src/types/provider.ts (1)
  • ProviderType (6-12)
src/lib/availability/types.ts (1)
  • AvailabilityStatus (12-12)
tests/unit/lib/endpoint-probe-scheduler.test.ts (2)
src/types/provider.ts (2)
  • Provider (39-145)
  • ProviderEndpoint (394-405)
src/lib/endpoint-probe-scheduler.ts (2)
  • runEndpointProbeCycleOnce (235-283)
  • cleanupEndpointProbeEventsOnce (285-307)
src/app/[locale]/settings/providers/_components/vendors/endpoint-list-item.tsx (2)
src/types/provider.ts (1)
  • ProviderEndpoint (394-405)
src/actions/provider-endpoints.ts (1)
  • deleteProviderEndpointAction (173-212)
src/app/[locale]/settings/providers/_components/vendors/vendor-list.tsx (2)
src/repository/provider-vendor.ts (1)
  • ProviderVendorSummary (75-78)
src/app/[locale]/settings/providers/_components/vendors/vendor-card.tsx (1)
  • VendorCard (16-64)
src/instrumentation.ts (2)
src/lib/endpoint-probe-scheduler.ts (3)
  • stopEndpointProbeScheduler (338-350)
  • isEndpointProbingEnabled (352-354)
  • startEndpointProbeScheduler (309-336)
src/lib/logger.ts (1)
  • logger (168-187)
src/repository/provider-endpoint-probe-event.ts (4)
src/drizzle/schema.ts (1)
  • providerEndpointProbeEvents (332-353)
src/types/provider.ts (3)
  • ProviderEndpointProbeSource (407-407)
  • ProviderEndpointProbeResult (409-409)
  • ProviderEndpointProbeEvent (411-422)
src/drizzle/db.ts (1)
  • db (37-44)
src/repository/_shared/transformers.ts (1)
  • toProviderEndpointProbeEvent (158-171)
src/app/[locale]/settings/providers/_components/vendors/vendor-card.tsx (3)
src/repository/provider-vendor.ts (1)
  • ProviderVendorSummary (75-78)
src/actions/provider-vendors.ts (1)
  • editProviderVendor (39-81)
src/app/[locale]/settings/providers/_components/vendors/vendor-edit-sheet.tsx (1)
  • VendorEditSheet (27-93)
tests/unit/proxy/provider-selector-vendor-fuse.test.ts (2)
src/lib/endpoint-circuit-breaker.ts (1)
  • openVendorTypeFuse (157-177)
src/types/provider.ts (1)
  • Provider (39-145)
src/app/api/availability/endpoints/events/route.ts (2)
src/lib/auth.ts (1)
  • getSession (116-128)
src/repository/provider-endpoint-probe-event.ts (1)
  • findProviderEndpointProbeEvents (68-92)
src/repository/_shared/transformers.ts (1)
src/types/provider.ts (3)
  • ProviderVendor (382-392)
  • ProviderEndpoint (394-405)
  • ProviderEndpointProbeEvent (411-422)
src/app/api/availability/endpoints/current/route.ts (2)
src/lib/auth.ts (1)
  • getSession (116-128)
src/lib/endpoint-availability/endpoint-availability-service.ts (1)
  • getCurrentEndpointStatus (270-371)
src/app/v1/_lib/proxy/provider-selector.ts (2)
src/lib/endpoint-circuit-breaker.ts (1)
  • isVendorTypeFuseOpen (179-191)
src/lib/logger.ts (1)
  • logger (168-187)
src/lib/endpoint-probe-scheduler.ts (5)
src/types/provider.ts (2)
  • ProviderType (6-12)
  • Provider (39-145)
src/drizzle/schema.ts (1)
  • providers (131-278)
src/repository/provider-endpoint.ts (1)
  • findProviderEndpointsByVendorIds (31-47)
src/repository/provider-endpoint-probe-event.ts (2)
  • createProviderEndpointProbeEvent (26-51)
  • deleteProviderEndpointProbeEventsOlderThan (53-66)
src/lib/logger.ts (1)
  • logger (168-187)
src/app/[locale]/settings/providers/_components/vendors/vendors-manager.tsx (4)
src/repository/provider-vendor.ts (1)
  • ProviderVendorSummary (75-78)
scripts/sync-settings-keys.js (1)
  • t (184-184)
src/components/ui/input.tsx (1)
  • Input (21-21)
src/app/[locale]/settings/providers/_components/vendors/vendor-list.tsx (1)
  • VendorList (12-32)
src/app/[locale]/dashboard/availability/page.tsx (3)
src/app/[locale]/dashboard/availability/_components/availability-skeleton.tsx (1)
  • AvailabilityViewSkeleton (4-18)
src/app/[locale]/dashboard/availability/_components/availability-providers-view.tsx (1)
  • AvailabilityProvidersView (107-530)
src/app/[locale]/dashboard/availability/_components/availability-endpoints-view.tsx (1)
  • AvailabilityEndpointsView (75-492)
tests/unit/lib/vendor-key.test.ts (1)
src/lib/utils/vendor-key.ts (1)
  • normalizeVendorKeyFromUrl (1-11)
src/app/[locale]/settings/providers/_components/vendors/endpoint-add-dialog.tsx (2)
src/actions/provider-endpoints.ts (1)
  • createProviderEndpointAction (61-117)
src/types/provider.ts (1)
  • ProviderType (6-12)
src/repository/provider-endpoint.ts (4)
src/drizzle/schema.ts (1)
  • providerEndpoints (306-329)
src/types/provider.ts (2)
  • ProviderEndpoint (394-405)
  • ProviderType (6-12)
src/drizzle/db.ts (1)
  • db (37-44)
src/repository/_shared/transformers.ts (1)
  • toProviderEndpoint (142-155)
src/app/[locale]/dashboard/availability/_components/availability-endpoints-view.tsx (1)
src/lib/endpoint-availability/types.ts (2)
  • EndpointAvailabilitySummary (28-42)
  • EndpointTimeBucketMetrics (15-26)
src/app/[locale]/settings/providers/_components/vendors/vendor-endpoints-manager.tsx (4)
src/actions/provider-endpoints.ts (1)
  • getProviderEndpointsByVendors (22-38)
src/types/provider.ts (1)
  • ProviderType (6-12)
src/app/[locale]/settings/providers/_components/vendors/endpoint-add-dialog.tsx (1)
  • EndpointAddDialog (55-202)
src/app/[locale]/settings/providers/_components/vendors/endpoint-list-item.tsx (1)
  • EndpointListItem (12-57)
src/actions/provider-endpoints.ts (2)
src/types/provider.ts (2)
  • ProviderEndpoint (394-405)
  • ProviderType (6-12)
src/repository/provider-endpoint.ts (6)
  • findProviderEndpointsByVendorIds (31-47)
  • findProviderEndpointsByVendorType (49-66)
  • createProviderEndpoint (68-89)
  • updateProviderEndpoint (91-114)
  • findProviderEndpointById (22-29)
  • deleteProviderEndpoint (116-128)
src/actions/provider-vendors.ts (7)
src/lib/auth.ts (1)
  • getSession (116-128)
src/repository/provider-vendor.ts (6)
  • ProviderVendorSummary (75-78)
  • findProviderVendorSummaries (80-125)
  • updateProviderVendor (158-191)
  • MergeProviderVendorsResult (231-239)
  • mergeProviderVendors (241-395)
  • splitProviderVendor (404-492)
src/lib/logger.ts (1)
  • logger (168-187)
src/actions/types.ts (1)
  • ActionResult (31-31)
src/types/provider.ts (1)
  • ProviderVendor (382-392)
src/lib/utils/error-messages.ts (1)
  • ERROR_CODES (110-117)
src/lib/utils/vendor-key.ts (1)
  • normalizeVendorKeyFromUrl (1-11)
src/app/v1/_lib/proxy/endpoint-resolver.ts (5)
src/types/provider.ts (2)
  • ProviderEndpoint (394-405)
  • Provider (39-145)
src/repository/provider-endpoint.ts (1)
  • findProviderEndpointsByVendorType (49-66)
src/app/v1/_lib/proxy/provider-selector.ts (1)
  • weightedRandom (1053-1072)
src/lib/logger.ts (1)
  • logger (168-187)
src/lib/endpoint-circuit-breaker.ts (2)
  • openVendorTypeFuse (157-177)
  • isEndpointCircuitOpen (76-97)
src/app/v1/_lib/proxy/response-handler.ts (1)
src/lib/endpoint-circuit-breaker.ts (2)
  • recordEndpointSuccess (134-155)
  • recordEndpointFailure (99-132)
tests/unit/lib/endpoint-availability.test.ts (1)
src/lib/endpoint-availability/endpoint-availability-service.ts (1)
  • queryEndpointAvailability (22-268)
src/actions/providers.ts (5)
src/lib/utils/vendor-key.ts (1)
  • normalizeVendorKeyFromUrl (1-11)
src/repository/provider-vendor.ts (1)
  • ensureProviderVendor (193-229)
src/repository/provider-endpoint.ts (1)
  • ensureProviderEndpoint (130-156)
src/repository/provider.ts (1)
  • findProviderById (284-344)
src/repository/index.ts (1)
  • findProviderById (42-42)
tests/unit/repository/provider-vendor.test.ts (1)
src/repository/provider-vendor.ts (2)
  • mergeProviderVendors (241-395)
  • splitProviderVendor (404-492)
src/repository/provider.ts (1)
src/drizzle/schema.ts (1)
  • providers (131-278)
🪛 Gitleaks (8.30.0)
tests/unit/actions/providers.test.ts

[high] 117-117: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

⏰ 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). (6)
  • GitHub Check: pr-review
  • GitHub Check: Docker Build Test
  • GitHub Check: pr-description
  • GitHub Check: 🌐 API Tests
  • GitHub Check: pr-label
  • GitHub Check: dev-build-deploy

Comment on lines +1 to +53
{
"title": "Vendors",
"description": "Manage AI model vendors and their API endpoints",
"searchPlaceholder": "Search vendors...",
"empty": {
"title": "No vendors found",
"description": "Try adjusting your search terms"
},
"stats": {
"providers": "Providers",
"endpoints": "Endpoints"
},
"actions": {
"configure": "Configure",
"updated": "Vendor updated successfully",
"saving": "Saving...",
"save": "Save Changes"
},
"edit": {
"title": "Edit Vendor",
"description": "Update vendor details and manage API endpoints"
},
"fields": {
"name": "Display Name",
"website": "Website URL",
"enabled": "Enabled"
},
"endpoints": {
"title": "API Endpoints",
"count": "Total endpoints",
"add": "Add Endpoint",
"empty": "No endpoints configured",
"baseUrl": "Base URL",
"priority": "Priority",
"weight": "Weight",
"createSuccess": "Endpoint created successfully",
"addTitle": "Add Endpoint",
"addDescription": "Add a new API endpoint for this vendor",
"fields": {
"type": "Provider Type",
"baseUrl": "Base URL",
"priority": "Priority",
"weight": "Weight"
},
"actions": {
"create": "Create"
},
"errors": {
"conflict": "Endpoint already exists",
"createFailed": "Failed to create endpoint"
}
}
}
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
# Check for vendors.json files in all language directories
echo "=== Checking vendors.json translation files across all languages ==="
for lang in en zh-CN zh-TW ja ru; do
  filepath="messages/${lang}/settings/providers/vendors.json"
  if [ -f "$filepath" ]; then
    echo "$filepath exists"
    # Show file size to confirm it has content
    wc -l "$filepath" | awk '{print "  Lines: " $1}'
  else
    echo "$filepath missing"
  fi
done

# Also check what language directories exist
echo ""
echo "=== Available language directories ==="
ls -d messages/*/ 2>/dev/null | sed 's|messages/||g' | sed 's|/||g'

Repository: ding113/claude-code-hub

Length of output: 494


🏁 Script executed:

#!/bin/bash
# Verify the JSON structure of the existing files
echo "=== Verifying JSON validity ==="
for filepath in messages/en/settings/providers/vendors.json messages/zh-CN/settings/providers/vendors.json; do
  if [ -f "$filepath" ]; then
    echo "Checking $filepath..."
    if python3 -m json.tool "$filepath" > /dev/null 2>&1; then
      echo "  ✓ Valid JSON"
    else
      echo "  ✗ Invalid JSON"
      python3 -m json.tool "$filepath" 2>&1 | head -5
    fi
  fi
done

# Check if there are other vendor-related i18n files
echo ""
echo "=== Checking for other vendors-related translation files ==="
find messages -name "*vendor*" -type f 2>/dev/null | sort

Repository: ding113/claude-code-hub

Length of output: 153


🏁 Script executed:

#!/bin/bash
# Simpler check - just verify files exist and check their structure with basic tools
echo "=== File existence and basic structure check ==="
for filepath in messages/en/settings/providers/vendors.json messages/zh-CN/settings/providers/vendors.json; do
  if [ -f "$filepath" ]; then
    echo "$filepath exists"
    # Check if file starts with { and ends with }
    first_char=$(head -c 1 "$filepath")
    last_char=$(tail -c 1 "$filepath")
    if [ "$first_char" = "{" ] && [ "$last_char" = "}" ]; then
      echo "  ✓ Valid JSON structure (starts with { and ends with })"
    fi
  fi
done

# Count total number of language directories
echo ""
echo "=== Language directories summary ==="
total_langs=$(ls -d messages/*/ 2>/dev/null | wc -l)
echo "Total language directories: $total_langs"

# Check which language has vendors.json
echo ""
echo "=== vendors.json availability by language ==="
for lang in en zh-CN zh-TW ja ru; do
  if [ -f "messages/${lang}/settings/providers/vendors.json" ]; then
    echo "$lang"
  else
    echo "$lang"
  fi
done

Repository: ding113/claude-code-hub

Length of output: 370


缺少三个语言版本的翻译文件

当前 PR 仅包含 en 和 zh-CN 的 vendors.json 翻译文件,缺少项目编码规范要求的其他三个语言版本:zh-TW、ja、ru。

根据编码规范,所有用户界面字符串必须支持全部 5 种语言(zh-CN、zh-TW、en、ja、ru),不能遗漏。请补充这三个语言的翻译文件。

🤖 Prompt for AI Agents
In @messages/en/settings/providers/vendors.json around lines 1 - 53, The PR is
missing vendors.json translations for zh-TW, ja, and ru; add three new files
named vendors.json for locales zh-TW, ja, and ru containing the exact same key
structure as the provided en file (keys like "title", "description",
"searchPlaceholder", "empty", "stats", "actions", "edit", "fields", "endpoints"
and its nested keys such as "title", "count", "add", "fields.type",
"errors.conflict", etc.), populating the values with the appropriate
translations (or temporary copied English strings if translations are pending)
so all five locales (zh-CN, zh-TW, en, ja, ru) support the same UI keys.

Comment on lines 723 to 727
const payload = {
...validated,
...(faviconUrl !== undefined && { favicon_url: faviconUrl }),
...(vendor ? { vendor_id: vendor.id } : {}),
};
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
# Check if there's other handling of vendor_id clearing logic
rg -n "vendor_id.*null" --type=ts src/actions/ src/repository/

Repository: ding113/claude-code-hub

Length of output: 208


🏁 Script executed:

#!/bin/bash
# Read the file to see broader context around lines 723-727
wc -l src/actions/providers.ts

Repository: ding113/claude-code-hub

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Get context around the problematic lines
sed -n '700,750p' src/actions/providers.ts

Repository: ding113/claude-code-hub

Length of output: 1787


🏁 Script executed:

#!/bin/bash
# Search for where this payload is used and how the update is performed
rg -n "vendor_id" --type=ts src/actions/providers.ts -B 2 -A 2

Repository: ding113/claude-code-hub

Length of output: 445


在 vendor 为 null 时应显式清除 vendor_id

vendorKey 解析失败导致 vendornull 时,条件式 spread 操作符 ...(vendor ? { vendor_id: vendor.id } : {}) 不会添加 vendor_id 字段,导致更新时保留旧的 vendor_id。

这与同文件第 531 行的实现不一致,那里使用了 vendor_id: vendor?.id ?? null 来显式清除 vendor_id。建议改为:

...(vendor ? { vendor_id: vendor.id } : { vendor_id: null }),

或采用与第 531 行相同的模式以保持一致性。

🤖 Prompt for AI Agents
In @src/actions/providers.ts around lines 723 - 727, The payload construction
currently omits vendor_id when vendor is null which preserves the old vendor_id;
change the spread that builds payload (the const payload block) to always
include vendor_id by setting it to vendor?.id ?? null (or using ...(vendor ? {
vendor_id: vendor.id } : { vendor_id: null })) so that vendor_id is explicitly
cleared when vendor is null.

Comment on lines +274 to +275
{t("summary.healthyEndpoints")}
</CardTitle>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find the structure of the project and locate i18n/language files
find . -type f -name "*.json" | grep -E "(i18n|lang|translation|locale)" | head -20

Repository: ding113/claude-code-hub

Length of output: 143


🏁 Script executed:

# Also list files in common i18n directory patterns
git ls-files | grep -E "(i18n|locales?|translations?)" | head -30

Repository: ding113/claude-code-hub

Length of output: 1681


🏁 Script executed:

# Check the file under review to understand the structure
cat -n src/app/[locale]/dashboard/availability/_components/availability-endpoints-view.tsx | sed -n '270,280p'

Repository: ding113/claude-code-hub

Length of output: 613


🏁 Script executed:

# List all files in the messages directory
ls -la messages/ | head -50

Repository: ding113/claude-code-hub

Length of output: 615


🏁 Script executed:

# Find all JSON files that might contain translations
find . -type f -name "*.json" | grep -E "messages|locales" | sort

Repository: ding113/claude-code-hub

Length of output: 13339


🏁 Script executed:

# Check if there are language-specific files
git ls-files messages/ | head -50

Repository: ding113/claude-code-hub

Length of output: 2039


🏁 Script executed:

# Check dashboard.json files for all languages to find the required keys
echo "=== zh-CN dashboard.json ===" && cat messages/zh-CN/dashboard.json | python3 -m json.tool | grep -A2 -B2 "healthyEndpoints\|unhealthyEndpoints\|unknownEndpoints\|noEndpoints\|noProbes"

Repository: ding113/claude-code-hub

Length of output: 153


🏁 Script executed:

echo "=== en dashboard.json ===" && cat messages/en/dashboard.json | python3 -m json.tool | grep -A2 -B2 "healthyEndpoints\|unhealthyEndpoints\|unknownEndpoints\|noEndpoints\|noProbes"

Repository: ding113/claude-code-hub

Length of output: 153


🏁 Script executed:

echo "=== ja dashboard.json ===" && cat messages/ja/dashboard.json | python3 -m json.tool | grep -A2 -B2 "healthyEndpoints\|unhealthyEndpoints\|unknownEndpoints\|noEndpoints\|noProbes"

Repository: ding113/claude-code-hub

Length of output: 153


🏁 Script executed:

echo "=== ru dashboard.json ===" && cat messages/ru/dashboard.json | python3 -m json.tool | grep -A2 -B2 "healthyEndpoints\|unhealthyEndpoints\|unknownEndpoints\|noEndpoints\|noProbes"

Repository: ding113/claude-code-hub

Length of output: 153


🏁 Script executed:

echo "=== zh-TW dashboard.json ===" && cat messages/zh-TW/dashboard.json | python3 -m json.tool | grep -A2 -B2 "healthyEndpoints\|unhealthyEndpoints\|unknownEndpoints\|noEndpoints\|noProbes"

Repository: ding113/claude-code-hub

Length of output: 153


🏁 Script executed:

# Use rg to search for the keys in dashboard.json files more efficiently
echo "=== Searching for healthyEndpoints ===" && rg "healthyEndpoints" messages/ --type json

Repository: ding113/claude-code-hub

Length of output: 105


🏁 Script executed:

echo "=== Searching for unhealthyEndpoints ===" && rg "unhealthyEndpoints" messages/ --type json

Repository: ding113/claude-code-hub

Length of output: 107


🏁 Script executed:

echo "=== Searching for unknownEndpoints ===" && rg "unknownEndpoints" messages/ --type json

Repository: ding113/claude-code-hub

Length of output: 105


🏁 Script executed:

echo "=== Searching for noEndpoints ===" && rg "noEndpoints" messages/ --type json

Repository: ding113/claude-code-hub

Length of output: 100


🏁 Script executed:

echo "=== Searching for probes key ===" && rg '"probes"' messages/ --type json

Repository: ding113/claude-code-hub

Length of output: 204


🏁 Script executed:

echo "=== Searching for noProbes ===" && rg "noProbes" messages/ --type json

Repository: ding113/claude-code-hub

Length of output: 208


🏁 Script executed:

# Check the actual component to see all keys being used
cat -n src/app/[locale]/dashboard/availability/_components/availability-endpoints-view.tsx | grep -A1 -B1 't("' | head -40

Repository: ding113/claude-code-hub

Length of output: 1924


🏁 Script executed:

# Search for these keys in all dashboard-related JSON files
echo "=== Searching in all dashboard JSON files ===" && rg "healthyEndpoints|unhealthyEndpoints|unknownEndpoints" messages/

Repository: ding113/claude-code-hub

Length of output: 112


🏁 Script executed:

# Check common.json files as well
echo "=== Checking common.json files ===" && rg "healthyEndpoints|unhealthyEndpoints|unknownEndpoints|noEndpoints" messages/*/common.json

Repository: ding113/claude-code-hub

Length of output: 101


🏁 Script executed:

# Let's check the full component to see all the keys it uses
wc -l src/app/[locale]/dashboard/availability/_components/availability-endpoints-view.tsx

Repository: ding113/claude-code-hub

Length of output: 154


🏁 Script executed:

# Check lines around the mentioned line numbers
cat -n src/app/[locale]/dashboard/availability/_components/availability-endpoints-view.tsx | sed -n '350,400p'

Repository: ding113/claude-code-hub

Length of output: 2931


🏁 Script executed:

# Let's look at the structure of dashboard.json files to understand the format
head -50 messages/zh-CN/dashboard.json

Repository: ding113/claude-code-hub

Length of output: 1481


🏁 Script executed:

# Search for summary prefix keys
rg '"summary' messages/zh-CN/dashboard.json

Repository: ding113/claude-code-hub

Length of output: 83


🏁 Script executed:

# Search for heatmap prefix keys
rg '"heatmap' messages/zh-CN/dashboard.json

Repository: ding113/claude-code-hub

Length of output: 83


🏁 Script executed:

# Extract the summary section from zh-CN dashboard.json
cat messages/zh-CN/dashboard.json | python3 -c "import json, sys; d=json.load(sys.stdin); print(json.dumps(d.get('summary', {}), indent=2))"

Repository: ding113/claude-code-hub

Length of output: 69


🏁 Script executed:

# Extract the heatmap section from zh-CN dashboard.json
cat messages/zh-CN/dashboard.json | python3 -c "import json, sys; d=json.load(sys.stdin); print(json.dumps(d.get('heatmap', {}), indent=2))"

Repository: ding113/claude-code-hub

Length of output: 69


🏁 Script executed:

# Check all languages for summary section
echo "=== zh-CN summary ===" && rg -A 10 '"summary"' messages/zh-CN/dashboard.json | head -15

Repository: ding113/claude-code-hub

Length of output: 365


🏁 Script executed:

# Check all languages for heatmap section
echo "=== zh-CN heatmap ===" && rg -A 10 '"heatmap"' messages/zh-CN/dashboard.json | head -15

Repository: ding113/claude-code-hub

Length of output: 334


🏁 Script executed:

# Check en dashboard.json summary
echo "=== en summary ===" && rg -A 10 '"summary"' messages/en/dashboard.json | head -15

Repository: ding113/claude-code-hub

Length of output: 431


🏁 Script executed:

# Check en dashboard.json heatmap
echo "=== en heatmap ===" && rg -A 10 '"heatmap"' messages/en/dashboard.json | head -15

Repository: ding113/claude-code-hub

Length of output: 386


🏁 Script executed:

# Check for states.noEndpoints
echo "=== Searching for noEndpoints ===" && rg "noEndpoints" messages/ --type json

Repository: ding113/claude-code-hub

Length of output: 100


🏁 Script executed:

# Check ja dashboard.json for heatmap
echo "=== ja heatmap ===" && rg -A 10 '"heatmap"' messages/ja/dashboard.json | head -15

Repository: ding113/claude-code-hub

Length of output: 351


🏁 Script executed:

# Check ru dashboard.json for heatmap
echo "=== ru heatmap ===" && rg -A 10 '"heatmap"' messages/ru/dashboard.json | head -15

Repository: ding113/claude-code-hub

Length of output: 412


🏁 Script executed:

# Check zh-TW dashboard.json for heatmap
echo "=== zh-TW heatmap ===" && rg -A 10 '"heatmap"' messages/zh-TW/dashboard.json | head -15

Repository: ding113/claude-code-hub

Length of output: 331


🏁 Script executed:

# Check for states.noEndpoints in all language files
echo "=== Searching states.noEndpoints ===" && rg '"noEndpoints"' messages/ --type json

Repository: ding113/claude-code-hub

Length of output: 103


🏁 Script executed:

# Search for "states" prefix to understand the structure
echo "=== Checking states in zh-CN ===" && rg -A 20 '"states"' messages/zh-CN/dashboard.json | head -25

Repository: ding113/claude-code-hub

Length of output: 716


🏁 Script executed:

# Search for all occurrences of these keys in the translation files to get complete picture
echo "=== Checking all summary keys in all languages ===" && rg '"summary"' messages/*/dashboard.json -A 6

Repository: ding113/claude-code-hub

Length of output: 2174


🏁 Script executed:

# Check for endpoints keyword anywhere in translations
echo "=== Searching for 'endpoints' ===" && rg "endpoints" messages/ --type json

Repository: ding113/claude-code-hub

Length of output: 752


🏁 Script executed:

# Check what the code actually references
echo "=== All t() calls in the component ===" && grep -o 't("[^"]*")' src/app/[locale]/dashboard/availability/_components/availability-endpoints-view.tsx | sort | uniq

Repository: ding113/claude-code-hub

Length of output: 898


修复 i18n 键名称,确保代码使用的键与翻译文件匹配。

代码使用的 i18n 键不存在于翻译文件中:

  • summary.healthyEndpoints/unhealthyEndpoints/unknownEndpoints 应为 summary.healthyProviders/unhealthyProviders/unknownProviders(已存在于所有 5 种语言)
  • states.noEndpoints 在任何语言文件中都不存在
  • heatmap.probesheatmap.noProbes 仅存在于 zh-CN 和 en,缺少 ja、ru、zh-TW 的翻译
🤖 Prompt for AI Agents
In
@src/app/[locale]/dashboard/availability/_components/availability-endpoints-view.tsx
around lines 274 - 275, In availability-endpoints-view.tsx update the i18n keys
used with the translation helper t(): replace summary.healthyEndpoints,
summary.unhealthyEndpoints, summary.unknownEndpoints with the existing keys
summary.healthyProviders, summary.unhealthyProviders, summary.unknownProviders
(used across all locales), remove or replace the nonexistent states.noEndpoints
key with a valid existing key (or add states.noEndpoints to locale files if
intended), and ensure heatmap.probes and heatmap.noProbes are added to ja, ru
and zh-TW locale files (or change calls to a key that exists in those locales);
search for all t("...") usages in this file/component and make the key
replacements or add the missing translations in the locale JSONs so keys match
across all five languages.

Comment on lines +324 to +327
<SelectItem value="availability">{t("sort.availability")}</SelectItem>
<SelectItem value="name">{t("sort.name")}</SelectItem>
<SelectItem value="probes">{t("sort.probes")}</SelectItem>
</SelectContent>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find i18n or translation-related directories and files
fd -e json -e ts -e tsx | grep -i "i18n\|locale\|trans\|lang" | head -20

Repository: ding113/claude-code-hub

Length of output: 1087


🏁 Script executed:

# Also check common i18n file locations
find . -type f -name "*.json" | grep -E "(i18n|locale|lang|trans)" | head -20

Repository: ding113/claude-code-hub

Length of output: 143


🏁 Script executed:

# Look for dashboard-related translation files
find . -type f -name "*.json" | grep -i "dashboard" | head -20

Repository: ding113/claude-code-hub

Length of output: 217


🏁 Script executed:

# Search for sort.probes in all language files
for lang in en ja ru zh-CN zh-TW; do
  echo "=== Checking messages/$lang/dashboard.json ==="
  rg -A 2 '"sort"' "messages/$lang/dashboard.json" 2>/dev/null | head -30
done

Repository: ding113/claude-code-hub

Length of output: 651


🏁 Script executed:

# More specific search for the sort.probes key
for lang in en ja ru zh-CN zh-TW; do
  echo "=== $lang: sort.probes ==="
  rg 'sort\.probes|"probes"' "messages/$lang/dashboard.json" 2>/dev/null || echo "NOT FOUND"
done

Repository: ding113/claude-code-hub

Length of output: 270


🏁 Script executed:

# Check the structure of sort section in one file
cat messages/en/dashboard.json | jq '.sort' 2>/dev/null || cat messages/en/dashboard.json | grep -A 10 '"sort"'

Repository: ding113/claude-code-hub

Length of output: 71


🏁 Script executed:

# Check all three sort keys across all languages
for lang in en ja ru zh-CN zh-TW; do
  echo "=== $lang ==="
  rg '"name"|"availability"|"probes"' "messages/$lang/dashboard.json" | grep -E '(availability|name|probes)' | head -5
done

Repository: ding113/claude-code-hub

Length of output: 699


🏁 Script executed:

# Get full sort section from each file
for lang in en ja ru zh-CN zh-TW; do
  echo "=== messages/$lang/dashboard.json ==="
  cat "messages/$lang/dashboard.json" | python3 -c "import sys, json; data = json.load(sys.stdin); print(json.dumps(data.get('sort', {}), indent=2, ensure_ascii=False))" 2>/dev/null || echo "Error parsing"
done

Repository: ding113/claude-code-hub

Length of output: 153


🏁 Script executed:

# Check what translations exist for sort section, focusing on probes
for lang in en ja ru zh-CN zh-TW; do
  echo "=== $lang/dashboard.json ==="
  cat "messages/$lang/dashboard.json" | grep -A 6 '"sort"' | head -8
done

Repository: ding113/claude-code-hub

Length of output: 921


添加缺失的 i18n 键 sort.probes

代码使用了 t("sort.probes") 但该键在所有语言文件中均不存在。翻译文件(messages/{en,ja,ru,zh-CN,zh-TW}/dashboard.json)的 sort 部分只包含 labelavailabilitynamerequests 键,缺少 probes 键。

需要添加 probes 键到所有 5 个语言文件中,或将代码改为使用 t("sort.requests")

🤖 Prompt for AI Agents
In
@src/app/[locale]/dashboard/availability/_components/availability-endpoints-view.tsx
around lines 324 - 327, The UI references t("sort.probes") inside the SelectItem
options (see SelectItem entries) but that i18n key is missing from all locale
messages; either add a "probes" key under the "sort" object in each
messages/{en,ja,ru,zh-CN,zh-TW}/dashboard.json with appropriate translations, or
change the component to use the existing key t("sort.requests") instead of
t("sort.probes"); update all 5 language files if adding the key so translations
are present across locales, or update the SelectItem value/text to consistently
use the existing "requests" key.

Comment on lines +52 to +53
<TabsTrigger value="providers">{t("tabs.providers")}</TabsTrigger>
<TabsTrigger value="endpoints">{t("tabs.endpoints")}</TabsTrigger>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's find the i18n file structure
find . -type f -name "*.json" | grep -E "(i18n|locales|translations|lang)" | head -20

Repository: ding113/claude-code-hub

Length of output: 190


🏁 Script executed:

# Also look for dashboard-related JSON files
find . -type f -name "*dashboard*" | grep -E "\.(json|ts|tsx)" | head -20

Repository: ding113/claude-code-hub

Length of output: 862


🏁 Script executed:

# Check the file structure near the dashboard page
ls -la src/app/\[locale\]/dashboard/availability/ 2>/dev/null || find . -path "*/dashboard/availability*" -type f

Repository: ding113/claude-code-hub

Length of output: 300


🏁 Script executed:

# Check all dashboard.json files for tabs.providers and tabs.endpoints
for file in ./messages/*/dashboard.json; do
  echo "=== $file ==="
  rg -A 10 '"tabs"' "$file" || echo "No 'tabs' key found"
done

Repository: ding113/claude-code-hub

Length of output: 3303


🏁 Script executed:

# Also check the page.tsx file to confirm the usage
cat -n src/app/\[locale\]/dashboard/availability/page.tsx | sed -n '45,60p'

Repository: ding113/claude-code-hub

Length of output: 826


缺少 tabs.endpoints 的 i18n 翻译。

验证发现 tabs.providers 键已在所有支持的语言文件中存在,但 tabs.endpoints 键在所有 5 个语言文件中均缺失。代码在第 52-53 行使用 t("tabs.endpoints"),需要将该键添加到以下所有语言文件中:

  • ./messages/en/dashboard.json
  • ./messages/ja/dashboard.json
  • ./messages/ru/dashboard.json
  • ./messages/zh-CN/dashboard.json
  • ./messages/zh-TW/dashboard.json
🤖 Prompt for AI Agents
In @src/app/[locale]/dashboard/availability/page.tsx around lines 52 - 53, Add
the missing i18n key "tabs.endpoints" used by t("tabs.endpoints") (TabsTrigger
value="endpoints") to every locale translation so the tab label renders; update
each locale file (en, ja, ru, zh-CN, zh-TW) by adding a "tabs": { "endpoints":
"<appropriate translation>" } entry alongside the existing "tabs.providers" key,
keeping JSON structure and escaping consistent.

Comment on lines +323 to +329
}, (table) => ({
providerEndpointsVendorTypeIdx: index('idx_provider_endpoints_vendor_type').on(table.vendorId, table.providerType).where(sql`${table.deletedAt} IS NULL`),
providerEndpointsEnabledPriorityIdx: index('idx_provider_endpoints_enabled_priority').on(table.isEnabled, table.vendorId, table.providerType, table.priority, table.weight).where(sql`${table.deletedAt} IS NULL`),
providerEndpointsVendorTypeBaseUrlUniqueIdx: uniqueIndex('uniq_provider_endpoints_vendor_type_base_url').on(table.vendorId, table.providerType, table.baseUrl),
providerEndpointsCreatedAtIdx: index('idx_provider_endpoints_created_at').on(table.createdAt),
providerEndpointsDeletedAtIdx: index('idx_provider_endpoints_deleted_at').on(table.deletedAt),
}));
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

唯一索引未排除软删除记录可能导致问题。

uniq_provider_endpoints_vendor_type_base_url 唯一索引没有添加 WHERE deleted_at IS NULL 条件。这意味着:

  1. 软删除的端点仍占用唯一性约束
  2. 无法重新创建与已删除端点相同的 vendor+type+baseUrl 组合

建议修改为部分唯一索引,或在应用层处理此场景。

建议修改
- providerEndpointsVendorTypeBaseUrlUniqueIdx: uniqueIndex('uniq_provider_endpoints_vendor_type_base_url').on(table.vendorId, table.providerType, table.baseUrl),
+ providerEndpointsVendorTypeBaseUrlUniqueIdx: uniqueIndex('uniq_provider_endpoints_vendor_type_base_url').on(table.vendorId, table.providerType, table.baseUrl).where(sql`${table.deletedAt} 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
}, (table) => ({
providerEndpointsVendorTypeIdx: index('idx_provider_endpoints_vendor_type').on(table.vendorId, table.providerType).where(sql`${table.deletedAt} IS NULL`),
providerEndpointsEnabledPriorityIdx: index('idx_provider_endpoints_enabled_priority').on(table.isEnabled, table.vendorId, table.providerType, table.priority, table.weight).where(sql`${table.deletedAt} IS NULL`),
providerEndpointsVendorTypeBaseUrlUniqueIdx: uniqueIndex('uniq_provider_endpoints_vendor_type_base_url').on(table.vendorId, table.providerType, table.baseUrl),
providerEndpointsCreatedAtIdx: index('idx_provider_endpoints_created_at').on(table.createdAt),
providerEndpointsDeletedAtIdx: index('idx_provider_endpoints_deleted_at').on(table.deletedAt),
}));
}, (table) => ({
providerEndpointsVendorTypeIdx: index('idx_provider_endpoints_vendor_type').on(table.vendorId, table.providerType).where(sql`${table.deletedAt} IS NULL`),
providerEndpointsEnabledPriorityIdx: index('idx_provider_endpoints_enabled_priority').on(table.isEnabled, table.vendorId, table.providerType, table.priority, table.weight).where(sql`${table.deletedAt} IS NULL`),
providerEndpointsVendorTypeBaseUrlUniqueIdx: uniqueIndex('uniq_provider_endpoints_vendor_type_base_url').on(table.vendorId, table.providerType, table.baseUrl).where(sql`${table.deletedAt} IS NULL`),
providerEndpointsCreatedAtIdx: index('idx_provider_endpoints_created_at').on(table.createdAt),
providerEndpointsDeletedAtIdx: index('idx_provider_endpoints_deleted_at').on(table.deletedAt),
}));
🤖 Prompt for AI Agents
In @src/drizzle/schema.ts around lines 323 - 329, The unique index
providerEndpointsVendorTypeBaseUrlUniqueIdx
(uniq_provider_endpoints_vendor_type_base_url) currently does not exclude
soft-deleted rows; update its definition in the schema builder so the
uniqueIndex on table.vendorId, table.providerType, table.baseUrl is a
partial/filtered index with WHERE table.deletedAt IS NULL (matching how
providerEndpointsVendorTypeIdx and providerEndpointsEnabledPriorityIdx are
filtered) so soft-deleted records no longer block creating the same
vendor+type+baseUrl combo.

Comment on lines +307 to +321
const events = await db
.select({
endpointId: providerEndpointProbeEvents.endpointId,
result: providerEndpointProbeEvents.result,
checkedAt: providerEndpointProbeEvents.checkedAt,
})
.from(providerEndpointProbeEvents)
.where(
and(
inArray(providerEndpointProbeEvents.endpointId, endpointIdList),
gte(providerEndpointProbeEvents.checkedAt, fifteenMinutesAgo),
lte(providerEndpointProbeEvents.checkedAt, now)
)
)
.orderBy(desc(providerEndpointProbeEvents.checkedAt));
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

getCurrentEndpointStatus 的事件查询缺少 LIMIT

queryEndpointAvailability 不同,此函数查询最近 15 分钟的事件时没有添加 LIMIT 约束。在高流量场景下,这可能导致内存问题。

建议添加合理的限制
     .orderBy(desc(providerEndpointProbeEvents.checkedAt))
+    .limit(10000); // 或其他合理的限制
🤖 Prompt for AI Agents
In @src/lib/endpoint-availability/endpoint-availability-service.ts around lines
307 - 321, The events query in getCurrentEndpointStatus is missing a LIMIT which
can cause high memory usage under load; modify the query that selects from
providerEndpointProbeEvents (using endpointIdList, fifteenMinutesAgo, now) to
add a reasonable .limit(...) (or apply the same per-query cap used in
queryEndpointAvailability) so only a bounded number of recent probe rows are
returned and memory pressure is avoided.

Comment on lines +259 to +269
let succeeded = 0;
let failed = 0;

await runWithConcurrency(selected, ENDPOINT_PROBE_CONCURRENCY, async (target) => {
try {
await probeTarget(target);
succeeded++;
} catch {
failed++;
}
});
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

成功/失败计数可能不准确。

probeTarget 函数内部捕获了所有异常并记录到数据库,不会向外抛出异常。因此 catch 块(Line 266-268)实际上不会被触发,failed 计数器将始终为 0。

建议修改
  await runWithConcurrency(selected, ENDPOINT_PROBE_CONCURRENCY, async (target) => {
-   try {
-     await probeTarget(target);
-     succeeded++;
-   } catch {
-     failed++;
-   }
+   await probeTarget(target);
+   // probeTarget 内部已处理成功/失败逻辑并记录到数据库
  });
+ 
+ // 如需统计,可从数据库查询本次探测结果

或者让 probeTarget 返回成功/失败状态供外部统计。

📝 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
let succeeded = 0;
let failed = 0;
await runWithConcurrency(selected, ENDPOINT_PROBE_CONCURRENCY, async (target) => {
try {
await probeTarget(target);
succeeded++;
} catch {
failed++;
}
});
let succeeded = 0;
let failed = 0;
await runWithConcurrency(selected, ENDPOINT_PROBE_CONCURRENCY, async (target) => {
await probeTarget(target);
// probeTarget handles success/failure logic internally and logs to database
});
// To obtain statistics, query results from the database
🤖 Prompt for AI Agents
In @src/lib/endpoint-probe-scheduler.ts around lines 259 - 269, The
success/failure counters are inaccurate because probeTarget swallows errors and
never throws, so the catch branch in the runWithConcurrency callback never
executes; change probeTarget to return an explicit boolean/result (e.g., true on
success, false on failure) or an object with status, then in the
runWithConcurrency callback (the async function passed to runWithConcurrency)
inspect that return value to increment succeeded or failed accordingly (instead
of relying on try/catch), ensuring the callback calls await probeTarget(target)
and uses its returned status to update the succeeded/failed counters.

Comment on lines +130 to +156
export async function ensureProviderEndpoint(input: {
vendorId: number;
providerType: ProviderType;
baseUrl: string;
}): Promise<ProviderEndpoint> {
await db
.insert(providerEndpoints)
.values({
vendorId: input.vendorId,
providerType: input.providerType,
baseUrl: input.baseUrl,
isEnabled: true,
priority: 0,
weight: 1,
})
.onConflictDoNothing();

const rows = await findProviderEndpointsByVendorType(input.vendorId, input.providerType);
const matched = rows.find((r) => r.baseUrl === input.baseUrl);
if (matched) return matched;

return await createProviderEndpoint({
vendorId: input.vendorId,
providerType: input.providerType,
baseUrl: input.baseUrl,
});
}
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

ensureProviderEndpoint 存在潜在的竞态和边界问题。

当前实现流程:

  1. 尝试插入,冲突时不做任何操作
  2. 查询匹配记录
  3. 未找到则再次创建

问题场景:

  • 如果唯一索引包含软删除记录(如前面提到的),onConflictDoNothing 会静默失败,但后续查询因 isNull(deletedAt) 过滤会找不到记录,导致再次尝试创建并失败
  • 并发调用时可能产生重复插入尝试

建议使用 onConflictDoUpdate 或先查询再插入的模式。

🤖 Prompt for AI Agents
In @src/repository/provider-endpoint.ts around lines 130 - 156,
ensureProviderEndpoint can race and fail when a unique index collides with
soft-deleted rows or concurrent calls; replace the
insert+onConflictDoNothing+separate-query flow with an upsert that returns the
row: use db.insert(providerEndpoints).values(...).onConflict((cp) =>
cp.constraint(/* appropriate unique constraint or columns */)).doUpdateSet({
isEnabled: true, deletedAt: null, baseUrl: input.baseUrl, priority: 0, weight: 1
}).returningAll() (or the equivalent in your query builder) to revive/update
soft-deleted rows and get the row atomically, then return that row instead of
calling createProviderEndpoint; alternatively, perform a SELECT for existing
non-deleted row first (findProviderEndpointsByVendorType) and only insert if
absent, but prefer the upsert approach to avoid concurrent duplicate inserts and
handle soft-deletes.

Comment on lines +467 to +483
let ensuredEndpoints = 0;

for (const p of movedProviders) {
await tx
.insert(providerEndpoints)
.values({
vendorId: newVendor.id,
providerType: (p.providerType || "claude") as ProviderType,
baseUrl: p.url,
isEnabled: true,
priority: 0,
weight: 1,
})
.onConflictDoNothing();

ensuredEndpoints++;
}
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

ensuredEndpoints 计数不准确。

onConflictDoNothing 静默处理冲突,但计数器无论是否实际插入都会递增。如需准确统计,应检查 returning 结果或使用 onConflictDoUpdate 并检查实际变更行数。

建议修改
  for (const p of movedProviders) {
-   await tx
+   const [inserted] = await tx
      .insert(providerEndpoints)
      .values({
        vendorId: newVendor.id,
        providerType: (p.providerType || "claude") as ProviderType,
        baseUrl: p.url,
        isEnabled: true,
        priority: 0,
        weight: 1,
      })
-     .onConflictDoNothing();
+     .onConflictDoNothing()
+     .returning({ id: providerEndpoints.id });

-   ensuredEndpoints++;
+   if (inserted) ensuredEndpoints++;
  }
📝 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
let ensuredEndpoints = 0;
for (const p of movedProviders) {
await tx
.insert(providerEndpoints)
.values({
vendorId: newVendor.id,
providerType: (p.providerType || "claude") as ProviderType,
baseUrl: p.url,
isEnabled: true,
priority: 0,
weight: 1,
})
.onConflictDoNothing();
ensuredEndpoints++;
}
let ensuredEndpoints = 0;
for (const p of movedProviders) {
const [inserted] = await tx
.insert(providerEndpoints)
.values({
vendorId: newVendor.id,
providerType: (p.providerType || "claude") as ProviderType,
baseUrl: p.url,
isEnabled: true,
priority: 0,
weight: 1,
})
.onConflictDoNothing()
.returning({ id: providerEndpoints.id });
if (inserted) ensuredEndpoints++;
}

Copy link
Contributor

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

Choose a reason for hiding this comment

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

Code Review

This is a very comprehensive pull request that introduces a robust vendor-endpoint architecture. The changes are well-structured, covering the database schema, backend services, API endpoints, and the user interface. The inclusion of features like circuit breaking, health monitoring, and intelligent endpoint resolution is impressive. The code is generally of high quality, and the addition of extensive unit tests for the new logic is commendable. I've identified a few areas for improvement, mainly concerning error handling and internationalization consistency.


const currentProvider = await findProviderById(providerId);
if (!currentProvider) {
return { ok: false, error: "供应商不存在" };
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The error message "供应商不存在" is hardcoded in Chinese. This should be internationalized using next-intl's getTranslations for consistency with other action files and to support multiple languages.

Suggested change
return { ok: false, error: "供应商不存在" };
return { ok: false, error: tError("NOT_FOUND"), errorCode: ERROR_CODES.NOT_FOUND };

Comment on lines +103 to +105
const isUniqueViolation =
message.includes("uniq_provider_endpoints_vendor_type_base_url") ||
message.toLowerCase().includes("duplicate");
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Relying on string matching for database error messages is brittle and can break if the database version changes or if a different database driver is used. It's better to check for the specific error code for unique constraint violations if the database driver exposes it. For PostgreSQL, the code is typically '23505'.

Suggested change
const isUniqueViolation =
message.includes("uniq_provider_endpoints_vendor_type_base_url") ||
message.toLowerCase().includes("duplicate");
const isUniqueViolation = (error as { code?: string }).code === '23505';

Comment on lines +200 to +203
const isUniqueViolation =
message.includes("uniq_provider_vendors_vendor_key") ||
message.includes("provider_vendors_vendor_key") ||
message.toLowerCase().includes("duplicate");
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This error handling for unique constraint violations is based on string matching, which can be fragile. If possible, it would be more robust to check for the database-specific error code (e.g., '23505' for PostgreSQL). This would make the error handling less likely to break with database or driver updates.

@github-actions github-actions bot added the size/XL Extra Large PR (> 1000 lines) label Jan 13, 2026
queryClient.invalidateQueries({ queryKey: ["provider-vendors"] });
toast.success(t("actions.updated"));
} else {
toast.error(t("errors.updateFailed"));
Copy link
Contributor

Choose a reason for hiding this comment

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

[High] [STANDARD-VIOLATION] Missing i18n key errors.updateFailed

Why this is a problem: CLAUDE.md requires all user-facing strings to use i18n. The key errors.updateFailed is referenced here but is not defined in either messages/en/settings/providers/vendors.json or messages/zh-CN/settings/providers/vendors.json.

Suggested fix:
Add the missing key to both i18n files:

// messages/en/settings/providers/vendors.json
{
  "errors": {
    "updateFailed": "Failed to update vendor",
    "conflict": "Endpoint already exists",
    "createFailed": "Failed to create endpoint"
  }
}
// messages/zh-CN/settings/providers/vendors.json
{
  "errors": {
    "updateFailed": "更新厂商失败",
    "conflict": "端点已存在",
    "createFailed": "创建端点失败"
  }
}

render={({ field }) => (
<Select onValueChange={field.onChange} defaultValue={field.value}>
<SelectTrigger id="providerType">
<SelectValue placeholder="Select type" />
Copy link
Contributor

Choose a reason for hiding this comment

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

[High] [STANDARD-VIOLATION] Hardcoded user-facing strings violate i18n requirement

Why this is a problem: CLAUDE.md requires all user-facing strings to use i18n (5 languages supported). Lines 123-131 contain hardcoded English strings:

  • Line 123: "Select type"
  • Lines 126-131: Provider type labels ("OpenAI Compatible", "Claude", "Claude Auth", "Gemini", "Gemini CLI", "Codex")

Suggested fix:
Add i18n keys and use translations:

// Add to messages/en/settings/providers/vendors.json
"endpoints": {
  "fields": {
    "typePlaceholder": "Select type",
    "types": {
      "openai-compatible": "OpenAI Compatible",
      "claude": "Claude",
      "claude-auth": "Claude Auth",
      "gemini": "Gemini",
      "gemini-cli": "Gemini CLI",
      "codex": "Codex"
    }
  }
}
// In component:
<SelectValue placeholder={t("fields.typePlaceholder")} />
<SelectItem value="openai-compatible">{t("fields.types.openai-compatible")}</SelectItem>
// ... etc

@@ -0,0 +1,53 @@
{
Copy link
Contributor

Choose a reason for hiding this comment

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

[Critical] [STANDARD-VIOLATION] Missing i18n translations for 3 required languages

Why this is a problem: CLAUDE.md explicitly requires i18n support for 5 languages: zh-CN, zh-TW, en, ja, ru. This PR only adds translations for en and zh-CN, missing:

  • messages/zh-TW/settings/providers/vendors.json
  • messages/ja/settings/providers/vendors.json
  • messages/ru/settings/providers/vendors.json

Quoting CLAUDE.md:

i18n Required - All user-facing strings must use i18n (5 languages supported). Never hardcode display text

Suggested fix:
Create the missing translation files for zh-TW, ja, and ru with the same structure as the en/zh-CN files. At minimum, copy the English translations as placeholders and mark them for translation.

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Code Review Summary

This PR introduces a comprehensive vendor-endpoint architecture with intelligent routing and circuit breaker support. The implementation is well-structured with good test coverage for core components. However, there are critical i18n compliance issues that must be addressed before merging.

PR Size: XL

  • Lines changed: 8,144 (8,073 additions, 71 deletions)
  • Files changed: 57

Split Suggestion: Given the XL size, consider splitting into:

  1. Database schema and repository layer (migrations, schema.ts, repositories)
  2. Core services (circuit-breaker, probe-scheduler, endpoint-resolver)
  3. UI components and i18n (vendors manager, dashboard views, translations)

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
Standards (i18n) 1 2 0 0

Critical Issues (Must Fix)

  1. Missing i18n translations for 3 required languages (messages/en/settings/providers/vendors.json:1)
    • CLAUDE.md requires 5 languages: zh-CN, zh-TW, en, ja, ru
    • Only en and zh-CN translations are provided
    • Missing: zh-TW, ja, ru translation files for vendors.json

High Priority Issues (Should Fix)

  1. Missing i18n key errors.updateFailed (vendor-card.tsx:27)

    • Key is referenced but not defined in i18n files
  2. Hardcoded user-facing strings (endpoint-add-dialog.tsx:123-131)

    • Provider type labels and placeholder text are hardcoded in English
    • Should use i18n translations

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean (proper logging throughout)
  • Type safety - Clean (appropriate use of TypeScript)
  • Documentation accuracy - Clean
  • Test coverage - Good (unit tests for core components)
  • i18n compliance - Issues found

Positive Observations (not for praise, for context)

  • Circuit breaker implementation follows standard patterns with proper state transitions
  • Endpoint resolver correctly handles priority/weight-based selection
  • Probe scheduler has proper concurrency control and cleanup
  • Repository layer uses consistent patterns with soft deletes

Automated review by Claude AI

@ding113 ding113 closed this Jan 13, 2026
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Jan 13, 2026
@ding113 ding113 deleted the feat/vendor-endpoint-architecture branch January 27, 2026 09:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant