-
Notifications
You must be signed in to change notification settings - Fork 142
feat(providers): add vendor endpoints with probes and failover #608
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. 📝 WalkthroughWalkthrough新增完整的提供商端点子系统:数据库迁移与快照、端点探测与记录、端点/供应商类型电路断路器、端点选择与代理重试集成、后端 actions/API、管理端 UI 多语言扩展及大量测试与调度/清理工具(新增表、仓库、调度器、前后端组件与路由)。 Changes
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @ding113, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the reliability and management of AI model providers by introducing a robust system for endpoint health monitoring and dynamic routing. It establishes a new data architecture to group providers by vendors, implements automated probing to assess endpoint availability and latency, and integrates sophisticated circuit breaker patterns to prevent cascading failures. The changes provide administrators with granular control and visibility over individual endpoints and entire vendor services, ensuring a more resilient and performant proxy layer. Highlights
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This is a substantial and well-executed pull request that introduces a robust provider endpoint management system, complete with failover logic and multi-level circuit breaking. The database schema changes are well-designed, the new backend logic is solid, and the UI additions follow modern React patterns. The inclusion of comprehensive unit tests for the new critical logic is particularly commendable. I have a couple of suggestions to enhance UI consistency and improve maintainability, but overall, this is an excellent contribution to the project.
| return { | ||
| ok: false, | ||
| error: "无权限执行此操作", | ||
| errorCode: ERROR_CODES.PERMISSION_DENIED, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Throughout this file, error messages returned in the ActionResult are hardcoded in Chinese (e.g., "无权限执行此操作"). While the frontend seems to use errorCode for translation, this approach has some drawbacks:
- Inconsistency: It's inconsistent with the i18n pattern used elsewhere in the application.
- Maintainability: If a new error is added, developers must remember to add a corresponding translation case in the frontend.
- Fallback Issue: If an
errorCodeis not handled by the frontend, the user will see a hardcoded Chinese message, which is not ideal for non-Chinese users.
A better practice would be for server actions to return only errorCodes and let the client be fully responsible for localization. The hardcoded strings should be removed, or replaced with English as a developer-facing fallback.
| return { | |
| ok: false, | |
| error: "无权限执行此操作", | |
| errorCode: ERROR_CODES.PERMISSION_DENIED, | |
| }; | |
| return { | |
| ok: false, | |
| error: "Permission denied", | |
| errorCode: ERROR_CODES.PERMISSION_DENIED, | |
| }; |
| onClick={() => { | ||
| if (confirm(t("confirmDeleteEndpoint"))) { | ||
| deleteMutation.mutate(); | ||
| } | ||
| }} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In @drizzle/0055_sour_wallow.sql:
- Around line 1-11: The created_at column in provider_endpoint_probe_logs has
DEFAULT now() but lacks NOT NULL; update the table definition (or migration) to
add NOT NULL on the "created_at" column while preserving DEFAULT now() (i.e.,
change the column definition to "created_at timestamp with time zone DEFAULT
now() NOT NULL"); if applying to an existing DB, include a pre-migration step to
backfill NULL created_at rows (UPDATE provider_endpoint_probe_logs SET
created_at = now() WHERE created_at IS NULL) before altering the column to set
NOT NULL.
In @messages/ru/dashboard.json:
- Around line 1001-1022: Remove the unused localization key columns.method under
probeHistory (i.e., "probeHistory.columns.method") since the UI only renders
time, status, latency and error; update the Russian file by deleting that
"method" entry and mirror the same removal across other locale files so all
language bundles stay consistent.
In @src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx:
- Line 240: The providerTypes array in provider-vendor-view.tsx is missing
"claude-auth" and "gemini-cli" and the button labels are rendered raw instead of
via i18n; update the providerTypes constant to include "claude-auth" and
"gemini-cli" and change the button rendering to call the translation function
(t) with the camel-cased key (use the same getCamelCase helper used in
provider-form.tsx) for each type, e.g.,
t(`providerTypes.${getCamelCase(type)}`), while keeping the Button props (key,
variant, size, onClick, className) the same.
In @src/app/v1/_lib/proxy/provider-selector.ts:
- Around line 879-885: The hard-coded Chinese string assigned to details in the
context.filteredProviders push (details: "供应商类型临时熔断") should be replaced with an
i18n lookup so user-visible messages honor localization and are safe when
verboseProviderError exposes them; update the push call in provider-selector
(where context.filteredProviders?.push({ id: p.id, name: p.name, reason:
"circuit_open", details: ... })) to call the i18n translation function or a
resource key (e.g., t('provider.circuit_open')) and ensure the same translation
is applied before sending errors to users (see verboseProviderError handling
around verboseProviderError / lines ~485-488) so no raw Chinese strings are
returned.
🧹 Nitpick comments (27)
src/lib/provider-endpoints/endpoint-selector.ts (1)
30-54: 实现正确,存在轻微冗余。整体逻辑合理:排除指定端点 → 过滤启用状态 → 并行检查熔断状态 → 排序返回。
Line 38 的
!e.deletedAt检查与findProviderEndpointsByVendorAndType仓库函数中的isNull(providerEndpoints.deletedAt)条件重复。这是防御性编码,不影响正确性,但如果确认仓库层保证不返回已删除记录,可以移除此检查以简化代码。可选:移除冗余的 deletedAt 检查
- const filtered = endpoints.filter((e) => e.isEnabled && !e.deletedAt && !excludeSet.has(e.id)); + const filtered = endpoints.filter((e) => e.isEnabled && !excludeSet.has(e.id));src/lib/redis/endpoint-circuit-breaker-state.ts (2)
30-48: 反序列化时缺少circuitState值的验证。
deserializeState函数直接将data.circuitState强制转换为EndpointCircuitState,但未验证该值是否为有效的枚举值("closed" | "open" | "half-open")。如果 Redis 中存储了无效值,可能导致意外行为。建议添加验证逻辑
+const VALID_CIRCUIT_STATES: EndpointCircuitState[] = ["closed", "open", "half-open"]; + +function isValidCircuitState(value: string): value is EndpointCircuitState { + return VALID_CIRCUIT_STATES.includes(value as EndpointCircuitState); +} + function deserializeState(data: Record<string, string>): EndpointCircuitBreakerState { + const circuitState = isValidCircuitState(data.circuitState) ? data.circuitState : "closed"; return { failureCount: Number.parseInt(data.failureCount || "0", 10), lastFailureTime: data.lastFailureTime ? Number.parseInt(data.lastFailureTime, 10) : null, - circuitState: (data.circuitState as EndpointCircuitState) || "closed", + circuitState, circuitOpenUntil: data.circuitOpenUntil ? Number.parseInt(data.circuitOpenUntil, 10) : null, halfOpenSuccessCount: Number.parseInt(data.halfOpenSuccessCount || "0", 10), }; }
89-93:hset和expire非原子操作,存在潜在的状态不一致风险。当前实现先调用
hset再调用expire,如果在两次调用之间发生错误或进程崩溃,可能导致 key 没有设置 TTL 而永不过期。可考虑使用 pipeline 或 Lua 脚本保证原子性
try { const key = getStateKey(endpointId); const data = serializeState(state); - await redis.hset(key, data); - await redis.expire(key, STATE_TTL_SECONDS); + const pipeline = redis.pipeline(); + pipeline.hset(key, data); + pipeline.expire(key, STATE_TTL_SECONDS); + await pipeline.exec(); } catch (error) {src/lib/endpoint-circuit-breaker.ts (2)
32-33: 内存缓存无淘汰机制,可能导致内存增长。
healthMap和loadedFromRedis会随着端点数量增加而无限增长。在长时间运行的进程中,如果端点被删除或不再使用,相关条目不会被清理。可以考虑:
- 在
resetEndpointCircuit中从healthMap删除条目- 或使用 LRU 缓存替代 Map
187-196:resetEndpointCircuit未清理内存缓存条目。虽然函数重置了健康状态的所有字段并删除了 Redis 条目,但
healthMap中的条目仍然保留。此外,loadedFromRedis中的条目也未清除,这意味着后续调用不会再从 Redis 加载(尽管对于重置场景这可能是期望的行为)。考虑清理缓存条目
export async function resetEndpointCircuit(endpointId: number): Promise<void> { - const health = getOrCreateHealthSync(endpointId); - health.circuitState = "closed"; - health.failureCount = 0; - health.lastFailureTime = null; - health.circuitOpenUntil = null; - health.halfOpenSuccessCount = 0; - + healthMap.delete(endpointId); + loadedFromRedis.delete(endpointId); await deleteEndpointCircuitState(endpointId); }tests/unit/lib/endpoint-circuit-breaker.test.ts (2)
3-9: 类型定义与主模块重复。
SavedEndpointCircuitState类型与src/lib/redis/endpoint-circuit-breaker-state.ts中的EndpointCircuitBreakerState相同。建议从主模块导入类型以避免不一致。从主模块导入类型
import { afterEach, describe, expect, test, vi } from "vitest"; +import type { EndpointCircuitBreakerState } from "@/lib/redis/endpoint-circuit-breaker-state"; -type SavedEndpointCircuitState = { - failureCount: number; - lastFailureTime: number | null; - circuitState: "closed" | "open" | "half-open"; - circuitOpenUntil: number | null; - halfOpenSuccessCount: number; -}; +type SavedEndpointCircuitState = EndpointCircuitBreakerState;
26-123: 建议增加边界情况测试。当前测试覆盖了主要流程,但缺少以下场景的测试:
- Redis 不可用时的降级行为
half-open状态下失败时重新打开断路器- 阈值为 0 时禁用断路器
建议后续补充这些边界情况的测试以提高覆盖率。
src/lib/redis/vendor-type-circuit-breaker-state.ts (2)
38-45: 与端点模块相同:反序列化缺少circuitState验证。建议添加对
circuitState值的验证,确保只接受 "closed" 或 "open"。建议添加验证逻辑
+const VALID_VENDOR_CIRCUIT_STATES: VendorTypeCircuitStateValue[] = ["closed", "open"]; + +function isValidVendorCircuitState(value: string): value is VendorTypeCircuitStateValue { + return VALID_VENDOR_CIRCUIT_STATES.includes(value as VendorTypeCircuitStateValue); +} + function deserializeState(data: Record<string, string>): VendorTypeCircuitBreakerState { + const circuitState = isValidVendorCircuitState(data.circuitState) ? data.circuitState : "closed"; return { - circuitState: (data.circuitState as VendorTypeCircuitStateValue) || "closed", + circuitState, circuitOpenUntil: data.circuitOpenUntil ? Number.parseInt(data.circuitOpenUntil, 10) : null, lastFailureTime: data.lastFailureTime ? Number.parseInt(data.lastFailureTime, 10) : null, manualOpen: data.manualOpen === "1", }; }
87-91: 与端点模块相同:hset和expire非原子操作。建议使用 pipeline 保证原子性,与端点模块保持一致的改进方案。
src/app/api/availability/endpoints/route.ts (2)
6-13:PROVIDER_TYPES常量在多处重复定义该常量在以下位置重复定义:
src/app/api/availability/endpoints/route.ts(此处)src/app/api/actions/[...route]/route.ts(ProviderTypeSchema)src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx建议将其提取到
src/types/provider.ts或创建一个共享常量文件,以保持单一数据源。建议:在 types/provider.ts 中导出常量
// src/types/provider.ts export const PROVIDER_TYPES = [ "claude", "claude-auth", "codex", "gemini", "gemini-cli", "openai-compatible", ] as const; export type ProviderType = (typeof PROVIDER_TYPES)[number];然后在其他文件中直接导入使用。
22-45: API 路由未使用 Hono 框架根据编码规范,
src/app/api/**/*.{ts,tsx}路径下的 API 路由应使用 Hono 框架。当前实现直接使用NextResponse。如果这是有意为之的简单路由设计,可以忽略此建议。否则建议考虑与项目中其他 API 路由保持一致的风格。
src/app/api/actions/[...route]/route.ts (1)
344-351:ProviderTypeSchema与其他位置的定义重复此处的
ProviderTypeSchema与src/app/api/availability/endpoints/route.ts中的PROVIDER_TYPES和src/types/provider.ts中的ProviderType类型定义重复。建议在类型定义文件中导出一个共享的 Zod schema 或常量数组,然后在此处引用。
建议:创建共享的 Zod schema
// src/types/provider.ts 或 src/lib/validation/schemas.ts import { z } from "zod"; export const PROVIDER_TYPES = [ "claude", "claude-auth", "codex", "gemini", "gemini-cli", "openai-compatible", ] as const; export const ProviderTypeSchema = z.enum(PROVIDER_TYPES); export type ProviderType = z.infer<typeof ProviderTypeSchema>;src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx (3)
35-42:PROVIDER_TYPES第三处重复定义此常量在项目中已有多处定义,建议从共享位置导入以保持一致性。
61-63: 加载供应商失败时用户无反馈当
getProviderVendors()失败时,错误仅记录到控制台,用户界面无任何提示。建议添加 toast 通知以改善用户体验。建议:添加错误提示
useEffect(() => { - getProviderVendors().then(setVendors).catch(console.error); + getProviderVendors() + .then(setVendors) + .catch((error) => { + console.error(error); + toast.error(tErrors("fetchFailed")); + }); }, []);
78-91: 加载端点失败时缺少用户反馈与上述问题类似,fetch 端点失败时仅
console.error,建议添加 toast 通知。建议:添加错误处理
fetch(`/api/availability/endpoints?${params.toString()}`) .then((res) => res.json()) .then((data) => { if (data.endpoints) { setEndpoints(data.endpoints); setSelectedEndpointId((prev) => prev && !data.endpoints.some((e: ProviderEndpoint) => e.id.toString() === prev) ? "" : prev ); } }) - .catch(console.error) + .catch((error) => { + console.error(error); + toast.error(tErrors("fetchFailed")); + }) .finally(() => setLoadingEndpoints(false));tests/unit/lib/provider-endpoints/probe.test.ts (1)
35-42: 考虑提取重复的 mock 对象。
loggermock 对象在每个测试中重复定义。可以考虑在describe块顶部提取为共享 helper 函数,减少重复代码。♻️ 建议的重构
function createLoggerMock() { return { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), trace: vi.fn(), error: vi.fn(), fatal: vi.fn(), }; }然后在每个测试中使用
const logger = createLoggerMock();src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (2)
73-77: 建议为healthStatus和statistics使用更具体的类型。当前
Record<number, any>类型过于宽泛,失去了类型安全的优势。建议定义具体的接口类型。♻️ 建议的类型定义
interface ProviderHealthStatus { isHealthy: boolean; lastCheckedAt?: Date; circuitState?: "open" | "closed" | "half-open"; } interface ProviderStatistics { todayCostUsd?: string; todayCallCount?: number; lastCallTime?: Date | null; } interface ProviderVendorViewProps { // ... healthStatus: Record<number, ProviderHealthStatus>; statistics: Record<number, ProviderStatistics>; // ... }
503-509: 考虑使用自定义 Dialog 替代confirm()。使用原生
confirm()对话框与应用其他部分的 UI 风格不一致。建议使用已导入的Dialog组件创建确认对话框,以保持一致的用户体验。src/app/v1/_lib/proxy/forwarder.ts (2)
220-257: 端点候选列表构建逻辑清晰,但建议处理getPreferredProviderEndpoints返回空数组的场景当前实现逻辑合理:
- MCP 请求直接使用 provider URL(不经过端点池)
- 非 MCP 请求且有 vendorId 时从端点池获取候选
- 异常时回退到 provider URL
但
getPreferredProviderEndpoints返回空数组(所有端点都被熔断)与抛出异常是不同的场景。当前代码在空数组时会在 Line 250-252 补充 fallback,这是正确的处理。建议增加日志区分这两种情况,便于运维排查:
建议添加空数组场景的日志
try { const preferred = await getPreferredProviderEndpoints({ vendorId: currentProvider.providerVendorId, providerType: currentProvider.providerType, }); + if (preferred.length === 0) { + logger.debug( + "[ProxyForwarder] No available endpoints from pool, fallback to provider.url", + { + providerId: currentProvider.id, + vendorId: currentProvider.providerVendorId, + providerType: currentProvider.providerType, + } + ); + } endpointCandidates.push(...preferred.map((e) => ({ endpointId: e.id, baseUrl: e.url }))); } catch (error) {
2201-2217:toUndiciBody类型转换辅助函数可能丢失不支持的 body 类型当前实现对不支持的类型返回
undefined,这可能导致请求体静默丢失。虽然在当前上下文中 body 通常是 JSON 字符串,但建议添加日志或抛出错误以便调试:建议添加未知类型的警告日志
if (ArrayBuffer.isView(body)) { return new Uint8Array(body.buffer, body.byteOffset, body.byteLength); } + logger.warn("ProxyForwarder: Unsupported body type for undici request", { + bodyType: body?.constructor?.name ?? typeof body, + }); return undefined;src/lib/vendor-type-circuit-breaker.ts (1)
45-91:getOrCreateStateRedis 同步逻辑存在潜在的状态不一致风险当前逻辑:
- Line 51-53:
needsRedisCheck在首次访问或状态非 closed/manualOpen 时触发 Redis 同步- Line 65-70: 如果 Redis 无数据但本地有非 closed 状态,则重置本地状态
潜在问题:
- 竞态条件:多个并发请求可能同时触发 Redis 加载,导致状态覆盖
- Line 65-70 逻辑:当 Redis 无数据但本地有状态时,强制重置为 closed 可能丢失有效的熔断状态(例如 Redis 数据过期但熔断仍应生效)
建议考虑添加互斥锁或使用
loadedFromRedis更严格地控制同步:建议增加状态同步的原子性保护
+const syncLocks = new Map<string, Promise<VendorTypeCircuitBreakerState>>(); async function getOrCreateState( vendorId: number, providerType: ProviderType ): Promise<VendorTypeCircuitBreakerState> { const key = getKey(vendorId, providerType); + + // 如果已有同步进行中,等待其完成 + const existingSync = syncLocks.get(key); + if (existingSync) { + return existingSync; + } + let state = stateMap.get(key); // ... rest of the logicsrc/actions/provider-endpoints.ts (3)
36-43:ProviderTypeSchema顺序与源类型定义不一致当前 schema 中
"gemini-cli"在"gemini"之前,而src/types/provider.ts中的ProviderType顺序是"gemini"在前。虽然功能上没有影响,但建议保持一致以便维护:建议调整顺序与类型定义一致
const ProviderTypeSchema = z.enum([ "claude", "claude-auth", "codex", - "gemini-cli", "gemini", + "gemini-cli", "openai-compatible", ]);
327-348: 双重端点存在性检查可简化
findProviderEndpointById和probeProviderEndpointAndRecord内部都会检查端点是否存在。当前的双重检查虽然提供了更清晰的错误定位,但略显冗余。如果保留当前实现,建议在第二个检查(Line 342-348)返回更具体的错误信息,例如"端点在探测过程中被删除",以区分两种场景:
可选:区分错误场景
if (!result) { return { ok: false, - error: "端点不存在", + error: "端点探测失败:端点可能已被删除", errorCode: ERROR_CODES.NOT_FOUND, }; }
421-428: Schema 复用命名可能造成混淆
DeleteProviderEndpointSchema被复用于getEndpointCircuitInfo和resetEndpointCircuit,虽然功能正确(都只需要endpointId),但命名暗示"删除"操作可能误导读者。建议提取通用 schema 或添加注释说明复用原因:
建议:提取通用 EndpointIdInputSchema
+const EndpointIdInputSchema = z.object({ + endpointId: EndpointIdSchema, +}); + const DeleteProviderEndpointSchema = z.object({ endpointId: EndpointIdSchema, }); // ... later in getEndpointCircuitInfo: - const parsed = DeleteProviderEndpointSchema.safeParse(input); + const parsed = EndpointIdInputSchema.safeParse(input);src/repository/provider-endpoints.ts (3)
49-97: 建议使用 Drizzle 推断类型替代any当前使用
eslint-disable来绕过any类型检查。可以考虑使用 Drizzle 的$inferSelect类型来获得更好的类型安全性。♻️ 可选的类型改进示例
// 使用 Drizzle 推断类型 type ProviderVendorRow = typeof providerVendors.$inferSelect; function toProviderVendor(row: ProviderVendorRow): ProviderVendor { return { id: row.id, websiteDomain: row.websiteDomain, // ... }; }
330-355: 空 payload 处理可以复用已有函数当
payload为空时,直接调用findProviderEndpointById会更简洁,避免重复 select 投影代码。♻️ 简化空 payload 处理
export async function updateProviderEndpoint( endpointId: number, payload: { url?: string; label?: string | null; sortOrder?: number; isEnabled?: boolean } ): Promise<ProviderEndpoint | null> { if (Object.keys(payload).length === 0) { - const existing = await db - .select({ - id: providerEndpoints.id, - vendorId: providerEndpoints.vendorId, - // ... 省略重复的字段投影 - }) - .from(providerEndpoints) - .where(and(eq(providerEndpoints.id, endpointId), isNull(providerEndpoints.deletedAt))) - .limit(1); - - return existing[0] ? toProviderEndpoint(existing[0]) : null; + return findProviderEndpointById(endpointId); }
444-468: 建议为分页参数添加边界验证
limit默认值 200 且无上限检查,可能导致性能问题。建议添加最大值限制。♻️ 添加参数边界验证
export async function findProviderEndpointProbeLogs( endpointId: number, limit: number = 200, offset: number = 0 ): Promise<ProviderEndpointProbeLog[]> { + const safeLimit = Math.min(Math.max(1, limit), 500); + const safeOffset = Math.max(0, offset); + const rows = await db .select({ // ... }) .from(providerEndpointProbeLogs) .where(eq(providerEndpointProbeLogs.endpointId, endpointId)) .orderBy(desc(providerEndpointProbeLogs.createdAt)) - .limit(limit) - .offset(offset); + .limit(safeLimit) + .offset(safeOffset); return rows.map(toProviderEndpointProbeLog); }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to Reviews > Disable Cache setting
📒 Files selected for processing (43)
drizzle/0055_sour_wallow.sqldrizzle/meta/0055_snapshot.jsondrizzle/meta/_journal.jsonmessages/en/dashboard.jsonmessages/en/settings/providers/strings.jsonmessages/ja/dashboard.jsonmessages/ja/settings/providers/strings.jsonmessages/ru/dashboard.jsonmessages/ru/settings/providers/strings.jsonmessages/zh-CN/dashboard.jsonmessages/zh-CN/settings/providers/strings.jsonmessages/zh-TW/dashboard.jsonmessages/zh-TW/settings/providers/strings.jsonsrc/actions/provider-endpoints.tssrc/actions/providers.tssrc/app/[locale]/dashboard/availability/_components/availability-view.tsxsrc/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsxsrc/app/[locale]/settings/providers/_components/provider-manager.tsxsrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsxsrc/app/api/actions/[...route]/route.tssrc/app/api/availability/endpoints/probe-logs/route.tssrc/app/api/availability/endpoints/route.tssrc/app/v1/_lib/proxy/forwarder.tssrc/app/v1/_lib/proxy/provider-selector.tssrc/drizzle/schema.tssrc/lib/endpoint-circuit-breaker.tssrc/lib/provider-endpoints/endpoint-selector.tssrc/lib/provider-endpoints/probe.tssrc/lib/redis/endpoint-circuit-breaker-state.tssrc/lib/redis/vendor-type-circuit-breaker-state.tssrc/lib/vendor-type-circuit-breaker.tssrc/repository/_shared/transformers.tssrc/repository/index.tssrc/repository/provider-endpoints.tssrc/repository/provider.tssrc/types/provider.tstests/api/api-openapi-spec.test.tstests/unit/lib/endpoint-circuit-breaker.test.tstests/unit/lib/provider-endpoints/endpoint-selector.test.tstests/unit/lib/provider-endpoints/probe.test.tstests/unit/lib/vendor-type-circuit-breaker.test.tstests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsxtests/unit/user-dialogs.test.tsx
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{js,ts,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Never use emoji characters in any code, comments, or string literals
Files:
src/actions/providers.tssrc/app/api/availability/endpoints/route.tstests/unit/lib/vendor-type-circuit-breaker.test.tssrc/app/api/availability/endpoints/probe-logs/route.tssrc/lib/provider-endpoints/endpoint-selector.tssrc/lib/provider-endpoints/probe.tssrc/app/[locale]/settings/providers/_components/provider-manager.tsxtests/unit/user-dialogs.test.tsxtests/api/api-openapi-spec.test.tssrc/lib/endpoint-circuit-breaker.tssrc/lib/vendor-type-circuit-breaker.tstests/unit/lib/endpoint-circuit-breaker.test.tssrc/app/v1/_lib/proxy/forwarder.tssrc/types/provider.tssrc/app/[locale]/dashboard/availability/_components/availability-view.tsxtests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsxtests/unit/lib/provider-endpoints/probe.test.tssrc/lib/redis/vendor-type-circuit-breaker-state.tssrc/repository/index.tssrc/app/v1/_lib/proxy/provider-selector.tstests/unit/lib/provider-endpoints/endpoint-selector.test.tssrc/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsxsrc/actions/provider-endpoints.tssrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsxsrc/drizzle/schema.tssrc/app/api/actions/[...route]/route.tssrc/repository/provider-endpoints.tssrc/lib/redis/endpoint-circuit-breaker-state.tssrc/repository/provider.tssrc/repository/_shared/transformers.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias@/to reference files in./src/directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width
Files:
src/actions/providers.tssrc/app/api/availability/endpoints/route.tstests/unit/lib/vendor-type-circuit-breaker.test.tssrc/app/api/availability/endpoints/probe-logs/route.tssrc/lib/provider-endpoints/endpoint-selector.tssrc/lib/provider-endpoints/probe.tssrc/app/[locale]/settings/providers/_components/provider-manager.tsxtests/unit/user-dialogs.test.tsxtests/api/api-openapi-spec.test.tssrc/lib/endpoint-circuit-breaker.tssrc/lib/vendor-type-circuit-breaker.tstests/unit/lib/endpoint-circuit-breaker.test.tssrc/app/v1/_lib/proxy/forwarder.tssrc/types/provider.tssrc/app/[locale]/dashboard/availability/_components/availability-view.tsxtests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsxtests/unit/lib/provider-endpoints/probe.test.tssrc/lib/redis/vendor-type-circuit-breaker-state.tssrc/repository/index.tssrc/app/v1/_lib/proxy/provider-selector.tstests/unit/lib/provider-endpoints/endpoint-selector.test.tssrc/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsxsrc/actions/provider-endpoints.tssrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsxsrc/drizzle/schema.tssrc/app/api/actions/[...route]/route.tssrc/repository/provider-endpoints.tssrc/lib/redis/endpoint-circuit-breaker-state.tssrc/repository/provider.tssrc/repository/_shared/transformers.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer named exports over default exports
Files:
src/actions/providers.tssrc/app/api/availability/endpoints/route.tstests/unit/lib/vendor-type-circuit-breaker.test.tssrc/app/api/availability/endpoints/probe-logs/route.tssrc/lib/provider-endpoints/endpoint-selector.tssrc/lib/provider-endpoints/probe.tssrc/app/[locale]/settings/providers/_components/provider-manager.tsxtests/unit/user-dialogs.test.tsxtests/api/api-openapi-spec.test.tssrc/lib/endpoint-circuit-breaker.tssrc/lib/vendor-type-circuit-breaker.tstests/unit/lib/endpoint-circuit-breaker.test.tssrc/app/v1/_lib/proxy/forwarder.tssrc/types/provider.tssrc/app/[locale]/dashboard/availability/_components/availability-view.tsxtests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsxtests/unit/lib/provider-endpoints/probe.test.tssrc/lib/redis/vendor-type-circuit-breaker-state.tssrc/repository/index.tssrc/app/v1/_lib/proxy/provider-selector.tstests/unit/lib/provider-endpoints/endpoint-selector.test.tssrc/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsxsrc/actions/provider-endpoints.tssrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsxsrc/drizzle/schema.tssrc/app/api/actions/[...route]/route.tssrc/repository/provider-endpoints.tssrc/lib/redis/endpoint-circuit-breaker-state.tssrc/repository/provider.tssrc/repository/_shared/transformers.ts
src/app/api/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
API routes should use Hono framework and follow Next.js 16 App Router conventions
Files:
src/app/api/availability/endpoints/route.tssrc/app/api/availability/endpoints/probe-logs/route.tssrc/app/api/actions/[...route]/route.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
All new features must have unit test coverage of at least 80%
Files:
tests/unit/lib/vendor-type-circuit-breaker.test.tstests/unit/user-dialogs.test.tsxtests/api/api-openapi-spec.test.tstests/unit/lib/endpoint-circuit-breaker.test.tstests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsxtests/unit/lib/provider-endpoints/probe.test.tstests/unit/lib/provider-endpoints/endpoint-selector.test.ts
tests/**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Vitest for unit testing and happy-dom for DOM testing
Files:
tests/unit/lib/vendor-type-circuit-breaker.test.tstests/unit/user-dialogs.test.tsxtests/api/api-openapi-spec.test.tstests/unit/lib/endpoint-circuit-breaker.test.tstests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsxtests/unit/lib/provider-endpoints/probe.test.tstests/unit/lib/provider-endpoints/endpoint-selector.test.ts
src/app/v1/_lib/proxy/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
The proxy pipeline processes requests through a GuardPipeline with sequential guards: auth, sensitive, client, model, version, probe, session, warmup, requestFilter, rateLimit, provider, providerRequestFilter, messageContext
Files:
src/app/v1/_lib/proxy/forwarder.tssrc/app/v1/_lib/proxy/provider-selector.ts
src/repository/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Drizzle ORM for data access in the repository layer
Files:
src/repository/index.tssrc/repository/provider-endpoints.tssrc/repository/provider.tssrc/repository/_shared/transformers.ts
src/drizzle/schema.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Modify database schema in
src/drizzle/schema.ts, then runbun run db:generateto generate migrations. Never create SQL migration files manually
Files:
src/drizzle/schema.ts
🧠 Learnings (10)
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (e.g., matches DB schema or special UI logic) and undefined signifies 'inherit default'. This pattern should be consistently reflected in type definitions across related fields to preserve semantic clarity between database constraints and UI behavior.
Applied to files:
src/actions/providers.tssrc/app/api/availability/endpoints/route.tssrc/app/api/availability/endpoints/probe-logs/route.tssrc/lib/provider-endpoints/endpoint-selector.tssrc/lib/provider-endpoints/probe.tssrc/lib/endpoint-circuit-breaker.tssrc/lib/vendor-type-circuit-breaker.tssrc/app/v1/_lib/proxy/forwarder.tssrc/types/provider.tssrc/lib/redis/vendor-type-circuit-breaker-state.tssrc/repository/index.tssrc/app/v1/_lib/proxy/provider-selector.tssrc/actions/provider-endpoints.tssrc/drizzle/schema.tssrc/app/api/actions/[...route]/route.tssrc/repository/provider-endpoints.tssrc/lib/redis/endpoint-circuit-breaker-state.tssrc/repository/provider.tssrc/repository/_shared/transformers.ts
📚 Learning: 2026-01-10T06:19:58.167Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:19:58.167Z
Learning: Do not modify hardcoded Chinese error messages in Server Actions under src/actions/*.ts as part of piecemeal changes. This is a repo-wide architectural decision that requires a coordinated i18n refactor across all Server Action files (e.g., model-prices.ts, users.ts, system-config.ts). Treat i18n refactor as a separate unified task rather than per-PR changes, and plan a project-wide approach for replacing hardcoded strings with localized resources.
Applied to files:
src/actions/providers.tssrc/actions/provider-endpoints.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/app/api/**/*.{ts,tsx} : API routes should use Hono framework and follow Next.js 16 App Router conventions
Applied to files:
src/app/api/availability/endpoints/route.tssrc/app/api/actions/[...route]/route.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : All new features must have unit test coverage of at least 80%
Applied to files:
tests/unit/lib/vendor-type-circuit-breaker.test.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/app/v1/_lib/proxy/**/*.{ts,tsx} : The proxy pipeline processes requests through a GuardPipeline with sequential guards: auth, sensitive, client, model, version, probe, session, warmup, requestFilter, rateLimit, provider, providerRequestFilter, messageContext
Applied to files:
src/app/api/availability/endpoints/probe-logs/route.tssrc/app/v1/_lib/proxy/forwarder.tssrc/app/v1/_lib/proxy/provider-selector.ts
📚 Learning: 2026-01-05T03:02:14.502Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx:66-66
Timestamp: 2026-01-05T03:02:14.502Z
Learning: In the claude-code-hub project, the translations.actions.addKey field in UserKeyTableRowProps is defined as optional for backward compatibility, but all actual callers in the codebase provide the complete translations object. The field has been added to all 5 locale files (messages/{locale}/dashboard.json).
Applied to files:
tests/unit/user-dialogs.test.tsxmessages/zh-CN/dashboard.json
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Applied to files:
tests/unit/user-dialogs.test.tsxtests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
📚 Learning: 2026-01-10T06:20:04.478Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:20:04.478Z
Learning: In the `ding113/claude-code-hub` repository, Server Actions (files under `src/actions/*.ts`) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.
Applied to files:
tests/unit/user-dialogs.test.tsxtests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/drizzle/schema.ts : Modify database schema in `src/drizzle/schema.ts`, then run `bun run db:generate` to generate migrations. Never create SQL migration files manually
Applied to files:
drizzle/meta/0055_snapshot.jsonsrc/drizzle/schema.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/repository/**/*.{ts,tsx} : Use Drizzle ORM for data access in the repository layer
Applied to files:
src/drizzle/schema.tssrc/repository/provider-endpoints.ts
🧬 Code graph analysis (20)
src/app/api/availability/endpoints/route.ts (4)
src/types/provider.ts (1)
ProviderType(6-12)src/lib/auth.ts (1)
getSession(116-128)src/repository/index.ts (1)
findProviderEndpointsByVendorAndType(52-52)src/repository/provider-endpoints.ts (1)
findProviderEndpointsByVendorAndType(248-282)
tests/unit/lib/vendor-type-circuit-breaker.test.ts (2)
src/types/provider.ts (1)
ProviderType(6-12)src/lib/vendor-type-circuit-breaker.ts (5)
setVendorTypeCircuitManualOpen(157-177)getVendorTypeCircuitInfo(107-113)isVendorTypeCircuitOpen(115-137)recordVendorTypeAllEndpointsTimeout(139-155)resetVendorTypeCircuit(179-187)
src/app/api/availability/endpoints/probe-logs/route.ts (4)
src/app/api/actions/[...route]/route.ts (1)
GET(1809-1809)src/app/api/availability/endpoints/route.ts (1)
GET(22-45)src/lib/auth.ts (1)
getSession(116-128)src/repository/provider-endpoints.ts (2)
findProviderEndpointById(188-215)findProviderEndpointProbeLogs(444-468)
src/lib/provider-endpoints/endpoint-selector.ts (3)
src/types/provider.ts (2)
ProviderEndpoint(387-404)ProviderType(6-12)src/repository/provider-endpoints.ts (1)
findProviderEndpointsByVendorAndType(248-282)src/lib/endpoint-circuit-breaker.ts (1)
isEndpointCircuitOpen(116-135)
src/lib/provider-endpoints/probe.ts (4)
src/lib/logger.ts (1)
logger(168-187)src/types/provider.ts (1)
ProviderEndpointProbeSource(385-385)src/repository/index.ts (2)
findProviderEndpointById(50-50)recordProviderEndpointProbeResult(55-55)src/repository/provider-endpoints.ts (2)
findProviderEndpointById(188-215)recordProviderEndpointProbeResult(405-442)
src/app/[locale]/settings/providers/_components/provider-manager.tsx (5)
src/types/provider.ts (1)
ProviderType(6-12)src/app/[locale]/settings/providers/_components/provider-sort-dropdown.tsx (1)
SortKey(13-13)src/components/ui/button.tsx (1)
Button(58-58)src/app/[locale]/settings/providers/_components/provider-list.tsx (1)
ProviderList(28-67)src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (1)
ProviderVendorView(79-145)
src/lib/vendor-type-circuit-breaker.ts (3)
src/types/provider.ts (1)
ProviderType(6-12)src/lib/redis/vendor-type-circuit-breaker-state.ts (4)
VendorTypeCircuitBreakerState(9-14)loadVendorTypeCircuitState(47-74)saveVendorTypeCircuitState(76-99)deleteVendorTypeCircuitState(101-121)src/lib/logger.ts (1)
logger(168-187)
tests/unit/lib/endpoint-circuit-breaker.test.ts (1)
src/lib/endpoint-circuit-breaker.ts (5)
recordEndpointFailure(137-158)isEndpointCircuitOpen(116-135)recordEndpointSuccess(160-185)resetEndpointCircuit(187-196)getEndpointHealthInfo(109-114)
src/app/v1/_lib/proxy/forwarder.ts (7)
src/lib/provider-endpoints/endpoint-selector.ts (1)
getPreferredProviderEndpoints(30-54)src/lib/logger.ts (1)
logger(168-187)src/lib/vendor-type-circuit-breaker.ts (2)
isVendorTypeCircuitOpen(115-137)recordVendorTypeAllEndpointsTimeout(139-155)src/lib/endpoint-circuit-breaker.ts (2)
recordEndpointSuccess(160-185)recordEndpointFailure(137-158)src/app/v1/_lib/proxy/errors.ts (1)
ProxyError(14-450)src/app/v1/_lib/gemini/protocol.ts (1)
GEMINI_PROTOCOL(1-20)src/app/v1/_lib/url.ts (1)
buildProxyUrl(25-84)
src/app/[locale]/dashboard/availability/_components/availability-view.tsx (1)
src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx (1)
EndpointProbeHistory(44-301)
tests/unit/lib/provider-endpoints/probe.test.ts (2)
src/types/provider.ts (1)
ProviderEndpoint(387-404)src/lib/provider-endpoints/probe.ts (2)
probeEndpointUrl(93-102)probeProviderEndpointAndRecord(104-129)
src/lib/redis/vendor-type-circuit-breaker-state.ts (2)
src/types/provider.ts (1)
ProviderType(6-12)src/lib/logger.ts (1)
logger(168-187)
src/app/v1/_lib/proxy/provider-selector.ts (2)
src/lib/vendor-type-circuit-breaker.ts (1)
isVendorTypeCircuitOpen(115-137)src/lib/logger.ts (1)
logger(168-187)
tests/unit/lib/provider-endpoints/endpoint-selector.test.ts (2)
src/types/provider.ts (1)
ProviderEndpoint(387-404)src/lib/provider-endpoints/endpoint-selector.ts (3)
rankProviderEndpoints(7-28)getPreferredProviderEndpoints(30-54)pickBestProviderEndpoint(56-63)
src/actions/provider-endpoints.ts (7)
src/lib/auth.ts (1)
getSession(116-128)src/types/provider.ts (4)
ProviderVendor(375-383)ProviderType(6-12)ProviderEndpoint(387-404)ProviderEndpointProbeLog(406-416)src/repository/provider-endpoints.ts (8)
findProviderVendors(148-168)findProviderVendorById(170-186)findProviderEndpointsByVendorAndType(248-282)createProviderEndpoint(284-324)updateProviderEndpoint(326-388)softDeleteProviderEndpoint(390-403)findProviderEndpointById(188-215)findProviderEndpointProbeLogs(444-468)src/lib/logger.ts (1)
logger(168-187)src/lib/utils/zod-i18n.ts (2)
formatZodError(154-159)extractZodErrorCode(176-190)src/lib/provider-endpoints/probe.ts (1)
probeProviderEndpointAndRecord(104-129)src/lib/endpoint-circuit-breaker.ts (2)
getEndpointHealthInfo(109-114)resetEndpointCircuit(187-196)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (3)
src/types/provider.ts (4)
ProviderDisplay(147-217)ProviderVendor(375-383)ProviderType(6-12)ProviderEndpoint(387-404)src/actions/provider-endpoints.ts (8)
getProviderVendors(118-130)getVendorTypeCircuitInfo(477-517)setVendorTypeCircuitManualOpen(519-551)getProviderEndpoints(146-172)probeProviderEndpoint(295-356)removeProviderEndpoint(258-293)addProviderEndpoint(174-211)editProviderEndpoint(213-256)src/lib/vendor-type-circuit-breaker.ts (2)
getVendorTypeCircuitInfo(107-113)setVendorTypeCircuitManualOpen(157-177)
src/drizzle/schema.ts (2)
src/types/provider.ts (1)
ProviderType(6-12)src/lib/webhook/types.ts (1)
ProviderType(79-79)
src/app/api/actions/[...route]/route.ts (1)
src/lib/api/action-adapter-openapi.ts (1)
createActionRoute(253-419)
src/repository/provider-endpoints.ts (2)
src/types/provider.ts (5)
ProviderVendor(375-383)ProviderEndpoint(387-404)ProviderEndpointProbeLog(406-416)ProviderType(6-12)ProviderEndpointProbeSource(385-385)src/drizzle/schema.ts (3)
providerVendors(132-145)providerEndpoints(297-339)providerEndpointProbeLogs(342-363)
src/lib/redis/endpoint-circuit-breaker-state.ts (2)
src/lib/logger.ts (1)
logger(168-187)scripts/sync-settings-keys.js (1)
key(32-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
- GitHub Check: pr-review
- GitHub Check: Greptile Review
- GitHub Check: pr-description
- GitHub Check: Docker Build Test
- GitHub Check: dev-build-deploy
🔇 Additional comments (82)
drizzle/meta/_journal.json (1)
389-396: LGTM!新增的迁移日志条目
0055_sour_wallow格式正确,与现有条目保持一致。tests/api/api-openapi-spec.test.ts (1)
192-198: LGTM!上限从 60 调整为 80 是合理的,以适应本 PR 新增的 provider endpoints 相关 API 路由。注释已说明这是"合理范围"约束,而非精确断言。
tests/unit/user-dialogs.test.tsx (2)
195-203: LGTM!新增的
ui.tagInput翻译键为标签输入组件提供了必要的错误消息支持,符合 i18n 规范。
331-334: LGTM!新增的
providerGroupSelect翻译键支持供应商分组选择器组件的国际化显示。tests/unit/settings/providers/model-multi-select-custom-models-ui.test.tsx (1)
68-69: LGTM!使用本地定义的
loadMessages()函数直接从实际的翻译文件加载消息,确保测试使用真实的国际化内容。messages/zh-CN/dashboard.json (1)
1128-1149: LGTM!新增的
probeHistory本地化条目已在全部 5 种语言(en、ja、ru、zh-CN、zh-TW)的 dashboard.json 中完整添加,所有语言的键结构保持一致,符合 i18n 多语言支持的要求。翻译准确且结构清晰。messages/zh-TW/dashboard.json (1)
999-1020: LGTM!新增的
probeHistory翻译块结构完整,涵盖了端点探测历史功能所需的所有 UI 字符串。繁体中文翻译准确,与其他语言版本(en、zh-CN、ja、ru)保持一致。messages/en/settings/providers/strings.json (1)
46-83: LGTM!新增的本地化键完整覆盖了端点管理、探测状态、熔断器控制等功能所需的 UI 字符串。键命名遵循现有约定,JSON 语法正确。
messages/zh-CN/settings/providers/strings.json (1)
46-83: LGTM!简体中文翻译完整且准确,与英文版本的键集保持一致。翻译用语符合中国大陆用户习惯(如"测速"、"熔断"、"端点"等)。
src/types/provider.ts (3)
375-416: LGTM!新增的类型定义结构清晰:
ProviderVendor正确定义了供应商聚合实体的字段,可空字段使用| null明确语义。ProviderEndpointProbeSource作为联合类型清晰定义了探测来源。ProviderEndpoint完整涵盖端点元数据,软删除字段deletedAt使用Date | null。ProviderEndpointProbeLog结构合理,与数据库 schema 对应。
44-45: LGTM!在
Provider接口中添加providerVendorId: number字段,建立了与ProviderVendor实体的关联。
160-161: LGTM!
ProviderDisplay接口同步添加providerVendorId字段,确保前端显示类型与核心类型一致。src/lib/provider-endpoints/endpoint-selector.ts (2)
7-28: LGTM!
rankProviderEndpoints实现清晰:
- 使用
slice()避免修改原数组。- 优先级排序逻辑合理:探测成功 > 未探测 > 探测失败。
- 延迟为
null时使用POSITIVE_INFINITY处理,确保排序稳定性。- 最后按
id排序保证确定性结果。
56-63: LGTM!
pickBestProviderEndpoint作为便捷包装函数,实现简洁,使用 nullish coalescing (??) 正确处理空数组情况。src/lib/redis/endpoint-circuit-breaker-state.ts (3)
1-28: 类型定义和常量设计合理。接口和默认状态定义清晰,TTL 为 24 小时对于断路器状态是合理的设置。
50-76: 加载函数的错误处理和日志记录实现良好。函数正确处理了 Redis 不可用、数据为空和异常情况,返回
null的策略允许调用方优雅降级。
102-118: 删除函数实现正确。静默处理 Redis 不可用的情况是合理的,错误日志包含足够的上下文信息便于排查问题。
src/lib/endpoint-circuit-breaker.ts (6)
1-23: 配置和接口定义清晰。断路器配置参数(3次失败阈值、5分钟开启时长、1次成功即恢复)对于端点健康检查场景是合理的默认值。
50-90:getOrCreateHealth中 Redis 同步逻辑存在边界情况。第 74-80 行:当本地状态为非 "closed" 但 Redis 返回 null 时,代码会将状态重置为 "closed"。这个行为可能导致问题:
- 如果 Redis TTL 过期但本地状态仍有效(例如刚开启断路器),会意外关闭断路器
- 在多实例部署中,某个实例可能在另一个实例设置状态前读取到空值
建议验证此行为是否符合预期。如果 Redis 是权威数据源,当前逻辑是正确的;如果本地状态应该优先,则需要调整。
92-107:persistStateToRedis使用 fire-and-forget 模式。使用
.catch()处理 Promise 拒绝是合理的,但调用方无法得知持久化是否成功。在网络问题导致 Redis 写入失败的情况下,状态可能只存在于本地内存中。当前实现对于断路器场景可接受,因为状态最终会通过后续操作重新同步。
116-135:isEndpointCircuitOpen状态转换逻辑正确。从 "open" 到 "half-open" 的转换在正确的时机触发,并正确持久化到 Redis。
137-158:recordEndpointFailure实现正确。失败计数、阈值检查和状态转换逻辑都是正确的。日志包含足够的诊断信息。
160-185:recordEndpointSuccess实现正确。正确处理了 "half-open" 状态下的成功计数,以及 "closed" 状态下清零失败计数的逻辑。
tests/unit/lib/endpoint-circuit-breaker.test.ts (2)
26-91: 测试覆盖了完整的断路器生命周期。测试验证了:
- 3次失败后断路器打开
- 5分钟后转为 half-open
- 1次成功后关闭并清零
resetEndpointCircuit调用删除函数测试逻辑清晰,使用 fake timers 正确模拟时间推进。
93-123:recordEndpointSuccess测试验证了 failureCount > 0 时的清零行为。测试覆盖了一个重要边界情况:在 "closed" 状态下有失败记录时,成功调用应清零失败计数。
src/lib/redis/vendor-type-circuit-breaker-state.ts (3)
1-27: 模块结构与端点断路器状态模块保持一致。类型定义清晰,30 天的 TTL 对于 vendor 级别的状态是合理的。
manualOpen字段支持管理员手动干预。
47-74: 加载函数实现正确。错误处理和日志记录包含了
vendorId和providerType上下文信息,便于问题排查。
101-121: 删除函数实现正确。与端点模块保持一致的实现模式。
messages/ja/settings/providers/strings.json (1)
46-83: 日语本地化内容完整且专业。新增的 38 个本地化键覆盖了端点管理 UI 的所有文案,包括:
- 视图模式切换(列表/供应商)
- 端点 CRUD 操作
- 探针测试和状态显示
- 断路器状态管理
翻译质量良好,符合日语 UI 惯例。
messages/ru/settings/providers/strings.json (1)
46-83: LGTM!俄语本地化字符串已正确添加,涵盖了新的端点管理、探针检查和电路断路器功能所需的所有 UI 文本。翻译与现有命名模式一致。
src/app/[locale]/dashboard/availability/_components/availability-view.tsx (2)
24-24: LGTM!从同目录导入
EndpointProbeHistory组件,遵循了项目的导入约定。
528-530: LGTM!
EndpointProbeHistory组件在 Legend 卡片之后逻辑性地放置,为可用性仪表板增加了端点探针历史功能。组件集成简洁,符合现有的 UI 布局模式。messages/ja/dashboard.json (1)
999-1020: LGTM!日语本地化字符串已正确添加,包含了端点探针历史功能所需的所有 UI 文本(标题、描述、选择器标签、列标题、状态消息等)。翻译质量良好,与组件需求匹配。
src/actions/providers.ts (1)
239-239: LGTM!
providerVendorId字段已正确添加到getProviders的返回数据中,与ProviderDisplay类型扩展保持一致。这使前端能够获取供应商的 vendor 关联信息,支持新的 vendor 视图功能。src/repository/_shared/transformers.ts (1)
69-69:providerVendorId的默认值 0 是正确的,无需修改根据数据库 schema 定义,
providerVendorId被标记为.notNull(),不允许 null 值。0 作为默认值是为了表示"未分配供应商"的哨兵值(sentinel value),这与 providerVendors 表的 serial 主键从 1 开始相匹配。整个代码库通过providerVendorId > 0的检查来判断是否已分配有效的供应商。此模式与其他字段默认值(如weight: 1)的处理方式一致。Likely an incorrect or invalid review comment.
src/app/api/availability/endpoints/probe-logs/route.ts (1)
1-46: LGTM! 路由实现清晰规范。此 API 路由实现与现有的
src/app/api/availability/endpoints/route.ts模式保持一致:
- 管理员身份验证
- 参数验证(endpointId、limit、offset)
- 适当的错误处理和状态码
唯一的小建议:根据编码指南,API 路由应使用 Hono 框架。但观察到现有的 availability endpoints 路由也使用原生 Next.js 处理方式,为保持一致性,当前实现是可接受的。
drizzle/meta/0055_snapshot.json (1)
1-2874: 自动生成的数据库快照文件,结构合理。此文件由
bun run db:generate自动生成。新增的表结构设计合理:
provider_vendors: 唯一索引website_domainprovider_endpoints: 复合唯一索引(vendor_id, provider_type, url),适当的部分索引过滤deleted_at IS NULLprovider_endpoint_probe_logs: 外键级联删除,索引支持按endpoint_id和created_at查询基于 learnings,此类文件不应手动创建或修改。
tests/unit/lib/provider-endpoints/endpoint-selector.test.ts (1)
1-156: 测试覆盖全面,实现规范。测试用例设计合理:
rankProviderEndpoints: 验证禁用/删除过滤和多级排序逻辑getPreferredProviderEndpoints: 验证排除逻辑(禁用、删除、显式排除、熔断)pickBestProviderEndpoint: 验证空候选时返回 nullmock 设置正确使用
vi.resetModules()+vi.doMock()+ 动态import(),确保模块隔离。src/app/[locale]/settings/providers/_components/provider-manager.tsx (2)
208-230: 视图切换 UI 实现规范。切换按钮组实现良好:
- 使用 i18n 获取标签文本(
tStrings("viewModeList")、tStrings("viewModeVendor"))- 包含
title属性提供无障碍支持- 按钮状态通过
variant正确反映当前选中视图
348-368: 条件渲染逻辑正确。
ProviderVendorView与ProviderList接收相同的 props,确保两种视图模式下功能一致。messages/en/dashboard.json (1)
1023-1044: probeHistory 本地化字符串已完整覆盖所有支持语言。验证确认英文、简体中文、繁体中文、日语和俄语的 messages/*/dashboard.json 文件中均包含完整的 probeHistory 翻译。所有18个键值(包括 columns 子部分的5个键)在所有5种语言中都正确翻译,命名规范与现有文本一致。
tests/unit/lib/vendor-type-circuit-breaker.test.ts (3)
27-76: 测试用例覆盖良好测试用例正确验证了:
- 手动开启时
isVendorTypeCircuitOpen始终返回truerecordVendorTypeAllEndpointsTimeout在手动开启状态下不会覆盖状态circuitOpenUntil在手动开启时为null测试逻辑清晰,断言完整。
78-130: 自动开启和最小超时测试逻辑正确测试正确验证了:
- 传入
openDurationMs: 0时,最小值 1000ms 被强制应用- 1000ms 后熔断器自动关闭
- 状态正确从
open转换为closed
132-160: 重置测试验证了缓存清理和 Redis 删除测试正确验证了
resetVendorTypeCircuit的行为:
- 调用
deleteMock清理 Redis 状态- 后续调用会重新从 Redis 加载(
loadMock被调用两次)src/app/api/actions/[...route]/route.ts (1)
387-604: 新增的供应商端点管理路由实现良好路由定义遵循了文件中现有的模式:
- 所有管理员路由都正确设置了
requiredRole: "admin"- 请求 schema 验证合理(如
timeoutMs限制在 1000-120000ms)- 路由命名清晰,职责明确
src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx (1)
149-300: 组件 UI 实现良好
- 使用了正确的 i18n 翻译函数
t()- 加载状态和空状态处理完善
- 表格渲染逻辑清晰
- Badge 组件使用了适当的颜色区分成功/失败状态
tests/unit/lib/provider-endpoints/probe.test.ts (1)
31-65: LGTM! 测试覆盖全面。测试用例覆盖了
probeEndpointUrl的主要场景:HEAD 成功、HEAD 失败回退 GET、5xx/4xx 状态码处理、超时处理等。模拟设置和断言逻辑正确。src/app/v1/_lib/proxy/provider-selector.ts (2)
532-544: LGTM! 正确实现了供应商类型熔断检查。在会话复用路径中添加 vendor-type 熔断检查是正确的,这可以防止通过会话复用绕过故障隔离机制。检查逻辑
provider.providerVendorId > 0确保只对有效的 vendorId 进行检查。
967-978: 熔断检查顺序合理。将 vendor-type 熔断检查放在 provider 级别熔断检查之前是正确的设计。vendor-type 熔断是更宽范围的保护(同一供应商+类型的所有端点),应该优先于单个 provider 的熔断检查。
drizzle/0055_sour_wallow.sql (2)
113-138: 索引策略合理。
- 使用部分索引 (
WHERE deleted_at IS NULL) 优化活跃记录查询uniq_provider_endpoints_vendor_type_url唯一索引防止重复端点ON DELETE restrict用于 providers FK 防止意外删除关联的 vendorON DELETE cascade用于 endpoints 和 probe_logs 保持数据一致性
44-66: 域名提取逻辑的技术描述有误,且无证据表明存在此问题。对于包含认证信息的 URL(如
https://user:pass@api.example.com/v1),当前逻辑的实际提取过程是:
- 去掉协议后:
user:pass@api.example.com/v1- 按
/分割取第一部分:user:pass@api.example.com- 按
:分割取第一部分:user结果会是
user而非user:pass@api.example.com,这样会导致完全错误的域名提取。但经过代码搜索,发现:
- 现有代码库中不存在包含认证信息的提供商 URL
website_url字段使用 Zod 的.url()验证,会限制输入格式- 此问题为假设性边界案例,不影响现有数据
若要增强鲁棒性,可在提取前处理
@符号,但基于当前数据情况,此修改不是必需的。src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (1)
535-564: 表单处理模式正确。表单提交逻辑正确处理了加载状态、错误处理和缓存失效。使用
try/catch确保错误不会导致 UI 崩溃。src/repository/provider.ts (2)
14-25: LGTM! 正确计算并存储providerVendorId。在创建 provider 时预先计算
providerVendorId并将其包含在插入数据中是正确的做法。getOrCreateProviderVendorIdFromUrls会确保 vendor 记录存在。
450-471: URL 变更时正确重新计算providerVendorId。当
url或website_url更新时重新计算providerVendorId的逻辑是正确的。代码正确处理了:
- 只在相关 URL 变更时触发重新计算
- 使用当前值作为回退,确保完整的 URL 信息传递给 vendor 查找函数
- 如果 provider 不存在(
current为 null),跳过 vendorId 更新,后续的 update 操作会返回空结果src/repository/index.ts (1)
47-59: LGTM!新增的 provider-endpoints 模块导出遵循了现有的代码组织模式,保持了 repository 层统一入口的一致性。导出的函数覆盖了端点管理所需的完整 CRUD 操作、探针记录和供应商查询功能。
src/lib/provider-endpoints/probe.ts (4)
20-39: LGTM!
fetchWithTimeout实现正确使用了AbortController进行超时控制,latency 测量准确。finally块确保了定时器在所有情况下都能被清理,避免内存泄漏。
51-91: LGTM!
tryProbe函数逻辑正确:
statusCode < 500将 4xx 视为"可达"是合理的探针策略,因为 4xx 表示端点响应正常(只是权限/请求问题)- 错误分类为
http_5xx、timeout、network_error等类型便于后续分析- debug 级别日志避免了生产环境日志过多
93-102: HEAD 优先策略的回退逻辑建议验证当前逻辑:HEAD 请求失败且
statusCode === null时回退到 GET。这意味着只有网络错误/超时才会触发回退。考虑场景:某些服务器对 HEAD 返回 405 Method Not Allowed,此时
statusCode不为 null,不会触发 GET 回退。建议确认这是否符合预期行为,或者是否需要扩展回退条件:export async function probeEndpointUrl( url: string, timeoutMs: number = DEFAULT_TIMEOUT_MS ): Promise<EndpointProbeResult> { const head = await tryProbe(url, "HEAD", timeoutMs); - if (head.statusCode === null) { + if (head.statusCode === null || head.statusCode === 405) { return tryProbe(url, "GET", timeoutMs); } return head; }
104-129: LGTM!
probeProviderEndpointAndRecord函数实现清晰:
- 先查询端点是否存在,避免无效的网络请求
- 探针结果完整记录到数据库,包含所有必要字段
- 返回
null处理端点不存在的边界情况src/app/v1/_lib/proxy/forwarder.ts (5)
266-280: LGTM!Vendor-type 熔断检查逻辑正确:
- 在重试循环前检查熔断状态,避免无效请求
- 熔断时通过设置
attemptCount = maxAttemptsPerProvider跳过内层循环- 正确将 providerId 加入失败列表以便后续供应商切换
286-295: 端点轮询选择逻辑正确Round-robin 策略通过
(attemptCount - 1) % endpointCandidates.length实现端点轮换。Line 250-252 确保了endpointCandidates至少有一个元素,避免了除零错误。
461-473: 端点失败记录逻辑正确,但allEndpointAttemptsTimedOut追踪存在边界情况当前逻辑:
- 只在
attemptCount <= endpointCandidates.length时更新endpointAttemptsEvaluated- 非超时错误会将
allEndpointAttemptsTimedOut设为 false潜在问题:如果
maxAttemptsPerProvider被整流器等逻辑动态增加(Line 638),额外的重试不会更新endpointAttemptsEvaluated,可能导致allEndpointAttemptsTimedOut判断不准确。建议确认这是预期行为,或者在 Line 894-907 的判断中考虑此边界情况。
894-907: LGTM!Vendor-type 全端点超时记录逻辑正确:
- 仅在所有端点均超时时触发熔断
- 条件检查包含
!isMcpRequest确保 MCP 请求不受影响- 熔断后立即 break 进入供应商切换逻辑
1029-1033: LGTM!
doForward签名变更正确引入了baseUrl参数,使得每次请求可以指定不同的端点 URL,为多端点路由提供了基础。src/lib/vendor-type-circuit-breaker.ts (4)
115-137: LGTM!
isVendorTypeCircuitOpen逻辑正确:
- 手动熔断优先级最高
- 时间窗口过期后自动关闭熔断并持久化
- 状态字段清理完整(circuitState、circuitOpenUntil、lastFailureTime)
139-155: LGTM!
recordVendorTypeAllEndpointsTimeout实现正确:
Math.max(1000, openDurationMs)确保最小熔断窗口为 1 秒,防止配置错误- 手动熔断状态不被自动熔断覆盖
- 同时更新
lastFailureTime便于调试和监控
157-177: LGTM!
setVendorTypeCircuitManualOpen状态管理清晰:
- 手动开启时
circuitOpenUntil = null确保不会自动关闭- 手动关闭时完全重置所有状态字段
- 两种操作都触发 Redis 持久化
179-187: LGTM!
resetVendorTypeCircuit清理逻辑完整:同时清除内存缓存、Redis 状态和loadedFromRedis标记,确保下次访问时能正确初始化。src/actions/provider-endpoints.ts (3)
110-116: LGTM!
getAdminSession辅助函数简洁有效,复用了现有的getSession并添加了管理员角色检查。
174-211: LGTM!
addProviderEndpoint实现规范:
- 完整的权限检查和输入验证
- 使用
formatZodError和extractZodErrorCode提供结构化错误信息- 错误处理覆盖了 Zod 验证失败和数据库操作异常
其他 CRUD 操作(edit、remove)遵循相同模式,代码结构一致。
477-584: LGTM!Vendor-type 熔断相关的三个 action(getVendorTypeCircuitInfo、setVendorTypeCircuitManualOpen、resetVendorTypeCircuit)实现一致,正确委托给底层状态管理函数,错误处理完善。
src/repository/provider-endpoints.ts (4)
15-24: 日期处理辅助函数设计合理
toDate和toNullableDate函数正确处理了各种输入类型,为数据库查询结果提供了一致的日期转换。
26-47: URL 域名规范化逻辑正确函数正确处理了多种输入格式,包括带/不带协议前缀的 URL,并规范化了
www.前缀的移除逻辑。
99-146: Upsert 模式实现正确,处理了并发竞态条件使用
onConflictDoNothing+ 回退查询的模式正确处理了并发插入场景。当多个请求同时尝试创建相同 vendor 时,能够安全地返回已存在的记录。
405-442: 探针记录事务处理正确使用事务确保日志插入和端点状态更新的原子性。当探针成功时清除错误字段的逻辑也是正确的。
messages/zh-TW/settings/providers/strings.json (1)
46-83: 新增的本地化键值完整且翻译质量良好新增的端点管理、探针检测和熔断状态相关的翻译键覆盖了 UI 所需的所有场景,翻译自然流畅。
src/drizzle/schema.ts (6)
131-145: providerVendors 表设计合理表结构简洁,使用
websiteDomain唯一索引确保供应商域名唯一性。注意该表没有deletedAt列,删除是物理删除,这与providers表的onDelete: 'restrict'约束配合使用是合理的设计。
296-339: providerEndpoints 表设计完善
- 复合唯一索引
(vendorId, providerType, url)防止重复端点- 使用部分索引
WHERE deletedAt IS NULL优化活跃端点查询onDelete: 'cascade'确保 vendor 删除时级联清理端点
341-363: providerEndpointProbeLogs 表设计正确索引
(endpointId, createdAt DESC)与 repository 层的查询模式匹配,支持高效的日志分页查询。建议: 探针日志可能会快速增长,考虑添加数据保留策略或定期清理机制(可作为后续优化)。
720-746: 关系定义完整且正确新增的关系定义正确建立了 vendors、endpoints 和 probe logs 之间的双向关联,支持 Drizzle 的关系查询功能。
154-156: 迁移脚本正确处理了 NOT NULL 约束转换
onDelete: 'restrict'设置正确。迁移脚本通过分阶段处理妥善解决了 NOT NULL 约束问题:
- 先添加列为可空状态
- 创建 provider_vendors 记录(包括无域名情况下的
unknown-{provider_id}后备方案)- 批量更新现有 providers 的 provider_vendor_id
- 最后才将列设置为 NOT NULL
现有数据在添加约束前已完全填充,迁移不会失败。
18-18: 导入正确,但注意命名冲突第 18 行的导入来自
@/types/provider,正确对应本文件中providerType字段使用的 API 供应商类型(claude、codex、gemini 等)。需注意:代码库中存在同名的
ProviderType类型定义在src/lib/webhook/types.ts,用于 Webhook 通知类型(wechat、feishu、dingtalk、telegram、custom),与本文件的ProviderType完全不同。在涉及 Webhook 相关代码时应确保引入正确的类型,Webhook 代码也导出了更明确的WebhookProviderType以避免混淆。
| CREATE TABLE IF NOT EXISTS "provider_endpoint_probe_logs" ( | ||
| "id" serial PRIMARY KEY NOT NULL, | ||
| "endpoint_id" integer NOT NULL, | ||
| "source" varchar(20) DEFAULT 'scheduled' NOT NULL, | ||
| "ok" boolean NOT NULL, | ||
| "status_code" integer, | ||
| "latency_ms" integer, | ||
| "error_type" varchar(64), | ||
| "error_message" text, | ||
| "created_at" timestamp with time zone DEFAULT now() | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
created_at 列缺少 NOT NULL 约束。
provider_endpoint_probe_logs.created_at 列默认值为 now() 但未设置 NOT NULL。如果插入时显式传入 NULL,会导致数据不完整。建议添加 NOT NULL 约束以保持数据一致性。
🔧 建议的修复
- "created_at" timestamp with time zone DEFAULT now()
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| CREATE TABLE IF NOT EXISTS "provider_endpoint_probe_logs" ( | |
| "id" serial PRIMARY KEY NOT NULL, | |
| "endpoint_id" integer NOT NULL, | |
| "source" varchar(20) DEFAULT 'scheduled' NOT NULL, | |
| "ok" boolean NOT NULL, | |
| "status_code" integer, | |
| "latency_ms" integer, | |
| "error_type" varchar(64), | |
| "error_message" text, | |
| "created_at" timestamp with time zone DEFAULT now() | |
| ); | |
| CREATE TABLE IF NOT EXISTS "provider_endpoint_probe_logs" ( | |
| "id" serial PRIMARY KEY NOT NULL, | |
| "endpoint_id" integer NOT NULL, | |
| "source" varchar(20) DEFAULT 'scheduled' NOT NULL, | |
| "ok" boolean NOT NULL, | |
| "status_code" integer, | |
| "latency_ms" integer, | |
| "error_type" varchar(64), | |
| "error_message" text, | |
| "created_at" timestamp with time zone DEFAULT now() NOT NULL | |
| ); |
🤖 Prompt for AI Agents
In @drizzle/0055_sour_wallow.sql around lines 1 - 11, The created_at column in
provider_endpoint_probe_logs has DEFAULT now() but lacks NOT NULL; update the
table definition (or migration) to add NOT NULL on the "created_at" column while
preserving DEFAULT now() (i.e., change the column definition to "created_at
timestamp with time zone DEFAULT now() NOT NULL"); if applying to an existing
DB, include a pre-migration step to backfill NULL created_at rows (UPDATE
provider_endpoint_probe_logs SET created_at = now() WHERE created_at IS NULL)
before altering the column to set NOT NULL.
| "probeHistory": { | ||
| "title": "История проверок эндпоинтов", | ||
| "description": "Просмотр журналов проверок и запуск ручной проверки для конкретного эндпоинта", | ||
| "selectVendor": "Выберите вендора", | ||
| "selectType": "Выберите тип провайдера", | ||
| "selectEndpoint": "Выберите эндпоинт", | ||
| "noEndpoints": "Эндпоинты не найдены", | ||
| "probeNow": "Проверить", | ||
| "probing": "Проверяем...", | ||
| "columns": { | ||
| "time": "Время", | ||
| "method": "Метод", | ||
| "status": "Статус", | ||
| "latency": "Задержка", | ||
| "error": "Ошибка" | ||
| }, | ||
| "success": "Успех", | ||
| "manual": "Ручная проверка", | ||
| "auto": "Автоматическая проверка", | ||
| "probeSuccess": "Проверка успешна", | ||
| "probeFailed": "Проверка не удалась" | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# 查找 probeHistory.columns.method 或相关使用
rg -n "probeHistory.*columns.*method|columns.*method|method.*column" --type=ts --type=tsx -iRepository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
# 查找 endpoint-probe-history 组件文件
fd -e tsx -e ts | grep -i "probe.*history\|endpoint.*probe"Repository: ding113/claude-code-hub
Length of output: 284
🏁 Script executed:
# 搜索 probeHistory 本身的使用情况
rg -n "probeHistory" --type=ts --type=tsx -A 3 -B 1Repository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
# Simple search for probeHistory usage
rg "probeHistory" -A 5 -B 2Repository: ding113/claude-code-hub
Length of output: 14090
🏁 Script executed:
# Read the component file to see what columns are actually used
cat -n src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx | head -200Repository: ding113/claude-code-hub
Length of output: 7794
🏁 Script executed:
# Search for "method" in relation to columns in the component
rg "columns\." src/app/[locale]/dashboard/availability/_components/endpoint-probe-history.tsx -A 2 -B 2Repository: ding113/claude-code-hub
Length of output: 464
🏁 Script executed:
# Check if columns.method is referenced anywhere in the codebase
rg "columns\.method"Repository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
# Check if there's any mention of "method" in relation to probe history in other files
rg "probeHistory.*method|method.*probe" -iRepository: ding113/claude-code-hub
Length of output: 200
🏁 Script executed:
# Read the probe.ts file to understand the context
cat -n src/lib/provider-endpoints/probe.ts | head -50Repository: ding113/claude-code-hub
Length of output: 1799
🏁 Script executed:
# Check the ProviderEndpointProbeLog type to see if method is a field
rg "ProviderEndpointProbeLog|EndpointProbeMethod" -A 5 -B 2Repository: ding113/claude-code-hub
Length of output: 11551
删除未使用的本地化键 columns.method
columns.method 在所有语言文件中定义,但组件实际只渲染 4 个表列(time、status、latency、error),未使用 method 列。虽然探针结果内部包含 method 字段(HEAD/GET),但该信息不在 UI 中显示,可以从本地化文件中删除。
🤖 Prompt for AI Agents
In @messages/ru/dashboard.json around lines 1001 - 1022, Remove the unused
localization key columns.method under probeHistory (i.e.,
"probeHistory.columns.method") since the UI only renders time, status, latency
and error; update the Russian file by deleting that "method" entry and mirror
the same removal across other locale files so all language bundles stay
consistent.
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
Outdated
Show resolved
Hide resolved
| context.filteredProviders?.push({ | ||
| id: p.id, | ||
| name: p.name, | ||
| reason: "circuit_open", | ||
| details: "供应商类型临时熔断", | ||
| }); | ||
| continue; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
硬编码的中文字符串应使用 i18n。
details: "供应商类型临时熔断" 是硬编码的中文字符串。根据编码规范,用户可见的字符串需要使用 i18n。当 verboseProviderError 启用时,这些详情会通过错误响应暴露给用户(参见 lines 485-488)。
建议将此类字符串迁移到 i18n 资源文件中,或者在返回给用户之前进行翻译。
🤖 Prompt for AI Agents
In @src/app/v1/_lib/proxy/provider-selector.ts around lines 879 - 885, The
hard-coded Chinese string assigned to details in the context.filteredProviders
push (details: "供应商类型临时熔断") should be replaced with an i18n lookup so
user-visible messages honor localization and are safe when verboseProviderError
exposes them; update the push call in provider-selector (where
context.filteredProviders?.push({ id: p.id, name: p.name, reason:
"circuit_open", details: ... })) to call the i18n translation function or a
resource key (e.g., t('provider.circuit_open')) and ensure the same translation
is applied before sending errors to users (see verboseProviderError handling
around verboseProviderError / lines ~485-488) so no raw Chinese strings are
returned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review Summary
This PR adds a comprehensive provider vendor endpoint system with probing, circuit breakers, and failover capabilities. The implementation is well-structured with proper separation of concerns across repository, service, and API layers. Error handling is consistent with logging throughout, and test coverage is adequate for the new functionality.
PR Size: XL
- Lines changed: 7707 (7672 additions, 35 deletions)
- Files changed: 43
Split Suggestions for Future PRs: Given the XL size, consider splitting similar features into:
- Data model + repository layer (migration, schema, repository functions)
- Circuit breaker implementations (endpoint + vendor-type)
- Proxy integration (forwarder changes, endpoint selector)
- Admin API + UI components
Issues Found
| Category | Critical | High | Medium | Low |
|---|---|---|---|---|
| Logic/Bugs | 0 | 0 | 0 | 0 |
| Security | 0 | 0 | 0 | 0 |
| Error Handling | 0 | 0 | 0 | 0 |
| Types | 0 | 0 | 0 | 0 |
| Comments/Docs | 0 | 0 | 0 | 0 |
| Tests | 0 | 0 | 0 | 0 |
| Simplification | 0 | 0 | 0 | 0 |
Review Coverage
- Logic and correctness - Clean
- Security (OWASP Top 10) - Clean (admin-only endpoints properly protected)
- Error handling - Clean (consistent logging, no silent failures)
- Type safety - Clean (proper TypeScript types throughout)
- Documentation accuracy - Clean
- Test coverage - Adequate (unit tests for circuit breakers, endpoint selector, probe logic)
- Code clarity - Good
Notable Observations (Not Issues)
Well-implemented patterns observed:
- Circuit breaker state properly synced between in-memory cache and Redis
- Graceful degradation when Redis is unavailable (returns null, logs warning)
- Proper use of
persistStateToRediswith fire-and-forget pattern and error logging - Admin session checks consistently applied across all endpoint actions
- Endpoint failover logic correctly tracks timeout vs non-timeout errors for vendor-type circuit breaker
Architecture notes:
- The dual-layer circuit breaker design (endpoint-level + vendor-type-level) provides good granularity
- Probe fallback from HEAD to GET on network error is a sensible approach
- The
allEndpointAttemptsTimedOuttracking ensures vendor-type circuit only opens when all endpoints timeout
Automated review by Claude AI
Records the selected endpoint in provider chain and seeds/backfills provider_endpoints so endpoint routing works without manual setup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@tests/unit/repository/provider-endpoints.test.ts`:
- Around line 19-253: Tests only cover two functions and ten repository
functions (getOrCreateProviderVendorIdFromUrls, findProviderVendors,
findProviderVendorById, findProviderEndpointById, updateProviderVendor,
findProviderEndpointsByVendorAndType, createProviderEndpoint,
updateProviderEndpoint, softDeleteProviderEndpoint,
findProviderEndpointProbeLogs) plus direct call coverage for
recordProviderEndpointProbeResult are missing; add unit tests for each missing
function to reach 80% coverage. For each function listed, write focused tests
that import the target (e.g., getOrCreateProviderVendorIdFromUrls,
findProviderVendors, etc.) from "@/repository/provider-endpoints", mock
"@/drizzle/db" with vi.doMock to provide controlled
select/insert/update/delete/returning behaviors, assert expected return values
and DB interaction (calls/params), include edge cases (invalid inputs,
conflicts, empty results), and add a direct unit test for
recordProviderEndpointProbeResult in this file that mocks any external probe
logger and verifies it records outcomes; ensure tests use vi.resetModules before
vi.doMock and validate coverage meets the 80% threshold.
🧹 Nitpick comments (8)
src/instrumentation.ts (1)
183-194: 建议提取重复的回填逻辑为辅助函数此回填逻辑与开发环境路径(Lines 242-254)几乎完全相同。为了遵循 DRY 原则并与现有代码风格保持一致(如
syncErrorRulesAndInitializeDetector),建议提取为独立的辅助函数。♻️ 建议的重构
在文件顶部添加辅助函数:
/** * 回填 provider_endpoints(从 providers.url/类型 生成端点池) * 幂等操作,失败不阻塞启动 */ async function backfillProviderEndpoints(): Promise<void> { try { const { backfillProviderEndpointsFromProviders } = await import( "@/repository/provider-endpoints" ); const result = await backfillProviderEndpointsFromProviders(); logger.info("[Instrumentation] Provider endpoints backfill completed", result); } catch (error) { logger.warn("[Instrumentation] Failed to backfill provider endpoints", { error: error instanceof Error ? error.message : String(error), }); } }然后在两处调用点简化为:
- // 回填 provider_endpoints(从 providers.url/类型 生成端点池,幂等) - try { - const { backfillProviderEndpointsFromProviders } = await import( - "@/repository/provider-endpoints" - ); - const result = await backfillProviderEndpointsFromProviders(); - logger.info("[Instrumentation] Provider endpoints backfill completed", result); - } catch (error) { - logger.warn("[Instrumentation] Failed to backfill provider endpoints", { - error: error instanceof Error ? error.message : String(error), - }); - } + await backfillProviderEndpoints();src/repository/provider.ts (1)
469-490: 条件判断逻辑需确认一致性当
provider_type变更但url未变更时:
- Line 469 的条件仅检查
url或website_url,不会重新计算providerVendorId- Line 553 的条件包含
provider_type,会触发端点 seed这种设计在当前逻辑下是可行的(vendor 由 URL 域名决定,与 type 无关),但建议添加注释说明意图,避免后续维护时产生困惑。
src/repository/provider-endpoints.ts (1)
530-567: 事务内时间戳处理可优化Line 563 在事务内创建新的
new Date()作为updatedAt,可能与 Line 540 的probedAt存在微小时间差。建议统一使用probedAt以保持一致性:建议的改动
.set({ lastProbedAt: probedAt, lastProbeOk: input.ok, lastProbeStatusCode: input.statusCode ?? null, lastProbeLatencyMs: input.latencyMs ?? null, lastProbeErrorType: input.ok ? null : (input.errorType ?? null), lastProbeErrorMessage: input.ok ? null : (input.errorMessage ?? null), - updatedAt: new Date(), + updatedAt: probedAt, })tests/unit/repository/provider-endpoints.test.ts (1)
3-17: 辅助函数缺少offset方法
createThenableQuery未实现offset方法,但backfillProviderEndpointsFromProviders的查询链不使用 offset(使用游标分页),所以当前测试不受影响。如果后续测试需要覆盖使用 offset 的查询,需要补充此方法。建议补充 offset 方法
type Query = Promise<T> & { from: (...args: unknown[]) => Query; where: (...args: unknown[]) => Query; orderBy: (...args: unknown[]) => Query; limit: (...args: unknown[]) => Query; + offset: (...args: unknown[]) => Query; }; const query = Promise.resolve(result) as unknown as Query; query.from = () => query; query.where = () => query; query.orderBy = () => query; query.limit = () => query; + query.offset = () => query; return query;tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts (1)
149-194: 建议补充缺失的 session 字段以提高测试健壮性
createSession辅助函数缺少ProxySession类中的部分字段,包括providersSnapshot、billingModelSourcePromise和cachedBillingPriceData。虽然当前测试场景可能不会访问这些字段,但如果ProxySession类未来发生变化,这些测试可能会意外失败。♻️ 建议补充缺失字段
cachedPriceData: undefined, cachedBillingModelSource: undefined, + billingModelSourcePromise: undefined, + cachedBillingPriceData: undefined, + providersSnapshot: null, isHeaderModified: () => false, });src/app/v1/_lib/proxy/forwarder.ts (3)
231-253:providerVendorId检查可能存在边界条件问题Line 231 使用
currentProvider.providerVendorId > 0进行检查。如果providerVendorId为undefined或null,JavaScript 的比较会返回false,这在逻辑上是正确的。但从类型安全角度,建议增加更明确的空值检查以提高代码可读性。♻️ 建议使用更明确的空值检查
- } else if (currentProvider.providerVendorId > 0) { + } else if (currentProvider.providerVendorId != null && currentProvider.providerVendorId > 0) {
267-281: 建议在 vendor-type 熔断跳过时记录到决策链当 vendor-type 熔断器处于打开状态时,供应商被跳过但未记录到决策链中。这可能导致在调试或审计时难以追踪为何某个供应商被跳过。
♻️ 建议记录熔断跳过事件
logger.warn("ProxyForwarder: Vendor-type circuit is open, skipping provider", { providerId: currentProvider.id, vendorId: currentProvider.providerVendorId, providerType: currentProvider.providerType, }); failedProviderIds.push(currentProvider.id); attemptCount = maxAttemptsPerProvider; + // 记录到决策链以便审计 + session.addProviderToChain(currentProvider, { + endpointId: null, + endpointUrl: sanitizeUrl(currentProvider.url), + reason: "retry_failed", + errorMessage: "Vendor-type circuit breaker is open", + circuitState: "open", + }); }
2221-2237:toUndiciBody辅助函数未处理所有BodyInit类型
toUndiciBody函数处理了null、string、Uint8Array、Buffer、ArrayBuffer和ArrayBufferView类型,但BodyInit还包括Blob、ReadableStream、FormData和URLSearchParams。对于这些类型,函数返回undefined,可能导致请求体丢失。根据当前代码上下文,请求体通常是 JSON 字符串(
JSON.stringify(filteredMessage)),所以这个问题在实际场景中可能不会触发。但为防止未来扩展时出现问题,建议记录或处理这些边界情况。♻️ 建议增加未处理类型的日志
if (ArrayBuffer.isView(body)) { return new Uint8Array(body.buffer, body.byteOffset, body.byteLength); } + // Blob, ReadableStream, FormData, URLSearchParams are not supported + // Log warning if these types are encountered + logger.warn("[ProxyForwarder] Unsupported body type for undici request", { + bodyType: body?.constructor?.name, + }); return undefined; };
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to Reviews > Disable Cache setting
📒 Files selected for processing (9)
src/app/v1/_lib/proxy/forwarder.tssrc/app/v1/_lib/proxy/session.tssrc/instrumentation.tssrc/repository/provider-endpoints.tssrc/repository/provider.tssrc/types/message.tstests/unit/proxy/proxy-forwarder-endpoint-audit.test.tstests/unit/proxy/session.test.tstests/unit/repository/provider-endpoints.test.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{js,ts,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Never use emoji characters in any code, comments, or string literals
Files:
tests/unit/repository/provider-endpoints.test.tssrc/types/message.tssrc/repository/provider.tssrc/app/v1/_lib/proxy/forwarder.tssrc/app/v1/_lib/proxy/session.tssrc/repository/provider-endpoints.tstests/unit/proxy/session.test.tstests/unit/proxy/proxy-forwarder-endpoint-audit.test.tssrc/instrumentation.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
All new features must have unit test coverage of at least 80%
Files:
tests/unit/repository/provider-endpoints.test.tstests/unit/proxy/session.test.tstests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias@/to reference files in./src/directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width
Files:
tests/unit/repository/provider-endpoints.test.tssrc/types/message.tssrc/repository/provider.tssrc/app/v1/_lib/proxy/forwarder.tssrc/app/v1/_lib/proxy/session.tssrc/repository/provider-endpoints.tstests/unit/proxy/session.test.tstests/unit/proxy/proxy-forwarder-endpoint-audit.test.tssrc/instrumentation.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer named exports over default exports
Files:
tests/unit/repository/provider-endpoints.test.tssrc/types/message.tssrc/repository/provider.tssrc/app/v1/_lib/proxy/forwarder.tssrc/app/v1/_lib/proxy/session.tssrc/repository/provider-endpoints.tstests/unit/proxy/session.test.tstests/unit/proxy/proxy-forwarder-endpoint-audit.test.tssrc/instrumentation.ts
tests/**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Vitest for unit testing and happy-dom for DOM testing
Files:
tests/unit/repository/provider-endpoints.test.tstests/unit/proxy/session.test.tstests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts
src/repository/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Drizzle ORM for data access in the repository layer
Files:
src/repository/provider.tssrc/repository/provider-endpoints.ts
src/app/v1/_lib/proxy/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
The proxy pipeline processes requests through a GuardPipeline with sequential guards: auth, sensitive, client, model, version, probe, session, warmup, requestFilter, rateLimit, provider, providerRequestFilter, messageContext
Files:
src/app/v1/_lib/proxy/forwarder.tssrc/app/v1/_lib/proxy/session.ts
🧠 Learnings (4)
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (e.g., matches DB schema or special UI logic) and undefined signifies 'inherit default'. This pattern should be consistently reflected in type definitions across related fields to preserve semantic clarity between database constraints and UI behavior.
Applied to files:
src/types/message.tssrc/repository/provider.tssrc/app/v1/_lib/proxy/forwarder.tssrc/app/v1/_lib/proxy/session.tssrc/repository/provider-endpoints.tssrc/instrumentation.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/app/v1/_lib/proxy/**/*.{ts,tsx} : The proxy pipeline processes requests through a GuardPipeline with sequential guards: auth, sensitive, client, model, version, probe, session, warmup, requestFilter, rateLimit, provider, providerRequestFilter, messageContext
Applied to files:
src/app/v1/_lib/proxy/forwarder.tssrc/app/v1/_lib/proxy/session.tstests/unit/proxy/session.test.tstests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/repository/**/*.{ts,tsx} : Use Drizzle ORM for data access in the repository layer
Applied to files:
src/repository/provider-endpoints.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to tests/**/*.test.{ts,tsx,js,jsx} : Use Vitest for unit testing and happy-dom for DOM testing
Applied to files:
tests/unit/proxy/session.test.ts
🧬 Code graph analysis (5)
tests/unit/repository/provider-endpoints.test.ts (1)
src/repository/provider-endpoints.ts (2)
ensureProviderEndpointExistsForUrl(331-365)backfillProviderEndpointsFromProviders(367-449)
src/app/v1/_lib/proxy/forwarder.ts (5)
src/lib/provider-endpoints/endpoint-selector.ts (1)
getPreferredProviderEndpoints(30-54)src/lib/vendor-type-circuit-breaker.ts (2)
isVendorTypeCircuitOpen(115-137)recordVendorTypeAllEndpointsTimeout(139-155)src/app/v1/_lib/proxy/errors.ts (1)
sanitizeUrl(1010-1053)src/lib/circuit-breaker.ts (1)
getCircuitState(395-398)src/app/v1/_lib/url.ts (1)
buildProxyUrl(25-84)
src/repository/provider-endpoints.ts (2)
src/types/provider.ts (5)
ProviderVendor(375-383)ProviderEndpoint(387-404)ProviderEndpointProbeLog(406-416)ProviderType(6-12)ProviderEndpointProbeSource(385-385)src/drizzle/schema.ts (3)
providerVendors(132-145)providerEndpoints(297-339)providerEndpointProbeLogs(342-363)
tests/unit/proxy/session.test.ts (1)
src/types/provider.ts (1)
Provider(39-144)
src/instrumentation.ts (1)
src/repository/provider-endpoints.ts (1)
backfillProviderEndpointsFromProviders(367-449)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Docker Build Test
- GitHub Check: dev-build-deploy
🔇 Additional comments (17)
src/instrumentation.ts (1)
242-254: 实现逻辑正确,位置合理开发环境中的回填逻辑放置在
isConnected分支内是正确的,确保只在数据库可用时执行。错误处理遵循了文件中其他非关键初始化操作的模式(catch + warn)。如上条评论所述,建议与生产环境路径共用提取后的辅助函数。
src/types/message.ts (1)
3-3: LGTM!类型扩展设计合理:
endpointId?: number | null正确区分了null(明确未选择端点)与undefined(字段未填充)的语义。- 新增字段与 PR 中 proxy/session 层的 endpoint audit 逻辑一致。
Also applies to: 14-20
src/repository/provider.ts (1)
135-147: LGTM!端点自动 seed 的实现采用了合理的防御性编程模式:
- try-catch 确保 seed 失败不会阻断主流程
- 日志包含足够的上下文信息用于排查
src/repository/provider-endpoints.ts (4)
20-52: LGTM!日期转换辅助函数和 URL 规范化逻辑实现正确:
toDate使用new Date()作为后备值,适用于数据库行总是包含有效时间戳的场景normalizeWebsiteDomainFromUrl正确处理了带/不带协议的 URL,并统一移除www.前缀
104-151: LGTM!
getOrCreateProviderVendorIdFromUrls的 upsert 实现采用了正确的竞态条件处理模式:
- 先查询现有记录
- 尝试插入(
onConflictDoNothing)- 若插入未返回行(冲突),再次查询获取已存在的 ID
这种模式避免了
INSERT ON CONFLICT DO UPDATE可能带来的不必要更新。
367-449: LGTM!批量回填实现考虑周全:
- 游标分页(
gt(providers.id, lastProviderId))适合大数据集- Set 去重避免单次运行内的重复插入
- 完整的验证逻辑(vendorId > 0、URL 非空、URL 格式合法)
onConflictDoNothing确保幂等性
153-329: LGTM!CRUD 函数实现符合 Drizzle ORM 最佳实践:
- 软删除过滤 (
isNull(deletedAt)) 一致应用- 空 payload 检查避免无意义的数据库写入
- 分页查询使用
limit/offset模式Also applies to: 451-528, 569-593
tests/unit/proxy/session.test.ts (1)
680-780: LGTM!endpoint audit 测试覆盖了关键场景:
- 字段写入验证(vendorId/providerType/endpointId/endpointUrl)
- 无 attemptNumber 时的去重逻辑
- 有 attemptNumber 时保留多条记录(支持重试审计)
测试设计与
ProviderChainItem类型扩展一致。tests/unit/proxy/proxy-forwarder-endpoint-audit.test.ts (1)
196-373: 测试覆盖全面,安全性验证到位三个测试用例覆盖了端点审计的关键场景:成功请求、重试、以及端点选择失败回退。每个测试都验证了 URL 脱敏逻辑,确保敏感信息(如
api_key、token)不会泄露到审计字段中。src/app/v1/_lib/proxy/forwarder.ts (7)
287-302: 端点轮询选择和审计字段设计合理使用取模运算实现端点轮询,确保多次重试时能依次尝试不同端点。
sanitizeUrl用于脱敏 URL,endpointAudit对象正确传递到doForward方法。
386-388: 端点级熔断器记录逻辑正确成功时仅在
endpointId有效时记录;失败时仅对超时(524)或系统错误进行记录,避免将客户端问题误计入端点熔断器。这种设计能准确反映端点的健康状况。Also applies to: 469-481
910-923: LGTM! vendor-type 熔断触发条件严谨仅当所有端点都尝试过且全部超时时才触发 vendor-type 熔断,避免因单个端点问题导致整个供应商被错误熔断。
!isMcpRequest条件确保 MCP 请求不影响 vendor-type 熔断状态。
1046-1051:doForward签名扩展支持端点级路由新增
baseUrl、endpointAudit和attemptNumber参数,使方法能够正确处理端点级路由和遥测数据。设计合理。
1120-1137: Gemini 路径的 baseUrl 处理逻辑正确
effectiveBaseUrl的回退链(baseUrl→provider.url→ 官方端点)设计合理,确保在各种配置场景下都能正确构建请求 URL 和 Headers。
1335-1378: MCP 透传路径正确使用端点 URLMCP 透传逻辑现在从
effectiveBaseUrl(可能来自端点 URL)提取基础域名,而非始终使用provider.url。这确保了当存在端点 URL 时能正确路由 MCP 请求。
438-447: 端点审计字段在决策链中一致传播所有
session.addProviderToChain调用现在都通过展开运算符...endpointAudit传递端点审计数据,确保决策链中每个条目都包含完整的端点级遥测信息。src/app/v1/_lib/proxy/session.ts (1)
471-472: LGTM! 端点审计字段扩展设计合理新增的
endpointId、endpointUrl、vendorId和providerType字段为决策链提供了完整的端点级遥测数据。endpointId使用number | null类型能正确区分"未选择端点"和"已选择端点"的场景。ProviderChainItem 类型定义(src/types/message.ts)已同步更新,新字段在 ProviderChainItem 构造中正确赋值,实现与类型定义保持一致。代码规范符合指南要求。
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| describe("provider-endpoints repository", () => { | ||
| test("ensureProviderEndpointExistsForUrl: url 为空时返回 false 且不写 DB", async () => { | ||
| vi.resetModules(); | ||
|
|
||
| const insertMock = vi.fn(); | ||
| vi.doMock("@/drizzle/db", () => ({ | ||
| db: { | ||
| insert: insertMock, | ||
| }, | ||
| })); | ||
|
|
||
| const { ensureProviderEndpointExistsForUrl } = await import("@/repository/provider-endpoints"); | ||
| const ok = await ensureProviderEndpointExistsForUrl({ | ||
| vendorId: 1, | ||
| providerType: "claude", | ||
| url: " ", | ||
| }); | ||
|
|
||
| expect(ok).toBe(false); | ||
| expect(insertMock).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| test("ensureProviderEndpointExistsForUrl: url 非法时返回 false 且不写 DB", async () => { | ||
| vi.resetModules(); | ||
|
|
||
| const insertMock = vi.fn(); | ||
| vi.doMock("@/drizzle/db", () => ({ | ||
| db: { | ||
| insert: insertMock, | ||
| }, | ||
| })); | ||
|
|
||
| const { ensureProviderEndpointExistsForUrl } = await import("@/repository/provider-endpoints"); | ||
| const ok = await ensureProviderEndpointExistsForUrl({ | ||
| vendorId: 1, | ||
| providerType: "claude", | ||
| url: "not a url", | ||
| }); | ||
|
|
||
| expect(ok).toBe(false); | ||
| expect(insertMock).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| test("ensureProviderEndpointExistsForUrl: 插入成功时返回 true(trim + label=null)", async () => { | ||
| vi.resetModules(); | ||
|
|
||
| const state = { values: undefined as unknown }; | ||
| const returning = vi.fn(async () => [{ id: 1 }]); | ||
| const onConflictDoNothing = vi.fn(() => ({ returning })); | ||
| const values = vi.fn((payload: unknown) => { | ||
| state.values = payload; | ||
| return { onConflictDoNothing }; | ||
| }); | ||
| const insertMock = vi.fn(() => ({ values })); | ||
|
|
||
| vi.doMock("@/drizzle/db", () => ({ | ||
| db: { | ||
| insert: insertMock, | ||
| }, | ||
| })); | ||
|
|
||
| const { ensureProviderEndpointExistsForUrl } = await import("@/repository/provider-endpoints"); | ||
| const ok = await ensureProviderEndpointExistsForUrl({ | ||
| vendorId: 1, | ||
| providerType: "claude", | ||
| url: " https://api.example.com ", | ||
| }); | ||
|
|
||
| expect(ok).toBe(true); | ||
| expect(insertMock).toHaveBeenCalledTimes(1); | ||
| expect(values).toHaveBeenCalledTimes(1); | ||
|
|
||
| expect(state.values).toEqual( | ||
| expect.objectContaining({ | ||
| vendorId: 1, | ||
| providerType: "claude", | ||
| url: "https://api.example.com", | ||
| label: null, | ||
| }) | ||
| ); | ||
| }); | ||
|
|
||
| test("ensureProviderEndpointExistsForUrl: 冲突不插入时返回 false", async () => { | ||
| vi.resetModules(); | ||
|
|
||
| const returning = vi.fn(async () => []); | ||
| const onConflictDoNothing = vi.fn(() => ({ returning })); | ||
| const values = vi.fn(() => ({ onConflictDoNothing })); | ||
| const insertMock = vi.fn(() => ({ values })); | ||
|
|
||
| vi.doMock("@/drizzle/db", () => ({ | ||
| db: { | ||
| insert: insertMock, | ||
| }, | ||
| })); | ||
|
|
||
| const { ensureProviderEndpointExistsForUrl } = await import("@/repository/provider-endpoints"); | ||
| const ok = await ensureProviderEndpointExistsForUrl({ | ||
| vendorId: 1, | ||
| providerType: "claude", | ||
| url: "https://api.example.com", | ||
| }); | ||
|
|
||
| expect(ok).toBe(false); | ||
| }); | ||
|
|
||
| test("backfillProviderEndpointsFromProviders: 全部无效时不写 DB", async () => { | ||
| vi.resetModules(); | ||
|
|
||
| const selectPages = [ | ||
| [ | ||
| { id: 1, vendorId: 0, providerType: "claude", url: "https://ok.example.com" }, | ||
| { id: 2, vendorId: 1, providerType: "claude", url: " " }, | ||
| { id: 3, vendorId: 1, providerType: "claude", url: "not a url" }, | ||
| ], | ||
| [], | ||
| ]; | ||
|
|
||
| const selectMock = vi.fn(() => createThenableQuery(selectPages.shift() ?? [])); | ||
| const insertMock = vi.fn(); | ||
|
|
||
| vi.doMock("@/drizzle/db", () => ({ | ||
| db: { | ||
| select: selectMock, | ||
| insert: insertMock, | ||
| }, | ||
| })); | ||
|
|
||
| const { backfillProviderEndpointsFromProviders } = await import( | ||
| "@/repository/provider-endpoints" | ||
| ); | ||
| const result = await backfillProviderEndpointsFromProviders(); | ||
|
|
||
| expect(result).toEqual({ inserted: 0, uniqueCandidates: 0, skippedInvalid: 3 }); | ||
| expect(insertMock).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| test("backfillProviderEndpointsFromProviders: 去重 + trim + 统计 inserted/uniqueCandidates/skippedInvalid", async () => { | ||
| vi.resetModules(); | ||
|
|
||
| const capturedValues: unknown[] = []; | ||
|
|
||
| const insertState = { values: undefined as unknown }; | ||
| const returning = vi.fn(async () => { | ||
| const values = insertState.values; | ||
| if (!Array.isArray(values)) return []; | ||
| return values.map((_, idx) => ({ id: idx + 1 })); | ||
| }); | ||
| const onConflictDoNothing = vi.fn(() => ({ returning })); | ||
| const values = vi.fn((payload: unknown) => { | ||
| insertState.values = payload; | ||
| if (Array.isArray(payload)) capturedValues.push(...payload); | ||
| return { onConflictDoNothing }; | ||
| }); | ||
| const insertMock = vi.fn(() => ({ values })); | ||
|
|
||
| const selectPages = [ | ||
| [ | ||
| { id: 1, vendorId: 1, providerType: "claude", url: " https://a.com " }, | ||
| { id: 2, vendorId: 1, providerType: "claude", url: "https://a.com" }, | ||
| { id: 3, vendorId: 1, providerType: "openai-compatible", url: "https://a.com" }, | ||
| ], | ||
| [ | ||
| { id: 4, vendorId: 2, providerType: "claude", url: "https://a.com" }, | ||
| { id: 5, vendorId: 0, providerType: "claude", url: "https://bad-vendor.com" }, | ||
| { id: 6, vendorId: 1, providerType: "claude", url: "not a url" }, | ||
| ], | ||
| [], | ||
| ]; | ||
|
|
||
| const selectMock = vi.fn(() => createThenableQuery(selectPages.shift() ?? [])); | ||
|
|
||
| vi.doMock("@/drizzle/db", () => ({ | ||
| db: { | ||
| select: selectMock, | ||
| insert: insertMock, | ||
| }, | ||
| })); | ||
|
|
||
| const { backfillProviderEndpointsFromProviders } = await import( | ||
| "@/repository/provider-endpoints" | ||
| ); | ||
| const result = await backfillProviderEndpointsFromProviders(); | ||
|
|
||
| expect(result).toEqual({ inserted: 3, uniqueCandidates: 3, skippedInvalid: 2 }); | ||
|
|
||
| expect(capturedValues).toEqual( | ||
| expect.arrayContaining([ | ||
| expect.objectContaining({ vendorId: 1, providerType: "claude", url: "https://a.com" }), | ||
| expect.objectContaining({ | ||
| vendorId: 1, | ||
| providerType: "openai-compatible", | ||
| url: "https://a.com", | ||
| }), | ||
| expect.objectContaining({ vendorId: 2, providerType: "claude", url: "https://a.com" }), | ||
| ]) | ||
| ); | ||
| }); | ||
|
|
||
| test("backfillProviderEndpointsFromProviders: 冲突不插入时 inserted=0 但 uniqueCandidates 仍统计", async () => { | ||
| vi.resetModules(); | ||
|
|
||
| const insertState = { values: undefined as unknown }; | ||
| const returning = vi.fn(async () => []); | ||
| const onConflictDoNothing = vi.fn(() => ({ returning })); | ||
| const values = vi.fn((payload: unknown) => { | ||
| insertState.values = payload; | ||
| return { onConflictDoNothing }; | ||
| }); | ||
| const insertMock = vi.fn(() => ({ values })); | ||
|
|
||
| const selectPages = [ | ||
| [ | ||
| { id: 1, vendorId: 1, providerType: "claude", url: "https://a.com" }, | ||
| { id: 2, vendorId: 1, providerType: "openai-compatible", url: "https://a.com" }, | ||
| ], | ||
| [], | ||
| ]; | ||
|
|
||
| const selectMock = vi.fn(() => createThenableQuery(selectPages.shift() ?? [])); | ||
|
|
||
| vi.doMock("@/drizzle/db", () => ({ | ||
| db: { | ||
| select: selectMock, | ||
| insert: insertMock, | ||
| }, | ||
| })); | ||
|
|
||
| const { backfillProviderEndpointsFromProviders } = await import( | ||
| "@/repository/provider-endpoints" | ||
| ); | ||
| const result = await backfillProviderEndpointsFromProviders(); | ||
|
|
||
| expect(result).toEqual({ inserted: 0, uniqueCandidates: 2, skippedInvalid: 0 }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for the missing functions in test files to verify coverage
rg -n --type=ts -C2 'getOrCreateProviderVendorIdFromUrls|createProviderEndpoint|updateProviderEndpoint|softDeleteProviderEndpoint|recordProviderEndpointProbeResult|findProviderEndpointProbeLogs' tests/Repository: ding113/claude-code-hub
Length of output: 2882
🏁 Script executed:
#!/bin/bash
# First, find the provider-endpoints repository file
fd -e ts -e js "provider-endpoints" src/repository/
# Then check what functions are exported
echo "=== Checking exports from provider-endpoints ==="
rg -n "export" src/repository/provider-endpoints.ts
# Search for all occurrences of the missing functions across the entire repo
echo "=== Searching for other functions across codebase ==="
rg -n 'getOrCreateProviderVendorIdFromUrls|createProviderEndpoint|updateProviderEndpoint|softDeleteProviderEndpoint|findProviderEndpointProbeLogs' . --type=ts --type=tsxRepository: ding113/claude-code-hub
Length of output: 1111
🏁 Script executed:
#!/bin/bash
# Search more carefully for actual usage (not mocking) of these functions in tests
rg -n 'getOrCreateProviderVendorIdFromUrls|createProviderEndpoint|updateProviderEndpoint|softDeleteProviderEndpoint|findProviderEndpointProbeLogs' tests/ --type=ts -g '!*.test.ts' -A 2
# Also check if they're directly imported and tested
echo "=== Direct imports in test files ==="
rg -n 'import.*\{' tests/ --type=ts -A 5 | grep -E '(getOrCreateProviderVendorIdFromUrls|createProviderEndpoint|updateProviderEndpoint|softDeleteProviderEndpoint|findProviderEndpointProbeLogs)'
# Check the complete test file to see what's imported
echo "=== provider-endpoints.test.ts imports ==="
head -50 tests/unit/repository/provider-endpoints.test.tsRepository: ding113/claude-code-hub
Length of output: 1487
🏁 Script executed:
#!/bin/bash
# Check the actual test file to see the complete list of tests
wc -l tests/unit/repository/provider-endpoints.test.ts
# Count how many functions are exported vs tested
echo "=== Total exported functions from provider-endpoints ==="
rg -c "^export " src/repository/provider-endpoints.ts
# List all exported functions
echo "=== All exported functions ==="
rg "^export (async )?function" src/repository/provider-endpoints.ts | sed 's/.*function //' | sed 's/(.*$//'
# Check if there are any integration or e2e tests
echo "=== Looking for other test files that might cover provider-endpoints ==="
fd -e test.ts tests/ | xargs grep -l "provider-endpoints\|provider.*endpoint" 2>/dev/null | head -20Repository: ding113/claude-code-hub
Length of output: 1003
🏁 Script executed:
#!/bin/bash
# Fix the fd search command
echo "=== Looking for other test files that mention provider-endpoints ==="
fd . tests/ -name "*.test.ts" -o -name "*.test.js" | xargs grep -l "provider-endpoints\|from.*repository" 2>/dev/null
# Check if any integration or API tests exist
echo "=== All test files in tests/unit ==="
find tests/unit -name "*.test.ts" -o -name "*.test.js" | sort
# Check what functions are actually used/tested in the probe.test.ts
echo "=== Functions tested in probe.test.ts ==="
rg -n "await|test\(" tests/unit/lib/provider-endpoints/probe.test.ts | head -30Repository: ding113/claude-code-hub
Length of output: 6945
🏁 Script executed:
#!/bin/bash
# Check the provider.test.ts to see if it covers provider-endpoints functions
echo "=== Checking provider.test.ts ==="
grep -n "createProviderEndpoint\|updateProviderEndpoint\|softDeleteProviderEndpoint\|getOrCreateProviderVendorIdFromUrls\|findProviderVendors\|findProviderVendorById\|findProviderEndpointProbeLogs" tests/unit/repository/provider.test.ts
# Check endpoint-selector.test.ts
echo "=== Checking endpoint-selector.test.ts ==="
grep -n "import.*from.*provider-endpoints" tests/unit/lib/provider-endpoints/endpoint-selector.test.ts
# Get a quick count of what is actually tested in the provider-endpoints.test.ts file
echo "=== Test cases in provider-endpoints.test.ts ==="
grep -n "test(" tests/unit/repository/provider-endpoints.test.ts | wc -l
echo "=== All test case names ==="
grep "test(" tests/unit/repository/provider-endpoints.test.ts | sed 's/.*test("//' | sed 's/".*//' | sortRepository: ding113/claude-code-hub
Length of output: 687
需要补充单元测试以达到 80% 覆盖率要求
当前测试仅覆盖了 ensureProviderEndpointExistsForUrl 和 backfillProviderEndpointsFromProviders 两个函数。根据编码指南,新功能单元测试覆盖率需要达到 80%,但以下 10 个函数缺少直接的单元测试覆盖:
getOrCreateProviderVendorIdFromUrlsfindProviderVendorsfindProviderVendorByIdfindProviderEndpointByIdupdateProviderVendorfindProviderEndpointsByVendorAndTypecreateProviderEndpointupdateProviderEndpointsoftDeleteProviderEndpointfindProviderEndpointProbeLogs
recordProviderEndpointProbeResult 仅在 probe.test.ts 中被 mock,缺少在 provider-endpoints.test.ts 中的直接单元测试。
🤖 Prompt for AI Agents
In `@tests/unit/repository/provider-endpoints.test.ts` around lines 19 - 253,
Tests only cover two functions and ten repository functions
(getOrCreateProviderVendorIdFromUrls, findProviderVendors,
findProviderVendorById, findProviderEndpointById, updateProviderVendor,
findProviderEndpointsByVendorAndType, createProviderEndpoint,
updateProviderEndpoint, softDeleteProviderEndpoint,
findProviderEndpointProbeLogs) plus direct call coverage for
recordProviderEndpointProbeResult are missing; add unit tests for each missing
function to reach 80% coverage. For each function listed, write focused tests
that import the target (e.g., getOrCreateProviderVendorIdFromUrls,
findProviderVendors, etc.) from "@/repository/provider-endpoints", mock
"@/drizzle/db" with vi.doMock to provide controlled
select/insert/update/delete/returning behaviors, assert expected return values
and DB interaction (calls/params), include edge cases (invalid inputs,
conflicts, empty results), and add a direct unit test for
recordProviderEndpointProbeResult in this file that mocks any external probe
logger and verifies it records outcomes; ensure tests use vi.resetModules before
vi.doMock and validate coverage meets the 80% threshold.
Replaces 0054/0055 with a single Drizzle-generated migration and adds idempotent guards/backfill so provider_vendors schema drift won't break startup migrations.
🧪 测试结果
总体结果: ✅ 所有测试通过 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@drizzle/0054_motionless_ulik.sql`:
- Around line 32-40: The timestamps in the provider_vendors table lack NOT NULL
constraints; update the CREATE TABLE statement for provider_vendors to add NOT
NULL to the created_at and updated_at columns (i.e., change "created_at"
timestamp with time zone DEFAULT now() and "updated_at" timestamp with time zone
DEFAULT now() to include NOT NULL) so they match other tables' behavior.
- Line 139: The unique index uniq_provider_endpoints_vendor_type_url on table
provider_endpoints currently does not exclude soft-deleted rows; drop and
recreate that index so the uniqueness applies only to active rows by adding a
WHERE deleted_at IS NULL predicate (i.e., remove the existing
uniq_provider_endpoints_vendor_type_url and create a unique index on
("vendor_id","provider_type","url") WITH the WHERE deleted_at IS NULL condition
so soft-deleted endpoints no longer block recreating identical records).
♻️ Duplicate comments (1)
drizzle/0054_motionless_ulik.sql (1)
1-11:created_at列缺少NOT NULL约束。
provider_endpoint_probe_logs.created_at列有DEFAULT now()但未设置NOT NULL。如果插入时显式传入NULL,会导致数据不完整。🔧 建议的修复
- "created_at" timestamp with time zone DEFAULT now() + "created_at" timestamp with time zone DEFAULT now() NOT NULL
🧹 Nitpick comments (1)
drizzle/0054_motionless_ulik.sql (1)
66-117: 域名提取逻辑重复。INSERT 语句(Lines 66-88)和 UPDATE 语句(Lines 90-117)中的域名提取逻辑完全相同。虽然在 SQL 迁移中这是可以接受的(迁移文件通常只运行一次),但如果需要修改逻辑,需要同时维护两处。
可以考虑使用 CTE 或临时表来避免重复:
-- 示例:使用 CTE 提取域名一次 WITH domain_mapping AS ( SELECT id AS provider_id, CASE WHEN domain_candidate IS NULL OR domain_candidate = '' THEN 'unknown-' || id ELSE domain_candidate END AS website_domain FROM ( SELECT id, lower(split_part(split_part(regexp_replace(COALESCE(NULLIF(website_url, ''), url), '^[a-zA-Z]+://', ''), '/', 1), ':', 1)) AS domain_candidate FROM providers ) n ) -- 然后在 INSERT 和 UPDATE 中引用 domain_mapping
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to Reviews > Disable Cache setting
📒 Files selected for processing (4)
drizzle/0054_motionless_ulik.sqldrizzle/0054_tidy_winter_soldier.sqldrizzle/meta/0054_snapshot.jsondrizzle/meta/_journal.json
💤 Files with no reviewable changes (1)
- drizzle/0054_tidy_winter_soldier.sql
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: 🌐 API Tests
- GitHub Check: Docker Build Test
- GitHub Check: dev-build-deploy
🔇 Additional comments (8)
drizzle/0054_motionless_ulik.sql (3)
42-60: 幂等性设计良好。使用
ADD COLUMN IF NOT EXISTS和CREATE ... IF NOT EXISTS确保迁移可以安全地重复执行,配合 backfill 逻辑处理现有数据,设计合理。
121-156: 外键约束的幂等性处理得当。使用
DO $$ ... EXCEPTION WHEN duplicate_object THEN NULL; END $$处理外键约束已存在的情况,确保迁移可以安全重跑。
151-158:providers到provider_vendors的外键使用ON DELETE restrict是正确的选择。防止在有关联 provider 时删除 vendor,保护数据完整性。同时
idx_providers_vendor_type索引的WHERE deleted_at IS NULL条件有助于优化查询。drizzle/meta/0054_snapshot.json (4)
1153-1272: 新表provider_endpoint_probe_logs的 schema 定义正确。包含端点探测日志所需的字段:endpoint_id、source、ok、status_code、latency_ms、error_type、error_message。索引设计合理,支持按 endpoint_id + created_at 的高效查询。
注意:
created_at的notNull: false与 SQL 迁移文件中的问题一致,建议在drizzle/schema.ts中修复后重新生成。
1273-1508: 新表provider_endpoints的 schema 设计合理。包含端点管理所需的完整字段,支持软删除(deleted_at)、探测状态快照(last_probe_* 字段)和排序(sort_order)。外键
onDelete: cascade确保删除 vendor 时级联删除关联端点。
1509-1596: 新表provider_vendors的 schema 定义正确。
website_domain的唯一索引确保供应商域名不重复。结构简洁,支持后续扩展(display_name、favicon_url 等)。
1631-1636:providers表的修改正确反映了新的 vendor 关联关系。
provider_vendor_id设置为notNull: true,确保每个 provider 必须关联一个 vendor- 新增的
idx_providers_vendor_type索引配合WHERE deleted_at IS NULL条件,优化按 vendor+type 查询- 外键
onDelete: restrict防止误删有关联的 vendorAlso applies to: 2016-2052
drizzle/meta/_journal.json (1)
383-389: 迁移日志更新正确。Journal 条目与新的迁移文件
0054_motionless_ulik.sql保持一致,时间戳已更新。
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| CREATE TABLE IF NOT EXISTS "provider_endpoints" ( | ||
| "id" serial PRIMARY KEY NOT NULL, | ||
| "vendor_id" integer NOT NULL, | ||
| "provider_type" varchar(20) DEFAULT 'claude' NOT NULL, | ||
| "url" text NOT NULL, | ||
| "label" varchar(200), | ||
| "sort_order" integer DEFAULT 0 NOT NULL, | ||
| "is_enabled" boolean DEFAULT true NOT NULL, | ||
| "last_probed_at" timestamp with time zone, | ||
| "last_probe_ok" boolean, | ||
| "last_probe_status_code" integer, | ||
| "last_probe_latency_ms" integer, | ||
| "last_probe_error_type" varchar(64), | ||
| "last_probe_error_message" text, | ||
| "created_at" timestamp with time zone DEFAULT now(), | ||
| "updated_at" timestamp with time zone DEFAULT now(), | ||
| "deleted_at" timestamp with time zone | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
created_at 和 updated_at 列缺少 NOT NULL 约束。
与 provider_endpoint_probe_logs 表相同的问题,provider_endpoints 表的 created_at 和 updated_at 列虽然有默认值,但允许 NULL 值插入。
🔧 建议的修复
- "created_at" timestamp with time zone DEFAULT now(),
- "updated_at" timestamp with time zone DEFAULT now(),
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| CREATE TABLE IF NOT EXISTS "provider_endpoints" ( | |
| "id" serial PRIMARY KEY NOT NULL, | |
| "vendor_id" integer NOT NULL, | |
| "provider_type" varchar(20) DEFAULT 'claude' NOT NULL, | |
| "url" text NOT NULL, | |
| "label" varchar(200), | |
| "sort_order" integer DEFAULT 0 NOT NULL, | |
| "is_enabled" boolean DEFAULT true NOT NULL, | |
| "last_probed_at" timestamp with time zone, | |
| "last_probe_ok" boolean, | |
| "last_probe_status_code" integer, | |
| "last_probe_latency_ms" integer, | |
| "last_probe_error_type" varchar(64), | |
| "last_probe_error_message" text, | |
| "created_at" timestamp with time zone DEFAULT now(), | |
| "updated_at" timestamp with time zone DEFAULT now(), | |
| "deleted_at" timestamp with time zone | |
| ); | |
| CREATE TABLE IF NOT EXISTS "provider_endpoints" ( | |
| "id" serial PRIMARY KEY NOT NULL, | |
| "vendor_id" integer NOT NULL, | |
| "provider_type" varchar(20) DEFAULT 'claude' NOT NULL, | |
| "url" text NOT NULL, | |
| "label" varchar(200), | |
| "sort_order" integer DEFAULT 0 NOT NULL, | |
| "is_enabled" boolean DEFAULT true NOT NULL, | |
| "last_probed_at" timestamp with time zone, | |
| "last_probe_ok" boolean, | |
| "last_probe_status_code" integer, | |
| "last_probe_latency_ms" integer, | |
| "last_probe_error_type" varchar(64), | |
| "last_probe_error_message" text, | |
| "created_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| "updated_at" timestamp with time zone DEFAULT now() NOT NULL, | |
| "deleted_at" timestamp with time zone | |
| ); |
| CREATE TABLE IF NOT EXISTS "provider_vendors" ( | ||
| "id" serial PRIMARY KEY NOT NULL, | ||
| "website_domain" varchar(255) NOT NULL, | ||
| "display_name" varchar(200), | ||
| "website_url" text, | ||
| "favicon_url" text, | ||
| "created_at" timestamp with time zone DEFAULT now(), | ||
| "updated_at" timestamp with time zone DEFAULT now() | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
provider_vendors 表的时间戳列同样缺少 NOT NULL 约束。
建议与其他表保持一致,为 created_at 和 updated_at 添加 NOT NULL 约束。
🔧 建议的修复
- "created_at" timestamp with time zone DEFAULT now(),
- "updated_at" timestamp with time zone DEFAULT now()
+ "created_at" timestamp with time zone DEFAULT now() NOT NULL,
+ "updated_at" timestamp with time zone DEFAULT now() NOT NULL🤖 Prompt for AI Agents
In `@drizzle/0054_motionless_ulik.sql` around lines 32 - 40, The timestamps in the
provider_vendors table lack NOT NULL constraints; update the CREATE TABLE
statement for provider_vendors to add NOT NULL to the created_at and updated_at
columns (i.e., change "created_at" timestamp with time zone DEFAULT now() and
"updated_at" timestamp with time zone DEFAULT now() to include NOT NULL) so they
match other tables' behavior.
| --> statement-breakpoint | ||
| CREATE INDEX IF NOT EXISTS "idx_provider_endpoint_probe_logs_created_at" ON "provider_endpoint_probe_logs" USING btree ("created_at"); | ||
| --> statement-breakpoint | ||
| CREATE UNIQUE INDEX IF NOT EXISTS "uniq_provider_endpoints_vendor_type_url" ON "provider_endpoints" USING btree ("vendor_id","provider_type","url"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
唯一索引未排除软删除记录。
uniq_provider_endpoints_vendor_type_url 索引没有 WHERE deleted_at IS NULL 条件,这意味着软删除的端点仍然占用唯一约束。如果需要重新创建一个之前软删除的端点(相同 vendor_id/provider_type/url),将会违反唯一约束。
🔧 建议的修复
-CREATE UNIQUE INDEX IF NOT EXISTS "uniq_provider_endpoints_vendor_type_url" ON "provider_endpoints" USING btree ("vendor_id","provider_type","url");
+CREATE UNIQUE INDEX IF NOT EXISTS "uniq_provider_endpoints_vendor_type_url" ON "provider_endpoints" USING btree ("vendor_id","provider_type","url") WHERE "deleted_at" IS NULL;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| CREATE UNIQUE INDEX IF NOT EXISTS "uniq_provider_endpoints_vendor_type_url" ON "provider_endpoints" USING btree ("vendor_id","provider_type","url"); | |
| CREATE UNIQUE INDEX IF NOT EXISTS "uniq_provider_endpoints_vendor_type_url" ON "provider_endpoints" USING btree ("vendor_id","provider_type","url") WHERE "deleted_at" IS NULL; |
🤖 Prompt for AI Agents
In `@drizzle/0054_motionless_ulik.sql` at line 139, The unique index
uniq_provider_endpoints_vendor_type_url on table provider_endpoints currently
does not exclude soft-deleted rows; drop and recreate that index so the
uniqueness applies only to active rows by adding a WHERE deleted_at IS NULL
predicate (i.e., remove the existing uniq_provider_endpoints_vendor_type_url and
create a unique index on ("vendor_id","provider_type","url") WITH the WHERE
deleted_at IS NULL condition so soft-deleted endpoints no longer block
recreating identical records).
Regenerates the 0054 migration and adds conditional backfill/insert logic so migrations succeed when provider_vendors has a NOT NULL vendor_key column.
🧪 测试结果
总体结果: ✅ 所有测试通过 |
🧪 测试结果
总体结果: ✅ 所有测试通过 |
🧪 测试结果
总体结果: ✅ 所有测试通过 |
🧪 测试结果
总体结果: ✅ 所有测试通过 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/app/`[locale]/settings/providers/_components/provider-vendor-view.tsx:
- Around line 494-496: formatDistanceToNow is called without a locale, so
user-visible relative times always render in English; update the call in
provider-vendor-view.tsx (where formatDistanceToNow(new
Date(endpoint.lastProbedAt), ... ) is used) to pass a date-fns locale object
mapped from the app locale. Retrieve the current app locale (e.g., via the
existing useLocale/useRouter or a currentLocale prop), map it to the
corresponding date-fns locale (using your app's date-fns locale map or add one),
then call formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix:
true, locale: mappedDateFnsLocale }). Ensure you import the mapped locale
objects or mapping util and use the same locale source used elsewhere for i18n.
♻️ Duplicate comments (5)
tests/unit/repository/provider-endpoints.test.ts (1)
19-253: 测试覆盖率仍未达到 80% 要求。当前测试仅覆盖 4 个函数(
ensureProviderEndpointExistsForUrl、backfillProviderEndpointsFromProviders、tryDeleteProviderVendorIfEmpty、deleteProviderVendor),但provider-endpoints.ts导出了 10+ 个函数仍缺少测试覆盖:
getOrCreateProviderVendorIdFromUrlsfindProviderVendors/findProviderVendorById/findProviderEndpointByIdfindProviderEndpointsByVendorAndTypecreateProviderEndpoint/updateProviderEndpoint/softDeleteProviderEndpointupdateProviderVendorfindProviderEndpointProbeLogsrecordProviderEndpointProbeResult根据编码指南,新功能单元测试覆盖率需要达到 80%。
src/actions/provider-endpoints.ts (2)
695-700: 同样存在英文错误消息不一致问题。Line 698 使用英文
"Vendor not found or could not be deleted"。建议的修改
if (!ok) { return { ok: false, - error: "Vendor not found or could not be deleted", + error: "供应商不存在或无法删除", errorCode: ERROR_CODES.DELETE_FAILED, }; }
658-664: 错误消息语言不一致。Line 661 使用英文
"Vendor not found",而其他错误消息使用中文(如 Line 260"端点不存在")。建议保持一致,使用中文或都改为英文(需要统一重构)。建议的修改(保持与其他消息一致)
if (!vendor) { return { ok: false, - error: "Vendor not found", + error: "供应商不存在", errorCode: ERROR_CODES.NOT_FOUND, }; }src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (2)
264-286: 缺少供应商类型且按钮文本未使用 i18n
providerTypes数组缺少"claude-auth"和"gemini-cli"类型,且第 284 行直接渲染{type}字符串而未使用翻译函数,违反了 i18n 规范。
528-534: 建议使用 AlertDialog 替代window.confirm()当前使用原生
confirm()对话框,与应用其他部分的 UI 风格不一致。建议使用AlertDialog组件以保持一致性,如同DeleteVendorDialog的实现方式。
🧹 Nitpick comments (7)
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx (1)
679-684: 建议:编辑和克隆操作也应失效 vendor 缓存。根据 PR 描述,
provider_vendor_id是根据 URL 域名自动关联的。当编辑 provider(尤其是 URL 变更)或克隆创建新 provider 时,vendor 关联可能会变化。建议在 edit 和 clone dialog 的onSuccess中也加上provider-vendors缓存失效,保持一致性。建议的修改
onSuccess={() => { setOpenEdit(false); queryClient.invalidateQueries({ queryKey: ["providers"] }); queryClient.invalidateQueries({ queryKey: ["providers-health"] }); + queryClient.invalidateQueries({ queryKey: ["provider-vendors"] }); router.refresh(); }}克隆 dialog(约第 700 行)也需要类似修改。
messages/ru/settings/providers/form/urlPreview.json (1)
9-19: 建议:确认俄语端点标签是否需要翻译。俄语本地化文件中
endpoints的所有标签均保留英文,而 zh-CN 和 zh-TW 版本则对部分标签进行了翻译(如 "Claude 消息"、"Gemini 生成内容")。如果这是有意为之(保持 API 术语一致性),则无需更改。否则,建议参考中文版本的翻译策略,对用户友好的标签进行俄语本地化。
src/repository/provider.ts (1)
579-581: 供应商清理调用可考虑添加防御性 try-catch虽然
tryDeleteProviderVendorIfEmpty内部已有 try-catch,但作为防御性编程,建议在调用处也添加错误处理,避免意外抛出影响主流程。🔧 可选的防御性改进
if (previousVendorId && transformed.providerVendorId !== previousVendorId) { - await tryDeleteProviderVendorIfEmpty(previousVendorId); + try { + await tryDeleteProviderVendorIfEmpty(previousVendorId); + } catch { + // Cleanup failure should not affect provider update result + } }src/actions/providers.ts (1)
740-761:removeProvider删除前获取 provider 存在潜在问题。当前实现在
deleteProvider之前获取 provider 信息,但如果findProviderById返回null(provider 不存在),deleteProvider仍会执行。这可能导致:
- 对不存在的 provider 执行删除操作(虽然不会报错,但语义上不一致)
- 自动清理逻辑因
provider为null而被跳过,但用户可能预期删除操作失败建议:在
findProviderById返回null时提前返回错误。建议的修改
export async function removeProvider(providerId: number): Promise<ActionResult> { try { const session = await getSession(); if (!session || session.user.role !== "admin") { return { ok: false, error: "无权限执行此操作" }; } const provider = await findProviderById(providerId); + if (!provider) { + return { ok: false, error: "供应商不存在" }; + } + await deleteProvider(providerId); // 清除内存缓存(无论 Redis 是否成功都要执行) clearConfigCache(providerId); await clearProviderState(providerId); // ... rest of the functionsrc/actions/provider-endpoints.ts (1)
293-312:removeProviderEndpoint中存在重复的 NOT_FOUND 检查。Line 293-300 先检查 endpoint 是否存在,Line 302-309 在
softDeleteProviderEndpoint返回 false 时再次返回 NOT_FOUND。由于第一次检查已确保 endpoint 存在,第二次检查理论上不会触发(除非存在并发删除)。代码逻辑是正确的(防御性编程),但可以考虑简化错误消息或添加注释说明。
tests/unit/actions/provider-endpoints.test.ts (1)
67-202: 考虑补充边缘情况的测试覆盖当前测试覆盖了主要的成功路径和部分错误处理。建议补充以下场景的测试以提高覆盖率:
editProviderVendor: 验证updateProviderVendor返回null时的NOT_FOUND响应removeProviderVendor: 验证deleteProviderVendor返回false时的DELETE_FAILED响应removeProviderEndpoint: 验证findProviderEndpointById返回null时的NOT_FOUND响应根据编码规范,新功能需要至少 80% 的单元测试覆盖率。
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (1)
88-89: 考虑为healthStatus和statistics定义具体类型当前使用
Record<number, any>类型,建议定义更具体的接口类型以提高类型安全性和代码可读性。
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to Reviews > Disable Cache setting
📒 Files selected for processing (27)
drizzle/0055_familiar_shatterstar.sqldrizzle/meta/0055_snapshot.jsondrizzle/meta/_journal.jsonmessages/en/settings/providers/form/urlPreview.jsonmessages/en/settings/providers/strings.jsonmessages/ja/settings/providers/form/urlPreview.jsonmessages/ja/settings/providers/strings.jsonmessages/ru/settings/providers/form/urlPreview.jsonmessages/ru/settings/providers/strings.jsonmessages/zh-CN/settings/providers/form/urlPreview.jsonmessages/zh-CN/settings/providers/strings.jsonmessages/zh-TW/settings/providers/form/urlPreview.jsonmessages/zh-TW/settings/providers/strings.jsonsrc/actions/provider-endpoints.tssrc/actions/providers.tssrc/app/[locale]/settings/providers/_components/forms/url-preview.tsxsrc/app/[locale]/settings/providers/_components/provider-rich-list-item.tsxsrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsxsrc/app/v1/_lib/url.tssrc/drizzle/schema.tssrc/repository/index.tssrc/repository/provider-endpoints.tssrc/repository/provider.tssrc/types/provider.tstests/unit/actions/provider-endpoints.test.tstests/unit/app/v1/url.test.tstests/unit/repository/provider-endpoints.test.ts
✅ Files skipped from review due to trivial changes (1)
- drizzle/meta/0055_snapshot.json
🚧 Files skipped from review as they are similar to previous changes (2)
- drizzle/meta/_journal.json
- src/types/provider.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{js,ts,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Never use emoji characters in any code, comments, or string literals
Files:
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsxtests/unit/app/v1/url.test.tssrc/app/[locale]/settings/providers/_components/forms/url-preview.tsxtests/unit/actions/provider-endpoints.test.tssrc/repository/provider.tstests/unit/repository/provider-endpoints.test.tssrc/repository/index.tssrc/repository/provider-endpoints.tssrc/drizzle/schema.tssrc/actions/providers.tssrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsxsrc/app/v1/_lib/url.tssrc/actions/provider-endpoints.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias@/to reference files in./src/directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width
Files:
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsxtests/unit/app/v1/url.test.tssrc/app/[locale]/settings/providers/_components/forms/url-preview.tsxtests/unit/actions/provider-endpoints.test.tssrc/repository/provider.tstests/unit/repository/provider-endpoints.test.tssrc/repository/index.tssrc/repository/provider-endpoints.tssrc/drizzle/schema.tssrc/actions/providers.tssrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsxsrc/app/v1/_lib/url.tssrc/actions/provider-endpoints.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer named exports over default exports
Files:
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsxtests/unit/app/v1/url.test.tssrc/app/[locale]/settings/providers/_components/forms/url-preview.tsxtests/unit/actions/provider-endpoints.test.tssrc/repository/provider.tstests/unit/repository/provider-endpoints.test.tssrc/repository/index.tssrc/repository/provider-endpoints.tssrc/drizzle/schema.tssrc/actions/providers.tssrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsxsrc/app/v1/_lib/url.tssrc/actions/provider-endpoints.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
All new features must have unit test coverage of at least 80%
Files:
tests/unit/app/v1/url.test.tstests/unit/actions/provider-endpoints.test.tstests/unit/repository/provider-endpoints.test.ts
tests/**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Vitest for unit testing and happy-dom for DOM testing
Files:
tests/unit/app/v1/url.test.tstests/unit/actions/provider-endpoints.test.tstests/unit/repository/provider-endpoints.test.ts
src/repository/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Drizzle ORM for data access in the repository layer
Files:
src/repository/provider.tssrc/repository/index.tssrc/repository/provider-endpoints.ts
src/drizzle/schema.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Modify database schema in
src/drizzle/schema.ts, then runbun run db:generateto generate migrations. Never create SQL migration files manually
Files:
src/drizzle/schema.ts
🧠 Learnings (10)
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (e.g., matches DB schema or special UI logic) and undefined signifies 'inherit default'. This pattern should be consistently reflected in type definitions across related fields to preserve semantic clarity between database constraints and UI behavior.
Applied to files:
src/repository/provider.tssrc/repository/index.tssrc/repository/provider-endpoints.tssrc/drizzle/schema.tssrc/actions/providers.tssrc/app/v1/_lib/url.tssrc/actions/provider-endpoints.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : All new features must have unit test coverage of at least 80%
Applied to files:
tests/unit/repository/provider-endpoints.test.ts
📚 Learning: 2026-01-05T03:02:14.502Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx:66-66
Timestamp: 2026-01-05T03:02:14.502Z
Learning: In the claude-code-hub project, the translations.actions.addKey field in UserKeyTableRowProps is defined as optional for backward compatibility, but all actual callers in the codebase provide the complete translations object. The field has been added to all 5 locale files (messages/{locale}/dashboard.json).
Applied to files:
messages/zh-CN/settings/providers/form/urlPreview.json
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/repository/**/*.{ts,tsx} : Use Drizzle ORM for data access in the repository layer
Applied to files:
src/repository/provider-endpoints.tssrc/drizzle/schema.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/drizzle/schema.ts : Modify database schema in `src/drizzle/schema.ts`, then run `bun run db:generate` to generate migrations. Never create SQL migration files manually
Applied to files:
src/drizzle/schema.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/app/v1/_lib/proxy/**/*.{ts,tsx} : The proxy pipeline processes requests through a GuardPipeline with sequential guards: auth, sensitive, client, model, version, probe, session, warmup, requestFilter, rateLimit, provider, providerRequestFilter, messageContext
Applied to files:
src/actions/providers.tssrc/app/v1/_lib/url.ts
📚 Learning: 2026-01-10T06:19:58.167Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:19:58.167Z
Learning: Do not modify hardcoded Chinese error messages in Server Actions under src/actions/*.ts as part of piecemeal changes. This is a repo-wide architectural decision that requires a coordinated i18n refactor across all Server Action files (e.g., model-prices.ts, users.ts, system-config.ts). Treat i18n refactor as a separate unified task rather than per-PR changes, and plan a project-wide approach for replacing hardcoded strings with localized resources.
Applied to files:
src/actions/providers.tssrc/actions/provider-endpoints.ts
📚 Learning: 2026-01-10T06:20:32.687Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx:118-125
Timestamp: 2026-01-10T06:20:32.687Z
Learning: In `src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx`, the "Cancel" button in the SyncConflictDialog is intentionally designed to call `onConfirm([])`, which triggers `doSync([])` to continue the sync while skipping (not overwriting) conflicting manual prices. This is the desired product behavior to allow users to proceed with LiteLLM sync for non-conflicting models while preserving their manual price entries.
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T06:20:04.478Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:20:04.478Z
Learning: In the `ding113/claude-code-hub` repository, Server Actions (files under `src/actions/*.ts`) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsxsrc/actions/provider-endpoints.ts
🧬 Code graph analysis (9)
tests/unit/app/v1/url.test.ts (1)
src/app/v1/_lib/url.ts (1)
buildProxyUrl(25-78)
src/app/[locale]/settings/providers/_components/forms/url-preview.tsx (1)
src/lib/utils/clipboard.ts (1)
copyToClipboard(18-30)
tests/unit/actions/provider-endpoints.test.ts (1)
src/actions/provider-endpoints.ts (3)
editProviderVendor(613-672)removeProviderVendor(674-718)removeProviderEndpoint(273-320)
src/repository/provider.ts (4)
src/types/provider.ts (2)
CreateProviderData(235-303)Provider(39-144)src/repository/provider-endpoints.ts (3)
getOrCreateProviderVendorIdFromUrls(105-152)ensureProviderEndpointExistsForUrl(412-446)tryDeleteProviderVendorIfEmpty(281-332)src/drizzle/schema.ts (1)
providers(149-295)src/repository/_shared/transformers.ts (1)
toProvider(66-119)
tests/unit/repository/provider-endpoints.test.ts (1)
src/repository/provider-endpoints.ts (4)
ensureProviderEndpointExistsForUrl(412-446)backfillProviderEndpointsFromProviders(448-530)tryDeleteProviderVendorIfEmpty(281-332)deleteProviderVendor(263-279)
src/repository/provider-endpoints.ts (2)
src/types/provider.ts (4)
ProviderVendor(375-384)ProviderEndpoint(388-405)ProviderType(6-12)ProviderEndpointProbeSource(386-386)src/drizzle/schema.ts (3)
providerVendors(132-146)providerEndpoints(298-340)providerEndpointProbeLogs(343-364)
src/drizzle/schema.ts (1)
src/types/provider.ts (1)
ProviderType(6-12)
src/actions/providers.ts (3)
src/repository/provider.ts (1)
findProviderById(312-372)src/repository/provider-endpoints.ts (1)
tryDeleteProviderVendorIfEmpty(281-332)src/app/v1/_lib/url.ts (1)
buildProxyUrl(25-78)
src/app/v1/_lib/url.ts (2)
src/lib/logger.ts (1)
logger(168-187)scripts/sync-settings-keys.js (2)
path(15-15)key(32-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: 🌐 API Tests
- GitHub Check: Docker Build Test
- GitHub Check: dev-build-deploy
🔇 Additional comments (46)
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx (1)
177-180: LGTM!删除操作的缓存失效逻辑正确。删除 provider 时同步失效
provider-vendors缓存是合理的,因为 vendor 可能需要根据关联的 provider 变化进行更新。drizzle/0055_familiar_shatterstar.sql (1)
1-1: LGTM!迁移语句简洁正确。使用
DEFAULT false NOT NULL确保现有记录自动填充默认值,且新记录必须有值。这是添加布尔标志列的标准做法。messages/zh-CN/settings/providers/form/urlPreview.json (1)
8-19: LGTM!新增的
endpoints对象结构正确,翻译质量良好。保留 OpenAI、Codex 等产品名称为英文是合理的做法,有助于用户识别。messages/zh-TW/settings/providers/form/urlPreview.json (1)
8-19: LGTM!繁體中文翻譯準確,使用了適當的地區用語(如「訊息」、「產生」、「串流」),與簡體中文版本保持了一致的結構但有正確的詞彙差異。
messages/en/settings/providers/form/urlPreview.json (1)
8-19: LGTM!英文版本作为基准本地化文件,
endpoints对象的标签清晰、描述性强。src/app/[locale]/settings/providers/_components/forms/url-preview.tsx (1)
112-157: LGTM! 基于 endpoint key 的 i18n 实现正确。使用
endpointKey作为翻译键 (t(\endpoints.${endpointKey}`)) 并作为 React 列表的key,符合 i18n 最佳实践。本地定义的copyToClipboard` 函数(第 66 行)正确接受两个参数,与调用处一致。messages/ja/settings/providers/form/urlPreview.json (1)
9-19: LGTM! 本地化条目已正确添加。JSON 语法正确,翻译键与
url.ts中的 endpoint key 对应。技术性 API 名称(如 "Codex Responses"、"OpenAI Chat Completions")保留英文在日语 UI 中是常见做法,可以接受。messages/zh-CN/settings/providers/strings.json (1)
46-97: LGTM! 完整的简体中文本地化。新增的端点管理、探测、熔断器等功能的翻译键完整,术语使用一致(服务商、端点、熔断),占位符格式正确。
messages/en/settings/providers/strings.json (1)
46-97: LGTM! 英文本地化与中文结构一致。新增的本地化键与 zh-CN 文件对应,术语清晰(endpoints、vendor、circuit breaker),占位符格式一致。
src/app/v1/_lib/url.ts (4)
32-38: Case 1 逻辑正确。当
requestPath已经是basePath的扩展时(例如 base=/v1/messages,req=/v1/messages/count_tokens),直接使用requestPath是正确的做法,避免了路径重复。
48-66: Case 2 端点根路径检测逻辑完善。同时检测
basePath.endsWith(endpoint)和basePath.endsWith(requestRoot)覆盖了两种配置场景(/messages和/v1/messages),后缀提取使用requestRoot.length确保只追加子路径部分。
100-135: 端点映射重构为 key-based 结构,支持 i18n。从
{ name, path }改为{ key, path }结构是正确的设计,key作为翻译键由 UI 层负责国际化,与url-preview.tsx中的t(\endpoints.${endpointKey}`)` 调用一致。
141-154: 默认端点回退逻辑合理。当
providerType未匹配到任何已知类型时,回退到常见端点(Claude、Codex、OpenAI),确保 UI 预览始终有内容展示。循环中的解构{ key, path }与类型定义一致。tests/unit/app/v1/url.test.ts (1)
1-57: 测试覆盖良好,LGTM!测试用例覆盖了
buildProxyUrl的核心场景:
- 标准路径拼接
- 避免重复路径段
- 子路径保留
- 带前缀的 baseUrl 处理
- query 参数覆盖
建议考虑添加以下边缘场景测试(可后续补充):
- 无效 URL 的降级处理(触发 catch 分支)
/chat/completions和/models端点的处理src/drizzle/schema.ts (5)
131-146: providerVendors 表设计合理表结构清晰,使用
websiteDomain作为唯一标识符,支持供应商元数据存储。建议:
isOfficial字段的业务语义建议在代码注释中补充说明(什么情况下标记为官方供应商)。
155-157: 外键约束设计合理使用
onDelete: 'restrict'可防止误删有关联 provider 的 vendor,保护数据完整性。这与tryDeleteProviderVendorIfEmpty的清理逻辑配合良好。
297-340: providerEndpoints 表设计良好
- 复合唯一索引
(vendorId, providerType, url)确保端点不重复lastProbe*字段反规范化设计合理,避免频繁关联查询探测日志表- 软删除支持便于数据恢复和审计
342-364: 探测日志表设计简洁有效
onDelete: 'cascade'适合日志数据,端点删除时自动清理(endpointId, createdAt DESC)索引优化了按端点查询最新日志的场景- 无
deletedAt字段是正确的选择,日志作为只追加的历史记录
721-747: 关系定义完整正确所有新增的关系定义遵循了现有模式,双向关系配置正确,支持 Drizzle 的关系查询功能。
src/repository/provider-endpoints.ts (7)
20-29: 日期处理辅助函数设计合理
toDate和toNullableDate提供了安全的日期转换,处理了多种输入类型。小建议:
toDate在无法解析时返回new Date()可能导致静默错误,考虑是否需要记录警告日志。
31-52: URL 规范化逻辑健壮函数正确处理了多种 URL 格式:
- 缺少协议的 URL 自动补充
https://- 移除
www.前缀保证域名一致性- 解析失败时返回
null而非抛出异常
105-152: 并发安全的 upsert 实现
onConflictDoNothing+ 回退查询的模式正确处理了并发创建场景,避免了竞态条件导致的重复插入或失败。
263-279: 硬删除逻辑正确使用事务事务保证了删除操作的原子性。由于
providers.providerVendorId使用onDelete: 'restrict',必须先删除 providers 才能删除 vendor。注意:此函数会硬删除所有关联的 providers,使用时需谨慎。建议在调用层添加确认逻辑或仅在特定场景(如测试数据清理)使用。
281-332: 条件删除逻辑设计周全多重检查确保只在真正"空"的情况下删除 vendor:
- 检查活跃 providers
- 检查活跃 endpoints
- 清理软删除的 providers(解除 FK 约束)
- 使用
NOT EXISTS子句双重验证错误处理返回
false而非抛出异常,适合自动清理场景。
448-530: 批量回填实现高效且安全
- 分页读取(1000)+ 批量写入(500)平衡了内存和数据库压力
- 内存去重避免重复冲突处理
- URL 验证确保数据质量
- 返回统计信息便于监控迁移进度
循环在
rows.length === 0时正确终止。
611-648: 探测结果记录逻辑正确事务确保日志插入和端点快照更新的一致性。成功时正确清除错误字段(
lastProbeErrorType和lastProbeErrorMessage设为null)。src/repository/provider.ts (3)
17-23: 创建 Provider 时正确初始化 providerVendorId在插入前通过
getOrCreateProviderVendorIdFromUrls确保 vendor 存在,避免 FK 约束失败。使用displayName作为 vendor 的显示名称是合理的默认行为。
136-148: 端点种子化的容错设计合理端点创建失败不阻塞 provider 创建是正确的选择。日志记录了足够的上下文信息便于排查问题。
471-495: URL 变更时正确更新 providerVendorId当
url或website_url变更时,重新计算providerVendorId确保供应商关联正确。保存previousVendorId用于后续清理是好的设计。src/repository/index.ts (1)
47-61: 导出列表完整且组织良好新增的 provider-endpoints 导出涵盖了 vendor 和 endpoint 的完整 CRUD 操作,按字母顺序排列便于维护。
tests/unit/repository/provider-endpoints.test.ts (3)
3-17:createThenableQuery辅助函数实现正确。该辅助函数正确模拟了 Drizzle ORM 的链式查询构建器模式,使
await db.select().from().where()...调用能够返回预设结果。
255-391:tryDeleteProviderVendorIfEmpty测试用例全面覆盖了各种场景。测试覆盖了:
- 有 active provider 时不删除
- 有 active endpoint 时不删除
- 无 active provider/endpoint 时成功删除
- vendor 不存在时返回 false
- transaction 抛错时返回 false
事务模拟和级联删除逻辑的验证都很充分。
412-486:deleteProviderVendor测试逻辑正确。测试正确验证了级联删除的顺序(endpoints → providers → vendor)和返回值逻辑。
src/actions/providers.ts (4)
5-5: 新增buildProxyUrl导入用于 URL 构建。该导入在
executeProviderApiTest函数中使用(Line 2129),用于安全地构建代理 URL,替代简单的字符串拼接。
47-47: 新增tryDeleteProviderVendorIfEmpty导入支持自动清理逻辑。该导入在
removeProvider中使用,用于在删除 provider 后自动清理孤立的 vendor 记录。
241-241: 正确暴露providerVendorId字段。该字段与
ProviderDisplay类型定义保持一致,支持前端按 vendor 分组展示 provider。
2129-2129: 使用buildProxyUrl构建 URL 是正确的做法。相比直接字符串拼接,
buildProxyUrl能正确处理路径前缀、查询参数等边界情况,避免产生无效 URL。src/actions/provider-endpoints.ts (4)
1-38: 导入和类型定义结构清晰。正确使用 Zod 进行输入验证,通过
@/lib/utils/zod-i18n提供的辅助函数支持 i18n 错误消息。类型导入与 repository 层一致。
40-123: Zod Schema 定义规范。
ProviderTypeSchema覆盖了所有支持的 provider 类型CreateProviderEndpointSchema正确使用.trim()和.url()验证UpdateProviderEndpointSchema使用.refine()确保至少有一个字段更新ProbeProviderEndpointSchema限制 timeout 范围合理(1s-120s)
125-131:getAdminSession辅助函数减少了重复代码。统一的 admin 权限检查逻辑,提高了代码可维护性。
703-710: 缓存失效逻辑正确处理了异常。使用 try-catch 包裹
publishProviderCacheInvalidation调用,确保缓存失效失败不会阻塞主流程,符合非关键路径的处理模式。messages/ja/settings/providers/strings.json (1)
46-97: 日语本地化键添加完整。新增的 i18n 键覆盖了 provider endpoints 和 vendor 管理 UI 所需的所有字符串,翻译质量良好,术语使用一致(如 "エンドポイント"、"ベンダー"、"回路")。
messages/ru/settings/providers/strings.json (1)
46-97: 俄语本地化键添加完整。新增的 i18n 键与其他语言文件(en、ja、zh-CN、zh-TW)保持一致,技术术语的俄语翻译/音译合理(如 "эндпоинт"、"вендор"),符合俄语技术文档惯例。
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (1)
811-896: 双重确认删除供应商的实现很好
DeleteVendorDialog使用AlertDialog并实现了两步确认流程,这是删除敏感数据的良好 UX 实践。messages/zh-TW/settings/providers/strings.json (1)
46-97: 本地化字符串完整覆盖了新功能新增的 zh-TW 翻译键完整覆盖了端点管理、供应商编辑/删除、熔断状态等新功能所需的所有用户界面文本,术语使用一致。
tests/unit/actions/provider-endpoints.test.ts (1)
13-15: Mock setup is correct and sufficientThe mock properly covers the auth imports. The
getAdminSessionis not a separate export from@/lib/authbut a local helper function within the action file that internally callsgetSession(), which is correctly mocked.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| <span className="text-muted-foreground text-[10px]"> | ||
| {formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true })} | ||
| </span> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
formatDistanceToNow 缺少 locale 参数,无法支持多语言
formatDistanceToNow 输出的时间字符串(如 "5 minutes ago")是用户可见内容,但未传入 locale 参数,导致始终显示英文。根据编码规范,所有用户可见字符串必须支持 i18n(5 种语言)。
建议修复
+import { useLocale } from "next-intl";
+import { zhCN, zhTW, ja, ru, enUS } from "date-fns/locale";
+const localeMap: Record<string, Locale> = {
+ "zh-CN": zhCN,
+ "zh-TW": zhTW,
+ ja: ja,
+ ru: ru,
+ en: enUS,
+};
// 在 EndpointRow 组件内
+const locale = useLocale();
+const dateLocale = localeMap[locale] || enUS;
-{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true })}
+{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true, locale: dateLocale })}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <span className="text-muted-foreground text-[10px]"> | |
| {formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true })} | |
| </span> | |
| <span className="text-muted-foreground text-[10px]"> | |
| {formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true, locale: dateLocale })} | |
| </span> |
🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/provider-vendor-view.tsx
around lines 494 - 496, formatDistanceToNow is called without a locale, so
user-visible relative times always render in English; update the call in
provider-vendor-view.tsx (where formatDistanceToNow(new
Date(endpoint.lastProbedAt), ... ) is used) to pass a date-fns locale object
mapped from the app locale. Retrieve the current app locale (e.g., via the
existing useLocale/useRouter or a currentLocale prop), map it to the
corresponding date-fns locale (using your app's date-fns locale map or add one),
then call formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix:
true, locale: mappedDateFnsLocale }). Ensure you import the mapped locale
objects or mapping util and use the same locale source used elsewhere for i18n.
🧪 测试结果
总体结果: ✅ 所有测试通过 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@src/actions/provider-endpoints.ts`:
- Around line 362-376: The returned endpoint is stale because
probeProviderEndpointAndRecord updates DB fields but you return the pre-probe
endpoint; after calling probeProviderEndpointAndRecord, re-fetch the endpoint by
id (e.g., call the repository/ORM method used elsewhere to load a provider
endpoint by endpoint.id or a function like getProviderEndpointById/getById) and
return that refreshed record instead (return { ok: true, data: { endpoint:
refreshedEndpoint, result } }); also handle the case where the re-fetched
endpoint is missing and return a NOT_FOUND error similarly.
♻️ Duplicate comments (3)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (3)
523-529: 使用window.confirm不符合 UI 一致性删除确认使用原生
confirm()对话框,与应用其他地方使用的AlertDialog组件风格不一致。参考同文件中DeleteVendorDialog的实现方式。建议:替换为 AlertDialog 组件
将删除操作改为使用
AlertDialog,与DeleteVendorDialog保持一致的交互模式。
259-280: 供应商类型列表不完整,按钮标签未使用 i18n
providerTypes数组缺少"claude-auth"和"gemini-cli",且按钮直接渲染{type}字符串违反了 i18n 要求。建议修复
-const providerTypes: ProviderType[] = ["claude", "codex", "gemini", "openai-compatible"]; +const providerTypes: ProviderType[] = ["claude", "claude-auth", "codex", "gemini", "gemini-cli", "openai-compatible"]; {providerTypes.map((type) => ( <Button key={type} variant={activeType === type ? "default" : "ghost"} size="sm" onClick={() => setActiveType(type)} className="h-7 text-xs capitalize" > - {type} + {t(`providerTypes.${type.replace(/-/g, "")}`)} </Button> ))}
489-491:formatDistanceToNow缺少 locale 参数相对时间显示(如 "5 minutes ago")始终为英文,不支持多语言。
建议修复:添加 date-fns locale 支持
+import { useLocale } from "next-intl"; +import { zhCN, zhTW, ja, ru, enUS } from "date-fns/locale"; +const localeMap: Record<string, Locale> = { + "zh-CN": zhCN, + "zh-TW": zhTW, + ja, + ru, + en: enUS, +}; // 在 EndpointRow 组件内 +const locale = useLocale(); +const dateLocale = localeMap[locale] || enUS; -{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true })} +{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true, locale: dateLocale })}
🧹 Nitpick comments (3)
src/actions/provider-endpoints.ts (1)
447-454: Schema 复用降低可读性
getEndpointCircuitInfo和resetEndpointCircuit复用了DeleteProviderEndpointSchema,虽然功能正确(两者都只需要endpointId),但语义上容易造成混淆。建议:创建专用 schema 提升可读性
+const EndpointIdOnlySchema = z.object({ + endpointId: EndpointIdSchema, +}); // 在 getEndpointCircuitInfo 和 resetEndpointCircuit 中使用 -const parsed = DeleteProviderEndpointSchema.safeParse(input); +const parsed = EndpointIdOnlySchema.safeParse(input);src/repository/provider-endpoints.ts (1)
605-642: recordProviderEndpointProbeResult 事务使用正确在同一事务中完成 probe log 插入和 endpoint 快照更新,保证数据一致性。
注意 Line 638
updatedAt: new Date()在事务内部再次调用new Date(),虽然时间差异极小,但为保持一致性,建议复用probedAt。♻️ 复用 probedAt 保持时间一致
await tx .update(providerEndpoints) .set({ lastProbedAt: probedAt, lastProbeOk: input.ok, lastProbeStatusCode: input.statusCode ?? null, lastProbeLatencyMs: input.latencyMs ?? null, lastProbeErrorType: input.ok ? null : (input.errorType ?? null), lastProbeErrorMessage: input.ok ? null : (input.errorMessage ?? null), - updatedAt: new Date(), + updatedAt: probedAt, })tests/unit/actions/provider-endpoints.test.ts (1)
1-199: 建议增加更多边界情况测试当前测试覆盖了主要的成功路径,建议补充以下场景:
editProviderVendor当 vendor 不存在时返回 NOT_FOUNDremoveProviderEndpoint当 endpoint 不存在时返回 NOT_FOUND- Zod 验证失败的情况(如 vendorId 为负数)
根据 coding guidelines,新功能应有至少 80% 的测试覆盖率。
💡 建议补充的测试用例
it("editProviderVendor: returns NOT_FOUND when vendor does not exist", async () => { getSessionMock.mockResolvedValue({ user: { role: "admin" } }); updateProviderVendorMock.mockResolvedValue(null); const { editProviderVendor } = await import("@/actions/provider-endpoints"); const res = await editProviderVendor({ vendorId: 999, displayName: "x" }); expect(res.ok).toBe(false); expect(res.errorCode).toBe("NOT_FOUND"); }); it("removeProviderEndpoint: returns NOT_FOUND when endpoint does not exist", async () => { getSessionMock.mockResolvedValue({ user: { role: "admin" } }); findProviderEndpointByIdMock.mockResolvedValue(null); const { removeProviderEndpoint } = await import("@/actions/provider-endpoints"); const res = await removeProviderEndpoint({ endpointId: 999 }); expect(res.ok).toBe(false); expect(res.errorCode).toBe("NOT_FOUND"); });
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to Reviews > Disable Cache setting
📒 Files selected for processing (10)
drizzle/0056_colorful_nightshade.sqldrizzle/meta/0056_snapshot.jsondrizzle/meta/_journal.jsonsrc/actions/provider-endpoints.tssrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsxsrc/drizzle/schema.tssrc/lib/circuit-breaker-probe.tssrc/repository/provider-endpoints.tssrc/types/provider.tstests/unit/actions/provider-endpoints.test.ts
✅ Files skipped from review due to trivial changes (1)
- drizzle/0056_colorful_nightshade.sql
🚧 Files skipped from review as they are similar to previous changes (1)
- drizzle/meta/_journal.json
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{js,ts,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Never use emoji characters in any code, comments, or string literals
Files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsxtests/unit/actions/provider-endpoints.test.tssrc/lib/circuit-breaker-probe.tssrc/types/provider.tssrc/repository/provider-endpoints.tssrc/drizzle/schema.tssrc/actions/provider-endpoints.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias@/to reference files in./src/directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width
Files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsxtests/unit/actions/provider-endpoints.test.tssrc/lib/circuit-breaker-probe.tssrc/types/provider.tssrc/repository/provider-endpoints.tssrc/drizzle/schema.tssrc/actions/provider-endpoints.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer named exports over default exports
Files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsxtests/unit/actions/provider-endpoints.test.tssrc/lib/circuit-breaker-probe.tssrc/types/provider.tssrc/repository/provider-endpoints.tssrc/drizzle/schema.tssrc/actions/provider-endpoints.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
All new features must have unit test coverage of at least 80%
Files:
tests/unit/actions/provider-endpoints.test.ts
tests/**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Vitest for unit testing and happy-dom for DOM testing
Files:
tests/unit/actions/provider-endpoints.test.ts
src/repository/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Drizzle ORM for data access in the repository layer
Files:
src/repository/provider-endpoints.ts
src/drizzle/schema.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Modify database schema in
src/drizzle/schema.ts, then runbun run db:generateto generate migrations. Never create SQL migration files manually
Files:
src/drizzle/schema.ts
🧠 Learnings (9)
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/drizzle/schema.ts : Modify database schema in `src/drizzle/schema.ts`, then run `bun run db:generate` to generate migrations. Never create SQL migration files manually
Applied to files:
drizzle/meta/0056_snapshot.jsonsrc/drizzle/schema.ts
📚 Learning: 2026-01-10T06:20:32.687Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx:118-125
Timestamp: 2026-01-10T06:20:32.687Z
Learning: In `src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx`, the "Cancel" button in the SyncConflictDialog is intentionally designed to call `onConfirm([])`, which triggers `doSync([])` to continue the sync while skipping (not overwriting) conflicting manual prices. This is the desired product behavior to allow users to proceed with LiteLLM sync for non-conflicting models while preserving their manual price entries.
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T06:20:04.478Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:20:04.478Z
Learning: In the `ding113/claude-code-hub` repository, Server Actions (files under `src/actions/*.ts`) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsxsrc/actions/provider-endpoints.ts
📚 Learning: 2026-01-10T06:20:19.207Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: messages/ja/dashboard.json:1342-1345
Timestamp: 2026-01-10T06:20:19.207Z
Learning: In this project, minor i18n formatting issues (such as spacing between numbers and units in Japanese/Chinese) are accepted when fixing them would require adding template complexity. The approach prioritizes code simplicity over perfect locale-specific formatting for minor cosmetic issues.
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Use `next-intl` for internationalization with support for 5 languages: zh-CN, zh-TW, en, ja, ru
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (e.g., matches DB schema or special UI logic) and undefined signifies 'inherit default'. This pattern should be consistently reflected in type definitions across related fields to preserve semantic clarity between database constraints and UI behavior.
Applied to files:
src/lib/circuit-breaker-probe.tssrc/types/provider.tssrc/repository/provider-endpoints.tssrc/drizzle/schema.tssrc/actions/provider-endpoints.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/repository/**/*.{ts,tsx} : Use Drizzle ORM for data access in the repository layer
Applied to files:
src/repository/provider-endpoints.tssrc/drizzle/schema.ts
📚 Learning: 2026-01-10T06:19:58.167Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:19:58.167Z
Learning: Do not modify hardcoded Chinese error messages in Server Actions under src/actions/*.ts as part of piecemeal changes. This is a repo-wide architectural decision that requires a coordinated i18n refactor across all Server Action files (e.g., model-prices.ts, users.ts, system-config.ts). Treat i18n refactor as a separate unified task rather than per-PR changes, and plan a project-wide approach for replacing hardcoded strings with localized resources.
Applied to files:
src/actions/provider-endpoints.ts
🧬 Code graph analysis (3)
tests/unit/actions/provider-endpoints.test.ts (1)
src/actions/provider-endpoints.ts (3)
editProviderVendor(612-670)removeProviderVendor(672-716)removeProviderEndpoint(272-319)
src/drizzle/schema.ts (1)
src/types/provider.ts (1)
ProviderType(6-12)
src/actions/provider-endpoints.ts (6)
src/types/provider.ts (4)
ProviderVendor(375-383)ProviderType(6-12)ProviderEndpoint(387-404)ProviderEndpointProbeLog(406-416)src/lib/logger.ts (1)
logger(168-187)src/lib/utils/zod-i18n.ts (2)
formatZodError(154-159)extractZodErrorCode(176-190)src/lib/provider-endpoints/probe.ts (1)
probeProviderEndpointAndRecord(104-129)src/lib/endpoint-circuit-breaker.ts (1)
getEndpointHealthInfo(109-114)src/lib/vendor-type-circuit-breaker.ts (3)
getVendorTypeCircuitInfo(107-113)setVendorTypeCircuitManualOpen(157-177)resetVendorTypeCircuit(179-187)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: 🌐 API Tests
- GitHub Check: Docker Build Test
- GitHub Check: dev-build-deploy
🔇 Additional comments (19)
src/lib/circuit-breaker-probe.ts (1)
9-20: 探测间隔从 30s 缩短到 10s,LGTM更短的探测间隔可以加快熔断恢复速度,与本 PR 新增的端点探测功能配合良好。建议在高负载场景下关注探测频率对系统和上游提供商的影响。
drizzle/meta/0056_snapshot.json (1)
1-2874: 数据库快照结构正确新增的三张表 (
provider_vendors、provider_endpoints、provider_endpoint_probe_logs) 的外键约束和索引设计合理:
- 端点和探测日志使用
CASCADE删除,供应商删除时自动清理providers.provider_vendor_id使用RESTRICT,防止误删有关联的供应商- 复合唯一索引
uniq_provider_endpoints_vendor_type_url防止重复端点src/types/provider.ts (1)
374-416: 新增类型定义清晰完整
ProviderVendor、ProviderEndpoint、ProviderEndpointProbeLog接口与数据库 schema 保持一致,字段类型和可空性定义正确。src/actions/provider-endpoints.ts (1)
634-648: favicon URL 生成逻辑清晰根据
websiteUrl自动生成 Google favicon 服务 URL,并在websiteUrl被清空时同步清除 favicon,逻辑完整。src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (1)
796-880: DeleteVendorDialog 实现了良好的二次确认模式使用
AlertDialog组件配合两步确认(confirm → double-confirm),对于破坏性操作提供了良好的用户保护。src/drizzle/schema.ts (5)
131-145: LGTM! providerVendors 表设计合理表结构清晰,包含必要的元数据字段(displayName、websiteUrl、faviconUrl)。
websiteDomain上的唯一索引确保了供应商的唯一性标识。
154-156: FK 约束设计合理使用
onDelete: 'restrict'防止在有关联 providers 时误删 vendor,这是正确的数据完整性保护。注意:删除 vendor 前需要先处理关联的 providers。
296-339: providerEndpoints 表设计完善
- 探测快照字段(lastProbe*)支持快速读取最近状态,避免每次查询 probe logs
- 复合唯一索引
(vendorId, providerType, url)确保端点不重复- 部分索引使用
deletedAt IS NULL过滤软删除记录,优化查询性能
341-363: providerEndpointProbeLogs 表设计合理
- 使用
onDelete: 'cascade'确保端点删除时自动清理探测日志- 索引
(endpointId, createdAt DESC)支持按端点查询最近日志的常见场景
720-746: 关系定义完整且正确所有新表的 relations 定义正确映射了 FK 关系:
providersRelations新增 vendor 关联providerVendorsRelations包含 providers 和 endpoints 的一对多关系providerEndpointsRelations包含 vendor 和 probeLogs 的关联src/repository/provider-endpoints.ts (4)
104-151: getOrCreateProviderVendorIdFromUrls 的竞态条件处理良好实现采用了 insert-on-conflict-do-nothing + fallback select 的模式,正确处理了并发创建同一 vendor 的竞态场景。这是标准的 upsert 模式。
275-326: tryDeleteProviderVendorIfEmpty 实现严谨此函数实现了安全的条件删除逻辑:
- 检查是否存在活跃的 providers 和 endpoints
- 先清理软删除的 providers 以满足 FK restrict 约束
- 使用 NOT EXISTS 子查询确保删除时的原子性检查
这是正确的防御性实现。
442-524: backfillProviderEndpointsFromProviders 批处理设计合理
- 使用游标分页(
gt(providers.id, lastProviderId))避免大表 offset 性能问题- 内存中去重(
seenSet)减少无效插入onConflictDoNothing确保幂等性一个小建议:考虑在
flush()中添加错误处理,避免部分批次失败导致整个流程中断。
1-1: 此注释不适用于当前代码架构。
src/repository/provider-endpoints.ts中的"use server"指令是必要且合理的。所有调用者均来自服务端上下文:
- Server Actions (
src/actions/provider-endpoints.ts、src/actions/providers.ts) — 静态导入相关函数- 服务器初始化脚本 (
src/instrumentation.ts) — 动态导入backfillProviderEndpointsFromProviders- 测试文件 (
tests/unit/repository/provider-endpoints.test.ts) — 测试环境中的动态导入无任何客户端组件直接导入该文件。repository 层使用
"use server"符合标准的服务端数据访问层架构。Likely an incorrect or invalid review comment.
tests/unit/actions/provider-endpoints.test.ts (5)
67-70: beforeEach 使用 vi.clearAllMocks() 是正确做法每个测试前清理 mocks,确保测试隔离性。
82-111: 测试验证了 favicon 自动计算逻辑正确验证了当提供
websiteUrl时,action 会自动计算并传递faviconUrl到 repository 层。
142-167: 缓存失效容错测试良好Lines 156-167 验证了即使
publishProviderCacheInvalidation抛出异常,removeProviderVendor仍然返回成功。这符合 action 实现中 try-catch 的容错设计。
169-198: removeProviderEndpoint 测试覆盖了 vendor 清理流程验证了删除端点后会触发
tryDeleteProviderVendorIfEmpty,确保孤立的 vendor 被自动清理。
72-80: 模拟设置完整,无需修改
getAdminSession的实现只检查两个条件:会话是否存在且session.user.role是否为"admin"。测试中的模拟对象{ user: { role: "user" } }提供了这两个必要的字段,足以验证权限拒绝逻辑。模拟设置与实际实现相匹配。
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| const result = await probeProviderEndpointAndRecord({ | ||
| endpointId: endpoint.id, | ||
| source: "manual", | ||
| timeoutMs: parsed.data.timeoutMs, | ||
| }); | ||
|
|
||
| if (!result) { | ||
| return { | ||
| ok: false, | ||
| error: "端点不存在", | ||
| errorCode: ERROR_CODES.NOT_FOUND, | ||
| }; | ||
| } | ||
|
|
||
| return { ok: true, data: { endpoint, result } }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
探测后返回的 endpoint 数据可能是过时的
probeProviderEndpointAndRecord 会更新数据库中的 lastProbeOk、lastProbeLatencyMs 等字段,但此处返回的 endpoint 是探测前查询的数据,不包含最新的探测结果。
建议修复:探测后重新获取 endpoint
const result = await probeProviderEndpointAndRecord({
endpointId: endpoint.id,
source: "manual",
timeoutMs: parsed.data.timeoutMs,
});
if (!result) {
return {
ok: false,
error: "端点不存在",
errorCode: ERROR_CODES.NOT_FOUND,
};
}
- return { ok: true, data: { endpoint, result } };
+ // Refetch to get updated probe fields
+ const updatedEndpoint = await findProviderEndpointById(endpoint.id);
+ return { ok: true, data: { endpoint: updatedEndpoint ?? endpoint, result } };🤖 Prompt for AI Agents
In `@src/actions/provider-endpoints.ts` around lines 362 - 376, The returned
endpoint is stale because probeProviderEndpointAndRecord updates DB fields but
you return the pre-probe endpoint; after calling probeProviderEndpointAndRecord,
re-fetch the endpoint by id (e.g., call the repository/ORM method used elsewhere
to load a provider endpoint by endpoint.id or a function like
getProviderEndpointById/getById) and return that refreshed record instead
(return { ok: true, data: { endpoint: refreshedEndpoint, result } }); also
handle the case where the re-fetched endpoint is missing and return a NOT_FOUND
error similarly.
| export async function deleteProviderVendor(vendorId: number): Promise<boolean> { | ||
| const deleted = await db.transaction(async (tx) => { | ||
| // 1. Delete endpoints (cascade would handle this, but manual is safe) | ||
| await tx.delete(providerEndpoints).where(eq(providerEndpoints.vendorId, vendorId)); | ||
| // 2. Delete providers (keys) - explicit delete required due to 'restrict' | ||
| await tx.delete(providers).where(eq(providers.providerVendorId, vendorId)); | ||
| // 3. Delete vendor | ||
| const result = await tx | ||
| .delete(providerVendors) | ||
| .where(eq(providerVendors.id, vendorId)) | ||
| .returning({ id: providerVendors.id }); | ||
|
|
||
| return result.length > 0; | ||
| }); | ||
|
|
||
| return deleted; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deleteProviderVendor 与 schema 定义的 FK 约束不一致
Schema 中 providers.providerVendorId 定义为 onDelete: 'restrict',但此处尝试在删除 vendor 前直接 delete(providers)。这意味着会物理删除所有关联的 providers(包括未软删除的),而不是遵循 restrict 约束的语义。
如果业务意图是强制级联删除,建议:
- 在 schema 中将 FK 改为
onDelete: 'cascade' - 或在此处改用软删除 providers
当前实现可能导致意外的数据丢失(物理删除而非软删除)。
🔧 建议改用软删除
export async function deleteProviderVendor(vendorId: number): Promise<boolean> {
const deleted = await db.transaction(async (tx) => {
// 1. Delete endpoints (cascade would handle this, but manual is safe)
await tx.delete(providerEndpoints).where(eq(providerEndpoints.vendorId, vendorId));
- // 2. Delete providers (keys) - explicit delete required due to 'restrict'
- await tx.delete(providers).where(eq(providers.providerVendorId, vendorId));
+ // 2. Soft delete providers - preserve data while removing FK constraint blocker
+ const now = new Date();
+ await tx
+ .update(providers)
+ .set({ deletedAt: now, isEnabled: false, updatedAt: now })
+ .where(and(eq(providers.providerVendorId, vendorId), isNull(providers.deletedAt)));
+ // 3. Hard delete already soft-deleted providers to satisfy FK restrict
+ await tx
+ .delete(providers)
+ .where(and(eq(providers.providerVendorId, vendorId), isNotNull(providers.deletedAt)));
// 3. Delete vendor
const result = await tx
.delete(providerVendors)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function deleteProviderVendor(vendorId: number): Promise<boolean> { | |
| const deleted = await db.transaction(async (tx) => { | |
| // 1. Delete endpoints (cascade would handle this, but manual is safe) | |
| await tx.delete(providerEndpoints).where(eq(providerEndpoints.vendorId, vendorId)); | |
| // 2. Delete providers (keys) - explicit delete required due to 'restrict' | |
| await tx.delete(providers).where(eq(providers.providerVendorId, vendorId)); | |
| // 3. Delete vendor | |
| const result = await tx | |
| .delete(providerVendors) | |
| .where(eq(providerVendors.id, vendorId)) | |
| .returning({ id: providerVendors.id }); | |
| return result.length > 0; | |
| }); | |
| return deleted; | |
| } | |
| export async function deleteProviderVendor(vendorId: number): Promise<boolean> { | |
| const deleted = await db.transaction(async (tx) => { | |
| // 1. Delete endpoints (cascade would handle this, but manual is safe) | |
| await tx.delete(providerEndpoints).where(eq(providerEndpoints.vendorId, vendorId)); | |
| // 2. Soft delete providers - preserve data while removing FK constraint blocker | |
| const now = new Date(); | |
| await tx | |
| .update(providers) | |
| .set({ deletedAt: now, isEnabled: false, updatedAt: now }) | |
| .where(and(eq(providers.providerVendorId, vendorId), isNull(providers.deletedAt))); | |
| // 3. Hard delete already soft-deleted providers to satisfy FK restrict | |
| await tx | |
| .delete(providers) | |
| .where(and(eq(providers.providerVendorId, vendorId), isNotNull(providers.deletedAt))); | |
| // 3. Delete vendor | |
| const result = await tx | |
| .delete(providerVendors) | |
| .where(eq(providerVendors.id, vendorId)) | |
| .returning({ id: providerVendors.id }); | |
| return result.length > 0; | |
| }); | |
| return deleted; | |
| } |
🧪 测试结果
总体结果: ✅ 所有测试通过 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/lib/provider-endpoints/probe.ts`:
- Around line 129-138: The probeEndpointUrl function only falls back to GET when
tryProbe(url, "HEAD") returns statusCode === null; update probeEndpointUrl to
also treat a 405 response as a need to retry with GET by checking
head.statusCode === 405 (or null) and then returning tryProbe(url, "GET",
timeoutMs); keep using the existing tryProbe and EndpointProbeResult types so
behavior and return shape remain unchanged.
♻️ Duplicate comments (1)
src/repository/provider-endpoints.ts (1)
334-350: deleteProviderVendor 与 schema FK 约束不一致(已知问题)此问题已在之前的审查中指出:schema 中
providers.providerVendorId定义为onDelete: 'restrict',但此处物理删除所有关联 providers,可能导致意外数据丢失。请参考之前的审查建议考虑软删除或调整 FK 约束。
🧹 Nitpick comments (7)
.env.example (1)
127-129: 可选:调整变量顺序以符合 linter 规范dotenv-linter 提示这些配置变量的顺序与字母序不符。虽然不影响功能,但如果团队有代码规范要求,可考虑调整顺序。
♻️ 建议的调整
将以下变量按字母序重新排列:
ENDPOINT_PROBE_CONCURRENCY应在ENDPOINT_PROBE_INTERVAL_MS之前ENDPOINT_PROBE_CYCLE_JITTER_MS应在ENDPOINT_PROBE_INTERVAL_MS之前ENDPOINT_PROBE_LOCK_TTL_MS应在ENDPOINT_PROBE_TIMEOUT_MS之前ENDPOINT_PROBE_LOG_CLEANUP_BATCH_SIZE应在ENDPOINT_PROBE_LOG_RETENTION_DAYS之前不过考虑到这是示例配置文件,当前按逻辑分组的排列方式可能更易读,可根据团队偏好决定是否调整。
Also applies to: 138-138
src/lib/provider-endpoints/probe.ts (1)
68-80: TypeError 判断可能过于宽泛
TypeError在 fetch 中不仅用于 URL 解析失败,还可能用于其他场景(如无效的 header、CORS 问题等)。将所有TypeError归类为invalid_url可能导致错误分类。建议检查 error message 内容以更准确地判断错误类型。
♻️ 可选的改进方案
function toErrorInfo(error: unknown): { type: string; message: string } { if (error instanceof Error) { if (error.name === "AbortError") { return { type: "timeout", message: error.message || "timeout" }; } if (error instanceof TypeError) { - // Fetch URL parsing failures should not leak the original URL. - return { type: "invalid_url", message: "invalid_url" }; + // TypeError can indicate various issues; use a generic type to avoid misclassification + return { type: "fetch_error", message: "fetch_error" }; } return { type: "network_error", message: error.message }; } return { type: "unknown_error", message: String(error) }; }tests/unit/lib/provider-endpoints/probe.test.ts (1)
185-222: 建议补充 HEAD 405 回退测试当前测试覆盖了 AbortError(timeout)场景,但缺少 HEAD 返回 405 Method Not Allowed 时回退到 GET 的测试(如果实现中添加此回退逻辑)。
src/repository/provider-endpoints.ts (1)
57-105: 建议使用更具体的类型替代any三个 mapper 函数使用
eslint-disable-next-line@typescript-eslint/no-explicit-any`` 来处理 row 参数。虽然对于内部 mapper 可接受,但可以考虑使用 Drizzle 的推断类型或定义一个宽松的 row 类型以提高类型安全性。src/lib/provider-endpoints/probe-log-cleanup.ts (2)
1-14:parseIntWithDefault重复定义此函数与
src/lib/provider-endpoints/probe.ts中的实现相同。建议提取到共享工具模块(如src/lib/provider-endpoints/utils.ts)以避免代码重复。♻️ 建议提取共享工具
// src/lib/provider-endpoints/utils.ts export function parseIntWithDefault(value: string | undefined, fallback: number): number { const n = value ? Number.parseInt(value, 10) : Number.NaN; return Number.isFinite(n) ? n : fallback; }然后在
probe.ts和probe-log-cleanup.ts中导入使用。
111-126: stopEndpointProbeLogCleanup 与运行中的清理存在潜在竞态如果
runCleanupOnce正在执行中调用stopEndpointProbeLogCleanup,第 119 行会将RUNNING设为 false,但异步删除操作仍在进行。虽然锁机制可防止其他实例启动,但在同一进程中,若立即调用startEndpointProbeLogCleanup,可能导致两个清理流程重叠。当前风险较低,因为这种调用模式不太常见,但建议在注释中说明此行为或考虑等待当前执行完成。
tests/unit/lib/provider-endpoints/probe-scheduler.test.ts (1)
97-170: 并发控制测试设计巧妙使用
pending数组手动控制 Promise 解析的方式能够精确验证并发限制。测试正确断言了:
- 最大并发数不超过配置值 (2)
- 发现操作仅执行一次
- 所有端点最终都被探测
建议后续可增加以下边缘场景测试:
- 探测周期中途失去 leadership
- 探测过程中请求停止调度器
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to Reviews > Disable Cache setting
📒 Files selected for processing (10)
.env.examplesrc/instrumentation.tssrc/lib/provider-endpoints/leader-lock.tssrc/lib/provider-endpoints/probe-log-cleanup.tssrc/lib/provider-endpoints/probe-scheduler.tssrc/lib/provider-endpoints/probe.tssrc/repository/index.tssrc/repository/provider-endpoints.tstests/unit/lib/provider-endpoints/probe-scheduler.test.tstests/unit/lib/provider-endpoints/probe.test.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{js,ts,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Never use emoji characters in any code, comments, or string literals
Files:
tests/unit/lib/provider-endpoints/probe.test.tssrc/lib/provider-endpoints/probe-log-cleanup.tssrc/lib/provider-endpoints/probe-scheduler.tssrc/repository/index.tstests/unit/lib/provider-endpoints/probe-scheduler.test.tssrc/lib/provider-endpoints/probe.tssrc/lib/provider-endpoints/leader-lock.tssrc/repository/provider-endpoints.tssrc/instrumentation.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
All new features must have unit test coverage of at least 80%
Files:
tests/unit/lib/provider-endpoints/probe.test.tstests/unit/lib/provider-endpoints/probe-scheduler.test.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias@/to reference files in./src/directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width
Files:
tests/unit/lib/provider-endpoints/probe.test.tssrc/lib/provider-endpoints/probe-log-cleanup.tssrc/lib/provider-endpoints/probe-scheduler.tssrc/repository/index.tstests/unit/lib/provider-endpoints/probe-scheduler.test.tssrc/lib/provider-endpoints/probe.tssrc/lib/provider-endpoints/leader-lock.tssrc/repository/provider-endpoints.tssrc/instrumentation.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer named exports over default exports
Files:
tests/unit/lib/provider-endpoints/probe.test.tssrc/lib/provider-endpoints/probe-log-cleanup.tssrc/lib/provider-endpoints/probe-scheduler.tssrc/repository/index.tstests/unit/lib/provider-endpoints/probe-scheduler.test.tssrc/lib/provider-endpoints/probe.tssrc/lib/provider-endpoints/leader-lock.tssrc/repository/provider-endpoints.tssrc/instrumentation.ts
tests/**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Vitest for unit testing and happy-dom for DOM testing
Files:
tests/unit/lib/provider-endpoints/probe.test.tstests/unit/lib/provider-endpoints/probe-scheduler.test.ts
src/repository/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Drizzle ORM for data access in the repository layer
Files:
src/repository/index.tssrc/repository/provider-endpoints.ts
🧠 Learnings (2)
📚 Learning: 2026-01-05T03:01:39.354Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/types/user.ts:158-170
Timestamp: 2026-01-05T03:01:39.354Z
Learning: In TypeScript interfaces, explicitly document and enforce distinct meanings for null and undefined. Example: for numeric limits like limitTotalUsd, use 'number | null | undefined' when null signifies explicitly unlimited (e.g., matches DB schema or special UI logic) and undefined signifies 'inherit default'. This pattern should be consistently reflected in type definitions across related fields to preserve semantic clarity between database constraints and UI behavior.
Applied to files:
src/lib/provider-endpoints/probe-log-cleanup.tssrc/lib/provider-endpoints/probe-scheduler.tssrc/repository/index.tssrc/lib/provider-endpoints/probe.tssrc/lib/provider-endpoints/leader-lock.tssrc/repository/provider-endpoints.tssrc/instrumentation.ts
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to src/repository/**/*.{ts,tsx} : Use Drizzle ORM for data access in the repository layer
Applied to files:
src/repository/provider-endpoints.ts
🧬 Code graph analysis (7)
tests/unit/lib/provider-endpoints/probe.test.ts (2)
src/types/provider.ts (1)
ProviderEndpoint(387-404)src/lib/provider-endpoints/probe.ts (2)
probeEndpointUrl(129-138)probeProviderEndpointAndRecord(205-220)
src/lib/provider-endpoints/probe-scheduler.ts (3)
src/lib/provider-endpoints/leader-lock.ts (4)
LeaderLock(6-10)renewLeaderLock(62-109)releaseLeaderLock(111-141)acquireLeaderLock(26-60)src/repository/provider-endpoints.ts (1)
findEnabledProviderEndpointsForProbing(112-132)src/lib/provider-endpoints/probe.ts (1)
probeProviderEndpointAndRecordByEndpoint(155-203)
tests/unit/lib/provider-endpoints/probe-scheduler.test.ts (1)
src/lib/provider-endpoints/probe-scheduler.ts (2)
startEndpointProbeScheduler(213-234)stopEndpointProbeScheduler(236-252)
src/lib/provider-endpoints/probe.ts (4)
src/lib/logger.ts (1)
logger(168-187)src/types/provider.ts (2)
ProviderEndpoint(387-404)ProviderEndpointProbeSource(385-385)src/lib/endpoint-circuit-breaker.ts (1)
recordEndpointFailure(137-158)src/repository/provider-endpoints.ts (3)
recordProviderEndpointProbeResult(682-719)updateProviderEndpointProbeSnapshot(134-157)findProviderEndpointById(270-297)
src/lib/provider-endpoints/leader-lock.ts (3)
scripts/sync-settings-keys.js (2)
key(32-32)result(40-40)src/lib/rate-limit/service.ts (1)
redis(102-104)src/lib/logger.ts (1)
logger(168-187)
src/repository/provider-endpoints.ts (4)
src/types/provider.ts (5)
ProviderVendor(375-383)ProviderEndpoint(387-404)ProviderEndpointProbeLog(406-416)ProviderType(6-12)ProviderEndpointProbeSource(385-385)src/repository/index.ts (15)
findEnabledProviderEndpointsForProbing(52-52)updateProviderEndpointProbeSnapshot(62-62)deleteProviderEndpointProbeLogsBeforeDateBatch(50-50)findProviderVendors(57-57)findProviderVendorById(56-56)findProviderEndpointById(53-53)updateProviderVendor(63-63)deleteProviderVendor(51-51)tryDeleteProviderVendorIfEmpty(60-60)findProviderEndpointsByVendorAndType(55-55)createProviderEndpoint(49-49)updateProviderEndpoint(61-61)softDeleteProviderEndpoint(59-59)recordProviderEndpointProbeResult(58-58)findProviderEndpointProbeLogs(54-54)src/drizzle/db.ts (1)
db(37-44)src/drizzle/schema.ts (4)
providerEndpoints(297-339)providerVendors(132-145)providers(148-294)providerEndpointProbeLogs(342-363)
src/instrumentation.ts (3)
src/lib/provider-endpoints/probe-scheduler.ts (2)
stopEndpointProbeScheduler(236-252)startEndpointProbeScheduler(213-234)src/lib/provider-endpoints/probe-log-cleanup.ts (2)
stopEndpointProbeLogCleanup(111-126)startEndpointProbeLogCleanup(93-109)src/repository/provider-endpoints.ts (1)
backfillProviderEndpointsFromProviders(519-601)
🪛 dotenv-linter (4.0.0)
.env.example
[warning] 127-127: [UnorderedKey] The ENDPOINT_PROBE_CONCURRENCY key should go before the ENDPOINT_PROBE_INTERVAL_MS key
(UnorderedKey)
[warning] 128-128: [UnorderedKey] The ENDPOINT_PROBE_CYCLE_JITTER_MS key should go before the ENDPOINT_PROBE_INTERVAL_MS key
(UnorderedKey)
[warning] 129-129: [UnorderedKey] The ENDPOINT_PROBE_LOCK_TTL_MS key should go before the ENDPOINT_PROBE_TIMEOUT_MS key
(UnorderedKey)
[warning] 138-138: [UnorderedKey] The ENDPOINT_PROBE_LOG_CLEANUP_BATCH_SIZE key should go before the ENDPOINT_PROBE_LOG_RETENTION_DAYS key
(UnorderedKey)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: 🌐 API Tests
- GitHub Check: Docker Build Test
- GitHub Check: dev-build-deploy
🔇 Additional comments (41)
.env.example (1)
122-138: 配置设计合理,参数选择得当!新增的端点探测配置整体设计良好:
- 超时(5秒)小于探测间隔(10秒),避免探测重叠
- 锁 TTL(30秒)是探测间隔的 3 倍,适合分布式领导者选举
- 并发度(10)和抖动(1秒)设置合理,可防止探测风暴
- 成功日志采样间隔(60秒)能有效减少日志量
- 日志保留期(7天)和清理批次大小(10000)均为典型生产配置
配置说明也很清晰,默认启用符合该功能的核心定位。
src/lib/provider-endpoints/probe.ts (4)
1-11: LGTM!导入和模块设置正确。使用
server-only确保仅在服务端执行,导入了必要的依赖。
12-26: LGTM!类型定义清晰,配置解析逻辑合理,带有默认值回退。
155-203: LGTM!
probeProviderEndpointAndRecordByEndpoint实现完整:
- 正确处理失败时的熔断器记录
- 根据 source 和采样间隔决定是写完整日志还是仅更新快照
- 逻辑清晰,分支处理合理
205-220: LGTM!
probeProviderEndpointAndRecord正确处理端点不存在的情况,返回 null 而非抛出异常。tests/unit/lib/provider-endpoints/probe.test.ts (6)
1-30: LGTM!测试辅助函数
makeEndpoint设置合理,afterEach清理逻辑确保测试隔离。
31-69: LGTM!HEAD 成功测试覆盖了正常路径,验证了不触发 GET 的预期行为。
71-111: LGTM!HEAD 网络错误回退 GET 的测试验证了容错逻辑。
113-183: LGTM!5xx 和 4xx 响应的处理测试覆盖了边界情况,验证了
ok状态的判断逻辑。
224-330: LGTM!端点不存在和记录入库的测试完整,验证了:
findProviderEndpointById返回 null 时正确处理- 记录字段包含所有必要信息(source、ok、statusCode、latency、probedAt)
- 时间戳使用 fake timers 确保确定性
332-423: LGTM!scheduled 采样和失败熔断计数的测试覆盖了核心业务逻辑:
- 验证了 scheduled 成功时仅更新快照而非写日志
- 验证了失败时
recordEndpointFailure被正确调用src/repository/provider-endpoints.ts (8)
1-19: LGTM!模块设置正确,使用
"use server"指令并导入了必要的 Drizzle ORM 组件和 schema 定义。按照 coding guidelines,repository 层使用 Drizzle ORM 进行数据访问。
20-29: LGTM!日期转换辅助函数处理了多种输入类型,
toNullableDate正确区分 null/undefined。
31-55: LGTM!
normalizeWebsiteDomainFromUrl实现健壮:
- 支持带/不带协议的 URL
- 正确移除
www.前缀- 解析失败时记录调试日志而不泄露完整 URL
159-179: LGTM!批量删除使用
FOR UPDATE SKIP LOCKED是正确的并发安全模式,避免了锁竞争和死锁。
181-228: LGTM!
getOrCreateProviderVendorIdFromUrls使用onConflictDoNothing处理并发插入竞态,并有回退查询确保最终获取 ID。
352-403: LGTM!
tryDeleteProviderVendorIfEmpty实现完善:
- 事务内检查无活跃 providers 和 endpoints
- 先清理软删除的 providers 满足 FK restrict 约束
- 使用
NOT EXISTS子查询确保原子性- 异常捕获并记录警告日志
519-601: LGTM!
backfillProviderEndpointsFromProviders实现高效:
- 分页读取避免内存溢出
- 使用 Set 去重避免重复插入
- 批量写入减少数据库往返
- URL 验证跳过无效数据
682-719: LGTM!
recordProviderEndpointProbeResult使用事务确保日志插入和端点更新的原子性,符合数据一致性要求。src/repository/index.ts (1)
47-64: LGTM!新增的 provider-endpoints 导出遵循现有模式,函数按字母顺序排列,为上层 actions 和 API 提供统一入口。
src/lib/provider-endpoints/probe-log-cleanup.ts (2)
27-32: LGTM!使用
globalThis管理单例状态是 Next.js 中处理模块级状态的常见模式,避免热重载时状态丢失。
34-91: LGTM!
runCleanupOnce实现健壮:
- 使用
RUNNING标志防止并发执行- 分布式锁确保多实例环境下单点执行
- 批量删除直到无更多数据或批次不满
- finally 块确保状态和锁正确释放
tests/unit/lib/provider-endpoints/probe-scheduler.test.ts (3)
1-41: 类型定义和辅助函数设计良好类型定义
ProbeTarget和ProbeResult与实际被测模块保持一致,辅助工厂函数makeEndpoint和makeOkResult简洁实用。flushMicrotasks是处理异步测试的常见模式。
43-68: Mock 设置结构合理使用模块级 mock 变量配合
vi.mock工厂函数的模式,能够在保持 mock 提升行为的同时允许每个测试自定义 mock 实现。afterEach中正确清理了定时器和环境变量存根。
70-95: 非 Leader 场景测试正确测试正确验证了当无法获取 leader 锁时,调度器不会执行端点发现和探测操作。断言清晰且覆盖了关键行为。
src/instrumentation.ts (5)
124-144: 关闭处理逻辑完善新增的
stopEndpointProbeScheduler和stopEndpointProbeLogCleanup调用遵循了现有的错误处理模式,使用 try/catch 包裹并记录警告日志。这确保了即使某个组件停止失败,也不会阻塞其他组件的清理。
202-220: 生产环境初始化增强合理
AUTO_MIGRATE=false检查允许外部管理迁移的生产部署跳过自动迁移,并清晰记录日志。端点回填使用 try/catch 包裹,失败不会阻塞应用启动。
257-277: 生产环境探测调度器启动逻辑一致探测调度器和日志清理的启动遵循了与其他后台任务相同的模式:try/catch 包裹、失败仅记录警告、不阻塞应用启动。
290-302: 开发环境回填逻辑正确开发环境在迁移完成后执行端点回填,与生产环境保持一致。错误处理模式相同。
343-369: 开发环境数据库可用性检查完善正确地在
isConnected条件内启动探测调度器和日志清理,避免本地开发无数据库时的错误。当数据库不可用时的警告信息清晰明确。src/lib/provider-endpoints/probe-scheduler.ts (7)
1-31: 导入和配置解析设计良好配置解析使用
Math.max确保最小边界值,parseIntWithDefault辅助函数处理了无效输入的回退。环境变量命名清晰,默认值合理。
41-55: 工具函数实现正确
sleep函数正确处理了非正数的边缘情况。shuffleInPlace使用 Fisher-Yates 算法实现无偏随机打乱。
57-76: Leader 锁获取逻辑正确先尝试续期现有锁,失败后释放并重新获取的模式是合理的。当续期失败时(无论是过期还是所有权丢失),先释放再重新获取能确保状态一致性。
78-125: Keep-alive 机制设计稳健续期间隔设置为
TTL/2确保在锁过期前完成续期。timer.unref()调用避免定时器阻塞进程退出。stopped和renewing标志防止了竞态条件。
127-174: 探测周期主逻辑结构清晰周期运行前的守卫检查(防止并发和响应停止请求)、leadership 确保、keep-alive 启动、随机抖动、容量预警计算都设计合理。
178-202: 并发 Worker 模式实现正确使用共享
index变量的 work-stealing 模式在 JavaScript 单线程模型下是安全的。每个 worker 在获取端点后立即递增索引,确保端点不会被重复处理。Promise.all等待所有 worker 完成后才退出周期。
213-272: 公共 API 函数设计完善
startEndpointProbeScheduler: 幂等启动,立即执行首次探测周期stopEndpointProbeScheduler: 正确设置停止标志、清理定时器、释放锁getEndpointProbeSchedulerStatus: 提供有用的调试信息所有函数使用命名导出,符合编码规范。
src/lib/provider-endpoints/leader-lock.ts (4)
1-24: 类型定义和辅助函数清晰
LeaderLock接口明确区分了redis和memory锁类型。内存锁清理函数正确处理过期条目。generateLockId使用时间戳和随机字符串组合确保唯一性。
26-60: Redis 错误回退行为需注意当 Redis 调用失败时(非不可用,而是实际错误如网络超时),代码会回退到内存锁。在多实例部署中,如果 Redis 间歇性不稳定,可能导致不同实例分别持有 Redis 锁和内存锁。
当前设计优先考虑优雅降级而非严格一致性,这在单实例部署或可以容忍短暂多 leader 的场景下是合理的。建议在部署文档中明确说明此行为。
62-82: 内存锁续期时的 Redis 可用性检查设计巧妙第 65-69 行的逻辑:当持有内存锁但 Redis 恢复可用时返回
false,强制调用方重新获取分布式锁。这有效缓解了 Redis 恢复后可能出现的脑裂问题。
111-141: 锁释放逻辑正确内存锁和 Redis 锁的释放都正确验证了所有权。Redis 不可用时静默处理,错误时仅记录警告不抛出异常,符合"尽力而为"的释放语义。
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| export async function probeEndpointUrl( | ||
| url: string, | ||
| timeoutMs: number = DEFAULT_TIMEOUT_MS | ||
| ): Promise<EndpointProbeResult> { | ||
| const head = await tryProbe(url, "HEAD", timeoutMs); | ||
| if (head.statusCode === null) { | ||
| return tryProbe(url, "GET", timeoutMs); | ||
| } | ||
| return head; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HEAD 返回 405 时未回退到 GET
当前逻辑仅在 statusCode === null(网络错误)时回退到 GET。但某些服务器不支持 HEAD 方法会返回 405 Method Not Allowed,此时 statusCode 为 405,不会触发 GET 回退,可能导致误判健康端点为不可用。
🔧 建议增加 405 回退
export async function probeEndpointUrl(
url: string,
timeoutMs: number = DEFAULT_TIMEOUT_MS
): Promise<EndpointProbeResult> {
const head = await tryProbe(url, "HEAD", timeoutMs);
- if (head.statusCode === null) {
+ if (head.statusCode === null || head.statusCode === 405) {
return tryProbe(url, "GET", timeoutMs);
}
return head;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export async function probeEndpointUrl( | |
| url: string, | |
| timeoutMs: number = DEFAULT_TIMEOUT_MS | |
| ): Promise<EndpointProbeResult> { | |
| const head = await tryProbe(url, "HEAD", timeoutMs); | |
| if (head.statusCode === null) { | |
| return tryProbe(url, "GET", timeoutMs); | |
| } | |
| return head; | |
| } | |
| export async function probeEndpointUrl( | |
| url: string, | |
| timeoutMs: number = DEFAULT_TIMEOUT_MS | |
| ): Promise<EndpointProbeResult> { | |
| const head = await tryProbe(url, "HEAD", timeoutMs); | |
| if (head.statusCode === null || head.statusCode === 405) { | |
| return tryProbe(url, "GET", timeoutMs); | |
| } | |
| return head; | |
| } |
🤖 Prompt for AI Agents
In `@src/lib/provider-endpoints/probe.ts` around lines 129 - 138, The
probeEndpointUrl function only falls back to GET when tryProbe(url, "HEAD")
returns statusCode === null; update probeEndpointUrl to also treat a 405
response as a need to retry with GET by checking head.statusCode === 405 (or
null) and then returning tryProbe(url, "GET", timeoutMs); keep using the
existing tryProbe and EndpointProbeResult types so behavior and return shape
remain unchanged.
🧪 测试结果
总体结果: ✅ 所有测试通过 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx`:
- Around line 131-143: The JSX for the test render is broken because the type
assertion `as any` was split onto its own line and treated as JSX props; wrap
the entire JSX expression passed to renderWithProviders (the ProviderVendorView
element) in parentheses and apply the `as any` type assertion to that
parenthesized expression so the assertion attaches to the JSX value rather than
creating stray props (fix the call that uses renderWithProviders and the
ProviderVendorView props/currentUser).
- Around line 166-178: The JSX has a broken type assertion split across lines
for the currentUser prop; fix the syntax by combining the assertion so
currentUser is passed as a single expression (e.g., currentUser={{ role: "admin"
} as any}) and remove the stray standalone "as" and "any" tokens after the
component props; verify the ProviderVendorView(...) call contains no broken
tokens and compiles.
♻️ Duplicate comments (3)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (3)
484-496: 删除确认使用原生window.confirm()。此问题在之前的评审中已被标记。建议使用 AlertDialog 组件替代原生
confirm()以保持 UI 一致性,类似于DeleteVendorDialog的实现方式。
221-260: providerTypes 数组不完整且按钮标签缺少 i18n 翻译。此问题在之前的评审中已被标记。当前
providerTypes数组缺少"claude-auth"和"gemini-cli",且第 245 行直接渲染{type}违反了 i18n 要求。根据
src/types/provider.ts中的ProviderType定义,应包含所有 6 种类型,并使用翻译函数显示按钮文本。建议修复
- const providerTypes: ProviderType[] = ["claude", "codex", "gemini", "openai-compatible"]; + const providerTypes: ProviderType[] = ["claude", "claude-auth", "codex", "gemini", "gemini-cli", "openai-compatible"];按钮渲染部分:
- > - {type} - </Button> + > + {t(`providerTypes.${type.replace(/-([a-z])/g, (_, c) => c.toUpperCase())}`)} + </Button>
450-457:formatDistanceToNow缺少 locale 参数。此问题在之前的评审中已被标记。第 452 行的
formatDistanceToNow调用没有传入 locale 参数,导致相对时间始终显示为英文,违反了 i18n 要求。建议修复
+import { useLocale } from "next-intl"; +import { zhCN, zhTW, ja, ru, enUS } from "date-fns/locale"; +const localeMap: Record<string, Locale> = { + "zh-CN": zhCN, + "zh-TW": zhTW, + ja: ja, + ru: ru, + en: enUS, +}; // 在 EndpointRow 组件内 +const locale = useLocale(); +const dateLocale = localeMap[locale] || enUS; -{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true })} +{formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true, locale: dateLocale })}
🧹 Nitpick comments (5)
tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsx (2)
40-56: 建议添加 afterEach 清理以提高测试隔离性。当前
renderWithProviders返回unmount函数由每个测试手动调用。如果测试中途失败抛出异常,unmount()可能不会被执行,导致 DOM 残留影响后续测试。♻️ 建议的改进
let queryClient: QueryClient; +let cleanupFns: Array<() => void> = []; function renderWithProviders(node: ReactNode) { const container = document.createElement("div"); document.body.appendChild(container); const root = createRoot(container); act(() => { root.render(<QueryClientProvider client={queryClient}>{node}</QueryClientProvider>); }); + const cleanup = () => { + act(() => root.unmount()); + container.remove(); + }; + cleanupFns.push(cleanup); + return { container, - unmount: () => { - act(() => root.unmount()); - container.remove(); - }, + unmount: cleanup, }; }然后在
beforeEach中添加清理逻辑:beforeEach(() => { + cleanupFns.forEach((fn) => fn()); + cleanupFns = []; queryClient = new QueryClient({或者使用
afterEach钩子进行清理。
78-148: 测试覆盖良好,涵盖了主要场景。三个测试用例覆盖了:
- 空数据状态渲染占位符
- 成功状态(绿色线条)
- 失败状态(红色线条)
建议考虑补充以下边界场景以提高覆盖率:
res.ok === false时的错误处理latencyMs为null的情况src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx (2)
162-162: 考虑将providerTypeItems提取为共享常量。
providerTypeItems数组在组件内部硬编码,如果其他地方也需要使用相同的类型列表,可能导致不一致。建议将其提取到共享的常量文件或类型定义中。建议的重构方案
// 在 src/types/provider.ts 或 src/lib/provider-type-utils.tsx 中定义 export const PROVIDER_TYPE_OPTIONS: ProviderType[] = ["claude", "codex", "gemini", "openai-compatible"];然后在组件中导入使用:
- const providerTypeItems: ProviderType[] = ["claude", "codex", "gemini", "openai-compatible"]; + // 从共享常量导入 PROVIDER_TYPE_OPTIONS
116-118: 使用占位符文本作为错误消息不太合适。当 API 密钥为空时,抛出的错误消息使用了
tForm("key.placeholder")(输入框占位符文本),这在语义上不正确。建议使用专门的验证错误消息。建议的修改
if (!apiKey) { - throw new Error(tForm("key.placeholder")); + throw new Error(tForm("key.required") || "API key is required"); }需要在翻译文件中添加对应的
key.required键值。src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (1)
83-91: Props 接口中any类型可以考虑改进。
healthStatus和statistics使用Record<number, any>类型,虽然可以工作,但会丢失类型安全性。建议在后续迭代中定义具体的类型接口。
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to Reviews > Disable Cache setting
📒 Files selected for processing (11)
messages/en/settings/providers/strings.jsonmessages/ja/settings/providers/strings.jsonmessages/ru/settings/providers/strings.jsonmessages/zh-CN/settings/providers/strings.jsonmessages/zh-TW/settings/providers/strings.jsonsrc/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsxsrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsxsrc/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsxtests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsxtests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsxtests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- messages/en/settings/providers/strings.json
- messages/ja/settings/providers/strings.json
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Never use emoji characters in any code, comments, or string literals
Files:
tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsxtests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsxsrc/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsxsrc/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsxtests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsxsrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
All new features must have unit test coverage of at least 80%
Files:
tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsxtests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsxtests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias@/to reference files in./src/directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width
Files:
tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsxtests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsxsrc/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsxsrc/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsxtests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsxsrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer named exports over default exports
Files:
tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsxtests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsxsrc/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsxsrc/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsxtests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsxsrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
tests/**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Vitest for unit testing and happy-dom for DOM testing
Files:
tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsxtests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsxtests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
🧠 Learnings (8)
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to tests/**/*.test.{ts,tsx,js,jsx} : Use Vitest for unit testing and happy-dom for DOM testing
Applied to files:
tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsxtests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsxtests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.test.{ts,tsx,js,jsx} : All new features must have unit test coverage of at least 80%
Applied to files:
tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsx
📚 Learning: 2026-01-05T03:02:14.502Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx:66-66
Timestamp: 2026-01-05T03:02:14.502Z
Learning: In the claude-code-hub project, the translations.actions.addKey field in UserKeyTableRowProps is defined as optional for backward compatibility, but all actual callers in the codebase provide the complete translations object. The field has been added to all 5 locale files (messages/{locale}/dashboard.json).
Applied to files:
tests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsxsrc/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
📚 Learning: 2026-01-10T06:20:32.687Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx:118-125
Timestamp: 2026-01-10T06:20:32.687Z
Learning: In `src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx`, the "Cancel" button in the SyncConflictDialog is intentionally designed to call `onConfirm([])`, which triggers `doSync([])` to continue the sync while skipping (not overwriting) conflicting manual prices. This is the desired product behavior to allow users to proceed with LiteLLM sync for non-conflicting models while preserving their manual price entries.
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T06:20:04.478Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:20:04.478Z
Learning: In the `ding113/claude-code-hub` repository, Server Actions (files under `src/actions/*.ts`) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T06:20:19.207Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: messages/ja/dashboard.json:1342-1345
Timestamp: 2026-01-10T06:20:19.207Z
Learning: In this project, minor i18n formatting issues (such as spacing between numbers and units in Japanese/Chinese) are accepted when fixing them would require adding template complexity. The approach prioritizes code simplicity over perfect locale-specific formatting for minor cosmetic issues.
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Use `next-intl` for internationalization with support for 5 languages: zh-CN, zh-TW, en, ja, ru
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
🧬 Code graph analysis (6)
tests/unit/settings/providers/endpoint-latency-sparkline-ui.test.tsx (1)
src/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx (1)
EndpointLatencySparkline(13-63)
tests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx (1)
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx (1)
VendorKeysCompactList(70-266)
src/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx (1)
src/actions/provider-endpoints.ts (1)
getProviderEndpointProbeLogs(384-418)
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx (4)
src/actions/provider-endpoints.ts (1)
getProviderEndpoints(160-186)src/actions/providers.ts (4)
addProvider(433-596)editProvider(599-730)removeProvider(733-772)getUnmaskedProviderKey(1354-1381)src/lib/provider-type-utils.tsx (1)
getProviderTypeTranslationKey(65-67)src/lib/utils/clipboard.ts (2)
isClipboardSupported(5-12)copyToClipboard(18-30)
tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx (1)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (1)
ProviderVendorView(93-149)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (3)
src/types/provider.ts (4)
ProviderDisplay(147-217)ProviderVendor(375-383)ProviderType(6-12)ProviderEndpoint(387-404)src/actions/provider-endpoints.ts (8)
getProviderVendors(132-144)getVendorTypeCircuitInfo(503-543)resetVendorTypeCircuit(579-610)getProviderEndpoints(160-186)probeProviderEndpoint(321-382)removeProviderEndpoint(272-319)addProviderEndpoint(188-225)editProviderEndpoint(227-270)src/drizzle/schema.ts (1)
providers(148-294)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: 🌐 API Tests
- GitHub Check: Docker Build Test
- GitHub Check: dev-build-deploy
🔇 Additional comments (16)
src/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx (1)
1-62: LGTM!组件实现清晰:
- 使用 react-query 正确配置了缓存策略
- 数据转换逻辑合理,通过 reverse 保证时间顺序
- 空状态处理得当
- 颜色根据最近探测状态动态切换
messages/zh-CN/settings/providers/strings.json (1)
46-101: 本地化键值完整且结构正确。新增的翻译键值覆盖了端点管理、服务商管理、熔断控制等新功能的全部 UI 文案。JSON 语法正确,与其他语言文件(en、ja、ru、zh-TW)的键名保持一致。
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx (2)
58-68: LGTM!辅助函数逻辑清晰,名称构建规则合理:使用域名、类型和密钥后4位组合,并限制在64字符内。
268-469: LGTM!
VendorKeyRow组件实现良好:
- 正确处理了敏感数据(密钥查看后及时清除)
- 剪贴板功能有完善的错误处理
- 删除操作使用了确认对话框
- 状态切换和删除后正确刷新缓存
messages/zh-TW/settings/providers/strings.json (1)
46-101: LGTM!繁体中文翻译完整,键名与简体中文及其他语言文件保持一致。JSON 结构正确。
tests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx (3)
62-101: LGTM!测试工具函数实现正确:
renderWithProviders正确设置了 QueryClient 和 NextIntl 上下文flushTicks用于等待异步操作完成setNativeValue正确处理了 React 受控输入的值设置
115-156: LGTM!第一个测试用例正确验证了回归点:新增密钥对话框中不应显示 URL 输入框和 URL 拼接预览。断言清晰明确。
158-229: LGTM!第二个测试用例全面验证了提交流程:
- 验证了
getProviderEndpoints被正确调用- 验证了
addProvider的调用参数包含正确的 URL、密钥、类型等- 验证了成功提示 toast 的显示
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (7)
1-17: 导入和客户端声明结构正确。导入组织清晰,使用了
@/路径别名,符合编码规范。React Query、next-intl 和 UI 组件的导入都是必要的。
151-219: VendorCard 组件实现良好。组件正确使用 i18n 翻译函数处理所有用户可见文本,Avatar 和 Card 组件组合合理,条件渲染逻辑清晰。
262-327: VendorTypeCircuitControl 实现正确。React Query 的 useQuery 和 useMutation 使用模式正确,错误处理通过 toast 通知用户,成功后正确调用
invalidateQueries刷新缓存。
329-378: EndpointsTable 组件结构清晰。查询使用了正确的 queryKey 模式
["provider-endpoints", vendorId, providerType],加载状态和空状态都有适当的 UI 反馈。
503-596: AddEndpointButton 实现良好。Dialog 模式使用正确,表单处理通过 FormData 实现,
useEffect在对话框关闭时重置状态。值得注意的是第 541 行的查询失效使用了精确的 queryKey["provider-endpoints", vendorId, providerType],这是比较好的实践。
598-672: EditEndpointDialog 实现正确。表单处理和 Switch 组件的使用方式正确。第 624 行的
invalidateQueries({ queryKey: ["provider-endpoints"] })会刷新所有端点查询,这在编辑场景下是合理的,因为用户可能会切换到其他类型查看。
674-759: DeleteVendorDialog 实现优秀。两步确认流程(confirm → double-confirm)为破坏性操作提供了额外的安全保障。正确使用 AlertDialog 组件而非原生
confirm(),保持了 UI 一致性。错误处理使用getErrorMessage(tErrors, res.errorCode)进行国际化翻译。这是删除确认对话框的良好实现范例,建议将 EndpointRow 中的删除逻辑也改为类似模式。
messages/ru/settings/providers/strings.json (1)
46-101: 俄语翻译完整且格式正确。新增的翻译键覆盖了所有端点管理、供应商管理和熔断器控制相关的 UI 文本。占位符变量(
{name}、{id}、{providerType})保留正确,JSON 结构有效。
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
🧪 测试结果
总体结果: ✅ 所有测试通过 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In `@src/app/`[locale]/settings/providers/_components/forms/provider-form.tsx:
- Around line 1135-1160: Replace the hardcoded SelectValue placeholder text with
the i18n lookup: use the page's translation function t(...) instead of literal
"inherit" for the SelectValue placeholder and for other SelectValue occurrences
(e.g., the Select using cacheTtlPreference / SelectValue placeholder and the
Context1m/Codex SelectValue placeholders mentioned); locate the Select
components (e.g., the Select with value={cacheTtlPreference} and its
SelectValue) and set
placeholder={t("sections.routing.cacheTtl.options.inherit")} (and equivalent
i18n keys for the other SelectValue placeholders) so all user-visible strings
flow through t(...).
- Around line 156-167: The default cloned name uses a hardcoded suffix
(`${cloneProvider.name}_Copy`) in the name useState initialization; change it to
use the app i18n instead by obtaining the translation function (e.g.,
useTranslation or t) and building the default as
`${cloneProvider.name}${t('provider.copySuffix')}` (or another existing i18n
key) so the user-visible suffix is localized; update the name useState
initializer in provider-form (and import/use the translation hook at the top of
the component) and ensure the chosen i18n key is added to translation files.
- Around line 860-887: The options mapping can omit the current providerType,
causing stale/invalid selections to be submitted; update the options generation
so that if providerType is set but not included in allowedProviderTypes you
insert it into the list (preferably as the first or last item and marked
disabled or annotated) before mapping; adjust the disabled logic that uses
enableMultiProviderTypes accordingly so the inserted fallback item uses the same
disabled rules, and keep using getProviderTypeConfig and renderProviderTypeLabel
to render its icon/label so the SelectItem shows a visible, non-crashing
fallback for providerType.
- Around line 286-314: The auto-resolve effect overwrites an existing
preset/website URL; update the useEffect in provider-form.tsx to skip calling
urlResolver when a preset or current websiteUrl exists (i.e., only run when
websiteUrl is empty and preset?.websiteUrl is falsy), and make sure to call the
correct state setter (setWebsiteUrl) instead of setUrl in the then/catch/finally
branches; keep the cancelled guard and autoUrlPending handling as-is.
In `@src/app/`[locale]/settings/providers/_components/provider-vendor-view.tsx:
- Line 246: providerTypes array is missing the "openai-compatible" member from
the ProviderType union; update the const providerTypes (in
provider-vendor-view.tsx) to include "openai-compatible" or add a brief inline
comment on why it's intentionally excluded so callers expecting ProviderType
variants (e.g., other endpoint handlers) remain consistent; modify the array
declaration for providerTypes to add "openai-compatible" (or add a clear comment
next to that declaration explaining its intentional omission).
In
`@src/app/`[locale]/settings/providers/_components/vendor-keys-compact-list.tsx:
- Around line 487-507: Add accessible aria-labels to the interactive elements
that lack visible text: for the Switch (checked={props.provider.isEnabled} /
onCheckedChange calling toggleMutation.mutate) and for the icon-only Button
inside AlertDialogTrigger (which shows Trash2 and uses deleteMutation and
deleteDialogOpen/setDeleteDialogOpen). Use i18n strings (translated messages)
that include the provider name (props.provider.name) to form labels like "Enable
{provider}" / "Disable {provider}" or "Delete {provider} key" as appropriate,
and pass them via aria-label on the Switch and the Button so screen readers get
descriptive text.
- Around line 50-55: The hardcoded fallback "vendor" in buildDefaultProviderName
(used with vendorWebsiteDomain and providerType) must be localized and not
embedded; change buildDefaultProviderName to accept a fallbackName parameter (or
an i18n function) and use that instead of the literal "vendor", then update
callers (including the other usage around the block at lines ~112-118) to pass a
localized string from the component via the i18n hook (e.g., t('...')) so all
user-visible defaults come from localized resources.
- Around line 413-425: The UI currently hardcodes the multiplier suffix "x" in
the InlineEditPopover suffix prop and in the read-only span; replace both
occurrences to use the i18n helper (e.g., tInline) instead of the literal "x".
Update InlineEditPopover's suffix argument to tInline("costMultiplierSuffix")
and render the read-only value using tInline("costMultiplierSuffix") so the
suffix is translatable; locate these in the cost multiplier cell around
InlineEditPopover and the span that renders props.provider.costMultiplier.
♻️ Duplicate comments (2)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (2)
516-527: 删除端点使用原生confirm()与 UI 风格不一致删除供应商使用
AlertDialog组件(DeleteVendorDialog),但删除端点却使用原生window.confirm(),导致用户体验不一致。建议统一使用AlertDialog组件。建议提取为独立的 DeleteEndpointDialog 组件
function DeleteEndpointDialog({ endpoint, onDelete }: { endpoint: ProviderEndpoint; onDelete: () => void; }) { const t = useTranslations("settings.providers"); const tCommon = useTranslations("settings.common"); return ( <AlertDialog> <AlertDialogTrigger asChild> <DropdownMenuItem className="text-destructive focus:text-destructive" onSelect={(e) => e.preventDefault()} > <Trash2 className="mr-2 h-4 w-4" /> {tCommon("delete")} </DropdownMenuItem> </AlertDialogTrigger> <AlertDialogContent> <AlertDialogHeader> <AlertDialogTitle>{t("confirmDeleteEndpoint")}</AlertDialogTitle> </AlertDialogHeader> <AlertDialogFooter> <AlertDialogCancel>{tCommon("cancel")}</AlertDialogCancel> <AlertDialogAction onClick={onDelete}> {tCommon("confirm")} </AlertDialogAction> </AlertDialogFooter> </AlertDialogContent> </AlertDialog> ); }
482-488:formatDistanceToNow缺少 locale 参数,不符合 i18n 规范相对时间字符串(如 "5 minutes ago")是用户可见内容,但未传入
locale参数,导致始终显示英文。根据编码规范,所有用户可见字符串必须支持 i18n(5 种语言)。建议的修复
+import { useLocale } from "next-intl"; +import { zhCN, zhTW, ja, ru, enUS } from "date-fns/locale"; +import type { Locale } from "date-fns"; +const DATE_LOCALE_MAP: Record<string, Locale> = { + "zh-CN": zhCN, + "zh-TW": zhTW, + ja: ja, + ru: ru, + en: enUS, +}; // 在 EndpointRow 组件内 function EndpointRow({ endpoint }: { endpoint: ProviderEndpoint }) { const t = useTranslations("settings.providers"); const tCommon = useTranslations("settings.common"); + const locale = useLocale(); + const dateLocale = DATE_LOCALE_MAP[locale] || enUS; // ... // 在渲染时 - {formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true })} + {formatDistanceToNow(new Date(endpoint.lastProbedAt), { addSuffix: true, locale: dateLocale })}
🧹 Nitpick comments (5)
tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx (2)
13-13: 使用路径别名导入消息文件根据编码规范,应使用
@/路径别名引用./src/目录下的文件。虽然messages/目录在src/外部,但建议确认项目中是否配置了对应的别名(如@messages/),以保持导入风格一致性。
184-185: CSS 类选择器断言较为脆弱使用
th[class*="w-[220px]"]依赖具体的 Tailwind 类名,若样式重构会导致测试失败。建议改用data-testid属性或基于文本内容查询(如getByText)来提高测试稳定性。建议的改进
- const latencyHeader = document.querySelector('th[class*="w-[220px]"]'); - expect(latencyHeader?.textContent || "").toContain("Latency"); + // 在组件中添加 data-testid="latency-header" + // 或直接使用文本断言 + expect(document.body.textContent || "").toContain("Latency");src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (2)
84-92:healthStatus属性未被使用
ProviderVendorViewProps定义了healthStatus属性,但在组件实现中从未使用该属性。如果暂不需要,建议移除以避免混淆;如果是后续功能预留,建议添加 TODO 注释说明。如果暂不需要,建议移除
interface ProviderVendorViewProps { providers: ProviderDisplay[]; currentUser?: User; enableMultiProviderTypes: boolean; - healthStatus: Record<number, any>; statistics: Record<number, any>; statisticsLoading: boolean; currencyCode: CurrencyCode; }
306-310: 错误处理可能产生不明确的错误信息当
res.ok为false时,res.error可能为undefined,导致抛出Error: undefined。建议添加回退消息。建议的改进
queryFn: async () => { const res = await getVendorTypeCircuitInfo({ vendorId, providerType }); - if (!res.ok) throw new Error(res.error); + if (!res.ok) throw new Error(res.error || "Failed to fetch circuit info"); return res.data; },src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx (1)
7-7: 复制提示的定时器缺少清理在组件卸载或重复点击时可能触发卸载后的 setState。建议用 useRef 保存计时器并在 unmount 时清理。
Proposed refactor
-import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; @@ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [editOpen, setEditOpen] = useState(false); + const copyResetTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); @@ useEffect(() => { setClipboardAvailable(isClipboardSupported()); }, []); + + useEffect(() => { + return () => { + if (copyResetTimerRef.current) { + clearTimeout(copyResetTimerRef.current); + } + }; + }, []); @@ setCopied(true); toast.success(tList("keyCopied")); - setTimeout(() => setCopied(false), 3000); + if (copyResetTimerRef.current) { + clearTimeout(copyResetTimerRef.current); + } + copyResetTimerRef.current = setTimeout(() => setCopied(false), 3000); };Also applies to: 342-345
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Cache: Disabled due to Reviews > Disable Cache setting
📒 Files selected for processing (6)
src/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsxsrc/app/[locale]/settings/providers/_components/forms/provider-form.tsxsrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsxsrc/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsxtests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsxtests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- tests/unit/settings/providers/vendor-keys-compact-list-ui.test.tsx
- src/app/[locale]/settings/providers/_components/endpoint-latency-sparkline.tsx
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,ts,tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Never use emoji characters in any code, comments, or string literals
Files:
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsxsrc/app/[locale]/settings/providers/_components/forms/provider-form.tsxtests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsxsrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Use path alias@/to reference files in./src/directory
Format code with Biome using: double quotes, trailing commas, 2-space indent, 100 character line width
Files:
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsxsrc/app/[locale]/settings/providers/_components/forms/provider-form.tsxtests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsxsrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer named exports over default exports
Files:
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsxsrc/app/[locale]/settings/providers/_components/forms/provider-form.tsxtests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsxsrc/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
All new features must have unit test coverage of at least 80%
Files:
tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
tests/**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use Vitest for unit testing and happy-dom for DOM testing
Files:
tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
🧠 Learnings (7)
📚 Learning: 2026-01-05T03:02:14.502Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 539
File: src/app/[locale]/dashboard/_components/user/user-key-table-row.tsx:66-66
Timestamp: 2026-01-05T03:02:14.502Z
Learning: In the claude-code-hub project, the translations.actions.addKey field in UserKeyTableRowProps is defined as optional for backward compatibility, but all actual callers in the codebase provide the complete translations object. The field has been added to all 5 locale files (messages/{locale}/dashboard.json).
Applied to files:
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to tests/**/*.test.{ts,tsx,js,jsx} : Use Vitest for unit testing and happy-dom for DOM testing
Applied to files:
tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx
📚 Learning: 2026-01-10T06:20:32.687Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx:118-125
Timestamp: 2026-01-10T06:20:32.687Z
Learning: In `src/app/[locale]/settings/prices/_components/sync-litellm-button.tsx`, the "Cancel" button in the SyncConflictDialog is intentionally designed to call `onConfirm([])`, which triggers `doSync([])` to continue the sync while skipping (not overwriting) conflicting manual prices. This is the desired product behavior to allow users to proceed with LiteLLM sync for non-conflicting models while preserving their manual price entries.
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T06:20:04.478Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: src/actions/model-prices.ts:275-335
Timestamp: 2026-01-10T06:20:04.478Z
Learning: In the `ding113/claude-code-hub` repository, Server Actions (files under `src/actions/*.ts`) currently return hardcoded Chinese error messages directly. This is a codebase-wide architectural decision that applies to all action files (e.g., model-prices.ts, users.ts, system-config.ts). Changing this pattern requires a coordinated i18n refactor across all Server Actions, which should be handled as a separate unified task rather than piecemeal changes in individual PRs.
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : All user-facing strings must use i18n (5 languages supported: zh-CN, zh-TW, en, ja, ru). Never hardcode display text
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T06:20:19.207Z
Learnt from: NieiR
Repo: ding113/claude-code-hub PR: 573
File: messages/ja/dashboard.json:1342-1345
Timestamp: 2026-01-10T06:20:19.207Z
Learning: In this project, minor i18n formatting issues (such as spacing between numbers and units in Japanese/Chinese) are accepted when fixing them would require adding template complexity. The approach prioritizes code simplicity over perfect locale-specific formatting for minor cosmetic issues.
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
📚 Learning: 2026-01-10T17:53:25.066Z
Learnt from: CR
Repo: ding113/claude-code-hub PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-10T17:53:25.066Z
Learning: Use `next-intl` for internationalization with support for 5 languages: zh-CN, zh-TW, en, ja, ru
Applied to files:
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
🧬 Code graph analysis (4)
src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx (6)
src/app/[locale]/settings/providers/_components/forms/provider-form.tsx (1)
ProviderForm(111-2215)src/actions/provider-endpoints.ts (1)
getProviderEndpoints(160-186)src/actions/providers.ts (3)
editProvider(599-730)removeProvider(733-772)getUnmaskedProviderKey(1354-1381)src/lib/provider-type-utils.tsx (2)
getProviderTypeConfig(55-57)getProviderTypeTranslationKey(65-67)src/lib/utils/clipboard.ts (2)
isClipboardSupported(5-12)copyToClipboard(18-30)src/app/[locale]/settings/providers/_components/inline-edit-popover.tsx (1)
InlineEditPopover(22-185)
src/app/[locale]/settings/providers/_components/forms/provider-form.tsx (3)
src/types/provider.ts (5)
ProviderType(6-12)CodexReasoningEffortPreference(17-24)CodexReasoningSummaryPreference(26-26)CodexTextVerbosityPreference(28-28)CodexParallelToolCallsPreference(31-31)src/app/[locale]/settings/providers/_components/forms/url-preview.tsx (1)
UrlPreview(31-162)src/lib/provider-type-utils.tsx (1)
getProviderTypeConfig(55-57)
tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx (2)
src/types/user.ts (1)
User(4-32)src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (1)
ProviderVendorView(94-160)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (4)
src/types/provider.ts (4)
ProviderDisplay(147-217)ProviderVendor(375-383)ProviderType(6-12)ProviderEndpoint(387-404)src/lib/provider-type-utils.tsx (2)
getProviderTypeConfig(55-57)getProviderTypeTranslationKey(65-67)src/app/[locale]/settings/providers/_components/forms/url-preview.tsx (1)
UrlPreview(31-162)src/lib/utils/error-messages.ts (1)
getErrorMessage(157-169)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: 🌐 API Tests
- GitHub Check: Docker Build Test
- GitHub Check: dev-build-deploy
🔇 Additional comments (12)
tests/unit/settings/providers/provider-vendor-view-circuit-ui.test.tsx (3)
80-94: 测试用户常量定义正确
ADMIN_USER包含了User类型所需的所有必填字段,且类型安全,无需使用as any类型断言。
139-149: 测试设置结构良好
beforeEach正确地重置QueryClient、清理 DOM 并重置 mocks。使用vi.hoisted()确保 mocks 在模块加载前初始化。
151-188: 测试用例覆盖基本场景当前测试验证了电路断开(open)和关闭(closed)两种核心状态的 UI 表现。建议后续补充以下场景的测试:
half-open状态manualOpen: true时的 UI 差异- 加载状态和错误状态
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (4)
535-628: 添加端点对话框实现正确表单处理、加载状态管理和查询失效逻辑都符合最佳实践。使用
useEffect在对话框关闭时重置表单状态是好的做法。
706-791: 删除供应商对话框实现优秀两步确认流程、加载状态处理、i18n 错误消息以及对话框关闭时重置状态,都是很好的 UX 实践。
94-160: 主组件结构清晰
ProviderVendorView使用命名导出、正确处理加载状态、使用useMemo优化派生数据计算。组件分解合理,各子组件职责明确。
344-356: 当前实现已正确处理所有可能的电路断路器状态VendorTypeCircuitInfo 只定义了两种状态:
"closed"和"open",不包含"half-open"状态。当前实现在circuitState === "open"时显示重置按钮,其他情况(即"closed"状态)返回 null,这已经正确处理了所有可能的状态。无需额外处理 "half-open" 状态。(注:
"half-open"状态用于其他电路断路器实现如 EndpointCircuit,但不适用于此处的 VendorTypeCircuit)src/app/[locale]/settings/providers/_components/forms/provider-form.tsx (5)
36-36: ProviderForm 扩展 API 与类型标签渲染集成良好
新增 props、类型配置读取与标签渲染逻辑清晰,能支撑多端点/多类型的表单需求。Also applies to: 69-78, 125-148
420-429: 提交前的 no-endpoints 保护合理
在隐藏 URL 场景下补充提示,逻辑清晰。
718-744: 隐藏 URL 时的空态与加载态提示清晰
空态与自动解析中的提示对用户友好。
767-782: 网站地址可选渲染逻辑 OK
开关式展示和校验保持一致。
1071-1134: 路由参数输入的校验与提示保持一致
数值处理与说明文本一致,整体 OK。
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| const [name, setName] = useState( | ||
| isEdit ? (provider?.name ?? "") : cloneProvider ? `${cloneProvider.name}_Copy` : "" | ||
| isEdit | ||
| ? (provider?.name ?? "") | ||
| : cloneProvider | ||
| ? `${cloneProvider.name}_Copy` | ||
| : (preset?.name ?? "") | ||
| ); | ||
| const [url, setUrl] = useState(sourceProvider?.url ?? ""); | ||
| const [url, setUrl] = useState(sourceProvider?.url ?? preset?.url ?? ""); | ||
| const [key, setKey] = useState(""); // 编辑时留空代表不更新 | ||
| const [providerType, setProviderType] = useState<ProviderType>( | ||
| sourceProvider?.providerType ?? "claude" | ||
| sourceProvider?.providerType ?? preset?.providerType ?? "claude" | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
克隆名称默认值使用了硬编码字符串
${cloneProvider.name}_Copy 会直接出现在输入框中,应走 i18n。
Proposed fix
- : cloneProvider
- ? `${cloneProvider.name}_Copy`
- : (preset?.name ?? "")
+ : cloneProvider
+ ? t("name.cloneDefault", { name: cloneProvider.name })
+ : (preset?.name ?? "")🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/forms/provider-form.tsx
around lines 156 - 167, The default cloned name uses a hardcoded suffix
(`${cloneProvider.name}_Copy`) in the name useState initialization; change it to
use the app i18n instead by obtaining the translation function (e.g.,
useTranslation or t) and building the default as
`${cloneProvider.name}${t('provider.copySuffix')}` (or another existing i18n
key) so the user-visible suffix is localized; update the name useState
initializer in provider-form (and import/use the translation hook at the top of
the component) and ensure the chosen i18n key is added to translation files.
| const [websiteUrl, setWebsiteUrl] = useState<string>( | ||
| sourceProvider?.websiteUrl ?? preset?.websiteUrl ?? "" | ||
| ); | ||
| const [autoUrlPending, setAutoUrlPending] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| if (isEdit) return; | ||
| if (!hideUrl || !urlResolver) return; | ||
|
|
||
| let cancelled = false; | ||
| setAutoUrlPending(true); | ||
| urlResolver(providerType) | ||
| .then((resolved) => { | ||
| if (cancelled) return; | ||
| setUrl(resolved?.trim() ? resolved.trim() : ""); | ||
| }) | ||
| .catch(() => { | ||
| if (cancelled) return; | ||
| setUrl(""); | ||
| }) | ||
| .finally(() => { | ||
| if (cancelled) return; | ||
| setAutoUrlPending(false); | ||
| }); | ||
|
|
||
| return () => { | ||
| cancelled = true; | ||
| }; | ||
| }, [isEdit, hideUrl, urlResolver, providerType]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
自动解析可能覆盖已有 preset URL
hideUrl + urlResolver 下会在 providerType 变化时重写 url,即使已有 preset/url 也会被覆盖。建议在已有值时跳过解析。
Proposed fix
useEffect(() => {
if (isEdit) return;
if (!hideUrl || !urlResolver) return;
+ if (url.trim()) return;
let cancelled = false;
setAutoUrlPending(true);
@@
- }, [isEdit, hideUrl, urlResolver, providerType]);
+ }, [isEdit, hideUrl, urlResolver, providerType, url]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const [websiteUrl, setWebsiteUrl] = useState<string>( | |
| sourceProvider?.websiteUrl ?? preset?.websiteUrl ?? "" | |
| ); | |
| const [autoUrlPending, setAutoUrlPending] = useState(false); | |
| useEffect(() => { | |
| if (isEdit) return; | |
| if (!hideUrl || !urlResolver) return; | |
| let cancelled = false; | |
| setAutoUrlPending(true); | |
| urlResolver(providerType) | |
| .then((resolved) => { | |
| if (cancelled) return; | |
| setUrl(resolved?.trim() ? resolved.trim() : ""); | |
| }) | |
| .catch(() => { | |
| if (cancelled) return; | |
| setUrl(""); | |
| }) | |
| .finally(() => { | |
| if (cancelled) return; | |
| setAutoUrlPending(false); | |
| }); | |
| return () => { | |
| cancelled = true; | |
| }; | |
| }, [isEdit, hideUrl, urlResolver, providerType]); | |
| const [websiteUrl, setWebsiteUrl] = useState<string>( | |
| sourceProvider?.websiteUrl ?? preset?.websiteUrl ?? "" | |
| ); | |
| const [autoUrlPending, setAutoUrlPending] = useState(false); | |
| useEffect(() => { | |
| if (isEdit) return; | |
| if (!hideUrl || !urlResolver) return; | |
| if (url.trim()) return; | |
| let cancelled = false; | |
| setAutoUrlPending(true); | |
| urlResolver(providerType) | |
| .then((resolved) => { | |
| if (cancelled) return; | |
| setUrl(resolved?.trim() ? resolved.trim() : ""); | |
| }) | |
| .catch(() => { | |
| if (cancelled) return; | |
| setUrl(""); | |
| }) | |
| .finally(() => { | |
| if (cancelled) return; | |
| setAutoUrlPending(false); | |
| }); | |
| return () => { | |
| cancelled = true; | |
| }; | |
| }, [isEdit, hideUrl, urlResolver, providerType, url]); |
🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/forms/provider-form.tsx
around lines 286 - 314, The auto-resolve effect overwrites an existing
preset/website URL; update the useEffect in provider-form.tsx to skip calling
urlResolver when a preset or current websiteUrl exists (i.e., only run when
websiteUrl is empty and preset?.websiteUrl is falsy), and make sure to call the
correct state setter (setWebsiteUrl) instead of setUrl in the then/catch/finally
branches; keep the cancelled guard and autoUrlPending handling as-is.
| {( | ||
| allowedProviderTypes ?? [ | ||
| "claude", | ||
| "claude-auth", | ||
| "codex", | ||
| "gemini", | ||
| "gemini-cli", | ||
| "openai-compatible", | ||
| ] | ||
| ).map((type) => { | ||
| const typeConfig = getProviderTypeConfig(type); | ||
| const TypeIcon = typeConfig.icon; | ||
| const label = renderProviderTypeLabel(type); | ||
| const disabled = | ||
| type === "openai-compatible" ? !enableMultiProviderTypes : false; | ||
| return ( | ||
| <SelectItem key={type} value={type} disabled={disabled}> | ||
| <div className="flex items-center gap-2"> | ||
| <span | ||
| className={`inline-flex h-5 w-5 items-center justify-center rounded ${typeConfig.bgColor}`} | ||
| > | ||
| <TypeIcon className={`h-3.5 w-3.5 ${typeConfig.iconColor}`} /> | ||
| </span> | ||
| <span>{label}</span> | ||
| </div> | ||
| </SelectItem> | ||
| ); | ||
| })} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
allowedProviderTypes 可能与当前 providerType 不一致
当 preset/providerType 不在 allowedProviderTypes 中时,Select 没有对应项但仍可提交旧值,易导致 UI/校验异常。建议在本地进行兜底纠正或上层保证包含当前类型。
Proposed fix
const [providerType, setProviderType] = useState<ProviderType>(
sourceProvider?.providerType ?? preset?.providerType ?? "claude"
);
+ useEffect(() => {
+ if (!allowedProviderTypes?.length) return;
+ if (!allowedProviderTypes.includes(providerType)) {
+ setProviderType(allowedProviderTypes[0]);
+ }
+ }, [allowedProviderTypes, providerType]);🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/forms/provider-form.tsx
around lines 860 - 887, The options mapping can omit the current providerType,
causing stale/invalid selections to be submitted; update the options generation
so that if providerType is set but not included in allowedProviderTypes you
insert it into the list (preferably as the first or last item and marked
disabled or annotated) before mapping; adjust the disabled logic that uses
enableMultiProviderTypes accordingly so the inserted fallback item uses the same
disabled rules, and keep using getProviderTypeConfig and renderProviderTypeLabel
to render its icon/label so the SelectItem shows a visible, non-crashing
fallback for providerType.
| <div className="space-y-2"> | ||
| <Label>{t("sections.routing.cacheTtl.label")}</Label> | ||
| <Select | ||
| value={cacheTtlPreference} | ||
| onValueChange={(val) => setCacheTtlPreference(val as "inherit" | "5m" | "1h")} | ||
| disabled={isPending} | ||
| > | ||
| <SelectTrigger className="w-full"> | ||
| <SelectValue placeholder="inherit" /> | ||
| </SelectTrigger> | ||
| <SelectContent> | ||
| <SelectItem value="inherit"> | ||
| {t("sections.routing.cacheTtl.options.inherit")} | ||
| </SelectItem> | ||
| <SelectItem value="5m"> | ||
| {t("sections.routing.cacheTtl.options.5m")} | ||
| </SelectItem> | ||
| <SelectItem value="1h"> | ||
| {t("sections.routing.cacheTtl.options.1h")} | ||
| </SelectItem> | ||
| </SelectContent> | ||
| </Select> | ||
| <p className="text-xs text-muted-foreground"> | ||
| {t("sections.routing.cacheTtl.desc")} | ||
| </p> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SelectValue 的 placeholder 存在硬编码
多处 placeholder="inherit" 属于用户可见文本,需使用 i18n。
Proposed fix
- <SelectValue placeholder="inherit" />
+ <SelectValue placeholder={t("sections.routing.cacheTtl.options.inherit")} />Also applies to: 1162-1192, 1194-1330
🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/forms/provider-form.tsx
around lines 1135 - 1160, Replace the hardcoded SelectValue placeholder text
with the i18n lookup: use the page's translation function t(...) instead of
literal "inherit" for the SelectValue placeholder and for other SelectValue
occurrences (e.g., the Select using cacheTtlPreference / SelectValue placeholder
and the Context1m/Codex SelectValue placeholders mentioned); locate the Select
components (e.g., the Select with value={cacheTtlPreference} and its
SelectValue) and set
placeholder={t("sections.routing.cacheTtl.options.inherit")} (and equivalent
i18n keys for the other SelectValue placeholders) so all user-visible strings
flow through t(...).
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx
Outdated
Show resolved
Hide resolved
| function buildDefaultProviderName(input: { | ||
| vendorWebsiteDomain: string; | ||
| providerType: ProviderType; | ||
| }): string { | ||
| const base = input.vendorWebsiteDomain.trim() || "vendor"; | ||
| return `${base}-${input.providerType}`.slice(0, 64); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
默认名称使用硬编码文案,需本地化
Line 54 的 "vendor" 会出现在用户可见的默认名称中,应通过 i18n 提供本地化文案。建议将回退名称作为参数从组件中传入。As per coding guidelines, 所有用户可见文本需使用 i18n。
Proposed fix
-function buildDefaultProviderName(input: {
- vendorWebsiteDomain: string;
- providerType: ProviderType;
-}): string {
- const base = input.vendorWebsiteDomain.trim() || "vendor";
- return `${base}-${input.providerType}`.slice(0, 64);
-}
+function buildDefaultProviderName(input: {
+ vendorWebsiteDomain: string;
+ providerType: ProviderType;
+ fallbackName: string;
+}): string {
+ const base = input.vendorWebsiteDomain.trim() || input.fallbackName;
+ return `${base}-${input.providerType}`.slice(0, 64);
+}- preset={{
- name: buildDefaultProviderName({
- vendorWebsiteDomain: props.vendorWebsiteDomain,
- providerType: defaultProviderType,
- }),
- providerType: defaultProviderType,
- websiteUrl: props.vendorWebsiteUrl ?? "",
- }}
+ preset={{
+ name: buildDefaultProviderName({
+ vendorWebsiteDomain: props.vendorWebsiteDomain,
+ providerType: defaultProviderType,
+ fallbackName: t("vendorFallbackName"),
+ }),
+ providerType: defaultProviderType,
+ websiteUrl: props.vendorWebsiteUrl ?? "",
+ }}Also applies to: 112-118
🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
around lines 50 - 55, The hardcoded fallback "vendor" in
buildDefaultProviderName (used with vendorWebsiteDomain and providerType) must
be localized and not embedded; change buildDefaultProviderName to accept a
fallbackName parameter (or an i18n function) and use that instead of the literal
"vendor", then update callers (including the other usage around the block at
lines ~112-118) to pass a localized string from the component via the i18n hook
(e.g., t('...')) so all user-visible defaults come from localized resources.
| <TableCell className="hidden md:table-cell py-1 text-right text-sm tabular-nums"> | ||
| {props.canEdit ? ( | ||
| <InlineEditPopover | ||
| value={props.provider.costMultiplier} | ||
| label={tInline("costMultiplierLabel")} | ||
| validator={validateCostMultiplier} | ||
| onSave={handleSaveCostMultiplier} | ||
| suffix="x" | ||
| type="number" | ||
| /> | ||
| ) : ( | ||
| <span className="text-xs">{props.provider.costMultiplier}x</span> | ||
| )} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
倍率后缀 "x" 为硬编码展示文本
Line 420 和 Line 424 的 "x" 会直接展示在 UI 中,应通过 i18n 提供可翻译后缀。As per coding guidelines, 所有用户可见文本需使用 i18n。
Proposed fix
const tInline = useTranslations("settings.providers.inlineEdit");
const tTypes = useTranslations("settings.providers.types");
+ const multiplierSuffix = tList("costMultiplierSuffix");
@@
<InlineEditPopover
value={props.provider.costMultiplier}
label={tInline("costMultiplierLabel")}
validator={validateCostMultiplier}
onSave={handleSaveCostMultiplier}
- suffix="x"
+ suffix={multiplierSuffix}
type="number"
/>
) : (
- <span className="text-xs">{props.provider.costMultiplier}x</span>
+ <span className="text-xs">
+ {props.provider.costMultiplier}
+ {multiplierSuffix}
+ </span>
)}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <TableCell className="hidden md:table-cell py-1 text-right text-sm tabular-nums"> | |
| {props.canEdit ? ( | |
| <InlineEditPopover | |
| value={props.provider.costMultiplier} | |
| label={tInline("costMultiplierLabel")} | |
| validator={validateCostMultiplier} | |
| onSave={handleSaveCostMultiplier} | |
| suffix="x" | |
| type="number" | |
| /> | |
| ) : ( | |
| <span className="text-xs">{props.provider.costMultiplier}x</span> | |
| )} | |
| <TableCell className="hidden md:table-cell py-1 text-right text-sm tabular-nums"> | |
| {props.canEdit ? ( | |
| <InlineEditPopover | |
| value={props.provider.costMultiplier} | |
| label={tInline("costMultiplierLabel")} | |
| validator={validateCostMultiplier} | |
| onSave={handleSaveCostMultiplier} | |
| suffix={tInline("costMultiplierSuffix")} | |
| type="number" | |
| /> | |
| ) : ( | |
| <span className="text-xs"> | |
| {props.provider.costMultiplier} | |
| {tInline("costMultiplierSuffix")} | |
| </span> | |
| )} |
🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
around lines 413 - 425, The UI currently hardcodes the multiplier suffix "x" in
the InlineEditPopover suffix prop and in the read-only span; replace both
occurrences to use the i18n helper (e.g., tInline) instead of the literal "x".
Update InlineEditPopover's suffix argument to tInline("costMultiplierSuffix")
and render the read-only value using tInline("costMultiplierSuffix") so the
suffix is translatable; locate these in the cost multiplier cell around
InlineEditPopover and the span that renders props.provider.costMultiplier.
| <Switch | ||
| checked={props.provider.isEnabled} | ||
| onCheckedChange={(checked) => toggleMutation.mutate(checked)} | ||
| className="scale-75 data-[state=checked]:bg-green-500" | ||
| /> | ||
| )} | ||
| {props.canEdit && ( | ||
| <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> | ||
| <AlertDialogTrigger asChild> | ||
| <Button | ||
| variant="ghost" | ||
| size="icon" | ||
| className="h-7 w-7 text-destructive hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity" | ||
| disabled={deleteMutation.isPending} | ||
| > | ||
| {deleteMutation.isPending ? ( | ||
| <Loader2 className="h-4 w-4 animate-spin" /> | ||
| ) : ( | ||
| <Trash2 className="h-4 w-4" /> | ||
| )} | ||
| </Button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
图标按钮与开关缺少可访问性标签
Switch 与图标按钮在无可见文本时应提供 aria-label,建议使用 i18n 文案并包含 provider 名称以提升可访问性。As per coding guidelines, 用户可见文本需使用 i18n。
Proposed fix
{props.canEdit && (
<Switch
checked={props.provider.isEnabled}
onCheckedChange={(checked) => toggleMutation.mutate(checked)}
className="scale-75 data-[state=checked]:bg-green-500"
+ aria-label={tList("toggleProvider", { name: props.provider.name })}
/>
)}
@@
<AlertDialogTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
disabled={deleteMutation.isPending}
+ aria-label={tList("deleteButton")}
>
@@
{clipboardAvailable && (
- <Button onClick={handleCopy} disabled={!unmaskedKey} size="icon" variant="outline">
+ <Button
+ onClick={handleCopy}
+ disabled={!unmaskedKey}
+ size="icon"
+ variant="outline"
+ aria-label={tList("copyButton")}
+ >Also applies to: 558-563
🤖 Prompt for AI Agents
In `@src/app/`[locale]/settings/providers/_components/vendor-keys-compact-list.tsx
around lines 487 - 507, Add accessible aria-labels to the interactive elements
that lack visible text: for the Switch (checked={props.provider.isEnabled} /
onCheckedChange calling toggleMutation.mutate) and for the icon-only Button
inside AlertDialogTrigger (which shows Trash2 and uses deleteMutation and
deleteDialogOpen/setDeleteDialogOpen). Use i18n strings (translated messages)
that include the provider name (props.provider.name) to form labels like "Enable
{provider}" / "Disable {provider}" or "Delete {provider} key" as appropriate,
and pass them via aria-label on the Switch and the Button so screen readers get
descriptive text.
…ap UI - Add tabs layout to availability page (Provider Availability | Endpoint Probe History) - Create heatmap grid for endpoint probes grouped by provider type (claude, gemini, codex, openai-compatible) - Add batch probe logs API endpoint for efficient data fetching - Create reusable components: endpoint-url-display, probe-cell-tooltip, endpoint-probe-row - Add i18n translations for all 5 languages (en, zh-CN, zh-TW, ja, ru) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create new provider form v2 with tabbed layout (General, Routing, Rate Limits, Resilience, Advanced, Test) - Add provider form context for shared state management - Refactor form sections into dedicated tab components - Update i18n translations for form tabs and sections (all 5 languages) - Minor fixes to existing provider components Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
src/app/[locale]/settings/providers/_components/provider-rich-list-item.tsx (1)
673-686: 在编辑和克隆提供商后需要同时刷新 provider-vendors 缓存。当前编辑和克隆的 onSuccess 回调中缺少
queryClient.invalidateQueries({ queryKey: ["provider-vendors"] })调用。删除提供商时已包含此缓存失效操作,而编辑/克隆应遵循相同模式以保持缓存一致性。建议修改
onSuccess={() => { setOpenEdit(false); queryClient.invalidateQueries({ queryKey: ["providers"] }); queryClient.invalidateQueries({ queryKey: ["providers-health"] }); + queryClient.invalidateQueries({ queryKey: ["provider-vendors"] }); router.refresh(); }}onSuccess={() => { setOpenClone(false); queryClient.invalidateQueries({ queryKey: ["providers"] }); queryClient.invalidateQueries({ queryKey: ["providers-health"] }); + queryClient.invalidateQueries({ queryKey: ["provider-vendors"] }); router.refresh(); }}src/app/[locale]/settings/providers/_components/provider-list-item.legacy.tsx (3)
128-143: 硬编码的中文字符串违反 i18n 规范。根据编码规范,所有面向用户的字符串必须使用 i18n(支持 5 种语言:zh-CN、zh-TW、en、ja、ru)。此处的 toast 消息应使用翻译函数。
示例:
"熔断器已重置"→t("circuitBreaker.resetSuccess")"供应商 "{item.name}" 的熔断状态已解除"→t("circuitBreaker.resetSuccessDesc", { name: item.name })
210-217: 禁止在代码中使用 emoji 字符。根据编码规范,不得在代码、注释或字符串字面量中使用 emoji 字符。
🔴应替换为非 emoji 的状态指示器(如图标组件或 CSS 样式)。建议修复
- <Badge variant="destructive" className="text-xs h-5 px-2"> - 🔴 熔断中 + <Badge variant="destructive" className="text-xs h-5 px-2 gap-1"> + <span className="h-2 w-2 rounded-full bg-destructive-foreground" /> + {t("circuitBreaker.open")}
256-263: 同样存在 emoji 和硬编码字符串问题。
🟡 恢复中同样违反了禁止使用 emoji 和必须使用 i18n 的规范。messages/en/settings/providers/form/sections.json (1)
252-254: 使用了 emoji 字符 "✓",违反编码规范。根据编码规范,不应在代码、注释或字符串字面量中使用 emoji 字符。Line 253 的
"allowAll": "✓ Allow all models (recommended)"包含了 emoji 勾号。建议的修复方案
- "allowAll": "✓ Allow all models (recommended)", + "allowAll": "Allow all models (recommended)",或者使用文本标记:
- "allowAll": "✓ Allow all models (recommended)", + "allowAll": "[Default] Allow all models (recommended)",
🤖 Fix all issues with AI agents
In `@src/app/`[locale]/dashboard/availability/_components/endpoint-probe-row.tsx:
- Around line 119-128: The Probe Button in the EndpointProbeRow component is
missing an accessibility label; add aria-label={t("probeNow")} to the Button
element (the one using onClick={handleProbe}, disabled={probing} and rendering
<Play .../>), and ensure the translation function t is imported or available in
the component scope (e.g., via useTranslation or passed props) so the aria-label
resolves correctly for screen readers and keyboard users.
- Around line 67-89: Summary: Hard-coded badge texts ("N/A", "OK", "ERR") and
tooltip strings ("OK", "Error") must be localized. Update the JSX that renders
the Badge when probeStatus (variable) is null/true/false in the
endpoint-probe-row component to use the i18n translation function (e.g.,
t('availability.na'), t('availability.ok'), t('availability.err')) instead of
raw strings, and similarly replace the literal "OK" and "Error" in
probe-cell-tooltip (referenced symbol: the tooltip render at line ~55) with
corresponding translation keys; ensure the component imports/uses the project's
translation hook (useTranslation or t) and add the new keys to the locale files.
In
`@src/app/`[locale]/dashboard/availability/_components/endpoint-url-display.tsx:
- Around line 110-112: The truncation math in endpoint-url-display.tsx can
produce a negative slice end when availableForPath < 3, causing incorrect
outputs; update the logic around truncatedSegment and lastSegment so you clamp
the slice length to a safe non-negative (and preferably at least 1) value before
slicing (e.g., compute const sliceLen = Math.max(0, availableForPath - 3) or use
Math.max(1, ...) if you want at least one visible char), then slice lastSegment
with that clamped value and append "..." only when truncation actually occurred;
ensure the final returned string still respects the intended maxLength by
checking if lastSegment.length <= sliceLen and return host/ellipsis/lastSegment
unmodified in that case.
In `@src/app/`[locale]/dashboard/availability/_components/probe-cell-tooltip.tsx:
- Around line 58-62: The latency display currently uses a truthy check
({probe.latencyMs && ...}) which hides 0ms; update the conditional in
probe-cell-tooltip.tsx to render when latency is present even if zero by using
an explicit null/undefined check (e.g., probe.latencyMs !== undefined &&
probe.latencyMs !== null or probe.latencyMs != null) around the JSX that renders
probe.latencyMs so 0 is shown as "0ms".
- Around line 20-27: The date formatting uses toLocaleString(undefined, ...)
which follows the browser locale; update formatTime in probe-cell-tooltip.tsx to
use the app locale from next-intl by importing and calling useLocale() and
passing that locale into toLocaleString (e.g. toLocaleString(locale, {...})); if
formatTime is defined outside the component, change it to accept a locale
parameter or move it inside the component so you can call useLocale() directly;
ensure you add the import { useLocale } from "next-intl" and propagate the
locale into formatTime to guarantee the app's language is used.
- Around line 53-56: Replace the hardcoded visible strings "OK" and "Error" in
the status span with i18n translation calls; in the probe-cell-tooltip component
(the span that currently renders {probe.statusCode ?? (probe.ok ? "OK" :
"Error")}) use the translation helper t(...) for the fallback values (e.g.
t("status.ok") and t("status.error") or your project's equivalent keys) so the
user-visible status text is localized.
In
`@src/app/`[locale]/settings/providers/_components/forms/v2/provider-form-v2.tsx:
- Around line 148-171: The state initializers for rate-limit fields (limit5hUsd,
limitDailyUsd, dailyResetMode, dailyResetTime, limitWeeklyUsd, limitMonthlyUsd,
limitTotalUsd, limitConcurrentSessions) use `isEdit ? provider?.field || null :
null`, which treats 0 as falsy and converts valid zeros to null; change these
initializations to use nullish coalescing or an explicit undefined check (e.g.
`isEdit ? (provider?.limit5hUsd ?? null) : null` or `isEdit ?
(provider?.limit5hUsd !== undefined ? provider.limit5hUsd : null) : null`) so
zero values from provider are preserved; apply the same fix for each referenced
state variable and keep dailyResetMode/dailyResetTime defaulting to
"rolling"/"00:00" only when provider values are truly undefined.
In
`@src/app/`[locale]/settings/providers/_components/forms/v2/tabs/advanced-tab.tsx:
- Around line 123-142: The Select currently emits "true"/"false" which
mismatches the CodexReasoningSummaryPreference type (allowed: "inherit" | "auto"
| "detailed"); update the SelectItems in the component that uses
codexReasoningSummaryPreference and setCodexReasoningSummaryPreference to use
values "auto" and "detailed" (keep "inherit") and swap the displayed labels to
the correct translation keys (replace tCommon("enabled")/("disabled") with the
appropriate strings for "auto" and "detailed"); ensure the Select value types
match CodexReasoningSummaryPreference so serialization/save logic works
correctly.
In
`@src/app/`[locale]/settings/providers/_components/forms/v2/tabs/general-tab.tsx:
- Around line 66-71: The default providerTypes array is missing "claude-auth"
and "gemini-cli" causing inconsistency with ProviderType and other components;
update the fallback used to compute providerTypes (the expression using
allowedProviderTypes and providerTypes) to include all six types: "claude",
"claude-auth", "gemini", "gemini-cli", "codex", and "openai-compatible" so it
matches the ProviderType definition, renderProviderTypeLabel logic, and the
PROVIDER_TYPES constant in provider-form-v2.tsx.
In
`@src/app/`[locale]/settings/providers/_components/forms/v2/tabs/routing-tab.tsx:
- Around line 119-126: The onChange handler for the Input in routing-tab.tsx
uses parseFloat(e.target.value) || 1 which converts a valid 0 to 1; change the
handler to parse the value and only fall back to 1 when the parsed result is NaN
(or empty), e.g. compute const v = parseFloat(e.target.value); then call
setCostMultiplier(Number.isFinite(v) ? v : 1) (reference the costMultiplier
state and setCostMultiplier function used in the Input onChange).
In `@src/app/api/availability/endpoints/probe-logs/batch/route.ts`:
- Around line 1-52: The GET handler currently uses NextRequest/NextResponse and
must be refactored to Hono: replace the exported async GET(request: NextRequest)
with a Hono handler that accepts ctx (Context) and uses ctx.req and ctx.json for
responses; keep and reuse getSession(), findProviderEndpointProbeLogsBatch(),
and the same validation logic for endpointIds and limit, but return errors with
ctx.json(..., status) instead of NextResponse.json and log errors via
console.error before returning a 500. Follow the Hono + OpenAPIHono pattern used
in the project (e.g. use ctx.getQuery or new URL(ctx.req.url).searchParams for
params), ensure the response body shape ({ logs }) is preserved and convert the
Map to a plain object as before, and export the Hono-compatible handler so the
route conforms to the repository standard.
In `@src/app/api/availability/endpoints/route.ts`:
- Around line 1-61: The route currently implements an exported GET using
NextRequest/NextResponse (functions: GET, isProviderType, PROVIDER_TYPES,
getSession, findAllProviderEndpointsWithVendorInfo,
findProviderEndpointsByVendorAndType) but must be converted to a Hono-style
handler per project conventions; replace the exported GET with a Hono
app/handler (use OpenAPIHono and app.handle or the pattern used in
src/app/api/actions/[...route]/route.ts) and adapt request/session handling to
Hono's Context (ctx.req, ctx.json, ctx.text) while keeping the same validation
logic (vendorId/vendorIdRaw, providerTypeRaw, isProviderType) and error
branches, and ensure you export the Hono handler via handle() so the route
registers under the OpenAPIHono pattern.
♻️ Duplicate comments (6)
src/app/[locale]/settings/providers/_components/provider-vendor-view.tsx (2)
246-246: providerTypes 缺少部分类型。 当前列表未包含claude-auth与gemini-cli,与 ProviderType 取值不一致。Proposed fix
- const providerTypes: ProviderType[] = ["claude", "gemini", "codex", "openai-compatible"]; + const providerTypes: ProviderType[] = [ + "claude", + "claude-auth", + "gemini", + "gemini-cli", + "codex", + "openai-compatible", + ];
483-485: 相对时间未传 locale 导致多语言失效。formatDistanceToNow默认输出英文,需按当前应用语言映射 date-fns locale。依据编码规范。src/repository/provider-endpoints.ts (1)
334-349: deleteProviderVendor 与 FK restrict 语义不一致,存在误删风险。
providers.providerVendorId 为 onDelete: restrict,这里直接物理删除 providers 可能绕过软删除语义并造成数据丢失。建议改为软删除 providers,或将 FK 调整为 cascade 并与业务语义一致。建议修改
export async function deleteProviderVendor(vendorId: number): Promise<boolean> { const deleted = await db.transaction(async (tx) => { // 1. Delete endpoints (cascade would handle this, but manual is safe) await tx.delete(providerEndpoints).where(eq(providerEndpoints.vendorId, vendorId)); - // 2. Delete providers (keys) - explicit delete required due to 'restrict' - await tx.delete(providers).where(eq(providers.providerVendorId, vendorId)); + // 2. Soft delete providers to respect FK restrict semantics + const now = new Date(); + await tx + .update(providers) + .set({ deletedAt: now, isEnabled: false, updatedAt: now }) + .where(and(eq(providers.providerVendorId, vendorId), isNull(providers.deletedAt))); + // 3. Hard delete already soft-deleted providers + await tx + .delete(providers) + .where(and(eq(providers.providerVendorId, vendorId), isNotNull(providers.deletedAt))); // 3. Delete vendor const result = await tx .delete(providerVendors) .where(eq(providerVendors.id, vendorId))src/app/[locale]/settings/providers/_components/vendor-keys-compact-list.tsx (3)
50-56: 默认名称回退值仍为硬编码。
回退值"vendor"会进入用户可见默认名称,应通过 i18n 提供并由调用方传入。As per coding guidelines, 所有用户可见文本需使用 i18n。
413-425: 倍率后缀仍为硬编码。
suffix="x"与展示文本需要可翻译。As per coding guidelines, 所有用户可见文本需使用 i18n。
487-507: 图标按钮与开关缺少 aria-label。
Switch、删除按钮、复制按钮在无可见文本时需提供 aria-label,并使用 i18n 文案。As per coding guidelines, 所有用户可见文本需使用 i18n。Also applies to: 558-563
🧹 Nitpick comments (10)
src/app/[locale]/settings/providers/_components/forms/v2/tabs/routing-tab.tsx (1)
9-11: 建议使用@/路径别名替代深层相对路径。
当前相对路径较深,可读性与重构成本偏高,建议按规范改为@/引用。As per coding guidelines, ...建议修改
-import { ModelMultiSelect } from "../../../model-multi-select"; -import { ModelRedirectEditor } from "../../../model-redirect-editor"; -import { useProviderForm } from "../provider-form-context"; +import { ModelMultiSelect } from "@/app/[locale]/settings/providers/_components/model-multi-select"; +import { ModelRedirectEditor } from "@/app/[locale]/settings/providers/_components/model-redirect-editor"; +import { useProviderForm } from "@/app/[locale]/settings/providers/_components/forms/v2/provider-form-context";src/app/[locale]/settings/providers/_components/forms/v2/tabs/test-tab.tsx (1)
32-41: 避免直接断言 providerType 为 ProviderType。
ProviderFormState 允许 string,直接断言可能把空值/未知值传入 ApiTestButton,导致测试请求参数不合法。建议在此处加类型守卫或在上下文层收窄类型,并在无效时禁用按钮。src/repository/provider-endpoints.ts (1)
603-665: 更新 URL 时建议做 trim + 校验,避免无效值或隐性重复。
目前 update 直接写入 payload.url,和 create/ensure 的校验策略不一致,可能引入无效 URL 或前后空格导致的重复记录。建议在更新时统一 trim 并校验。建议修改
const now = new Date(); const updates: Partial<typeof providerEndpoints.$inferInsert> = { updatedAt: now }; - if (payload.url !== undefined) updates.url = payload.url; + if (payload.url !== undefined) { + const trimmedUrl = payload.url.trim(); + // eslint-disable-next-line no-new + new URL(trimmedUrl); + updates.url = trimmedUrl; + }messages/zh-TW/settings/providers/form/sections.json (1)
97-106: mcpPassthrough.type 与 mcpPassthrough.select 存在重复。
mcpPassthrough.type(lines 97-103) 的内容与mcpPassthrough.select(lines 70-89) 中的标签高度重复。建议确认这是否为不同 UI 组件的需要,如果可以复用,考虑简化翻译结构。src/app/[locale]/settings/providers/_components/forms/v2/provider-form-layout.tsx (1)
55-78: "Resilience" 和 "Test" 标签页使用了相同的 Activity 图标。第 60 行和第 76 行都使用了
<Activity />图标。考虑为 "Test" 标签页使用不同的图标以增强视觉区分度,例如FlaskConical或TestTube2。建议修复
-import { Activity, Banknote, Bot, Settings2, Sliders } from "lucide-react"; +import { Activity, Banknote, Bot, FlaskConical, Settings2, Sliders } from "lucide-react";<TabsTrigger id="provider-form-tab-test" value="test" className="gap-2 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm" > - <Activity className="h-4 w-4" /> + <FlaskConical className="h-4 w-4" /> <span className="hidden sm:inline">{t("tabs.test")}</span> </TabsTrigger>src/app/[locale]/settings/providers/_components/forms/v2/tabs/advanced-tab.tsx (1)
58-89: 避免使用 any 以保留类型约束。
cacheTtlPreference/context1mPreference的取值域已固定,建议显式联合类型(或导入类型别名)以恢复类型校验。请确认实际类型定义后调整。建议修改
- <Select value={cacheTtlPreference} onValueChange={(v: any) => setCacheTtlPreference(v)}> + <Select + value={cacheTtlPreference} + onValueChange={(v: "inherit" | "5m" | "1h") => setCacheTtlPreference(v)} + >- <Select - value={context1mPreference} - onValueChange={(v: any) => setContext1mPreference(v)} - > + <Select + value={context1mPreference} + onValueChange={(v: "inherit" | "force_enable" | "disabled") => + setContext1mPreference(v) + } + >src/app/[locale]/settings/providers/_components/forms/v2/provider-form-v2.tsx (3)
335-336:proxyUrl缺少 URL 格式验证。
url字段使用了isValidUrl进行验证(line 281-284),但proxyUrl(line 335)在提交时未进行格式验证。虽然代理 URL 支持多种协议(HTTP、HTTPS、SOCKS4、SOCKS5),如果用户输入无效格式可能会导致运行时错误。建议添加代理 URL 格式验证,或至少验证其为有效的 URL 结构。
436-449: 删除确认使用原生confirm()对话框。使用浏览器原生
confirm()对话框可以工作,但与应用的整体 UI 风格可能不一致。考虑使用自定义的确认对话框组件以保持 UX 一致性。
20-31: 使用satisfies确保PROVIDER_TYPES与ProviderType类型定义保持同步。
PROVIDER_TYPES数组与ProviderType类型定义目前保持同步,但建议添加satisfies ProviderType[]注解,以实现编译时类型检查。这样,如果ProviderType类型定义中添加或删除了提供商类型,TypeScript 将强制要求同步更新此数组,防止运行时不匹配。messages/en/settings/providers/form/sections.json (1)
97-106:mcpPassthrough.type与现有mcpPassthrough.select存在重复。与俄语文件相同的问题:新增的
mcpPassthrough.type对象与现有的mcpPassthrough.select对象存在重复的选项定义。请确认是否需要两组键,或者可以合并以减少维护成本。
| {probeStatus === null ? ( | ||
| <Badge | ||
| variant="outline" | ||
| className="text-[10px] px-1.5 py-0.5 bg-gray-100 text-gray-500 border-gray-200 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-700" | ||
| > | ||
| N/A | ||
| </Badge> | ||
| ) : probeStatus ? ( | ||
| <Badge | ||
| variant="outline" | ||
| className="text-[10px] px-1.5 py-0.5 bg-green-100 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800" | ||
| > | ||
| <CheckCircle2 className="w-3 h-3 mr-0.5" /> | ||
| OK | ||
| </Badge> | ||
| ) : ( | ||
| <Badge | ||
| variant="outline" | ||
| className="text-[10px] px-1.5 py-0.5 bg-red-100 text-red-700 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800" | ||
| > | ||
| <XCircle className="w-3 h-3 mr-0.5" /> | ||
| ERR | ||
| </Badge> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Find the file and check its existence
fd -t f "endpoint-probe-row.tsx" --exec wc -l {}Repository: ding113/claude-code-hub
Length of output: 147
🏁 Script executed:
# Read the actual file to see the code at specified lines
fd -t f "endpoint-probe-row.tsx" -x cat -n {} | head -100Repository: ding113/claude-code-hub
Length of output: 4357
🏁 Script executed:
# Check i18n file structure
fd -t f -e json "\.json$" | grep -E "(en|zh-CN|ja)" | head -20Repository: ding113/claude-code-hub
Length of output: 626
🏁 Script executed:
# Search for existing translation keys related to status and legend
rg -t json "status|legend" --max-count 5Repository: ding113/claude-code-hub
Length of output: 26837
🏁 Script executed:
# Read the complete file since it's 131 lines
cat -n src/app/[locale]/dashboard/availability/_components/endpoint-probe-row.tsxRepository: ding113/claude-code-hub
Length of output: 5437
🏁 Script executed:
# Search for the specific i18n namespace and keys in dashboard.json files
rg -t json "probeHistory|status\.na|legend\.(success|failure)|tooltip\.noData" messages/ -A 2Repository: ding113/claude-code-hub
Length of output: 1117
🏁 Script executed:
# Check the dashboard.json structure for availability/probeHistory namespace
fd -t f "dashboard.json" -x grep -l "probeHistory" {}Repository: ding113/claude-code-hub
Length of output: 217
🏁 Script executed:
# Search for the "-" placeholder mentioned in the review in the probe-related components
rg '"-"' src/app/[locale]/dashboard/availability/_components/ -A 2 -B 2Repository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
# Check probe-cell-tooltip to see if it has hardcoded strings
cat -n src/app/[locale]/dashboard/availability/_components/probe-cell-tooltip.tsx | head -150Repository: ding113/claude-code-hub
Length of output: 3158
🏁 Script executed:
# Search for all hardcoded strings in the availability component directory
rg "N/A|OK|ERR" src/app/[locale]/dashboard/availability/_components/Repository: ding113/claude-code-hub
Length of output: 473
🏁 Script executed:
# Check what keys exist in the probeHistory namespace for all languages
for file in messages/*/dashboard.json; do echo "=== $file ==="; rg -A 20 '"probeHistory"' "$file" | head -25; doneRepository: ding113/claude-code-hub
Length of output: 3418
🏁 Script executed:
# Search for tooltip.noData key in all i18n files
rg "tooltip\.noData" messages/ -B 2 -A 2Repository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
# Get the full probeHistory namespace from one file to see all available keys
cat messages/en/dashboard.json | jq '.availability.probeHistory' 2>/dev/null || rg -A 50 '"probeHistory"' messages/en/dashboard.json | head -60Repository: ding113/claude-code-hub
Length of output: 1139
徽章状态文本需 i18n。N/A、OK、ERR 为硬编码用户可见文本,需替换为 i18n 翻译键。同时 probe-cell-tooltip.tsx 第 55 行的 OK 和 Error 也需本地化。
修复方案
在 endpoint-probe-row.tsx 第 67-90 行:
- N/A
+ {t("legend.noData")}- OK
+ {t("legend.success")}- ERR
+ {t("legend.failure")}在 probe-cell-tooltip.tsx 第 55 行:
- {probe.statusCode ?? (probe.ok ? "OK" : "Error")}
+ {probe.statusCode ?? (probe.ok ? t("legend.success") : t("legend.failure"))}🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/availability/_components/endpoint-probe-row.tsx
around lines 67 - 89, Summary: Hard-coded badge texts ("N/A", "OK", "ERR") and
tooltip strings ("OK", "Error") must be localized. Update the JSX that renders
the Badge when probeStatus (variable) is null/true/false in the
endpoint-probe-row component to use the i18n translation function (e.g.,
t('availability.na'), t('availability.ok'), t('availability.err')) instead of
raw strings, and similarly replace the literal "OK" and "Error" in
probe-cell-tooltip (referenced symbol: the tooltip render at line ~55) with
corresponding translation keys; ensure the component imports/uses the project's
translation hook (useTranslation or t) and add the new keys to the locale files.
| {/* Probe Button */} | ||
| <Button | ||
| variant="ghost" | ||
| size="icon" | ||
| className="shrink-0 h-7 w-7" | ||
| onClick={handleProbe} | ||
| disabled={probing} | ||
| > | ||
| <Play className={cn("h-3.5 w-3.5", probing && "animate-pulse")} /> | ||
| </Button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, verify the file exists and examine the content at the specified lines
cat -n src/app/[locale]/dashboard/availability/_components/endpoint-probe-row.tsx | sed -n '115,135p'Repository: ding113/claude-code-hub
Length of output: 520
🏁 Script executed:
# Check the file structure around the Button component
head -130 src/app/[locale]/dashboard/availability/_components/endpoint-probe-row.tsx | tail -30Repository: ding113/claude-code-hub
Length of output: 927
🏁 Script executed:
# Look for other Button components with aria-label in similar files
fd -e tsx -e ts . src/app/[locale]/dashboard/availability/_components --exec grep -l "aria-label" {} \;Repository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
# Check for i18n translation patterns and the "probeNow" key
rg "probeNow|aria-label.*t\(" src/app/[locale]/dashboard/availability/ -A 2Repository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
# Check the i18n configuration and available translations
fd -e json . --path "*locale*" --path "*translation*" --path "*i18n*" | head -20Repository: ding113/claude-code-hub
Length of output: 300
🏁 Script executed:
# Search for aria-label usage more broadly across the codebase
rg "aria-label" src/ -A 1 -B 1 | head -40Repository: ding113/claude-code-hub
Length of output: 3800
🏁 Script executed:
# Check the imports in the target file to understand i18n setup
head -30 src/app/[locale]/dashboard/availability/_components/endpoint-probe-row.tsxRepository: ding113/claude-code-hub
Length of output: 1352
🏁 Script executed:
# Find translation/i18n files
find src -type f -name "*message*" -o -name "*translation*" -o -name "*locale*" | grep -E "\.(json|ts|tsx)$" | head -20Repository: ding113/claude-code-hub
Length of output: 1081
🏁 Script executed:
# Look for the t() function usage pattern in the codebase
rg "t\(" src/ -A 1 | head -50Repository: ding113/claude-code-hub
Length of output: 2822
🏁 Script executed:
# Search for similar icon buttons to see accessibility patterns
rg "Play|Icon.*Button|icon.*size" src/app/[locale]/dashboard/ -B 2 -A 2 | head -60Repository: ding113/claude-code-hub
Length of output: 3304
🏁 Script executed:
# Find translation/messages files
find src -type f \( -name "*.json" -o -name "*message*" -o -name "*locale*" \) | grep -v node_modules | head -30Repository: ding113/claude-code-hub
Length of output: 1113
🏁 Script executed:
# Look for probe-related translations and keys
rg "probe|Probe" src -l | grep -E "\.(json|ts)$" | head -20Repository: ding113/claude-code-hub
Length of output: 777
🏁 Script executed:
# Check the messages directory structure
find src -type d -name "messages" -o -name "locales" -o -name "i18n" 2>/dev/nullRepository: ding113/claude-code-hub
Length of output: 132
🏁 Script executed:
# Search for existing translation patterns in the dashboard.availability namespace
rg "dashboard\.availability" src -A 2 -B 2 | head -60Repository: ding113/claude-code-hub
Length of output: 2407
🏁 Script executed:
# Examine i18n directory structure
ls -la src/i18n/Repository: ding113/claude-code-hub
Length of output: 481
🏁 Script executed:
# Look for messages directory and JSON files
find src/i18n -type f | head -30Repository: ding113/claude-code-hub
Length of output: 162
🏁 Script executed:
# Check English and Chinese translation files for dashboard.availability
find src/i18n -name "*.json" -exec grep -l "dashboard\|availability" {} \;Repository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
# Look for dashboard availability translations specifically
find src/i18n -type f \( -name "*.json" -o -name "*.ts" -o -name "*.js" \) -exec grep -l "probeHistory\|probeNow\|probe" {} \;Repository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
# Check the i18n config to understand how translations are structured
cat src/i18n/config.tsRepository: ding113/claude-code-hub
Length of output: 860
🏁 Script executed:
# Look for messages directory anywhere in the codebase
find . -type d -name "messages" 2>/dev/null | grep -v node_modulesRepository: ding113/claude-code-hub
Length of output: 136
🏁 Script executed:
# Search for where translations are actually stored
rg "probeHistory|dashboard" --type json | head -20Repository: ding113/claude-code-hub
Length of output: 1634
🏁 Script executed:
# Check if translations are embedded in the code or external
grep -r "useTranslations\|next-intl" src/i18n/ src/app/[locale]/ | head -10Repository: ding113/claude-code-hub
Length of output: 808
🏁 Script executed:
# Look for JSON files that might contain translations
find . -name "*.json" -path "*/messages/*" -o -name "*.json" -path "*/locales/*" -o -name "*.json" -path "*/translations/*" 2>/dev/null | grep -v node_modules | head -20Repository: ding113/claude-code-hub
Length of output: 706
🏁 Script executed:
# Check the English dashboard.json for probeHistory keys
cat messages/en/dashboard.json | grep -A 50 "probeHistory"Repository: ding113/claude-code-hub
Length of output: 1638
🏁 Script executed:
# Check Chinese simplified (zh-CN) dashboard.json for probeHistory
cat messages/zh-CN/dashboard.json | grep -A 50 "probeHistory"Repository: ding113/claude-code-hub
Length of output: 1290
🏁 Script executed:
# Search for "probeNow" key across all message files
rg "probeNow" messages/Repository: ding113/claude-code-hub
Length of output: 350
🏁 Script executed:
# Look for probe-related keys in the dashboard files
rg "probe" messages/en/dashboard.json messages/zh-CN/dashboard.json -A 1 -B 1Repository: ding113/claude-code-hub
Length of output: 1893
为探测按钮添加无障碍标签 aria-label属性缺失。应为这个图标按钮补充aria-label={t("probeNow")},便于屏幕阅读器用户和键盘导航。
修改建议
<Button
variant="ghost"
size="icon"
className="shrink-0 h-7 w-7"
onClick={handleProbe}
disabled={probing}
+ aria-label={t("probeNow")}
>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {/* Probe Button */} | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="shrink-0 h-7 w-7" | |
| onClick={handleProbe} | |
| disabled={probing} | |
| > | |
| <Play className={cn("h-3.5 w-3.5", probing && "animate-pulse")} /> | |
| </Button> | |
| {/* Probe Button */} | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| className="shrink-0 h-7 w-7" | |
| onClick={handleProbe} | |
| disabled={probing} | |
| aria-label={t("probeNow")} | |
| > | |
| <Play className={cn("h-3.5 w-3.5", probing && "animate-pulse")} /> | |
| </Button> |
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/availability/_components/endpoint-probe-row.tsx
around lines 119 - 128, The Probe Button in the EndpointProbeRow component is
missing an accessibility label; add aria-label={t("probeNow")} to the Button
element (the one using onClick={handleProbe}, disabled={probing} and rendering
<Play .../>), and ensure the translation function t is imported or available in
the component scope (e.g., via useTranslation or passed props) so the aria-label
resolves correctly for screen readers and keyboard users.
| // Truncate the last segment | ||
| const truncatedSegment = `${lastSegment.slice(0, availableForPath - 3)}...`; | ||
| return `${host}${ellipsis}/${truncatedSegment}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
availableForPath 值较小时,截断逻辑存在边界问题
当 availableForPath 小于 3 时,availableForPath - 3 会产生负数。JavaScript 的 slice(0, -n) 会保留除最后 n 个字符以外的所有内容,这会导致:
- 截断结果可能超出
maxLength限制 - 对于极短的 segment,可能产生空字符串加
...的无意义输出
建议修复
// Truncate the last segment
- const truncatedSegment = `${lastSegment.slice(0, availableForPath - 3)}...`;
- return `${host}${ellipsis}/${truncatedSegment}`;
+ if (availableForPath < 4) {
+ // Not enough space for meaningful truncation, just show host with ellipsis
+ return `${host}${ellipsis}`;
+ }
+ const truncatedSegment = `${lastSegment.slice(0, availableForPath - 3)}...`;
+ return `${host}${ellipsis}/${truncatedSegment}`;🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/availability/_components/endpoint-url-display.tsx
around lines 110 - 112, The truncation math in endpoint-url-display.tsx can
produce a negative slice end when availableForPath < 3, causing incorrect
outputs; update the logic around truncatedSegment and lastSegment so you clamp
the slice length to a safe non-negative (and preferably at least 1) value before
slicing (e.g., compute const sliceLen = Math.max(0, availableForPath - 3) or use
Math.max(1, ...) if you want at least one visible char), then slice lastSegment
with that clamped value and append "..." only when truncation actually occurred;
ensure the final returned string still respects the intended maxLength by
checking if lastSegment.length <= sliceLen and return host/ellipsis/lastSegment
unmodified in that case.
| const formatTime = (date: Date) => { | ||
| return new Date(date).toLocaleString(undefined, { | ||
| month: "short", | ||
| day: "numeric", | ||
| hour: "2-digit", | ||
| minute: "2-digit", | ||
| second: "2-digit", | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Check if the file exists and read the relevant lines
fd "probe-cell-tooltip.tsx" && echo "File found" || echo "File not found"Repository: ding113/claude-code-hub
Length of output: 152
🏁 Script executed:
# Read the file to verify the code snippet
cat -n src/app/\[locale\]/dashboard/availability/_components/probe-cell-tooltip.tsx | head -40Repository: ding113/claude-code-hub
Length of output: 1407
🏁 Script executed:
# Check for next-intl imports and useLocale usage patterns in the project
rg "useLocale" --type ts --type tsx -A 2 -B 2 | head -50Repository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
# Search for useLocale usage patterns without explicit type flags
rg "useLocale" -A 2 -B 2 | head -60Repository: ding113/claude-code-hub
Length of output: 4752
🏁 Script executed:
# Check other dashboard components for locale usage patterns
rg "toLocaleString\|useLocale\|useFormatter" src/app/\[locale\]/dashboard -A 1 -B 1 | head -40Repository: ding113/claude-code-hub
Length of output: 49
时间格式应使用当前应用语言环境。 toLocaleString(undefined, …) 会跟随浏览器语言,可能与应用语言不一致。建议使用 next-intl 的 useLocale() 获取应用当前语言环境。
建议的修复
-import { useTranslations } from "next-intl";
+import { useLocale, useTranslations } from "next-intl";
@@
export function ProbeCellTooltip({ probe }: ProbeCellTooltipProps) {
const t = useTranslations("dashboard.availability.probeHistory");
+ const locale = useLocale();
@@
const formatTime = (date: Date) => {
return new Date(date).toLocaleString(locale, {
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const formatTime = (date: Date) => { | |
| return new Date(date).toLocaleString(undefined, { | |
| month: "short", | |
| day: "numeric", | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| second: "2-digit", | |
| }); | |
| const formatTime = (date: Date) => { | |
| return new Date(date).toLocaleString(locale, { | |
| month: "short", | |
| day: "numeric", | |
| hour: "2-digit", | |
| minute: "2-digit", | |
| second: "2-digit", | |
| }); | |
| }; |
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/availability/_components/probe-cell-tooltip.tsx
around lines 20 - 27, The date formatting uses toLocaleString(undefined, ...)
which follows the browser locale; update formatTime in probe-cell-tooltip.tsx to
use the app locale from next-intl by importing and calling useLocale() and
passing that locale into toLocaleString (e.g. toLocaleString(locale, {...})); if
formatTime is defined outside the component, change it to accept a locale
parameter or move it inside the component so you can call useLocale() directly;
ensure you add the import { useLocale } from "next-intl" and propagate the
locale into formatTime to guarantee the app's language is used.
| <span className="text-muted-foreground">{t("columns.status")}:</span> | ||
| <span className={probe.ok ? "text-green-500" : "text-red-500"}> | ||
| {probe.statusCode ?? (probe.ok ? "OK" : "Error")} | ||
| </span> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's find and read the probe-cell-tooltip.tsx file
fd "probe-cell-tooltip.tsx" -t fRepository: ding113/claude-code-hub
Length of output: 190
🏁 Script executed:
# Search for the file and get its full content
find . -name "probe-cell-tooltip.tsx" -type f | head -1 | xargs wc -lRepository: ding113/claude-code-hub
Length of output: 146
🏁 Script executed:
# Read the file to understand the context
cat -n src/app/*/dashboard/availability/_components/probe-cell-tooltip.tsx 2>/dev/null || cat -n src/app/dashboard/availability/_components/probe-cell-tooltip.tsx 2>/dev/null || find . -name "probe-cell-tooltip.tsx" -type f | head -1 | xargs cat -nRepository: ding113/claude-code-hub
Length of output: 3158
🏁 Script executed:
# Search for translation files to check if "success" and "columns.error" keys exist
find . -name "*.json" -path "*/locales/*" -o -name "*.json" -path "*/i18n/*" -o -name "*.json" -path "*/translations/*" | head -20Repository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
# Look for i18n configuration and usage patterns
fd -e "ts" -e "tsx" -e "js" -e "jsx" | xargs grep -l "useTranslation\|i18n" | head -10Repository: ding113/claude-code-hub
Length of output: 569
🏁 Script executed:
# Search for translation JSON files
find . -type f \( -name "*.json" -o -name "*.ts" -o -name "*.js" \) | grep -iE "(locale|i18n|translation)" | head -20Repository: ding113/claude-code-hub
Length of output: 1280
🏁 Script executed:
# Look for locales directory structure
find . -type d -name "locales" -o -type d -name "i18n" -o -type d -name "messages" | head -10Repository: ding113/claude-code-hub
Length of output: 165
🏁 Script executed:
# Search for where "columns.status" and related keys are defined
rg '"columns\.' -A 2 -B 2 | head -50Repository: ding113/claude-code-hub
Length of output: 5714
🏁 Script executed:
# Check for "OK" or "Error" translations in the codebase
rg '"OK"|"Error"' -t ts -t tsx -t json | head -20Repository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
# Look for existing status-related translation keys
rg 'success|error' --type json -i | grep -E '(status|ok|error)' | head -30Repository: ding113/claude-code-hub
Length of output: 2325
🏁 Script executed:
# Let's look at the messages directory structure and find locale files
find ./messages -type f -name "*.json" | head -20Repository: ding113/claude-code-hub
Length of output: 706
🏁 Script executed:
# Check the main dashboard translation file
cat ./messages/en/dashboard.json | grep -A 5 -B 5 "columns\|success\|status" | head -100Repository: ding113/claude-code-hub
Length of output: 3223
🏁 Script executed:
# Look for what translation namespace this component uses - "dashboard.availability.probeHistory"
find ./messages -type f -name "*.json" -exec grep -l "probeHistory\|probe" {} \;Repository: ding113/claude-code-hub
Length of output: 488
🏁 Script executed:
# Check if there's a availability-specific translation file
find ./messages -type f \( -name "*availability*" -o -name "*probe*" \)Repository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
# Look at the structure of dashboard.json to understand translation hierarchy
python3 << 'EOF'
import json
import os
# Find all message files
message_files = []
for root, dirs, files in os.walk('./messages'):
for file in files:
if file.endswith('.json'):
message_files.append(os.path.join(root, file))
# Check en/dashboard.json specifically
en_dashboard = './messages/en/dashboard.json'
if os.path.exists(en_dashboard):
with open(en_dashboard, 'r') as f:
data = json.load(f)
# Look for the probeHistory key
if 'availability' in data:
if 'probeHistory' in data['availability']:
print("Found probeHistory:")
print(json.dumps(data['availability']['probeHistory'], indent=2))
else:
print("No probeHistory found. Available availability keys:")
print(json.dumps(list(data['availability'].keys())))
else:
print("No availability found. Available top-level keys:")
print(json.dumps(list(data.keys()), indent=2))
EOFRepository: ding113/claude-code-hub
Length of output: 1159
🏁 Script executed:
# Search for existing "columns.error" usage in the codebase
rg 'columns\.error|"error"' ./messages -t json -C 2Repository: ding113/claude-code-hub
Length of output: 44905
用户可见状态文本需要使用 i18n 翻译。 行 55 中的 "OK" 和 "Error" 是硬编码的用户可见字符串,违反了国际化规范。应使用对应的翻译键替代。
修复方案
- <span className={probe.ok ? "text-green-500" : "text-red-500"}>
- {probe.statusCode ?? (probe.ok ? "OK" : "Error")}
- </span>
+ <span className={probe.ok ? "text-green-500" : "text-red-500"}>
+ {probe.statusCode ?? (probe.ok ? t("success") : t("columns.error"))}
+ </span>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <span className="text-muted-foreground">{t("columns.status")}:</span> | |
| <span className={probe.ok ? "text-green-500" : "text-red-500"}> | |
| {probe.statusCode ?? (probe.ok ? "OK" : "Error")} | |
| </span> | |
| <span className="text-muted-foreground">{t("columns.status")}:</span> | |
| <span className={probe.ok ? "text-green-500" : "text-red-500"}> | |
| {probe.statusCode ?? (probe.ok ? t("success") : t("columns.error"))} | |
| </span> |
🤖 Prompt for AI Agents
In `@src/app/`[locale]/dashboard/availability/_components/probe-cell-tooltip.tsx
around lines 53 - 56, Replace the hardcoded visible strings "OK" and "Error" in
the status span with i18n translation calls; in the probe-cell-tooltip component
(the span that currently renders {probe.statusCode ?? (probe.ok ? "OK" :
"Error")}) use the translation helper t(...) for the fallback values (e.g.
t("status.ok") and t("status.error") or your project's equivalent keys) so the
user-visible status text is localized.
| <div className="space-y-2"> | ||
| <Label>{t("routing.codexOverrides.reasoningSummary.label")}</Label> | ||
| <Select | ||
| value={codexReasoningSummaryPreference} | ||
| onValueChange={(v: CodexReasoningSummaryPreference) => | ||
| setCodexReasoningSummaryPreference(v) | ||
| } | ||
| > | ||
| <SelectTrigger> | ||
| <SelectValue /> | ||
| </SelectTrigger> | ||
| <SelectContent> | ||
| <SelectItem value="inherit"> | ||
| {t("routing.codexOverrides.reasoningSummary.options.inherit")} | ||
| </SelectItem> | ||
| <SelectItem value="true">{tCommon("enabled")}</SelectItem> | ||
| <SelectItem value="false">{tCommon("disabled")}</SelectItem> | ||
| </SelectContent> | ||
| </Select> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reasoningSummary 选项值与类型定义不一致。
CodexReasoningSummaryPreference 在 src/types/provider.ts 仅允许 inherit | auto | detailed,当前写入 true/false 会落入非法值并影响保存/序列化。建议改为 auto/detailed 选项并使用对应文案。
建议修改
- <SelectItem value="true">{tCommon("enabled")}</SelectItem>
- <SelectItem value="false">{tCommon("disabled")}</SelectItem>
+ <SelectItem value="auto">
+ {t("routing.codexOverrides.reasoningSummary.options.auto")}
+ </SelectItem>
+ <SelectItem value="detailed">
+ {t("routing.codexOverrides.reasoningSummary.options.detailed")}
+ </SelectItem>🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/forms/v2/tabs/advanced-tab.tsx
around lines 123 - 142, The Select currently emits "true"/"false" which
mismatches the CodexReasoningSummaryPreference type (allowed: "inherit" | "auto"
| "detailed"); update the SelectItems in the component that uses
codexReasoningSummaryPreference and setCodexReasoningSummaryPreference to use
values "auto" and "detailed" (keep "inherit") and swap the displayed labels to
the correct translation keys (replace tCommon("enabled")/("disabled") with the
appropriate strings for "auto" and "detailed"); ensure the Select value types
match CodexReasoningSummaryPreference so serialization/save logic works
correctly.
| const providerTypes = (allowedProviderTypes || [ | ||
| "claude", | ||
| "gemini", | ||
| "codex", | ||
| "openai-compatible", | ||
| ]) as ProviderType[]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat -n src/app/[locale]/settings/providers/_components/forms/v2/tabs/general-tab.tsx | head -80Repository: ding113/claude-code-hub
Length of output: 2742
🏁 Script executed:
rg "ProviderType" --type=ts --type=tsx -B 2 -A 5 | head -100Repository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
rg "renderProviderTypeLabel" --type=ts --type=tsx -B 2 -A 15Repository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
rg "claude-auth|gemini-cli" --type=ts --type=tsxRepository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
cat src/types/provider.ts 2>/dev/null || cat src/types/provider.tsx 2>/dev/null || find src -name "provider.ts" -o -name "provider.tsx"Repository: ding113/claude-code-hub
Length of output: 11834
🏁 Script executed:
rg "type ProviderType|enum ProviderType" -A 15Repository: ding113/claude-code-hub
Length of output: 3481
🏁 Script executed:
rg "allowedProviderTypes" -B 3 -A 3Repository: ding113/claude-code-hub
Length of output: 10279
🏁 Script executed:
rg "claude-auth|gemini-cli" -B 2 -A 2Repository: ding113/claude-code-hub
Length of output: 50379
🏁 Script executed:
rg "allowedProviderTypes\s*\?" -B 5 -A 5 | head -50Repository: ding113/claude-code-hub
Length of output: 4718
🏁 Script executed:
rg "PROVIDER_TYPES\|providerTypes\s*=" src/app/[locale]/settings/providers/_components/forms/v2/ -A 10Repository: ding113/claude-code-hub
Length of output: 49
🏁 Script executed:
cat src/app/[locale]/settings/providers/_components/forms/v2/provider-form-v2.tsx | grep -A 10 "const PROVIDER_TYPES"Repository: ding113/claude-code-hub
Length of output: 338
🏁 Script executed:
grep -n "const providerTypes\|const PROVIDER_TYPES" src/app/[locale]/settings/providers/_components/forms/v2/tabs/general-tab.tsxRepository: ding113/claude-code-hub
Length of output: 120
默认 provider 类型列表不完整,应与其他组件保持一致。
默认数组 ["claude", "gemini", "codex", "openai-compatible"] 缺少 "claude-auth" 和 "gemini-cli"。这两个类型在 ProviderType 定义、renderProviderTypeLabel 处理函数和其他组件(如 provider-form-v2.tsx 中的 PROVIDER_TYPES 常量)中都被完整支持,应保持一致。建议改为包含全部 6 种类型。
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/forms/v2/tabs/general-tab.tsx
around lines 66 - 71, The default providerTypes array is missing "claude-auth"
and "gemini-cli" causing inconsistency with ProviderType and other components;
update the fallback used to compute providerTypes (the expression using
allowedProviderTypes and providerTypes) to include all six types: "claude",
"claude-auth", "gemini", "gemini-cli", "codex", and "openai-compatible" so it
matches the ProviderType definition, renderProviderTypeLabel logic, and the
PROVIDER_TYPES constant in provider-form-v2.tsx.
| <Input | ||
| id="costMultiplier" | ||
| type="number" | ||
| step="0.01" | ||
| min={0} | ||
| value={costMultiplier} | ||
| onChange={(e) => setCostMultiplier(parseFloat(e.target.value) || 1)} | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -name "routing-tab.tsx" -type fRepository: ding113/claude-code-hub
Length of output: 146
🏁 Script executed:
cat -n ./src/app/[locale]/settings/providers/_components/forms/v2/tabs/routing-tab.tsx | sed -n '110,135p'Repository: ding113/claude-code-hub
Length of output: 1123
🏁 Script executed:
cat -n ./src/app/[locale]/settings/providers/_components/forms/v2/tabs/routing-tab.tsx | head -30Repository: ding113/claude-code-hub
Length of output: 1163
🏁 Script executed:
cat -n ./src/app/[locale]/settings/providers/_components/forms/v2/tabs/routing-tab.tsx | sed -n '30,70p'Repository: ding113/claude-code-hub
Length of output: 1568
costMultiplier 当前无法设置为 0。
parseFloat(...) || 1 会把合法的 0 变成 1,导致最小值 0 无法输入。
建议修改
- onChange={(e) => setCostMultiplier(parseFloat(e.target.value) || 1)}
+ onChange={(e) => {
+ const value = Number.parseFloat(e.target.value);
+ setCostMultiplier(Number.isNaN(value) ? 1 : value);
+ }}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Input | |
| id="costMultiplier" | |
| type="number" | |
| step="0.01" | |
| min={0} | |
| value={costMultiplier} | |
| onChange={(e) => setCostMultiplier(parseFloat(e.target.value) || 1)} | |
| /> | |
| <Input | |
| id="costMultiplier" | |
| type="number" | |
| step="0.01" | |
| min={0} | |
| value={costMultiplier} | |
| onChange={(e) => { | |
| const value = Number.parseFloat(e.target.value); | |
| setCostMultiplier(Number.isNaN(value) ? 1 : value); | |
| }} | |
| /> |
🤖 Prompt for AI Agents
In
`@src/app/`[locale]/settings/providers/_components/forms/v2/tabs/routing-tab.tsx
around lines 119 - 126, The onChange handler for the Input in routing-tab.tsx
uses parseFloat(e.target.value) || 1 which converts a valid 0 to 1; change the
handler to parse the value and only fall back to 1 when the parsed result is NaN
(or empty), e.g. compute const v = parseFloat(e.target.value); then call
setCostMultiplier(Number.isFinite(v) ? v : 1) (reference the costMultiplier
state and setCostMultiplier function used in the Input onChange).
| import { type NextRequest, NextResponse } from "next/server"; | ||
| import { getSession } from "@/lib/auth"; | ||
| import { findProviderEndpointProbeLogsBatch } from "@/repository"; | ||
|
|
||
| export async function GET(request: NextRequest) { | ||
| const session = await getSession(); | ||
| if (!session || session.user.role !== "admin") { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| const { searchParams } = new URL(request.url); | ||
|
|
||
| const endpointIdsRaw = searchParams.get("endpointIds"); | ||
| const limitRaw = searchParams.get("limit"); | ||
|
|
||
| if (!endpointIdsRaw) { | ||
| return NextResponse.json({ error: "endpointIds is required" }, { status: 400 }); | ||
| } | ||
|
|
||
| const endpointIds = endpointIdsRaw | ||
| .split(",") | ||
| .map((id) => Number.parseInt(id.trim(), 10)) | ||
| .filter((id) => Number.isFinite(id) && id > 0); | ||
|
|
||
| if (endpointIds.length === 0) { | ||
| return NextResponse.json({ error: "No valid endpoint IDs provided" }, { status: 400 }); | ||
| } | ||
|
|
||
| // Limit to 100 endpoints per request | ||
| if (endpointIds.length > 100) { | ||
| return NextResponse.json({ error: "Too many endpoint IDs (max 100)" }, { status: 400 }); | ||
| } | ||
|
|
||
| const limit = limitRaw ? Number.parseInt(limitRaw, 10) : 30; | ||
| if (!Number.isFinite(limit) || limit <= 0 || limit > 100) { | ||
| return NextResponse.json({ error: "Invalid limit (1-100)" }, { status: 400 }); | ||
| } | ||
|
|
||
| try { | ||
| const logsMap = await findProviderEndpointProbeLogsBatch(endpointIds, limit); | ||
|
|
||
| // Convert Map to object for JSON serialization | ||
| const logs: Record<string, Array<unknown>> = {}; | ||
| for (const [endpointId, probeLogs] of logsMap) { | ||
| logs[endpointId.toString()] = probeLogs; | ||
| } | ||
|
|
||
| return NextResponse.json({ logs }); | ||
| } catch (error) { | ||
| console.error("Batch probe logs API error:", error); | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Find all API route files in src/app/api
fd -t f 'route\.(ts|tsx)$' src/app/apiRepository: ding113/claude-code-hub
Length of output: 906
🏁 Script executed:
# Search for Hono imports and usage patterns in API routes
rg -n -A 3 'from ["'"'"']hono|import.*Hono' src/app/apiRepository: ding113/claude-code-hub
Length of output: 608
🏁 Script executed:
# Check the specific file content and surrounding API routes for context
cat -n src/app/api/availability/endpoints/probe-logs/batch/route.tsRepository: ding113/claude-code-hub
Length of output: 2276
🏁 Script executed:
# Check implementation of a few other API routes to see the pattern
head -20 src/app/api/availability/endpoints/probe-logs/route.tsRepository: ding113/claude-code-hub
Length of output: 893
🏁 Script executed:
head -20 src/app/api/availability/endpoints/route.tsRepository: ding113/claude-code-hub
Length of output: 579
🏁 Script executed:
head -20 src/app/api/admin/database/status/route.tsRepository: ding113/claude-code-hub
Length of output: 606
🏁 Script executed:
# Search for all NextResponse usage in API routes
rg -n 'from "next/server"|NextResponse' src/app/api --type ts --type tsx | head -20Repository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
# Search for NextResponse usage across API routes with proper type handling
rg -n 'NextResponse' src/app/api --glob '*.ts' --glob '*.tsx' | head -30Repository: ding113/claude-code-hub
Length of output: 2794
🏁 Script executed:
# Count how many API routes use Hono vs NextResponse
echo "=== Hono usage ===" && \
rg -l 'from.*hono|import.*Hono' src/app/api --glob '*.ts' --glob '*.tsx' | wc -l && \
echo "=== NextResponse usage ===" && \
rg -l 'NextResponse' src/app/api --glob '*.ts' --glob '*.tsx' | wc -lRepository: ding113/claude-code-hub
Length of output: 117
🏁 Script executed:
# Check the pattern in actions route which uses Hono
head -40 src/app/api/actions/[...route]/route.tsRepository: ding113/claude-code-hub
Length of output: 1738
API 路由应使用 Hono 框架而非 NextResponse。
该路由不符合编码规范要求。根据仓库标准,src/app/api 下的所有路由需要使用 Hono 框架以保持一致性。参考 src/app/api/actions/[...route]/route.ts 中的 Hono + OpenAPIHono 实现模式进行重构。
🤖 Prompt for AI Agents
In `@src/app/api/availability/endpoints/probe-logs/batch/route.ts` around lines 1
- 52, The GET handler currently uses NextRequest/NextResponse and must be
refactored to Hono: replace the exported async GET(request: NextRequest) with a
Hono handler that accepts ctx (Context) and uses ctx.req and ctx.json for
responses; keep and reuse getSession(), findProviderEndpointProbeLogsBatch(),
and the same validation logic for endpointIds and limit, but return errors with
ctx.json(..., status) instead of NextResponse.json and log errors via
console.error before returning a 500. Follow the Hono + OpenAPIHono pattern used
in the project (e.g. use ctx.getQuery or new URL(ctx.req.url).searchParams for
params), ensure the response body shape ({ logs }) is preserved and convert the
Map to a plain object as before, and export the Hono-compatible handler so the
route conforms to the repository standard.
| import { type NextRequest, NextResponse } from "next/server"; | ||
| import { getSession } from "@/lib/auth"; | ||
| import { | ||
| findAllProviderEndpointsWithVendorInfo, | ||
| findProviderEndpointsByVendorAndType, | ||
| } from "@/repository"; | ||
| import type { ProviderType } from "@/types/provider"; | ||
|
|
||
| const PROVIDER_TYPES: ProviderType[] = [ | ||
| "claude", | ||
| "claude-auth", | ||
| "codex", | ||
| "gemini-cli", | ||
| "gemini", | ||
| "openai-compatible", | ||
| ]; | ||
|
|
||
| function isProviderType(value: string | null): value is ProviderType { | ||
| if (!value) { | ||
| return false; | ||
| } | ||
| return PROVIDER_TYPES.includes(value as ProviderType); | ||
| } | ||
|
|
||
| export async function GET(request: NextRequest) { | ||
| const session = await getSession(); | ||
| if (!session || session.user.role !== "admin") { | ||
| return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); | ||
| } | ||
|
|
||
| const { searchParams } = new URL(request.url); | ||
|
|
||
| const vendorIdRaw = searchParams.get("vendorId"); | ||
| const providerTypeRaw = searchParams.get("providerType"); | ||
|
|
||
| // If no filters provided, return all endpoints with vendor info | ||
| if (!vendorIdRaw && !providerTypeRaw) { | ||
| try { | ||
| const endpoints = await findAllProviderEndpointsWithVendorInfo(); | ||
| return NextResponse.json({ endpoints }); | ||
| } catch (error) { | ||
| console.error("Endpoint availability API error:", error); | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }); | ||
| } | ||
| } | ||
|
|
||
| // Original filtered query | ||
| const vendorId = vendorIdRaw ? Number.parseInt(vendorIdRaw, 10) : Number.NaN; | ||
|
|
||
| if (!Number.isFinite(vendorId) || vendorId <= 0 || !isProviderType(providerTypeRaw)) { | ||
| return NextResponse.json({ error: "Invalid query" }, { status: 400 }); | ||
| } | ||
|
|
||
| try { | ||
| const endpoints = await findProviderEndpointsByVendorAndType(vendorId, providerTypeRaw); | ||
| return NextResponse.json({ vendorId, providerType: providerTypeRaw, endpoints }); | ||
| } catch (error) { | ||
| console.error("Endpoint availability API error:", error); | ||
| return NextResponse.json({ error: "Internal server error" }, { status: 500 }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Search for Hono usage in API routes
rg -n 'from "hono"|from \'hono\'|Hono|hono' src/app/api --type ts --type tsxRepository: ding113/claude-code-hub
Length of output: 190
🏁 Script executed:
# Check if Hono is listed as a dependency
cat package.json | grep -i honoRepository: ding113/claude-code-hub
Length of output: 189
🏁 Script executed:
# Look for other API route examples in the codebase
fd -e 'route.ts' -e 'route.tsx' src/app/apiRepository: ding113/claude-code-hub
Length of output: 416
🏁 Script executed:
# Check the structure of API directory
find src/app/api -type f -name '*.ts' -o -name '*.tsx' | head -20Repository: ding113/claude-code-hub
Length of output: 877
🏁 Script executed:
# Check one of the API routes to see the actual pattern being used
cat -n src/app/api/availability/endpoints/route.ts | head -40Repository: ding113/claude-code-hub
Length of output: 1542
🏁 Script executed:
# Search for Hono framework usage across all API routes
rg 'from "hono"|from \'hono\'|new Hono' src/app/api --type ts --type tsx -A 2Repository: ding113/claude-code-hub
Length of output: 135
🏁 Script executed:
# Check a few other API route files to see the pattern
cat -n src/app/api/auth/login/route.ts | head -30Repository: ding113/claude-code-hub
Length of output: 1114
🏁 Script executed:
# Search for NextResponse usage across API routes
rg 'NextResponse|NextRequest' src/app/api --type ts --type tsx | head -20Repository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
# Search for actual Hono usage in the codebase (try different pattern)
rg 'Hono|from "hono"|from .hono.' --type ts --type tsxRepository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
# Check if there are any route handlers using Hono pattern
rg 'new Hono|Hono()' --type ts --type tsxRepository: ding113/claude-code-hub
Length of output: 94
🏁 Script executed:
# Look at the structure - check if there's a different API pattern used
cat -n src/app/api/actions/\[...\route\]/route.ts | head -40Repository: ding113/claude-code-hub
Length of output: 2018
🏁 Script executed:
# Check which API routes use Hono pattern
rg 'from "@hono|from .hono' src/app/api -lRepository: ding113/claude-code-hub
Length of output: 106
🏁 Script executed:
# Check which use NextResponse pattern
rg 'from "next/server"' src/app/api -lRepository: ding113/claude-code-hub
Length of output: 659
🏁 Script executed:
# Look at the full actions route to understand the Hono pattern
cat -n src/app/api/actions/\[...\route\]/route.ts | tail -80Repository: ding113/claude-code-hub
Length of output: 2220
API 路由应使用 Hono 框架以遵循统一规范。
根据编码规范,src/app/api/**/*.{ts,tsx} 的 API 路由应使用 Hono 框架。当前代码使用 NextResponse,应改为 Hono 模式。参考 src/app/api/actions/[...route]/route.ts 中使用 OpenAPIHono 和 handle() 的实现方式。
🤖 Prompt for AI Agents
In `@src/app/api/availability/endpoints/route.ts` around lines 1 - 61, The route
currently implements an exported GET using NextRequest/NextResponse (functions:
GET, isProviderType, PROVIDER_TYPES, getSession,
findAllProviderEndpointsWithVendorInfo, findProviderEndpointsByVendorAndType)
but must be converted to a Hono-style handler per project conventions; replace
the exported GET with a Hono app/handler (use OpenAPIHono and app.handle or the
pattern used in src/app/api/actions/[...route]/route.ts) and adapt
request/session handling to Hono's Context (ctx.req, ctx.json, ctx.text) while
keeping the same validation logic (vendorId/vendorIdRaw, providerTypeRaw,
isProviderType) and error branches, and ensure you export the Hono handler via
handle() so the route registers under the OpenAPIHono pattern.
Summary
Add vendor-endpoint architecture enabling providers to have multiple endpoints with intelligent routing, health probing, and automatic failover via circuit breakers.
Problem
Currently, providers are limited to a single endpoint configuration. This creates limitations for:
Related:
Solution
Implement a vendor-endpoint architecture that allows:
Changes
Database Schema (Migration 0055)
provider_vendorstable for vendor management (grouped by domain)provider_endpointstable with probe state trackingprovider_endpoint_probe_logstable for health monitoring historyprovider_vendor_idforeign key toproviderstable (auto-migrated from existing URL domains)Core Services
src/lib/endpoint-circuit-breaker.ts): Per-endpoint failure tracking with Redis statesrc/lib/vendor-type-circuit-breaker.ts): Aggregate circuit breaker for vendor+type combinationssrc/lib/provider-endpoints/endpoint-selector.ts): Smart endpoint selection based on availabilitysrc/lib/provider-endpoints/probe.ts): Health check implementation with loggingProxy Pipeline Integration
forwarder.tsto support endpoint-level routing with failoverprovider-selector.tswith vendor-type circuit integrationManagement API
src/actions/provider-endpoints.tsfor endpoint CRUD, probing, and circuit managementDashboard UI
Breaking Changes
provider_vendor_idcolumn onproviderstablebun run db:migrateor enableAUTO_MIGRATE=trueTesting
Automated Tests
Manual Testing
Checklist
bun run db:generateDescription enhanced by Claude AI