Skip to content

release v0.5.2#672

Closed
ding113 wants to merge 10 commits intomainfrom
dev
Closed

release v0.5.2#672
ding113 wants to merge 10 commits intomainfrom
dev

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Jan 28, 2026

Summary

Release v0.5.2 consolidates multiple feature enhancements and bug fixes from the dev branch, including timezone consistency improvements, lease-based rate limiting, dynamic probe intervals, Gemini image billing fixes, vendor reclustering, and dashboard UI improvements.

Included Changes

Features

Timezone Consistency & Lease-Based Quota (#668)

  • System-wide timezone setting stored in DB with IANA validation
  • Unified resolveSystemTimezone() with fallback chain: DB -> env TZ -> UTC
  • Lease-based budget slicing for 5h/daily/weekly/monthly rate limits
  • Rolling window semantics for time-based limits
  • Configurable lease percentage settings in system config

Dynamic Endpoint Probe Intervals (#669)

  • Three-tier priority system for probe frequency optimization
  • Base interval: 60s (configurable via ENDPOINT_PROBE_INTERVAL_MS)
  • Single-endpoint vendor: 10min (reduces unnecessary probing)
  • Timeout override: 10s (faster recovery for timeout errors)

Vendor Reclustering by Host:Port (#670)

  • computeVendorKey helper with host:port support for IP-based providers
  • When website_url is empty, use host:port as vendor key
  • Support for IPv6 addresses with [ipv6]:port format
  • ReclusterVendorsDialog UI component with preview/apply mode

Dashboard UI Improvements (#657)

  • Server timezone support for logs filtering
  • Provider chain popover improvements with exclusion reason translations
  • TagInput dropdown positioning fix inside dialogs
  • Provider vendor view enhancements with inline endpoint toggle
  • Cache badge alignment improvements

Bug Fixes

Gemini Image Billing Fix (#664)

  • Fixes billing calculation for IMAGE modality tokens in Gemini image generation models
  • IMAGE tokens now correctly billed at $0.00012/token (10x TEXT rate)
  • Extended UsageMetrics type to include output_image_tokens and input_image_tokens

Settings Persistence Fix

  • Fix config persistence on first save by revalidating all local caches

Server Actions Compliance

  • Make computeVendorKey async for Server Actions compliance

Related Issues

Database Migrations

4 new migrations included:

  1. 0048_add_system_timezone.sql - Add timezone column to system_settings
  2. 0057_conscious_quicksilver.sql - Token columns changed to bigint
  3. 0058_silly_sleepwalker.sql - Add lease configuration columns
  4. 0059_safe_xorn.sql - Change expires_at to timestamptz

Breaking Changes

Change Impact Migration
Default ENDPOINT_PROBE_INTERVAL_MS changed from 10s to 60s Existing deployments will probe less frequently Set ENDPOINT_PROBE_INTERVAL_MS=10000 to restore old behavior

Configuration

New Environment Variables

  • ENDPOINT_PROBE_INTERVAL_MS - Base probe interval (default: 60000ms)

New System Settings

  • timezone - IANA timezone (e.g., 'Asia/Shanghai')
  • quota_db_refresh_interval_seconds - Lease refresh interval (default: 10s)
  • quota_lease_percent_5h/daily/weekly/monthly - Lease percentages (default: 5%)
  • quota_lease_cap_usd - Optional cap in USD

Files Changed

  • 111 files changed across the codebase
  • i18n: All 5 languages updated (en, zh-CN, zh-TW, ja, ru)
  • Tests: Comprehensive test coverage added for all new features

Checklist

  • All included PRs have been reviewed and merged to dev
  • Database migrations generated and reviewed
  • i18n strings added for all 5 supported languages
  • Tests pass locally

Description enhanced by Claude AI

Greptile Overview

Greptile Summary

This release (v0.5.2) introduces several major features and improvements:

Key Features:

  • Lease-based rate limiting system: Implements budget slicing mechanism with Redis caching and database authoritative usage tracking. Uses configurable lease percentages (5h, daily, weekly, monthly windows) with atomic Lua script decrements for consistency.
  • System timezone support: Adds timezone configuration with fallback chain (DB → env TZ → UTC) and integrates with next-intl for consistent date/time handling across the application.
  • Provider vendor reclustering: New clustering logic uses host:port as vendor key when websiteUrl is empty (previously used only hostname). Includes admin UI with preview/confirm workflow.
  • Token overflow handling: Changes token columns from integer to bigint to handle large token counts without overflow.

Bug Fixes:

  • Fixed async function calls for getResetInfo and getResetInfoWithMode (were missing await)
  • Fixed Gemini image generation model billing for IMAGE modality tokens
  • Fixed cache revalidation to handle all locale paths for i18n routes

Improvements:

  • Dynamic endpoint probe intervals based on health status
  • Dashboard UI improvements with better loading states
  • Comprehensive test coverage for new features (1000+ new test lines)

Issues Found:

  • Two functions (computeVendorKey, deriveDisplayNameFromDomain) are declared async but don't use await statements - should be synchronous

Confidence Score: 4/5

  • This PR is mostly safe to merge with extensive test coverage and well-structured changes, but contains unnecessary async declarations
  • Score reflects comprehensive testing (18k+ lines changed with substantial test coverage), clear feature separation, and proper migration strategy. Minor style issues with async functions don't affect functionality but should be cleaned up.
  • src/repository/provider-endpoints.ts should have async removed from computeVendorKey and deriveDisplayNameFromDomain functions

Important Files Changed

Filename Overview
drizzle/0058_silly_sleepwalker.sql Adds quota lease configuration columns for the new lease-based rate limiting system
drizzle/0059_safe_xorn.sql Updates expires_at to timestamp with timezone and adds timezone column to system_settings
src/lib/rate-limit/lease.ts Implements core lease-based budget slicing mechanism with time window calculations
src/lib/rate-limit/lease-service.ts Implements lease service with Redis caching, DB queries, and atomic Lua script for budget decrements
src/lib/utils/timezone.ts Adds timezone validation and resolution with fallback chain: DB → env TZ → UTC
src/repository/provider-endpoints.ts Implements new vendor clustering logic using host:port when websiteUrl is empty
src/actions/providers.ts Adds reclusterProviderVendors action to re-cluster vendors with new clustering rules, fixes async getResetInfo calls
src/i18n/request.ts Integrates system timezone resolution into next-intl configuration for consistent timezone handling

Sequence Diagram

sequenceDiagram
    participant Client
    participant RateLimitGuard as Rate Limit Guard
    participant RateLimitService as Rate Limit Service
    participant LeaseService as Lease Service
    participant Redis
    participant DB as Database

    Client->>RateLimitGuard: API Request
    
    Note over RateLimitGuard: 13-layer check order<br/>Total limits, Resource protection<br/>Short-term and Long-term windows
    
    RateLimitGuard->>RateLimitService: checkCostLimits(entityId, window)
    RateLimitService->>LeaseService: getCostLease(entityId, window, limitAmount)
    
    alt Cache Hit in Redis
        LeaseService->>Redis: GET lease key
        Redis-->>LeaseService: Cached lease
        
        alt Lease expired or limit changed
            LeaseService->>LeaseService: refreshCostLeaseFromDb()
            LeaseService->>DB: Query usage in time window
            DB-->>LeaseService: Current usage
            LeaseService->>LeaseService: calculateLeaseSlice(limit, usage, percent, cap)
            LeaseService->>Redis: Store lease with TTL
        end
    else Cache Miss
        LeaseService->>DB: Query usage in time window
        DB-->>LeaseService: Current usage
        LeaseService->>LeaseService: calculateLeaseSlice(limit, usage, percent, cap)
        Note over LeaseService: Lease equals min of limit times percent<br/>remaining budget and cap USD
        LeaseService->>Redis: Store lease with TTL
    end
    
    LeaseService-->>RateLimitService: BudgetLease
    
    alt Budget sufficient
        RateLimitService-->>RateLimitGuard: allowed equals true
        RateLimitGuard-->>Client: Process request
        
        Note over Client,DB: After request completes
        Client->>LeaseService: decrementLeaseBudget(cost)
        LeaseService->>Redis: EVAL Lua script atomically
        Note over Redis: Atomic decrement with<br/>budget check and TTL preservation
        Redis-->>LeaseService: newRemaining
    else Budget insufficient
        RateLimitService-->>RateLimitGuard: allowed equals false with reason
        RateLimitGuard-->>Client: RateLimitError
    end
Loading

ding113 and others added 10 commits January 27, 2026 20:57
* feat(dashboard): enhance logs UI and provider circuit breaker display

- Add time range filter improvements with preset options
- Enhance provider chain popover with better error display
- Improve provider vendor view circuit breaker UI
- Add availability dashboard and provider endpoints display name tests
- Update i18n messages for provider chain and settings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: format code (feat-dashboard-ui-improvements-601d7b0)

* fix: make deriveDisplayNameFromDomain async for Server Actions compatibility

The file has "use server" directive, which requires all exported functions
to be async. This fixes the Next.js Turbopack build error:
"Server Actions must be async functions."

CI Run: https://github.com/ding113/claude-code-hub/actions/runs/21357337034

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix(ui): improve TagInput dropdown positioning inside Dialog

- Detect when TagInput is inside a Dialog and portal suggestions to dialog-content
- Use absolute positioning relative to dialog container instead of fixed viewport positioning
- Fix scroll/resize event listeners to target the correct scroll container
- Add unit tests for TagInput behavior inside Dialog

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: format code (feat-dashboard-ui-improvements-65ea8e5)

* fix(db): prevent token count overflow by using bigint columns

Change token-related columns in message_request table from integer to
bigint to prevent overflow when storing large token counts. Also update
all SQL aggregation queries to use double precision instead of ::int
casting to avoid overflow during SUM operations.

Changes:
- Schema: inputTokens, outputTokens, cacheCreationInputTokens,
  cacheReadInputTokens, cacheCreation5mInputTokens,
  cacheCreation1hInputTokens now use bigint
- Schema: providerVendorId is now NOT NULL
- Queries: All token SUM operations use ::double precision
- Tests: Add unit tests to verify no ::int casting in token aggregations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: format code (feat-dashboard-ui-improvements-7e73f97)

* fix(repository): handle multiple API prefixes in domain name derivation

Extend deriveDisplayNameFromDomain to skip common API prefixes (api, v1,
v2, v3, www) when extracting display names from domains. This fixes
handling of multi-prefix domains like v1.api.anthropic.com.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(dashboard): improve home layout with sidebar and responsive grid

- Refactor DashboardBento to use two-column layout with fixed-width
  sidebar (300px) for LiveSessionsPanel on admin view
- Update DashboardMain to remove max-w-7xl constraint on dashboard home
  page for wider content area
- Simplify ActiveSessionsSkeleton to match compact list style
- Add showTokensCost prop to SessionListItem and ActiveSessionsList
  for conditional token/cost display
- Fix router import to use i18n routing in ActiveSessionsList
- Add unit tests for layout behavior and SessionListItem props

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(dashboard): prevent decision chain overflow with text wrapping

Add overflow constraints to LogicTraceTab and StepCard components:
- Add min-w-0 to grid containers to allow content shrinking
- Add break-all to font-mono text for long IDs and URLs
- Add flex-wrap and truncate for provider name badges
- Add overflow-hidden to StepCard details container

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(dashboard): improve home layout alignment and structure

- Add max-w-7xl constraint to dashboard home container for consistent alignment with header
- Restructure dashboard-bento into 3 independent sections: metrics, chart, leaderboards
- Remove nested grid structure that caused misalignment on desktop
- Change admin layout from 2-column (content + sidebar) to 4-column grid (3 leaderboards + live sessions)
- Remove colSpan/rowSpan from StatisticsChartCard for full-width independent rendering
- Add min-h-[280px] to LeaderboardCard for consistent card heights
- Update tests to match new layout structure

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address code review feedback

- Remove redundant isDashboardHomePage conditional branch (now same as default)
- Remove trailing period from vendorAggregationRule for style consistency

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
* feat(probe): implement dynamic endpoint probe intervals

- Change probe scheduling from global interval to per-endpoint dynamic intervals:
  - Base interval: 60s (configurable via ENDPOINT_PROBE_INTERVAL_MS)
  - Single-endpoint vendor: 10min (reduces unnecessary probing)
  - Timeout override: 10s (faster recovery for timeout errors)
- Filter probes to only "due" endpoints based on lastProbedAt + effectiveInterval
- Remove type tabs from VendorEndpointsSection, show all endpoints in single list
- Display type icon with tooltip for each endpoint row
- Sort endpoints by type order (from getAllProviderTypes) then sortOrder
- Add type selector in AddEndpointButton dialog
- Update i18n strings for all 5 languages
- Add comprehensive unit tests for dynamic interval rules

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: derive selectableTypes from getAllProviderTypes

Address code review feedback: use getAllProviderTypes().filter() instead
of hardcoded array to ensure automatic sync when new provider types are added.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
问题背景:
- gemini-3-pro-image-preview 等图片生成模型返回的 usage 中包含 candidatesTokensDetails
- 该字段按 modality 细分 token (IMAGE/TEXT)
- IMAGE modality token 价格为 $0.00012/token,是普通 TEXT token 的 10 倍
- 原系统未解析此字段,导致 IMAGE token 按 TEXT 价格计费,计费偏低约 7.6 倍

类型扩展 (src/types/model-price.ts):
- 新增 output_cost_per_image_token: 输出图片 token 单价 (按 token 计费)
- 新增 input_cost_per_image_token: 输入图片 token 单价 (按 token 计费)
- 保留 input_cost_per_image: 输入图片固定价格 (按张计费,$0.0011/张)
- 保留 output_cost_per_image: 输出图片固定价格 (按张计费)

Usage 提取逻辑 (src/app/v1/_lib/proxy/response-handler.ts):
- 解析 candidatesTokensDetails 提取 output_image_tokens 和 output_tokens (TEXT)
- 解析 promptTokensDetails 提取 input_image_tokens 和 input_tokens (TEXT)
- 使用 toUpperCase() 进行大小写不敏感匹配 (IMAGE/image/Image)
- 添加 hasValidToken 守卫,仅在解析到有效 token 时覆盖原始值
- 修复 promptTokensDetails 解析不完整导致 input IMAGE tokens 被重复计费的问题
- 计算 candidatesTokenCount 与 details 总和的差值作为未分类 TEXT tokens
  (这些是图片生成的内部开销,按 TEXT 价格计费)

计费逻辑 (src/lib/utils/cost-calculation.ts):
- output_image_tokens 优先使用 output_cost_per_image_token 计费
- input_image_tokens 优先使用 input_cost_per_image_token 计费
- 若未配置 image token 价格,回退到普通 token 价格 (向后兼容)
- 倍率 (multiplier) 同时作用于 image token 费用

测试覆盖:
- 新增 cost-calculation-image-tokens.test.ts (10 个测试)
- 扩展 extract-usage-metrics.test.ts (12 个 Gemini image 测试)
- 覆盖场景: 纯 IMAGE、IMAGE+TEXT 混合、无效数据、大小写变体、向后兼容、
  混合输入输出、candidatesTokenCount 差值计算

计费示例 (完整图片生成请求):
- promptTokenCount=326, candidatesTokenCount=2340, thoughtsTokenCount=337
- candidatesTokensDetails: IMAGE=2000 (差值 340 为未分类 TEXT)
- 输入 TEXT: 326 × $0.000002 = $0.000652
- 输出 TEXT: (340+337) × $0.000012 = $0.008124
- 输出 IMAGE: 2000 × $0.00012 = $0.240000
- 总计: $0.248776 (修复前 $0.244696,少收 $0.00408)

Fixes #663
* feat(rate-limit): implement lease-based budget slicing for quota enforcement

Introduce a "DB authoritative + Redis lease deduction" mechanism for rate
limiting that ensures UI and decision chain use the same data source.

Key changes:
- Add BudgetLease model with snapshotAtMs anchoring for window calculation
- Implement LeaseService with getCostLease, refreshCostLeaseFromDb, and
  decrementLeaseBudget (atomic Lua script) methods
- Add checkCostLimitsWithLease to RateLimitService for lease-based checks
- Add system settings for quota lease configuration:
  - quotaDbRefreshIntervalSeconds (lease TTL)
  - quotaLeasePercent5h/Daily/Weekly/Monthly (slice percentages)
  - quotaLeaseCapUsd (maximum slice cap)
- Add UI form for quota lease settings with i18n support (5 languages)
- Add database migration for new system settings columns
- Implement fail-open behavior on Redis/DB errors
- Add comprehensive unit tests (108 quota tests passing)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(rate-limit): force lease refresh when quota limit changes

When admin modifies a user/key's quota limit, the cached lease in Redis
may have stale limitAmount. This causes:
- Users still blocked after limit increase
- Users still allowed after limit decrease

Add limitAmount comparison in getCostLease() to detect changes and
force DB refresh when the configured limit differs from cached value.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(rate-limit): use rolling window semantics for 5h/daily limits

Rolling windows (5h, daily rolling) now correctly indicate that there
is no fixed reset time - usage gradually expires over the window period.

Changes:
- Add RATE_LIMIT_5H_ROLLING_EXCEEDED and RATE_LIMIT_DAILY_ROLLING_EXCEEDED
  error codes to distinguish rolling from fixed windows
- Update i18n messages in all 5 languages to explain rolling behavior
  ("usage gradually expires") instead of showing incorrect reset times
- Allow RateLimitError.resetTime to be null for rolling windows
- Skip X-RateLimit-Reset and Retry-After headers when resetTime is null
- Add comprehensive unit tests for 5h rolling window behavior (21 tests)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(timezone): unify timezone handling across frontend and backend

- Add system timezone setting with IANA validation and UI configuration
- Create parseDateInputAsTimezone helper for date-only (end-of-day) and
  datetime inputs parsed in system timezone
- Update key/user expiresAt parsing to use system timezone
- Replace hardcoded zh-CN locale in webhook date formatting with
  formatInTimeZone for locale-independent output
- Add resolveSystemTimezone with fallback chain: DB -> env TZ -> UTC
- Configure next-intl with system timezone for consistent SSR/CSR display
- Add comprehensive tests for DST transitions and timezone boundaries

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: format code (fix-quota-limit-refactor-edf12a8)

* fix: address bugbot review comments for timezone and rate-limit

- drizzle/0059: add IF NOT EXISTS to prevent migration conflict
- date-input: detect timezone designator (Z/+-HH:MM) in ISO strings
  to avoid double conversion by fromZonedTime
- lease.ts: add boundary protection for calculateLeaseSlice
  (clamp percent to [0,1], ensure non-negative capUsd and result)
- placeholders.ts: wrap formatLocalTimestamp in try-catch to handle
  invalid IANA timezone gracefully
- availability-view: pass locale to formatBucketTime for i18n month names
- lease-service.test: use vi.hoisted() to fix TDZ issue with mock

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…efault

Add collapsible behavior to system settings form:
- Quota Lease Settings section now collapsed by default
- Response Fixer sub-options now collapsed by default
- Both sections expand on click with chevron rotation animation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
#670)

* feat(providers): recluster vendors by host:port when website_url empty

- Add computeVendorKey helper with host:port support for IP-based providers
- When website_url is empty, use host:port as vendor key (different ports = different vendors)
- Support IPv6 addresses with [ipv6]:port format
- Use protocol default ports (http=80, https=443) when port not specified
- Add reclusterProviderVendors action with preview/apply mode
- Add ReclusterVendorsDialog UI component
- Add i18n support for 5 languages (zh-CN, zh-TW, en, ja, ru)
- Existing behavior unchanged when website_url is present (hostname only)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* perf(providers): optimize recluster with batch vendor loading and Map lookup

- Batch load all vendor data upfront with Promise.all to avoid N+1 queries
- Use Map for O(1) provider lookup instead of O(N) find() in transaction loop
- Addresses bugbot review comments from gemini-code-assist and greptile-apps

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The computeVendorKey function was exported from a file with "use server"
directive but was not async, causing Next.js build to fail with:
"Server Actions must be async functions."

Changes:
- Made computeVendorKey async and return Promise<string | null>
- Added await to all call sites in provider-endpoints.ts
- Added await to call site in providers.ts (reclusterProviderVendors)
- Updated all test cases to use async/await

CI Run: https://github.com/ding113/claude-code-hub/actions/runs/21436692308

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
…e paths

The revalidatePath calls in saveSystemSettings lacked locale prefixes,
causing non-default locale pages (/en/*, /ja/*, etc.) to serve stale
cached data after save. Users had to save twice for changes to appear.

- Add locale loop for revalidatePath in system-config.ts
- Add unit tests for saveSystemSettings revalidation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

Important

Review skipped

Too many files!

This PR contains 182 files, which is 82 over the limit of 100.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • ✅ Full review completed - (🔄 Check again to review again)
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dev

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

❤️ Share

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

@github-actions github-actions bot added enhancement New feature or request area:UI area:i18n labels Jan 28, 2026
@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 release significantly refines the platform's infrastructure by centralizing timezone configuration for consistent data handling and display. It introduces a robust, performant lease-based rate limiting system and intelligent dynamic probing for provider endpoints. Furthermore, it enhances provider management with a new re-clustering tool and expands cost calculation capabilities to include image tokens, ensuring more accurate usage tracking.

Highlights

  • Centralized Timezone Management: Introduced a system-wide timezone setting, allowing consistent date and time calculations and displays across the backend and frontend. This includes timezone-aware parsing for key and user expiration dates, and formatting in UI components.
  • Advanced Rate Limiting with Lease Mechanism: Implemented a new lease-based rate limiting system that uses Redis for budget slicing. This improves performance by reducing direct database queries for every rate limit check, while maintaining accuracy. Rolling window rate limits are now explicitly supported with dedicated error messages.
  • Dynamic Endpoint Probing Intervals: Enhanced provider endpoint probing to use dynamic intervals. Endpoints with recent timeouts are probed more frequently (10s), single-endpoint vendors are probed less frequently (10min), and others use a base interval (60s).
  • Provider Re-clustering Feature: Added a new feature to re-cluster providers based on updated rules. When a provider lacks a website URL, its host:port combination will now be used as the clustering key, improving vendor aggregation logic.
  • Image Token Cost Calculation: Integrated support for calculating costs based on image tokens, particularly relevant for multi-modal models like Gemini, ensuring accurate billing for image-related usage.
  • Database Schema Updates: Migrated token-related columns in the message_request table to bigint for larger values and added new columns to system_settings for timezone and quota lease configurations. Also updated keys.expires_at to timestamp with time zone.
Using Gemini Code Assist

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

Invoking Gemini

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

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

Customization

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

Limitations & Feedback

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

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

Footnotes

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

@github-actions github-actions bot added the size/XL Extra Large PR (> 1000 lines) label Jan 28, 2026
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

8 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

* - IPv6 format: [ipv6]:port
* - Missing port: use protocol default (http=80, https=443)
* - No scheme: assume https
*/
Copy link

Choose a reason for hiding this comment

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

computeVendorKey is declared as async but doesn't use any await statements internally - functions are synchronous

Suggested change
*/
export function computeVendorKey(input: {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/repository/provider-endpoints.ts
Line: 111:111

Comment:
`computeVendorKey` is declared as async but doesn't use any await statements internally - functions are synchronous

```suggestion
export function computeVendorKey(input: {
```

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

function deriveDisplayNameFromDomain(domain: string): string {
const parts = domain.split(".");
const name = parts[0] === "api" && parts[1] ? parts[1] : parts[0];
export async function deriveDisplayNameFromDomain(domain: string): Promise<string> {
Copy link

Choose a reason for hiding this comment

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

deriveDisplayNameFromDomain is declared as async but doesn't use await - synchronous function

Suggested change
export async function deriveDisplayNameFromDomain(domain: string): Promise<string> {
export function deriveDisplayNameFromDomain(domain: string): string {
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/repository/provider-endpoints.ts
Line: 311:311

Comment:
`deriveDisplayNameFromDomain` is declared as async but doesn't use await - synchronous function

```suggestion
export function deriveDisplayNameFromDomain(domain: string): string {
```

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

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 substantial release that introduces several major improvements. The most significant is the comprehensive timezone support, which will ensure consistent date and time handling across the application. The introduction of a lease-based mechanism for rate limiting is a smart performance optimization that will reduce database load. The endpoint probing logic has also been made more intelligent with dynamic intervals. Additionally, there are numerous UI/UX enhancements, including a revamped dashboard layout and a new provider reclustering tool. The database schema has been updated to support these new features and to prevent data overflow for token counts. Overall, this is a very solid release with well-implemented features and improvements.

@@ -0,0 +1,2 @@
ALTER TABLE "keys" ALTER COLUMN "expires_at" SET DATA TYPE timestamp with time zone;--> statement-breakpoint
ALTER TABLE "system_settings" ADD COLUMN IF NOT EXISTS "timezone" varchar(64); No newline at end of file
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 ADD COLUMN statement is redundant as it's already present in the 0048_add_system_timezone.sql migration. While IF NOT EXISTS prevents an error, it's best to remove duplicate statements from migrations to keep them clean and avoid potential confusion. This might indicate a small issue in the migration generation process.

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 is a large release PR (v0.5.2) with significant changes across 182 files. The PR introduces several important features including:

  1. System Timezone Configuration - New timezone settings with proper IANA validation
  2. Lease-based Rate Limiting - New budget slicing mechanism for rate limits
  3. Rolling Window Rate Limits - Proper handling of 5h and daily rolling windows
  4. Provider Vendor Reclustering - New admin tool for reorganizing provider vendors
  5. Gemini Image Token Support - Extraction of modality-specific tokens

PR Size: XL

  • Lines changed: 19,797 (18,763 additions, 1,034 deletions)
  • Files changed: 182

Split Suggestion: This XL PR could be split into:

  • Timezone feature (timezone.ts, date-input.ts, time-utils changes)
  • Rate limit lease system (lease.ts, lease-service.ts)
  • Provider recluster feature (recluster dialog, computeVendorKey)
  • UI improvements (logs components, settings form)

Issues Found

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

Medium Priority Issues (Should Consider)

1. src/actions/keys.ts:1072 - Missing specific error handling for invalid date input

The parseDateInputAsTimezone function throws an error when the date input is invalid. In renewKeyExpiresAt, while the outer try-catch will catch this error, it exposes the raw error message (e.g., "Invalid date input: xyz") to the user instead of returning a proper i18n error message with ERROR_CODES.INVALID_FORMAT.

Compare with editKey function (lines 507-519) which properly catches this specific error and returns an i18n message.

Suggested fix:

const timezone = await resolveSystemTimezone();
let expiresAt: Date;
try {
  expiresAt = parseDateInputAsTimezone(data.expiresAt, timezone);
} catch {
  return { ok: false, error: tError("INVALID_FORMAT"), errorCode: ERROR_CODES.INVALID_FORMAT };
}

2. src/actions/users.ts:1345 - Same issue in renewUser function

The renewUser function has the same pattern where parseDateInputAsTimezone can throw but is not specifically caught to return a proper i18n error message.

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - 2 medium issues noted
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Adequate (new tests for lease, timezone, date-input)
  • Code clarity - Good

Positive Observations (not reported per instructions, but noted)

  • Comprehensive test coverage for new lease and timezone features
  • Proper i18n support for new error messages
  • Good use of fail-open pattern in rate limiting
  • Clean separation of concerns in new modules

Automated review by Claude AI

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

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (15)
src/app/[locale]/dashboard/availability/_components/endpoint/probe-terminal.tsx (1)

189-211: 修复未使用的 Icon 变量(流水线失败)

Icon 变量已定义但从未使用,导致 lint 检查失败。应该在日志行中渲染图标,或者移除未使用的变量。

建议方案:在状态标签前渲染图标
               {/* Timestamp */}
               <span className="text-muted-foreground opacity-60 w-20 shrink-0">
                 [{formatTime(log.createdAt, timeZone)}]
               </span>

+              {/* Status Icon */}
+              <Icon className={cn("h-3.5 w-3.5 shrink-0", config.color)} />
+
               {/* Status */}
               <span className={cn("w-12 shrink-0 font-bold", config.color)}>{config.label}</span>

或者,如果不需要图标,直接移除未使用的变量:

             const level = getLogLevel(log);
             const config = levelConfig[level];
-            const Icon = config.icon;
src/components/customs/active-sessions-list.tsx (1)

14-20: 硬编码的错误消息违反 i18n 规范

第 17 行的错误消息 "获取活跃 Session 失败" 是硬编码的中文文本。根据编码规范,所有用户可见的字符串必须使用 i18n(支持 5 种语言:zh-CN、zh-TW、en、ja、ru)。

🔧 建议的修复
 async function fetchActiveSessions(): Promise<ActiveSessionInfo[]> {
   const result = await getActiveSessions();
   if (!result.ok) {
-    throw new Error(result.error || "获取活跃 Session 失败");
+    throw new Error(result.error || "Failed to fetch active sessions");
   }
   return result.data;
 }

或者,如果此错误消息需要对用户显示,应使用翻译函数。不过由于此函数在组件外部定义,可能需要重构为在组件内部处理错误,以便访问 useTranslations hook。

As per coding guidelines: "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text"

src/components/ui/data-table.tsx (1)

274-274: 硬编码的中文字符串违反 i18n 规范。

默认参数 "操作" 是硬编码的中文字符串,应使用 i18n 机制替代。根据编码规范,所有用户可见的文本必须支持国际化(支持 zh-CN、zh-TW、en、ja、ru 五种语言)。

🌐 建议的修复

由于这是一个静态工厂函数而非组件,无法直接使用 useTranslations。建议移除默认值,由调用方传入已翻译的字符串:

  actions: <T extends TableData>(
-   title: string = "操作",
+   title: string,
    render: (value: any, record: T, index: number) => ReactNode,
    options?: Partial<TableColumn<T>>
  ): TableColumn<T> => ({

As per coding guidelines: "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text"

src/app/[locale]/dashboard/sessions/[sessionId]/messages/_components/request-list-sidebar.tsx (3)

127-127: 存在硬编码的用户可见字符串。

title 属性中的 "Unknown" 应使用 i18n 翻译。

-                  title={`#${req.sequence} - ${req.model || "Unknown"}`}
+                  title={`#${req.sequence} - ${req.model || t("requestList.unknownModel")}`}

Based on learnings: "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text"


167-169: 搜索占位符文本未国际化。

placeholder="Search..." 应使用翻译函数。

             <Input
-              placeholder="Search..."
+              placeholder={t("requestList.searchPlaceholder")}
               className="h-7 text-xs pl-7 bg-muted/20 border-muted-foreground/20"

Based on learnings: "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text"


244-246: "Unknown Model" 文本未国际化。

-                      {request.model || "Unknown Model"}
+                      {request.model || t("requestList.unknownModel")}

Based on learnings: "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text"

src/repository/statistics.ts (1)

471-479: 硬编码的中文字符串违反 i18n 规范。

SQL 查询中直接硬编码了 '其他用户' 作为 user_name,这违反了项目的国际化要求(支持 5 种语言:zh-CN, zh-TW, en, ja, ru)。

建议将此标识符改为一个语言无关的常量(如 '__others__'),然后在前端展示层进行国际化处理。

建议的修复方案
         SELECT
           -1 AS user_id,
-          '其他用户' AS user_name,
+          '__others__' AS user_name,
           hour AS date,
           api_calls::integer,
           total_cost::numeric

同样的修改需要应用到以下位置:

  • Line 545
  • Line 617
  • Line 689

As per coding guidelines: "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text"

src/app/[locale]/settings/prices/_components/price-list.tsx (1)

23-75: 未使用的 locale 触发 lint 失败,需要移除。

流水线已报错:lint/correctness/noUnusedVariables。建议移除 useLocalelocale 变量。

建议修复
-import { useLocale, useTimeZone, useTranslations } from "next-intl";
+import { useTimeZone, useTranslations } from "next-intl";
@@
-  const locale = useLocale();
   const timeZone = useTimeZone() ?? "UTC";
src/lib/webhook/templates/test-messages.ts (1)

28-48: 时区参数未在所有消息类型中一致应用。

buildTestMessage 接收 timezone 参数但仅传递给 circuit-breaker 类型。cost-alertdaily-leaderboard 两个消息类型均使用 new Date() 创建时间戳,但不接收 timezone 参数,导致这些消息的日期/时间格式化无法进行时区转换。建议为 buildCostAlertMessagebuildDailyLeaderboardMessage 的函数签名添加 timezone 参数,保持实现的一致性。

src/lib/webhook/templates/circuit-breaker.ts (1)

17-22: 代码中包含 emoji 字符,违反编码规范。

Line 21 的 icon: "🔌" 使用了 emoji 字符。根据编码规范,不应在代码、注释或字符串字面量中使用 emoji 字符。

建议使用图标组件或图标名称字符串替代 emoji。

As per coding guidelines: "Never use emoji characters in any code, comments, or string literals"

src/app/[locale]/dashboard/_components/statistics/chart.tsx (1)

233-250: 处理日期字符串的时区解析问题,防止图表跨日偏移。

dateStr 为 "YYYY-MM-DD" 格式(日分辨率)时,new Date(dateStr) 会按 UTC 解析为午夜,再用 formatInTimeZone 转换负时区时会显示前一天,导致图表 X 轴与 tooltip 日期不匹配。

建议使用 zonedTimeToUtc 按目标时区正确解析日期字符串:

修正方案
  1. 更新导入:
import { formatInTimeZone, zonedTimeToUtc } from "date-fns-tz";
  1. 添加日期转换辅助函数(lines 233 前):
const toChartDate = (dateStr: string) => {
  const isDateOnly = /^\d{4}-\d{2}-\d{2}$/.test(dateStr);
  return isDateOnly ? zonedTimeToUtc(`${dateStr}T00:00:00`, timeZone) : new Date(dateStr);
};
  1. 更新两个格式化函数:
const formatDate = (dateStr: string) => {
  const date = toChartDate(dateStr);
  // ... rest remains same
};

const formatTooltipDate = (dateStr: string) => {
  const date = toChartDate(dateStr);
  // ... rest remains same
};
src/app/[locale]/dashboard/logs/_components/error-details-dialog/components/LogicTraceTab.tsx (1)

51-57: 未使用的参数会导致 lint 失败。

statusCode 当前未使用,CI 已报错;请移除或实际使用该参数以通过校验。

修复建议
-export function LogicTraceTab({
-  statusCode,
-  providerChain,
+export function LogicTraceTab({
+  providerChain,
   blockedBy,
   blockedReason,
   requestSequence,
 }: LogicTraceTabProps) {
src/app/[locale]/dashboard/_components/bento/dashboard-bento.tsx (1)

260-309: data-testid 未透传到 LiveSessionsPanel 实际节点。
此处传入 data-testid,但 LiveSessionsPanel 未展开剩余 props,最终不会落到 BentoCard/DOM,测试选择器可能失效。建议在组件内接收并透传剩余属性。

建议修复
-export function LiveSessionsPanel({
-  sessions,
-  isLoading,
-  maxItems: maxItemsProp,
-  className,
-}: LiveSessionsPanelProps) {
+export function LiveSessionsPanel({
+  sessions,
+  isLoading,
+  maxItems: maxItemsProp,
+  className,
+  ...rest
+}: LiveSessionsPanelProps) {
-    <BentoCard
-      ref={containerRef}
-      colSpan={1}
-      rowSpan={2}
-      className={cn(
-        "flex flex-col overflow-hidden p-0",
-        "bg-slate-50 dark:bg-[`#0a0a0c`]",
-        "border-slate-200 dark:border-white/[0.06]",
-        className
-      )}
-    >
+    <BentoCard
+      ref={containerRef}
+      colSpan={1}
+      rowSpan={2}
+      className={cn(
+        "flex flex-col overflow-hidden p-0",
+        "bg-slate-50 dark:bg-[`#0a0a0c`]",
+        "border-slate-200 dark:border-white/[0.06]",
+        className
+      )}
+      {...rest}
+    >
src/lib/webhook/templates/placeholders.ts (1)

25-30: 占位符描述新增文案需 i18n。
该描述为用户可见文案,请改为多语言资源。
As per coding guidelines: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text.

src/lib/rate-limit/time-utils.ts (1)

43-235: 修复硬编码的中文字符串,改为使用 i18n

getResetInfo 和 getResetInfoWithMode 返回的 period 字段含有硬编码中文字符串("5 小时"、"24 小时"),这些是用户界面展示的内容,必须使用 i18n 国际化而非硬编码。请将这些字符串改为从 i18n 翻译函数获取。

🤖 Fix all issues with AI agents
In `@drizzle/meta/0058_snapshot.json`:
- Around line 200-205: 快照显示 keys.expires_at 仍为
timestamp、notification_target_bindings.schedule_timezone 带默认值且 system_settings
缺少 timezone,与代码中的模式不一致;在模式定义文件中修正对应列(更新 keys.expires_at 的类型、移除或更新
notification_target_bindings.schedule_timezone 的默认值、为 system_settings 添加
timezone 列),然后运行生成命令 bun run db:generate 重新生成迁移与快照,确保不手工编辑 SQL 文件以使迁移与模式对齐。

In `@src/actions/users.ts`:
- Around line 1487-1490: The daily time range calculation currently calls
getTimeRangeForPeriod("daily", user.dailyResetTime || "00:00") which ignores the
user's rolling vs fixed reset setting; replace this with
getTimeRangeForPeriodWithMode("daily", user.dailyResetTime || "00:00",
user.dailyResetMode) (or the equivalent parameter order used by
getTimeRangeForPeriodWithMode) so rangeDaily is computed with the dailyResetMode
taken into account; update any references to rangeDaily accordingly.

In `@src/app/`[locale]/dashboard/logs/_components/usage-logs-filters.tsx:
- Around line 264-268: The quick-filter ("today", "this-week") range calculation
currently uses local timezone startOfDay/startOfWeek and thus diverges from
TimeFilters' serverTimeZone-aware parsing; update the quick-filter logic that
sets localFilters (used by TimeFilters via setLocalFilters) to compute ranges
using the existing getQuickDateRange(timeKey, serverTimeZone) utility from
time-range.ts so the generated start/end dates are timezone-aware and consistent
with TimeFilters' display and parsing. Ensure you replace any direct
startOfDay/startOfWeek calls with getQuickDateRange and pass serverTimeZone
through to the function when handling quick-filter selection.

In
`@src/app/`[locale]/settings/providers/_components/provider-list-item.legacy.tsx:
- Line 66: The code risks a RangeError when formatInTimeZone receives an invalid
date or timezone: update the timeZone assignment to use fallback with || (e.g.,
const timeZone = useTimeZone() || "UTC") and add a date validity check before
calling formatInTimeZone — compute const d = new Date(item.lastCallTime) and
ensure it's valid (e.g., !Number.isNaN(d.getTime()) or use isValid from
date-fns) and only call formatInTimeZone(d, timeZone, ...) when valid; otherwise
render a safe fallback (empty string or "—") to avoid rendering crashes.

In `@src/app/v1/_lib/proxy/rate-limit-guard.ts`:
- Around line 359-426: The current guard if (user.dailyQuota !== null)
incorrectly allows undefined to proceed into RateLimitService.checkUserDailyCost
and cause unintended throttling; update the logic to distinguish undefined
(inherit default) from null (unlimited) by only running the daily check when the
quota is a concrete number. Concretely, in the block around user.dailyQuota,
compute a local quota like const quota = user.dailyQuota; and change the
condition to typeof quota === "number" (or explicitly check !== null && quota
!== undefined), pass quota into RateLimitService.checkUserDailyCost and use
quota (not user.dailyQuota) when formatting limit and building RateLimitError
messages, keeping the existing branches for rolling vs fixed reset using
getResetInfoWithMode and getErrorMessageServer.

In `@src/components/ui/data-table.tsx`:
- Line 3: The date renderer is hardcoding the timezone "UTC" via
formatInTimeZone; update the date-column rendering to use a configurable
timezone (from system/user settings or React context) instead of "UTC". Locate
the date column renderer (the column type or renderCell function that calls
formatInTimeZone in src/components/ui/data-table.tsx) and replace the literal
"UTC" with a timezone value obtained from props/context/config (e.g., a
TimezoneContext, a global config hook, or a column-specific tz option) and
ensure a sensible default is used when no timezone is provided.

In `@src/components/ui/relative-time.tsx`:
- Line 3: Remove the unused import "format as formatDate" from date-fns in
src/components/ui/relative-time.tsx; the code now uses formatInTimeZone instead,
so delete the import statement referencing formatDate to resolve the pipeline
error and keep imports consistent.

In `@src/drizzle/schema.ts`:
- Around line 96-98: The migration for keys.expires_at in Migration 0059 changes
the column to timestamp with time zone but lacks an explicit timezone
conversion, which causes data drift; update the ALTER TABLE statement in
Migration 0059 to perform an explicit conversion using the USING clause (e.g.,
converting stored values from UTC) so existing timestamps are interpreted
correctly, and verify or document that the DB server timezone is UTC before
running the migration; look for the expiresAt column definition (expiresAt:
timestamp('expires_at', { withTimezone: true })) and the migration script that
alters "expires_at" to add the USING ... AT TIME ZONE 'UTC' conversion.
- Around line 155-159: The providerVendorId column in the schema currently has a
.notNull() constraint which breaks the backfillProviderVendorsFromProviders()
logic; remove the .notNull() call from the providerVendorId definition in
src/drizzle/schema.ts so the column is nullable while keeping the references(()
=> providerVendors.id, { onDelete: 'restrict' }) intact.

In `@src/lib/rate-limit/service.ts`:
- Around line 1299-1407: The failure reason is hardcoded in
checkCostLimitsWithLease; replace the literal English message with an
i18n-backed string (or the project's existing limit-error builder) so
user-facing text is localized and consistent. Locate the return in
checkCostLimitsWithLease where it constructs reason using typeName, check.name,
lease.currentUsage and check.limit, and call the i18n translation function (e.g.
i18n.t or the project's error formatter) with a key like
"rate_limit.cost_limit_reached" and interpolations { type: typeName, window:
check.name, usage: lease.currentUsage.toFixed(4), limit: check.limit.toFixed(4)
} (or reuse the project's existing helper that formats limit-exceeded messages)
so all five supported locales are covered and styling matches other limit
errors.

In `@src/lib/utils/date-input.ts`:
- Around line 54-62: The ISO datetime parsing branch currently uses new
Date(input) which interprets zone-less ISO strings in the runtime local zone
causing inconsistent results; update the logic in the date parsing function in
date-input.ts to use the same approach as the date-only branch by calling
parse(input, "yyyy-MM-dd'T'HH:mm:ss", new Date()) (or equivalent parse pattern)
then pass that parsed Date into fromZonedTime(timeParsed, timezone) and keep the
existing Number.isNaN check on the parsed Date; ensure you reference and reuse
the parse utility (instead of new Date(input)) and call
fromZonedTime(parsedDate, timezone) so zone-less ISO datetimes are interpreted
correctly for the target timezone.

In `@src/lib/validation/schemas.ts`:
- Around line 732-740: The timezone zod schema currently uses a hardcoded
Chinese error message; replace that with an i18n lookup so user-facing
validation messages are localized. Update the timezone schema (the timezone
field in src/lib/validation/schemas.ts) to call the project's i18n function or
error-mapping utility (e.g., t('validation.invalidTimezone') or the centralized
error mapper) instead of the literal string, ensuring keys exist for zh-CN,
zh-TW, en, ja, and ru; keep the refine predicate (isValidIANATimezone) unchanged
and return the i18n key/value for the message so the validation layer emits
localized text rather than a hardcoded string.
- Around line 786-813: The Zod validators for quotaDbRefreshIntervalSeconds,
quotaLeasePercent5h, quotaLeasePercentDaily, quotaLeasePercentWeekly,
quotaLeasePercentMonthly, and quotaLeaseCapUsd currently use hardcoded English
error messages; replace those literal strings with i18n lookups (use the
project's translation function and stable i18n keys) so validators call the i18n
resource instead of embedding text, and add corresponding keys/translations for
zh-CN, zh-TW, en, ja, ru; keep the same validation logic (.int, .min, .max,
.nullable, .optional) and ensure messages are passed the translated string
values.

In `@src/lib/webhook/templates/circuit-breaker.ts`:
- Around line 8-11: The fields array in circuit-breaker.ts contains hardcoded
user-facing labels ("失败次数", "预计恢复") which violate i18n rules; replace these
literals with calls to the project's translation function (e.g., t or i18n.t)
and add appropriate message keys (e.g., "circuitBreaker.failureCount",
"circuitBreaker.estimatedRecovery") in the i18n message files for all supported
locales (zh-CN, zh-TW, en, ja, ru); update the const fields initialization to
use the translation calls (passing any interpolations like `${data.failureCount}
次` as variables where appropriate) and ensure formatDateTime(data.retryAt,
timezone || "UTC") remains as the translated value for the "estimated recovery"
label.

In `@src/repository/_shared/transformers.ts`:
- Around line 198-211: The code incorrectly uses truthy checks so string "0"
becomes false and falls back to defaults; update each quota field check to test
for null/undefined (e.g., dbSettings?.quotaLeasePercent5h != null) then
parseFloat and if Number.isNaN(parsed) fall back to the intended default (0.05
for percent fields, null for quotaLeaseCapUsd). Apply this to
quotaLeasePercent5h, quotaLeasePercentDaily, quotaLeasePercentWeekly,
quotaLeasePercentMonthly and quotaLeaseCapUsd, keeping
quotaDbRefreshIntervalSeconds as-is (it already uses ??).

In `@src/repository/provider-endpoints.ts`:
- Around line 63-99: normalizeHostWithPort produces ambiguous IPv6 keys (e.g.,
"::1:443") because URL.hostname returns IPv6 without brackets; update the
function (normalizeHostWithPort) to wrap the normalizedHostname in square
brackets when it contains a colon and doesn't already start with "[" before
appending the port (i.e., detect hostname.includes(":") &&
!hostname.startsWith("["), then set normalizedHostname =
`[${normalizedHostname}]`) so returned strings follow the "[ipv6]:port" rule.
🟡 Minor comments (13)
src/app/[locale]/dashboard/availability/_components/provider/latency-chart.tsx-6-6 (1)

6-6: 移除未使用的导入 ResponsiveContainer

管道检测到未使用的导入。经检查,ResponsiveContainer 在代码中确实未被使用,应将其移除。

建议修复
-import { Area, AreaChart, CartesianGrid, ResponsiveContainer, XAxis, YAxis } from "recharts";
+import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
tests/unit/dashboard/availability/availability-dashboard-ui.test.tsx-62-75 (1)

62-75: 缺少异步操作等待机制,测试结果可能不稳定。

组件在 useEffect 中调用异步的 fetchData,但测试在 renderWithIntl 后立即同步检查 container.textContent,此时组件可能尚未完成数据加载和状态更新。

建议使用异步等待机制确保组件渲染完成:

🔧 建议的修复方案
-  test("does not render Probe All floating button", async () => {
+  test("does not render Probe All floating button", async () => {
     vi.stubGlobal(
       "fetch",
       vi.fn(async () => ({
         ok: true,
         json: async () => ({ providers: [], systemAvailability: 0 }),
       }))
     );

     const { container, unmount } = renderWithIntl(<AvailabilityDashboard />);

+    // 等待异步操作完成
+    await act(async () => {
+      await new Promise((resolve) => setTimeout(resolve, 0));
+    });
+
     expect(container.textContent).not.toContain("Probe All");

     unmount();
   });
src/app/[locale]/dashboard/availability/_components/endpoint/latency-curve.tsx-6-6 (1)

6-6: Pipeline 失败:存在未使用的导入。

根据 CI 报告,需要移除未使用的导入以修复 lint 错误:

修复建议
-import { CartesianGrid, Line, LineChart, ResponsiveContainer, XAxis, YAxis } from "recharts";
+import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
 import {
   type ChartConfig,
   ChartContainer,
   ChartTooltip,
-  ChartTooltipContent,
 } from "@/components/ui/chart";

Also applies to: 11-11

src/actions/keys.ts-1070-1073 (1)

1070-1073: 续期接口同样需要拦截无效日期,避免英文错误泄漏。
当前解析失败会被外层 catch 直接返回 error.message;建议与 editKey 统一返回 INVALID_FORMAT

可选的修正方式
-    const timezone = await resolveSystemTimezone();
-    const expiresAt = parseDateInputAsTimezone(data.expiresAt, timezone);
+    let expiresAt: Date;
+    try {
+      const timezone = await resolveSystemTimezone();
+      expiresAt = parseDateInputAsTimezone(data.expiresAt, timezone);
+    } catch {
+      return {
+        ok: false,
+        error: tError("INVALID_FORMAT"),
+        errorCode: ERROR_CODES.INVALID_FORMAT,
+      };
+    }
Based on learnings: 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.
src/actions/keys.ts-285-291 (1)

285-291: 添加密钥时缺少对无效日期的专门处理,可能回传英文错误。
parseDateInputAsTimezone 抛错会被外层 catch 直接透传 error.message,导致用户看到英文且缺少 INVALID_FORMAT 的错误码。建议在此处捕获并返回统一的错误。

可选的修正方式
-    const timezone = await resolveSystemTimezone();
-    const expiresAt =
-      validatedData.expiresAt === undefined
-        ? null
-        : parseDateInputAsTimezone(validatedData.expiresAt, timezone);
+    const timezone = await resolveSystemTimezone();
+    let expiresAt: Date | null;
+    if (validatedData.expiresAt === undefined) {
+      expiresAt = null;
+    } else {
+      try {
+        expiresAt = parseDateInputAsTimezone(validatedData.expiresAt, timezone);
+      } catch {
+        return {
+          ok: false,
+          error: tError("INVALID_FORMAT"),
+          errorCode: ERROR_CODES.INVALID_FORMAT,
+        };
+      }
+    }
Based on learnings: 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.
src/actions/providers.ts-3693-3711 (1)

3693-3711: 事务内调用外部函数可能导致一致性问题。

getOrCreateProviderVendorIdFromUrls 在事务回调内部调用,但该函数使用独立的数据库连接执行操作,而非事务上下文 (tx)。如果事务回滚,新创建的 vendor 记录不会被撤销。

建议将 vendor 创建逻辑也纳入事务,或者接受这种部分一致性(vendor 是可复用的实体,孤立的 vendor 记录影响较小)。

src/lib/utils/timezone.ts-19-54 (1)

19-54: 修复无效的时区标识符

列表中的 "Pacific/Sydney" 不是有效的 IANA 时区标识符。根据 IANA 时区数据库,Sydney 应该使用 "Australia/Sydney"。这会导致该选项在通过 isValidIANATimezone() 验证时失败。

建议修复
-  "Pacific/Sydney",
+  "Australia/Sydney",
src/app/[locale]/dashboard/_components/bento/statistics-chart-card.tsx-154-168 (1)

154-168: 改用locale占位符以支持locale特定的日期顺序

当前代码使用固定的日期格式字符串("M/d"、"yyyy MMMM d"等),这可能不遵循特定locale的日期排列规则。建议改用date-fns的locale占位符("p"、"P"、"PPP"等),以让formatInTimeZone正确地根据locale调整日期组件的顺序。

建议修复
   const formatDate = (dateStr: string) => {
     const date = new Date(dateStr);
     if (data.resolution === "hour") {
-      return formatInTimeZone(date, timeZone, "HH:mm", { locale: dateFnsLocale });
+      return formatInTimeZone(date, timeZone, "p", { locale: dateFnsLocale });
     }
-    return formatInTimeZone(date, timeZone, "M/d", { locale: dateFnsLocale });
+    return formatInTimeZone(date, timeZone, "P", { locale: dateFnsLocale });
   };
 
   const formatTooltipDate = (dateStr: string) => {
     const date = new New(dateStr);
     if (data.resolution === "hour") {
-      return formatInTimeZone(date, timeZone, "MMMM d HH:mm", { locale: dateFnsLocale });
+      return formatInTimeZone(date, timeZone, "PPpp", { locale: dateFnsLocale });
     }
-    return formatInTimeZone(date, timeZone, "yyyy MMMM d", { locale: dateFnsLocale });
+    return formatInTimeZone(date, timeZone, "PPP", { locale: dateFnsLocale });
   };
src/lib/rate-limit/lease.ts-136-160 (1)

136-160: deserializeLease 未验证枚举值的有效性。

当前验证仅检查字段类型,但未验证 entityTypewindowresetMode 是否为有效的枚举值。如果存储了损坏的数据,可能导致下游逻辑出错。

🔧 建议添加枚举值验证
+import { LeaseEntityType, LeaseWindow } from "./lease";
+import { DailyResetModes } from "./time-utils"; // 假设导出了有效值数组

 export function deserializeLease(json: string): BudgetLease | null {
   try {
     const parsed = JSON.parse(json);

     // Validate required fields
     if (
       typeof parsed.entityType !== "string" ||
       typeof parsed.entityId !== "number" ||
       typeof parsed.window !== "string" ||
       typeof parsed.resetMode !== "string" ||
       typeof parsed.resetTime !== "string" ||
       typeof parsed.snapshotAtMs !== "number" ||
       typeof parsed.currentUsage !== "number" ||
       typeof parsed.limitAmount !== "number" ||
       typeof parsed.remainingBudget !== "number" ||
       typeof parsed.ttlSeconds !== "number"
     ) {
       return null;
     }

+    // Validate enum values
+    if (
+      !LeaseEntityType.includes(parsed.entityType) ||
+      !LeaseWindow.includes(parsed.window)
+    ) {
+      return null;
+    }

     return parsed as BudgetLease;
   } catch {
     return null;
   }
 }
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx-439-452 (1)

439-452: 类型图标仅图形展示,建议补充可访问性标签

当前单元格只显示图标,建议添加 aria-label(或 sr-only 文本)避免无文本可读。

建议修改
-              <span
-                className={`inline-flex h-6 w-6 items-center justify-center rounded ${typeConfig.bgColor}`}
-              >
+              <span
+                role="img"
+                aria-label={typeLabel}
+                className={`inline-flex h-6 w-6 items-center justify-center rounded ${typeConfig.bgColor}`}
+              >
src/app/[locale]/settings/config/_components/system-settings-form.tsx-435-445 (1)

435-445: 折叠触发按钮缺少可访问性标签

该触发器只有图标,建议补充 aria-label,避免无文本可读。

建议修改
-                <button
-                  type="button"
-                  className="flex items-center gap-1.5 mt-3 ml-11 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
-                >
+                <button
+                  type="button"
+                  aria-label={t("enableResponseFixer")}
+                  className="flex items-center gap-1.5 mt-3 ml-11 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
+                >
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx-223-235 (1)

223-235: 图标按钮缺少可访问性标签

该按钮只有图标,建议补充 aria-label 以便屏幕阅读器识别。

建议修改
-                        <button
-                          type="button"
-                          className="text-muted-foreground hover:text-foreground"
-                        >
+                        <button
+                          type="button"
+                          aria-label={t("vendorAggregationRule")}
+                          className="text-muted-foreground hover:text-foreground"
+                        >
src/lib/rate-limit/lease-service.ts-306-312 (1)

306-312: Lua 脚本在 TTL <= 0 时不会持久化更新

TTL 返回 -1(key 存在但无过期时间)或 0 时,更新后的 lease 不会被保存回 Redis。虽然正常流程中 setex 总是设置 TTL,但边缘情况(如外部修改、竞态条件)可能导致 key 无 TTL,此时扣减成功返回但实际未持久化。

建议处理 TTL <= 0 的情况
    -- Get TTL and update lease
    local ttl = redis.call('TTL', key)
    if ttl > 0 then
      redis.call('SETEX', key, ttl, cjson.encode(lease))
+    else
+      -- Key has no TTL or is expiring, use original ttlSeconds from lease
+      local leaseTtl = tonumber(lease.ttlSeconds) or 10
+      redis.call('SETEX', key, leaseTtl, cjson.encode(lease))
    end

或者在 TTL 无效时返回失败,由调用方触发刷新:

    local ttl = redis.call('TTL', key)
-    if ttl > 0 then
-      redis.call('SETEX', key, ttl, cjson.encode(lease))
-    end
+    if ttl <= 0 then
+      return {remaining, 0}  -- 返回失败,触发调用方刷新
+    end
+    redis.call('SETEX', key, ttl, cjson.encode(lease))
🧹 Nitpick comments (24)
src/app/[locale]/dashboard/logs/_components/active-sessions-skeleton.tsx (1)

15-15: 建议:使用 Tailwind 类替代内联样式。

为了与项目中其他样式保持一致,建议将内联 style 替换为 Tailwind 工具类。

♻️ 建议的修改
-      <div style={{ maxHeight: "200px" }} className="overflow-y-auto">
+      <div className="max-h-[200px] overflow-y-auto">
src/app/[locale]/dashboard/logs/_components/provider-chain-popover.tsx (1)

335-350: 建议:将 TooltipProvider 提升到外层,避免为每个徽章创建独立实例。

当前实现为每个 group badge 创建独立的 TooltipProvider,这会导致不必要的上下文开销。建议将 TooltipProvider 提升到外层,包裹所有 group badges。

♻️ 建议的优化
             )}
-            {/* Group tag badges (if present) */}
-            {finalGroupTags.map((group) => (
-              <TooltipProvider key={group}>
-                <Tooltip delayDuration={200}>
-                  <TooltipTrigger asChild>
-                    <Badge
-                      variant="outline"
-                      className="text-[10px] px-1 py-0 shrink-0 bg-slate-50 text-slate-600 border-slate-200 dark:bg-slate-900/30 dark:text-slate-400 dark:border-slate-700 max-w-[120px] truncate"
-                    >
-                      {group}
-                    </Badge>
-                  </TooltipTrigger>
-                  <TooltipContent>{group}</TooltipContent>
-                </Tooltip>
-              </TooltipProvider>
-            ))}
+            {/* Group tag badges (if present) */}
+            {finalGroupTags.length > 0 && (
+              <TooltipProvider>
+                {finalGroupTags.map((group) => (
+                  <Tooltip key={group} delayDuration={200}>
+                    <TooltipTrigger asChild>
+                      <Badge
+                        variant="outline"
+                        className="text-[10px] px-1 py-0 shrink-0 bg-slate-50 text-slate-600 border-slate-200 dark:bg-slate-900/30 dark:text-slate-400 dark:border-slate-700 max-w-[120px] truncate"
+                      >
+                        {group}
+                      </Badge>
+                    </TooltipTrigger>
+                    <TooltipContent>{group}</TooltipContent>
+                  </Tooltip>
+                ))}
+              </TooltipProvider>
+            )}
src/app/[locale]/settings/providers/_components/forms/test-result-card.tsx (1)

137-137: 考虑提取日期格式常量。

日期格式字符串 "yyyy-MM-dd HH:mm:ss" 在第 137 行和第 381 行重复使用。建议提取为常量以便统一维护。

♻️ 建议的重构

在文件顶部添加常量:

const DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";

然后在两处使用:

- formatInTimeZone(new Date(result.testedAt), timeZone, "yyyy-MM-dd HH:mm:ss")
+ formatInTimeZone(new Date(result.testedAt), timeZone, DATETIME_FORMAT)
tests/unit/components/session-list-item.test.tsx (2)

41-49: 冗余的类型定义和不必要的类型转换

SessionListItemProps 已从 @/components/customs/session-list-item 导出,此处重复定义是多余的。类型转换也可以简化。

♻️ 建议的改进
-import { SessionListItem } from "@/components/customs/session-list-item";
+import { SessionListItem, type SessionListItemProps } from "@/components/customs/session-list-item";
 import type { CurrencyCode } from "@/lib/utils/currency";
 import type { ActiveSessionInfo } from "@/types/session";

 // ... mocks ...

 const COST_SENTINEL = "__COST__";

-type SessionListItemProps = {
-  session: ActiveSessionInfo;
-  currencyCode?: CurrencyCode;
-  showTokensCost?: boolean;
-};
-
-const SessionListItemTest = SessionListItem as unknown as (
-  props: SessionListItemProps
-) => JSX.Element;

然后在 renderTextContent 中直接使用 SessionListItem


82-103: 测试覆盖率有限

当前仅测试了 showTokensCost 的开关行为。根据编码规范,新功能需要至少 80% 的测试覆盖率。建议补充以下测试场景:

  • inputTokensoutputTokensundefined 时的显示
  • costUsdundefined 时的显示
  • 不同 status 状态(in_progresserror)的图标渲染
  • durationMs 不同区间(毫秒、秒、分钟)的格式化
tests/unit/dashboard/availability/availability-dashboard-ui.test.tsx (2)

61-77: 测试覆盖率不足,建议补充更多测试用例。

根据 availability-dashboard.tsx 的实现,组件包含多个功能点:状态管理、数据获取、自动刷新、计算指标、标签页切换等。当前仅有一个检查 "Probe All" 按钮不存在的负向断言测试,覆盖率远低于编码规范要求的 80%。

建议补充以下测试场景:

  • 初始加载状态的渲染
  • 数据获取成功后的渲染
  • 数据获取失败时的错误处理
  • 标签页切换行为
  • 时间范围变更

基于 learnings:All new features must have unit test coverage of at least 80%


63-69: 全局 fetch 存根未清理,可能影响其他测试。

vi.stubGlobal("fetch", ...) 在测试结束后未恢复,可能导致其他测试受到污染。建议在测试后或使用 afterEach 钩子进行清理。

🔧 建议的修复方案
 describe("AvailabilityDashboard UI", () => {
+  afterEach(() => {
+    vi.unstubAllGlobals();
+  });
+
   test("does not render Probe All floating button", async () => {
src/components/ui/__tests__/tag-input-dialog.test.tsx (2)

67-68: 存在未使用的变量 container

两个测试用例中都解构了 container 但未使用。建议移除或使用下划线前缀标记为有意忽略。

建议修复
- const { container, unmount } = render(<DialogTagInput />);
+ const { unmount } = render(<DialogTagInput />);

Also applies to: 97-98


73-76: 异步等待使用 setTimeout 可能导致测试不稳定。

使用固定延迟(50ms)等待状态更新可能在不同环境下不够可靠。考虑使用更健壮的等待策略,如轮询断言或 waitFor 工具。

不过,对于当前的 focus 触发 suggestions 显示的场景,这种方式在 happy-dom 环境下是可以接受的。

Also applies to: 103-106

tests/unit/dashboard-logs-virtualized-special-settings-ui.test.tsx (1)

164-177: 测试断言可能过于依赖实现细节。

直接断言 ml-auto CSS 类名是脆弱的测试方式——如果将来重构样式(例如改用 justify-end 或其他布局方案),测试会失败但功能可能仍然正确。

建议考虑:

  1. 仅保留 "1h" 的存在性检查(核心功能)
  2. 或使用视觉回归测试来验证布局

当前实现可以接受,但标记为潜在的维护风险。

src/lib/webhook/utils/date.ts (1)

19-24: formatTimestamp 的 "backward compatibility" 注释具有误导性。

formatTimestamp 函数同样需要 timezone 参数,并非真正的向后兼容。如果旧代码使用单参数调用,仍会报错。建议修改注释以更准确地描述其用途(例如"别名函数"或"语义化命名")。

📝 建议的注释修改
 /**
- * Alias for formatDateTime for backward compatibility
+ * Alias for formatDateTime with semantic naming for timestamp formatting
  */
 export function formatTimestamp(date: Date, timezone: string): string {
   return formatDateTime(date, timezone);
 }
tests/unit/dashboard/dashboard-home-layout.test.tsx (1)

138-146: findClosestWithClasses 辅助函数未被使用。

该函数已定义但在测试中未被调用。建议移除未使用的代码,或者如果计划在后续测试中使用,可以添加相关测试用例。

🧹 建议移除未使用的辅助函数
-function findClosestWithClasses(element: Element | null, classes: string[]) {
-  let current = element?.parentElement ?? null;
-  while (current) {
-    const hasAll = classes.every((cls) => current.classList.contains(cls));
-    if (hasAll) return current;
-    current = current.parentElement;
-  }
-  return null;
-}
-
src/lib/webhook/types.ts (1)

102-107: 明确 timezone 的 null/undefined 语义,避免调用侧语义歧义

若调用侧需要显式表达“清空/不覆盖”,建议允许 null 并在注释中写清楚语义区分(undefined=继承默认,null=显式不指定)。这有助于和其他配置字段语义保持一致。

Proposed change
-  /** IANA timezone identifier for date/time formatting */
-  timezone?: string;
+  /** IANA timezone identifier for date/time formatting. undefined=inherit, null=explicit default */
+  timezone?: string | null;

Based on learnings: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (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.

tests/unit/lib/date-format-timezone.test.ts (1)

1-60: 测试覆盖全面且实现正确。

测试套件覆盖了关键场景:

  • UTC 和多个 IANA 时区(Asia/Shanghai、America/New_York、Asia/Tokyo)
  • 跨午夜的日期格式化
  • 字符串和数值类型的日期输入
  • 未定义时区时的降级行为
  • 带时区的 locale 格式化

使用固定的 UTC 时间戳确保了测试的确定性。

可考虑添加一个 DST(夏令时)边界的测试用例,例如使用 6 月的日期测试 America/New_York(EDT,UTC-4)以验证夏令时处理。当前 Line 25-29 的测试使用 1 月(EST,UTC-5),Line 44-48 使用 6 月但针对 Asia/Tokyo(无 DST)。

src/app/[locale]/dashboard/_components/user/key-list-header.tsx (1)

41-75: 避免硬编码日期格式,改为本地化格式化

超过 7 天时输出固定 yyyy-MM-dd,这是用户可见的日期格式,建议基于 locale 做本地化格式化以避免不同语言环境显示不一致。

建议修改示例
-import { formatInTimeZone } from "date-fns-tz";
+// 若改用 Intl.DateTimeFormat,可移除此导入
 
-function createFormatRelativeTime(
-  t: (key: string, params?: Record<string, number>) => string,
-  timeZone: string
-) {
+function createFormatRelativeTime(
+  t: (key: string, params?: Record<string, number>) => string,
+  locale: string,
+  timeZone: string
+) {
   return (timestamp: number): string => {
     const diff = Date.now() - timestamp;
@@
-    return formatInTimeZone(new Date(timestamp), timeZone, "yyyy-MM-dd");
+    return new Intl.DateTimeFormat(locale, { timeZone, dateStyle: "medium" }).format(
+      new Date(timestamp)
+    );
   };
 }
@@
-const formatRelativeTime = useMemo(() => createFormatRelativeTime(t, timeZone), [t, timeZone]);
+const formatRelativeTime = useMemo(
+  () => createFormatRelativeTime(t, locale, timeZone),
+  [t, locale, timeZone]
+);

As per coding guidelines: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text.

src/lib/utils/date.ts (1)

42-60: formatInTimeZone 添加错误处理以防止无效时区导致崩溃

formatInTimeZone 在收到无效 IANA 时区字符串时会抛出异常,可能中断渲染链路。建议添加 try/catch 后退到本地格式化,保证应用稳定性。

修改建议
 export function formatDate(date: Date, locale = "zh-CN", timezone?: string): string {
   if (timezone) {
-    return formatInTimeZone(date, timezone, "yyyy-MM-dd");
+    try {
+      return formatInTimeZone(date, timezone, "yyyy-MM-dd");
+    } catch {
+      // fall through to locale formatting
+    }
   }
   return date.toLocaleDateString(locale, {
     year: "numeric",
     month: "2-digit",
     day: "2-digit",
   });
 }
@@
 export function formatDateTime(date: Date, locale = "zh-CN", timezone?: string): string {
   if (timezone) {
-    return formatInTimeZone(date, timezone, "yyyy-MM-dd HH:mm:ss");
+    try {
+      return formatInTimeZone(date, timezone, "yyyy-MM-dd HH:mm:ss");
+    } catch {
+      // fall through to locale formatting
+    }
   }
   return date.toLocaleString(locale, {
     year: "numeric",
     month: "2-digit",
     day: "2-digit",
src/lib/webhook/renderers/dingtalk.ts (1)

1-10: 导入路径建议改为 @/ 别名以符合仓库规范。
当前仍使用相对路径导入 ../types / ../utils/date / ./index,建议统一为 @/lib/webhook/...,避免路径风格混用。

可选的调整示例
-import type {
-  ListItem,
-  Section,
-  SectionContent,
-  StructuredMessage,
-  WebhookPayload,
-  WebhookSendOptions,
-} from "../types";
-import { formatTimestamp } from "../utils/date";
-import type { Renderer } from "./index";
+import type {
+  ListItem,
+  Section,
+  SectionContent,
+  StructuredMessage,
+  WebhookPayload,
+  WebhookSendOptions,
+} from "@/lib/webhook/types";
+import { formatTimestamp } from "@/lib/webhook/utils/date";
+import type { Renderer } from "@/lib/webhook/renderers";
As per coding guidelines: Use path alias `@/` to reference files in `./src/` directory
src/app/[locale]/dashboard/logs/_components/logs-date-range-picker.tsx (2)

21-21: PickerQuickPeriod 中的 "custom" 值未被使用。

detectQuickPeriod 函数的返回类型声明为 PickerQuickPeriod | null,但实际上当日期范围不匹配任何快捷周期时返回 null 而非 "custom"。如果 "custom" 是设计上需要的返回值,建议修改函数逻辑;否则可简化类型定义。

如果需要返回 "custom" 而非 null
 function detectQuickPeriod(
   startDate?: string,
   endDate?: string,
   serverTimeZone?: string
-): PickerQuickPeriod | null {
+): PickerQuickPeriod {
   if (!startDate || !endDate) return null;
 
   for (const period of QUICK_PERIODS) {
     const range = getDateRangeForPeriod(period, serverTimeZone);
     if (range.startDate === startDate && range.endDate === endDate) {
       return period;
     }
   }
-  return null;
+  return "custom";
 }

Also applies to: 47-61


40-45: 考虑内联 getDateRangeForPeriod 函数。

此函数仅是 getQuickDateRange 的简单包装,可考虑直接在调用处使用 getQuickDateRange,减少间接层。

src/lib/notification/notification-queue.ts (1)

141-151: 重复调用 getBindingById 可优化。

bindingId 存在时,getBindingById 被调用两次:

  1. 第 145-147 行:获取 scheduleTimezone
  2. 第 228-230 行:获取 templateOverride

建议合并为一次调用并复用结果。

♻️ 建议重构
+      // Fetch binding once if bindingId is provided
+      let binding: Awaited<ReturnType<typeof getBindingById>> | null = null;
+      if (bindingId) {
+        const { getBindingById } = await import("@/repository/notification-bindings");
+        binding = await getBindingById(bindingId);
+      }
+
       // Resolve timezone for formatting
       // Priority: binding's scheduleTimezone > system timezone
       let timezone: string | undefined;
-      if (bindingId) {
-        const { getBindingById } = await import("@/repository/notification-bindings");
-        const binding = await getBindingById(bindingId);
-        timezone = binding?.scheduleTimezone ?? undefined;
-      }
+      timezone = binding?.scheduleTimezone ?? undefined;
       if (!timezone) {
         timezone = await resolveSystemTimezone();
       }

       // ... later in the code ...

-        let templateOverride: Record<string, unknown> | null = null;
-        if (bindingId) {
-          const { getBindingById } = await import("@/repository/notification-bindings");
-          const binding = await getBindingById(bindingId);
-          templateOverride = binding?.templateOverride ?? null;
-        }
+        const templateOverride = binding?.templateOverride ?? null;

Also applies to: 226-231

src/lib/rate-limit/lease.ts (2)

24-25: 类型命名建议:LeaseEntityTypeType 命名冗余。

LeaseEntityTypeType 包含两个 "Type" 后缀,建议简化为 LeaseEntityType 并将 const 数组重命名为 LeaseEntityTypesLEASE_ENTITY_TYPES

♻️ 建议重构
-export const LeaseEntityType = ["key", "user", "provider"] as const;
-export type LeaseEntityTypeType = (typeof LeaseEntityType)[number];
+export const LEASE_ENTITY_TYPES = ["key", "user", "provider"] as const;
+export type LeaseEntityType = (typeof LEASE_ENTITY_TYPES)[number];

66-84: 类型转换可简化:LeaseWindowTypeTimePeriod 定义相同。

LeaseWindowTypeTimePeriod 都是 "5h" | "daily" | "weekly" | "monthly",建议直接复用 TimePeriod 类型以避免类型断言和潜在的不同步风险。

♻️ 建议重构
+import { TimePeriod } from "./time-utils";
+
-export const LeaseWindow = ["5h", "daily", "weekly", "monthly"] as const;
-export type LeaseWindowType = (typeof LeaseWindow)[number];
+export type LeaseWindowType = TimePeriod;
+export const LeaseWindow: readonly LeaseWindowType[] = ["5h", "daily", "weekly", "monthly"];

这样 getLeaseTimeRangegetLeaseTtlSeconds 中就不需要 as TimePeriod 转换。

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

29-46: 导出 ReclusterChangeReclusterResult 类型供组件复用。

这两个类型在 src/actions/providers.ts(第 3565-3581 行)和当前组件中分别定义,造成了不必要的类型重复。建议在 providers.ts 中导出这些类型,然后在组件中通过 @/actions/providers 导入,以保持类型定义的单一性。

建议的改动

src/actions/providers.ts 中:

- type ReclusterChange = {
+ export type ReclusterChange = {
- type ReclusterResult = {
+ export type ReclusterResult = {

在组件中移除本地类型定义,改为导入:

+ import type { ReclusterChange, ReclusterResult } from "@/actions/providers";
src/lib/rate-limit/lease-service.ts (1)

153-154: 冗余的空值合并操作符

settings.quotaLeaseCapUsd ?? undefined 是冗余的——如果 quotaLeaseCapUsdnullundefined,结果仍然是 undefined

建议简化
      const ttlSeconds = settings.quotaDbRefreshIntervalSeconds ?? 10;
-      const capUsd = settings.quotaLeaseCapUsd ?? undefined;
+      const capUsd = settings.quotaLeaseCapUsd ?? undefined; // 如果确需将 null 转为 undefined,保留此写法并添加注释说明意图

如果这是为了显式将 null 转换为 undefined(根据项目约定 null 表示"显式无限制",undefined 表示"继承默认"),建议添加注释说明意图。否则可直接使用 settings.quotaLeaseCapUsd

Comment on lines +200 to +205
"expires_at": {
"name": "expires_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
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

快照与 schema 不一致,需重新生成

快照中 keys.expires_at 仍是 timestamp、notification_target_bindings.schedule_timezone 仍带默认值,且 system_settings 中缺少 timezone 列,与 src/drizzle/schema.ts 不一致。请重新运行生成流程并确保迁移对齐。Based on learnings: Modify database schema in src/drizzle/schema.ts, then run bun run db:generate to generate migrations. Never create SQL migration files manually

Also applies to: 1062-1068, 2354-2390

🤖 Prompt for AI Agents
In `@drizzle/meta/0058_snapshot.json` around lines 200 - 205, 快照显示 keys.expires_at
仍为 timestamp、notification_target_bindings.schedule_timezone 带默认值且
system_settings 缺少 timezone,与代码中的模式不一致;在模式定义文件中修正对应列(更新 keys.expires_at
的类型、移除或更新 notification_target_bindings.schedule_timezone 的默认值、为 system_settings
添加 timezone 列),然后运行生成命令 bun run db:generate 重新生成迁移与快照,确保不手工编辑 SQL 文件以使迁移与模式对齐。

Comment on lines +1487 to +1490
const range5h = await getTimeRangeForPeriod("5h");
const rangeDaily = await getTimeRangeForPeriod("daily", user.dailyResetTime || "00:00");
const rangeWeekly = await getTimeRangeForPeriod("weekly");
const rangeMonthly = await getTimeRangeForPeriod("monthly");
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

日限额统计需考虑 rolling 模式
当前 daily 使用 getTimeRangeForPeriod,会忽略 rolling 配置,导致日限额展示偏差。建议改为 getTimeRangeForPeriodWithMode 并传入 dailyResetMode。

建议修复
-    const { getTimeRangeForPeriod } = await import("@/lib/rate-limit/time-utils");
+    const { getTimeRangeForPeriod, getTimeRangeForPeriodWithMode } = await import(
+      "@/lib/rate-limit/time-utils"
+    );
@@
-    const rangeDaily = await getTimeRangeForPeriod("daily", user.dailyResetTime || "00:00");
+    const rangeDaily = await getTimeRangeForPeriodWithMode(
+      "daily",
+      user.dailyResetTime || "00:00",
+      user.dailyResetMode ?? "fixed"
+    );
🤖 Prompt for AI Agents
In `@src/actions/users.ts` around lines 1487 - 1490, The daily time range
calculation currently calls getTimeRangeForPeriod("daily", user.dailyResetTime
|| "00:00") which ignores the user's rolling vs fixed reset setting; replace
this with getTimeRangeForPeriodWithMode("daily", user.dailyResetTime || "00:00",
user.dailyResetMode) (or the equivalent parameter order used by
getTimeRangeForPeriodWithMode) so rangeDaily is computed with the dailyResetMode
taken into account; update any references to rangeDaily accordingly.

Comment on lines +264 to +268
<TimeFilters
filters={localFilters}
onFiltersChange={setLocalFilters}
serverTimeZone={serverTimeZone}
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the TimeFilters component
fd "TimeFilters" --type f

Repository: ding113/claude-code-hub

Length of output: 49


🏁 Script executed:

# Also, let's look at the file structure around the mentioned file
find src/app/\[locale\]/dashboard/logs/_components -type f -name "*.tsx" -o -name "*.ts" | head -20

Repository: ding113/claude-code-hub

Length of output: 1653


🏁 Script executed:

# Let's read the usage-logs-filters.tsx file to understand the context
cat -n src/app/\[locale\]/dashboard/logs/_components/usage-logs-filters.tsx | head -100

Repository: ding113/claude-code-hub

Length of output: 3953


🏁 Script executed:

# Continue reading the usage-logs-filters.tsx file to see the TimeFilters usage
cat -n src/app/\[locale\]/dashboard/logs/_components/usage-logs-filters.tsx | sed -n '250,280p'

Repository: ding113/claude-code-hub

Length of output: 1255


🏁 Script executed:

# Let's examine the TimeFilters component itself
cat -n src/app/\[locale\]/dashboard/logs/_components/filters/time-filters.tsx

Repository: ding113/claude-code-hub

Length of output: 7379


🏁 Script executed:

# Let's also check the quick-filters-bar to see how presets are calculated
cat -n src/app/\[locale\]/dashboard/logs/_components/filters/quick-filters-bar.tsx

Repository: ding113/claude-code-hub

Length of output: 2827


🏁 Script executed:

# Let's search for where the preset logic is applied (the onPresetToggle implementation)
cat -n src/app/\[locale\]/dashboard/logs/_components/usage-logs-filters.tsx | sed -n '100,250p'

Repository: ding113/claude-code-hub

Length of output: 5804


🏁 Script executed:

# Let's check what imports are used at the top of usage-logs-filters.tsx for date operations
cat -n src/app/\[locale\]/dashboard/logs/_components/usage-logs-filters.tsx | sed -n '1,50p'

Repository: ding113/claude-code-hub

Length of output: 2128


🏁 Script executed:

# Let's look for the time-range utility functions
cat -n src/app/\[locale\]/dashboard/logs/_utils/time-range.ts

Repository: ding113/claude-code-hub

Length of output: 4140


快速筛选的时区计算与 TimeFilters 不一致。

TimeFilters 已正确按 serverTimeZone 展示和解析日期,但快速筛选("today" 和 "this-week")仍使用本地时区的 startOfDay()startOfWeek() 计算。这导致用户选择快速筛选时的时间范围与显示时区产生偏差。

建议将快速筛选的时间计算改为基于 serverTimeZone,可参考已存在的 getQuickDateRange() 工具函数(time-range.ts)进行改造,该函数已支持时区感知的日期范围计算。

🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/logs/_components/usage-logs-filters.tsx around
lines 264 - 268, The quick-filter ("today", "this-week") range calculation
currently uses local timezone startOfDay/startOfWeek and thus diverges from
TimeFilters' serverTimeZone-aware parsing; update the quick-filter logic that
sets localFilters (used by TimeFilters via setLocalFilters) to compute ranges
using the existing getQuickDateRange(timeKey, serverTimeZone) utility from
time-range.ts so the generated start/end dates are timezone-aware and consistent
with TimeFilters' display and parsing. Ensure you replace any direct
startOfDay/startOfWeek calls with getQuickDateRange and pass serverTimeZone
through to the function when handling quick-filter selection.

enableMultiProviderTypes,
}: ProviderListItemProps) {
const router = useRouter();
const timeZone = useTimeZone() ?? "UTC";
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:

cat -n src/app/[locale]/settings/providers/_components/provider-list-item.legacy.tsx | head -80

Repository: ding113/claude-code-hub

Length of output: 3682


🌐 Web query:

date-fns-tz formatInTimeZone RangeError invalid date timezone

💡 Result:

date-fns-tz’s formatInTimeZone(date, timeZone, fmt) ultimately calls date-fns/format. If either the date or the time zone can’t be resolved, it becomes an Invalid Date, and format throws a RangeError (often shown as “Invalid time value” / “invalid date”). [1][2]

Common causes + fixes:

  1. Your date is invalid
  • Happens when parsing a non-ISO string, a “time-only” string, undefined, or an out-of-bounds timestamp; operations like ISO conversion/formatting then throw RangeError. [3]
  • Fix: ensure you pass a real Date (or a valid ISO string), and validate before formatting.
    import { isValid } from 'date-fns'
    import { formatInTimeZone } from 'date-fns-tz'
    
    if (!isValid(date)) throw new Error('Invalid date input')
  1. Your timeZone is invalid in that runtime
  • formatInTimeZone expects an IANA zone like America/New_York (or an offset like -05:00). If the zone string is wrong/empty, it will throw. [1][2]
  • Also, some environments/users can hit RangeError: Invalid time zone specified: <IANA zone> even when the zone looks valid (because time zone support comes from the platform Intl implementation). This has been reported in the wild. [4]
  1. Missing / limited Intl time zone support (notably some React Native / Android setups)
  • date-fns-tz relies on Intl. On Android in React Native, Intl isn’t available by default in some configurations; you may need an Intl-enabled JS engine build or a polyfill. [5]
  • Fix: add the recommended Intl support/polyfill for your target (RN/Android, Hermes vs non-Hermes). [5]

If you paste:

  • the exact date value you’re passing (and how you create it),
  • the exact timeZone string,
  • and your runtime (browser + version / Node version / React Native + Hermes?)
    …I can tell you which of the above is triggering your specific RangeError.

Sources: [1] [2] [3] [4] [5]


🌐 Web query:

next-intl useTimeZone hook return value behavior empty string

💡 Result:

useTimeZone() just returns the timeZone value from the active next-intl config/context as-is (it doesn’t normalize, validate, or “fall back” if it’s empty). In the compiled hook, it effectively does return useConfig("useTimeZone").timeZone; [1].

So if you observe "" (empty string), that means your resolved next-intl config’s timeZone is actually "" (e.g., you passed timeZone="" to NextIntlClientProvider, or returned timeZone: '' from getRequestConfig). next-intl won’t replace that with a default [1].

When you don’t configure timeZone, next-intl’s server-side default is taken from the runtime via Intl.DateTimeFormat().resolvedOptions().timeZone [2], and the docs describe this as “the time zone of the server runtime will be used” [3].

Practical takeaway: don’t use "" to mean “unset”—omit timeZone (or use undefined) or provide a valid IANA zone like "America/New_York" [3]. If you need the user’s browser zone, compute it separately with Intl.DateTimeFormat().resolvedOptions().timeZone and pass that.

Sources: [1] [2] [3]


🏁 Script executed:

cat -n src/app/[locale]/settings/providers/_components/provider-list-item.legacy.tsx | sed -n '350,365p'

Repository: ding113/claude-code-hub

Length of output: 965


防止 formatInTimeZone 因无效日期抛 RangeError

date-fns-tz 在接收无效日期或时区时会抛 RangeError。当前代码仅对 lastCallTime 做 truthy 判断,若其值为非空但无效的日期字符串(如格式错误的字符串),new Date(item.lastCallTime) 将创建 Invalid Date,formatInTimeZone 随后会抛错导致渲染崩溃。建议添加日期有效性检查。另外,使用 || 替代 ?? 做 timeZone 的回退更为稳妥。

建议修改
-  const timeZone = useTimeZone() ?? "UTC";
+  const timeZone = useTimeZone() || "UTC";
...
-            {item.lastCallTime
-              ? formatInTimeZone(new Date(item.lastCallTime), timeZone, "yyyy-MM-dd HH:mm")
-              : "-"}
+            {item.lastCallTime
+              ? (() => {
+                  const lastCallDate = new Date(item.lastCallTime);
+                  if (Number.isNaN(lastCallDate.getTime())) {
+                    return "-";
+                  }
+                  return formatInTimeZone(lastCallDate, timeZone, "yyyy-MM-dd HH:mm");
+                })()
+              : "-"}
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/provider-list-item.legacy.tsx
at line 66, The code risks a RangeError when formatInTimeZone receives an
invalid date or timezone: update the timeZone assignment to use fallback with ||
(e.g., const timeZone = useTimeZone() || "UTC") and add a date validity check
before calling formatInTimeZone — compute const d = new Date(item.lastCallTime)
and ensure it's valid (e.g., !Number.isNaN(d.getTime()) or use isValid from
date-fns) and only call formatInTimeZone(d, timeZone, ...) when valid; otherwise
render a safe fallback (empty string or "—") to avoid rendering crashes.

Comment on lines +359 to +426
// 8. User 每日额度(User 独有的常用预算)- null 表示无限制
if (user.dailyQuota !== null) {
const dailyCheck = await RateLimitService.checkUserDailyCost(
user.id,
user.dailyQuota,
user.dailyResetTime,
user.dailyResetMode
);

if (!dailyCheck.allowed) {
logger.warn(`[RateLimit] User daily limit exceeded: user=${user.id}, ${dailyCheck.reason}`);

const { getLocale } = await import("next-intl/server");
const locale = await getLocale();

// 根据模式选择不同的错误消息
if (user.dailyResetMode === "rolling") {
// rolling 模式:使用滚动窗口专用消息(无固定重置时间)
const message = await getErrorMessageServer(
locale,
ERROR_CODES.RATE_LIMIT_DAILY_ROLLING_EXCEEDED,
{
current: (dailyCheck.current || 0).toFixed(4),
limit: user.dailyQuota.toFixed(4),
}
);

throw new RateLimitError(
"rate_limit_error",
message,
"daily_quota",
dailyCheck.current || 0,
user.dailyQuota,
null, // 滚动窗口没有固定重置时间
null
);
} else {
// fixed 模式:有固定重置时间
const resetInfo = await getResetInfoWithMode(
"daily",
user.dailyResetTime,
user.dailyResetMode
);
const resetTime =
resetInfo.resetAt?.toISOString() ??
new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();

const message = await getErrorMessageServer(
locale,
ERROR_CODES.RATE_LIMIT_DAILY_QUOTA_EXCEEDED,
{
current: (dailyCheck.current || 0).toFixed(4),
limit: user.dailyQuota.toFixed(4),
resetTime,
}
);

throw new RateLimitError(
"rate_limit_error",
message,
"daily_quota",
dailyCheck.current || 0,
user.dailyQuota,
resetTime,
null
);
}
}
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

User 日额度判断需区分 undefined 与 null

当前条件只排除了 null,若 dailyQuota 为 undefined 仍会进入校验,可能导致意外限流。建议显式区分 undefined(继承默认)与 null(无限制),或在此处先解析默认值后再判断。

建议修改
-    if (user.dailyQuota !== null) {
+    if (user.dailyQuota !== null && user.dailyQuota !== undefined) {
Based on learnings: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (e.g., matches DB schema or special UI logic) and undefined signifies 'inherit default'. This pattern should be consistently reflected in type definitions across related fields to preserve semantic clarity between database constraints and UI behavior.
🤖 Prompt for AI Agents
In `@src/app/v1/_lib/proxy/rate-limit-guard.ts` around lines 359 - 426, The
current guard if (user.dailyQuota !== null) incorrectly allows undefined to
proceed into RateLimitService.checkUserDailyCost and cause unintended
throttling; update the logic to distinguish undefined (inherit default) from
null (unlimited) by only running the daily check when the quota is a concrete
number. Concretely, in the block around user.dailyQuota, compute a local quota
like const quota = user.dailyQuota; and change the condition to typeof quota ===
"number" (or explicitly check !== null && quota !== undefined), pass quota into
RateLimitService.checkUserDailyCost and use quota (not user.dailyQuota) when
formatting limit and building RateLimitError messages, keeping the existing
branches for rolling vs fixed reset using getResetInfoWithMode and
getErrorMessageServer.

Comment on lines +732 to +740
// 系统时区配置(可选)
// 必须是有效的 IANA 时区标识符(如 "Asia/Shanghai", "America/New_York")
timezone: z
.string()
.refine((val) => isValidIANATimezone(val), {
message: "无效的时区标识符,请使用 IANA 时区格式(如 Asia/Shanghai)",
})
.nullable()
.optional(),
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

时区校验提示需走 i18n。
新增错误提示为硬编码字符串,属于用户可见文案;请改为多语言资源或统一错误映射。
As per coding guidelines: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text.

🤖 Prompt for AI Agents
In `@src/lib/validation/schemas.ts` around lines 732 - 740, The timezone zod
schema currently uses a hardcoded Chinese error message; replace that with an
i18n lookup so user-facing validation messages are localized. Update the
timezone schema (the timezone field in src/lib/validation/schemas.ts) to call
the project's i18n function or error-mapping utility (e.g.,
t('validation.invalidTimezone') or the centralized error mapper) instead of the
literal string, ensuring keys exist for zh-CN, zh-TW, en, ja, and ru; keep the
refine predicate (isValidIANATimezone) unchanged and return the i18n key/value
for the message so the validation layer emits localized text rather than a
hardcoded string.

Comment on lines +786 to +813
// Quota lease settings
quotaDbRefreshIntervalSeconds: z.coerce
.number()
.int("DB refresh interval must be an integer")
.min(1, "DB refresh interval cannot be less than 1 second")
.max(300, "DB refresh interval cannot exceed 300 seconds")
.optional(),
quotaLeasePercent5h: z.coerce
.number()
.min(0, "Lease percent cannot be negative")
.max(1, "Lease percent cannot exceed 1")
.optional(),
quotaLeasePercentDaily: z.coerce
.number()
.min(0, "Lease percent cannot be negative")
.max(1, "Lease percent cannot exceed 1")
.optional(),
quotaLeasePercentWeekly: z.coerce
.number()
.min(0, "Lease percent cannot be negative")
.max(1, "Lease percent cannot exceed 1")
.optional(),
quotaLeasePercentMonthly: z.coerce
.number()
.min(0, "Lease percent cannot be negative")
.max(1, "Lease percent cannot exceed 1")
.optional(),
quotaLeaseCapUsd: z.coerce.number().min(0, "Lease cap cannot be negative").nullable().optional(),
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

租赁配额校验文案需统一多语言。
新增英文提示会直接返回给前端,和规范不一致;建议使用 i18n 资源。
As per coding guidelines: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text.

🤖 Prompt for AI Agents
In `@src/lib/validation/schemas.ts` around lines 786 - 813, The Zod validators for
quotaDbRefreshIntervalSeconds, quotaLeasePercent5h, quotaLeasePercentDaily,
quotaLeasePercentWeekly, quotaLeasePercentMonthly, and quotaLeaseCapUsd
currently use hardcoded English error messages; replace those literal strings
with i18n lookups (use the project's translation function and stable i18n keys)
so validators call the i18n resource instead of embedding text, and add
corresponding keys/translations for zh-CN, zh-TW, en, ja, ru; keep the same
validation logic (.int, .min, .max, .nullable, .optional) and ensure messages
are passed the translated string values.

Comment on lines 8 to 11
const fields = [
{ label: "失败次数", value: `${data.failureCount} 次` },
{ label: "预计恢复", value: formatDateTime(data.retryAt) },
{ label: "预计恢复", value: formatDateTime(data.retryAt, timezone || "UTC") },
];
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

硬编码的中文字符串违反 i18n 规范。

根据编码规范,所有用户可见的字符串必须使用 i18n(支持 5 种语言:zh-CN、zh-TW、en、ja、ru)。当前 "失败次数""预计恢复" 等字符串是硬编码的。

建议将这些字符串提取到 i18n 消息文件中,并通过翻译函数获取。

As per coding guidelines: "All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text"

🤖 Prompt for AI Agents
In `@src/lib/webhook/templates/circuit-breaker.ts` around lines 8 - 11, The fields
array in circuit-breaker.ts contains hardcoded user-facing labels ("失败次数",
"预计恢复") which violate i18n rules; replace these literals with calls to the
project's translation function (e.g., t or i18n.t) and add appropriate message
keys (e.g., "circuitBreaker.failureCount", "circuitBreaker.estimatedRecovery")
in the i18n message files for all supported locales (zh-CN, zh-TW, en, ja, ru);
update the const fields initialization to use the translation calls (passing any
interpolations like `${data.failureCount} 次` as variables where appropriate) and
ensure formatDateTime(data.retryAt, timezone || "UTC") remains as the translated
value for the "estimated recovery" label.

Comment on lines +198 to +211
quotaDbRefreshIntervalSeconds: dbSettings?.quotaDbRefreshIntervalSeconds ?? 10,
quotaLeasePercent5h: dbSettings?.quotaLeasePercent5h
? parseFloat(dbSettings.quotaLeasePercent5h)
: 0.05,
quotaLeasePercentDaily: dbSettings?.quotaLeasePercentDaily
? parseFloat(dbSettings.quotaLeasePercentDaily)
: 0.05,
quotaLeasePercentWeekly: dbSettings?.quotaLeasePercentWeekly
? parseFloat(dbSettings.quotaLeasePercentWeekly)
: 0.05,
quotaLeasePercentMonthly: dbSettings?.quotaLeasePercentMonthly
? parseFloat(dbSettings.quotaLeasePercentMonthly)
: 0.05,
quotaLeaseCapUsd: dbSettings?.quotaLeaseCapUsd ? parseFloat(dbSettings.quotaLeaseCapUsd) : null,
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

避免把 0 误判为未配置
当前使用 truthy 判断时,"0" 会被回退到默认值或 null,导致无法显式关闭/清零配额租赁。建议改为 null/undefined 判断并对 NaN 回退。

建议修复
 export function toSystemSettings(dbSettings: any): SystemSettings {
   const defaultResponseFixerConfig: ResponseFixerConfig = {
     fixTruncatedJson: true,
     fixSseFormat: true,
     fixEncoding: true,
     maxJsonDepth: 200,
     maxFixSize: 1024 * 1024,
   };
+
+  const parseOptionalFloatOrDefault = (value: unknown, fallback: number): number => {
+    if (value === null || value === undefined) return fallback;
+    const parsed = Number.parseFloat(String(value));
+    return Number.isNaN(parsed) ? fallback : parsed;
+  };
+
+  const parseOptionalFloatOrNull = (value: unknown): number | null => {
+    if (value === null || value === undefined) return null;
+    const parsed = Number.parseFloat(String(value));
+    return Number.isNaN(parsed) ? null : parsed;
+  };
@@
-    quotaLeasePercent5h: dbSettings?.quotaLeasePercent5h
-      ? parseFloat(dbSettings.quotaLeasePercent5h)
-      : 0.05,
-    quotaLeasePercentDaily: dbSettings?.quotaLeasePercentDaily
-      ? parseFloat(dbSettings.quotaLeasePercentDaily)
-      : 0.05,
-    quotaLeasePercentWeekly: dbSettings?.quotaLeasePercentWeekly
-      ? parseFloat(dbSettings.quotaLeasePercentWeekly)
-      : 0.05,
-    quotaLeasePercentMonthly: dbSettings?.quotaLeasePercentMonthly
-      ? parseFloat(dbSettings.quotaLeasePercentMonthly)
-      : 0.05,
-    quotaLeaseCapUsd: dbSettings?.quotaLeaseCapUsd ? parseFloat(dbSettings.quotaLeaseCapUsd) : null,
+    quotaLeasePercent5h: parseOptionalFloatOrDefault(dbSettings?.quotaLeasePercent5h, 0.05),
+    quotaLeasePercentDaily: parseOptionalFloatOrDefault(
+      dbSettings?.quotaLeasePercentDaily,
+      0.05
+    ),
+    quotaLeasePercentWeekly: parseOptionalFloatOrDefault(
+      dbSettings?.quotaLeasePercentWeekly,
+      0.05
+    ),
+    quotaLeasePercentMonthly: parseOptionalFloatOrDefault(
+      dbSettings?.quotaLeasePercentMonthly,
+      0.05
+    ),
+    quotaLeaseCapUsd: parseOptionalFloatOrNull(dbSettings?.quotaLeaseCapUsd),
📝 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
quotaDbRefreshIntervalSeconds: dbSettings?.quotaDbRefreshIntervalSeconds ?? 10,
quotaLeasePercent5h: dbSettings?.quotaLeasePercent5h
? parseFloat(dbSettings.quotaLeasePercent5h)
: 0.05,
quotaLeasePercentDaily: dbSettings?.quotaLeasePercentDaily
? parseFloat(dbSettings.quotaLeasePercentDaily)
: 0.05,
quotaLeasePercentWeekly: dbSettings?.quotaLeasePercentWeekly
? parseFloat(dbSettings.quotaLeasePercentWeekly)
: 0.05,
quotaLeasePercentMonthly: dbSettings?.quotaLeasePercentMonthly
? parseFloat(dbSettings.quotaLeasePercentMonthly)
: 0.05,
quotaLeaseCapUsd: dbSettings?.quotaLeaseCapUsd ? parseFloat(dbSettings.quotaLeaseCapUsd) : null,
const parseOptionalFloatOrDefault = (value: unknown, fallback: number): number => {
if (value === null || value === undefined) return fallback;
const parsed = Number.parseFloat(String(value));
return Number.isNaN(parsed) ? fallback : parsed;
};
const parseOptionalFloatOrNull = (value: unknown): number | null => {
if (value === null || value === undefined) return null;
const parsed = Number.parseFloat(String(value));
return Number.isNaN(parsed) ? null : parsed;
};
quotaDbRefreshIntervalSeconds: dbSettings?.quotaDbRefreshIntervalSeconds ?? 10,
quotaLeasePercent5h: parseOptionalFloatOrDefault(dbSettings?.quotaLeasePercent5h, 0.05),
quotaLeasePercentDaily: parseOptionalFloatOrDefault(
dbSettings?.quotaLeasePercentDaily,
0.05
),
quotaLeasePercentWeekly: parseOptionalFloatOrDefault(
dbSettings?.quotaLeasePercentWeekly,
0.05
),
quotaLeasePercentMonthly: parseOptionalFloatOrDefault(
dbSettings?.quotaLeasePercentMonthly,
0.05
),
quotaLeaseCapUsd: parseOptionalFloatOrNull(dbSettings?.quotaLeaseCapUsd),
🤖 Prompt for AI Agents
In `@src/repository/_shared/transformers.ts` around lines 198 - 211, The code
incorrectly uses truthy checks so string "0" becomes false and falls back to
defaults; update each quota field check to test for null/undefined (e.g.,
dbSettings?.quotaLeasePercent5h != null) then parseFloat and if
Number.isNaN(parsed) fall back to the intended default (0.05 for percent fields,
null for quotaLeaseCapUsd). Apply this to quotaLeasePercent5h,
quotaLeasePercentDaily, quotaLeasePercentWeekly, quotaLeasePercentMonthly and
quotaLeaseCapUsd, keeping quotaDbRefreshIntervalSeconds as-is (it already uses
??).

Comment on lines +63 to +99
function normalizeHostWithPort(rawUrl: string): string | null {
const trimmed = rawUrl.trim();
if (!trimmed) return null;

// Add https:// if no scheme present
let urlString = trimmed;
if (!/^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(trimmed)) {
urlString = `https://${trimmed}`;
}

try {
const parsed = new URL(urlString);
const hostname = parsed.hostname?.toLowerCase();
if (!hostname) return null;

// Strip www. prefix
const normalizedHostname = hostname.startsWith("www.") ? hostname.slice(4) : hostname;

// Determine port
let port: string;
if (parsed.port) {
port = parsed.port;
} else {
// Use protocol default port
port = parsed.protocol === "http:" ? "80" : "443";
}

// IPv6 addresses already have brackets from URL parser (e.g., "[::1]")
// Just append the port directly
return `${normalizedHostname}:${port}`;
} catch (error) {
logger.debug("[ProviderVendor] Failed to parse URL for host:port", {
urlLength: rawUrl.length,
error: error instanceof Error ? error.message : String(error),
});
return null;
}
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

IPv6 host:port 归一化未加方括号会产生歧义

URL.hostname 对 IPv6 会返回不带方括号的地址,当前实现会生成 ::1:443 这类不可解析的 key,与注释中的 [ipv6]:port 规则不一致。建议检测包含冒号的主机名并补齐方括号。

建议修改
-    // IPv6 addresses already have brackets from URL parser (e.g., "[::1]")
-    // Just append the port directly
-    return `${normalizedHostname}:${port}`;
+    const hostForKey =
+      normalizedHostname.includes(":") && !normalizedHostname.startsWith("[")
+        ? `[${normalizedHostname}]`
+        : normalizedHostname;
+    return `${hostForKey}:${port}`;
🤖 Prompt for AI Agents
In `@src/repository/provider-endpoints.ts` around lines 63 - 99,
normalizeHostWithPort produces ambiguous IPv6 keys (e.g., "::1:443") because
URL.hostname returns IPv6 without brackets; update the function
(normalizeHostWithPort) to wrap the normalizedHostname in square brackets when
it contains a colon and doesn't already start with "[" before appending the port
(i.e., detect hostname.includes(":") && !hostname.startsWith("["), then set
normalizedHostname = `[${normalizedHostname}]`) so returned strings follow the
"[ipv6]:port" rule.

@ding113 ding113 closed this Jan 28, 2026
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Jan 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

Status: Done

2 participants