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:
parent
74d53ae542
commit
7ed1a086fe
|
|
@ -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 |
|
||||||
|
|
|
||||||
|
|
@ -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、错误码、路由、环境变量分层、预算常量 |
|
||||||
|
|
|
||||||
|
|
@ -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** |
|
||||||
|
|
|
||||||
|
|
@ -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、配置项、错误码、出站常量 | **可核对事实** |
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
@ -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)。
|
||||||
|
|
@ -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_-]+)'
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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]*`')
|
||||||
|
|
|
||||||
|
|
@ -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 = []
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue