Skip to content

release v0.8.4#1243

Merged
ding113 merged 13 commits into
mainfrom
dev
Jun 4, 2026
Merged

release v0.8.4#1243
ding113 merged 13 commits into
mainfrom
dev

Conversation

@ding113

@ding113 ding113 commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Summary

Release v0.8.4 — aggregates 7 targeted fixes and 1 feature from the dev branch since v0.8.3, covering proxy robustness (compressed request bodies, benign EPIPE handling), notification scheduling correctness, model price local-first protection, Anthropic API spec compliance, and UI regression fixes.

Changes

New Feature: Compressed Request Body Support

  • Support content-encoding: zstd (and gzip/deflate/br) on inbound proxy requests — Codex CLI sends zstd-compressed bodies; the proxy now decompresses them before parsing, filtering, and forwarding to upstream. Node.js >= 22.15 is now required (native node:zlib zstd support).
    • Closes 功能请求: 支持codex zstd请求header: content-encoding: zstd #1193 — Feature request: support Codex zstd request header
    • New file: src/app/v1/_lib/proxy/request-body-codec.ts (220 lines) — decompression pipeline with bomb-guard (100MB cap), single-layer encoding limit, and graceful passthrough for unsupported encodings
    • Modified: src/app/v1/_lib/proxy/session.ts — integrates decompression into parseRequestBody, strips content-encoding header post-decode so upstream receives plaintext
    • Infrastructure: package.json engines field set to node >= 22.15.0, Dockerfiles updated to node:22-slim / node:trixie-slim, CI uses Node 22

Bug Fixes

  1. Don't crash process on benign EPIPE from streaming disconnect

  2. Fix notification scheduling — master/sub switches and interval edge cases

    • Related to 关于每日用户消费排行榜消息推送异常 #1236 — Notifications (leaderboard, cost-alert) still fire when master switch is off; cost-alert interval >= 60 minutes collapses to broken cron
    • Modified: src/lib/notification/notification-queue.ts — runtime re-check of master + sub switches before sending; removeAllRepeatableJobs with graceful partial-failure handling; intervalToRepeat() correctly maps intervals that don't divide 60 to {every} instead of broken cron expressions; clampIntervalMinutes() normalizes to [1, 1440]
    • Modified: src/actions/notifications.ts — scheduleNotifications now called unconditionally (was gated behind NODE_ENV === "production"), internal fail-open handles missing Redis
  3. Protect locally-uploaded model prices from cloud auto-sync overwrite (local-first)

    • Related to Cloud price sync fails after price table redirects to docs host #1169 — Cloud price sync can silently overwrite user-created manual prices
    • Modified: src/actions/model-prices.tsprocessPriceTableInternal accepts a source parameter ("litellm" for auto-sync, "manual" for user uploads); manual-source writes bypass the local-protection skip; uploadPriceTable marks as "manual" source
    • Modified: src/repository/model-price.tsupsertModelPrice accepts configurable source instead of hardcoded "manual"
    • Model name whitespace normalization prevents cloud keys with leading/trailing spaces from bypassing protection
  4. Fix provider group dropdown and model suggestion API client wrappers

    • Fixes 创建用户无法选择已有供应商分组 #1212 — Provider group dropdown in create-user dialog shows no suggestions and reports "Load failed"
    • Modified: src/lib/api-client/v1/actions/providers.tsgetProviderGroupsWithCount() and getModelSuggestionsByProviderGroup() now wrap raw API responses in ActionResult<{ok, data}> via toActionResult(), matching dashboard consumer expectations
    • Modified: src/components/ui/tag-input.tsx — auto-open suggestions when async data arrives while input is focused (regression guard for 创建用户无法选择已有供应商分组 #1212)
  5. Normalize Anthropic /v1/models created_at to second precision

Tests Added

Test File Lines Coverage Area
tests/unit/proxy/request-body-codec.test.ts +170 zstd/gzip/deflate/br round-trips, bomb guard, multi-layer rejection, passthrough
tests/unit/proxy/session-request-decode.test.ts +152 End-to-end decompression in ProxySession.fromContext, header stripping, error cases
tests/unit/benign-broken-pipe-error.test.ts +124 EPIPE detection, cause-chain traversal, ambiguous code exclusion
tests/unit/instrumentation-crash-handler.test.ts +163 Crash handler wiring — benign EPIPE skips exit, genuine errors still exit(1)
tests/unit/notification/notification-queue.test.ts +456 Switch guard, interval-to-cron/every mapping, repeatable job lifecycle
tests/unit/actions/model-prices.test.ts +174 Local-first source handling, cloud/manual interaction, whitespace normalization
tests/unit/api/v1/api-client-actions.test.ts +52 ActionResult wrapping for group counts and model suggestions
tests/unit/proxy/available-models.test.ts +33 Timestamp normalization edge cases
tests/unit/settings/providers/provider-form-*.test.tsx +2 Mock return value update for new ActionResult shape
src/components/ui/__tests__/provider-group-tag-input.test.tsx +148 Async suggestion auto-open, dialog portal rendering, data flow regression

Breaking Changes

Change Impact Migration
Node.js minimum version raised from 20 to 22.15 Self-hosted deployments using Node 20 must upgrade to Node >= 22.15 for native zstd decompression Update node-version in CI; pull latest Docker images which already use Node 22/24

Checklist

  • Code follows project conventions
  • Tests pass locally
  • No manual migration required (Node version only affects runtime, not database)

Description enhanced by Claude AI

Greptile Summary

This PR bundles v0.8.4 with 7 fixes and 1 feature: compressed request body decompression (zstd/gzip/deflate/br) for inbound proxy requests, EPIPE crash suppression, notification scheduling correctness, local-first model price protection, Anthropic API timestamp compliance, and provider group UI fixes — all backed by ~2,200 lines of new tests.

  • Compressed body pipeline (request-body-codec.ts): single-layer enforcement, 100 MB decompression bomb guard, and header stripping after decode are correctly integrated into ProxySession.fromContext.
  • Notification fixes: intervalToRepeat() now maps non-cron-compatible intervals (≥ 60 min or non-factors of 60) to Bull's every mode instead of generating invalid cron expressions like */60 * * * *; master/sub-switch runtime re-checks prevent stale repeatable jobs from firing after the switch is turned off.
  • Local-first prices: processPriceTableInternal accepts a source parameter so user-uploaded manual prices are protected from cloud auto-sync overwrites.

Confidence Score: 5/5

Safe to merge; all core changes — decompression, EPIPE suppression, notification scheduling, local-first price protection, and UI fixes — are logically correct with solid test coverage.

No correctness or data-integrity defects were found in the changed code paths. The notification interval logic correctly fixes the broken */60 * * * * cron generation. The model-price source-protection logic is sound. The EPIPE suppression is deliberately scoped to write-side errors only. The two flagged items are a silent timezone discard (documented limitation) and an inflated update count for source-only conversions — neither affects correctness of stored data or notifications fired.

No files require special attention, though src/lib/notification/notification-queue.ts is worth a second look regarding the silent timezone drop for intervals >= 60 minutes in bindings that carry an explicit timezone setting.

Important Files Changed

Filename Overview
src/app/v1/_lib/proxy/request-body-codec.ts New file: robust decompression pipeline with bomb guard, single-layer limit, and graceful passthrough for unsupported encodings; logic and error handling are correct.
src/app/v1/_lib/proxy/session.ts Integrates decodeRequestBody into parseRequestBody and strips content-encoding only when decoding actually occurred; header deletion logic is correct.
src/lib/notification/notification-queue.ts Adds removeAllRepeatableJobs, clampIntervalMinutes, intervalToRepeat helpers; timezone parameter is silently discarded for intervals >= 60 min (every mode), affecting the 60-min default costAlertCheckInterval.
src/actions/notifications.ts Removes NODE_ENV gate; scheduleNotifications is fail-open internally so missing Redis in dev mode is handled gracefully.
src/lib/lifecycle/benign-errors.ts New file: EPIPE-only benign error predicate with cause-chain traversal and depth guard; intentionally excludes ECONNRESET/ERR_STREAM_PREMATURE_CLOSE.
src/instrumentation.ts Crash handlers now suppress benign EPIPE at warn level; genuine uncaughtException/unhandledRejection still calls process.exit(1).
src/actions/model-prices.ts Source parameter correctly gates the local-first skip; source-change-only updates (litellm->manual) trigger upsert even with identical price data, which is intentional.
src/repository/model-price.ts upsertModelPrice now accepts a configurable source (defaults to 'manual'); transactional delete+insert prevents orphan rows from prior litellm+manual co-existence.
src/app/v1/_lib/models/available-models.ts normalizeAnthropicTimestamp correctly strips milliseconds and normalizes to UTC; passthrough for unparseable values is safe.
src/components/ui/tag-input.tsx didAutoOpenRef fires auto-open only once when async suggestions arrive while the input is already focused; correctly avoids re-triggering on subsequent suggestion refreshes.
src/lib/api-client/v1/actions/providers.ts getProviderGroupsWithCount and getModelSuggestionsByProviderGroup now wrapped in toActionResult, matching dashboard consumer expectations.

Sequence Diagram

sequenceDiagram
    participant Client as Codex / Client
    participant Proxy as Proxy (session.ts)
    participant Codec as request-body-codec.ts
    participant Guard as Auth Guard
    participant Upstream as Upstream LLM

    Client->>Proxy: POST /v1/messages content-encoding: zstd
    Proxy->>Codec: decodeRequestBody(rawBodyBuffer, zstd)
    Note over Codec: Check layers <= 1, supported encoding, compressed size <= limit
    Codec->>Codec: zstdDecompressSync(buffer, maxOutputLength)
    Note over Codec: Decompression bomb guard (100 MB cap)
    Codec-->>Proxy: "DecodedRequestBody decoded=true encoding=zstd"
    Proxy->>Proxy: headers.delete(content-encoding)
    Proxy->>Guard: ProxySession (plaintext body, no CE header)
    Guard->>Guard: Auth, filter, billing on plaintext
    Guard->>Upstream: Forward plaintext body (no content-encoding)
    Upstream-->>Client: Stream response
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/lib/notification/notification-queue.ts:777-787
The `tz` parameter is silently discarded whenever `intervalMinutes` falls outside the cron-compatible range (i.e., anything ≥ 60 or not a factor of 60). The default `costAlertCheckInterval` is 60 minutes, so the most common case already hits this branch. Users who have configured a `scheduleTimezone` on a binding with a 60-minute (or 90-minute, 120-minute, etc.) interval will find their timezone setting quietly ignored — Bull's `every` mode fires at a fixed offset from job-add time, not aligned to a clock boundary in their timezone.

```suggestion
function intervalToRepeat(
  intervalMinutes: number,
  tz?: string
): { cron: string; tz?: string } | { every: number } {
  if (intervalMinutes <= 59 && 60 % intervalMinutes === 0) {
    return tz
      ? { cron: `*/${intervalMinutes} * * * *`, tz }
      : { cron: `*/${intervalMinutes} * * * *` };
  }
  if (tz) {
    logger.warn("[intervalToRepeat] Interval is not cron-compatible; timezone setting will be ignored", {
      intervalMinutes,
      tz,
    });
  }
  return { every: intervalMinutes * 60 * 1000 };
}
```

### Issue 2 of 2
src/actions/model-prices.ts:196-205
**Source-only change inflates the `updated` count**

The new `existingPrice.source !== source` condition triggers `upsertModelPrice` (a full delete + insert transaction) even when the price data is byte-for-byte identical. This means that every time a user re-uploads an unchanged price table with `source = "manual"` over an existing `"litellm"` record, each such model is reported as "updated" and incurs a DB round-trip. The result count can mislead operators into thinking prices changed when only the source tag flipped. Consider adding a secondary condition that skips the upsert when `isPriceDataEqual` is true and the only change is a harmless `litellm → manual` promotion that has already been committed on a previous upload.

Reviews (5): Last reviewed commit: "fix(proxy): raise compressed body limit ..." | Re-trigger Greptile

ding113 and others added 7 commits May 29, 2026 21:19
…#1227)

Normalize the created_at field in Anthropic /v1/models responses to
second-precision UTC, stripping any millisecond fraction from upstream
timestamps. Unparseable values are returned unchanged to avoid errors.

Fixes #1226
…ers in toActionResult (#1235)

getProviderGroupsWithCount and getModelSuggestionsByProviderGroup returned raw HTTP bodies, but dashboard consumers check res.ok, so the user-edit panel showed 获取供应商分组统计失败: undefined on every open. Wrap both in toActionResult; add real-wrapper contract tests and align stale model-suggestion mocks.
… focused (#1237)

Fixes the create-user dialog where the "provider group" dropdown would not open
to select existing groups. TagInput now auto-opens its suggestions the first
time they load asynchronously while the input is focused (fire-once,
disabled-guarded), so focusing the field before the data arrives no longer
leaves the dropdown stuck closed.

The data-fetch half of #1212 was fixed separately in #1235; this completes the
interaction-layer fix. Includes regression tests (incl. the dialog/portal path)
and corrects the test mock path.

Fixes #1212

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tream disconnect (#1239)

Narrow process-level uncaughtException/unhandledRejection suppression to EPIPE (write-side, unambiguous downstream disconnect) only; ECONNRESET/ERR_STREAM_PREMATURE_CLOSE stay fatal to preserve fail-fast. Adds getBenignBrokenPipeCode for accurate nested-code logging.

Closes #1234
…1240)

* fix(notification): prevent stale repeatable jobs from firing after switch off and fix cost alert interval collapse

When the master switch or sub-switch is turned off, previously enqueued
repeatable jobs continued to fire because the processor did not re-check
settings at execution time. Additionally, the scheduleNotifications
function was guarded by NODE_ENV="production", so changes in development
were silently ignored. The cost alert interval >=60 minutes collapsed to
an hourly cron because Bull's cron only supports minutes 0-59.

- Remove NODE_ENV guard so scheduleNotifications always runs on settings save.
- Add enabled/sub-switch checks in daily-leaderboard and cost-alert job
  processors to skip sending when disabled.
- Extract removeAllRepeatableJobs and make individual key removal resilient
  to transient Redis errors.
- For cost alert intervals >=60 minutes, use {every} instead of */N cron to
  schedule every N minutes correctly.
- Add unit tests covering processor skip behavior, removal resilience, and
  cost alert interval mapping.

Fixes #1236

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(notification-queue): prevent double-firing and stale alerts (#1236)

The scheduler previously added new repeatable jobs even when old ones
could not be removed, causing duplicate notifications. Circuit-breaker
jobs also lacked a run-time switch check, so alerts could fire after
the feature was disabled.

- Extract clampIntervalMinutes and intervalToRepeat helpers; use cron
  only when the interval divides 60 evenly, falling back to the
  every option otherwise. This avoids uneven cron patterns and
  simplifies scheduling across legacy and targets modes.
- Return a success boolean from removeAllRepeatableJobs and abort the
  reschedule if any removal fails, preventing old and new jobs from
  running simultaneously.
- Add an execution-time guard in the circuit-breaker queue processor
  that re-verifies the master and circuit-breaker switches before
  sending, skipping with success: true, skipped: true when off.
- Add unit tests for the circuit-breaker switch logic, the cost-alert
  positive path, targets-mode scheduling with binding jobId and
  timezone, and the abort-on-removal-failure scenario.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(notification): add circuit-breaker sub-switch-off skip test

Add a test case verifying that the notification queue processor
skips sending when circuitBreakerEnabled is false. Covers a
scenario flagged as missing during code review.

Refs #1236

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…br) (#1238)

Decode inbound content-encoding (zstd/gzip/deflate/br) so codex and other clients can send compressed request bodies. Single-layer cap + decompression-bomb guard; strips content-encoding after decode. Requires Node >=22.15 (native node:zlib zstd). Closes #1193.
…nc overwrite (#1241)

模型价格表「本地优先」修复:

- 用户显式上传的价格表(uploadPriceTable)标记为 source='manual',不再被云端自动同步静默覆盖。
- 跳过保护仅在 source='litellm'(云端/自动同步)时生效;手动上传为权威导入,可正常覆盖与重新上传更新。
- update 路径改用事务型 upsertModelPrice 原子替换(delete+insert 同事务),消除崩溃丢数据窗口并清理孤儿行。
- 云端模型名 trim 归一化后再做保护比对。
- 新增针对上传/重新上传/转换/归一化的单元测试。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f1100fee-d204-4f2c-b090-38c5e57d3ffc

📥 Commits

Reviewing files that changed from the base of the PR and between 492c508 and 58346ce.

📒 Files selected for processing (3)
  • .env.example
  • src/app/v1/_lib/proxy/request-body-codec.ts
  • tests/unit/proxy/request-body-codec.test.ts
✅ Files skipped from review due to trivial changes (1)
  • .env.example
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/app/v1/_lib/proxy/request-body-codec.ts
  • tests/unit/proxy/request-body-codec.test.ts

📝 Walkthrough

Walkthrough

此 PR 升级 Node 到 22、在代理层实现 Content-Encoding 解码和请求头剥离、将 model-prices 引入 source 并用 upsert 实现本地优先、重构通知调度与队列、添加良性 EPIPE 抑制、扩展 usage-logs 为 CSV/XLSX(含 XLSX 生成与异步任务),并补充大量测试与 UI/i18n 调整。

Changes

全部更改(单一审阅切片)

Layer / File(s) Summary
运行时与环境升级
.github/workflows/pr-check.yml, Dockerfile, package.json, README.md, README.en.md, deploy/Dockerfile, .env.example
将 CI 与运行镜像升级到 Node 22、在 package.json 加入 engines >=22.15、新增 fflate 依赖,并在 README/.env.example 中同步说明 native node:zlib 对 zstd 的支持与限制。
入站 content-encoding 解码(代理层)
src/app/v1/_lib/proxy/request-body-codec.ts, src/app/v1/_lib/proxy/session.ts, tests/proxy
新增 parseContentEncodingdecodeRequestBody 支持 zstd/gzip/br/deflate、设定压缩/解压硬上限与编码层数上限、错误映射(413/400),并在 ProxySession.fromContext 中按解码结果剥离 content-encoding;包含丰富单元与集成测试。
模型价格来源与 upsert
src/actions/model-prices.ts, src/repository/model-price.ts, tests/model-prices
processPriceTableInternal 增加 source 参数(默认 litellm)、模型名 trim() 归一、冲突跳过仅在 cloud litellm 路径生效、调用 upsertModelPrice(..., source) 实现原子替换;uploadPriceTablesource="manual" 写入,本地优先行为有测试覆盖。
通知调度与队列重构
src/actions/notifications.ts, src/lib/notification/notification-queue.ts, tests/notification
updateNotificationSettingsAction 在任意环境皆触发 scheduleNotifications();队列处理器在执行期再次校验通知开关并可跳过;scheduleNotifications 重写 repeatable 生成/移除与 interval→repeat 映射,含 legacy/targets 模式与绑定级调度,补充测试。
良性断管(EPIPE)判定与崩溃处理
src/lib/lifecycle/benign-errors.ts, src/instrumentation.ts, tests/instrumentation
新增基于 cause 链提取 EPIPEgetBenignBrokenPipeCode/isBenignBrokenPipeError,在 uncaughtException/unhandledRejection 中对 EPIPE 仅 warn 并短路,不写诊断报告或退出。
错误规则 / 请求过滤 的 reload 与 UI 刷新接线
src/actions/error-rules.ts, src/actions/request-filters.ts, src/lib/error-rule-detector.ts, src/lib/request-filter-engine.ts, settings UI components, tests/error-rules
动作在 create/update/delete 成功后 best-effort 调用 detector/engine.reload;detector/engine 增加 in-flight reload 队列/复用语义(queueIfRunning/reload(queue = true));设置页相关组件在成功后调用 router.refresh(),并补充相应测试。
Usage Logs 导出(CSV 同步 / XLSX 异步)
src/lib/usage-logs/export/*, src/actions/usage-logs.ts, src/app/.../usage-logs-filters.tsx, API handlers, client api, tests/usage-logs
新增列定义/CSV 渲染、时间/数字格式化、汇总累加器、XLSX(OpenXML + fflate)生成、导出任务与 result store、API/前端按 format 协作(同步 CSV、异步 XLSX)、并在前端用 Blob 下载;大幅测试覆盖。
Provider UI 与 API 兼容调整
src/lib/api-client/v1/actions/providers.ts, src/components/ui/tag-input.tsx, provider tests
providers API 返回包装为 toActionResultTagInput 在 suggestions 首次由空变非空且输入仍聚焦时自动展开建议,下游测试/Mocks 同步调整。
i18n 与 UI 微改动
messages/*/dashboard.json, error details, usage logs UI
新增导出选项键(CSV/XLSX)、effort.tooltip 文案、Anthropic effort badge xhigh 样式、SummaryTab tooltip 与统计面板 refreshKey 联动等界面文字与行为更新。

Estimated code review effort:
🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs:

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces native request body decompression (zstd, gzip, deflate, br) in the proxy layer, safe handling of benign client disconnects (EPIPE) to prevent process crashes, a 'local-first' model pricing sync strategy, and improved notification scheduling. The code reviewer provided several valuable recommendations: lowering the 100MB decompression limit to mitigate DoS risks, deleting the content-length header alongside content-encoding to prevent upstream truncation, defensively handling NaN values in clampIntervalMinutes, adopting a more granular fallback when notification job removals fail, and wrapping model price updates in a single database transaction to avoid N+1 performance bottlenecks.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

* next.config.ts 的 proxyClientMaxBodySize 钳制(见 proxy.matcher.ts),故入站压缩体
* 体积本身不另设限。逐层解压按此上限增量限制,超过即按 413 拒绝。
*/
export const MAX_DECOMPRESSED_REQUEST_BYTES = 100 * 1024 * 1024;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-high high

A default limit of 100MB for decompressed request bodies is extremely high for typical LLM chat/completion APIs (where even a 2-million token prompt is usually under 10MB). Since decompression, TextDecoder.decode(), and JSON.parse() are all executed synchronously on the main thread, a 100MB payload can block the Node.js event loop for several seconds and consume hundreds of megabytes of memory, making the server highly vulnerable to Denial of Service (DoS) attacks (e.g., decompression bombs). Consider lowering this default limit significantly (e.g., to 10MB or 20MB) or making it configurable via environment variables.

Suggested change
export const MAX_DECOMPRESSED_REQUEST_BYTES = 100 * 1024 * 1024;
export const MAX_DECOMPRESSED_REQUEST_BYTES = Number(process.env.MAX_DECOMPRESSED_REQUEST_BYTES) || 10 * 1024 * 1024;

Comment on lines +249 to +251
if (bodyResult.decodedContentEncoding) {
headers.delete("content-encoding");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

When a request body is decompressed by the proxy, the content-encoding header is correctly deleted. However, the original content-length header (which corresponds to the compressed body size) is still preserved in the headers object. If the outbound forwarding logic does not explicitly strip or recalculate content-length, the upstream provider will receive the decompressed (much larger) body with a tiny content-length header, leading to request truncation or protocol errors. To prevent this, explicitly delete the content-length header as well so that the HTTP client can automatically recalculate it based on the decompressed payload.

Suggested change
if (bodyResult.decodedContentEncoding) {
headers.delete("content-encoding");
}
if (bodyResult.decodedContentEncoding) {
headers.delete("content-encoding");
headers.delete("content-length");
}

Comment on lines +767 to +769
function clampIntervalMinutes(rawMinutes: number): number {
return Math.min(Math.max(1, Math.trunc(rawMinutes)), 24 * 60);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

If rawMinutes is NaN, Math.trunc(rawMinutes) will return NaN. This NaN value will propagate through Math.max and Math.min, causing clampIntervalMinutes to return NaN. Consequently, intervalToRepeat will return { every: NaN }, which will cause Bull/Redis to throw a runtime error when scheduling the job. Ensure that NaN or non-finite values are handled defensively by falling back to a default interval (e.g., 60 minutes).

Suggested change
function clampIntervalMinutes(rawMinutes: number): number {
return Math.min(Math.max(1, Math.trunc(rawMinutes)), 24 * 60);
}
function clampIntervalMinutes(rawMinutes: number): number {
const minutes = Number.isFinite(rawMinutes) ? Math.trunc(rawMinutes) : 60;
return Math.min(Math.max(1, minutes), 24 * 60);
}
References
  1. When scheduling repeatable tasks using Bull queue, only use a cron expression if the interval in minutes is less than or equal to 59 and divides 60 evenly. For intervals that do not divide 60 or are 60 or greater, use a fixed millisecond interval to prevent uneven execution intervals at hour boundaries.

Comment on lines +816 to 823
const removedAll = await removeAllRepeatableJobs(queue);
if (!removedAll) {
logger.error({
action: "schedule_notifications_aborted",
reason: "stale_repeatable_remove_failed",
});
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Aborting the entire notification scheduling if a single repeatable job fails to be removed might be too aggressive and could lead to a complete outage of all notifications (e.g., cost alerts, daily leaderboards, cache hit rate alerts) due to a transient Redis issue with a single job key. Consider a more granular approach where only the failed notification types are skipped, or proceed with a warning, as duplicate notifications are often preferable to a total notification blackout.


// 处理每个模型的价格
for (const [modelName, priceData] of entries) {
for (const [rawModelName, priceData] of entries) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Performing individual database transactions (await createModelPrice / await upsertModelPrice) inside a loop for each model entry can lead to severe performance bottlenecks (N+1 transactions) when processing large price tables. Consider wrapping the entire loop inside a single database transaction (db.transaction) to commit all changes atomically and significantly reduce database I/O overhead.

Comment thread package.json
@@ -2,6 +2,9 @@
"name": "claude-code-hub",
"version": "0.8.0",
"private": true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Version not bumped for this release

The PR title is "release v0.8.4" but package.json still declares "version": "0.8.0". Any build artifact, API response, or UI element that reads the package version will report 0.8.0, making it impossible to distinguish this release from prior ones by version string alone.

Prompt To Fix With AI
This is a comment left during a code review.
Path: package.json
Line: 4

Comment:
**Version not bumped for this release**

The PR title is "release v0.8.4" but `package.json` still declares `"version": "0.8.0"`. Any build artifact, API response, or UI element that reads the package version will report 0.8.0, making it impossible to distinguish this release from prior ones by version string alone.

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

Comment thread Dockerfile
RUN --mount=type=cache,target=/app/.next/cache bun run build

FROM node:20-slim AS runner
FROM node:22-slim AS runner

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 node:22-slim is a rolling tag that can resolve to any 22.x patch release. The zstdDecompressSync API (used in request-body-codec.ts) was first shipped in Node 22.15.0. If this image is built against a stale Docker cache that points to an earlier 22.x build, the decompression call will fail at runtime with TypeError: zstdDecompressSync is not a function. Pinning to node:22.15-slim (or later) makes the minimum requirement explicit and matches what package.json engines already declares.

Suggested change
FROM node:22-slim AS runner
FROM node:22.15-slim AS runner
Prompt To Fix With AI
This is a comment left during a code review.
Path: Dockerfile
Line: 15

Comment:
`node:22-slim` is a rolling tag that can resolve to any 22.x patch release. The `zstdDecompressSync` API (used in `request-body-codec.ts`) was first shipped in Node 22.15.0. If this image is built against a stale Docker cache that points to an earlier 22.x build, the decompression call will fail at runtime with `TypeError: zstdDecompressSync is not a function`. Pinning to `node:22.15-slim` (or later) makes the minimum requirement explicit and matches what `package.json` `engines` already declares.

```suggestion
FROM node:22.15-slim AS runner
```

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

Comment on lines +163 to +171
if (encodings.length > MAX_CONTENT_ENCODING_LAYERS) {
// 防御多层编码放大:每层都是一次同步解压,过多层数纯属攻击/异常。
throw new ProxyError(
`Too many content-encoding layers (${encodings.length}); at most ${MAX_CONTENT_ENCODING_LAYERS} are allowed.`,
400
);
}

const unsupported = encodings.filter((enc) => !SUPPORTED_ENCODINGS.has(enc));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The multi-layer encoding check happens after the unsupported-encoding passthrough, so a request with two layers of supported encodings correctly gets a 400, while one with an unsupported outer encoding is passed through without hitting the layer limit. The ordering is intentional, but the inline comment doesn't mention that the passthrough also bypasses the layer-count guard — a future reader tightening the limit might not realise this. Consider adding a note to make the intentional bypass explicit.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/request-body-codec.ts
Line: 163-171

Comment:
The multi-layer encoding check happens **after** the unsupported-encoding passthrough, so a request with two layers of supported encodings correctly gets a 400, while one with an unsupported outer encoding is passed through without hitting the layer limit. The ordering is intentional, but the inline comment doesn't mention that the passthrough also bypasses the layer-count guard — a future reader tightening the limit might not realise this. Consider adding a note to make the intentional bypass explicit.

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

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
src/actions/model-prices.ts (1)

142-142: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

统一覆盖列表与导入键的归一化,避免同轮重复写入同一模型。

Line 142 的 overwriteSet 未做 trim(),且 Line 164-167 才对模型名归一化;当输入同时出现 "model"" model " 时,会在同一轮被当作两条记录处理,导致覆盖判断偏差和重复写入风险。

建议修改
-    const entries = Object.entries(priceTable).filter(([modelName]) => {
-      // 排除元数据字段
-      if (METADATA_FIELDS.includes(modelName)) {
-        logger.debug(`跳过元数据字段: ${modelName}`);
-        return false;
-      }
-      return typeof modelName === "string" && modelName.trim().length > 0;
-    });
-
-    // 创建覆盖列表的 Set 用于快速查找
-    const overwriteSet = new Set(overwriteManual ?? []);
+    const normalizedEntries = new Map<string, unknown>();
+    for (const [rawModelName, priceData] of Object.entries(priceTable)) {
+      if (METADATA_FIELDS.includes(rawModelName)) {
+        logger.debug(`跳过元数据字段: ${rawModelName}`);
+        continue;
+      }
+      const modelName = rawModelName.trim();
+      if (!modelName) continue;
+      // 同名归一化后保留最后一次定义,避免同轮重复写入
+      normalizedEntries.set(modelName, priceData);
+    }
+    const entries = Array.from(normalizedEntries.entries());
+
+    // 创建覆盖列表的 Set 用于快速查找(与 modelName 归一化保持一致)
+    const overwriteSet = new Set((overwriteManual ?? []).map((name) => name.trim()).filter(Boolean));
@@
-    for (const [rawModelName, priceData] of entries) {
-      // 与 manual 记录入库时(upsertModelPrice 使用 trim 后的名称)保持一致地归一化,
-      // 避免云端表里带空白的同名键绕过本地手动模型的保护检查。
-      const modelName = typeof rawModelName === "string" ? rawModelName.trim() : rawModelName;
+    for (const [modelName, priceData] of entries) {

Also applies to: 164-167, 187-187

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/actions/model-prices.ts` at line 142, Normalize entries when creating
overwriteSet so keys match the later model-name normalization: trim whitespace
(and apply the same normalization rules used in the block around the existing
model-name normalization at lines 164-167) for every value from overwriteManual
before adding to the Set; also apply the same normalized trimming behavior for
any other user-provided model key uses (e.g., the logic referenced near line
187) so "model" and " model " are treated identically by overwriteSet and the
overwrite-check logic.
🧹 Nitpick comments (2)
src/app/v1/_lib/proxy/request-body-codec.ts (1)

25-25: ⚡ Quick win

导入路径建议改为 @/ 别名。

Line 25 新增了相对导入,和仓库 TS 导入规范不一致。建议改为 @/app/v1/_lib/proxy/errors 形式。

As per coding guidelines "Use path alias @/ to map to ./src/ for imports".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/v1/_lib/proxy/request-body-codec.ts` at line 25, Update the import
for ProxyError in request-body-codec.ts to use the project path-alias instead of
a relative path: replace the relative import that references "./errors" with the
aliased module "`@/app/v1/_lib/proxy/errors`" so the ProxyError symbol is imported
via the TS `@/` -> ./src/ mapping and adheres to the repository import convention.
src/app/v1/_lib/proxy/session.ts (1)

32-32: ⚡ Quick win

新增导入建议使用 @/ 别名路径。

Line 32 建议改为 @/app/v1/_lib/proxy/request-body-codec,保持与仓库导入规范一致。

As per coding guidelines "Use path alias @/ to map to ./src/ for imports".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/v1/_lib/proxy/session.ts` at line 32, Import uses a relative path for
decodeRequestBody; change the import in src/app/v1/_lib/proxy/session.ts to use
the repository alias by replacing the relative module path with
"`@/app/v1/_lib/proxy/request-body-codec`" so the symbol decodeRequestBody is
imported via the project alias consistent with the coding guideline "Use path
alias `@/` to map to ./src/".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/pr-check.yml:
- Line 34: 将 CI 的 Node 版本从宽泛的 "22" 锁定到具体下限以避免运行时漂移:在
`.github/workflows/pr-check.yml` 中把 node-version 字段的值改为
"22.15.x"(或更高的明确版本),确保使用的 Node API(如 zstdDecompressSync)在 CI 环境中可用并且不会因 22.x
的不同次版本而失败;更新后运行一次 CI 以验证兼容性。

In `@Dockerfile`:
- Line 15: The Dockerfile uses a floating runtime image tag ("FROM
node:22-slim"); change this to a fully pinned patch-level image (for example
"node:22.15.0-slim" or a specific distro variant like
"node:22.15.0-bookworm-slim") to ensure reproducible builds—update the FROM
instruction that currently references node:22-slim to the chosen exact patch tag
across build stages.

In `@src/actions/notifications.ts`:
- Around line 43-46: The scheduleNotifications import/call can throw and
currently bubbles to the outer catch after updateNotificationSettings(payload)
succeeded; wrap the dynamic import and await scheduleNotifications() in their
own try/catch so any errors are caught, logged (or processLogger.warn/error) and
not rethrown—i.e., perform a fail-open: use try { const { scheduleNotifications
} = await import("`@/lib/notification/notification-queue`"); await
scheduleNotifications(); } catch (err) { /* log err but do not throw */ } to
ensure updateNotificationSettings completes successfully even if scheduling
fails.

---

Outside diff comments:
In `@src/actions/model-prices.ts`:
- Line 142: Normalize entries when creating overwriteSet so keys match the later
model-name normalization: trim whitespace (and apply the same normalization
rules used in the block around the existing model-name normalization at lines
164-167) for every value from overwriteManual before adding to the Set; also
apply the same normalized trimming behavior for any other user-provided model
key uses (e.g., the logic referenced near line 187) so "model" and " model " are
treated identically by overwriteSet and the overwrite-check logic.

---

Nitpick comments:
In `@src/app/v1/_lib/proxy/request-body-codec.ts`:
- Line 25: Update the import for ProxyError in request-body-codec.ts to use the
project path-alias instead of a relative path: replace the relative import that
references "./errors" with the aliased module "`@/app/v1/_lib/proxy/errors`" so
the ProxyError symbol is imported via the TS `@/` -> ./src/ mapping and adheres to
the repository import convention.

In `@src/app/v1/_lib/proxy/session.ts`:
- Line 32: Import uses a relative path for decodeRequestBody; change the import
in src/app/v1/_lib/proxy/session.ts to use the repository alias by replacing the
relative module path with "`@/app/v1/_lib/proxy/request-body-codec`" so the symbol
decodeRequestBody is imported via the project alias consistent with the coding
guideline "Use path alias `@/` to map to ./src/".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0ba0c110-2a7c-4958-90d6-a568df2f0e1c

📥 Commits

Reviewing files that changed from the base of the PR and between 79bc572 and 08599ec.

📒 Files selected for processing (28)
  • .github/workflows/pr-check.yml
  • Dockerfile
  • README.en.md
  • README.md
  • deploy/Dockerfile
  • package.json
  • src/actions/model-prices.ts
  • src/actions/notifications.ts
  • src/app/v1/_lib/models/available-models.ts
  • src/app/v1/_lib/proxy/request-body-codec.ts
  • src/app/v1/_lib/proxy/session.ts
  • src/components/ui/__tests__/provider-group-tag-input.test.tsx
  • src/components/ui/tag-input.tsx
  • src/instrumentation.ts
  • src/lib/api-client/v1/actions/providers.ts
  • src/lib/lifecycle/benign-errors.ts
  • src/lib/notification/notification-queue.ts
  • src/repository/model-price.ts
  • tests/unit/actions/model-prices.test.ts
  • tests/unit/api/v1/api-client-actions.test.ts
  • tests/unit/benign-broken-pipe-error.test.ts
  • tests/unit/instrumentation-crash-handler.test.ts
  • tests/unit/notification/notification-queue.test.ts
  • tests/unit/proxy/available-models.test.ts
  • tests/unit/proxy/request-body-codec.test.ts
  • tests/unit/proxy/session-request-decode.test.ts
  • tests/unit/settings/providers/provider-form-endpoint-pool.test.tsx
  • tests/unit/settings/providers/provider-form-total-limit-ui.test.tsx

uses: actions/setup-node@v6
with:
node-version: '20'
node-version: '22'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

在 Node.js 官方文档中确认:node:zlib的 zstd 同步解压(如zstdDecompressSync)从哪个 Node 版本开始可用?Node 22.0.0 是否保证可用?

💡 Result:

在 Node.js 中,node:zlib 模块提供的 Zstd 同步解压方法 zstdDecompressSync 是从 Node.js v22.15.0 版本开始可用的 [1][2]。 关于您询问的 Node 22.0.0 版本:该功能在 Node 22.0.0 中不可用 [2]。zstdDecompressSync 直到后续的补丁版本 v22.15.0 才被正式加入到 API 中 [1][2]。如果您需要在 Node 22.x 系列中使用此功能,请确保您的 Node.js 版本至少升级到 v22.15.0 或更高版本。

Citations:


CI Node 版本应固定到明确下限(22.15.x)
当前 .github/workflows/pr-check.ymlnode-version 固定为 "22",可能解析到 22.15.0 之前的 22.x;而 node:zlibzstdDecompressSync 需要至少 Node.js v22.15.0 才提供(22.0.0 不包含该能力)。为避免 CI 运行时漂移,建议固定到 22.15.x(或更高)。

建议修改
-          node-version: '22'
+          node-version: "22.15.x"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pr-check.yml at line 34, 将 CI 的 Node 版本从宽泛的 "22"
锁定到具体下限以避免运行时漂移:在 `.github/workflows/pr-check.yml` 中把 node-version 字段的值改为
"22.15.x"(或更高的明确版本),确保使用的 Node API(如 zstdDecompressSync)在 CI 环境中可用并且不会因 22.x
的不同次版本而失败;更新后运行一次 CI 以验证兼容性。

Comment thread Dockerfile
RUN --mount=type=cache,target=/app/.next/cache bun run build

FROM node:20-slim AS runner
FROM node:22-slim AS runner

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Node 官方 Docker 镜像是否提供 node:22.15-slim(或 22.15.x-slim)标签?推荐的生产固定标签策略是什么?

💡 Result:

是的,Node 官方 Docker 镜像提供 node:22.15-slim 这一类“具体版本+slim”的标签;但从官方文档当前维护的标签列表来看,更常见/推荐使用的精确固定形式是 node:22.15.0-slim(或 node:22.15.0-*),因为 22.15.x-slim/22.15-slim 这类“模糊 x”并不是官方文档中最标准的命名方式。 证据:Docker Hub 对 node:22.15-slim 有对应镜像条目(可拉取的标签存在)[1],同时官方镜像文档列出了 node:<version>-slim 体系及具体支持标签形如 22.15-slim22.15.0-slim(以及不同 Debian 发行版变体)[2]。 推荐的生产固定标签策略: 1) 固定到“完整版本号 + 变体” - 直接使用类似 node:22.15.0-slim(或你需要的 -bullseye-slim/-bookworm-slim/-trixie-slim 等后缀),避免使用 node:22-slimnode:lts-slimnode:current-slim 这类会随时间移动的浮动标签。[3][2] 2) 记录并更新策略(自动更新但可回滚) - 用固定精确标签保证可复现;当需要升级时再显式把镜像版本从旧的 22.15.0-slim 变为新版本(例如下一次补丁/次版本),由 CI/CD 触发验证后再发布。 3) 如需“最小体积”,才用 -slim - 官方对 -slim 的描述是:它不包含默认标签里的常见包,只包含运行 node 所需的最少组件;除非你明确在空间敏感场景,否则建议优先使用默认镜像(但这不改变“生产固定到精确版本”的策略)[4][2]。 如果你告诉我你希望基于哪个 Debian 发行版(bookworm/bullseye/trixie)以及是否需要 openssl/构建工具等,我也可以给出你可以直接写进 Dockerfile/compose 的具体推荐标签组合(如 node:22.15.0-bookworm-slim vs node:22.15.0-slim)。

Citations:


建议将运行时基础镜像固定到精确补丁版本,避免使用漂移标签(Dockerfile 第15行:node:22-slim

  • node:22-slim/类似“宽泛标签”会随时间移动,影响可重复性;而 engines >=22.15.0 更适合用精确匹配的 node:22.15.0-slim(或按需选择 -bookworm-slim / -bullseye-slim 等发行版变体)来固定运行时。
  • 目前虽然存在 node:22.15-slim,但生产实践上仍更建议直接 pin 到完整版本(到 .0)以减少补丁层面的漂移风险。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` at line 15, The Dockerfile uses a floating runtime image tag
("FROM node:22-slim"); change this to a fully pinned patch-level image (for
example "node:22.15.0-slim" or a specific distro variant like
"node:22.15.0-bookworm-slim") to ensure reproducible builds—update the FROM
instruction that currently references node:22-slim to the chosen exact patch tag
across build stages.

Comment on lines +43 to +46
// 重新调度通知任务,使总开关、子开关、时间/间隔等变更立即生效(添加/移除 repeatable 作业)。
// 动态导入避免静态加载 Bull;scheduleNotifications 内部已 fail-open,缺少 REDIS_URL 时不会影响设置保存。
const { scheduleNotifications } = await import("@/lib/notification/notification-queue");
await scheduleNotifications();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

将调度调用改为真正的 fail-open,避免“设置已保存但返回失败”

这里如果 import()scheduleNotifications() 抛错,会进入外层 catch,但 updateNotificationSettings(payload) 已经成功提交,接口会返回失败并可能诱发重复提交。

建议修改
-    const { scheduleNotifications } = await import("`@/lib/notification/notification-queue`");
-    await scheduleNotifications();
+    try {
+      const { scheduleNotifications } = await import("`@/lib/notification/notification-queue`");
+      await scheduleNotifications();
+    } catch {
+      // fail-open: 调度失败不影响设置保存结果
+    }
📝 Committable suggestion

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

Suggested change
// 重新调度通知任务,使总开关、子开关、时间/间隔等变更立即生效(添加/移除 repeatable 作业)。
// 动态导入避免静态加载 Bull;scheduleNotifications 内部已 fail-open,缺少 REDIS_URL 时不会影响设置保存。
const { scheduleNotifications } = await import("@/lib/notification/notification-queue");
await scheduleNotifications();
// 重新调度通知任务,使总开关、子开关、时间/间隔等变更立即生效(添加/移除 repeatable 作业)。
// 动态导入避免静态加载 Bull;scheduleNotifications 内部已 fail-open,缺少 REDIS_URL 时不会影响设置保存。
try {
const { scheduleNotifications } = await import("`@/lib/notification/notification-queue`");
await scheduleNotifications();
} catch {
// fail-open: 调度失败不影响设置保存结果
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/actions/notifications.ts` around lines 43 - 46, The scheduleNotifications
import/call can throw and currently bubbles to the outer catch after
updateNotificationSettings(payload) succeeded; wrap the dynamic import and await
scheduleNotifications() in their own try/catch so any errors are caught, logged
(or processLogger.warn/error) and not rethrown—i.e., perform a fail-open: use
try { const { scheduleNotifications } = await
import("`@/lib/notification/notification-queue`"); await scheduleNotifications();
} catch (err) { /* log err but do not throw */ } to ensure
updateNotificationSettings completes successfully even if scheduling fails.

@github-actions github-actions Bot added the size/L Large PR (< 1000 lines) label Jun 3, 2026
Comment thread Dockerfile
RUN --mount=type=cache,target=/app/.next/cache bun run build

FROM node:20-slim AS runner
FROM node:22-slim AS runner

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Medium] [STANDARD-VIOLATION] Rolling Docker tag doesn't guarantee the minimum Node version required by engines

package.json declares "engines": { "node": ">=22.15.0" } and request-body-codec.ts relies on zstdDecompressSync (available since 22.15). The node:22-slim tag is rolling and can resolve to any 22.x patch. If Docker build cache is stale and resolves to a pre-22.15 image, zstdDecompressSync will throw TypeError at runtime for any zstd-encoded request body.

The deploy/Dockerfile already uses node:trixie-slim (Node 24+) with a comment explaining the 22.15 requirement. The main Dockerfile should be similarly explicit.

Suggested fix:

FROM node:22.15-slim AS runner

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review Summary

This release PR (v0.8.4) introduces compressed request body support (zstd/gzip/deflate/br), benign EPIPE suppression, notification scheduling fixes, local-first model price protection, and UI regression fixes. The new code is well-structured with strong test coverage (~1,400 lines of new tests). Two issues were identified.

PR Size: L

  • Lines changed: 2,172 (2,049 additions + 123 deletions)
  • Files changed: 28

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 1 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 1 0

High Priority Issues (Should Fix)

1. [LOGIC-BUG] Missing runtime switch guard for cache-hit-rate-alert notification type
src/lib/notification/notification-queue.ts ~line 495 (dev branch)

This PR adds runtime switch guards to the circuit-breaker, daily-leaderboard, and cost-alert processor cases, but omits the cache-hit-rate-alert case. The PR's stated goal is "Fix notification scheduling -- master/sub switches still fire when master switch is off". Without a guard, an existing repeatable cache-hit-rate-alert job will still execute and send notifications even after the user disables the master switch or cacheHitRateAlertEnabled sub-switch.

All three other cases follow the same guard pattern:

const { getNotificationSettings } = await import("@/repository/notifications");
const settings = await getNotificationSettings();
if (\!settings.enabled || \!settings.<subSwitch>Enabled) {
  logger.info({ action: "<type>_disabled", jobId: job.id });
  return { success: true, skipped: true };
}

Suggested fix — add the same guard to the cache-hit-rate-alert case, before the payload generation logic:

case "cache-hit-rate-alert": {
  const { getNotificationSettings } = await import("@/repository/notifications");
  const settings = await getNotificationSettings();
  if (\!settings.enabled || \!settings.cacheHitRateAlertEnabled) {
    logger.info({ action: "cache_hit_rate_alert_disabled", jobId: job.id });
    return { success: true, skipped: true };
  }
  // ... existing payload generation logic

Medium Priority Issues

2. [STANDARD-VIOLATION] Dockerfile rolling tag doesn't guarantee minimum Node 22.15 (inline comment posted)

node:22-slim is a rolling tag; package.json engines requires >=22.15.0 for native zstdDecompressSync. Stale Docker cache could produce an image where zstd decompression fails silently. Pin to node:22.15-slim to match the engines declaration (deploy/Dockerfile already uses node:trixie-slim for Node 24+).

Review Coverage

  • Logic and correctness
  • Security (OWASP Top 10)
  • Error handling
  • Type safety
  • Documentation accuracy
  • Test coverage
  • Code clarity

Automated review by Claude AI

请求过滤器与错误规则在增删改/启停后立即同步代理内存缓存并刷新 UI 列表,无需手动点刷新缓存。RequestFilterEngine.reload() 增加并发补跑队列;mutation action 写库后 await reload(best-effort,失败不影响写入成功语义,并复用 emit 触发的在途 reload 避免双读);error-rules UI 各操作补 router.refresh()。处理了 Gemini/CodeRabbit 的 review 意见。

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09d549ca54

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// 非 multipart:按 content-encoding(zstd/gzip/deflate/br)解压请求体,
// 使下游模型解析、过滤、计费、日志与转发都基于明文。
const decodedBody = decodeRequestBody(rawBodyBuffer, contentEncoding);
const requestBodyBuffer = decodedBody.buffer;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Authenticate before inflating request bodies

For requests with supported content-encoding, this synchronously decompresses the body while ProxySession.fromContext is still building the session, before the auth guard has rejected invalid tokens. Because the proxy path has no compressed input-size cap and allows up to 100 MB of decompressed output, an unauthenticated client can repeatedly send small gzip/zstd bombs to force large CPU and memory work before authorization; move the auth decision ahead of decompression or add a much tighter pre-auth compressed/decompressed limit.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
src/app/[locale]/settings/error-rules/_components/rule-list-table.tsx (1)

157-178: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

为这些仅图标控件补上可访问名称。

这里的 Switch、编辑按钮和删除按钮都没有可访问名称,读屏器只会读成“switch / button”,用户无法判断具体操作,会直接影响错误规则管理流程。建议为每个控件补 aria-label(必要时再加 title),并通过 t(...) 提供翻译;这样测试也能改成按 role/name 查询,不再依赖图标类名。

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

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`[locale]/settings/error-rules/_components/rule-list-table.tsx around
lines 157 - 178, Add accessible, translatable names for the icon-only controls:
supply aria-label (and title if desired) to the Switch and both Buttons using
the i18n function t(...). For the Switch in the rule row (component using Switch
and handleToggleEnabled) provide a label that reflects the rule state or action
(e.g., t('rules.enable_rule', { name: rule.pattern }) or t('rules.disable_rule',
... ) depending on rule.isEnabled). For the edit Button (handleEdit + Pencil)
add aria-label/title like t('rules.edit_rule', { name: rule.pattern }). For the
delete Button (handleDelete + Trash2) add aria-label/title like
t('rules.delete_rule', { name: rule.pattern }) and preserve disabled behavior
when rule.isDefault. Ensure all strings are added to the i18n resource files for
the supported locales.
🧹 Nitpick comments (3)
tests/unit/actions/error-rules-cache-reload.test.ts (1)

68-135: ⚡ Quick win

补一条 refreshCacheAction() 的参数断言测试。

这个套件现在只覆盖了 create/update/delete,但本次改动里 refreshCacheAction() 的关键语义是 reload({ queueIfRunning: true })。如果后续有人回退成普通 reload(),当前测试不会报警。

建议补测
+  it("refreshCacheAction queues a follow-up reload when one is already running", async () => {
+    const { refreshCacheAction } = await import("`@/actions/error-rules`");
+    const res = await refreshCacheAction();
+
+    expect(res.ok).toBe(true);
+    expect(reloadMock).toHaveBeenCalledWith({ queueIfRunning: true });
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/actions/error-rules-cache-reload.test.ts` around lines 68 - 135,
Add a unit test for refreshCacheAction to assert it calls reload with the
expected options; specifically, import refreshCacheAction from
"`@/actions/error-rules`" in the test suite and verify that calling
refreshCacheAction() results in reload being invoked with the object {
queueIfRunning: true } (i.e., expect(reloadMock).toHaveBeenCalledWith({
queueIfRunning: true })); this ensures the key semantic in refreshCacheAction
(calling reload({ queueIfRunning: true })) is covered so regressions to a plain
reload() will fail the test.
tests/unit/actions/request-filters-cache-reload.test.ts (1)

105-123: ⚡ Quick win

reload(false) 的参数契约也断言到 update/delete。

这组测试目前只在 create 场景验证了 reload(false),update/delete 只检查“被调用过”。如果后续有人把它们改回 reload(),测试仍会通过,但会重新引入额外 DB 读取。建议和 create 一样显式断言 toHaveBeenCalledWith(false)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/actions/request-filters-cache-reload.test.ts` around lines 105 -
123, 测试中 updateRequestFilterAction 和 deleteRequestFilterAction 仅断言 reloadMock
被调用,但未验证参数;请在对应测试块中像 create 场景一样断言 reloadMock 被调用时传入 false(即为
updateRequestFilterAction 和 deleteRequestFilterAction 的断言加入
expect(reloadMock).toHaveBeenCalledWith(false) 或等价断言),以确保它们调用 reload(false)
而不是无参 reload(),参照测试中使用的 updateRequestFilterAction、deleteRequestFilterAction 和
reloadMock 标识定位并修改相应断言。
tests/unit/error-rules-list-refresh-ui.test.tsx (1)

191-304: ⚡ Quick win

把失败分支也补进这组回归测试。

现在只证明了“成功后会 router.refresh()”,但没有覆盖“action 返回 ok: false 或抛错时绝不能 refresh”这个同样关键的契约。若后续有人把 router.refresh() 挪到错误分支外侧,这组测试仍会全部通过。建议至少为 toggle、delete 和 refresh-cache 各补一个失败路径断言:expect(refreshMock).not.toHaveBeenCalled()

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/error-rules-list-refresh-ui.test.tsx` around lines 191 - 304, The
tests only assert router.refresh() is called on successful actions; add
failing-branch tests for toggle (RuleListTable + updateErrorRuleActionMock),
delete (RuleListTable + deleteErrorRuleActionMock) and refresh-cache
(RefreshCacheButton + refreshCacheActionMock) that simulate the action returning
{ ok: false } or throwing, then assert refreshMock.not.toHaveBeenCalled();
modify the corresponding mocks (updateErrorRuleActionMock,
deleteErrorRuleActionMock, refreshCacheActionMock) before triggering the UI
interactions and verify the UI still behaves but does not call refreshMock in
the failure case.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/actions/error-rules.ts`:
- Around line 179-187: The call to emitErrorRulesUpdated() should be made
best-effort so transient pub/sub/Redis failures don't cause the whole action to
return ok: false; wrap the emitErrorRulesUpdated() call in its own try/catch
(similar to how errorRuleDetector.reload() is handled) and on error log a
warning with context (e.g., "[ErrorRulesAction] Failed to emit rules update")
but do not rethrow; apply the same change to the other occurrences referenced
(the other emitErrorRulesUpdated() calls around the 321-327 and 382-388 regions)
so emits never bubble errors back to the caller.

---

Outside diff comments:
In `@src/app/`[locale]/settings/error-rules/_components/rule-list-table.tsx:
- Around line 157-178: Add accessible, translatable names for the icon-only
controls: supply aria-label (and title if desired) to the Switch and both
Buttons using the i18n function t(...). For the Switch in the rule row
(component using Switch and handleToggleEnabled) provide a label that reflects
the rule state or action (e.g., t('rules.enable_rule', { name: rule.pattern })
or t('rules.disable_rule', ... ) depending on rule.isEnabled). For the edit
Button (handleEdit + Pencil) add aria-label/title like t('rules.edit_rule', {
name: rule.pattern }). For the delete Button (handleDelete + Trash2) add
aria-label/title like t('rules.delete_rule', { name: rule.pattern }) and
preserve disabled behavior when rule.isDefault. Ensure all strings are added to
the i18n resource files for the supported locales.

---

Nitpick comments:
In `@tests/unit/actions/error-rules-cache-reload.test.ts`:
- Around line 68-135: Add a unit test for refreshCacheAction to assert it calls
reload with the expected options; specifically, import refreshCacheAction from
"`@/actions/error-rules`" in the test suite and verify that calling
refreshCacheAction() results in reload being invoked with the object {
queueIfRunning: true } (i.e., expect(reloadMock).toHaveBeenCalledWith({
queueIfRunning: true })); this ensures the key semantic in refreshCacheAction
(calling reload({ queueIfRunning: true })) is covered so regressions to a plain
reload() will fail the test.

In `@tests/unit/actions/request-filters-cache-reload.test.ts`:
- Around line 105-123: 测试中 updateRequestFilterAction 和 deleteRequestFilterAction
仅断言 reloadMock 被调用,但未验证参数;请在对应测试块中像 create 场景一样断言 reloadMock 被调用时传入 false(即为
updateRequestFilterAction 和 deleteRequestFilterAction 的断言加入
expect(reloadMock).toHaveBeenCalledWith(false) 或等价断言),以确保它们调用 reload(false)
而不是无参 reload(),参照测试中使用的 updateRequestFilterAction、deleteRequestFilterAction 和
reloadMock 标识定位并修改相应断言。

In `@tests/unit/error-rules-list-refresh-ui.test.tsx`:
- Around line 191-304: The tests only assert router.refresh() is called on
successful actions; add failing-branch tests for toggle (RuleListTable +
updateErrorRuleActionMock), delete (RuleListTable + deleteErrorRuleActionMock)
and refresh-cache (RefreshCacheButton + refreshCacheActionMock) that simulate
the action returning { ok: false } or throwing, then assert
refreshMock.not.toHaveBeenCalled(); modify the corresponding mocks
(updateErrorRuleActionMock, deleteErrorRuleActionMock, refreshCacheActionMock)
before triggering the UI interactions and verify the UI still behaves but does
not call refreshMock in the failure case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e5b6e3f7-371d-41ee-8621-97c6ea7a3fd8

📥 Commits

Reviewing files that changed from the base of the PR and between 08599ec and 09d549c.

📒 Files selected for processing (12)
  • src/actions/error-rules.ts
  • src/actions/request-filters.ts
  • src/app/[locale]/settings/error-rules/_components/add-rule-dialog.tsx
  • src/app/[locale]/settings/error-rules/_components/edit-rule-dialog.tsx
  • src/app/[locale]/settings/error-rules/_components/refresh-cache-button.tsx
  • src/app/[locale]/settings/error-rules/_components/rule-list-table.tsx
  • src/lib/error-rule-detector.ts
  • src/lib/request-filter-engine.ts
  • tests/unit/actions/error-rules-cache-reload.test.ts
  • tests/unit/actions/request-filters-cache-reload.test.ts
  • tests/unit/error-rules-list-refresh-ui.test.tsx
  • tests/unit/lib/request-filter-engine-reload-queue.test.ts

Comment on lines 179 to +187
// 刷新缓存(事件广播,支持多 worker 同步)
await emitErrorRulesUpdated();
// 上面的 emit 已在本进程触发一次携带最新数据的 reload,这里复用它即可(无需补跑第二轮)。
// reload 仅为本进程缓存同步(跨 worker 由 emit 覆盖),失败不应把已成功的写入误报为失败。
try {
await errorRuleDetector.reload();
} catch (reloadError) {
logger.warn("[ErrorRulesAction] Failed to reload detector after mutation", { reloadError });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

emitErrorRulesUpdated() 也改成 best-effort。

现在只有 reload() 失败会被降级处理;如果数据库写入已经成功,但 emitErrorRulesUpdated() 因 Redis/pubsub 短暂故障抛错,这三个 action 仍会走外层 catch 返回 ok: false。创建路径下这会诱导用户重试并重复创建,更新/删除也会把已提交的变更误报成失败。

可选修正
-    await emitErrorRulesUpdated();
+    try {
+      await emitErrorRulesUpdated();
+    } catch (emitError) {
+      logger.warn("[ErrorRulesAction] Failed to broadcast error-rule update", { emitError });
+    }
     try {
       await errorRuleDetector.reload();
     } catch (reloadError) {
       logger.warn("[ErrorRulesAction] Failed to reload detector after mutation", { reloadError });
     }

Also applies to: 321-327, 382-388

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/actions/error-rules.ts` around lines 179 - 187, The call to
emitErrorRulesUpdated() should be made best-effort so transient pub/sub/Redis
failures don't cause the whole action to return ok: false; wrap the
emitErrorRulesUpdated() call in its own try/catch (similar to how
errorRuleDetector.reload() is handled) and on error log a warning with context
(e.g., "[ErrorRulesAction] Failed to emit rules update") but do not rethrow;
apply the same change to the other occurrences referenced (the other
emitErrorRulesUpdated() calls around the 321-327 and 382-388 regions) so emits
never bubble errors back to the caller.

… & timezone-aware timestamps (#1245)

Add XLSX usage-logs export (detail sheet + daily/hourly summary sheet), Excel-safe numeric normalization (<=15 sig digits) so SUM() works, and timezone-aware timestamps rendered in the system timezone. Streams batches (no full row retention), async zip, hand-rolled OOXML via fflate. Addressed automated review: OOXML gray125 fill, strict XML 1.0 char filter, invalid-Date guards, sync-xlsx rejection, typed download cast, test cleanup.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e3070dd131

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +179 to +182
return {
buffer,
decoded: false,
encoding: null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject unsupported encodings instead of pretending passthrough

When a client sends a non-empty request with an unsupported Content-Encoding (for example compress), this branch returns decoded: false/encoding: null, so ProxySession leaves the content-encoding header intact, but the normal forwarder path still parses/logs the raw compressed bytes as text and later sends JSON.stringify(session.request.message) rather than session.request.buffer. The upstream then receives an uncompressed JSON body while the original encoding header is still present, causing it to try to decode the wrong bytes; either reject unsupported encodings on preprocessed endpoints or carry an explicit raw-passthrough signal through forwarding.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (2)
src/actions/usage-logs.ts (1)

177-191: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

导出路径漏掉了未完成请求的 live-chain 补全。

这里直接把 findUsageLogsBatch() 的结果拿去渲染,但同文件 Line 686-706 的 getUsageLogsBatch() 会先为未 finalized 的行补 _liveChain。现在导出列里的 Retry Count 仍只看 providerChainsrc/lib/usage-logs/export/columns.ts Line 30-31),所以导出正在进行中的请求时,重试次数和链路相关字段会落成 0/旧值,和列表页不一致。建议把这里也复用同一套 live-chain merge 逻辑,并让导出列优先读取 _liveChain.chain

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/actions/usage-logs.ts` around lines 177 - 191, The export is using
findUsageLogsBatch() results directly which misses the live-chain completion
applied in getUsageLogsBatch(), causing Retry Count and chain fields to be stale
for in-progress requests; before passing batch.logs into buildDetailRowXml or
buildCsvRows, run the same live-chain merge logic used by getUsageLogsBatch()
(apply the _liveChain augmentation to each log) and update the export columns
(export/columns.ts) to prefer reading _liveChain.chain (and related fields) over
providerChain so Retry Count reflects the live chain; reference
findUsageLogsBatch, getUsageLogsBatch, _liveChain, buildDetailRowXml,
buildCsvRows, and the export columns module when making the changes.
tests/unit/dashboard-logs-export-progress-ui.test.tsx (1)

31-35: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

这里 mock 错了模块路径,当前测试并没有拦住组件实际调用。

UsageLogsFilters 导入的是 @/lib/api-client/v1/actions/usage-logs,但这里 mock 的是 @/actions/usage-logs。这样一来,下面对 startUsageLogsExportMock / downloadUsageLogsExportMock 的断言并不能证明组件导出链路真的按预期工作。

建议修正
-vi.mock("`@/actions/usage-logs`", () => ({
+vi.mock("`@/lib/api-client/v1/actions/usage-logs`", () => ({
   startUsageLogsExport: startUsageLogsExportMock,
   getUsageLogsExportStatus: getUsageLogsExportStatusMock,
   downloadUsageLogsExport: downloadUsageLogsExportMock,
 }));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/dashboard-logs-export-progress-ui.test.tsx` around lines 31 - 35,
The test is mocking the wrong module path so the component still calls the real
implementation; update the mock to target the same module imported by
UsageLogsFilters (replace "`@/actions/usage-logs`" with
"`@/lib/api-client/v1/actions/usage-logs`") so that startUsageLogsExportMock,
getUsageLogsExportStatusMock, and downloadUsageLogsExportMock are actually used
by the component under test; ensure the vi.mock call references that exact
module string and keep the same mock exports (startUsageLogsExport,
getUsageLogsExportStatus, downloadUsageLogsExport) so your assertions on
startUsageLogsExportMock/downloadUsageLogsExportMock validate the real export
flow.
🧹 Nitpick comments (3)
tests/api/v1/usage-logs/usage-logs.test.ts (1)

78-87: ⚡ Quick win

format 也纳入导出状态测试。

这次 UsageLogsExportStatus 新增了 format,但这里的 fixture 和 GET /exports/{jobId} 断言都没覆盖它;状态接口即使漏返回该字段,这组 API 测试也会继续通过。建议在 mock 里补上 format: "csv",并顺手断言响应里包含它。

Also applies to: 270-276

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/api/v1/usage-logs/usage-logs.test.ts` around lines 78 - 87, The
export-status test fixture is missing the new format field so add format: "csv"
to the object returned by getUsageLogsExportStatusMock.mockResolvedValue(...)
and update the corresponding GET /exports/{jobId} response assertion to expect
response.body.data.format === "csv" (also apply the same change for the other
fixture used around the later test block that calls
getUsageLogsExportStatusMock); reference the mock function
getUsageLogsExportStatusMock and the route/handler that asserts the export
status response to locate the places to change.
src/lib/usage-logs/export/xlsx.ts (1)

23-31: ⚡ Quick win

改用 @/ 别名导入本地模块。

这一组 ./columns./format./numeric./summary 不符合仓库当前的导入约定。

As per coding guidelines **/*.{ts,tsx,js,jsx}: Use path alias @/ to map to ./src/ for imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/usage-logs/export/xlsx.ts` around lines 23 - 31, Update the local
imports in xlsx.ts to use the repository path alias instead of relative paths:
replace "./columns", "./format", "./numeric" and "./summary" with the
corresponding "`@/lib/usage-logs/export/`..." module paths so they resolve from
src via the `@/` alias; ensure you keep the same named imports (e.g., isValidDate,
toExcelZonedDate, normalizeDecimalForSpreadsheet, createSummaryAccumulator,
SUMMARY_HEADERS, SummaryRow, UsageLogsSummary) and adjust any import specifiers
to match the aliased module locations.
src/lib/usage-logs/export/summary.ts (1)

13-14: ⚡ Quick win

改用 @/ 别名导入本地模块。

这里的 ./format./numeric 偏离了仓库当前的导入约定,后续移动 src/ 目录时也更难统一重构。

As per coding guidelines **/*.{ts,tsx,js,jsx}: Use path alias @/ to map to ./src/ for imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/usage-logs/export/summary.ts` around lines 13 - 14, Replace the local
relative imports with project path-alias imports: update the imports that bring
in isValidDate and toFiniteNumber (currently from "./format" and "./numeric") to
use "`@/lib/usage-logs/export/format`" and "`@/lib/usage-logs/export/numeric`"
respectively so they follow the repository alias convention and remain stable if
src/ is moved; ensure the imported symbols (isValidDate, toFiniteNumber) keep
the same names and update any import statements in summary.ts accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/api-client/v1/actions/usage-logs.ts`:
- Around line 85-96: The downloadUsageLogsExport action currently returns only a
Blob which drops Content-Disposition filename and server problem details; update
UsageLogsExportDownloadFile and downloadUsageLogsExport to parse the filename
from the response.headers.get('Content-Disposition') (add filename to the
returned object alongside blob) and, on non-ok responses, read and parse the
response JSON as a problem object (extract errorCode/errorParams and include
them in the thrown/rejected error or returned action result) before passing to
toActionResult so downstream callers (e.g., UsageLogsFilters) receive both the
filename and structured error info.

In `@src/lib/usage-logs/export/xlsx.ts`:
- Around line 243-267: The current assembleUsageLogsXlsx +
detailSheetXml/worksheetXml flow keeps the entire detail sheet in memory because
detailRowsXml: string[] is joined into one big string before zipping; change the
API and implementation to stream the detail sheet instead of materializing it:
replace detailRowsXml: string[] with an async iterable/iterator or chunked
reader (e.g., AsyncIterable<string> or generator) produced by buildDetailRowXml,
update assembleUsageLogsXlsx to create the "xl/worksheets/sheet1.xml" entry as a
streamed/iterable zip entry (emit header, then iterate yielding row chunks, then
footer) rather than calling detailSheetXml(...).join(""), and adjust
detailSheetXml/worksheetXml/buildDetailRowXml to emit/return chunks rather than
a full string so peak memory holds only current chunk + zip buffers. Ensure the
zip writer you use supports streaming entries (or switch to a streaming zip API)
and update callers/tests to provide the new async iterable interface.

---

Outside diff comments:
In `@src/actions/usage-logs.ts`:
- Around line 177-191: The export is using findUsageLogsBatch() results directly
which misses the live-chain completion applied in getUsageLogsBatch(), causing
Retry Count and chain fields to be stale for in-progress requests; before
passing batch.logs into buildDetailRowXml or buildCsvRows, run the same
live-chain merge logic used by getUsageLogsBatch() (apply the _liveChain
augmentation to each log) and update the export columns (export/columns.ts) to
prefer reading _liveChain.chain (and related fields) over providerChain so Retry
Count reflects the live chain; reference findUsageLogsBatch, getUsageLogsBatch,
_liveChain, buildDetailRowXml, buildCsvRows, and the export columns module when
making the changes.

In `@tests/unit/dashboard-logs-export-progress-ui.test.tsx`:
- Around line 31-35: The test is mocking the wrong module path so the component
still calls the real implementation; update the mock to target the same module
imported by UsageLogsFilters (replace "`@/actions/usage-logs`" with
"`@/lib/api-client/v1/actions/usage-logs`") so that startUsageLogsExportMock,
getUsageLogsExportStatusMock, and downloadUsageLogsExportMock are actually used
by the component under test; ensure the vi.mock call references that exact
module string and keep the same mock exports (startUsageLogsExport,
getUsageLogsExportStatus, downloadUsageLogsExport) so your assertions on
startUsageLogsExportMock/downloadUsageLogsExportMock validate the real export
flow.

---

Nitpick comments:
In `@src/lib/usage-logs/export/summary.ts`:
- Around line 13-14: Replace the local relative imports with project path-alias
imports: update the imports that bring in isValidDate and toFiniteNumber
(currently from "./format" and "./numeric") to use
"`@/lib/usage-logs/export/format`" and "`@/lib/usage-logs/export/numeric`"
respectively so they follow the repository alias convention and remain stable if
src/ is moved; ensure the imported symbols (isValidDate, toFiniteNumber) keep
the same names and update any import statements in summary.ts accordingly.

In `@src/lib/usage-logs/export/xlsx.ts`:
- Around line 23-31: Update the local imports in xlsx.ts to use the repository
path alias instead of relative paths: replace "./columns", "./format",
"./numeric" and "./summary" with the corresponding "`@/lib/usage-logs/export/`..."
module paths so they resolve from src via the `@/` alias; ensure you keep the same
named imports (e.g., isValidDate, toExcelZonedDate,
normalizeDecimalForSpreadsheet, createSummaryAccumulator, SUMMARY_HEADERS,
SummaryRow, UsageLogsSummary) and adjust any import specifiers to match the
aliased module locations.

In `@tests/api/v1/usage-logs/usage-logs.test.ts`:
- Around line 78-87: The export-status test fixture is missing the new format
field so add format: "csv" to the object returned by
getUsageLogsExportStatusMock.mockResolvedValue(...) and update the corresponding
GET /exports/{jobId} response assertion to expect response.body.data.format ===
"csv" (also apply the same change for the other fixture used around the later
test block that calls getUsageLogsExportStatusMock); reference the mock function
getUsageLogsExportStatusMock and the route/handler that asserts the export
status response to locate the places to change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 82ef7179-c378-40b2-b129-2b20e8e6b8cc

📥 Commits

Reviewing files that changed from the base of the PR and between 09d549c and e3070dd.

📒 Files selected for processing (27)
  • messages/en/dashboard.json
  • messages/ja/dashboard.json
  • messages/ru/dashboard.json
  • messages/zh-CN/dashboard.json
  • messages/zh-TW/dashboard.json
  • package.json
  • src/actions/usage-logs.ts
  • src/app/[locale]/dashboard/logs/_components/usage-logs-filters.tsx
  • src/app/api/v1/resources/usage-logs/handlers.ts
  • src/lib/api-client/v1/actions/usage-logs.ts
  • src/lib/api-client/v1/openapi-types.gen.ts
  • src/lib/api/v1/schemas/usage-logs.ts
  • src/lib/usage-logs/export/columns.ts
  • src/lib/usage-logs/export/csv.ts
  • src/lib/usage-logs/export/format.ts
  • src/lib/usage-logs/export/numeric.ts
  • src/lib/usage-logs/export/summary.ts
  • src/lib/usage-logs/export/xlsx.ts
  • tests/api/v1/usage-logs/usage-logs.test.ts
  • tests/unit/actions/usage-logs-export-retry-count.test.ts
  • tests/unit/actions/usage-logs-export-xlsx.test.ts
  • tests/unit/api/v1/api-client-actions.test.ts
  • tests/unit/dashboard-logs-export-progress-ui.test.tsx
  • tests/unit/usage-logs/export-csv.test.ts
  • tests/unit/usage-logs/export-numeric.test.ts
  • tests/unit/usage-logs/export-summary.test.ts
  • tests/unit/usage-logs/export-xlsx.test.ts
✅ Files skipped from review due to trivial changes (4)
  • messages/zh-CN/dashboard.json
  • messages/en/dashboard.json
  • src/lib/api-client/v1/openapi-types.gen.ts
  • messages/ja/dashboard.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • package.json
  • tests/unit/api/v1/api-client-actions.test.ts

Comment on lines +85 to 96
export interface UsageLogsExportDownloadFile {
blob: Blob;
}

export function downloadUsageLogsExport(jobId: string) {
return toActionResult(
return toActionResult<UsageLogsExportDownloadFile>(
fetch(`/api/v1/usage-logs/exports/${encodeURIComponent(jobId)}/download`, {
credentials: "include",
}).then(async (response) => {
if (!response.ok) throw new Error(response.statusText || "Export download failed");
return response.text();
return { blob: await response.blob() };
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

保留下载元信息和结构化错误,不要只返回 Blob

这里把下载响应压缩成 { blob } 后,服务端新加的 Content-Disposition 文件名和 problem errorCode/errorParams 都丢了。下游 UsageLogsFilters 只能猜文件名,并且失败时拿不到结构化错误码,和本 PR 新建的服务端契约已经脱节了。建议把 filename(至少)一并从响应头解析出来,并在非 2xx 时解析 problem body 后再交给 toActionResult

建议修正
 export interface UsageLogsExportDownloadFile {
   blob: Blob;
+  filename: string;
 }

 export function downloadUsageLogsExport(jobId: string) {
   return toActionResult<UsageLogsExportDownloadFile>(
     fetch(`/api/v1/usage-logs/exports/${encodeURIComponent(jobId)}/download`, {
       credentials: "include",
     }).then(async (response) => {
-      if (!response.ok) throw new Error(response.statusText || "Export download failed");
-      return { blob: await response.blob() };
+      if (!response.ok) {
+        const problem = await response.json().catch(() => null);
+        const error = new Error(problem?.detail || response.statusText || "Export download failed");
+        Object.assign(error, {
+          errorCode: problem?.errorCode,
+          errorParams: problem?.errorParams,
+        });
+        throw error;
+      }
+
+      const disposition = response.headers.get("content-disposition") ?? "";
+      const filename =
+        /filename="([^"]+)"/.exec(disposition)?.[1] ??
+        `usage-logs.${response.headers.get("content-type")?.includes("spreadsheetml") ? "xlsx" : "csv"}`;
+
+      return {
+        blob: await response.blob(),
+        filename,
+      };
     })
   );
 }
📝 Committable suggestion

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

Suggested change
export interface UsageLogsExportDownloadFile {
blob: Blob;
}
export function downloadUsageLogsExport(jobId: string) {
return toActionResult(
return toActionResult<UsageLogsExportDownloadFile>(
fetch(`/api/v1/usage-logs/exports/${encodeURIComponent(jobId)}/download`, {
credentials: "include",
}).then(async (response) => {
if (!response.ok) throw new Error(response.statusText || "Export download failed");
return response.text();
return { blob: await response.blob() };
})
export interface UsageLogsExportDownloadFile {
blob: Blob;
filename: string;
}
export function downloadUsageLogsExport(jobId: string) {
return toActionResult<UsageLogsExportDownloadFile>(
fetch(`/api/v1/usage-logs/exports/${encodeURIComponent(jobId)}/download`, {
credentials: "include",
}).then(async (response) => {
if (!response.ok) {
const problem = await response.json().catch(() => null);
const error = new Error(problem?.detail || response.statusText || "Export download failed");
Object.assign(error, {
errorCode: problem?.errorCode,
errorParams: problem?.errorParams,
});
throw error;
}
const disposition = response.headers.get("content-disposition") ?? "";
const filename =
/filename="([^"]+)"/.exec(disposition)?.[1] ??
`usage-logs.${response.headers.get("content-type")?.includes("spreadsheetml") ? "xlsx" : "csv"}`;
return {
blob: await response.blob(),
filename,
};
})
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/api-client/v1/actions/usage-logs.ts` around lines 85 - 96, The
downloadUsageLogsExport action currently returns only a Blob which drops
Content-Disposition filename and server problem details; update
UsageLogsExportDownloadFile and downloadUsageLogsExport to parse the filename
from the response.headers.get('Content-Disposition') (add filename to the
returned object alongside blob) and, on non-ok responses, read and parse the
response JSON as a problem object (extract errorCode/errorParams and include
them in the thrown/rejected error or returned action result) before passing to
toActionResult so downstream callers (e.g., UsageLogsFilters) receive both the
filename and structured error info.

Comment on lines +243 to +267
export interface XlsxParts {
/** Pre-rendered detail row XML (one entry per data row, row numbers from 2). */
detailRowsXml: string[];
summary: UsageLogsSummary;
timezone: string;
}

/**
* Assemble an XLSX workbook (detail sheet + daily/hourly summary sheet) from
* pre-rendered detail rows and an aggregated summary. Compression runs via
* fflate's async zip so a large export does not block the event loop.
*/
export function assembleUsageLogsXlsx(parts: XlsxParts): Promise<Uint8Array> {
const files: Record<string, Uint8Array> = {
"[Content_Types].xml": strToU8(CONTENT_TYPES_XML),
"_rels/.rels": strToU8(ROOT_RELS_XML),
"xl/workbook.xml": strToU8(workbookXml(summarySheetName(parts.summary))),
"xl/_rels/workbook.xml.rels": strToU8(WORKBOOK_RELS_XML),
"xl/styles.xml": strToU8(STYLES_XML),
"xl/worksheets/sheet1.xml": strToU8(detailSheetXml(parts.detailRowsXml, parts.timezone)),
"xl/worksheets/sheet2.xml": strToU8(buildSummarySheet(parts.summary)),
};
return new Promise((resolve, reject) => {
zip(files, { level: 6 }, (error, data) => (error ? reject(error) : resolve(data)));
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

这里的“流式”接口实际上仍会把整张明细表一次性留在内存里。

assembleUsageLogsXlsx 要求 detailRowsXml: string[],随后 detailSheetXml/worksheetXml 又把这些行整体展开并 join("")。这样会同时持有“每行 XML 数组 + 拼接后的整张 sheet XML + zip 输入字节”,大导出时峰值内存仍然随结果大小线性增长,buildDetailRowXml 也无法兑现 Line 168-Line 169 “无需保留整个结果集”的承诺。建议把明细行改成分块/迭代器输入,或直接按流式 zip entry 生成 sheet1.xml

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/usage-logs/export/xlsx.ts` around lines 243 - 267, The current
assembleUsageLogsXlsx + detailSheetXml/worksheetXml flow keeps the entire detail
sheet in memory because detailRowsXml: string[] is joined into one big string
before zipping; change the API and implementation to stream the detail sheet
instead of materializing it: replace detailRowsXml: string[] with an async
iterable/iterator or chunked reader (e.g., AsyncIterable<string> or generator)
produced by buildDetailRowXml, update assembleUsageLogsXlsx to create the
"xl/worksheets/sheet1.xml" entry as a streamed/iterable zip entry (emit header,
then iterate yielding row chunks, then footer) rather than calling
detailSheetXml(...).join(""), and adjust
detailSheetXml/worksheetXml/buildDetailRowXml to emit/return chunks rather than
a full string so peak memory holds only current chunk + zip buffers. Ensure the
zip writer you use supports streaming entries (or switch to a streaming zip API)
and update callers/tests to provide the new async iterable interface.

ding113 added 2 commits June 4, 2026 14:37
Make the decompression output cap configurable via MAX_DECOMPRESSED_REQUEST_BYTES
env var (default 100MB), and introduce a new MAX_COMPRESSED_REQUEST_BYTES
env var (default 10MB) that rejects oversized compressed requests before
decompression. This closes a DoS amplification vector in the /v1 and /v1beta
proxy paths, which are not governed by proxyClientMaxBodySize.

The compressed size check runs pre-auth, limiting CPU/memory usage from
unauthenticated clients and bounding the decompression amplification ratio.

Refs #1243
Add a tooltip around the Anthropic effort badge in the request details
dialog, explaining that the badge shows the verbatim output_config.effort
value sent by the client; the proxy does not rename or convert levels.

Also add the missing xhigh badge style in anthropic-effort-badge so
xhigh is visually distinct between high and max instead of falling
back to the gray default.
@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

The hardcoded 10MB default was rejecting large compressed requests
(e.g. 60MB prose, image-heavy payloads) that the plaintext path
accepts up to 100MB. Real compression never grows the body, so the
limit now tracks the decompressed ceiling, eliminating the asymmetry.

The .env.example and code comments document the rationale and the
ability to override via MAX_COMPRESSED_REQUEST_BYTES.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 58346ce633

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/actions/usage-logs.ts
csvLines.push(...buildCsvRows(batch.logs));
if (format === "xlsx" && xlsxSummary) {
for (const log of batch.logs) {
xlsxDetailRows.push(buildDetailRowXml(log, xlsxDetailRows.length + 2, timezone));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Split XLSX detail sheets before Excel's row limit

When an async XLSX export matches more than 1,048,575 usage-log records, this loop keeps assigning all detail rows to the single Usage Logs worksheet (rowNumber starts at 2), which produces row numbers beyond Excel's 1,048,576-row worksheet limit. Large unfiltered exports will therefore generate a workbook Excel cannot open cleanly instead of failing early or splitting across sheets; add a row cap/error for XLSX or roll over to additional worksheets before this limit.

Useful? React with 👍 / 👎.

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

@ding113 ding113 merged commit d575b9a into main Jun 4, 2026
17 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core area:deployment enhancement New feature or request size/L Large PR (< 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

创建用户无法选择已有供应商分组 功能请求: 支持codex zstd请求header: content-encoding: zstd

1 participant