docs: add hand-holding quickstart tutorial (EN + ZH)

New docs/quickstart.md and docs/quickstart.zh-CN.md walk a human operator from
zero to the first notification: create a Telegram bot via BotFather, find the
chat id, get a Stalwart app password, provision Redis, deploy the container,
sign in with CONFIG_ENCRYPTION_KEY, fill the business config, register
callbacks, enable processing, verify end-to-end, and schedule the cron drain.

Every claim was verified against the code:
- healthz is static 200 'ok'; /ready returns 503 until all upstreams
  reachable, 200 with per-component booleans
- admin session TTL = 1800s (30 min) in state.rs
- first save must be complete -> 422 (is_complete in config.rs)
- 409 conflict on revision mismatch
- secrets are write-only (Option<SecretString>, never echoed)
- /worker and /reconcile return 204 on success, 503 when disabled
- webhook secret mismatch returns 401 (not 403)
- search triggers: 搜索/查找/检索 + /search command only
- cron-drain.sh uses MW_APP_URL (req), MW_WORKER_TOKEN (req),
  MW_RECONCILE_TOKEN (opt)

Registered both files in all 5 validator DOCS lists (audit_paths globs
automatically). Added rows to README §6 and charter §7 in both languages.
Added discoverable links from README §3 to the quickstart.

GATE-DOCS 6/6 PASS (71 anchors, 735 path-refs).
This commit is contained in:
anonsaber 2026-10-08 17:35:35 +08:00
parent 74d53ae542
commit 7ed1a086fe
11 changed files with 404 additions and 5 deletions

View File

@ -75,6 +75,9 @@ their own addresses in both forms.
## 3. 5-minute setup ## 3. 5-minute setup
Requires a reachable Redis instance and a JMAP session plus a Telegram bot token. Requires a reachable Redis instance and a JMAP session plus a Telegram bot token.
If you do not have those yet, the hand-holding walkthrough in
[`docs/quickstart.md`](docs/quickstart.md) covers BotFather, chat-id lookup, app-password
creation and the full first-run from zero.
**This is a local container smoke test.** Production deployment can use any platform that **This is a local container smoke test.** Production deployment can use any platform that
can run the backend container, inject encrypted secrets, and connect to externally managed can run the backend container, inject encrypted secrets, and connect to externally managed
@ -158,6 +161,7 @@ mail. See §2.
| Document | What it answers | | Document | What it answers |
|---|---| |---|---|
| [`docs/quickstart.md`](docs/quickstart.md) | Hand-holding first run: create a Telegram bot, find the chat id, deploy one backend, fill the form, register callbacks |
| [`docs/design.md`](docs/design.md) | Why the system is shaped this way: data flow, JMAP semantics, Redis streams, AI consent rules | | [`docs/design.md`](docs/design.md) | Why the system is shaped this way: data flow, JMAP semantics, Redis streams, AI consent rules |
| [`docs/deployment.md`](docs/deployment.md) | How to deploy: Dockerfile, secrets, Redis hosting, webhook/push/reconcile routing, multi-instance load balancing | | [`docs/deployment.md`](docs/deployment.md) | How to deploy: Dockerfile, secrets, Redis hosting, webhook/push/reconcile routing, multi-instance load balancing |
| [`docs/reference.md`](docs/reference.md) | **Single source of truth for verifiable facts** — Redis keys and TTLs, error codes, routes, environment layers, budgets | | [`docs/reference.md`](docs/reference.md) | **Single source of truth for verifiable facts** — Redis keys and TTLs, error codes, routes, environment layers, budgets |

View File

@ -55,6 +55,8 @@ MessageWeave 位于 JMAP 邮件服务器(例如 Stalwart)与 Telegram bot
## 3. 五分钟跑起来 ## 3. 五分钟跑起来
需要可用的 Redis 服务、一个 JMAP session 与一个 Telegram bot token。 需要可用的 Redis 服务、一个 JMAP session 与一个 Telegram bot token。
如果这些你还没有,[`docs/quickstart.zh-CN.md`](docs/quickstart.zh-CN.md) 有手把手教程:BotFather
申请、chat id 获取、应用密码创建与首次配置全过程。
这段是**本地容器冒烟测试**。生产部署可以用任何能跑起后端容器、注入加密 secret 并连到外部托管 Redis 的平台,见 `docs/deployment.md` §3 与 `docs/reference.md` 的平台相关部分。 这段是**本地容器冒烟测试**。生产部署可以用任何能跑起后端容器、注入加密 secret 并连到外部托管 Redis 的平台,见 `docs/deployment.md` §3 与 `docs/reference.md` 的平台相关部分。
@ -118,6 +120,7 @@ Telegram 发送。它由双因子开关控制——进程必须带 `--debug` **
| 文档 | 回答什么 | | 文档 | 回答什么 |
|---|---| |---|---|
| [`docs/quickstart.zh-CN.md`](docs/quickstart.zh-CN.md) | 手把手首次运行:创建 Telegram bot、找到 chat id、部署一个后端、填表、注册回调 |
| [`docs/design.zh-CN.md`](docs/design.zh-CN.md) | 为什么这样设计:数据流、JMAP 语义、Redis streams、AI 授权规则 | | [`docs/design.zh-CN.md`](docs/design.zh-CN.md) | 为什么这样设计:数据流、JMAP 语义、Redis streams、AI 授权规则 |
| [`docs/deployment.zh-CN.md`](docs/deployment.zh-CN.md) | 怎么部署:Dockerfile、secrets、Redis 托管、webhook/push/对账路由、多实例负载均衡 | | [`docs/deployment.zh-CN.md`](docs/deployment.zh-CN.md) | 怎么部署:Dockerfile、secrets、Redis 托管、webhook/push/对账路由、多实例负载均衡 |
| [`docs/reference.zh-CN.md`](docs/reference.zh-CN.md) | **可核对事实的唯一权威来源**——Redis 键与 TTL、错误码、路由、环境变量分层、预算常量 | | [`docs/reference.zh-CN.md`](docs/reference.zh-CN.md) | **可核对事实的唯一权威来源**——Redis 键与 TTL、错误码、路由、环境变量分层、预算常量 |

View File

@ -171,6 +171,7 @@ Answer only one question per document; **tables, lists and values must not be du
|---|---|---| |---|---|---|
| [`../AGENTS.md`](../AGENTS.md) | Universal, language-independent code writing and environment building specifications | Quality rules | | [`../AGENTS.md`](../AGENTS.md) | Universal, language-independent code writing and environment building specifications | Quality rules |
| `README.md` / `README.zh-CN.md` | User-oriented: what is it, how to run it, how to configure it | User-visible facts (information must be equivalent in Chinese and English) | | `README.md` / `README.zh-CN.md` | User-oriented: what is it, how to run it, how to configure it | User-visible facts (information must be equivalent in Chinese and English) |
| `docs/quickstart.md` / `docs/quickstart.zh-CN.md` | Hand-holding first run: create a Telegram bot, find the chat id, deploy one backend, fill the form, register callbacks | Steps and decisions (field semantics, defaults and TTLs stay in `docs/reference.md`) |
| `docs/design.md` | Why it is designed this way: data flow, module boundaries, state machines, error handling | Architectural intent | | `docs/design.md` | Why it is designed this way: data flow, module boundaries, state machines, error handling | Architectural intent |
| `docs/deployment.md` | How to deploy: Dockerfile, secrets, gateway, multiple instances, cron | Deployment and operation and maintenance | | `docs/deployment.md` | How to deploy: Dockerfile, secrets, gateway, multiple instances, cron | Deployment and operation and maintenance |
| `docs/reference.md` | The single authoritative source of verifiable facts: routing, Redis keys and TTLs, configuration items, error codes, outbound constants | **verifiable facts** | | `docs/reference.md` | The single authoritative source of verifiable facts: routing, Redis keys and TTLs, configuration items, error codes, outbound constants | **verifiable facts** |

View File

@ -171,6 +171,7 @@ Stalwart 与 Telegram 的业务凭据已在真机联调通过——出站、查
|---|---|---| |---|---|---|
| [`../AGENTS.md`](../AGENTS.md) | 通用、语言无关的代码编写与环境构建规范 | 质量规则 | | [`../AGENTS.md`](../AGENTS.md) | 通用、语言无关的代码编写与环境构建规范 | 质量规则 |
| `README.md` / `README.zh-CN.md` | 面向使用者:是什么、怎么跑、怎么配置 | 用户可见事实(中英必须信息对等) | | `README.md` / `README.zh-CN.md` | 面向使用者:是什么、怎么跑、怎么配置 | 用户可见事实(中英必须信息对等) |
| `docs/quickstart.md` / `docs/quickstart.zh-CN.md` | 手把手首次运行:创建 Telegram bot、找到 chat id、部署一个后端、填表、注册回调 | 步骤与决策(字段语义、默认值、TTL 归 `docs/reference.md`) |
| `docs/design.md` | 为什么这样设计:数据流、模块边界、状态机、错误处理 | 架构意图 | | `docs/design.md` | 为什么这样设计:数据流、模块边界、状态机、错误处理 | 架构意图 |
| `docs/deployment.md` | 怎么部署:Dockerfile、secrets、网关、多实例、cron | 部署与运维 | | `docs/deployment.md` | 怎么部署:Dockerfile、secrets、网关、多实例、cron | 部署与运维 |
| `docs/reference.md` | 可核对事实的唯一权威来源:路由、Redis 键与 TTL、配置项、错误码、出站常量 | **可核对事实** | | `docs/reference.md` | 可核对事实的唯一权威来源:路由、Redis 键与 TTL、配置项、错误码、出站常量 | **可核对事实** |

203
docs/quickstart.md Normal file
View File

@ -0,0 +1,203 @@
# 1. Quickstart
Chinese: [quickstart.zh-CN.md](quickstart.zh-CN.md)
This page is a from-zero walkthrough for a human operator: create a Telegram bot, deploy
one backend, fill the configuration page, register the callbacks, and get the first
notification. It only contains steps and decisions. Field semantics, defaults, TTLs and
error codes live in [reference.md](reference.md); production, scaling and gateway
operations live in [deployment.md](deployment.md).
## 1.1 What you need
- A mailbox on a JMAP server you administer (this guide assumes Stalwart), HTTPS only.
- A Telegram account.
- A Redis endpoint with persistence (Upstash's free tier works; it is TLS-only, so the URL
starts with `rediss://`).
- A machine or platform that can run one container and expose one HTTP port.
- One public HTTPS origin that can receive webhooks — your own domain, a tunnel, or a
Cloudflare route. Telegram and Stalwart both require HTTPS.
- Optional: an OpenAI-compatible LLM key, a Cloudflare account if you want the gateway.
Nothing else is required: no Postgres, no message broker, no Kubernetes.
## 1.2 Create the Telegram bot
1. In Telegram open a chat with `@BotFather` and send `/newbot`.
2. Pick a display name, then a username that ends in `bot` or `_bot`.
3. BotFather replies with a token that looks like `1234567890:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw1`.
That string is the bot token. Treat it like a password: anyone holding it can read your
webhook traffic and send messages as your bot. It is never echoed back by the API once saved.
4. Send the new bot any message (for example `/start`) from the account or group that should
receive notifications. Until you do, Telegram has no chat to deliver to.
## 1.3 Find the chat id
1. Open `https://api.telegram.org/bot<TOKEN>/getUpdates` in a browser, replacing `<TOKEN>` with
the token from step 3.
2. In the JSON, find `result[].message.chat.id`. Private chats give a positive number such as
`123456789`; groups and supergroups give a negative one such as `-1001234567890`.
3. For a group, add the bot to the group first, then send a message in the group before you call
`getUpdates`. The bot only sees messages in groups after you disable privacy mode under
`@BotFather` → `/setprivacy` → `Disable`, otherwise group messages never reach it.
You will use this id twice in step 7: as the notification target and as a member of the inbound
allowlist. The allowlist must contain at least one id — an empty allowlist is rejected as invalid
configuration rather than treated as "allow everyone".
## 1.4 Get the JMAP credentials
1. In your Stalwart admin, create an application password for the mailbox — an app password, not
the account login password. The backend authenticates with HTTP Basic over JMAP.
2. The session URL is your mail origin: `https://mail.example.com`. You may also paste
`https://mail.example.com/.well-known/jmap`; the backend collapses both to the origin, and
rejects any URL with embedded credentials, a query string or a fragment.
3. The JMAP account id is optional. Leave it empty: after the first successful save the backend
reads it back from the server and the page shows it.
## 1.5 Run the backend
The container needs exactly two environment variables:
```bash
# .env — never commit this file
REDIS_URL=rediss://default:<password>@<host>:6379
CONFIG_ENCRYPTION_KEY=<64 hex characters, from: openssl rand -hex 32>
```
- `REDIS_URL` is your only state store. Back it up like a database.
- `CONFIG_ENCRYPTION_KEY` doubles as the SPA admin credential and the encryption key for stored
business secrets. If you lose it you can no longer read the configuration page or decrypt the
stored secrets; if you rotate it, previously stored secrets become unreadable.
Then build and start:
```bash
docker build -t messageweave .
docker run --rm --env-file .env -p 8080:8080 messageweave
curl localhost:8080/healthz # → ok
```
`GET /healthz` is a static liveness reply and never touches Redis. The readiness report is
`GET /ready`: it answers 503 until the configuration exists and every configured upstream is
reachable, and 200 with a JSON component report once they are. Point your HTTPS reverse proxy or
tunnel at this port now — steps 8 and 9 need a public HTTPS origin.
## 1.6 Sign in to the configuration page
1. Open the page in a browser. If the backend started with the required variables the first card
is **Create admin session**.
2. Paste `CONFIG_ENCRYPTION_KEY` — not the Redis password. This mints an admin session that lives
only in the page's memory for 30 minutes; reloading the page logs you out.
## 1.7 Fill in the business configuration
The form is a full replacement, not a patch: whatever you submit becomes the whole stored
configuration. The sections:
- **01 JMAP mailbox** — session URL and username, plus the app password from step 4.
- **02 Telegram and access auth** — bot token, notification chat id, allowlist, and three secrets
you make up yourself: webhook secret, reconcile token and worker token. Generate them with
`openssl rand -hex 32` each. The webhook secret must match on the Telegram side later; the
reconcile and worker tokens protect the scheduler endpoints.
- **03 LLM service** — optional. Leave disabled unless you want AI digests; if you enable it, the
HTTPS base URL, model name and API key all become required.
- **04 Notification display** — the time zone used to render received times in notifications.
Then, in order:
1. Click **Test connection**. It preflights the credentials without saving anything.
2. Click **Save business config and hot-reload**. The first save must contain every required
field or the server answers 422 and stores nothing.
3. Read the result line under the button: `runtime_applied: true` means the new runtime was
installed on this backend. `false` means it saved but the runtime was not installed, and the
`warnings` array says which component failed — most often an unreachable `jmap_session_url`.
Fix the field and save again; `/ready` stays 503 until it goes green.
4. If the save answers **409 conflict**, someone else (another tab, a second operator) saved in
between. Reload the page to pick up their values, then re-apply your edit.
Secrets are write-only: the page never shows a stored secret again, and submitting an empty secret
field keeps the previously stored value — clear a secret deliberately in a separate edit.
## 1.8 Register the callbacks
Still on the same page, under **External callbacks**:
1. Paste the public HTTPS origin that reaches this backend, scheme and host only, no path —
for example `https://mw.example.com`. If you put the Cloudflare gateway in front, use the
gateway's origin, not the backend's: callbacks must survive a backend restart, and the
gateway is what stays up.
2. Click **Register callbacks**. One click registers both directions: the Telegram webhook at
`/webhook/tg` and the Stalwart push subscription at `/push/jmap`. Verification is handled
automatically; the page reports the result.
Registration is explicit and repeatable. Registering a new origin cancels the previous subscription,
so the old origin stops receiving pushes. A failed registration never removes the old one — if it
can't reach Telegram, the previous webhook stays in place. If it did reach Telegram but not
Stalwart, the page reports a partial failure and tells you to click again.
Verify from a terminal:
```bash
curl -s "https://api.telegram.org/bot<TOKEN>/getWebhookInfo"
# expect "url": "https://mw.example.com/webhook/tg" and "pending_update_count": 0
```
## 1.9 Turn on processing
Flip **Enable business processing** at the top of the page. This is the master switch: with it off,
webhooks are accepted but `/worker` and `/reconcile` refuse to drain, so nothing reaches Telegram.
With it on, incoming mail and messages are enqueued and the worker drains them.
## 1.10 Verify end to end
1. Send yourself an email. Within a few seconds the bot should post a notification with the
sender, subject and received time — never the body. That path alone proves JMAP ingest,
Redis, the queue and the Telegram send all work.
2. In Telegram try `/search 发票`. Note: the search triggers are the Chinese words `搜索`, `查找`
and `检索`, plus the `/search` command — English keywords like `search invoice` are not
recognised as triggers.
3. Try `/summary <email_id>` from the notification's email id. The first call asks for
confirmation; the summary only goes out after you confirm, and only if the LLM section is
enabled.
## 1.11 Schedule the drain
Push delivery can be delayed or missed; an external scheduler is the safety net. On any machine
that can reach your origin, every 5–10 minutes:
```bash
MW_APP_URL=https://mw.example.com \
MW_WORKER_TOKEN=<worker token> \
MW_RECONCILE_TOKEN=<reconcile token> \
scripts/cron-drain.sh
```
The script drains the queue (`/worker`) and rescans for missed pushes (`/reconcile`); both return
204 with an empty body on success. `MW_RECONCILE_TOKEN` is optional — if omitted, the reconcile
step is skipped and only the queue is drained. If you put the gateway in front, `/worker` must
reach the backend origin directly — see deployment.md for why.
## 1.12 Optional: the Cloudflare gateway
Skip this unless you want a load balancer across two backends. The Worker is optional, gives
free-tier Cloudflare users a load balancer, and is not a security layer: the backend origins stay
directly reachable either way. Setup lives in `cloudflare-worker/README.md`; when you use it,
register the callback origin from step 8 against the gateway.
## 1.13 When something is wrong
- **Nothing arrives after saving.** Check the enable toggle (step 9), then
`getWebhookInfo` (step 8). A `last_error_message` about a 401 means the webhook secret does not
match what Telegram sends — re-save section 02 and re-register.
- **`/ready` answers 503.** That body is just an error code — it does not name the failing
component. When `/ready` returns 200, its JSON has per-field booleans (`jmap`, `telegram`,
`redis`). Common causes are an unreachable `jmap_session_url`, a wrong app password, or
`runtime_applied: false` from the last save.
- **The form says conflict.** Two sessions saved at once; reload and redo (step 7.3).
- **The bot ignores you in a group.** Privacy mode is on; disable it under `@BotFather` (step 3).
- **`/search` answers with a help hint.** Your query started with a Latin word; the prefix triggers
are `搜索` / `查找` / `检索`.
Field-level errors, TTLs, Redis keys and route tables: [reference.md](reference.md).
Scaling, secrets rotation, gateways and cron on the wire: [deployment.md](deployment.md).

177
docs/quickstart.zh-CN.md Normal file
View File

@ -0,0 +1,177 @@
# 1. 快速上手
英文:[quickstart.md](quickstart.md)
本文是为人类运维者准备的从零开始指南:创建 Telegram bot、部署一个后端、填写配置页、注册回调、
收到第一条通知。只包含步骤与决策。字段语义、默认值、TTL 与错误码在 [reference.md](reference.md);
生产、扩展、网关运维在 [deployment.md](deployment.md)。
## 1.1 你需要准备什么
- 一个你管理的 JMAP 邮箱(本指南以 Stalwart 为例),必须 HTTPS。
- 一个 Telegram 账号。
- 一个带持久化的 Redis 实例(Upstash 免费版可用;它是纯 TLS 的,URL 以 `rediss://` 开头)。
- 一台能跑容器并暴露一个 HTTP 端口的机器或平台。
- 一个能接收 webhook 的公网 HTTPS 域名——自有域名、隧道或 Cloudflare 路由都行。Telegram 与
Stalwart 都要求 HTTPS。
- 可选:OpenAI 兼容的 LLM 密钥;想要网关的话再来一个 Cloudflare 账号。
不需要别的:没有 Postgres、没有消息队列、没有 Kubernetes。
## 1.2 创建 Telegram bot
1. 在 Telegram 中打开 `@BotFather` 的对话,发送 `/newbot`。
2. 选一个显示名,再选一个以 `bot` 或 `_bot` 结尾的用户名。
3. BotFather 会回复一串 token,形如 `1234567890:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw1`。
这就是 bot token。当成密码对待:拿到它的人能读取你的 webhook 流量、以你的 bot 身份发消息。
一旦保存,API 不再回显它。
4. 用应当接收通知的账号或群给新 bot 发任意一条消息(例如 `/start`)。在此之前 Telegram 没有可投递
的对话。
## 1.3 找到 chat id
1. 在浏览器打开 `https://api.telegram.org/bot<TOKEN>/getUpdates`,把 `<TOKEN>` 替换为第 3 步拿到的
token。
2. 在 JSON 中找 `result[].message.chat.id`。私聊是正数,例如 `123456789`;群与超级群是负数,例如
`-1001234567890`。
3. 如果是群:先把 bot 拉进群,再在群里发一条消息,然后才调 `getUpdates`。bot 在群里只有在关闭隐私模式
后才能看到非命令消息:`@BotFather` → `/setprivacy` → `Disable`。
这个 id 在第 7 步要用两次:一次当通知目标,一次放进入站白名单。白名单必须至少一个 id——空名单会被
判为非法配置而拒绝,不会当成「允许所有人」。
## 1.4 拿到 JMAP 凭据
1. 在 Stalwart 管理端为邮箱创建一个应用密码——是应用密码,不是账号登录密码。后端用 HTTP Basic 走
JMAP。
2. session URL 就是你的邮箱源站:`https://mail.example.com`。也可以粘贴
`https://mail.example.com/.well-known/jmap`;后端会把两者都折叠成源站,并拒绝带凭据、查询串或片段
的 URL。
3. JMAP account id 是可选的。留空:首次保存成功后后端会从服务器读回,页面会显示出来。
## 1.5 跑起后端
容器只需要两个环境变量:
```bash
# .env — 不要提交这个文件
REDIS_URL=rediss://default:<password>@<host>:6379
CONFIG_ENCRYPTION_KEY=<64 位十六进制,用:openssl rand -hex 32>
```
- `REDIS_URL` 是你唯一的状态存储。像数据库一样备份它。
- `CONFIG_ENCRYPTION_KEY` 兼作 SPA 管理员凭据与已存业务密钥的加密密钥。丢了它,你既登不上配置页,
也解不开已存密钥;轮换它,之前存的密钥全部不可读。
然后构建并启动:
```bash
docker build -t messageweave .
docker run --rm --env-file .env -p 8080:8080 messageweave
curl localhost:8080/healthz # → ok
```
`GET /healthz` 是静态存活回复,不碰 Redis。就绪报告是 `GET /ready`:在配置存在且所有已配置上游可达
之前回答 503,之后回答 200 并带一份 JSON 组件报告。现在把你的 HTTPS 反代或隧道指向这个端口——第 8、
9 步需要一个公网 HTTPS 域名。
## 1.6 登录配置页
1. 在浏览器打开页面。如果后端带着所需变量启动,第一张卡是 **Create admin session**。
2. 粘贴 `CONFIG_ENCRYPTION_KEY`——不是 Redis 密码。这会签发一个管理员会话,只活在页面内存里,有效期
30 分钟;刷新页面即登出。
## 1.7 填写业务配置
表单是整存整换,不是增量补丁:你提交的就是整份存储配置。各节:
- **01 JMAP mailbox**——session URL、用户名,以及第 4 步的应用密码。
- **02 Telegram and access auth**——bot token、通知 chat id、白名单,加上你自己生成的三个密钥:webhook
密钥、reconcile token 与 worker token。各自用 `openssl rand -hex 32` 生成。webhook 密钥之后要在
Telegram 侧对得上;reconcile 与 worker token 保护调度端点。
- **03 LLM service**——可选。不需要 AI 摘要就别开;一旦开启,HTTPS base URL、模型名与 API key 三者都
变成必填。
- **04 Notification display**——通知里收信时间的渲染时区。
然后按顺序:
1. 点 **Test connection**。它预检凭据但不保存任何东西。
2. 点 **Save business config and hot-reload**。首次保存必须包含所有必填字段,否则服务器回答 422 且
什么也不存。
3. 读按钮下方那行结果:`runtime_applied: true` 表示新运行时已在本后端装配;`false` 表示存了但运行时未
装配,`warnings` 数组会指出失败的组件——最常见是 `jmap_session_url` 不可达。改字段再存一次;`/ready`
会一直 503 直到它变绿。
4. 如果保存回答 **409 conflict**,说明有人(另一个标签页、第二个运维者)在中间也存了。刷新页面拿到
他们的值,再把你的改动重新套上去。
密钥是只写的:页面永不再显示已存密钥,提交空的密钥字段会保留之前的值——要清密钥就在单独一次编辑里
故意留空。
## 1.8 注册回调
仍在同一页,**External callbacks** 区:
1. 粘贴能到达本后端的公网 HTTPS 源站,只写 scheme + host,不带路径——例如 `https://mw.example.com`。
如果你把 Cloudflare 网关放在前面,就填网关的源站,不是后端的:回调要能在后端重启后仍然可达,而
网关才是那个不掉线的。
2. 点 **Register callbacks**。一次点击注册双向:Telegram webhook 注册到 `/webhook/tg`,Stalwart push
订阅注册到 `/push/jmap`。验证自动完成,页面上报结果。
注册是显式的、可重复的。注册新源站会取消上一个订阅,于是旧源站不再收到 push。失败的注册绝不会删掉
旧订阅——如果连不上 Telegram,上一个 webhook 原样留着;如果连上了 Telegram 但没连上 Stalwart,页面会
报部分失败并让你再点一次。
在终端验证:
```bash
curl -s "https://api.telegram.org/bot<TOKEN>/getWebhookInfo"
# 期望 "url": "https://mw.example.com/webhook/tg" 且 "pending_update_count": 0
```
## 1.9 打开处理开关
把页面顶部的 **Enable business processing** 拨开。这是总开关:关着时 webhook 照收,但 `/worker` 与
`/reconcile` 拒绝排空,所以什么也到不了 Telegram。打开后,入站邮件与消息才会进队列、由 worker 排空。
## 1.10 端到端验证
1. 给自己发一封邮件。几秒内 bot 应当贴出一条通知,含发件人、主题与收信时间——绝不包含正文。这一条
路径同时证明 JMAP 入站、Redis、队列与 Telegram 发送全通。
2. 在 Telegram 里试 `/search 发票`。注意:搜索触发词是中文 `搜索`、`查找`、`检索`,外加 `/search`
命令——`search invoice` 这类英文关键词不会被识别为触发。
3. 从通知里的 email id 试 `/summary <email_id>`。首次调用会请求确认;只有你确认之后、且 LLM 节开启时,
摘要才会发出。
## 1.11 排程排空
Push 投递可能延迟或丢失;外部调度器是兜底。在任意一台能访问你源站的机器上,每 5–10 分钟跑一次:
```bash
MW_APP_URL=https://mw.example.com \
MW_WORKER_TOKEN=<worker token> \
MW_RECONCILE_TOKEN=<reconcile token> \
scripts/cron-drain.sh
```
脚本会排空队列(`/worker`)并重扫漏掉的 push(`/reconcile`);两者成功都回答 204 空 body。如果你把网关放
在前面,`/worker` 必须直达后端源站——原因见 deployment.md。
## 1.12 可选:Cloudflare 网关
除非你想跨两个后端做负载均衡,否则跳过。Worker 是可选的,给免费层 Cloudflare 用户提供一组负载均衡器,
不是安全层:无论用不用它,后端源站都保持可直接访问。安装见 `cloudflare-worker/README.md`;用时把第 8 步
的回调源站对准网关。
## 1.13 出问题时
- **保存后什么也没到。** 查处理开关(第 9 步),再查 `getWebhookInfo`(第 8 步)。`last_error_message`
里提到 401,说明 webhook 密钥与 Telegram 发来的不一致——重存 02 节并重新注册。
- **`/ready` 回 503。** 503 的 body 只是一个错误码,不会指名是哪个组件挂了。`/ready` 回 200 时,JSON
里有逐组件布尔值(`jmap`、`telegram`、`redis`)。常见原因:`jmap_session_url` 不可达、应用密码错、或
上次保存 `runtime_applied: false`。
- **表单提示冲突。** 两个会话同时存了;刷新重做(第 7.3 步)。
- **bot 在群里不理你。** 隐私模式开着;去 `@BotFather` 关掉(第 3 步)。
- **`/search` 回的是帮助提示。** 你的查询以拉丁词开头;触发词是 `搜索` / `查找` / `检索`。
字段级错误、TTL、Redis 键与路由表:[reference.md](reference.md)。
扩展、密钥轮换、网关与 cron 在线上的细节:[deployment.md](deployment.md)。

View File

@ -49,7 +49,9 @@ DOCS = ['AGENTS.md', 'README.md', 'README.zh-CN.md', 'docs/design.md',
'docs/design.zh-CN.md', 'docs/deployment.md', 'docs/deployment.zh-CN.md', 'docs/design.zh-CN.md', 'docs/deployment.md', 'docs/deployment.zh-CN.md',
'docs/reference.md', 'docs/reference.zh-CN.md', 'docs/retired.md', 'docs/reference.md', 'docs/reference.zh-CN.md', 'docs/retired.md',
'docs/retired.zh-CN.md', 'docs/opengaps.md', 'docs/opengaps.zh-CN.md', 'docs/retired.zh-CN.md', 'docs/opengaps.md', 'docs/opengaps.zh-CN.md',
'docs/charter.md', 'docs/charter.zh-CN.md', 'cloudflare-worker/README.md', 'docs/charter.md', 'docs/charter.zh-CN.md',
'docs/quickstart.md', 'docs/quickstart.zh-CN.md',
'cloudflare-worker/README.md',
'cloudflare-worker/README.zh-CN.md'] 'cloudflare-worker/README.zh-CN.md']
FILE = r'((?:[A-Za-z0-9_.-]+/)?[A-Za-z0-9_-]+)' FILE = r'((?:[A-Za-z0-9_.-]+/)?[A-Za-z0-9_-]+)'

View File

@ -45,7 +45,9 @@ DOCS = ['AGENTS.md', 'README.md', 'README.zh-CN.md', 'docs/design.md',
'docs/design.zh-CN.md', 'docs/deployment.md', 'docs/deployment.zh-CN.md', 'docs/design.zh-CN.md', 'docs/deployment.md', 'docs/deployment.zh-CN.md',
'docs/reference.md', 'docs/reference.zh-CN.md', 'docs/retired.md', 'docs/reference.md', 'docs/reference.zh-CN.md', 'docs/retired.md',
'docs/retired.zh-CN.md', 'docs/opengaps.md', 'docs/opengaps.zh-CN.md', 'docs/retired.zh-CN.md', 'docs/opengaps.md', 'docs/opengaps.zh-CN.md',
'docs/charter.md', 'docs/charter.zh-CN.md', 'cloudflare-worker/README.md', 'docs/charter.md', 'docs/charter.zh-CN.md',
'docs/quickstart.md', 'docs/quickstart.zh-CN.md',
'cloudflare-worker/README.md',
'cloudflare-worker/README.zh-CN.md'] 'cloudflare-worker/README.zh-CN.md']
# `Ident` or `Module::Ident` inside backticks, optional () after. # `Ident` or `Module::Ident` inside backticks, optional () after.

View File

@ -17,7 +17,9 @@ DOCS = ["AGENTS.md", "README.md", "README.zh-CN.md", "docs/design.md",
"docs/design.zh-CN.md", "docs/deployment.md", "docs/deployment.zh-CN.md", "docs/design.zh-CN.md", "docs/deployment.md", "docs/deployment.zh-CN.md",
"docs/reference.md", "docs/reference.zh-CN.md", "docs/retired.md", "docs/reference.md", "docs/reference.zh-CN.md", "docs/retired.md",
"docs/retired.zh-CN.md", "docs/opengaps.md", "docs/opengaps.zh-CN.md", "docs/retired.zh-CN.md", "docs/opengaps.md", "docs/opengaps.zh-CN.md",
"docs/charter.md", "docs/charter.zh-CN.md", "cloudflare-worker/README.md", "docs/charter.md", "docs/charter.zh-CN.md",
"docs/quickstart.md", "docs/quickstart.zh-CN.md",
"cloudflare-worker/README.md",
"cloudflare-worker/README.zh-CN.md"] "cloudflare-worker/README.zh-CN.md"]
HEADING = re.compile(r"^(#{2,4})\s+(\d+(?:\.\d+)?\.?)\s") HEADING = re.compile(r"^(#{2,4})\s+(\d+(?:\.\d+)?\.?)\s")

View File

@ -6,7 +6,9 @@ DOCS = ['AGENTS.md', 'README.md', 'README.zh-CN.md', 'docs/design.md',
'docs/design.zh-CN.md', 'docs/deployment.md', 'docs/deployment.zh-CN.md', 'docs/design.zh-CN.md', 'docs/deployment.md', 'docs/deployment.zh-CN.md',
'docs/reference.md', 'docs/reference.zh-CN.md', 'docs/retired.md', 'docs/reference.md', 'docs/reference.zh-CN.md', 'docs/retired.md',
'docs/retired.zh-CN.md', 'docs/opengaps.md', 'docs/opengaps.zh-CN.md', 'docs/retired.zh-CN.md', 'docs/opengaps.md', 'docs/opengaps.zh-CN.md',
'docs/charter.md', 'docs/charter.zh-CN.md', 'cloudflare-worker/README.md', 'docs/charter.md', 'docs/charter.zh-CN.md',
'docs/quickstart.md', 'docs/quickstart.zh-CN.md',
'cloudflare-worker/README.md',
'cloudflare-worker/README.zh-CN.md'] 'cloudflare-worker/README.zh-CN.md']
CODE = re.compile(r'`[^`\n]*`') CODE = re.compile(r'`[^`\n]*`')

View File

@ -5,7 +5,9 @@ DOCS = ['AGENTS.md', 'README.md', 'README.zh-CN.md', 'docs/design.md',
'docs/design.zh-CN.md', 'docs/deployment.md', 'docs/deployment.zh-CN.md', 'docs/design.zh-CN.md', 'docs/deployment.md', 'docs/deployment.zh-CN.md',
'docs/reference.md', 'docs/reference.zh-CN.md', 'docs/retired.md', 'docs/reference.md', 'docs/reference.zh-CN.md', 'docs/retired.md',
'docs/retired.zh-CN.md', 'docs/opengaps.md', 'docs/opengaps.zh-CN.md', 'docs/retired.zh-CN.md', 'docs/opengaps.md', 'docs/opengaps.zh-CN.md',
'docs/charter.md', 'docs/charter.zh-CN.md', 'cloudflare-worker/README.md', 'docs/charter.md', 'docs/charter.zh-CN.md',
'docs/quickstart.md', 'docs/quickstart.zh-CN.md',
'cloudflare-worker/README.md',
'cloudflare-worker/README.zh-CN.md'] 'cloudflare-worker/README.zh-CN.md']
errors = [] errors = []