Commit Graph

4 Commits

Author SHA1 Message Date
anonsaber bc68c6b241 docs: unblock GATE-DOCS (debug.rs anchors drifted + file over 500 lines)
debug.rs grew past 500 lines with no SPLIT-EVAL marker, and the seven
/src/debug/* anchors in deployment.md pointed at the blank line just
above each function - so GATE-DOCS was red on two unrelated pre-existing
counts (SIZE CHECK, ANCHOR AUDIT) while the other four checks were green.

Re-anchor all seven to the function bodies they describe, add the marker
with the reason we are keeping the module whole, and document the
redis_failure_detail classification that deployment.md still called
"redis_probe_failed". Bump the GATE-P0 baseline to 88.
2026-09-30 12:17:04 +08:00
anonsaber 1353745f58 fix: let the Redis connection reconnect instead of dying with the process
RedisState built one MultiplexedConnection at startup and kept it for the
life of the process; every call cloned the handle, which clones the handle
and not the socket. When the underlying TCP connection dropped -- an Upstash
idle timeout, a network blip, an idle-connection cutoff -- the handle was
permanently dead. The process kept running, every Redis read failed, and
only a restart recovered it. That is what had to be restarted.

The crate says so in its own docs: redis 0.27.6 lib.rs:71 recommends
ConnectionManager "for automatic reconnections" behind the
connection-manager feature, which we had never enabled.

RedisState now holds a ConnectionManager built from the Client. Both types
derive Clone and implement ConnectionLike, so the five query_async call
sites are untouched; the three helper signatures that took a cloned
connection by value or by mutable reference change type only.
MultiplexedConnection no longer appears anywhere in src/.

Start-up semantics are unchanged: ConnectionManager::new awaits
new_connection before returning, so an unreachable Redis still fails closed
at boot rather than succeeding and erroring on first use. Reconnect is gated
on is_io_error(), which is exactly kind() == IoError. A dropped socket is an
IoError, so it reconnects with exponential backoff and jitter (base 2,
100ms factor, 6 retries); an authentication failure is AuthenticationFailed
and is deliberately not retried, because that error does not fix itself.

No Drop or cancel is needed: the live connection sits behind
Arc<ArcSwap<SharedRedisFuture>>, the Client is retained for rebuilding, and
no detached control task is spawned, so nothing leaks when RedisState is
dropped and the existing #[derive(Clone)] still holds.

/debug/redis collapsed every failure into the single string
"redis_probe_failed", which is why the real error was invisible and the
outage took a session to trace. StateError::Redis carries the underlying
RedisError, so the class is readable here: authentication_failed,
io_error, busy_loading, client_error, response_error, plus
state_lock_poisoned and encryption_failed. ErrorKind is #[non_exhaustive],
so the match keeps a fallback arm for future variants.

No NotReachable variant exists in redis 0.27.6 -- a network outage
surfaces there as IoError or ClientError -- so the classifier does not
mention one.

Four new transitive dependencies come in with the feature: backon 1.6.0,
fastrand 2.4.1, futures 0.3.32, futures-executor 0.3.32.

Not covered: there is no test for the automatic reconnect path, which would
require tearing down a real TCP connection mid-flight. /debug/redis still
probes through is_enabled() rather than issuing a bare PING.

Gates: rust fmt clean, clippy --all-targets -D warnings clean,
cargo test 87 passed 2 ignored, unchanged from the previous baseline.
2026-09-30 09:13:38 +08:00
anonsaber 0cee50b4c9 feat: read business config back and accept partial patches
GET /api/business-config returns the stored values with presence-only secrets, and PUT now merges a patch against the stored config instead of replacing it, so the SPA can show what is already set and send back only the fields that changed.

The SPA marks secrets as set-but-unreadable, edits chat_allowlist as an unordered set, and omits fields it did not touch; blank required fields keep the stored value, blank optional fields clear it. The read-back is Serialize-only, so a client cannot inject secrets_present.

Forward both paths at the gateway: /api/business-config accepted PUT only, so the read-back returned 405, and the preflight path was missing from the safelist entirely, so it returned 404. The SPA is served from the Worker domain, so the whole edit experience was unreachable behind the production gateway. Route methods are now asserted per path, which the safelist test never checked.

Also: /debug/config reports timezone again, the timezone select is styled like the other fields, and the docs follow the new contract.
2026-09-30 00:53:58 +08:00
anonsaber afa6cec8ef debug: 新增 /debug/* 远程联调面,双因子门禁默认绝对关闭(01a0e319)
新增 src/debug.rs:/debug/ping、/config、/redis、/jmap、/telegram、/worker 六个只读
探针 + POST /debug/notify 一条测试出站消息。

SAF-DEBUG-GATE 双因子,缺任一即完全不挂载路由(请求落到通用 404,无"未配置即放行"
降级):
- 启动命令带 --debug(main.rs 最小 std::env::args 解析,未新增依赖)
- Secret 环境变量 DEBUG_TOKEN 非空

两层执行点:main.rs 推导 debug_token 并传入 router builder(None 则 debug_router 不被
merge);debug.rs 侧 auth_token 缺失时直接返回空 Router。

SAF-DEBUG-AUTH:每个 handler 先做 Authorization: Bearer DEBUG_TOKEN 常数时间比较
(复用 worker_authorized),失败 401,鉴权通过前不产生任何副作用。

探针只读侧全部复用现成实现,不另写一套:Redis 走 is_enabled,JMAP 走
probe_jmap_session,Telegram 走 probe_telegram_get_me,出站走唯一构造点
business_client(notify.rs 新增 pub(crate))到 send_text,/debug/notify 与
refresh_business_config 共用同一条客户端构造路径。

SAF-NO-SECRET-ECHO / SAF-LOG-PURITY:/debug/config 只回"是否配置"、计数与超时等
运行时参数,bot token / JMAP password / worker / webhook / reconcile token / LLM key
一律不出现在响应体;DEBUG_TOKEN 只在鉴权内部暴露,不回显、不入日志,日志仅一条
"debug endpoints enabled" 且不含令牌值。allowlist 用 chat_allowlist_snapshot 取生效值
(运行时业务配置优先、否则启动值),/debug/notify 显式 chat_id 不在 allowlist 则 403。

ARCH-LB-WORKER 白名单不含 /debug/*,该面仅供直连 origin 联调,公网入口不可达。

AGENTS.md 登记 4 个稳定 ID:MOD-DEBUG / SAF-DEBUG-GATE / SAF-DEBUG-AUTH /
REQ-DEBUG-ENDPOINTS(§2 第 19 条硬边界、§5 测试条、§7 索引 4 行)。

调试路由测试(4 项,notify.rs + debug.rs 内):Bearer 放行 200、缺失/错误令牌 401、
未提供 DEBUG_TOKEN 因子时 /debug/* 完全不注册而落 404、/debug/notify POST 未认证 401。

门禁四段全绿(rust:1-slim-bookworm):cargo fmt --all -- --check / cargo check --locked
/ cargo clippy --locked --all-targets -- -D warnings / cargo test --locked =
49 passed / 0 failed / 1 ignored。
2026-09-28 15:00:49 +08:00