Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Apeuriox/lazybot-renewal/llms.txt

Use this file to discover all available pages before exploring further.

Lazybot applies rate limiting at three independent layers. The first is a global HTTP interceptor that guards every incoming HTTP request based on the caller’s IP address, protecting the Spring Boot application itself from request floods. The second is a per-command token-bucket enforced inside the command chain, configurable per command class via a Java annotation. Both of these layers use the Bucket4j library. The third is the Shiro/OneBot WebSocket adapter’s own built-in event limiter, which throttles raw OneBot events before they ever reach the command router.

Layer 1 — Global HTTP Rate Limiter

RateLimitInterceptor implements Spring’s HandlerInterceptor and runs as a preHandle check on all HTTP requests. It maintains a ConcurrentHashMap<String, Bucket> keyed by the caller’s remote IP address, creating a new bucket on first contact. Each per-IP bucket is configured with:
  • Capacity — the maximum token count and burst ceiling.
  • Refill — tokens added greedily every 1 minute.
When the bucket for an IP is exhausted, the interceptor returns HTTP 429 with the body:
{"code": 429, "msg": "oops, too many requests", "data": null}
The interceptor is disabled entirely when rate-limit.enabled is false.

Configuration

rate-limit:
  capacity: 24   # burst ceiling — maximum tokens held at any time
  refill: 8      # tokens added per minute (greedy refill)
  enabled: true  # set to false to disable the HTTP-layer limiter
These three keys are injected directly into RateLimitInterceptor via @Value. There is no hot-reload; a restart is required after changing them.

Layer 2 — Per-Command Rate Limiter

RateLimitHandler is the third link in the command chain (@Order(2)). It reads the @LazybotRateLimit annotation from the command class at runtime. If the annotation is absent, the handler is a no-op and passes control through immediately. When the annotation is present, the handler constructs a bucket key from the configured scope and calls LazybotCommandRateLimitManager.tryConsume(key, rateLimit). The manager keeps its own ConcurrentHashMap<String, Bucket> and uses Bucket.refillIntervally (not greedy) so tokens are added in discrete pulses rather than continuously.

Scope and Bucket Keys

ScopeKey formatEffect
USERuser:<userId>:cmd:<commandType>Each user has their own independent bucket for this command
CHANNELchannel:<groupId>:cmd:<commandType>All users in a group share one bucket for this command
GLOBALglobal:cmd:<commandType>One shared bucket across all users and groups

@LazybotRateLimit Fields

FieldTypeDefaultDescription
capacitylong(required)Maximum tokens in the bucket (burst size)
refillTokenslong(required)Tokens added per refill interval
refillPeriodlong(required)Duration of the refill interval
unitTimeUnitSECONDSTime unit for refillPeriod
scopeScope enumGLOBALUSER, CHANNEL, or GLOBAL

Annotating a Custom Command

import me.aloic.lazybot.annotation.LazybotCommandMapping;
import me.aloic.lazybot.annotation.LazybotRateLimit;
import me.aloic.lazybot.command.LazybotSlashCommand;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

// Allow each user to run this command at most 3 times, refilling
// 1 token every 30 seconds — effectively a 30-second per-use cooldown
// with a 3-request burst.
@LazybotRateLimit(
    capacity     = 3,
    refillTokens = 1,
    refillPeriod = 30,
    unit         = TimeUnit.SECONDS,
    scope        = LazybotRateLimit.Scope.USER
)
@LazybotCommandMapping({"mycommand"})
@Component
public class MyCommand implements LazybotSlashCommand {
    // ...
}
When tryConsume returns false on QQ, RateLimitHandler sends the group channel the message [Lazybot] 达到速率限制,请等待50秒 and stops the chain — the command body is not executed.

Layer 3 — Shiro Built-in Limiter

The Shiro/OneBot WebSocket adapter has its own separate limiter that throttles the number of incoming OneBot events before they even reach the command router. It is configured under the shiro.limiter key:
shiro:
  limiter:
    enable: true
    rate: 10       # sustained events per second allowed through
    capacity: 30   # burst capacity
    awaitTask: true
    timeout: 10    # milliseconds to wait before dropping a blocked task
This limiter operates independently from both Lazybot layers and is the first line of defence against high-volume event floods from the OneBot server.
For a public bot serving multiple QQ groups, consider the following starting points:
  • HTTP interceptor: capacity: 30, refill: 10 — allows occasional bursts while preventing scraping.
  • Image-rendering commands (e.g. /bp, /score, /card): capacity: 3, refillTokens: 1, refillPeriod: 20, unit: SECONDS, scope: USER — rendering is CPU-intensive; per-user scoping prevents one active user from starving others.
  • Lightweight commands (e.g. /setmode, /link): no @LazybotRateLimit annotation needed; the HTTP interceptor alone is sufficient.
  • Shiro limiter: rate: 10, capacity: 30 is sensible for a single-server deployment.
Always monitor JVM CPU and memory after tuning — Resvg rendering is the dominant cost, not the token-bucket arithmetic.

Build docs developers (and LLMs) love