32b582aac8
Task 9: 服务层对全局 settings 的业务引用清零,每个 DEFAULT_CONFIG 组 都有真实消费方。resolve_*_settings(email/push/ai)以空槽回退 env (平台默认层);contact 行为开关/种子白名单/国名扩表/限流上限经 manager 的 runtime.config 接线。收口: lead_push session_factory 必填、 config.py 死代码清理、PromptProvider frozen、no_contact 视图直取、 AIService wall 参数定型 CompiledWall。403 测试全绿(389+14)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
44 lines
1.5 KiB
Python
44 lines
1.5 KiB
Python
"""Minimal in-memory rate limiter (per (tenant, ip, device_id), rolling 1h window).
|
|
|
|
Kept aligned with the reference implementation's rate_limit.py shape. Only the chat message route uses this;
|
|
session/history endpoints are cheap and not throttled. State is process-local
|
|
and resets on restart, which is acceptable for a single-worker ai-kefu deploy.
|
|
Task 9: 桶键加租户维度——多租户同 IP(共享出口 NAT/公司网络)不互相挤占
|
|
配额;limit 经 config.engine.rate_limit_per_hour 逐租户可调(消费点
|
|
chat_rest, 默认 DEFAULT_MAX)。
|
|
|
|
Caveats:
|
|
- Single-process, in-memory. Under `uvicorn --workers >1` each worker keeps
|
|
its own `_buckets`, so the effective cap is `max_requests * workers`.
|
|
- Keyed on (tenant, ip, device_id); a client rotating the `X-Device-Id` header per
|
|
request bypasses it. This is a light safeguard against accidental GLM-cost
|
|
burn, NOT abuse-proof.
|
|
"""
|
|
import time
|
|
from collections import defaultdict
|
|
|
|
_buckets: dict[tuple[str, str, str], list[float]] = defaultdict(list)
|
|
WINDOW = 3600
|
|
DEFAULT_MAX = 30
|
|
|
|
|
|
def check(
|
|
tenant: str | None,
|
|
ip: str | None,
|
|
device_id: str | None,
|
|
*,
|
|
max_requests: int = DEFAULT_MAX,
|
|
) -> bool:
|
|
"""Return True if the request is under the cap (and record it), else False."""
|
|
key = (tenant or "", ip or "", device_id or "")
|
|
now = time.time()
|
|
_buckets[key] = [t for t in _buckets[key] if t > now - WINDOW]
|
|
if len(_buckets[key]) >= max_requests:
|
|
return False
|
|
_buckets[key].append(now)
|
|
return True
|
|
|
|
|
|
def _reset_for_test() -> None:
|
|
_buckets.clear()
|