Conversation
- ManageMultiKeys 各 key 变更操作后回算渠道状态:全部密钥禁用且无冷却时 渠道自动禁用并退出 abilities 选择池,重新启用任一密钥时自动恢复 - 分发器不再吞掉 SetupContextForSelectedChannel 的错误:选中渠道暂无可用 密钥时按优先级逐级降级重选,全部不可用时返回 503 与明确错误信息
- LimitPattern 新增 reset_cycle(daily / weekly:N / monthly:N),冷却优先级: 错误信息捕获的重置时间 > 周期边界 > 默认分钟数;当月无 N 号时顺延到下月 1 号 - 新增阿里云 token-plan 额度耗尽预设(无重置时间,按每月 1 号冷却) - 前端校验不再强制 (?P<reset>...) 捕获组,日期布局行仅在含捕获组时显示, 新增重置周期下拉与日号输入
Expand all / Export Now / 日志导出任务提示 / OAuth / Resize column / Top 均为代码中已使用但 locale 文件缺失的 key
- 手动「测试连接」失败接入 processChannelError,与正常转发、定时巡检一致: 先按多 Key 限额模式给命中密钥设置冷却,未命中且满足自动禁用条件时禁用该密钥 - GetRootUser 与 CheckNotificationLimit 增加空依赖防护,避免后台通知协程 在 DB/Redis 未初始化时 panic
There was a problem hiding this comment.
🟡 Changes recommended
The distributor fallback selection and root-user lookup have edge cases that can still fail or terminate early, and the new fallback tests can be flaky with SQLite :memory: without a single-connection pool.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR improves multi-key channel resiliency and “limit pattern” cooldown behavior by adding reset-cycle support (daily/weekly/monthly) and by making distributor/key-management flows handle “no available key” scenarios more safely.
Changes:
- Add
reset_cyclesupport for multi-key limit patterns (frontend editor + backend cooldown computation + tests). - Improve multi-key channel status syncing and distributor fallback when selected channels have no usable keys.
- Align manual “test connection” error handling with relay behavior (cooldown/auto-disable) and add i18n strings.
File summaries
| File | Description |
|---|---|
| web/src/i18n/locales/zh.json | Add new UI strings for reset-cycle and limit-pattern editor updates. |
| web/src/i18n/locales/zh-TW.json | Add new UI strings for reset-cycle and limit-pattern editor updates. |
| web/src/i18n/locales/vi.json | Add new UI strings for reset-cycle and limit-pattern editor updates. |
| web/src/i18n/locales/ru.json | Add new UI strings for reset-cycle and limit-pattern editor updates. |
| web/src/i18n/locales/ja.json | Add new UI strings for reset-cycle and limit-pattern editor updates. |
| web/src/i18n/locales/fr.json | Add new UI strings for reset-cycle and limit-pattern editor updates. |
| web/src/i18n/locales/en.json | Add new UI strings for reset-cycle and limit-pattern editor updates. |
| web/src/features/channels/types.ts | Extend LimitPattern schema with optional reset_cycle; relax date_layout to allow empty. |
| web/src/features/channels/lib/limit-pattern-utils.ts | Add reset-cycle parsing/formatting helpers and a new preset that relies on cycle-based reset. |
| web/src/features/channels/lib/limit-pattern-utils.test.ts | Expand unit tests to cover reset-cycle helpers and preset behavior. |
| web/src/features/channels/lib/channel-form.ts | Update channel form schema to allow empty date_layout and include optional reset_cycle. |
| web/src/features/channels/components/limit-patterns-editor.tsx | Update UI to support reset cycles and optional reset capture group patterns. |
| service/notify-limit.go | Avoid Redis limiter usage when Redis client isn’t initialized. |
| service/channel.go | Add nextCycleResetTime and use ResetCycle as a cooldown strategy when no reset timestamp is captured. |
| service/channel_limit_test.go | Add tests for cycle reset behavior and precedence rules. |
| model/user.go | Guard GetRootUser when DB is nil to avoid background-task panics. |
| model/channel.go | Add ResetCycle to LimitPattern; add SyncMultiKeyChannelStatus and unify the “all keys disabled” reason. |
| model/channel_status_test.go | Add tests for SyncMultiKeyChannelStatus behavior and DB/abilities persistence. |
| middleware/distributor.go | Add distributor re-selection logic when a selected channel has no available key; add new i18n error path. |
| middleware/distributor_fallback_test.go | New tests covering distributor fallback selection behavior. |
| i18n/locales/zh-TW.yaml | Add backend i18n message for “channel has no available key”. |
| i18n/locales/zh-CN.yaml | Add backend i18n message for “channel has no available key”. |
| i18n/locales/en.yaml | Add backend i18n message for “channel has no available key”. |
| i18n/keys.go | Add new backend i18n key constant. |
| controller/channel.go | Ensure multi-key management operations re-sync channel status after key state changes. |
| controller/channel-test.go | Preserve upstream status codes in manual tests; route failures through unified error processing. |
| controller/channel_test_internal_test.go | Add integration-style tests for manual test error handling (cooldown/auto-disable). |
Review details
- Files reviewed: 27/27 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| func GetRootUser() (user *User) { | ||
| // 异步通知等后台任务可能在 DB 尚未初始化或已被回收后调用,此时视为无 root 用户 | ||
| if DB == nil { | ||
| return &User{} | ||
| } | ||
| DB.Where("role = ?", common.RoleRootUser).First(&user) | ||
| return user | ||
| } |
| func selectChannelWithAvailableKey(c *gin.Context, failedChannelId int, modelName string) *model.Channel { | ||
| usingGroup := common.GetContextKeyString(c, constant.ContextKeyUsingGroup) | ||
| tried := map[int]bool{failedChannelId: true} | ||
| for retry := 1; ; retry++ { | ||
| next, _, err := service.CacheGetRandomSatisfiedChannel(&service.RetryParam{ | ||
| Ctx: c, | ||
| ModelName: modelName, | ||
| TokenGroup: usingGroup, | ||
| RequestPath: c.Request.URL.Path, | ||
| Retry: common.GetPointer(retry), | ||
| }) | ||
| if err != nil || next == nil || tried[next.Id] { | ||
| return nil | ||
| } | ||
| tried[next.Id] = true | ||
| if SetupContextForSelectedChannel(c, next, modelName) == nil { | ||
| return next | ||
| } | ||
| } | ||
| } |
| <Input | ||
| value={pattern.regex} | ||
| placeholder={t('Regex with (?P<reset>...) group')} | ||
| placeholder={t('Regex with optional (?P<reset>...) group')} | ||
| className='font-mono text-sm' | ||
| onChange={(e) => updatePattern(index, { regex: e.target.value })} | ||
| aria-invalid={!validation.valid} |
- 降级重选不再因随机重复渠道而提前放弃:同层内继续重试,仅当尝试次数 明显超过已见渠道数仍无新渠道时终止(PR #14 评论 2) - 降级重选测试的 SQLite :memory: fixture 限制单连接,避免多连接各自独立 内存库导致的偶发失败(PR #14 评论 3) - 限额模式编辑器在正则含 (?P<reset>) 捕获组而时间格式为空时给出警告 (后端会忽略捕获的时间戳,回退周期/分钟数)(PR #14 评论 4) - 评论 1(GetRootUser 在无 root 用户时返回 nil)经实测不成立:GORM 在 record-not-found 时分配零值结构体,ToBaseUser 不会 panic
There was a problem hiding this comment.
🟡 Changes recommended
The new distributor fallback helper can skip same-priority candidates due to retry-tier advancement on repeats, which can incorrectly fall through to lower priority even when a same-tier channel has an available key.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 27/27 changed files
- Comments generated: 2
- Review effort level: Lite
| for retry := 1; ; retry++ { | ||
| attempts++ | ||
| next, _, err := service.CacheGetRandomSatisfiedChannel(&service.RetryParam{ | ||
| Ctx: c, | ||
| ModelName: modelName, |
| db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) | ||
| if err != nil { | ||
| panic("failed to open test db: " + err.Error()) | ||
| } | ||
| // :memory: 下每个连接是独立库,单连接保证所有读写落在同一份数据上 | ||
| sqlDB, err := db.DB() | ||
| if err != nil { | ||
| panic("failed to get sql.DB: " + err.Error()) | ||
| } | ||
| sqlDB.SetMaxOpenConns(1) | ||
| model.DB = db | ||
| common.SetDatabaseTypes(common.DatabaseTypeSQLite, common.DatabaseTypeSQLite) | ||
| if err := db.AutoMigrate(&model.Channel{}, &model.Ability{}); err != nil { | ||
| panic("failed to migrate test db: " + err.Error()) | ||
| } | ||
| } | ||
| require.NoError(t, model.DB.Exec("DELETE FROM abilities").Error) | ||
| require.NoError(t, model.DB.Exec("DELETE FROM channels").Error) | ||
|
|
||
| memoryCacheEnabled := common.MemoryCacheEnabled | ||
| common.MemoryCacheEnabled = true | ||
| t.Cleanup(func() { | ||
| common.MemoryCacheEnabled = memoryCacheEnabled | ||
| model.InitChannelCache() | ||
| }) |
No description provided.