POST /reconcile 是后端同步长任务:后端持集群级单飞锁 lock:reconcile(初租 300 s, 期间每 30 s 心跳续租至 90 s,notify.rs:159/172),但网关原先只有全局超时 10 s、 最多 2 次尝试。一次完整对账很容易撑过网关窗口被误判失败,随后故障转移到第二实例, 而第二实例必然立刻返回 409;客户端既拿不到 204 也拿不到 409,只能看到 "all backends failed"。 改动: - handleFetch 为 /reconcile 单独覆盖超时与尝试次数:超时取 LB_RECONCILE_TIMEOUT_MS (默认 320000 ms,大于锁初租 300 s + 心跳余量),maxAttempts=1 绝不故障转移 (切实例只会撞锁,没有任何收益) - 其余快路径完全不受影响,仍用全局 LB_REQUEST_TIMEOUT_MS / LB_MAX_ATTEMPTS - 新增测试:记录 setTimeout 收到的延迟来断言 /reconcile 走 320000 ms,并用 503 响应 断言它只尝试 1 个 origin;同时断言 /webhook/tg 仍用 5000 ms 且仍故障转移到第二 实例;并断言 env 覆盖生效而 maxAttempts 恒为 1 - worker README 补 LB_RECONCILE_TIMEOUT_MS 说明与 /reconcile 例外 - deployment.md §6.3.1 删掉「请把 LB_REQUEST_TIMEOUT_MS 调到大于锁租期」的旧 workaround 建议(现已由 per-route 覆盖解决),§10 worker 故障转移条目补例外,secret 清单补新变量 - 顺手清掉 health.js 注释里已退役的 GATE-READY-DEPS,改为 ARCH-READY-BASELINE 验证:cloudflare-worker node --test 29/29 通过(原 28 + 新增 1); web config.test.mjs 6/6 通过。无 src/ 改动,Rust 门禁不受影响。 |
||
|---|---|---|
| .github/workflows | ||
| cloudflare-worker | ||
| docs | ||
| src | ||
| web | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| AGENTS.md | ||
| Cargo.lock | ||
| Cargo.toml | ||
| Dockerfile | ||
| README.md | ||
| README.zh-CN.md | ||
| hoststack.yaml | ||
README.md
MessageWeave
中文文档 / Chinese documentation → README.zh-CN.md
One Telegram bot per mailbox. New mail arrives as metadata; the original is always fetched read-only from your own JMAP server.
1. What it is
MessageWeave sits between a JMAP mail server (for example Stalwart) and a Telegram bot.
- Inbound. A JMAP push subscription tells the service that a message changed. Nothing is polled at rest.
- Outbound. Each event becomes a short, metadata-only Telegram notification: sender, subject, timestamp. Mail bodies are never sent to Telegram.
- On demand. When you ask, the service reads the original through a fresh JMAP read call. Nothing is cached in between.
- Optionally summarised. If you explicitly opt in, an OpenAI-compatible LLM returns a short summary. Requests, responses and attachment bytes are never persisted.
State is an architectural boundary here, not a preference.
- Redis is the only state store. No database, no embedded store, no second store.
- The process writes nothing to disk. No log files, no data files, no local volume.
- No long-lived connections. No SSE, no WebSocket, no long polling.
- Logs go to stdout only. The platform's collector owns log storage and retention.
- What it is not. It is not a mail client, not a proxy, and not a relay. It forwards metadata to one bot; it does not read arbitrary URLs, open tunnels, or broker traffic.
2. 5-minute setup
Requires a reachable Redis 7 instance and a JMAP session plus a Telegram bot token.
cp .env.example .env # fill REDIS_URL and CONFIG_ENCRYPTION_KEY
docker build -t messageweave:latest .
docker run --env-file .env -p 8080:8080 messageweave:latest
Then open http://localhost:8080 in a browser. The first-boot SPA calls
POST /api/bootstrap, stores the business secrets in Redis, and creates the admin session.
From that point the browser configures the service through Redis — no environment variable
is read or written again for business configuration.
For local builds without Docker: cargo build --locked produces the message-weave binary.
If the service answers requests but does nothing useful, check the logs for a startup warning: a missing required variable does not crash the process. It serves a read-only setup router over in-memory state instead. See §3.
3. Configuration entry points
Three layers. They are not interchangeable.
Boot-time environment — 2 required, 2 optional.
| Variable | Required | Default |
|---|---|---|
REDIS_URL |
yes | — |
CONFIG_ENCRYPTION_KEY |
yes | — |
PORT |
no | 8080 |
RUN_MODE |
no | webhook |
RUN_MODE accepts webhook or reconcile. .env.example ships only the two required
variables; the rest are documented defaults.
Business configuration — Redis, written by the browser.
POST /api/bootstrap is the one-shot trust bootstrap; PUT /api/business-config hot-
reloads individual settings within about one second. The process never reads business
secrets from the environment in normal operation.
Health.
GET /healthz returns 200 unconditionally — it is liveness, not readiness. GET /ready
returns 503 until configuration, Redis, and both upstreams (a JMAP session GET and
Telegram getMe, 3s each) are reachable. Probe /ready, not /healthz, when deciding
whether to route traffic — but /ready is the heavier probe (~3s worst case), so a monitor
that must stay cheap should watch the gateway's aggregated /healthz.
4. Security boundary, in one sentence
All state lives in Redis; the process reads only two environment variables and writes nothing to disk.
One corollary matters for setup: the bootstrap one-shot token is the password component of
REDIS_URL. The Redis ACL credential is therefore both the application connection
credential and the trust root for bootstrap — keep it private and rotate it as one value.
5. Where to read more
| Document | What it answers |
|---|---|
docs/design.md |
Why the system is shaped this way: data flow, JMAP semantics, Redis streams, AI consent rules |
docs/deployment.md |
How to deploy: Dockerfile, secrets, Redis hosting, webhook/push/reconcile routing, multi-instance load balancing |
docs/reference.md |
Single source of truth for verifiable facts — Redis keys and TTLs, error codes, routes, environment layers, budgets |
docs/roadmap.md |
Gaps, blockers and the next phase |
docs/retired.md |
What was tried and dropped — abandoned routes, unreleased designs, and names that never existed |
AGENTS.md |
Contribution rules, hard boundaries, the stable-ID index for cross-document references |
Facts that matter are traceable. If two documents disagree, docs/reference.md wins.