docs: reanchor the notify.rs and state.rs line refs the backend moved
The retire-on-change commit inserted ~500 lines into notify.rs and ~110
into state.rs, shifting every anchor at or after the insertion point.
The ANCHOR audit only proves the referenced line exists and is
non-empty, so all of these passed while pointing at the wrong code:
deployment.md/.zh-CN register_telegram_webhook 1619 -> 1623
register_push 1464 -> 1468 (EN)
register_telegram_webhook 1439 -> 1623 (zh)
register_push 1315 -> 1468 (zh)
retired.md/.zh-CN registration_lock fmt 1328-1330 -> 1481-1483
register_push 1315 -> 1468
state.rs read_batch 491 -> 506
state.rs query_async 531 -> 546
state.rs XGROUP CREATE 499-507 -> 514-522
opengaps.md/.zh-CN get_reconcile_state 777 -> 792
set_reconcile_state 785 -> 800
acquire_lock 1076 -> 1130
Every target was verified against the source before editing: two of the
pre-existing anchors pointed inside a different function than the one
they named, and the EN deployment.md had both register anchors off while
the zh twin still carried the original pre-move numbers.
Also documents the new push:registration:current key in the Redis key
table (writer, readers, TTL, and why its literal suffix cannot collide
with the hex session_digest suffix) and carries the zh twin the EN
version already got. Mirror the retire-on-change contract in
deployment.zh-CN.md - the zh twin has its own callback section, which
the EN update skipped.
This commit is contained in:
parent
2089a95b79
commit
baa6d8a6a1
|
|
@ -71,7 +71,7 @@ section in the configuration page. Enter the public HTTPS origin that receives c
|
|||
the Worker URL when the gateway is enabled, otherwise the backend origin. The page registers
|
||||
`/webhook/tg` with Telegram and `/push/jmap` with Stalwart. Credentials remain in the backend;
|
||||
the browser sends only the callback URLs. The protected registration handlers are
|
||||
`src/notify.rs:1619` for Telegram and `src/notify.rs:1464` for Stalwart.
|
||||
`src/notify.rs:1623` for Telegram and `src/notify.rs:1468` for Stalwart.
|
||||
|
||||
Telegram's secret token and `allowed_updates: ["message"]` are taken from the saved business
|
||||
configuration. Stalwart verification and verification-code writeback happen automatically
|
||||
|
|
|
|||
|
|
@ -64,9 +64,9 @@ Debian slim 运行时阶段;有关运行时内容,请参阅 [§8.2](#82-dock
|
|||
|
||||
## 4.1 注册 Telegram 和 Stalwart 回调
|
||||
|
||||
保存业务配置后,在配置页的“注册 Telegram 和 Stalwart 回调”区域操作。输入接收回调的公开 HTTPS origin:启用 Worker 时填写 Worker 地址,否则填写后端地址。页面会向 Telegram 注册 `/webhook/tg`,并向 Stalwart 注册 `/push/jmap`。凭据保留在后端,浏览器只提交回调 URL。Telegram 和 Stalwart 注册处理器分别位于 `src/notify.rs:1439` 与 `src/notify.rs:1315`。
|
||||
保存业务配置后,在配置页的“注册 Telegram 和 Stalwart 回调”区域操作。输入接收回调的公开 HTTPS origin:启用 Worker 时填写 Worker 地址,否则填写后端地址。页面会向 Telegram 注册 `/webhook/tg`,并向 Stalwart 注册 `/push/jmap`。凭据保留在后端,浏览器只提交回调 URL。Telegram 和 Stalwart 注册处理器分别位于 `src/notify.rs:1623` 与 `src/notify.rs:1468`。
|
||||
|
||||
Telegram 使用已保存的 webhook secret 和 `allowed_updates: ["message"]`。注册后,Stalwart 会自动完成验证回调和 verification-code 写回。可以安全地重复注册;更改回调 origin 会更新 Telegram webhook,并为新的 URL 创建 Stalwart subscription。
|
||||
Telegram 使用已保存的 webhook secret 和 `allowed_updates: ["message"]`。注册后,Stalwart 会自动完成验证回调和 verification-code 写回。可以安全地重复注册,重复注册同一 origin 是幂等的。更改回调 origin 会更新 Telegram(原子替换),对 Stalwart 则会在注册锁内先销毁旧订阅、再创建新订阅,因此旧 origin 不再收到推送。
|
||||
|
||||
## 5. 运行时配置
|
||||
|
||||
|
|
|
|||
|
|
@ -103,8 +103,8 @@ Single accounts are fixed by requirement (`REQ-SINGLE-ACCOUNT`) and never enter
|
|||
|
||||
**Minimum changes if necessary in the future** (The current situation has been verified in this round and will be used directly when disassembly is needed)
|
||||
|
||||
- `state:jmap:since` is a **single global cursor**, no account dimension (read `src/state.rs:777`, write `src/state.rs:785`) - both accounts will overwrite each other's cursors, which is the first place to move.
|
||||
- `lock:reconcile` is a **single global solo lock** (Redis implementation `src/state.rs:1076`) - the reconciliation of two accounts will be serialized by the same lock.
|
||||
- `state:jmap:since` is a **single global cursor**, no account dimension (read `src/state.rs:792`, write `src/state.rs:800`) - both accounts will overwrite each other's cursors, which is the first place to move.
|
||||
- `lock:reconcile` is a **single global solo lock** (Redis implementation `src/state.rs:1130`) - the reconciliation of two accounts will be serialized by the same lock.
|
||||
- `dedup:jmap:{account_id}:{email_id}` **Already has the account segment** (`docs/reference.md` §1), the key shape does not need to be changed.
|
||||
- The `ACCOUNT_ID` environment variable already exists and has been verified (leave it blank to take the session main account, and an error will be reported if the out-of-bounds value is `src/domain/jmap/client.rs:485`). The business configuration layer can also override `account_id` (`unwrap_or_else` of `src/config.rs:465` falls back). The configuration interface is ready, all that is missing is the sub-dimension status within the instance.
|
||||
|
||||
|
|
|
|||
|
|
@ -103,8 +103,8 @@ TTL 那条真机绿灯已经用你给的 Upstash URL 跑过了(见 §1),
|
|||
|
||||
**将来若真要做的最小改动面**(本轮核实过现状,供拆需求时直接用)
|
||||
|
||||
- `state:jmap:since` 是**单一全局游标**,无账户维度(读 `src/state.rs:777`、写 `src/state.rs:785`)——两个账户会互相覆盖游标,这是第一个要动的地方。
|
||||
- `lock:reconcile` 是**单一全局单飞锁**(Redis 实现 `src/state.rs:1076`)——两个账户的对账会被同一把锁串行化。
|
||||
- `state:jmap:since` 是**单一全局游标**,无账户维度(读 `src/state.rs:792`、写 `src/state.rs:800`)——两个账户会互相覆盖游标,这是第一个要动的地方。
|
||||
- `lock:reconcile` 是**单一全局单飞锁**(Redis 实现 `src/state.rs:1130`)——两个账户的对账会被同一把锁串行化。
|
||||
- `dedup:jmap:{account_id}:{email_id}` **已经带账户段**(`docs/reference.md` §1),键形无需改。
|
||||
- `ACCOUNT_ID` 环境变量已存在且经校验(留空取 session 主账户,越界值在 `src/domain/jmap/client.rs:485` 报错),业务配置层也能覆盖 `account_id`(`src/config.rs:465` 的 `unwrap_or_else` 回落)。配置面已就绪,缺的只是实例内的分维度状态。
|
||||
|
||||
|
|
|
|||
|
|
@ -143,16 +143,24 @@ duplicate. Source comment:
|
|||
| `push:subscription:id` | No EX | `remember_push_subscription_id` | current-subscription lookup |
|
||||
| `push:subscription:{id}:status` | 900 / 300 / 86_400 (min 1 enforced) | `set_push_subscription_status`; `pending` on verification request, `verified` on success, `disabled` on disable | **none** |
|
||||
| `push:registration:{sha256(callback_url)}` | EX 604_800 | `remember_push_subscription_for_callback`, called from `register_push` | `get_push_subscription_for_callback`; removed by `remove_push_subscription_for_callback` |
|
||||
| `push:registration:current` | EX 604_800 | `set_push_registration_current`, called from `register_push` | `get_push_registration_current`; cleared by `clear_push_registration_current`, and by `disable_push` via `clear_push_registration_current_if` |
|
||||
| `push:orphan:{subscription_id}` | EX 604_800 | `record_push_orphan` | orphan sweep |
|
||||
|
||||
**Two distinct keys that are frequently confused. Read this before editing either.**
|
||||
**Three distinct keys that are frequently confused. Read this before editing any of them.**
|
||||
|
||||
- `push:registration:{sha256(callback_url)}` is a **mapping from callback URL to
|
||||
subscription id**, TTL seven days. It is not a lock.
|
||||
- `push:registration:current` holds the **raw callback URL of the live subscription**, same
|
||||
seven-day TTL. It is written last, after the new subscription is created. Registering with a
|
||||
different URL destroys the previous subscription under the registration lock before creating
|
||||
the new one, so the old origin stops receiving pushes; `disable_push` clears it only when the
|
||||
disabled URL is the current one. Its suffix is the literal `current`, which cannot collide
|
||||
with the `session_digest` hex suffix of the per-URL key. It is neither a mapping nor a lock.
|
||||
- `lock:push-register:{sha256(callback_url)}` is the **single-flight registration lock**,
|
||||
TTL 360 s. It is not a mapping.
|
||||
|
||||
They use the same digest but are separate keys with unrelated lifetimes and purposes.
|
||||
The first two share the same TTL but are separate keys with unrelated purposes. The lock uses the
|
||||
same digest as the per-URL mapping yet is a different key entirely.
|
||||
|
||||
**Annotation: `push:subscription:{id}:status` is write-only by design.**
|
||||
The source carries an explicit note on `state.rs`:
|
||||
|
|
|
|||
|
|
@ -142,16 +142,22 @@ DLQ 是 **append-and-ack**:附加到 `stalwart:jmap:dlq` 的相同 Lua 脚本
|
|||
| `推送:订阅:id` |没有 EX | `记住推送订阅 ID` |当前订阅查找 |
|
||||
| `推送:订阅:{id}:状态` | 900 / 300 / 86_400(最低 1 强制执行)| `set_push_subscription_status`;验证请求时为“pending”,成功时为“verified”,禁用时为“disabled” | **无** |
|
||||
| `推送:注册:{sha256(callback_url)}` |前 604_800 | `remember_push_subscription_for_callback`,从 `register_push` 调用 | `get_push_subscription_for_callback`;通过 `remove_push_subscription_for_callback` 删除 |
|
||||
| `push:registration:current` |前 604_800 | `set_push_registration_current`,从 `register_push` 调用 | `get_push_registration_current`;通过 `clear_push_registration_current` 清除,`disable_push` 经 `clear_push_registration_current_if` 清除 |
|
||||
| `push:orphan:{subscription_id}` |前 604_800 | `record_push_orphan` |孤儿扫荡|
|
||||
|
||||
**两个不同的键经常被混淆。在编辑之前请阅读本文。**
|
||||
**三个不同的键经常被混淆。在编辑之前请阅读本文。**
|
||||
|
||||
- `push:registration:{sha256(callback_url)}` 是从回调 URL 到
|
||||
订阅 ID**,TTL 7 天。它不是一把锁。
|
||||
- `lock:push-register:{sha256(callback_url)}` 是**单次航班注册锁**,
|
||||
订阅 ID 的**映射**,TTL 7 天。它不是一把锁。
|
||||
- `push:registration:current` 存的是**当前存活订阅的回调 URL 原文**,同样 7 天 TTL。它在新订阅
|
||||
创建之后最后写入。用一个不同的回调 URL 注册时,后端会在注册锁内先销毁上一个订阅、再创建
|
||||
新订阅,因此旧 origin 不再收到推送;`disable_push` 只在被注销的正是当前地址时才清它。
|
||||
其后缀是字面量 `current`,不会与 per-URL key 的 `session_digest` 十六进制后缀相撞。
|
||||
它既不是映射也不是锁。
|
||||
- `lock:push-register:{sha256(callback_url)}` 是**单飞注册锁**,
|
||||
TTL 360 秒。它不是映射。
|
||||
|
||||
它们使用相同的摘要,但是是具有不相关生命周期和用途的单独密钥。
|
||||
前两者 TTL 相同但是用途无关的独立键。锁与 per-URL 映射使用相同的摘要,但完全是不同的键。
|
||||
|
||||
**注释:`push:subscription:{id}:status` 设计为只写。**
|
||||
来源对“state.rs”有明确的注释:
|
||||
|
|
|
|||
|
|
@ -78,9 +78,9 @@ These names never exist in the code. Some of them appear in the directory tree o
|
|||
| Directory scheme that splits Telegram access into four files "module entry/command parsing/session state/reply rendering" | Fictitious directory scheme | Drawn in the target directory tree, never created | Command parsing is in `parse_intent` (`src/worker.rs`); the session/authorization state is Redis TTL (the AI authorization state section of `docs/reference.md`); the rendering function is in `src/channel.rs` |
|
||||
| `delivery:pending:{stream}` | Fictional key | Confused with Redis Streams' pending-entries list (PEL) - PEL is maintained internally by Redis and is not a writable key | Real key: `delivery:inflight:{stream}:{id}` (EX 60) and `delivery:committed:{stream}:{id}` (EX 604_800), see Delivery pipeline keys section of `docs/reference.md` |
|
||||
| `check_config_reload` | Fictional function | Uncommitted name in draft, never made it into code | Hot updated to `refresh_business_config` (`src/notify.rs:1130`) |
|
||||
| `push:registration:{sha256(callback_url)}` was once written as "360s registration solo lock" | Fact mislabeling | The key is 7d callback → subscription ID mapping | The real 360s solo lock is `lock:push-register:{sha256(callback_url)}` (`src/notify.rs:1328-1330`) |
|
||||
| `push:registration:{sha256(callback_url)}` was once written as "360s registration solo lock" | Fact mislabeling | The key is 7d callback → subscription ID mapping | The real 360s solo lock is `lock:push-register:{sha256(callback_url)}` (`src/notify.rs:1481-1483`) |
|
||||
| `MESSAGWEAVE_DOMAIN` was once written as "Worker whitelist is invalid when production is not configured" | Fictional assertion (unsubmitted draft/appeared in discussion, not documented) | This environment variable has zero hits in all positions; Worker whitelist is **unconditional fail-closed**: unknown path 404, method does not match 405, backend is missing or parsing failure 503 (`cloudflare-worker/src/index.js:77-92`) | No need to configure switches, the whitelist will always take effect |
|
||||
| `read_batch` / `retry_or_dlq` "Redis may still return `Ok(())` when an error occurs, so the consumption loop will not exit due to a single failure" | Fictional assertion | The consumption entry is HTTP handler `worker` (`notify.rs:390`), **not a background loop**: full position `src/` zero hit `select!`, no signal processing, no resident worker process. `read_batch` (`state.rs:491`) throws the Redis error as it is (`state.rs:531`) via `?`, and the only discarded result is the `BUSYGROUP` idempotent protection of XGROUP `CREATE` (`state.rs:499-507`); `retry_or_dlq` is also thrown up via `?`. The caller returns `503 service_unavailable` + `retryable=true` for every `Err`, so the semantic premise of "the loop does not exit due to a single failure" itself is not true | None - the gap does not exist; `docs/opengaps.md` (original name `docs/roadmap.md`) "Code Gap" 4 → 3, `docs/design.md` "Known Boundary" same sentence has been deleted |
|
||||
| `read_batch` / `retry_or_dlq` "Redis may still return `Ok(())` when an error occurs, so the consumption loop will not exit due to a single failure" | Fictional assertion | The consumption entry is HTTP handler `worker` (`notify.rs:390`), **not a background loop**: full position `src/` zero hit `select!`, no signal processing, no resident worker process. `read_batch` (`state.rs:506`) throws the Redis error as it is (`state.rs:546`) via `?`, and the only discarded result is the `BUSYGROUP` idempotent protection of XGROUP `CREATE` (`state.rs:514-522`); `retry_or_dlq` is also thrown up via `?`. The caller returns `503 service_unavailable` + `retryable=true` for every `Err`, so the semantic premise of "the loop does not exit due to a single failure" itself is not true | None - the gap does not exist; `docs/opengaps.md` (original name `docs/roadmap.md`) "Code Gap" 4 → 3, `docs/design.md` "Known Boundary" same sentence has been deleted |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ These names never exist in the code. Some of them appear in the directory tree o
|
|||
| Build the channel layer into a directory module (including module entry file) | Fictitious directory | The channel layer is a file called `src/channel.rs` | None |
|
||||
| `notify::push_handler` / `notify::worker` / `notify::reconcile` as module paths | Fictitious module paths | These are free functions within `src/notify.rs` (`jmap_push` / `worker` / `reconcile`), not module paths | None |
|
||||
| `mod_dedup` / `mod_streams` / `mod_sincestate` | Fictional module name | `state.rs` / `notify.rs` are tile files, no submodules; deduplication and Streams logic exist as free functions | None |
|
||||
| `PushVerification` type | Fictitious type | Undefined; `register_push` (`notify.rs:1315`) handles callback URL and verification code writeback inline | None |
|
||||
| `PushVerification` type | Fictitious type | Undefined; `register_push` (`notify.rs:1468`) handles callback URL and verification code writeback inline | None |
|
||||
| `CancellationToken` | fictitious type | unused; no graceful shutdown, no signal handling (`src/` zero hits `tokio::signal` / `ctrl_c`) | None |
|
||||
| `Preview` type / 4000 character long email protection / `[Continue to view original text]` button / `/llm-fallback` button | Fictional type and UI | All not implemented; after authorization, the full text will be handed over to LLM, and if it fails, the first 300 characters will be returned, without any truncation mark or button | None |
|
||||
| `LlmErr` (5 variants) | Fictional enum | Real is `AiError`, only 3 variants (`InvalidEndpoint` / `Request` / `Response`) | None |
|
||||
|
|
|
|||
|
|
@ -78,9 +78,9 @@
|
|||
| 把 Telegram 接入拆成「模块入口 / 命令解析 / 会话状态 / 回复渲染」四个文件的目录方案 | 虚构目录方案 | 目标目录树里画出,从未创建 | 命令解析在 `parse_intent`(`src/worker.rs`);会话/授权态走 Redis TTL(`docs/reference.md` 的 AI 授权态一节);渲染函数在 `src/channel.rs` 内 |
|
||||
| `delivery:pending:{stream}` | 虚构键 | 与 Redis Streams 的 pending-entries list(PEL)混淆——PEL 由 Redis 内部维护,不是可写键 | 真实键:`delivery:inflight:{stream}:{id}`(EX 60)与 `delivery:committed:{stream}:{id}`(EX 604_800),见 `docs/reference.md` 的投递流水线键一节 |
|
||||
| `check_config_reload` | 虚构函数 | 未提交草稿中的名字,从未进入代码 | 热更新为 `refresh_business_config`(`src/notify.rs:1130`) |
|
||||
| `push:registration:{sha256(callback_url)}` 曾被写成「360s 注册单飞锁」 | 事实误标 | 该键是 7d 回调→订阅 ID 映射 | 真正的 360s 单飞锁是 `lock:push-register:{sha256(callback_url)}`(`src/notify.rs:1328-1330`) |
|
||||
| `push:registration:{sha256(callback_url)}` 曾被写成「360s 注册单飞锁」 | 事实误标 | 该键是 7d 回调→订阅 ID 映射 | 真正的 360s 单飞锁是 `lock:push-register:{sha256(callback_url)}`(`src/notify.rs:1481-1483`) |
|
||||
| `MESSAGWEAVE_DOMAIN` 曾被写成「生产未配置时 Worker 白名单失效」 | 虚构断言(未提交草稿/讨论中出现,未进文档) | 该环境变量全仓零命中;Worker 白名单是**无条件 fail-closed**:未知路径 404、method 不符 405、后端缺失或解析失败 503(`cloudflare-worker/src/index.js:77-92`) | 无需配置开关,白名单恒生效 |
|
||||
| `read_batch` / `retry_or_dlq`「Redis 出错时仍可能返回 `Ok(())`,消费循环因此不会因单次失败退出」 | 虚构断言 | 消费入口是 HTTP handler `worker`(`notify.rs:390`),**不是后台循环**:全仓 `src/` 零命中 `select!`,无信号处理、无常驻 worker 进程。`read_batch`(`state.rs:491`)经 `?` 把 Redis 错误原样上抛(`state.rs:531`),唯一被丢弃的结果是 XGROUP `CREATE` 的 `BUSYGROUP` 幂等保护(`state.rs:499-507`);`retry_or_dlq` 同样经 `?` 上抛。调用方对每一处 `Err` 都返回 `503 service_unavailable` + `retryable=true`,故「循环不因单次失败退出」这一语义前提本身不成立 | 无——不存在该缺口;`docs/opengaps.md`(原名 `docs/roadmap.md`)「代码缺口」4 → 3,`docs/design.md`「已知边界」同句已删 |
|
||||
| `read_batch` / `retry_or_dlq`「Redis 出错时仍可能返回 `Ok(())`,消费循环因此不会因单次失败退出」 | 虚构断言 | 消费入口是 HTTP handler `worker`(`notify.rs:390`),**不是后台循环**:全仓 `src/` 零命中 `select!`,无信号处理、无常驻 worker 进程。`read_batch`(`state.rs:506`)经 `?` 把 Redis 错误原样上抛(`state.rs:546`),唯一被丢弃的结果是 XGROUP `CREATE` 的 `BUSYGROUP` 幂等保护(`state.rs:514-522`);`retry_or_dlq` 同样经 `?` 上抛。调用方对每一处 `Err` 都返回 `503 service_unavailable` + `retryable=true`,故「循环不因单次失败退出」这一语义前提本身不成立 | 无——不存在该缺口;`docs/opengaps.md`(原名 `docs/roadmap.md`)「代码缺口」4 → 3,`docs/design.md`「已知边界」同句已删 |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
| 把渠道层建成目录模块(含模块入口文件) | 虚构目录 | 渠道层就是 `src/channel.rs` 一个文件 | 无 |
|
||||
| `notify::push_handler` / `notify::worker` / `notify::reconcile` 作为模块路径 | 虚构模块路径 | 这些是 `src/notify.rs` 内的自由函数(`jmap_push` / `worker` / `reconcile`),不是模块路径 | 无 |
|
||||
| `mod_dedup` / `mod_streams` / `mod_sincestate` | 虚构模块名 | `state.rs` / `notify.rs` 都是平铺文件,无子模块;去重与 Streams 逻辑以自由函数存在 | 无 |
|
||||
| `PushVerification` 类型 | 虚构类型 | 未定义;`register_push`(`notify.rs:1315`)内联处理回调 URL 与验证码回写 | 无 |
|
||||
| `PushVerification` 类型 | 虚构类型 | 未定义;`register_push`(`notify.rs:1468`)内联处理回调 URL 与验证码回写 | 无 |
|
||||
| `CancellationToken` | 虚构类型 | 未使用;无优雅关闭、无信号处理(`src/` 零命中 `tokio::signal` / `ctrl_c`) | 无 |
|
||||
| `Preview` 类型 / 4000 字符长邮件保护 / `[继续查看原文]` 按钮 / `/llm-fallback` 按钮 | 虚构类型与 UI | 全部未实现;授权后把全文交给 LLM,失败即回退前 300 字符,无任何截断标注或按钮 | 无 |
|
||||
| `LlmErr`(5 变体) | 虚构枚举 | 真实是 `AiError`,仅 3 个变体(`InvalidEndpoint` / `Request` / `Response`) | 无 |
|
||||
|
|
|
|||
Loading…
Reference in New Issue