Skip to content

feat(users): sync user limits to keys#1112

Closed
dofastted wants to merge 1 commit into
ding113:devfrom
dofastted:pr/dev-user-key-sync-20260426
Closed

feat(users): sync user limits to keys#1112
dofastted wants to merge 1 commit into
ding113:devfrom
dofastted:pr/dev-user-key-sync-20260426

Conversation

@dofastted
Copy link
Copy Markdown
Contributor

@dofastted dofastted commented Apr 26, 2026

Summary

Split from #1106 for focused review. Implements user-to-key configuration sync: propagating a user's quota, reset, provider-group, and concurrency settings to their existing keys, plus a settings page reorganization for billing-related controls.

Related to #1111 (sibling split for self-service quota windows).

Problem

When an admin changes a user's rate limits (daily quota, 5h/weekly/monthly/total spend caps, concurrent sessions, provider group, daily reset mode/time), those changes only apply to the user row. The user's existing API keys retain their old limits, creating a mismatch. Admins had to manually edit each key individually.

Additionally, when creating a new user, the default key did not inherit the user's configured limits -- it was created with minimal defaults regardless of the user's settings.

A secondary issue: cache invalidation failures during user operations were silently swallowed, making it hard to diagnose stale-cache bugs.

Solution

1. User-to-Key Config Sync

New lib module (src/lib/users/user-key-sync.ts): a cent-level distribution algorithm that splits user-level limits across N keys:

  • USD amount fields (5h, daily, weekly, monthly, total limits): converted to cents, divided evenly via Math.floor, remainder discarded (logged in summary). If total cents < key count, cents assigned to early keys, rest get null (no limit).
  • Concurrent sessions: integer floor-division across keys, remainder distributed one-per-key to earliest keys.
  • Provider group, daily reset mode/time: broadcast (same value for all keys).

Two new server actions:

  • syncUserConfigToKeys(userId, data): saves user edits, then updates all undeleted keys in a transaction. Available via "Sync to Keys" button in the edit user dialog.
  • batchSyncUserConfigToKeys({ userIds, updates }): batch variant for the dashboard batch-edit flow. Optionally applies user-field edits first, then syncs keys per user. Capped at 500 users.

2. Default Key Inherits User Settings on Creation

When a new user is created (addUser server action, and the CreateUserDialog), the default key now receives limits derived from buildFirstSyncedKeyConfig() instead of bare defaults. The create-user dialog also hides the provider-group and enable-status fields from the key section (they are now controlled at the user level).

3. Cache Invalidation Failure Logging

Two new helper functions (warnUserCacheInvalidationFailure, warnKeyCacheInvalidationFailure) log warnings instead of silently swallowing cache invalidation errors during user operations.

4. Settings Page: Billing Correction Section

Reorganizes the system settings form: groups the billing model source, Codex Priority billing source, and billing header rectifier toggle into a visually distinct "Billing and Rate Correction" card. Adds a direct-link button in the settings page header for quick navigation.

Changes

Core Logic

  • src/lib/users/user-key-sync.ts (+185) -- New: distribution algorithm, buildSyncedKeyConfigs, buildFirstSyncedKeyConfig
  • src/actions/users.ts (+520/-4) -- New: syncUserConfigToKeys, batchSyncUserConfigToKeys, buildUserDbUpdates, cache warn helpers; modified addUser to apply synced config to default key

UI

  • edit-user-dialog.tsx (+56/-2) -- "Sync to Keys" button in footer
  • batch-edit-dialog.tsx (+84/-34) -- Sync toggle in batch user section, updated submit logic
  • batch-user-section.tsx (+14) -- New syncKeysEnabled toggle field
  • create-user-dialog.tsx (+25/-11) -- Uses buildFirstSyncedKeyConfig for first key; hides provider-group/enable-status in key section
  • key-edit-section.tsx (+42/-34) -- New showProviderGroup, showEnableStatus props
  • system-settings-form.tsx (+82/-68) -- Billing correction grouped card
  • settings/config/page.tsx (+7) -- Direct link to billing correction section

i18n (5 languages)

  • messages/{en,ja,ru,zh-CN,zh-TW}/dashboard.json -- Sync-to-keys strings, batch dialog labels, toast messages
  • messages/{en,ja,ru,zh-CN,zh-TW}/settings/config.json -- Billing correction section title/description

Tests

  • tests/unit/lib/users/user-key-sync.test.ts (+66) -- New: distribution algorithm (averaging, small-amount handling, null/non-positive clearing)
  • tests/unit/actions/users-key-sync.test.ts (+315) -- New: action-level tests for single sync, batch sync, permission checks, default key inheritance
  • tests/unit/user-dialogs.test.tsx (+23/-3) -- Updated: "Sync to Keys" button rendering, create dialog prop assertions
  • tests/unit/actions/users-reset-all-statistics.test.ts (+1) -- Mock fix for new invalidateCachedKey import

Intentionally Excluded

Temporary key groups, self-service quota windows, billing multiplier correction, status aliases, and deployment scripts -- these remain in #1106.

Testing

Automated

  • Unit tests for distribution algorithm (cent-level averaging, remainder, edge cases)
  • Unit tests for syncUserConfigToKeys action (single user, permission check)
  • Unit tests for batchSyncUserConfigToKeys action (multi-user, user updates + key sync, permission check)
  • Unit tests for addUser default key inheritance
  • Dialog rendering tests for new buttons and props

Verification Commands

bunx vitest run tests/unit/actions/users-key-sync.test.ts tests/unit/lib/users/user-key-sync.test.ts tests/unit/user-dialogs.test.tsx --reporter=verbose --testTimeout=30000
bun run typecheck

Greptile Summary

This PR implements user-to-key configuration sync: a new distribution algorithm splits user-level limits (quotas, spend caps, concurrency, provider group, reset modes) across a user's existing keys, exposed via a "Sync to Keys" button in the edit dialog and a batch sync toggle. New key creation is also updated to inherit user limits via buildFirstSyncedKeyConfig.

  • Reset-mode default asymmetry (user-key-sync.ts lines 137–140): limit5hResetMode defaults to \"rolling\" while dailyResetMode defaults to \"fixed\" when the source value is null/absent. If both fields are intended to share the same fallback, one of the two conditional expressions is inverted and will silently apply the wrong mode to synced keys.

Confidence Score: 3/5

Functional but has a reset-mode default asymmetry that will silently apply the wrong daily reset mode to synced keys for users who have never explicitly set it.

One P1 finding (asymmetric reset-mode defaults) plus several P1s from prior threads (hardcoded i18n strings, N+1 in transaction, concurrent-session remainder not distributed, implicit unsaved-changes save on sync) pull the ceiling to 4 with multiple P1s warranting 3.

src/lib/users/user-key-sync.ts (reset mode defaults), src/actions/users.ts (hardcoded strings, N+1 loops)

Important Files Changed

Filename Overview
src/lib/users/user-key-sync.ts New distribution algorithm for syncing user limits to keys; asymmetric reset-mode defaults (line 138 vs 140) and an unimplemented concurrent-session remainder distribution are correctness concerns.
src/actions/users.ts Adds syncUserConfigToKeys and batchSyncUserConfigToKeys; logic is largely correct but both actions use per-key await loops (N+1) inside transactions, and batchSyncUserConfigToKeys has hardcoded Chinese error strings (see prior threads).
src/app/[locale]/dashboard/_components/user/batch-edit/batch-edit-dialog.tsx Adds syncKeysEnabled toggle and dispatches batchSyncUserConfigToKeys; user+key updates changed from parallel to sequential, which is a minor performance regression.
src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx Adds "Sync to Keys" button; the action implicitly persists all unsaved form fields (name, note, tags, etc.) in addition to limits — see prior thread for the labeling concern.
tests/unit/actions/users-key-sync.test.ts Good coverage of single sync, batch sync, permission checks, and default key inheritance. Missing a test for the concurrent-session remainder path (normalizedTotal >= keyCount).

Sequence Diagram

sequenceDiagram
    participant Admin
    participant EditDialog
    participant SyncAction as syncUserConfigToKeys
    participant DB
    participant Cache

    Admin->>EditDialog: Click "Sync to Keys"
    EditDialog->>SyncAction: syncUserConfigToKeys(userId, formData)
    SyncAction->>DB: BEGIN TRANSACTION
    SyncAction->>DB: UPDATE users SET ... WHERE id=userId
    DB-->>SyncAction: updatedUser (with new limits)
    SyncAction->>DB: SELECT keys WHERE userId=userId
    DB-->>SyncAction: keyRows[]
    loop For each key (N+1 queries)
        SyncAction->>DB: UPDATE keys SET config[i] WHERE id=keyRow.id
    end
    SyncAction->>DB: COMMIT
    SyncAction->>Cache: invalidateCachedUser + invalidateCachedKey (fire-and-forget)
    SyncAction-->>EditDialog: { ok: true, keyCount }
    EditDialog-->>Admin: toast.success("Synced N keys")
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/lib/users/user-key-sync.ts
Line: 137-141

Comment:
**Asymmetric defaults for reset modes**

`limit5hResetMode` defaults to `"rolling"` when the source value is absent/null (`=== "fixed" ? "fixed" : "rolling"`), while `dailyResetMode` defaults to `"fixed"` (`=== "rolling" ? "rolling" : "fixed"`). These two guards use opposite logic, so a user row where neither reset mode has been explicitly set will have its keys synced with `limit5hResetMode: "rolling"` and `dailyResetMode: "fixed"`. If the intended fallback for both fields is the same value (e.g., both "rolling" or both "fixed"), one of these expressions is inverted.

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

---

This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/user/batch-edit/batch-edit-dialog.tsx
Line: 327-393

Comment:
**Sequential execution replaces prior parallel execution**

The original code used `Promise.all(tasks)` to run user updates and key updates concurrently. The refactored code now awaits each operation in sequence (user/sync first, then keys). For the common case where sync is off and both user-field and key-field updates are enabled, the two requests are now serialized rather than running in parallel. This is a performance regression with no correctness benefit in that path.

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

Reviews (3): Last reviewed commit: "feat(users): sync user limits to keys" | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

📝 Walkthrough

Walkthrough

新增“同步用户配置到 Keys”后端逻辑与构建器、在仪表板的用户创建/编辑/批量编辑中加入对应 UI 控件与流程,重组设置页面的账单纠正分组,并添加多语言文案与单元测试覆盖。

Changes

Cohort / File(s) Summary
本地化 (dashboard & settings)
messages/en/dashboard.json, messages/ja/dashboard.json, messages/ru/dashboard.json, messages/zh-CN/dashboard.json, messages/zh-TW/dashboard.json, messages/en/settings/config.json, messages/ja/settings/config.json, messages/ru/settings/config.json, messages/zh-CN/settings/config.json, messages/zh-TW/settings/config.json
新增“Sync to Keys”相关翻译条目(按钮/加载/成功/失败、批量确认说明、吐司通知、字段说明)并新增 form.billingCorrection 文本块。
用户键同步库
src/lib/users/user-key-sync.ts
新增按键同步类型与构建器:buildSyncedKeyConfigsbuildFirstSyncedKeyConfig,实现配额/会话/提供商组分配、分配舍入与汇总结构。
服务器动作与用户逻辑
src/actions/users.ts
添加 syncUserConfigToKeysbatchSyncUserConfigToKeys(事务性更新用户并重写每个 key 的 limit/reset/providerGroup 字段)、新增 buildUserDbUpdates 等辅助函数,且在适用处调用 invalidateCachedKey
批量编辑 UI
src/app/[locale]/dashboard/_components/user/batch-edit/batch-edit-dialog.tsx, src/app/[locale]/dashboard/_components/user/batch-edit/batch-user-section.tsx
引入 syncKeysEnabled 状态与 FieldCard,确认流程支持仅同步或与用户/键更新组合执行,改为按需顺序调用后端 batchSyncUserConfigToKeys 并扩展吐司/确认文案与计数显示。
单用户编辑/创建与 Key 编辑区
src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx, src/app/[locale]/dashboard/_components/user/create-user-dialog.tsx, src/app/[locale]/dashboard/_components/user/forms/key-edit-section.tsx
在编辑对话框加入“同步到 Keys”操作与加载状态;创建对话框在保存后使用 buildFirstSyncedKeyConfig 填充默认 key;KeyEditSectionProps 新增可选 showProviderGroup?showEnableStatus? 控制并据此隐藏/显示相关 UI。
设置页面:账单纠正分组与导航动作
src/app/[locale]/settings/config/_components/system-settings-form.tsx, src/app/[locale]/settings/config/page.tsx
将账单模型源、Codex Priority 源与“启用账单头纠正”开关合并到新的 billing-correction 卡片,并在 Settings 页头添加跳转按钮。
单元测试
tests/unit/actions/users-key-sync.test.ts, tests/unit/lib/users/user-key-sync.test.ts, tests/unit/actions/users-reset-all-statistics.test.ts, tests/unit/user-dialogs.test.tsx
新增/更新测试:覆盖 key 同步分配规则、syncUserConfigToKeys/batchSyncUserConfigToKeys 的事务与缓存失效、addUser/创建/编辑对话框行为及相关 mock 调整。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR标题清晰地反映了主要变更内容:为用户限制同步到其API密钥实现了功能。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed 拉取请求的描述详细说明了改动内容,包括新增的用户配置同步功能、设置页面重组,以及相关的UI和i18n变更。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a "Sync to Keys" feature, allowing administrators to synchronize user-level configurations—including rate limits and provider groups—to all associated API keys during individual or batch edits. The implementation includes new server actions, a utility for distributing limits across multiple keys, and UI enhancements in the user management and system settings dashboards, such as a new "Billing and Rate Correction" section. Review feedback highlights several performance and maintainability improvements: addressing an N+1 update pattern in the batch synchronization loop, moving cache invalidations to fire-and-forget tasks to reduce latency, and replacing hardcoded strings with localized messages. Additionally, it is recommended to include the missing limit5hResetMode field in the synchronization logic to ensure full configuration parity.

Comment thread src/actions/users.ts
Comment on lines +1418 to +1437
for (const [index, keyRow] of userKeys.entries()) {
const config = configs[index];
await tx
.update(keysTable)
.set({
updatedAt: new Date(),
limit5hUsd: config.limit5hUsd === null ? null : config.limit5hUsd.toString(),
limitDailyUsd: config.limitDailyUsd === null ? null : config.limitDailyUsd.toString(),
dailyResetMode: config.dailyResetMode,
dailyResetTime: config.dailyResetTime,
limitWeeklyUsd:
config.limitWeeklyUsd === null ? null : config.limitWeeklyUsd.toString(),
limitMonthlyUsd:
config.limitMonthlyUsd === null ? null : config.limitMonthlyUsd.toString(),
limitTotalUsd: config.limitTotalUsd === null ? null : config.limitTotalUsd.toString(),
limitConcurrentSessions: config.limitConcurrentSessions,
providerGroup: config.providerGroup,
})
.where(and(eq(keysTable.id, keyRow.id), isNull(keysTable.deletedAt)));
}
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

Updating keys one by one in a loop inside a transaction results in an N+1 update pattern. With a batch size of up to 500 users and potentially multiple keys per user, this can lead to significant performance degradation and extended lock times on the keys table. Consider using a more efficient batch update strategy, such as a single SQL statement with a CASE expression or a temporary table join, if supported by the database and ORM.

Comment thread src/actions/users.ts Outdated
const missingIds = requestedIds.filter((id) => !existingSet.has(id));
if (missingIds.length > 0) {
throw new BatchUpdateError(
`部分用户不存在: ${missingIds.join(", ")}`,
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

The error message is hardcoded in Chinese, which is inconsistent with the internationalization strategy used elsewhere in the project. Please use the tError function to retrieve a localized message from the errors namespace.

Comment thread src/actions/users.ts Outdated
Comment on lines +1456 to +1467
await Promise.all([
...requestedIds.map((userId) =>
invalidateCachedUser(userId).catch((error) =>
warnUserCacheInvalidationFailure("batch user-key sync", userId, error)
)
),
...keyStringsForCache.map((key) =>
invalidateCachedKey(key).catch((error) =>
warnKeyCacheInvalidationFailure("batch user-key sync", key, error)
)
),
]);
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

Executing a large number of concurrent cache invalidations using Promise.all can overwhelm the Redis connection pool or the event loop. In performance-sensitive paths, non-critical I/O operations like Redis cache invalidation should be executed as fire-and-forget tasks to avoid blocking the main logic. Consider triggering these invalidations without awaiting their completion.

References
  1. In performance-sensitive code paths, non-critical I/O operations (e.g., releasing a session in Redis) should be executed as fire-and-forget tasks to avoid blocking the main logic.

Comment thread src/actions/users.ts Outdated
Comment on lines +1484 to +1485
logger.error("批量同步用户配置到 Key 失败:", error);
const message = error instanceof Error ? error.message : "批量同步用户配置到 Key 失败";
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

Log messages and fallback error messages are hardcoded in Chinese. For consistency and better maintainability, please use English for logs and the tError function for user-facing error messages.

Comment thread src/actions/users.ts
Comment on lines +2119 to +2129
const { code, params } = await import("@/lib/utils/error-messages").then((m) =>
m.zodErrorToCode(issue.code, {
minimum: "minimum" in issue ? issue.minimum : undefined,
maximum: "maximum" in issue ? issue.maximum : undefined,
type: "expected" in issue ? issue.expected : undefined,
received: "received" in issue ? issue.received : undefined,
validation: "validation" in issue ? issue.validation : undefined,
path: issue.path,
message: "message" in issue ? issue.message : undefined,
params: "params" in issue ? issue.params : undefined,
})
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

This dynamic import inside a server action can add unnecessary latency to every request. It is recommended to move this import to the top level of the file to ensure it is loaded once during initialization.

Comment on lines +11 to +33
export interface UserKeySyncSource {
dailyQuota?: number | string | null;
limit5hUsd?: number | string | null;
limitWeeklyUsd?: number | string | null;
limitMonthlyUsd?: number | string | null;
limitTotalUsd?: number | string | null;
limitConcurrentSessions?: number | string | null;
providerGroup?: string | null;
dailyResetMode?: "fixed" | "rolling" | null;
dailyResetTime?: string | null;
}

export interface SyncedKeyConfig {
limit5hUsd: number | null;
limitDailyUsd: number | null;
limitWeeklyUsd: number | null;
limitMonthlyUsd: number | null;
limitTotalUsd: number | null;
limitConcurrentSessions: number;
providerGroup: string;
dailyResetMode: "fixed" | "rolling";
dailyResetTime: string;
}
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

The limit5hResetMode field is missing from the synchronization interfaces. This field should be included to ensure that the 5-hour limit reset behavior (fixed vs. rolling) is correctly synced from users to their keys, maintaining consistency across the limit configuration.

Comment thread src/actions/users.ts
Comment on lines +1376 to +1379
throw new BatchUpdateError(
`部分用户不存在: ${missingIds.join(", ")}`,
ERROR_CODES.NOT_FOUND
);
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 Hardcoded Chinese user-facing error string

BatchUpdateError's message is returned verbatim as result.error in the catch block and surfaced via toast.error(t("toast.syncFailed", { error: errorMessage })). Non-Chinese users will see "部分用户不存在: 3, 7" in the toast. The identical pattern already exists in batchUpdateUsers (line 1166, pre-existing), but this PR adds a second occurrence. Use a translation key or the tError("USER_NOT_FOUND") pattern used elsewhere in the same file.

Suggested change
throw new BatchUpdateError(
`部分用户不存在: ${missingIds.join(", ")}`,
ERROR_CODES.NOT_FOUND
);
throw new BatchUpdateError(
tError("USER_NOT_FOUND"),
ERROR_CODES.NOT_FOUND
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/users.ts
Line: 1376-1379

Comment:
**Hardcoded Chinese user-facing error string**

`BatchUpdateError`'s message is returned verbatim as `result.error` in the catch block and surfaced via `toast.error(t("toast.syncFailed", { error: errorMessage }))`. Non-Chinese users will see `"部分用户不存在: 3, 7"` in the toast. The identical pattern already exists in `batchUpdateUsers` (line 1166, pre-existing), but this PR adds a second occurrence. Use a translation key or the `tError("USER_NOT_FOUND")` pattern used elsewhere in the same file.

```suggestion
        throw new BatchUpdateError(
          tError("USER_NOT_FOUND"),
          ERROR_CODES.NOT_FOUND
        );
```

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

Comment thread src/actions/users.ts Outdated
Comment on lines +1484 to +1485
logger.error("批量同步用户配置到 Key 失败:", error);
const message = error instanceof Error ? error.message : "批量同步用户配置到 Key 失败";
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 Hardcoded Chinese fallback error message

The fallback string "批量同步用户配置到 Key 失败" can be returned to the caller as result.error when the caught error has no message. Compare to syncUserConfigToKeys (same file) which correctly calls tError("UPDATE_USER_FAILED") for its generic fallback.

Suggested change
logger.error("批量同步用户配置到 Key 失败:", error);
const message = error instanceof Error ? error.message : "批量同步用户配置到 Key 失败";
logger.error("Failed to batch-sync user config to keys:", error);
const tError = await getTranslations("errors");
const message = error instanceof Error ? error.message : tError("UPDATE_USER_FAILED");
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/users.ts
Line: 1484-1485

Comment:
**Hardcoded Chinese fallback error message**

The fallback string `"批量同步用户配置到 Key 失败"` can be returned to the caller as `result.error` when the caught error has no message. Compare to `syncUserConfigToKeys` (same file) which correctly calls `tError("UPDATE_USER_FAILED")` for its generic fallback.

```suggestion
    logger.error("Failed to batch-sync user config to keys:", error);
    const tError = await getTranslations("errors");
    const message = error instanceof Error ? error.message : tError("UPDATE_USER_FAILED");
```

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

Comment on lines 307 to +342

const handleSyncKeys = async () => {
setIsSyncingKeys(true);
try {
const data = form.values || defaultValues;
const res = await syncUserConfigToKeys(user.id, {
name: data.name,
note: data.note,
tags: data.tags,
expiresAt: data.expiresAt ?? null,
providerGroup: normalizeProviderGroup(data.providerGroup),
rpm: data.rpm,
limit5hUsd: data.limit5hUsd,
dailyQuota: data.dailyQuota,
limitWeeklyUsd: data.limitWeeklyUsd,
limitMonthlyUsd: data.limitMonthlyUsd,
limitTotalUsd: data.limitTotalUsd,
limitConcurrentSessions: data.limitConcurrentSessions,
dailyResetMode: data.dailyResetMode,
dailyResetTime: data.dailyResetTime,
allowedClients: data.allowedClients,
blockedClients: data.blockedClients,
allowedModels: data.allowedModels,
});

if (!res.ok) {
toast.error(res.error || t("editDialog.syncKeys.error"));
return;
}

toast.success(t("editDialog.syncKeys.success", { count: res.data.keyCount }));
onSuccess?.();
queryClient.invalidateQueries({ queryKey: ["users"] });
queryClient.invalidateQueries({ queryKey: ["userKeyGroups"] });
queryClient.invalidateQueries({ queryKey: ["userTags"] });
router.refresh();
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 "Sync to Keys" implicitly saves all unsaved form changes

syncUserConfigToKeys receives the full current form state — including name, note, tags, expiresAt, allowedClients, etc. — and the server action writes all those fields to the user record via buildUserDbUpdates before syncing limits to keys. An admin who has unsaved edits and clicks "Sync to Keys" will unknowingly persist name/note/tag changes too. The button label "Sync to Keys" gives no indication of this side effect.

Consider either (a) only passing the sync-relevant fields (providerGroup, dailyQuota, limit*, dailyReset*, limitConcurrentSessions) to the action, or (b) updating the button tooltip/label to communicate that form changes are saved first.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx
Line: 307-342

Comment:
**"Sync to Keys" implicitly saves all unsaved form changes**

`syncUserConfigToKeys` receives the full current form state — including `name`, `note`, `tags`, `expiresAt`, `allowedClients`, etc. — and the server action writes all those fields to the user record via `buildUserDbUpdates` before syncing limits to keys. An admin who has unsaved edits and clicks "Sync to Keys" will unknowingly persist name/note/tag changes too. The button label "Sync to Keys" gives no indication of this side effect.

Consider either (a) only passing the sync-relevant fields (`providerGroup`, `dailyQuota`, `limit*`, `dailyReset*`, `limitConcurrentSessions`) to the action, or (b) updating the button tooltip/label to communicate that form changes are saved first.

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

Comment thread src/actions/users.ts
Comment on lines +1418 to +1443
for (const [index, keyRow] of userKeys.entries()) {
const config = configs[index];
await tx
.update(keysTable)
.set({
updatedAt: new Date(),
limit5hUsd: config.limit5hUsd === null ? null : config.limit5hUsd.toString(),
limitDailyUsd: config.limitDailyUsd === null ? null : config.limitDailyUsd.toString(),
dailyResetMode: config.dailyResetMode,
dailyResetTime: config.dailyResetTime,
limitWeeklyUsd:
config.limitWeeklyUsd === null ? null : config.limitWeeklyUsd.toString(),
limitMonthlyUsd:
config.limitMonthlyUsd === null ? null : config.limitMonthlyUsd.toString(),
limitTotalUsd: config.limitTotalUsd === null ? null : config.limitTotalUsd.toString(),
limitConcurrentSessions: config.limitConcurrentSessions,
providerGroup: config.providerGroup,
})
.where(and(eq(keysTable.id, keyRow.id), isNull(keysTable.deletedAt)));
}

userResults.push({
updatedUserId: userRow.id,
updatedKeyIds: userKeys.map((keyRow) => keyRow.id),
keyCount: userKeys.length,
summary,
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 N+1 key update queries inside the transaction

Each key is updated with a separate await tx.update(keysTable).set(config).where(eq(keysTable.id, keyRow.id)) call. For a batch of 500 users with even a modest average of 3 keys each, this results in ~1,500 round-trips to the DB within one transaction, which will be slow and may hit connection or statement timeouts. At minimum, consider adding a comment about this known limitation and a practical upper bound on users-per-batch that keeps latency acceptable.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/users.ts
Line: 1418-1443

Comment:
**N+1 key update queries inside the transaction**

Each key is updated with a separate `await tx.update(keysTable).set(config).where(eq(keysTable.id, keyRow.id))` call. For a batch of 500 users with even a modest average of 3 keys each, this results in ~1,500 round-trips to the DB within one transaction, which will be slow and may hit connection or statement timeouts. At minimum, consider adding a comment about this known limitation and a practical upper bound on users-per-batch that keeps latency acceptable.

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

Copy link
Copy Markdown

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

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: 588a923aea

ℹ️ 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/users.ts
Comment on lines 1650 to 1655
const newKey = await createKey({
user_id: newUser.id,
name: "default",
key: generatedKey,
is_enabled: true,
expires_at: undefined,
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 Make addUser user/key creation atomic

addUser now creates the user first and the default key second, but this path is not wrapped in a transaction and does not roll back the user if key creation fails. In any partial-failure case (e.g. DB error during createKey), the action returns failure while leaving a persisted user without the expected default key, which can produce orphaned users and duplicate users on retry.

Useful? React with 👍 / 👎.

Comment thread src/actions/users.ts
Comment on lines +1656 to +1660
limit_5h_usd: defaultKeyConfig.limit5hUsd,
limit_daily_usd: defaultKeyConfig.limitDailyUsd,
daily_reset_mode: defaultKeyConfig.dailyResetMode,
daily_reset_time: defaultKeyConfig.dailyResetTime,
limit_weekly_usd: defaultKeyConfig.limitWeeklyUsd,
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 Pass 5h reset mode when creating synced default key

This new default-key creation payload copies quota fields from the user but omits limit_5h_reset_mode. Because createKey defaults that field to "rolling", creating a user with limit5hResetMode: "fixed" will still produce a default key with rolling 5h reset behavior, so the key no longer matches the user's configured limit reset policy.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (8)
tests/unit/user-dialogs.test.tsx (1)

350-350: 建议通过 messages 常量引用按钮文本,避免硬编码。

文件中其他断言(如第 366、367 行)都通过 messages.dashboard.userManagement.editDialog.reset5h.button 这种方式引用 i18n 文案,而此处直接硬编码了 "Sync to Keys"。如果未来英文文案调整(例如改成 "Sync to keys"),测试会失败但代码本身没问题。建议保持一致性。

♻️ 推荐写法
-    expect(buttonTexts).toContain("Sync to Keys");
+    expect(buttonTexts).toContain(
+      messages.dashboard.userManagement.editDialog.syncKeys.button
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/user-dialogs.test.tsx` at line 350, Replace the hardcoded button
label check expect(buttonTexts).toContain("Sync to Keys"); with the i18n
messages constant used elsewhere—use
messages.dashboard.userManagement.editDialog.reset5h.button (the same symbol
used in other assertions) so the test references the localized string instead of
a literal.
src/app/[locale]/dashboard/_components/user/forms/key-edit-section.tsx (1)

469-542: 建议合并 showProviderGroup 的条件分支以减少重复判断。

当前 4 个分支都以 showProviderGroup && 开头,可以将整个三元链统一包裹一层 showProviderGroup && (...),提升可读性。

♻️ 推荐重构
-        {showProviderGroup && isAdmin ? (
-          <ProviderGroupSelect
-            value={keyData.providerGroup || PROVIDER_GROUP.DEFAULT}
-            onChange={(val) => onChange("providerGroup", val)}
-            disabled={false}
-            translations={{
-              label: translations.fields.providerGroup.label,
-              placeholder: translations.fields.providerGroup.placeholder,
-            }}
-          />
-        ) : showProviderGroup && userGroups.length > 0 ? (
-          <div className="space-y-2">
-            {/* ... */}
-          </div>
-        ) : showProviderGroup && keyGroupOptions.length > 0 ? (
-          <div className="space-y-2">
-            {/* ... */}
-          </div>
-        ) : showProviderGroup ? (
-          <div className="text-sm text-muted-foreground">
-            {translations.fields.providerGroup.noGroupHint || "您没有分组限制,可以访问所有供应商"}
-          </div>
-        ) : null}
+        {showProviderGroup &&
+          (isAdmin ? (
+            <ProviderGroupSelect ... />
+          ) : userGroups.length > 0 ? (
+            <div className="space-y-2">{/* ... */}</div>
+          ) : keyGroupOptions.length > 0 ? (
+            <div className="space-y-2">{/* ... */}</div>
+          ) : (
+            <div className="text-sm text-muted-foreground">
+              {translations.fields.providerGroup.noGroupHint || "您没有分组限制,可以访问所有供应商"}
+            </div>
+          ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/dashboard/_components/user/forms/key-edit-section.tsx
around lines 469 - 542, The JSX repeats "showProviderGroup &&" across several
ternary branches; refactor by extracting a single outer guard: wrap the entire
multi-branch expression with showProviderGroup && ( ... ) and inside keep the
existing nested ternaries/JSX for the isAdmin branch (ProviderGroupSelect), the
userGroups branch (read-only Badge list when keyData.id > 0 and TagInputField
when creating), the keyGroupOptions branch, and the fallback hint; ensure you
reference and preserve logic around isAdmin, ProviderGroupSelect, TagInputField,
keyGroupOptions, userGroups and keyData.id so behavior and props
(onChange/handleUserProviderGroupChange/translations) remain unchanged.
src/actions/users.ts (4)

2253-2261: 兜底默认值 result ?? {...}buildSyncedKeyConfigs({}, 0).summary 在每次回退时重新构造,可提取为常量

事务正常完成时 result 必然被赋值,该兜底分支只在防御性场景中触发(理论上不可达)。但更优写法是把空 summary 抽成 const EMPTY_SYNC_SUMMARY = buildSyncedKeyConfigs({}, 0).summary(模块级常量)避免每次返回时重新计算,也让"空场景下的 summary 形状"显式化。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/users.ts` around lines 2253 - 2261, The fallback object
reconstructs buildSyncedKeyConfigs({}, 0).summary on every call; extract that
empty summary into a module-level constant (e.g. const EMPTY_SYNC_SUMMARY =
buildSyncedKeyConfigs({}, 0).summary) and replace buildSyncedKeyConfigs({},
0).summary in the fallback of the return with EMPTY_SYNC_SUMMARY; update usages
in this module to import/use that constant so the "empty" summary shape is
computed once and explicit (reference: buildSyncedKeyConfigs and the fallback
result ?? { ... summary: ... }).

1373-1380: 用户存在性校验放在 UPDATE 之后,行为符合预期但有一个细微点需确认

Line 1349-1354 先对 requestedIds 范围内"未删除"用户执行 UPDATE;Line 1356-1371 再 SELECT 现存用户;Line 1373-1380 抛 BatchUpdateError(NOT_FOUND)。由于在事务内,抛异常会回滚 UPDATE,语义正确。

但与 batchUpdateUsers 的对称实现(Line 1156-1201:先 SELECT 存在性,再 UPDATE,再断言行数)不同,本实现 UPDATE 时不报错,而是事后查存在性。两者最终都正确,但建议保持一致风格(先校验存在性、再 UPDATE),以便未来在 UPDATE 加 RETURNING 计数差异时不会出现两套断言路径。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/users.ts` around lines 1373 - 1380, The current routine updates
rows first then SELECTs to detect missing IDs (using requestedIds, existingSet,
missingIds, and throwing BatchUpdateError with ERROR_CODES.NOT_FOUND); change it
to match batchUpdateUsers' style: first SELECT the non-deleted users for
requestedIds and throw BatchUpdateError if any are missing, then perform the
UPDATE (and optionally use RETURNING or check affected row count) so existence
checks occur before mutation and both paths share the same assertion pattern as
batchUpdateUsers.

2092-2230: syncUserConfigToKeysbatchSyncUserConfigToKeys 的 Key 更新内循环高度重复

Line 2211-2230 的 Key UPDATE 块与 Line 1418-1437 的批量版本字段映射完全相同。可抽取一个 applySyncedKeyConfig(tx, keyId, config) helper 复用,既减少重复,又能在以后字段增减时只改一处:

♻️ 提炼共享 helper
+async function applySyncedKeyConfig(
+  tx: Parameters<Parameters<typeof db.transaction>[0]>[0],
+  keyId: number,
+  config: SyncedKeyConfig
+): Promise<void> {
+  await tx
+    .update(keysTable)
+    .set({
+      updatedAt: new Date(),
+      limit5hUsd: config.limit5hUsd === null ? null : config.limit5hUsd.toString(),
+      limitDailyUsd: config.limitDailyUsd === null ? null : config.limitDailyUsd.toString(),
+      dailyResetMode: config.dailyResetMode,
+      dailyResetTime: config.dailyResetTime,
+      limitWeeklyUsd:
+        config.limitWeeklyUsd === null ? null : config.limitWeeklyUsd.toString(),
+      limitMonthlyUsd:
+        config.limitMonthlyUsd === null ? null : config.limitMonthlyUsd.toString(),
+      limitTotalUsd: config.limitTotalUsd === null ? null : config.limitTotalUsd.toString(),
+      limitConcurrentSessions: config.limitConcurrentSessions,
+      providerGroup: config.providerGroup,
+    })
+    .where(and(eq(keysTable.id, keyId), isNull(keysTable.deletedAt)));
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/users.ts` around lines 2092 - 2230, The key-update block in
syncUserConfigToKeys duplicates the same field mapping used in
batchSyncUserConfigToKeys; create a helper function (e.g., applySyncedKeyConfig)
that accepts the transaction (tx), keyId (or keyRow.id) and a config object and
performs the update against keysTable with the exact fields: updatedAt,
limit5hUsd, limitDailyUsd, dailyResetMode, dailyResetTime, limitWeeklyUsd,
limitMonthlyUsd, limitTotalUsd, limitConcurrentSessions, providerGroup
(converting numeric/null limits to strings as currently done); replace the
inline tx.update(...) loop in syncUserConfigToKeys (and the loop in
batchSyncUserConfigToKeys) to call this helper to remove duplication and keep
mapping logic in one place.

284-303: EditableUserData 类型与 buildUserDbUpdates 处理范围一致,但与 UpdateUserSchema 字段集存在差异

类型上不包含 limit5hResetMode,而 syncUserConfigToKeys 在 Line 2116 用 UpdateUserSchema.safeParse(data) 校验——若调用方通过 as any/类型断言传入 limit5hResetMode,Zod 会保留它(默认 strip 未知键,但已知键会保留),而 buildUserDbUpdates(Line 319-368)并未处理该字段,会被静默丢弃。

考虑到:

  1. TypeScript 类型已限制了显式调用方的入参;
  2. limit5hResetMode 改变时 batchUpdateUsers 需要清理 Redis 5h 缓存(Line 1203-1224),而本路径没有该清理逻辑;

建议在 buildUserDbUpdates 顶部加注释明确"本 helper 仅处理同步到 Key 的字段子集,limit5hResetMode 等仅用户级字段不在此处理",或通过 pick() 派生一个更窄的 schema 用于 syncUserConfigToKeys,避免后续维护者混淆字段范围。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/users.ts` around lines 284 - 303, The helper buildUserDbUpdates
only handles a subset of user fields synced to keys but UpdateUserSchema (used
in syncUserConfigToKeys) can accept extra fields like limit5hResetMode which
buildUserDbUpdates currently ignores and thus silently drops (and also misses
the Redis 5h cache cleanup logic in batchUpdateUsers). Either: add a clear
comment at the top of buildUserDbUpdates stating it only processes the subset of
fields that are synced to keys and that user-only fields (e.g.,
limit5hResetMode) are intentionally not handled here; or better, derive a
narrower Zod/TypeScript schema via pick() for syncUserConfigToKeys that only
permits the keys buildUserDbUpdates expects (so unknown fields are rejected),
and if you intend to support limit5hResetMode here, add explicit handling in
buildUserDbUpdates plus the corresponding Redis cache cleanup logic used by
batchUpdateUsers. Ensure references: EditableUserData, buildUserDbUpdates,
syncUserConfigToKeys, UpdateUserSchema, batchUpdateUsers are updated
accordingly.
src/lib/users/user-key-sync.ts (2)

183-185: buildFirstSyncedKeyConfig 在 keyCount=1 时一定返回非空,但类型上未被静态保证

buildSyncedKeyConfigs(source, 1).configs[0] 在硬编码 keyCount=1 时一定有元素;但若未来 keyCount 被参数化或 Math.max(0, Math.floor(keyCount)) 边界条件被重构,configs[0] 可能为 undefined,而函数签名仍声明返回 SyncedKeyConfig(非可选),会让调用方在 strict 模式下静默拿到 undefined。可加一行防御:

♻️ 防御性写法
 export function buildFirstSyncedKeyConfig(source: UserKeySyncSource): SyncedKeyConfig {
-  return buildSyncedKeyConfigs(source, 1).configs[0];
+  const { configs } = buildSyncedKeyConfigs(source, 1);
+  if (!configs[0]) {
+    throw new Error("buildFirstSyncedKeyConfig: failed to build first config");
+  }
+  return configs[0];
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/users/user-key-sync.ts` around lines 183 - 185,
buildFirstSyncedKeyConfig currently returns buildSyncedKeyConfigs(source,
1).configs[0] which assumes configs[0] exists; to make this safe change
buildFirstSyncedKeyConfig to defensively handle missing element by checking the
result of buildSyncedKeyConfigs(source, 1).configs and throwing a clear error or
asserting when configs[0] is undefined, ensuring the function always either
returns a valid SyncedKeyConfig or fails fast (reference:
buildFirstSyncedKeyConfig, buildSyncedKeyConfigs, SyncedKeyConfig, configs).

70-100: 确认:小额配额下的"前 N 个 Key 各得 1 分"分配是有意为之

totalCents < keyCount 时,实现把前 totalCents 个 Key 各分 1 分,其余 Key 设为 null(无限制)。例如用户 5h USD = 0.02、3 个 Key,会得到 [0.01, 0.01, null],这意味着排在后面的 Key 在该字段上无配额上限。这与 distributeConcurrentSessions 的行为一致(余下的 Key 得 0,即明确为零)。两个函数对"无法平均分摊"语义不同(null vs 0),建议在文件顶部加一行 JSDoc 说明:limit*Usd 字段下"分配不到的 Key 视为不限制",limitConcurrentSessions 字段下"分配不到的 Key 视为零"——避免后续维护者误以为是 bug。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/users/user-key-sync.ts` around lines 70 - 100, The file lacks a
top-level JSDoc clarifying divergent semantics between monetary and session
distribution: update the header of src/lib/users/user-key-sync.ts to document
that distributeAmount (used for limit*Usd fields) intentionally gives the first
N keys 0.01 when totalCents < keyCount and sets the remaining keys to null to
mean "no limit", whereas distributeConcurrentSessions (and
limitConcurrentSessions) treats remaining keys as 0 (explicit zero capacity);
mention the exact fields (limit*Usd and limitConcurrentSessions), the functions
distributeAmount and distributeConcurrentSessions, and that this is intentional
to avoid future confusion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@messages/ja/dashboard.json`:
- Around line 1684-1685: The Japanese locale file contains English text for the
keys "syncKeys" and "syncKeysDescription" (and additional English strings around
the same area at the other noted entries), so replace those English values with
proper Japanese translations to keep UI language consistent; locate the keys
"syncKeys", "syncKeysDescription" and the other affected keys in
messages/ja/dashboard.json (the entries reported at the nearby blocks) and
update their string values to Japanese equivalents that convey the meaning
("同期するキー" etc.), ensuring placeholders like {users} remain intact and
punctuation/escaping matches the JSON style used elsewhere.

In `@messages/ru/settings/config.json`:
- Around line 16-19: Translate the untranslated English strings under
billingCorrection in messages/ru/settings/config.json: replace
billingCorrection.title and billingCorrection.description with proper Russian
translations (matching style used in ja/zh-CN/zh-TW entries) so the settings
page header/button shown by src/app/[locale]/settings/config/page.tsx is
localized; update the JSON values for the keys "billingCorrection.title" and
"billingCorrection.description" accordingly and ensure valid JSON string
quoting/escaping.

In `@src/actions/users.ts`:
- Around line 1382-1437: The current batchSyncUserConfigToKeys logic issues a
separate tx.update for each key (loop around keysTable updates), causing many
serial round-trips and long transactions; replace the per-key updates inside
batchSyncUserConfigToKeys by building a single batched UPDATE using Drizzle's
sql template with CASE WHEN keyed by keysTable.id (similar to the pattern in
src/repository/provider.ts lines ~954-964 and
src/repository/message-write-buffer.ts lines ~116-160): collect all key ids and
for each updatable column (limit5hUsd, limitDailyUsd, limitWeeklyUsd,
limitMonthlyUsd, limitTotalUsd, limitConcurrentSessions, providerGroup,
dailyResetMode, dailyResetTime, updatedAt) build CASE expressions mapping id ->
value (use NULL where appropriate), then execute one tx.sql`UPDATE ${keysTable}
SET ... WHERE ${inArray(keysTable.id, ids)} AND ${isNull(keysTable.deletedAt)}`
inside the transaction to replace the looped tx.update calls.

In
`@src/app/`[locale]/dashboard/_components/user/batch-edit/batch-edit-dialog.tsx:
- Around line 441-443: The confirmation copy underestimates affected keys when
syncUsersToKeys is true but no individual keys are selected: change the
keysCount computation and the copy-generation logic so that if
willSyncUsersToKeys && !willUpdateKeys you do not treat keysCount as 0; instead
set a sentinel (e.g., undefined or a boolean like syncingAllKeys) and update the
UI text path to either omit a numeric key count or explicitly say "同步这些用户下的全部未删除
key"/"all non-deleted keys of these users"; update the variables/expressions
around willSyncUsersToKeys, willUpdateKeys, keysCount, usersCount and the
confirmation-copy rendering in the batch-edit-dialog component (the block that
reads pendingUpdate.keyIds / pendingUpdate.userIds and the message construction
used for the dialog) and make the identical change in the other confirmation
block noted (the similar logic around the later block referenced).

In `@src/app/`[locale]/dashboard/_components/user/create-user-dialog.tsx:
- Around line 175-185: The code builds the default key config from the form
draft (data.user) which can differ from the persisted user returned by the
server; change the construction of firstKeyConfig (the call to
buildFirstSyncedKeyConfig) and any subsequent use of limit5hResetMode to derive
values from the server response object userRes.data.user instead of data.user so
the first API key reflects server-side defaults (also update the analogous logic
referenced around lines 194-204 to use userRes.data.user).

In `@src/app/`[locale]/dashboard/_components/user/edit-user-dialog.tsx:
- Around line 312-329: The syncUserConfigToKeys call is missing the
limit5hResetMode field, so updates to that setting aren't persisted or
propagated when using the "Sync to Keys" path; update the argument object passed
to syncUserConfigToKeys (the call in edit-user-dialog.tsx that includes name,
note, tags, expiresAt, providerGroup, rpm, limit5hUsd, dailyQuota,
limitWeeklyUsd, limitMonthlyUsd, limitTotalUsd, limitConcurrentSessions,
dailyResetMode, dailyResetTime, allowedClients, blockedClients, allowedModels)
to also include limit5hResetMode: data.limit5hResetMode (ensuring the same key
name as used by the regular save path).

---

Nitpick comments:
In `@src/actions/users.ts`:
- Around line 2253-2261: The fallback object reconstructs
buildSyncedKeyConfigs({}, 0).summary on every call; extract that empty summary
into a module-level constant (e.g. const EMPTY_SYNC_SUMMARY =
buildSyncedKeyConfigs({}, 0).summary) and replace buildSyncedKeyConfigs({},
0).summary in the fallback of the return with EMPTY_SYNC_SUMMARY; update usages
in this module to import/use that constant so the "empty" summary shape is
computed once and explicit (reference: buildSyncedKeyConfigs and the fallback
result ?? { ... summary: ... }).
- Around line 1373-1380: The current routine updates rows first then SELECTs to
detect missing IDs (using requestedIds, existingSet, missingIds, and throwing
BatchUpdateError with ERROR_CODES.NOT_FOUND); change it to match
batchUpdateUsers' style: first SELECT the non-deleted users for requestedIds and
throw BatchUpdateError if any are missing, then perform the UPDATE (and
optionally use RETURNING or check affected row count) so existence checks occur
before mutation and both paths share the same assertion pattern as
batchUpdateUsers.
- Around line 2092-2230: The key-update block in syncUserConfigToKeys duplicates
the same field mapping used in batchSyncUserConfigToKeys; create a helper
function (e.g., applySyncedKeyConfig) that accepts the transaction (tx), keyId
(or keyRow.id) and a config object and performs the update against keysTable
with the exact fields: updatedAt, limit5hUsd, limitDailyUsd, dailyResetMode,
dailyResetTime, limitWeeklyUsd, limitMonthlyUsd, limitTotalUsd,
limitConcurrentSessions, providerGroup (converting numeric/null limits to
strings as currently done); replace the inline tx.update(...) loop in
syncUserConfigToKeys (and the loop in batchSyncUserConfigToKeys) to call this
helper to remove duplication and keep mapping logic in one place.
- Around line 284-303: The helper buildUserDbUpdates only handles a subset of
user fields synced to keys but UpdateUserSchema (used in syncUserConfigToKeys)
can accept extra fields like limit5hResetMode which buildUserDbUpdates currently
ignores and thus silently drops (and also misses the Redis 5h cache cleanup
logic in batchUpdateUsers). Either: add a clear comment at the top of
buildUserDbUpdates stating it only processes the subset of fields that are
synced to keys and that user-only fields (e.g., limit5hResetMode) are
intentionally not handled here; or better, derive a narrower Zod/TypeScript
schema via pick() for syncUserConfigToKeys that only permits the keys
buildUserDbUpdates expects (so unknown fields are rejected), and if you intend
to support limit5hResetMode here, add explicit handling in buildUserDbUpdates
plus the corresponding Redis cache cleanup logic used by batchUpdateUsers.
Ensure references: EditableUserData, buildUserDbUpdates, syncUserConfigToKeys,
UpdateUserSchema, batchUpdateUsers are updated accordingly.

In `@src/app/`[locale]/dashboard/_components/user/forms/key-edit-section.tsx:
- Around line 469-542: The JSX repeats "showProviderGroup &&" across several
ternary branches; refactor by extracting a single outer guard: wrap the entire
multi-branch expression with showProviderGroup && ( ... ) and inside keep the
existing nested ternaries/JSX for the isAdmin branch (ProviderGroupSelect), the
userGroups branch (read-only Badge list when keyData.id > 0 and TagInputField
when creating), the keyGroupOptions branch, and the fallback hint; ensure you
reference and preserve logic around isAdmin, ProviderGroupSelect, TagInputField,
keyGroupOptions, userGroups and keyData.id so behavior and props
(onChange/handleUserProviderGroupChange/translations) remain unchanged.

In `@src/lib/users/user-key-sync.ts`:
- Around line 183-185: buildFirstSyncedKeyConfig currently returns
buildSyncedKeyConfigs(source, 1).configs[0] which assumes configs[0] exists; to
make this safe change buildFirstSyncedKeyConfig to defensively handle missing
element by checking the result of buildSyncedKeyConfigs(source, 1).configs and
throwing a clear error or asserting when configs[0] is undefined, ensuring the
function always either returns a valid SyncedKeyConfig or fails fast (reference:
buildFirstSyncedKeyConfig, buildSyncedKeyConfigs, SyncedKeyConfig, configs).
- Around line 70-100: The file lacks a top-level JSDoc clarifying divergent
semantics between monetary and session distribution: update the header of
src/lib/users/user-key-sync.ts to document that distributeAmount (used for
limit*Usd fields) intentionally gives the first N keys 0.01 when totalCents <
keyCount and sets the remaining keys to null to mean "no limit", whereas
distributeConcurrentSessions (and limitConcurrentSessions) treats remaining keys
as 0 (explicit zero capacity); mention the exact fields (limit*Usd and
limitConcurrentSessions), the functions distributeAmount and
distributeConcurrentSessions, and that this is intentional to avoid future
confusion.

In `@tests/unit/user-dialogs.test.tsx`:
- Line 350: Replace the hardcoded button label check
expect(buttonTexts).toContain("Sync to Keys"); with the i18n messages constant
used elsewhere—use messages.dashboard.userManagement.editDialog.reset5h.button
(the same symbol used in other assertions) so the test references the localized
string instead of a literal.
🪄 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: 769b0fc0-0a6d-46bf-b445-ee48210254b0

📥 Commits

Reviewing files that changed from the base of the PR and between 757a8fb and 588a923.

📒 Files selected for processing (23)
  • messages/en/dashboard.json
  • messages/en/settings/config.json
  • messages/ja/dashboard.json
  • messages/ja/settings/config.json
  • messages/ru/dashboard.json
  • messages/ru/settings/config.json
  • messages/zh-CN/dashboard.json
  • messages/zh-CN/settings/config.json
  • messages/zh-TW/dashboard.json
  • messages/zh-TW/settings/config.json
  • src/actions/users.ts
  • src/app/[locale]/dashboard/_components/user/batch-edit/batch-edit-dialog.tsx
  • src/app/[locale]/dashboard/_components/user/batch-edit/batch-user-section.tsx
  • src/app/[locale]/dashboard/_components/user/create-user-dialog.tsx
  • src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx
  • src/app/[locale]/dashboard/_components/user/forms/key-edit-section.tsx
  • src/app/[locale]/settings/config/_components/system-settings-form.tsx
  • src/app/[locale]/settings/config/page.tsx
  • src/lib/users/user-key-sync.ts
  • tests/unit/actions/users-key-sync.test.ts
  • tests/unit/actions/users-reset-all-statistics.test.ts
  • tests/unit/lib/users/user-key-sync.test.ts
  • tests/unit/user-dialogs.test.tsx

Comment thread messages/ja/dashboard.json Outdated
Comment on lines +1684 to +1685
"syncKeys": "Sync to Keys",
"syncKeysDescription": "This will overwrite all undeleted keys for these {users} users from their current user settings.",
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

日文语言包中混入英文文案,建议统一为日文。

这些键会直接展示给 ja 用户,当前英文整句会导致界面语言不一致。

建议修复
-        "syncKeys": "Sync to Keys",
-        "syncKeysDescription": "This will overwrite all undeleted keys for these {users} users from their current user settings.",
+        "syncKeys": "キーに同期",
+        "syncKeysDescription": "選択した {users} ユーザーの未削除キーを、現在のユーザー設定で上書きします。",
@@
-        "keysSynced": "Synced {keys} keys for {users} users",
+        "keysSynced": "{users} ユーザーの {keys} 個のキーを同期しました",
@@
-        "syncFailed": "Sync to keys failed: {error}",
+        "syncFailed": "キーへの同期に失敗しました: {error}",
@@
-          "syncKeys": "Sync to Keys"
+          "syncKeys": "キーに同期"
@@
-          "syncKeysDescription": "When enabled, user fields from this batch edit are saved first, then all undeleted keys for each user are overwritten from that user's settings."
+          "syncKeysDescription": "有効にすると、この一括編集のユーザー項目を先に保存し、その後各ユーザーの未削除キーをユーザー設定で上書きします。"

Also applies to: 1694-1697, 1720-1720, 1726-1726

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@messages/ja/dashboard.json` around lines 1684 - 1685, The Japanese locale
file contains English text for the keys "syncKeys" and "syncKeysDescription"
(and additional English strings around the same area at the other noted
entries), so replace those English values with proper Japanese translations to
keep UI language consistent; locate the keys "syncKeys", "syncKeysDescription"
and the other affected keys in messages/ja/dashboard.json (the entries reported
at the nearby blocks) and update their string values to Japanese equivalents
that convey the meaning ("同期するキー" etc.), ensuring placeholders like {users}
remain intact and punctuation/escaping matches the JSON style used elsewhere.

Comment thread messages/ru/settings/config.json
Comment thread src/actions/users.ts
Comment on lines +1382 to +1437
const keyRows = await tx
.select({
id: keysTable.id,
key: keysTable.key,
userId: keysTable.userId,
})
.from(keysTable)
.where(and(inArray(keysTable.userId, requestedIds), isNull(keysTable.deletedAt)))
.orderBy(asc(keysTable.userId), asc(keysTable.createdAt), asc(keysTable.id));

const keysByUserId = new Map<number, typeof keyRows>();
for (const keyRow of keyRows) {
const rows = keysByUserId.get(keyRow.userId) ?? [];
rows.push(keyRow);
keysByUserId.set(keyRow.userId, rows);
}

const userResults: SyncUserConfigToKeysResult[] = [];

for (const userRow of userRows) {
const userKeys = keysByUserId.get(userRow.id) ?? [];
const { configs, summary } = buildSyncedKeyConfigs(
{
dailyQuota: userRow.dailyQuota,
limit5hUsd: userRow.limit5hUsd,
limitWeeklyUsd: userRow.limitWeeklyUsd,
limitMonthlyUsd: userRow.limitMonthlyUsd,
limitTotalUsd: userRow.limitTotalUsd,
limitConcurrentSessions: userRow.limitConcurrentSessions,
providerGroup: userRow.providerGroup,
dailyResetMode: userRow.dailyResetMode,
dailyResetTime: userRow.dailyResetTime,
},
userKeys.length
);

for (const [index, keyRow] of userKeys.entries()) {
const config = configs[index];
await tx
.update(keysTable)
.set({
updatedAt: new Date(),
limit5hUsd: config.limit5hUsd === null ? null : config.limit5hUsd.toString(),
limitDailyUsd: config.limitDailyUsd === null ? null : config.limitDailyUsd.toString(),
dailyResetMode: config.dailyResetMode,
dailyResetTime: config.dailyResetTime,
limitWeeklyUsd:
config.limitWeeklyUsd === null ? null : config.limitWeeklyUsd.toString(),
limitMonthlyUsd:
config.limitMonthlyUsd === null ? null : config.limitMonthlyUsd.toString(),
limitTotalUsd: config.limitTotalUsd === null ? null : config.limitTotalUsd.toString(),
limitConcurrentSessions: config.limitConcurrentSessions,
providerGroup: config.providerGroup,
})
.where(and(eq(keysTable.id, keyRow.id), isNull(keysTable.deletedAt)));
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 检查仓库是否已有 CASE WHEN 批量更新的工具/先例,以决定参考写法
rg -nP --type=ts -C2 "CASE\s+WHEN" -g 'src/**'
rg -nP --type=ts -C2 "sql\`" -g 'src/repository/**'

Repository: ding113/claude-code-hub

Length of output: 50379


🏁 Script executed:

cd src/actions && wc -l users.ts && sed -n '1382,1437p' users.ts

Repository: ding113/claude-code-hub

Length of output: 2513


🏁 Script executed:

# Check for MAX_BATCH_SIZE definition
rg -n "MAX_BATCH_SIZE" src/

Repository: ding113/claude-code-hub

Length of output: 751


🏁 Script executed:

# Check the full function context to understand the batch processing
sed -n '1350,1450p' src/actions/users.ts

Repository: ding113/claude-code-hub

Length of output: 4124


性能问题:批量同步在事务内对每把 Key 串行发 UPDATE,大批量场景下事务持有时间过长

batchSyncUserConfigToKeys 在事务中对每个用户的每把 Key 顺序发起单独 UPDATE(第 1418-1437 行)。MAX_BATCH_SIZE=500,若平均每用户 3-5 把 Key,合计 1500-2500 次串行 round-trip 在同一事务连接上执行,容易触发:

  • 连接级别的 statement timeout / 事务空闲超时
  • 长事务对 keys 表上其它写操作的锁等待
  • 进程被取消后已提交部分可能丢失

建议采用 Drizzle 的 sql 模板 + CASE WHEN id THEN ... END 的批量更新模式(参考已有的 src/repository/provider.ts:954-964src/repository/message-write-buffer.ts:116-160),将事务内的 N 次 UPDATE 降低到 O(字段数) 的规模,显著降低事务持有时间和锁竞争风险。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/users.ts` around lines 1382 - 1437, The current
batchSyncUserConfigToKeys logic issues a separate tx.update for each key (loop
around keysTable updates), causing many serial round-trips and long
transactions; replace the per-key updates inside batchSyncUserConfigToKeys by
building a single batched UPDATE using Drizzle's sql template with CASE WHEN
keyed by keysTable.id (similar to the pattern in src/repository/provider.ts
lines ~954-964 and src/repository/message-write-buffer.ts lines ~116-160):
collect all key ids and for each updatable column (limit5hUsd, limitDailyUsd,
limitWeeklyUsd, limitMonthlyUsd, limitTotalUsd, limitConcurrentSessions,
providerGroup, dailyResetMode, dailyResetTime, updatedAt) build CASE expressions
mapping id -> value (use NULL where appropriate), then execute one tx.sql`UPDATE
${keysTable} SET ... WHERE ${inArray(keysTable.id, ids)} AND
${isNull(keysTable.deletedAt)}` inside the transaction to replace the looped
tx.update calls.

Comment thread src/app/[locale]/dashboard/_components/user/create-user-dialog.tsx
Comment thread src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx
@dofastted dofastted force-pushed the pr/dev-user-key-sync-20260426 branch from 588a923 to c7b774f Compare April 26, 2026 05:04
@dofastted
Copy link
Copy Markdown
Contributor Author

Updated after review:\n\n- user-to-key sync now includes for single-user sync, batch sync, and default key creation.\n- validation helper import moved to the module top level.\n- cache invalidation now runs as a background task with warning logs on failure.\n- batch sync error logging and fallback user-facing error now follow the existing English log / translated error pattern.\n\nVerification after update:\n-
RUN v4.1.4 /mnt/x/project/claude-code-hub
API started at http://127.0.0.1:51205/

stdout | tests/unit/lib/users/user-key-sync.test.ts

Vitest 测试环境初始化...

测试配置:

stdout | tests/unit/lib/users/user-key-sync.test.ts

Vitest 测试环境清理...

Vitest 测试环境清理完成

✓ |0| tests/unit/lib/users/user-key-sync.test.ts > user key sync allocation > single key gets the same user limits 1ms
✓ |0| tests/unit/lib/users/user-key-sync.test.ts > user key sync allocation > amount limits are averaged by cents and discard remainder 0ms
✓ |0| tests/unit/lib/users/user-key-sync.test.ts > user key sync allocation > small amount limits assign cents to early keys and null to the rest 0ms
✓ |0| tests/unit/lib/users/user-key-sync.test.ts > user key sync allocation > small concurrent limits assign one session to early keys and zero to the rest 0ms
✓ |0| tests/unit/lib/users/user-key-sync.test.ts > user key sync allocation > null and non-positive values clear key limits 0ms
stdout | tests/unit/actions/users-key-sync.test.ts

Vitest 测试环境初始化...

测试配置:

stdout | tests/unit/user-dialogs.test.tsx

Vitest 测试环境初始化...

测试配置:

✓ |0| tests/unit/user-dialogs.test.tsx > EditUserDialog > renders dialog with user data when open 43ms
✓ |0| tests/unit/user-dialogs.test.tsx > EditUserDialog > does not render content when closed 3ms
✓ |0| tests/unit/user-dialogs.test.tsx > EditUserDialog > passes correct user id to UserEditSection 11ms
✓ |0| tests/unit/user-dialogs.test.tsx > EditUserDialog > passes correct user id to DangerZone 14ms
✓ |0| tests/unit/user-dialogs.test.tsx > EditUserDialog > has save and cancel buttons 11ms
✓ |0| tests/unit/user-dialogs.test.tsx > EditUserDialog > EditUserDialog renders reset 5h button beside reset all limits button 12ms
stdout | tests/unit/user-dialogs.test.tsx

Vitest 测试环境清理...

Vitest 测试环境清理完成

✓ |0| tests/unit/user-dialogs.test.tsx > EditUserDialog > EditUserDialog disables reset 5h button when no 5h limit is configured 14ms
✓ |0| tests/unit/user-dialogs.test.tsx > EditKeyDialog > renders dialog with key data when open 4ms
✓ |0| tests/unit/user-dialogs.test.tsx > EditKeyDialog > passes keyData to EditKeyForm 3ms
✓ |0| tests/unit/user-dialogs.test.tsx > EditKeyDialog > calls onOpenChange when dialog is closed 5ms
✓ |0| tests/unit/user-dialogs.test.tsx > AddKeyDialog > renders dialog with add key form when open 3ms
✓ |0| tests/unit/user-dialogs.test.tsx > AddKeyDialog > passes userId to AddKeyForm 2ms
✓ |0| tests/unit/user-dialogs.test.tsx > AddKeyDialog > calls onSuccess after successful key creation 8ms
✓ |0| tests/unit/user-dialogs.test.tsx > CreateUserDialog > renders dialog with user and key sections when open 6ms
✓ |0| tests/unit/user-dialogs.test.tsx > CreateUserDialog > does not render content when closed 1ms
✓ |0| tests/unit/user-dialogs.test.tsx > CreateUserDialog > has create and cancel buttons 5ms
✓ |0| tests/unit/user-dialogs.test.tsx > Dialog Component Integration > EditUserDialog re-renders with new user when user prop changes 18ms
✓ |0| tests/unit/user-dialogs.test.tsx > Dialog Component Integration > all dialogs have accessible title 10ms
✓ |0| tests/unit/actions/users-key-sync.test.ts > users key sync actions > addUser creates the default key with user limits 9012ms
stdout | tests/unit/actions/users-key-sync.test.ts

Vitest 测试环境清理...

Vitest 测试环境清理完成

✓ |0| tests/unit/actions/users-key-sync.test.ts > users key sync actions > syncUserConfigToKeys saves user and updates all undeleted keys by created order 4ms
✓ |0| tests/unit/actions/users-key-sync.test.ts > users key sync actions > batchSyncUserConfigToKeys saves batch user fields and syncs keys per user 2ms
✓ |0| tests/unit/actions/users-key-sync.test.ts > users key sync actions > syncUserConfigToKeys rejects non-admin users 0ms

Test Files 3 passed (3)
Tests 27 passed (27)
Start at 13:04:27
Duration 15.44s (transform 6.77s, setup 384ms, import 12.28s, tests 9.20s, environment 9.62s)\n-

claude-code-hub@0.7.0 typecheck
tsgo -p tsconfig.json --noEmit

@dofastted
Copy link
Copy Markdown
Contributor Author

Correction for the previous comment formatting. Updated after review:\n\n- user-to-key sync now includes limit5hResetMode for single-user sync, batch sync, and default key creation.\n- validation helper import moved to the module top level.\n- cache invalidation now runs as a background task with warning logs on failure.\n- batch sync error logging and fallback user-facing error now follow the existing English log / translated error pattern.\n\nVerification after update:\n- Vitest targeted user-key sync tests passed with testTimeout=30000.\n- npm run typecheck passed.

Comment on lines +117 to +123
if (normalizedTotal >= keyCount) {
const perKey = Math.floor(normalizedTotal / keyCount);
return {
values: Array.from({ length: keyCount }, () => perKey),
discarded: normalizedTotal - perKey * keyCount,
};
}
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 Concurrent session remainder silently dropped

When normalizedTotal >= keyCount, the floor-division remainder is computed but never distributed — all keys get perKey sessions and the leftover is discarded. For example, 7 sessions across 3 keys produces [2, 2, 2] (total: 6) instead of [3, 2, 2] (total: 7). The PR description explicitly states "remainder distributed one-per-key to earliest keys", but this branch doesn't implement that. The test suite only exercises normalizedTotal < keyCount, so this path is untested.

Suggested change
if (normalizedTotal >= keyCount) {
const perKey = Math.floor(normalizedTotal / keyCount);
return {
values: Array.from({ length: keyCount }, () => perKey),
discarded: normalizedTotal - perKey * keyCount,
};
}
if (normalizedTotal >= keyCount) {
const perKey = Math.floor(normalizedTotal / keyCount);
const remainder = normalizedTotal - perKey * keyCount;
return {
values: Array.from({ length: keyCount }, (_, index) =>
index < remainder ? perKey + 1 : perKey
),
discarded: 0,
};
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/users/user-key-sync.ts
Line: 117-123

Comment:
**Concurrent session remainder silently dropped**

When `normalizedTotal >= keyCount`, the floor-division remainder is computed but never distributed — all keys get `perKey` sessions and the leftover is discarded. For example, 7 sessions across 3 keys produces `[2, 2, 2]` (total: 6) instead of `[3, 2, 2]` (total: 7). The PR description explicitly states *"remainder distributed one-per-key to earliest keys"*, but this branch doesn't implement that. The test suite only exercises `normalizedTotal < keyCount`, so this path is untested.

```suggestion
  if (normalizedTotal >= keyCount) {
    const perKey = Math.floor(normalizedTotal / keyCount);
    const remainder = normalizedTotal - perKey * keyCount;
    return {
      values: Array.from({ length: keyCount }, (_, index) =>
        index < remainder ? perKey + 1 : perKey
      ),
      discarded: 0,
    };
  }
```

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (4)
src/app/[locale]/dashboard/_components/user/create-user-dialog.tsx (1)

175-185: ⚠️ Potential issue | 🟠 Major

首个 Key 同步仍使用表单草稿,可能与已落库用户配置不一致。

Line 175 至 Line 185 仍从 data.user 构建 firstKeyConfig,Line 197 也未使用 firstKeyConfig.limit5hResetMode。当服务端在 createUserOnly 后补齐/规范化默认值时,首个 Key 会和真实用户配置偏离。

建议修复
-          const firstKeyConfig = buildFirstSyncedKeyConfig({
-            dailyQuota: data.user.dailyQuota ?? null,
-            limit5hUsd: data.user.limit5hUsd ?? null,
-            limitWeeklyUsd: data.user.limitWeeklyUsd ?? null,
-            limitMonthlyUsd: data.user.limitMonthlyUsd ?? null,
-            limitTotalUsd: data.user.limitTotalUsd ?? null,
-            limitConcurrentSessions: data.user.limitConcurrentSessions ?? null,
-            providerGroup: data.user.providerGroup ?? PROVIDER_GROUP.DEFAULT,
-            dailyResetMode: data.user.dailyResetMode,
-            dailyResetTime: data.user.dailyResetTime,
-          });
+          const persistedUser = userRes.data.user;
+          const firstKeyConfig = buildFirstSyncedKeyConfig({
+            dailyQuota: persistedUser.dailyQuota ?? null,
+            limit5hUsd: persistedUser.limit5hUsd ?? null,
+            limitWeeklyUsd: persistedUser.limitWeeklyUsd ?? null,
+            limitMonthlyUsd: persistedUser.limitMonthlyUsd ?? null,
+            limitTotalUsd: persistedUser.limitTotalUsd ?? null,
+            limitConcurrentSessions: persistedUser.limitConcurrentSessions ?? null,
+            providerGroup: persistedUser.providerGroup ?? PROVIDER_GROUP.DEFAULT,
+            limit5hResetMode: persistedUser.limit5hResetMode ?? null,
+            dailyResetMode: persistedUser.dailyResetMode ?? null,
+            dailyResetTime: persistedUser.dailyResetTime ?? null,
+          });
@@
-            limit5hResetMode: data.user.limit5hResetMode ?? data.key.limit5hResetMode,
+            limit5hResetMode: firstKeyConfig.limit5hResetMode,

Also applies to: 197-197

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/dashboard/_components/user/create-user-dialog.tsx around
lines 175 - 185, The code builds firstKeyConfig from the form draft (data.user)
which can differ from the server-normalized user after createUserOnly; change
the logic to construct firstKeyConfig from the server-returned/created user
object (the response from createUserOnly) rather than data.user, and ensure you
actually use firstKeyConfig.limit5hResetMode where intended (replace any direct
references to data.user.limit5hResetMode or hardcoded draft values with
firstKeyConfig.limit5hResetMode). Locate usages around
buildFirstSyncedKeyConfig, firstKeyConfig, and createUserOnly and update them so
the first key uses the persisted/normalized user fields.
messages/ja/dashboard.json (1)

1620-1625: ⚠️ Potential issue | 🟡 Minor

日文文案里仍混入了英文 Key

这组提示会直接展示给 ja 用户,当前的 Key 会让这块 UI 语言不一致,建议统一改成日文表述(例如 キー)。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@messages/ja/dashboard.json` around lines 1620 - 1625, The Japanese copy in
the syncKeys object still contains the English word "Key"; update all
occurrences in the syncKeys fields ("button", "loading", "success", "error") to
use the Japanese term "キー" (e.g., change "Key に同期" → "キーに同期", "{count} 個の Key
に同期しました" → "{count} 個の キーに同期しました" or adjust spacing to match project style) so
the UI text is fully Japanese and consistent.
src/actions/users.ts (1)

1439-1458: ⚠️ Potential issue | 🟠 Major

批量同步仍在事务里按 Key 串行执行单行 UPDATE

这里在 500 用户上限下会把事务拉长成大量逐 key 的往返更新,容易放大锁等待和 statement timeout 风险。更稳妥的做法是把同一批 key 合并成批量更新(例如按列构造 CASE WHEN id THEN ... END),至少不要在事务里逐条写。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/users.ts` around lines 1439 - 1458, The loop over userKeys (and
configs) issues one awaited tx.update per key which prolongs the transaction and
causes many round-trips; refactor the update logic in the function that uses
userKeys/configs to perform a single batched update instead of per-key updates:
collect ids and for each updatable column build a CASE WHEN ... THEN ... END
expression (or use your DB client's bulk upsert/batch update API) and call
tx.update(keysTable).set({...CASE expressions...}).where(in(keysTable.id, ids))
so you remove the per-iteration await and collapse all key updates into one
statement within the transaction (or move the batch update outside the
transaction if appropriate). Ensure you reference userKeys, configs,
tx.update(keysTable), and keysTable.id when implementing the change.
src/app/[locale]/dashboard/_components/user/batch-edit/batch-edit-dialog.tsx (1)

441-448: ⚠️ Potential issue | 🟠 Major

确认框仍会低估这次操作会改动的 Key 数量。

当只开启“同步到 Keys”而没有单独选中 key 字段时,这里仍把 keysCount 算成 0,但下面的说明又写着会覆盖这些用户的全部未删除 key。确认信息不能比真实影响范围更小;这里至少应改成不显示具体 key 数,或单独走“同步这些用户下全部未删除 key”的文案分支。

Also applies to: 458-463

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/dashboard/_components/user/batch-edit/batch-edit-dialog.tsx
around lines 441 - 448, The confirmation text underestimates affected keys
because keysCount is set to 0 when willUpdateKeys is false even if
willSyncUsersToKeys is true; update the logic around keysCount and the rendering
branch: compute keysCount only when willUpdateKeys is true (keep keysCount =
pendingUpdate.keyIds.length) but add a separate branch/flag (e.g., isSyncAllKeys
= willSyncUsersToKeys && !willUpdateKeys) and when isSyncAllKeys is true render
a different translation/message (or omit numeric keys parameter) that says "sync
all undeleted keys for these users" instead of showing 0, using the existing
symbols willSyncUsersToKeys, willUpdateKeys, keysCount, usersCount and
pendingUpdate.keyIds in the component that renders t("confirm.description") (and
the other occurrence around lines 458–463).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@messages/ru/dashboard.json`:
- Around line 1689-1690: The Russian messages file contains English text for
several i18n keys (e.g., "syncKeys", "syncKeysDescription" and other keys added
between lines ~1689–1731); replace those English strings with their Russian
translations in messages/ru/dashboard.json so the ru locale is fully localized
(do not hardcode display text elsewhere), ensuring keys like syncKeys,
syncKeysDescription and the other mentioned keys (around 1699–1702, 1725–1726,
1731) contain proper Russian text consistent with the other locales.

In `@src/actions/users.ts`:
- Around line 96-108: The helper invalidateUserKeySyncCaches currently
fire-and-forgets invalidation via void Promise.all(...), causing callers like
syncUserConfigToKeys and batchSyncUserConfigToKeys to return before Redis
invalidation completes; change invalidateUserKeySyncCaches to return
Promise<void> (remove void and return the Promise) and preserve the existing
per-item .catch logging (warnUserCacheInvalidationFailure /
warnKeyCacheInvalidationFailure), then update the two callers to await
invalidateUserKeySyncCaches("user-key sync", [userId], keyStringsForCache) and
await invalidateUserKeySyncCaches("batch user-key sync", requestedIds,
keyStringsForCache) so that cache invalidation (invalidateCachedUser /
invalidateCachedKey) completes before success is returned.

---

Duplicate comments:
In `@messages/ja/dashboard.json`:
- Around line 1620-1625: The Japanese copy in the syncKeys object still contains
the English word "Key"; update all occurrences in the syncKeys fields ("button",
"loading", "success", "error") to use the Japanese term "キー" (e.g., change "Key
に同期" → "キーに同期", "{count} 個の Key に同期しました" → "{count} 個の キーに同期しました" or adjust
spacing to match project style) so the UI text is fully Japanese and consistent.

In `@src/actions/users.ts`:
- Around line 1439-1458: The loop over userKeys (and configs) issues one awaited
tx.update per key which prolongs the transaction and causes many round-trips;
refactor the update logic in the function that uses userKeys/configs to perform
a single batched update instead of per-key updates: collect ids and for each
updatable column build a CASE WHEN ... THEN ... END expression (or use your DB
client's bulk upsert/batch update API) and call
tx.update(keysTable).set({...CASE expressions...}).where(in(keysTable.id, ids))
so you remove the per-iteration await and collapse all key updates into one
statement within the transaction (or move the batch update outside the
transaction if appropriate). Ensure you reference userKeys, configs,
tx.update(keysTable), and keysTable.id when implementing the change.

In
`@src/app/`[locale]/dashboard/_components/user/batch-edit/batch-edit-dialog.tsx:
- Around line 441-448: The confirmation text underestimates affected keys
because keysCount is set to 0 when willUpdateKeys is false even if
willSyncUsersToKeys is true; update the logic around keysCount and the rendering
branch: compute keysCount only when willUpdateKeys is true (keep keysCount =
pendingUpdate.keyIds.length) but add a separate branch/flag (e.g., isSyncAllKeys
= willSyncUsersToKeys && !willUpdateKeys) and when isSyncAllKeys is true render
a different translation/message (or omit numeric keys parameter) that says "sync
all undeleted keys for these users" instead of showing 0, using the existing
symbols willSyncUsersToKeys, willUpdateKeys, keysCount, usersCount and
pendingUpdate.keyIds in the component that renders t("confirm.description") (and
the other occurrence around lines 458–463).

In `@src/app/`[locale]/dashboard/_components/user/create-user-dialog.tsx:
- Around line 175-185: The code builds firstKeyConfig from the form draft
(data.user) which can differ from the server-normalized user after
createUserOnly; change the logic to construct firstKeyConfig from the
server-returned/created user object (the response from createUserOnly) rather
than data.user, and ensure you actually use firstKeyConfig.limit5hResetMode
where intended (replace any direct references to data.user.limit5hResetMode or
hardcoded draft values with firstKeyConfig.limit5hResetMode). Locate usages
around buildFirstSyncedKeyConfig, firstKeyConfig, and createUserOnly and update
them so the first key uses the persisted/normalized user fields.
🪄 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: 4a5e4ab3-2afc-41a6-99fd-8b9f22e3bdd3

📥 Commits

Reviewing files that changed from the base of the PR and between 588a923 and c7b774f.

📒 Files selected for processing (23)
  • messages/en/dashboard.json
  • messages/en/settings/config.json
  • messages/ja/dashboard.json
  • messages/ja/settings/config.json
  • messages/ru/dashboard.json
  • messages/ru/settings/config.json
  • messages/zh-CN/dashboard.json
  • messages/zh-CN/settings/config.json
  • messages/zh-TW/dashboard.json
  • messages/zh-TW/settings/config.json
  • src/actions/users.ts
  • src/app/[locale]/dashboard/_components/user/batch-edit/batch-edit-dialog.tsx
  • src/app/[locale]/dashboard/_components/user/batch-edit/batch-user-section.tsx
  • src/app/[locale]/dashboard/_components/user/create-user-dialog.tsx
  • src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx
  • src/app/[locale]/dashboard/_components/user/forms/key-edit-section.tsx
  • src/app/[locale]/settings/config/_components/system-settings-form.tsx
  • src/app/[locale]/settings/config/page.tsx
  • src/lib/users/user-key-sync.ts
  • tests/unit/actions/users-key-sync.test.ts
  • tests/unit/actions/users-reset-all-statistics.test.ts
  • tests/unit/lib/users/user-key-sync.test.ts
  • tests/unit/user-dialogs.test.tsx
✅ Files skipped from review due to trivial changes (11)
  • messages/en/settings/config.json
  • messages/ja/settings/config.json
  • tests/unit/actions/users-reset-all-statistics.test.ts
  • src/app/[locale]/settings/config/page.tsx
  • tests/unit/lib/users/user-key-sync.test.ts
  • src/app/[locale]/dashboard/_components/user/batch-edit/batch-user-section.tsx
  • messages/zh-CN/settings/config.json
  • messages/ru/settings/config.json
  • messages/en/dashboard.json
  • src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx
  • src/lib/users/user-key-sync.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • messages/zh-TW/settings/config.json

Comment on lines +1689 to +1690
"syncKeys": "Sync to Keys",
"syncKeysDescription": "This will overwrite all undeleted keys for these {users} users from their current user settings.",
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

俄语语言包中出现英文文案,批量同步流程会出现混合语言。

Line 1689 至 Line 1731 的新增键值仍是英文(如 Sync to KeysThis will overwrite...),这会直接影响俄语用户的关键操作确认与错误提示体验。

建议修复(俄语示例)
-        "syncKeys": "Sync to Keys",
-        "syncKeysDescription": "This will overwrite all undeleted keys for these {users} users from their current user settings.",
+        "syncKeys": "Синхронизировать в ключи",
+        "syncKeysDescription": "Это перезапишет все неудалённые ключи для {users} пользователей на основе их текущих настроек пользователя.",
@@
-        "keysSynced": "Synced {keys} keys for {users} users",
+        "keysSynced": "Синхронизировано {keys} ключей для {users} пользователей",
@@
-        "syncFailed": "Sync to keys failed: {error}",
+        "syncFailed": "Синхронизация в ключи не удалась: {error}",
@@
-          "syncKeys": "Sync to Keys"
+          "syncKeys": "Синхронизировать в ключи"
@@
-          "syncKeysDescription": "When enabled, user fields from this batch edit are saved first, then all undeleted keys for each user are overwritten from that user's settings."
+          "syncKeysDescription": "При включении сначала сохраняются поля пользователя из этого массового редактирования, затем для каждого пользователя перезаписываются все неудалённые ключи по его настройкам."

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".

Also applies to: 1699-1702, 1725-1726, 1731-1731

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@messages/ru/dashboard.json` around lines 1689 - 1690, The Russian messages
file contains English text for several i18n keys (e.g., "syncKeys",
"syncKeysDescription" and other keys added between lines ~1689–1731); replace
those English strings with their Russian translations in
messages/ru/dashboard.json so the ru locale is fully localized (do not hardcode
display text elsewhere), ensuring keys like syncKeys, syncKeysDescription and
the other mentioned keys (around 1699–1702, 1725–1726, 1731) contain proper
Russian text consistent with the other locales.

Comment thread src/actions/users.ts
Comment on lines +96 to +108
function invalidateUserKeySyncCaches(context: string, userIds: number[], keyStrings: string[]) {
void Promise.all([
...userIds.map((userId) =>
invalidateCachedUser(userId).catch((error) =>
warnUserCacheInvalidationFailure(context, userId, error)
)
),
...keyStrings.map((key) =>
invalidateCachedKey(key).catch((error) =>
warnKeyCacheInvalidationFailure(context, key, error)
)
),
]);
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

不要把鉴权缓存失效做成 fire-and-forget。

这里直接 void Promise.all(...),会让 syncUserConfigToKeys / batchSyncUserConfigToKeys 在 Redis 失效真正完成前就返回成功。下一次请求如果刚好命中旧缓存,用户或 Key 仍可能短时间继续使用旧额度/旧 providerGroup,和“同步成功”的语义不一致。建议让这个 helper 返回 Promise<void>,并在两个 action 的成功路径里显式 await

建议修改
-function invalidateUserKeySyncCaches(context: string, userIds: number[], keyStrings: string[]) {
-  void Promise.all([
+async function invalidateUserKeySyncCaches(
+  context: string,
+  userIds: number[],
+  keyStrings: string[]
+) {
+  await Promise.all([
     ...userIds.map((userId) =>
       invalidateCachedUser(userId).catch((error) =>
         warnUserCacheInvalidationFailure(context, userId, error)
@@
     ...keyStrings.map((key) =>
       invalidateCachedKey(key).catch((error) =>
         warnKeyCacheInvalidationFailure(context, key, error)
       )
     ),
   ]);
 }

并把两个调用点改成:

await invalidateUserKeySyncCaches("batch user-key sync", requestedIds, keyStringsForCache);
await invalidateUserKeySyncCaches("user-key sync", [userId], keyStringsForCache);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/actions/users.ts` around lines 96 - 108, The helper
invalidateUserKeySyncCaches currently fire-and-forgets invalidation via void
Promise.all(...), causing callers like syncUserConfigToKeys and
batchSyncUserConfigToKeys to return before Redis invalidation completes; change
invalidateUserKeySyncCaches to return Promise<void> (remove void and return the
Promise) and preserve the existing per-item .catch logging
(warnUserCacheInvalidationFailure / warnKeyCacheInvalidationFailure), then
update the two callers to await invalidateUserKeySyncCaches("user-key sync",
[userId], keyStringsForCache) and await invalidateUserKeySyncCaches("batch
user-key sync", requestedIds, keyStringsForCache) so that cache invalidation
(invalidateCachedUser / invalidateCachedKey) completes before success is
returned.

Copy link
Copy Markdown

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

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

ℹ️ 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 +319 to +321
limit5hUsd: data.limit5hUsd,
dailyQuota: data.dailyQuota,
limitWeeklyUsd: data.limitWeeklyUsd,
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 Include 5h reset mode in sync-to-keys payload

The new handleSyncKeys request omits limit5hResetMode, so clicking Sync to Keys after changing the 5h window mode in the edit form will keep using the old mode from the database and propagate that stale mode to keys. This creates a silent mismatch where the UI shows a new mode but the sync operation does not apply it; include limit5hResetMode in this payload (as the normal save path already does).

Useful? React with 👍 / 👎.

Comment on lines +117 to +121
if (normalizedTotal >= keyCount) {
const perKey = Math.floor(normalizedTotal / keyCount);
return {
values: Array.from({ length: keyCount }, () => perKey),
discarded: normalizedTotal - perKey * keyCount,
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 Preserve concurrent-session remainder when splitting keys

distributeConcurrentSessions drops the remainder whenever normalizedTotal >= keyCount (discarded is returned but never reassigned), so a user limit like 5 sessions across 3 keys becomes [1,1,1] instead of preserving all 5 sessions. This systematically under-allocates concurrency after sync for many non-divisible totals, causing avoidable throttling versus the configured user limit.

Useful? React with 👍 / 👎.

@dofastted dofastted force-pushed the pr/dev-user-key-sync-20260426 branch from c7b774f to a66fbe9 Compare April 26, 2026 05:12
@dofastted
Copy link
Copy Markdown
Contributor Author

Second review update pushed. Additional changes:\n\n- Create-user flow now builds the first key config from the saved user returned by the server, not only from the local form draft.\n- Edit-user sync path now passes limit5hResetMode.\n- createUserOnly response now returns daily reset fields needed by the first-key sync path.\n- Japanese dashboard sync strings and Russian billing correction strings are localized.\n\nVerification:\n- Targeted user-key sync Vitest passed: 27 passed.\n- npm run typecheck passed.

Comment on lines +137 to +141
const limit5hResetMode: "fixed" | "rolling" =
source.limit5hResetMode === "fixed" ? "fixed" : "rolling";
const dailyResetMode: "fixed" | "rolling" =
source.dailyResetMode === "rolling" ? "rolling" : "fixed";
const dailyResetTime = source.dailyResetTime || "00:00";
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 Asymmetric defaults for reset modes

limit5hResetMode defaults to "rolling" when the source value is absent/null (=== "fixed" ? "fixed" : "rolling"), while dailyResetMode defaults to "fixed" (=== "rolling" ? "rolling" : "fixed"). These two guards use opposite logic, so a user row where neither reset mode has been explicitly set will have its keys synced with limit5hResetMode: "rolling" and dailyResetMode: "fixed". If the intended fallback for both fields is the same value (e.g., both "rolling" or both "fixed"), one of these expressions is inverted.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/users/user-key-sync.ts
Line: 137-141

Comment:
**Asymmetric defaults for reset modes**

`limit5hResetMode` defaults to `"rolling"` when the source value is absent/null (`=== "fixed" ? "fixed" : "rolling"`), while `dailyResetMode` defaults to `"fixed"` (`=== "rolling" ? "rolling" : "fixed"`). These two guards use opposite logic, so a user row where neither reset mode has been explicitly set will have its keys synced with `limit5hResetMode: "rolling"` and `dailyResetMode: "fixed"`. If the intended fallback for both fields is the same value (e.g., both "rolling" or both "fixed"), one of these expressions is inverted.

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
messages/ja/dashboard.json (1)

1720-1726: ⚠️ Potential issue | 🟠 Major

日语语言包这里仍有英文文案,批量同步流程会出现混合语言。

Line 1720 和 Line 1726 仍是英文,建议改成日文以保持 ja 体验一致。

建议修复
-          "syncKeys": "Sync to Keys"
+          "syncKeys": "キーに同期"
@@
-          "syncKeysDescription": "When enabled, user fields from this batch edit are saved first, then all undeleted keys for each user are overwritten from that user's settings."
+          "syncKeysDescription": "有効にすると、この一括編集のユーザー項目を先に保存し、その後各ユーザーの未削除キーをユーザー設定で上書きします。"
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 the current code and only fix it if needed.

In `@messages/ja/dashboard.json` around lines 1720 - 1726, The ja locale contains
English strings for the batch-sync UI keys "syncKeys" and "syncKeysDescription";
update messages/ja/dashboard.json by replacing the values for "syncKeys" and
"syncKeysDescription" with appropriate Japanese translations (keeping the key
names unchanged) so the batch edit UI is fully localized in Japanese and follows
i18n guidelines.
messages/ru/dashboard.json (1)

1689-1731: ⚠️ Potential issue | 🟠 Major

俄语语言包中这组新增文案仍是英文,会导致关键流程混合语言。

syncKeys 相关确认提示、toast、字段描述在 ru locale 下仍未本地化,建议统一为俄语。

建议修复
-        "syncKeys": "Sync to Keys",
-        "syncKeysDescription": "This will overwrite all undeleted keys for these {users} users from their current user settings.",
+        "syncKeys": "Синхронизировать в ключи",
+        "syncKeysDescription": "Это перезапишет все неудалённые ключи для {users} пользователей на основе их текущих настроек пользователя.",
@@
-        "keysSynced": "Synced {keys} keys for {users} users",
+        "keysSynced": "Синхронизировано {keys} ключей для {users} пользователей",
@@
-        "syncFailed": "Sync to keys failed: {error}",
+        "syncFailed": "Не удалось синхронизировать в ключи: {error}",
@@
-          "syncKeys": "Sync to Keys"
+          "syncKeys": "Синхронизировать в ключи"
@@
-          "syncKeysDescription": "When enabled, user fields from this batch edit are saved first, then all undeleted keys for each user are overwritten from that user's settings."
+          "syncKeysDescription": "При включении сначала сохраняются поля пользователя из этого массового редактирования, затем для каждого пользователя все неудалённые ключи перезаписываются его пользовательскими настройками."
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 the current code and only fix it if needed.

In `@messages/ru/dashboard.json` around lines 1689 - 1731, The ru locale contains
several untranslated keys—update the string values for "syncKeys",
"syncKeysDescription", "fields.syncKeys", "placeholders.syncKeysDescription",
and the toast messages "toast.keysSynced" and "toast.syncFailed" to Russian;
ensure you preserve interpolation tokens like {users}, {keys}, {error}, and
{count} exactly as in the originals and keep JSON formatting/escaping correct so
that syncKeys, syncKeysDescription, fields.syncKeys,
placeholders.syncKeysDescription, toast.keysSynced, and toast.syncFailed are
fully localized.
🧹 Nitpick comments (3)
src/app/[locale]/settings/config/_components/system-settings-form.tsx (1)

447-462: Optional:与卡片外开关风格对齐时可补一个图标容器。

该开关行使用了 rounded-lg border border-white/5 bg-background/30 p-3 的紧凑样式且未带图标容器,而 Toggle Settings 区块内的其他开关(如 enable-http2enable-thinking-signature-rectifier)统一为 p-4 rounded-xl + 彩色图标容器。当前布局是嵌套在已有顶部图标的 billing-correction 卡片内,差异是合理的;如果希望视觉上完全统一,可以补一个小尺寸图标容器(例如 FileCode/Pencil),否则保持现状即可。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/settings/config/_components/system-settings-form.tsx around
lines 447 - 462, 当前在 system-settings-form.tsx
中与开关相关的卡片(enableBillingHeaderRectifier / Switch
id="enable-billing-header-rectifier")使用了紧凑的 p-3 rounded-lg 样式,与其它 Toggle
Settings 中使用的 p-4 rounded-xl +
彩色图标容器不一致;若要视觉统一,请在该开关行左侧加入一个小尺寸图标容器(与其它开关相同的结构:占位圆角背景 + 图标组件,例如 FileCode 或
Pencil),并把外层容器样式调整为 p-4 rounded-xl(或在保持 border/bg 的同时增加左侧空间),确保 Switch 的
checked/onCheckedChange 逻辑(enableBillingHeaderRectifier,
setEnableBillingHeaderRectifier)不变且 disabled={isPending} 保持;如果不需要统一外观,则无需改动。
tests/unit/user-dialogs.test.tsx (1)

348-351: 避免在断言里硬编码同步按钮文案。

Line 350 直接写死 "Sync to Keys",后续文案或默认 locale 调整会让测试变脆。建议复用消息对象中的翻译键。

建议修复
-    expect(buttonTexts).toContain("Sync to Keys");
+    expect(buttonTexts).toContain(messages.dashboard.userManagement.editDialog.syncKeys.button);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/user-dialogs.test.tsx` around lines 348 - 351, 测试中不应硬编码同步按钮文案:把
expect(buttonTexts).toContain("Sync to Keys") 替换为使用现有国际化消息/键(例如
messages.syncToKeys 或 调用 intl.formatMessage(messages.syncToKeys) /
t('syncToKeys')),确保断言使用同一翻译源;在 user-dialogs.test.tsx 中定位 buttonTexts
断言并引入或复用测试上下文中的 messages/intl 实例来获取同步按钮的文案进行断言。
src/app/[locale]/dashboard/_components/user/forms/key-edit-section.tsx (1)

469-542: 可选重构:将 showProviderGroup 提到单一外层判断以减少重复。

当前四个分支每个都重复 showProviderGroup &&,逻辑等价但稍显冗余。可以将其提到外层一次性判断,提升可读性,行为不变。

♻️ 建议改写
-        {showProviderGroup && isAdmin ? (
-          <ProviderGroupSelect ... />
-        ) : showProviderGroup && userGroups.length > 0 ? (
-          <div className="space-y-2"> ... </div>
-        ) : showProviderGroup && keyGroupOptions.length > 0 ? (
-          <div className="space-y-2"> ... </div>
-        ) : showProviderGroup ? (
-          <div className="text-sm text-muted-foreground">
-            {translations.fields.providerGroup.noGroupHint || "..."}
-          </div>
-        ) : null}
+        {showProviderGroup &&
+          (isAdmin ? (
+            <ProviderGroupSelect ... />
+          ) : userGroups.length > 0 ? (
+            <div className="space-y-2"> ... </div>
+          ) : keyGroupOptions.length > 0 ? (
+            <div className="space-y-2"> ... </div>
+          ) : (
+            <div className="text-sm text-muted-foreground">
+              {translations.fields.providerGroup.noGroupHint || "..."}
+            </div>
+          ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[locale]/dashboard/_components/user/forms/key-edit-section.tsx
around lines 469 - 542, Refactor the JSX so the repeated "showProviderGroup &&"
is checked once: wrap the entire current multi-branch expression with a single
conditional on showProviderGroup and then keep the existing nested logic
(isAdmin ? render ProviderGroupSelect : userGroups.length > 0 ? render read-only
or TagInputField based on keyData.id : keyGroupOptions.length > 0 ? render
badges : render noGroupHint). Ensure you preserve the exact components/props and
text uses (ProviderGroupSelect, TagInputField, keyGroupOptions, userGroups,
keyData, translations, PROVIDER_GROUP) and the read-only vs create-mode branches
so behavior remains identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@messages/ja/dashboard.json`:
- Around line 1720-1726: The ja locale contains English strings for the
batch-sync UI keys "syncKeys" and "syncKeysDescription"; update
messages/ja/dashboard.json by replacing the values for "syncKeys" and
"syncKeysDescription" with appropriate Japanese translations (keeping the key
names unchanged) so the batch edit UI is fully localized in Japanese and follows
i18n guidelines.

In `@messages/ru/dashboard.json`:
- Around line 1689-1731: The ru locale contains several untranslated keys—update
the string values for "syncKeys", "syncKeysDescription", "fields.syncKeys",
"placeholders.syncKeysDescription", and the toast messages "toast.keysSynced"
and "toast.syncFailed" to Russian; ensure you preserve interpolation tokens like
{users}, {keys}, {error}, and {count} exactly as in the originals and keep JSON
formatting/escaping correct so that syncKeys, syncKeysDescription,
fields.syncKeys, placeholders.syncKeysDescription, toast.keysSynced, and
toast.syncFailed are fully localized.

---

Nitpick comments:
In `@src/app/`[locale]/dashboard/_components/user/forms/key-edit-section.tsx:
- Around line 469-542: Refactor the JSX so the repeated "showProviderGroup &&"
is checked once: wrap the entire current multi-branch expression with a single
conditional on showProviderGroup and then keep the existing nested logic
(isAdmin ? render ProviderGroupSelect : userGroups.length > 0 ? render read-only
or TagInputField based on keyData.id : keyGroupOptions.length > 0 ? render
badges : render noGroupHint). Ensure you preserve the exact components/props and
text uses (ProviderGroupSelect, TagInputField, keyGroupOptions, userGroups,
keyData, translations, PROVIDER_GROUP) and the read-only vs create-mode branches
so behavior remains identical.

In `@src/app/`[locale]/settings/config/_components/system-settings-form.tsx:
- Around line 447-462: 当前在 system-settings-form.tsx
中与开关相关的卡片(enableBillingHeaderRectifier / Switch
id="enable-billing-header-rectifier")使用了紧凑的 p-3 rounded-lg 样式,与其它 Toggle
Settings 中使用的 p-4 rounded-xl +
彩色图标容器不一致;若要视觉统一,请在该开关行左侧加入一个小尺寸图标容器(与其它开关相同的结构:占位圆角背景 + 图标组件,例如 FileCode 或
Pencil),并把外层容器样式调整为 p-4 rounded-xl(或在保持 border/bg 的同时增加左侧空间),确保 Switch 的
checked/onCheckedChange 逻辑(enableBillingHeaderRectifier,
setEnableBillingHeaderRectifier)不变且 disabled={isPending} 保持;如果不需要统一外观,则无需改动。

In `@tests/unit/user-dialogs.test.tsx`:
- Around line 348-351: 测试中不应硬编码同步按钮文案:把 expect(buttonTexts).toContain("Sync to
Keys") 替换为使用现有国际化消息/键(例如 messages.syncToKeys 或 调用
intl.formatMessage(messages.syncToKeys) / t('syncToKeys')),确保断言使用同一翻译源;在
user-dialogs.test.tsx 中定位 buttonTexts 断言并引入或复用测试上下文中的 messages/intl
实例来获取同步按钮的文案进行断言。

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8a9779e2-27af-4c5f-8adb-331d00226340

📥 Commits

Reviewing files that changed from the base of the PR and between c7b774f and a66fbe9.

📒 Files selected for processing (23)
  • messages/en/dashboard.json
  • messages/en/settings/config.json
  • messages/ja/dashboard.json
  • messages/ja/settings/config.json
  • messages/ru/dashboard.json
  • messages/ru/settings/config.json
  • messages/zh-CN/dashboard.json
  • messages/zh-CN/settings/config.json
  • messages/zh-TW/dashboard.json
  • messages/zh-TW/settings/config.json
  • src/actions/users.ts
  • src/app/[locale]/dashboard/_components/user/batch-edit/batch-edit-dialog.tsx
  • src/app/[locale]/dashboard/_components/user/batch-edit/batch-user-section.tsx
  • src/app/[locale]/dashboard/_components/user/create-user-dialog.tsx
  • src/app/[locale]/dashboard/_components/user/edit-user-dialog.tsx
  • src/app/[locale]/dashboard/_components/user/forms/key-edit-section.tsx
  • src/app/[locale]/settings/config/_components/system-settings-form.tsx
  • src/app/[locale]/settings/config/page.tsx
  • src/lib/users/user-key-sync.ts
  • tests/unit/actions/users-key-sync.test.ts
  • tests/unit/actions/users-reset-all-statistics.test.ts
  • tests/unit/lib/users/user-key-sync.test.ts
  • tests/unit/user-dialogs.test.tsx
✅ Files skipped from review due to trivial changes (7)
  • messages/ja/settings/config.json
  • messages/en/dashboard.json
  • src/app/[locale]/settings/config/page.tsx
  • messages/zh-CN/settings/config.json
  • messages/ru/settings/config.json
  • src/app/[locale]/dashboard/_components/user/batch-edit/batch-user-section.tsx
  • src/lib/users/user-key-sync.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • messages/zh-TW/settings/config.json
  • tests/unit/actions/users-reset-all-statistics.test.ts
  • tests/unit/lib/users/user-key-sync.test.ts
  • messages/zh-TW/dashboard.json
  • tests/unit/actions/users-key-sync.test.ts
  • messages/en/settings/config.json
  • src/actions/users.ts
  • src/app/[locale]/dashboard/_components/user/batch-edit/batch-edit-dialog.tsx

Copy link
Copy Markdown

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

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

ℹ️ 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/users.ts
Comment on lines +2260 to +2262
invalidateUserKeySyncCaches("user-key sync", [userId], keyStringsForCache);

revalidatePath("/dashboard");
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 Clear 5h cost caches when sync changes reset mode

syncUserConfigToKeys writes limit5hResetMode updates and then only invalidates auth caches; unlike editUser/batchUpdateUsers, it never calls clearUserCostCache when the 5h mode flips. If an admin switches rolling↔fixed through the new Sync-to-Keys flow, Redis can retain mode-specific 5h counters from the previous mode, so subsequent limit checks may use stale/partial usage and mis-enforce quotas until TTL expiry. Please run the same reset-mode cache cleanup here (and in the batch sync path) whenever the mode changes.

Useful? React with 👍 / 👎.

@github-actions github-actions Bot mentioned this pull request Apr 27, 2026
5 tasks
@ding113
Copy link
Copy Markdown
Owner

ding113 commented Apr 28, 2026

您好,用户和 key 限额分离的设计是预期。用户、key、供应商三者限额最终的效果是取其小。

@ding113 ding113 closed this Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants