feat: 配置列表分页与钉钉机器人分钟级排队限流
统一 sources/templates/channels/rules 列表为分页响应,避免配置增多时全量返回;按钉钉 access_token 限制每分钟发送并在超限时等待下一分钟,降低触发官方封禁风险。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// TryIncr implements adapter.MinuteWindowStore for DingTalk per-minute quotas.
|
||||
func (c *Cache) TryIncr(ctx context.Context, key string, minute int64, limit int) (int64, bool, error) {
|
||||
if c == nil || c.rdb == nil {
|
||||
return 0, false, fmt.Errorf("redis unavailable")
|
||||
}
|
||||
redisKey := fmt.Sprintf("dingtalk:rl:%s:%d", key, minute)
|
||||
script := redis.NewScript(`
|
||||
local key = KEYS[1]
|
||||
local limit = tonumber(ARGV[1])
|
||||
local cur = tonumber(redis.call('GET', key) or '0')
|
||||
if cur >= limit then
|
||||
return {cur, 0}
|
||||
end
|
||||
local n = redis.call('INCR', key)
|
||||
if n == 1 then
|
||||
redis.call('EXPIRE', key, 120)
|
||||
end
|
||||
return {n, 1}
|
||||
`)
|
||||
res, err := script.Run(ctx, c.rdb, []string{redisKey}, limit).Slice()
|
||||
if err != nil {
|
||||
return 0, false, fmt.Errorf("dingtalk TryIncr: %w", err)
|
||||
}
|
||||
if len(res) != 2 {
|
||||
return 0, false, fmt.Errorf("dingtalk TryIncr: unexpected result %#v", res)
|
||||
}
|
||||
count, err := toInt64(res[0])
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
okFlag, err := toInt64(res[1])
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return count, okFlag == 1, nil
|
||||
}
|
||||
|
||||
func toInt64(v interface{}) (int64, error) {
|
||||
switch n := v.(type) {
|
||||
case int64:
|
||||
return n, nil
|
||||
case int:
|
||||
return int64(n), nil
|
||||
case string:
|
||||
var x int64
|
||||
_, err := fmt.Sscan(n, &x)
|
||||
return x, err
|
||||
default:
|
||||
return 0, fmt.Errorf("cannot convert %T to int64", v)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user