news 2026/9/24 14:08:20

Fluxer 限流机制深度解析:路由桶、全局桶与限流响应契约

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Fluxer 限流机制深度解析:路由桶、全局桶与限流响应契约

【免费下载链接】fluxer

A free and open source instant messaging and VoIP chat app built for friends, groups, and communities.

项目地址:https://gitcode.com/gh_mirrors/flu/fluxer
点击查看免费下载

Fluxer 是一个面向朋友、群组与社区的开源即时通讯与 VoIP 应用,其 HTTP API 通过逐路由限流桶(per-route bucket)全局限流桶(global bucket)双层结构约束流量。本文以 fluxer_docs/src/content/docs/topics/rate-limits.md 为骨架,结合 限流中间件、限流配置聚合 与 限流服务实现 等源码,完整讲解桶的作用域规则、429 响应体、X-RateLimit-*响应头、处理器内限流(handler-level limits)与慢速模式(slowmode),帮助你在接入或运维 Fluxer API 时准确理解并优雅处理限流。

Buckets 与作用域(Buckets and scope)

Fluxer 中,一个 bucket 就是在一个时间窗口(window)内对一个允许额度(allowance)的计数。几乎所有路由都声明自己的 bucket;超出额度的请求会被拒绝,返回:

  • 状态码429
  • codeRATE_LIMITED
  • 携带一个限流响应对象;
  • 携带限流响应头。

路径参数占位符:每个资源独立额度

bucket 名称可以包含路径参数占位符,写法是冒号加参数名,例如guild:emojis:list::guild_id。Fluxer 在键控(keying)前会用请求中的实际值替换占位符,因此同一个路由对每个路径资源持有独立额度

这一点在源码中有直接体现:中间件中的resolveBucket遍历请求路径参数,逐一替换 bucket 字符串中的:参数名占位符(见 RateLimitMiddleware.ts):

function resolveBucket(bucket: string, clientId: string, ctx: Context<HonoEnv>): string { let resolved = bucket; const params = ctx.req.param(); for (const [key, value] of Object.entries(params)) { resolved = resolved.replace(`:${key}`, String(value)); } return `${clientId}:${resolved}`; }

替换后的 bucket 还会以调用者身份为前缀进行键控(见下文)。

调用者身份键控:认证、Bot、Bearer 与 IP

每个 bucket 都会按调用者身份键控:

  • 已认证请求:按账号(account)与凭证类型(credential kind)键控;
  • OAuth2 bearer 凭证:额外按所属应用(owning application)键控;
  • 因此,会话(session)、机器人令牌(bot token)、Admin API 密钥以及每个 bearer 应用,对同一个账号会各自占用独立的额度。

源码中getClientIdentifier精确实现了这一规则(RateLimitMiddleware.ts):

if (user?.id) { const tokenType = ctx.get('authTokenType') ?? 'session'; if (tokenType === 'bearer') { return `user:${user.id}:bearer:${ctx.get('oauthBearerApplicationId') ?? 'unknown'}`; } return `user:${user.id}:${tokenType}`; }
  • 未解析出账号的请求:按客户端 IP 键控,IPv4 使用精确地址,IPv6 使用/64前缀,因此同一/64网段内的客户端会共享额度。源码中getSameIpDecisionKey负责将 IP 归一化到决策键。
  • 若部署配置为从某个请求头读取地址而请求没有携带该头,Fluxer 会在评估任何 bucket之前直接以403 FORBIDDEN拒绝该请求。

全局桶:共享的全局额度

  • 请求还会计入全局 bucket(除非其路由 bucket 声明为豁免);
  • 全局桶按同一身份键控,因此未解析出账号的请求会消耗其客户端 IP 的全局额度;
  • 中间件中,checkGlobalLimit(clientId, globalLimit)在任何路由桶检查之前执行(RateLimitMiddleware.ts)。

豁免路由:只消费自己的额度

以下 bucket 是豁免的,且是各自路由声明的唯一bucket,它们不消耗任何全局额度

  • webhook:execute::webhook_id
  • webhook:message_get::webhook_id
  • webhook:message_edit::webhook_id
  • webhook:message_delete::webhook_id
  • webhook:github::webhook_id
  • webhook:instatus::webhook_id
  • stripe:webhook

源码在 WebhookRateLimitConfig.ts 与 IntegrationRateLimitConfig.ts 中确认了这些配置,例如webhook:execute::webhook_id{limit: 60, windowMs: ms('1 minute'), exemptFromGlobal: true}stripe:webhook{limit: 300, windowMs: ms('1 minute'), exemptFromGlobal: true}

此外,user:group_dm:createuser:group_dm:recipient:add两个 bucket 也是豁免的,但它们是某路由上的第二个 bucket(第一个 bucket 不豁免),因此这两个路由仍然会消耗全局额度。源码中可见user:group_dm:create配置为{limit: 10, windowMs: ms('1 hour'), exemptFromGlobal: true}(UserRateLimitConfig.ts)。

全局窗口与默认额度

  • 全局窗口为1 秒
  • 默认额度为每秒 50 次请求
  • 持有HIGH_GLOBAL_RATE_LIMIT账号标记(account flags 文档见 fluxer_admin 的账号标记实现)的账号获得每秒 1200 次
  • 持有RATE_LIMIT_BYPASS标记的账号豁免于全局桶与所有路由桶,其成功响应不携带任何限流头。

上述常量在源码中都有对应实现(RateLimitMiddleware.ts):

if (user?.flags && (user.flags & UserFlags.HIGH_GLOBAL_RATE_LIMIT) !== 0n) { return 1200; } return 50;

同时,RateLimitServiceDEFAULT_GLOBAL_WINDOW_MS = 1000(RateLimitService.ts)也印证了 1 秒全局窗口。

两个容易混淆的注意点

:::note[An allowance drains continuously] 每个 bucket 都是漏桶(leaky bucket):一次性最多允许声明的额度,并在每个声明窗口内以该额度持续补充(refill)。因此,客户端耗尽额度后,只要桶中补充出足够额度即可再次发送。 :::

:::note[The headers and the body report different instants]X-RateLimit-ResetX-RateLimit-Reset-After报告的是桶完全排空的时刻;而响应体中的retry_after报告的是再次准入一个请求所需等待的较短延迟。 :::

:::note[A 429 can hide a 401 or 403] 一个被限流的请求即使其凭证本应被以401403拒绝,也可能返回429。测试 GlobalRateLimitRevokesSession.test.ts 正好验证了这一点:会话被全局限流吊销后,再次请求返回 401UNAUTHORIZED,而非 429。 :::

第二个路由桶与计数时机

部分路由声明了第二个路由桶,包括群组私信创建(group DM creation)、添加群组成员(adding group recipients)以及删除 guild 表情/贴纸(deleting guild emoji or stickers)。Fluxer 在处理器运行之前就对请求进行路由桶计数,所以被处理器拒绝的请求仍然会消耗额度

警告:全局拒绝会吊销用户会话

:::caution[A global denial revokes a user session] 当全局桶拒绝了由非 Bot 账号的用户会话令牌认证的请求时,Fluxer 会在写回 429之前吊销该令牌,客户端必须重新认证。Bot 令牌、OAuth2 访问令牌与 Admin API 密钥绝不会被这样吊销;路由桶拒绝也绝不会吊销任何凭证。 :::

源码中的revokeAuthenticatedSessionOnGlobalRateLimit精确实现了这一行为:仅当authTokenType === 'session'且用户非 Bot 时调用AuthSession.revokeToken(RateLimitMiddleware.ts),并由测试 GlobalRateLimitRevokesSession.test.ts 验证。

通过实例配置整体关闭

部署可以通过实例配置同时禁用两类桶。禁用期间:

  • 没有任何响应携带X-RateLimit-*头;
  • 不会有请求以429 RATE_LIMITED被拒绝;
  • 该开关同时关闭登录额度(login allowances);
  • 其他所有处理器内限流仍然生效。

中间件中shouldEnforceRateLimits读取Config.dev.disableRateLimits(RateLimitMiddleware.ts),测试模式下还支持通过x-fluxer-test-enable-rate-limits请求头开启、x-fluxer-test-global-rate-limit覆盖全局额度,便于测试验证。

限流响应对象(Rate limit response object)

拒绝响应体包含普通错误响应的全部成员,并额外增加两个成员。

结构

字段类型描述
code1stringbucket 拒绝时为RATE_LIMITED
message2string本地化的限流提示消息
globalboolean拒绝是否由全局桶产生;路由桶拒绝时存在且为false
retry_after3number距离下一个请求被准入的延迟(小数秒)

1在路由桶中间件之外强制执行的限流可以复用该响应体但使用自己的 code。Send phone verification 是目前唯一活跃的例子,返回PHONE_RATE_LIMIT_EXCEEDED

2使用为请求解析出的本地化(locale),账号设置优先于 Accept-Language。

3永不低于 0.001;当没有计算出小数延迟时,回退为整秒的Retry-After值。RateLimitServiceretryAfterDecimal: result.retryAfterMs > 0 ? Math.max(0.001, retryAfterDecimal) : undefined正是这一规则的实现(RateLimitService.ts)。

Resend IP authorisation 的冷却期以自己的 code 和不同的响应体回答 429,具体内容见下文回答 429 的额度。慢速模式(slowmode) 的拒绝则回答400 SLOWMODE_RATE_LIMITED

示例

{ "code": "RATE_LIMITED", "message": "You're being rate limited.", "global": false, "retry_after": 0.428 }

测试 GlobalRateLimitRevokesSession.test.ts 验证了全局拒绝时json.global === trueX-RateLimit-Global头为trueX-RateLimit-Scopeglobal,且X-RateLimit-Bucket头为null

限流作用域(Rate limit scopes)

X-RateLimit-Scope头表示产生拒绝的作用域。

描述
user拒绝来自仅按调用者键控的额度
global拒绝来自全局桶
shared1拒绝来自多个账号可相互耗尽的额度

1没有任何路由桶声明自己的作用域,所以每个路由桶拒绝都报告user。Send phone verification 是唯一现实存在的shared来源:当每个号码的发送额度或号码级 provider 冷却产生拒绝时,两者都按提交的号码键控,因此两个账号向同一个号码发送会共享额度。

在中间件实现中,路由桶拒绝使用routeConfig.scope ?? 'user',email bucket 拒绝使用scope: 'shared'(RateLimitMiddleware.ts)。

限流响应头(Rate limit headers)

这些头描述一次限流决策。返回 429 的操作会携带这组头

字段类型描述
Retry-After?1string距下一次请求被准入的延迟(整秒,向上取整,永不小于 1)
X-RateLimit-Scope?1string产生拒绝的限流作用域
X-RateLimit-Global?2string全局 HTTP 429 时的字面值true
X-RateLimit-Limit?3string路由额度一次性最多允许的请求数
X-RateLimit-Remaining?3string路由额度仍允许的请求数,拒绝时恒为 0
X-RateLimit-Reset?34string路由额度完全恢复可用的 Unix 秒级时间戳
X-RateLimit-Reset-After?35string路由额度完全恢复可用还需的秒数
X-RateLimit-Bucket?36string路由桶的稳定 16 位十六进制标识符

1在所有限流拒绝时发送,且绝不会出现在成功响应上。

2仅在全局桶产生拒绝时发送,此时不发送任何路由桶头

3在路由 HTTP 429 时发送(X-RateLimit-Bucket受注 6 约束)。在成功响应上,仅当凭证解析为Bot 账号、或请求未解析出账号但位于同时具备webhook_idtoken路径参数的路由时才会发送。

4值若早于或等于当前秒,会被替换为下一秒。

5成功响应上四舍五入到毫秒精度并去除尾部零;拒绝时输出精确计算的小数。

6是路由 bucket 的不透明标识符,不代表调用者

从源码看,setRateLimitHeaders通过createHash('sha256').update(bucket).digest('hex').slice(0, 16)生成桶哈希(RateLimitMiddleware.ts),与测试中对X-RateLimit-Bucket的断言完全一致。

处理器内强制执行的限流返回 429 时携带其他路由头但没有X-RateLimit-Bucket

:::note[Cross-origin clients cannot read rate-limit headers] 跨域策略(cross-origin policy) 不会暴露这些头。请改用响应体中的retry_after值。 :::

处理器内限流(Limits enforced inside a handler)

处理器内强制的额度独立于路由桶与全局桶键控,因此即使两类桶仍有空间,耗尽该额度也会拒绝请求。以下是完整清单。

disable_rate_limits部署开关会连同两类桶一起关闭登录额度;relax_registration_rate_limits关闭注册额度。其余所有额度在所有部署上都会强制执行。

拒绝有两种形态:

  • 回答 429 的额度:携带限流响应对象与限流头(减去X-RateLimit-Bucket);
  • 回答 400 的额度:回答400 INVALID_FORM_BODY,并附带一个code指向被耗尽额度的校验错误条目。

400 形态没有retry_after成员、X-RateLimit-*头与Retry-After头,剩余延迟只出现在条目的本地化message中。

回答 429 的额度(Allowances answering 429)

操作额度错误码
Log in with a password每 15 分钟 5 次,按提交的邮箱地址键控RATE_LIMITED
Log in with a password每 30 分钟 10 次,按客户端 IP 键控(IPv4 精确、IPv6 按/64RATE_LIMITED
Register an account每 15 分钟 3 次,按提交的邮箱键控RATE_LIMITED
Register an account每小时 3 次,按客户端 IP 键控RATE_LIMITED
Register an account每小时 15 次,按客户端子网键控(IPv4/24或 IPv6/48RATE_LIMITED
Resend email verification每 15 分钟 3 次,按账号存储的邮箱键控RATE_LIMITED
Request password recovery每 30 分钟 20 次,按客户端 IP 键控RATE_LIMITED
Request password recovery每 30 分钟 5 次,按提交的邮箱键控RATE_LIMITED
Start email change 与 Resend original email code每 15 分钟 3 次发送,按已认证账号键控RATE_LIMITED
Request new email、Resend new email code 与两种 bounced email recovery 发送每 15 分钟 5 次发送,按已认证账号键控RATE_LIMITED
Start password change每 15 分钟 3 次发送,按已认证账号键控RATE_LIMITED
Resend password change code每 15 分钟 3 次发送,按已认证账号键控RATE_LIMITED
邮箱/密码变更 ticket 上的每次 code 重发与每次新地址请求每 30 秒 1 次发送,按 ticket 键控并从其上一次发送起算RATE_LIMITED
Report message、Report user、Report guild 与 Create DSA report每小时 5 次,按举报人键控(账号或已验证邮箱)RATE_LIMITED
Report message每小时 3 次,按举报人与频道共同键控RATE_LIMITED
Report message每小时 20 次,按被举报消息键控,跨所有举报人RATE_LIMITED
Report message每小时 4 次,按举报人与 guild 共同键控(guild 消息)RATE_LIMITED
Send phone verification每 6 小时 3 次,按已认证账号键控PHONE_RATE_LIMIT_EXCEEDED
Send phone verification每 5 天 3 次,按提交的号码键控PHONE_RATE_LIMIT_EXCEEDED
Resend IP authorisationticket 签发后 30 秒内无任何操作,按授权 ticket 键控IP_AUTHORIZATION_RESEND_COOLDOWN

SMS provider 节流可能施加额外冷却,返回PHONE_RATE_LIMIT_EXCEEDED并携带剩余延迟。

Resend IP authorisation 冷却没有X-RateLimit-*头,只有整秒的Retry-After头,且响应体以顶层resend_available_inretry_after再次报告该延迟。同一 ticket 上的第二次重发返回400 IP_AUTHORIZATION_RESEND_LIMIT_EXCEEDED。该额度永不补充,且 ticket 在签发 15 分钟后过期。

回答 400 的额度(Allowances answering 400)

操作额度校验错误码
Modify current user结果用户名或判别符每 3 小时 5 次USERNAME_CHANGED_TOO_MANY_TIMES
Modify current user个人简介每 30 分钟 25 次(提交值不同时)BIO_CHANGED_TOO_MANY_TIMES
Modify current user代词每 30 分钟 25 次(提交值不同时)PRONOUNS_CHANGED_TOO_MANY_TIMES
Modify current user强调色每 30 分钟 25 次(提交值不同时)ACCENT_COLOR_CHANGED_TOO_MANY_TIMES
Modify current user任何非 null 头像每 30 分钟 25 次AVATAR_CHANGED_TOO_MANY_TIMES
Modify current user通过权益检查后的任何横幅值每 30 分钟 25 次BANNER_CHANGED_TOO_MANY_TIMES
Update bot profileBot 结果用户名或判别符每 3 小时 5 次USERNAME_CHANGED_TOO_MANY_TIMES
Modify current guild memberguild 头像每 30 分钟 25 次(无论何时提供)AVATAR_CHANGED_TOO_MANY_TIMES
Modify current guild memberguild 横幅每 30 分钟 25 次(无论何时提供)BANNER_CHANGED_TOO_MANY_TIMES
Modify current guild memberguild 个人简介每 30 分钟 25 次(提交值不同时)BIO_CHANGED_TOO_MANY_TIMES
Modify current guild memberguild 代词每 30 分钟 25 次(提交值不同时)PRONOUNS_CHANGED_TOO_MANY_TIMES
Modify current guild memberguild 强调色每 30 分钟 25 次(提交值不同时)ACCENT_COLOR_CHANGED_TOO_MANY_TIMES
Modify voice activity sharing分享默认值每 24 小时 1 次VOICE_ACTIVITY_SHARING_ON_COOLDOWN
Complete login with TOTP 与 Complete login with WebAuthn MFA每 15 分钟 10 次多因素尝试INVALID_CODE
Complete login with TOTP 与 Complete login with WebAuthn MFA单个 MFA ticket 上每 5 分钟 5 次多因素尝试INVALID_CODE
Sudo mode(totp方法)每 15 分钟 10 次多因素尝试INVALID_MFA_CODE

键控归属:每个 Modify current user 额度按已认证账号键控,Bot 标签额度按 Bot 账号键控——因此所有者修改 Bot 标签会消耗 Bot 的额度。guild 成员额度按guild 与成员共同键控,一个账号在每个 guild 中持有独立额度。登录额度分别按账号与 MFA ticket 键控,sudo 额度按账号键控。

关键行为:Fluxer 在检查 code 之前就消耗每个多因素额度,因此用正确 code 撞上已耗尽额度时,报告结果与错误 code 完全一致。正确 code 会清空计数器。ticket 额度在拒绝时还会销毁 MFA ticket,客户端需从 Log in with a password 重新开始。

两种形态都不回答的额度(Allowances answering neither shape)

  • Get desktop handoff information 与 Complete desktop handoff共享一个按客户端 IP 键控的失败尝试计数器:5 次失败会让两个操作被阻塞 15 分钟(自最近一次失败起算),被阻塞的请求返回400 INVALID_HANDOFF_CODE(顶层 code,无校验条目)。Get desktop handoff information 还单独允许每个 handoff code 进行 3 次成功查询,第 4 次同样返回该顶层 code。
  • Refund latest purchase允许每个账号每 30 天一次自助退款,窗口内的请求返回403 STRIPE_REFUND_COOLDOWN_ACTIVE
  • Slowmode同样在处理器内强制执行。

慢速模式(Slowmode)

慢速模式限制一个账号在一个频道内发送消息的频率。Fluxer 将拒绝报告为普通请求失败:被拒绝的发送返回400 SLOWMODE_RATE_LIMITED,携带顶层小数秒retry_after与整秒Retry-After头。响应没有X-RateLimit-*,客户端可通过状态码与 code 将其与桶拒绝区分。

  • 额度为频道在rate_limit_per_user中配置的每个时间间隔一条消息,按每个账号与频道对分别计数;
  • 仅对在 guild 频道中发送、且配置间隔大于零非 Bot 账号计数;
  • 持有 BYPASS_SLOWMODE 权限的调用者豁免;
  • Get channel slowmode state 会报告调用者在尝试发送前的剩余延迟。

源码中,消息发送服务在发送前检查慢速模式:slowmodeKey = \slowmode:${channelId}:${user.id}`,并通过checkLimit检查([MessageSendService.ts](https://link.gitcode.com/i/e9cbedf17c4f07152c19798637a875fc));频道接口也暴露了get_channel_slowmode_state([ChannelController.ts](https://link.gitcode.com/i/ada8d5d0528cde497fe9e15a5bc82d7c))。测试 [SlowmodeEnforcement.test.ts](https://link.gitcode.com/i/69c8645d3876eef2b8ce1367836c5e1e) 验证了发送成功后才消耗慢速额度、以及超限返回400 SLOWMODE_RATE_LIMITED` 的行为。

其他协议面(Other surfaces)

每个协议面都有独立的限流契约:

  • 主 Gateway:其会话、命令、重放(replay)、背压(backpressure)与准入(admission)限制见 Gateway limits and rate limits;
  • Media Proxy API:没有请求计数限流,通过并发度、负载与截止时间限制约束工作;
  • 上传中继(upload relay):每次传输都用会过期的上传 URL授权,并限制请求体大小。

小结:客户端与运维的实践要点

  1. 区分桶拒绝与处理器内拒绝:429 +X-RateLimit-*头 +retry_after是桶拒绝;400SLOWMODE_RATE_LIMITED、400INVALID_FORM_BODY(带指向额度的校验码)等是处理器内限流。
  2. 用响应体retry_after而不是响应头做退避:跨域客户端无法读取X-RateLimit-*头(见跨域策略)。
  3. 注意 429 会掩盖 401/403:凭证无效的请求也可能因限流返回 429,不要仅凭状态码判断认证失败。
  4. 全局拒绝会吊销用户会话:非 Bot 用户会话令牌被全局桶拒绝后必须重新认证;Bot 令牌与 OAuth2 令牌不受影响。
  5. 额度是持续补充的漏桶:耗尽后等待retry_after(准入一个请求)或X-RateLimit-Reset-After(完全恢复)即可继续。
  6. IP 与子网键控:同一 IPv6/64(或注册场景的/48子网)内的客户端共享额度,NAT/代理场景请留意共享影响。

围绕上述机制的完整配置聚合位于 RateLimitConfig.ts,它合并了 Auth、OAuth、User、Channel、Guild、Webhook、Admin 等 12 个模块的限流段;核心执行逻辑在 RateLimitMiddleware.ts,底层漏桶服务在 RateLimitService.ts,可供继续深入阅读。

【免费下载链接】fluxer

A free and open source instant messaging and VoIP chat app built for friends, groups, and communities.

项目地址:https://gitcode.com/gh_mirrors/flu/fluxer
点击查看免费下载
上一篇:【告别Python依赖】SmartJavaAI:Java开发者的离线AI算法工具箱完全指南
下一篇:【限时免费】 SmartJavaAI 人脸识别技术详解与实战指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 14:08:18

【Dv3admin】ORM数据库无法查询的问题

Django 运行过程中,数据库连接的健康状态直接影响应用的稳定性和数据访问准确性。长时间空闲的数据库连接经常因外部机制被回收,进而引发数据查询异常和返回无效结果。 本文围绕 Django 中数据库连接长时间空闲导致的连接失效问题,介绍相关的背景成因,并给出配置与中间件层…

作者头像 李华
网站建设 2026/9/24 14:07:12

Wand-Enhancer:5分钟免费解锁WeMod专业版

Wand-Enhancer&#xff1a;5分钟免费解锁WeMod专业版 【免费下载链接】Wand-Enhancer Advanced UX and interoperability extension for Wand (WeMod) app 项目地址: https://gitcode.com/GitHub_Trending/we/Wand-Enhancer 每次在 WeMod 里点开 Pro 功能&#xff0c;都…

作者头像 李华
网站建设 2026/9/24 14:06:28

给Cursor和Claude Code装上长期记忆:agent-memory通过MCP接入的完整教程

给Cursor和Claude Code装上长期记忆&#xff1a;agent-memory通过MCP接入的完整教程 【免费下载链接】agent-memory Memory 是一款面向 AI 智能体的长期记忆模块&#xff0c;为运行在 openJiuwen 框架上的智能体提供记忆提取、存储、检索与迁移能力。 项目地址: https://gitc…

作者头像 李华
网站建设 2026/9/24 14:06:25

palera1n 三步完成老设备越狱:A8 到 A11 芯片完整上手指南

palera1n 三步完成老设备越狱&#xff1a;A8 到 A11 芯片完整上手指南 【免费下载链接】palera1n Jailbreak for A8 through A11, T2 devices, on iOS/iPadOS/tvOS 15.0, bridgeOS 5.0 and higher. 项目地址: https://gitcode.com/GitHub_Trending/pa/palera1n 手边躺着…

作者头像 李华