docs: 同步 /worker 进 safelist(17 -> 18 条),cron 示例收敛到单一域名
- deployment.md:§6.3.1 两条端点都可直接打 Worker 域名,手动 curl 与 crontab、
k8s CronJob 示例去掉 WORKER_ORIGIN 只留 APP_URL,-m 分别给 320s / 300s(要略大于
LB 侧 per-route 超时,否则 curl 先掐掉一个本来能完成的排空);§10.4 safelist
18 条并补 /worker、不在 safelist 的从 3 类减为 2 类;LB_WORKER_TIMEOUT_MS 补进
secret put 清单与 §10.9 变量表;测试计数 35 -> 37(index 12 -> 14)
- README.md:路由白名单 17 -> 18 条、/worker 入列、LB_WORKER_TIMEOUT_MS 补进
变量表与配置片段、/reconcile 例外段落扩成 /reconcile + /worker 两条长超时
- charter.md / reference.md:17 -> 18 条、Safe Routes 16 -> 18 entries
- cron-drain.sh:MW_WORKER_URL 由「必须指源站直连地址」改为「可选,默认走平台域名」;
404 route not forwarded 的提示改为 LB 版本太旧(需 >= 2026.10.3),升级或绕过 LB
顺手修正 6 处已漂移的行号锚点(此前指向空行或已迁移的文件):
backends.js:10-28 -> 10-30、index.js:47-65 -> 54-73、index.js:76-78 -> 87-91、
index.js:87-89 -> 95-96、index.js:93 -> 105、src/index.js:138 -> src/lb.js:155
(x-lb-backend 的 catch 在 656034d 修不可变响应头时已迁到 lb.js,文档一直指旧文件)。
GATE-DOCS PASS(115 line-refs / 477 path-refs / 252 section-refs,0 error)。
This commit is contained in:
parent
6bb57801ac
commit
f9889288ba
|
|
@ -3,7 +3,7 @@
|
||||||
统一 HTTPS 入口 + 多后端 origin 故障转移(HA/LB 子项目)。
|
统一 HTTPS 入口 + 多后端 origin 故障转移(HA/LB 子项目)。
|
||||||
**透传模型**:Worker 不感知业务,原样转发请求到多个 https 后端 origin;仅「超时 / 5xx」做有界故障转移。
|
**透传模型**:Worker 不感知业务,原样转发请求到多个 https 后端 origin;仅「超时 / 5xx」做有界故障转移。
|
||||||
|
|
||||||
> 本组件是 **safelist 受限的边缘负载均衡器(edge load balancer)**:路由固定 17 条白名单、
|
> 本组件是 **safelist 受限的边缘负载均衡器(edge load balancer)**:路由固定 18 条白名单、
|
||||||
> 后端 origin 在部署期固定且仅允许 https、未知路径一律 404。它只对固定后端做请求转发与
|
> 后端 origin 在部署期固定且仅允许 https、未知路径一律 404。它只对固定后端做请求转发与
|
||||||
> 故障转移,不接受任意目标主机,也不提供任何形式的流量中转或访问隐藏能力。
|
> 故障转移,不接受任意目标主机,也不提供任何形式的流量中转或访问隐藏能力。
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
## 目录
|
## 目录
|
||||||
|
|
||||||
- `wrangler.toml` — Wrangler 示例配置(name/main/compat/vars;后端 secret 走 `wrangler secret`)
|
- `wrangler.toml` — Wrangler 示例配置(name/main/compat/vars;后端 secret 走 `wrangler secret`)
|
||||||
- `src/index.js` — Worker 入口(路由分发、/healthz 聚合、safelist 校验)
|
- `src/index.js` — Worker 入口(路由分发、/healthz-worker 聚合、safelist 校验)
|
||||||
- `src/backends.js` — origin 解析 + https/凭据/query 校验(C-HTTPS-INBOUND)
|
- `src/backends.js` — origin 解析 + https/凭据/query 校验(C-HTTPS-INBOUND)
|
||||||
- `src/lb.js` — 透传转发 + 有界故障转移(SAF-LB-PASSTHRU)
|
- `src/lb.js` — 透传转发 + 有界故障转移(SAF-LB-PASSTHRU)
|
||||||
- `src/health.js` — 健康聚合探针(MOD-HEALTH-AGG / C-NO-DB)
|
- `src/health.js` — 健康聚合探针(MOD-HEALTH-AGG / C-NO-DB)
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
|
|
||||||
### 路由 safelist(ARCH-LB-WORKER / C-LB-SINGLE-REG-URL)
|
### 路由 safelist(ARCH-LB-WORKER / C-LB-SINGLE-REG-URL)
|
||||||
- 透传公开 `GET /api/status` 启动状态,以及管理 SPA API:`POST /api/admin/session[/revoke]`、`GET|PUT /api/enabled`(业务总开关,SPA 服务卡片读写)、`GET|PUT /api/config`、`GET|PUT /api/business-config`(读回 + 增量保存)、`POST /api/business-config/preflight`(保存前逐组件连通性预检)。启动状态只包含缺少的环境变量名称;管理 API 的鉴权仍由后端执行。
|
- 透传公开 `GET /api/status` 启动状态,以及管理 SPA API:`POST /api/admin/session[/revoke]`、`GET|PUT /api/enabled`(业务总开关,SPA 服务卡片读写)、`GET|PUT /api/config`、`GET|PUT /api/business-config`(读回 + 增量保存)、`POST /api/business-config/preflight`(保存前逐组件连通性预检)。启动状态只包含缺少的环境变量名称;管理 API 的鉴权仍由后端执行。
|
||||||
- 只透传 `POST /webhook/tg`、`POST /push/jmap`、`POST /api/push/register`、`POST /api/push/disable`、`POST /reconcile`、`GET /ready`。
|
- 只透传 `POST /webhook/tg`、`POST /push/jmap`、`POST /api/push/register`、`POST /api/push/disable`、`POST /reconcile`、`POST /worker`、`GET /ready`。
|
||||||
- 其它路径 → **404**(不透传,避免 Worker 沦为后端任意路径的跳板)。
|
- 其它路径 → **404**(不透传,避免 Worker 沦为后端任意路径的跳板)。
|
||||||
- method 不符 → **405**。
|
- method 不符 → **405**。
|
||||||
|
|
||||||
|
|
@ -45,12 +45,18 @@
|
||||||
- **仅** 超时(AbortError)或 5xx 触发换下一个实例;4xx / 2xx / 3xx 直接返回。
|
- **仅** 超时(AbortError)或 5xx 触发换下一个实例;4xx / 2xx / 3xx 直接返回。
|
||||||
- 起点按 `rng` 随机化(默认 `Math.random`),实现双活分摊;单 origin 配置时退化为确定性。
|
- 起点按 `rng` 随机化(默认 `Math.random`),实现双活分摊;单 origin 配置时退化为确定性。
|
||||||
- 全失败 → **503 All Backends Unavailable**(交由 Telegram / Stalwart 自动重投兜底,不丢消息)。
|
- 全失败 → **503 All Backends Unavailable**(交由 Telegram / Stalwart 自动重投兜底,不丢消息)。
|
||||||
- 健康缓存(`aggregateHealth` / `LB_HEALTH_TTL_MS`)仅服务于 `/healthz` 聚合视图;转发目前**不做健康路由过滤**(`docs/deployment.md` §10.4「只向健康实例转发」为后续增强,当前靠故障转移兜底)。
|
- 健康缓存(`aggregateHealth` / `LB_HEALTH_TTL_MS`)仅服务于 `/healthz-worker` 聚合视图;转发目前**不做健康路由过滤**(`docs/deployment.md` §10.4「只向健康实例转发」为后续增强,当前靠故障转移兜底)。
|
||||||
- **例外:`POST /reconcile` 走 per-route 覆盖。** 它是后端同步长任务——后端持集群级锁 `lock:reconcile`(初租 300s,期间由心跳续期),全局 10s 超时会把它误判为失败并故障转移到第二实例,而第二实例必然立刻返回 409。所以该路由改为:超时取 `LB_RECONCILE_TIMEOUT_MS`(默认 `320000`,留足锁初租 + 心跳余量),且 `maxAttempts=1` 绝不故障转移。其余快路径保持全局默认。
|
- **例外:`POST /reconcile` 与 `POST /worker` 走 per-route 覆盖。** 两者都是后端同步长任务:
|
||||||
|
全局 10s 超时会把它们误判为失败并故障转移到第二实例,而第二实例对 `/reconcile` 必然立刻
|
||||||
|
返回 409(集群级锁 `lock:reconcile`,初租 300s 由心跳续期),对 `/worker` 只会重复排同一批
|
||||||
|
消息。所以这两条路由改为:超时分别取 `LB_RECONCILE_TIMEOUT_MS`(默认 `320000`,留足锁初租 +
|
||||||
|
心跳余量)与 `LB_WORKER_TIMEOUT_MS`(默认 `300000`,等于后端单条事件下界),且 `maxAttempts=1`
|
||||||
|
绝不故障转移。其余快路径保持全局默认。
|
||||||
|
|
||||||
### 健康聚合(MOD-HEALTH-AGG;C-NO-DB / C-REDIS-ONLY-STATE)
|
### 健康聚合(MOD-HEALTH-AGG;C-NO-DB / C-REDIS-ONLY-STATE)
|
||||||
- `GET /healthz`(由 Worker 自身承载):按 TTL 缓存(默认 30s)探测各后端 `/healthz`,
|
- `GET /healthz-worker`(由 Worker 自身承载;入站 `/healthz` 保留给源站自己的健康检查并透传,两者互不遮蔽):
|
||||||
返回 `{status: ok|down, available, total, backends:[{origin, up, status}]}`;
|
按 TTL 缓存(默认 30s)探测各后端 `/healthz`,返回
|
||||||
|
`{status: ok|down, version, available, total, backends:[{origin, up, status}]}`;
|
||||||
有 ≥1 后端 up → 200;全部 down → 503。
|
有 ≥1 后端 up → 200;全部 down → 503。
|
||||||
- **不代理 Redis/JMAP**:Worker 不做数据库侧检查;后端端到端就绪(配置完整性 + Redis 可达 + JMAP session + Telegram getMe,对应 `ARCH-READY-BASELINE`)由 `/ready`(透传)承担。
|
- **不代理 Redis/JMAP**:Worker 不做数据库侧检查;后端端到端就绪(配置完整性 + Redis 可达 + JMAP session + Telegram getMe,对应 `ARCH-READY-BASELINE`)由 `/ready`(透传)承担。
|
||||||
|
|
||||||
|
|
@ -64,10 +70,11 @@
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `BACKEND_ORIGINS_JSON` | 是 | JSON 数组,形如 `["https://a.platform1.example","https://b.platform2.example"]`;必须全部 https、无内嵌凭据/query/fragment/路径。已写在 `wrangler.toml` 的 `[vars]`。 |
|
| `BACKEND_ORIGINS_JSON` | 是 | JSON 数组,形如 `["https://a.platform1.example","https://b.platform2.example"]`;必须全部 https、无内嵌凭据/query/fragment/路径。已写在 `wrangler.toml` 的 `[vars]`。 |
|
||||||
| `LB_REQUEST_TIMEOUT_MS` | 否 | 单 origin 请求超时;默认 `10000`。建议 > 最坏 cold start + 最长 JMAP 拉取。 |
|
| `LB_REQUEST_TIMEOUT_MS` | 否 | 单 origin 请求超时;默认 `10000`。建议 > 最坏 cold start + 最长 JMAP 拉取。 |
|
||||||
| `LB_MAX_ATTEMPTS` | 否 | 每请求最多 origin 尝试次数;默认 `2`(= 1 次故障转移)。`POST /reconcile` 固定为 `1`。 |
|
| `LB_MAX_ATTEMPTS` | 否 | 每请求最多 origin 尝试次数;默认 `2`(= 1 次故障转移)。`POST /reconcile` 与 `POST /worker` 固定为 `1`。 |
|
||||||
| `LB_RECONCILE_TIMEOUT_MS` | 否 | 仅 `POST /reconcile` 的单 origin 超时覆盖;默认 `320000`(须大于后端锁初租 300s + 心跳余量)。 |
|
| `LB_RECONCILE_TIMEOUT_MS` | 否 | 仅 `POST /reconcile` 的单 origin 超时覆盖;默认 `320000`(须大于后端锁初租 300s + 心跳余量)。 |
|
||||||
|
| `LB_WORKER_TIMEOUT_MS` | 否 | 仅 `POST /worker` 的单 origin 超时覆盖;默认 `300000`(= 后端单条事件下界 `SINGLE_EVENT_CEILING_FLOOR_MS`)。一次排空 batch 条(默认 10,硬上限 10),batch 拉大时需相应上调。 |
|
||||||
| `LB_HEALTH_TTL_MS` | 否 | 健康探测缓存 TTL;默认 `30000`。 |
|
| `LB_HEALTH_TTL_MS` | 否 | 健康探测缓存 TTL;默认 `30000`。 |
|
||||||
| `LB_VERSION` | 否 | `/healthz-worker` 响应体里的 `version` 字段,用于确认线上是哪次部署在回答;取自 `wrangler.toml` 的 `[vars]`(当前 `2026.10.2`),改 LB 逻辑的提交同时 bump |
|
| `LB_VERSION` | 否 | `/healthz-worker` 响应体里的 `version` 字段,用于确认线上是哪次部署在回答;取自 `wrangler.toml` 的 `[vars]`(当前 `2026.10.3`),改 LB 逻辑的提交同时 bump |
|
||||||
|
|
||||||
> 默认写在 `wrangler.toml` 的 `[vars]`(**明文例外**,因为当前值只是公开可达的 https origin,
|
> 默认写在 `wrangler.toml` 的 `[vars]`(**明文例外**,因为当前值只是公开可达的 https origin,
|
||||||
> 不是凭据)。一旦要放内网地址、带内嵌凭据的 URL、或不想公开的 staging 主机名,
|
> 不是凭据)。一旦要放内网地址、带内嵌凭据的 URL、或不想公开的 staging 主机名,
|
||||||
|
|
@ -96,7 +103,8 @@ npm install --no-audit --no-fund
|
||||||
# BACKEND_ORIGINS_JSON 已写在 wrangler.toml 的 [vars],直接部署:
|
# BACKEND_ORIGINS_JSON 已写在 wrangler.toml 的 [vars],直接部署:
|
||||||
npx wrangler deploy
|
npx wrangler deploy
|
||||||
# 可选调参(默认值已够用;要覆盖就用 dashboard 的 Add Variable):
|
# 可选调参(默认值已够用;要覆盖就用 dashboard 的 Add Variable):
|
||||||
# LB_REQUEST_TIMEOUT_MS / LB_MAX_ATTEMPTS / LB_RECONCILE_TIMEOUT_MS / LB_HEALTH_TTL_MS
|
# LB_REQUEST_TIMEOUT_MS / LB_MAX_ATTEMPTS / LB_HEALTH_TTL_MS
|
||||||
|
# LB_RECONCILE_TIMEOUT_MS / LB_WORKER_TIMEOUT_MS
|
||||||
# 仅当 [vars] 里要放非公开值时才改用 secret 注入:
|
# 仅当 [vars] 里要放非公开值时才改用 secret 注入:
|
||||||
npx wrangler secret put BACKEND_ORIGINS_JSON
|
npx wrangler secret put BACKEND_ORIGINS_JSON
|
||||||
# 部署后确认:
|
# 部署后确认:
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,7 @@ Stalwart 与 Telegram 的业务凭据已在真机联调通过——出站、查
|
||||||
| `SAF-DEBUG-GATE` | src/main.rs / src/debug.rs / docs/design.md §7.6 / docs/deployment.md §2.1 | 双因子门禁:「`DEBUG_ENABLED` 为真值或命令行带 `--debug`」**且** `DEBUG_TOKEN` 非空才挂载路由;缺任一完全不挂载(请求落通用 `404`),默认绝对关闭。开启信号走 env 而非 argv,使启动命令保持静态、开关可在平台控制台单点切换 | 安全 |
|
| `SAF-DEBUG-GATE` | src/main.rs / src/debug.rs / docs/design.md §7.6 / docs/deployment.md §2.1 | 双因子门禁:「`DEBUG_ENABLED` 为真值或命令行带 `--debug`」**且** `DEBUG_TOKEN` 非空才挂载路由;缺任一完全不挂载(请求落通用 `404`),默认绝对关闭。开启信号走 env 而非 argv,使启动命令保持静态、开关可在平台控制台单点切换 | 安全 |
|
||||||
| `SAF-DEBUG-AUTH` | src/debug.rs / docs/deployment.md §2.1 | 挂载后 `/debug/*` 须 `Authorization: Bearer DEBUG_TOKEN` 常数时间比较,失败 `401` 且无副作用 | 安全 |
|
| `SAF-DEBUG-AUTH` | src/debug.rs / docs/deployment.md §2.1 | 挂载后 `/debug/*` 须 `Authorization: Bearer DEBUG_TOKEN` 常数时间比较,失败 `401` 且无副作用 | 安全 |
|
||||||
| `REQ-DEBUG-ENDPOINTS` | src/debug.rs / docs/reference.md §3 / docs/deployment.md §2.1 | 端点契约:`GET /debug/ping`、`/config`、`/redis`、`/jmap`、`/telegram`、`/worker` 均只读;`POST /debug/notify` 走真实出站链路发一条测试消息;响应体不含 secret 原文(凭据字段只出 `*_configured` 布尔,非密文的身份与预算字段仍明文返回) | 需求 |
|
| `REQ-DEBUG-ENDPOINTS` | src/debug.rs / docs/reference.md §3 / docs/deployment.md §2.1 | 端点契约:`GET /debug/ping`、`/config`、`/redis`、`/jmap`、`/telegram`、`/worker` 均只读;`POST /debug/notify` 走真实出站链路发一条测试消息;响应体不含 secret 原文(凭据字段只出 `*_configured` 布尔,非密文的身份与预算字段仍明文返回) | 需求 |
|
||||||
| `SAF-DEBUG-ORIGIN-ONLY` | docs/deployment.md §2.1 / docs/reference.md §4 | `/debug/*` 不在网关 17 条安全路由内,Worker 一律 `404 route not forwarded`;只能直连后端 origin,公网不可达 | 安全 |
|
| `SAF-DEBUG-ORIGIN-ONLY` | docs/deployment.md §2.1 / docs/reference.md §4 | `/debug/*` 不在网关 18 条安全路由内,Worker 一律 `404 route not forwarded`;只能直连后端 origin,公网不可达 | 安全 |
|
||||||
| `SAF-DEBUG-ALLOWLIST` | src/debug.rs / docs/deployment.md §2.1 | `POST /debug/notify` 仅在 chat 白名单**非空**时校验 `chat_id`;白名单未配置(空)时不拦截,故启用本面须确认业务白名单已配置 | 安全 |
|
| `SAF-DEBUG-ALLOWLIST` | src/debug.rs / docs/deployment.md §2.1 | `POST /debug/notify` 仅在 chat 白名单**非空**时校验 `chat_id`;白名单未配置(空)时不拦截,故启用本面须确认业务白名单已配置 | 安全 |
|
||||||
| `NFR-HA-MULTI-INSTANCE` | docs/deployment.md §10.7 / §9.1 | 多实例高可用语义;双活或主备均可;Redis 单点故障不在方案范围(用户外部解决) | 非功能 |
|
| `NFR-HA-MULTI-INSTANCE` | docs/deployment.md §10.7 / §9.1 | 多实例高可用语义;双活或主备均可;Redis 单点故障不在方案范围(用户外部解决) | 非功能 |
|
||||||
| `C-NO-DB` | docs/deployment.md §0 / §9.1 / §3 | 生产不使用任何数据库(无 SQLite/Postgres/MySQL/嵌入式),Redis 为唯一状态存储;应用不连接第二个数据库 | 约束 |
|
| `C-NO-DB` | docs/deployment.md §0 / §9.1 / §3 | 生产不使用任何数据库(无 SQLite/Postgres/MySQL/嵌入式),Redis 为唯一状态存储;应用不连接第二个数据库 | 约束 |
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@
|
||||||
|
|
||||||
- **绝不回显凭据值**:`debug_config`(`src/debug.rs:96-152`)对每个 Secret 字段只输出 `*_configured` **布尔**——JMAP 密码、bot token、`worker_token`、`reconcile_token`、LLM `api_key` 一律不落响应体(`SAF-DEBUG-AUTH`)。注意这是「不含凭据」,不是「全脱敏」:**非密文的身份与预算字段是明文返回的**(JMAP session URL 与 username、Telegram `chat_id`、LLM `base_url`/`model`、各类超时与重试数),所以该面仍只能放在可信网络上。
|
- **绝不回显凭据值**:`debug_config`(`src/debug.rs:96-152`)对每个 Secret 字段只输出 `*_configured` **布尔**——JMAP 密码、bot token、`worker_token`、`reconcile_token`、LLM `api_key` 一律不落响应体(`SAF-DEBUG-AUTH`)。注意这是「不含凭据」,不是「全脱敏」:**非密文的身份与预算字段是明文返回的**(JMAP session URL 与 username、Telegram `chat_id`、LLM `base_url`/`model`、各类超时与重试数),所以该面仍只能放在可信网络上。
|
||||||
- **`/debug/notify` 受 chat 白名单约束,但空白名单不拦截**:它复用生产同一份白名单快照(`src/notify.rs:1563`),判定条件是「白名单**非空**且 `chat_id` 不在其中」才回 `403 chat_not_allowed`(`src/debug.rs:257-260`)。因此已配置白名单时无法绕过业务侧发送限制;若白名单未配置(为空)则此判定不生效,`chat_id` 可任意指定——所以启用本面时应确认业务白名单已真正配置。`text` 缺省为固定联调文案,并按 1024 字符截断(`src/debug.rs:265-270`)。
|
- **`/debug/notify` 受 chat 白名单约束,但空白名单不拦截**:它复用生产同一份白名单快照(`src/notify.rs:1563`),判定条件是「白名单**非空**且 `chat_id` 不在其中」才回 `403 chat_not_allowed`(`src/debug.rs:257-260`)。因此已配置白名单时无法绕过业务侧发送限制;若白名单未配置(为空)则此判定不生效,`chat_id` 可任意指定——所以启用本面时应确认业务白名单已真正配置。`text` 缺省为固定联调文案,并按 1024 字符截断(`src/debug.rs:265-270`)。
|
||||||
- **不在 Worker 白名单内,只能直连 origin**:网关的 17 条安全路由(`cloudflare-worker/src/backends.js:10-28`)不含任何 `/debug/*`,Worker 对未白名单路径返回 `404 route not forwarded: /debug/...`(`cloudflare-worker/src/index.js:87-89`)。因此 `/debug/*` 只能通过直连后端 origin 访问;若必须经代理,请自行在代理层加鉴权,不要让公网可达。
|
- **不在 Worker 白名单内,只能直连 origin**:网关的 18 条安全路由(`cloudflare-worker/src/backends.js:10-30`)不含任何 `/debug/*`,Worker 对未白名单路径返回 `404 route not forwarded: /debug/...`(`cloudflare-worker/src/index.js:95-96`)。因此 `/debug/*` 只能通过直连后端 origin 访问;若必须经代理,请自行在代理层加鉴权,不要让公网可达。
|
||||||
|
|
||||||
> **建议**:生产环境不开启。需要远程联调时在控制台设置 `DEBUG_ENABLED=1` 与一次性
|
> **建议**:生产环境不开启。需要远程联调时在控制台设置 `DEBUG_ENABLED=1` 与一次性
|
||||||
> `DEBUG_TOKEN`,重新部署后联调;结束立即把 `DEBUG_ENABLED` 置空并轮换 `DEBUG_TOKEN`。
|
> `DEBUG_TOKEN`,重新部署后联调;结束立即把 `DEBUG_ENABLED` 置空并轮换 `DEBUG_TOKEN`。
|
||||||
|
|
@ -409,7 +409,8 @@ GET /ready 公开就绪探针;检查配置完整性 + Redis 可达
|
||||||
| 作用 | 增量入队 + 推进游标 | 排空两条 stream 并发通知 |
|
| 作用 | 增量入队 + 推进游标 | 排空两条 stream 并发通知 |
|
||||||
| 调度顺序 | 先 | 紧随其后 |
|
| 调度顺序 | 先 | 紧随其后 |
|
||||||
| 鉴权 | Bearer + `reconcile_token` | Bearer + `worker_token` |
|
| 鉴权 | Bearer + `reconcile_token` | Bearer + `worker_token` |
|
||||||
| 可走 Worker 吗 | 可以(在 `SAFE_ROUTES` 内) | 不行,必须直连后端 origin |
|
| 可走 Worker 吗 | 可以(在 `SAFE_ROUTES` 内) | 可以(在 `SAFE_ROUTES` 内) |
|
||||||
|
| LB 超时 | `LB_RECONCILE_TIMEOUT_MS` 默认 320s,`maxAttempts=1` 不故障转移 | `LB_WORKER_TIMEOUT_MS` 默认 300s,`maxAttempts=1` 不故障转移 |
|
||||||
| 幂等 | `SET NX lock:reconcile 300s` 单飞锁,撞锁回 409 | 无单飞锁,靠逐条 `delivery:*` 去重声明防重投 |
|
| 幂等 | `SET NX lock:reconcile 300s` 单飞锁,撞锁回 409 | 无单飞锁,靠逐条 `delivery:*` 去重声明防重投 |
|
||||||
| 单次上限 | 100 条变更(`src/notify.rs:294`) | 10 条消息(`src/notify.rs:407`) |
|
| 单次上限 | 100 条变更(`src/notify.rs:294`) | 10 条消息(`src/notify.rs:407`) |
|
||||||
| 常态返回 | 204 | 204(**不能当成功信号**) |
|
| 常态返回 | 204 | 204(**不能当成功信号**) |
|
||||||
|
|
@ -420,8 +421,11 @@ admin-session 兜底**(`src/notify.rs:395`),这是有意的:cron 不需
|
||||||
token 轮换后**无需重启**——worker 处理函数开头先 `refresh_business_config`(`src/notify.rs:390`),
|
token 轮换后**无需重启**——worker 处理函数开头先 `refresh_business_config`(`src/notify.rs:390`),
|
||||||
再从热更新快照取值(`:385`,快照由 `auth_snapshot` 读取)。
|
再从热更新快照取值(`:385`,快照由 `auth_snapshot` 读取)。
|
||||||
|
|
||||||
`/worker` 不在 Worker 转发白名单里(`SAFE_ROUTES`:`/reconcile` 在内、`/worker` 不在),
|
两个端点都在 Worker 转发白名单里,**cron 打 Worker 域名和打后端 origin 都能用**。它们走
|
||||||
走 Worker 会被 404 `route not forwarded` 挡掉,必须打后端 origin。见 §10.4。
|
per-route 覆盖:各自用独立的更长超时(见上表)并且 `maxAttempts=1` 绝不故障转移——全局 10s
|
||||||
|
超时会把一次正常的排空误判成失败,而第二个实例对 `/reconcile` 必然撞锁 409、对 `/worker`
|
||||||
|
只会重复排同一批消息。`/worker` 此前被挡在白名单外(走 Worker 是 404 `route not forwarded`),
|
||||||
|
现已加入;它和 `/reconcile` 一样只接受 Bearer token,不带人工流量。见 §10.4。
|
||||||
|
|
||||||
不想自己拼 curl 的话,仓库里有现成脚本 `scripts/cron-drain.sh`:两步都做了,参数全部走
|
不想自己拼 curl 的话,仓库里有现成脚本 `scripts/cron-drain.sh`:两步都做了,参数全部走
|
||||||
环境变量(`MW_APP_URL` / `MW_WORKER_URL` / `MW_RECONCILE_TOKEN` / `MW_WORKER_TOKEN`
|
环境变量(`MW_APP_URL` / `MW_WORKER_URL` / `MW_RECONCILE_TOKEN` / `MW_WORKER_TOKEN`
|
||||||
|
|
@ -444,28 +448,29 @@ curl -sS -o /dev/null -w '%{http_code}\n' \
|
||||||
--max-time 60
|
--max-time 60
|
||||||
```
|
```
|
||||||
|
|
||||||
`/worker` 也先手动跑一遍,但要打**后端 origin 直连地址**(它不在 Worker 转发白名单里,见 §10.4)。空请求体是合法的,等价于取默认 10 条,不用带 `-d`:
|
`/worker` 也先手动跑一遍,可以直接打 Worker 域名(它与 `/reconcile` 一样在转发白名单里,见 §10.4)。空请求体是合法的,等价于取默认 10 条,不用带 `-d`;它可能跑几分钟,`-m` 要给够(LB 侧默认 `LB_WORKER_TIMEOUT_MS` = 300s):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sS -o /dev/null -w '%{http_code}\n' \
|
curl -sS -o /dev/null -w '%{http_code}\n' \
|
||||||
-X POST "https://<后端origin直连地址>/worker" \
|
-X POST "https://<你的平台URL>/worker" \
|
||||||
-H "Authorization: Bearer <WORKER_TOKEN>" \
|
-H "Authorization: Bearer <WORKER_TOKEN>" \
|
||||||
--max-time 60
|
--max-time 300
|
||||||
```
|
```
|
||||||
|
|
||||||
最小调度(两步一条 cron,顺序不能反):
|
最小调度(两步一条 cron,顺序不能反):
|
||||||
|
|
||||||
```
|
```
|
||||||
# 每 5 分钟:先对账入队,再排空队列
|
# 每 5 分钟:先对账入队,再排空队列(两步都走 Worker 域名,顺序不能反)
|
||||||
*/5 * * * * curl -fsS -m 60 -X POST https://<你的平台URL>/reconcile \
|
*/5 * * * * curl -fsS -m 320 -X POST https://<你的平台URL>/reconcile \
|
||||||
-H "Authorization: Bearer $RECONCILE_TOKEN" >/dev/null ; \
|
-H "Authorization: Bearer $RECONCILE_TOKEN" >/dev/null ; \
|
||||||
curl -fsS -m 60 -X POST https://<后端origin直连地址>/worker \
|
curl -fsS -m 300 -X POST https://<你的平台URL>/worker \
|
||||||
-H "Authorization: Bearer $WORKER_TOKEN" >/dev/null
|
-H "Authorization: Bearer $WORKER_TOKEN" >/dev/null
|
||||||
```
|
```
|
||||||
|
|
||||||
- 两条之间用 `;` 而不是 `&&`:对账撞锁拿 409、或临时 503,都不该阻断排空——上一次对账
|
- 两条之间用 `;` 而不是 `&&`:对账撞锁拿 409、或临时 503,都不该阻断排空——上一次对账
|
||||||
入队的消息同样需要被消费。
|
入队的消息同样需要被消费。
|
||||||
- 排空那条必须是**后端 origin 的直连地址**,不是 Worker 域名。
|
- 两条都用 Worker 域名即可(都在转发白名单里);`-m` 要比 LB 侧的 per-route 超时略大,
|
||||||
|
否则 curl 会先掐掉一个本来能完成的排空。
|
||||||
- 请求体可带 `{"batch":N}`;`N` 会被压回上限 10,不会因传大值而报错(`src/notify.rs:407`)。
|
- 请求体可带 `{"batch":N}`;`N` 会被压回上限 10,不会因传大值而报错(`src/notify.rs:407`)。
|
||||||
传非法 JSON 才是 400 `invalid_request`(`src/notify.rs:411`)。
|
传非法 JSON 才是 400 `invalid_request`(`src/notify.rs:411`)。
|
||||||
|
|
||||||
|
|
@ -493,15 +498,13 @@ spec:
|
||||||
command: ["/bin/sh", "-c"]
|
command: ["/bin/sh", "-c"]
|
||||||
args:
|
args:
|
||||||
- |
|
- |
|
||||||
curl -fsS -m 60 -X POST "${APP_URL}/reconcile" \
|
curl -fsS -m 320 -X POST "${APP_URL}/reconcile" \
|
||||||
-H "Authorization: Bearer ${RECONCILE_TOKEN}" >/dev/null
|
-H "Authorization: Bearer ${RECONCILE_TOKEN}" >/dev/null
|
||||||
curl -fsS -m 60 -X POST "${WORKER_ORIGIN}/worker" \
|
curl -fsS -m 300 -X POST "${APP_URL}/worker" \
|
||||||
-H "Authorization: Bearer ${WORKER_TOKEN}" >/dev/null
|
-H "Authorization: Bearer ${WORKER_TOKEN}" >/dev/null
|
||||||
env:
|
env:
|
||||||
- name: APP_URL
|
- name: APP_URL
|
||||||
value: "https://<你的平台URL>"
|
value: "https://<你的平台URL>"
|
||||||
- name: WORKER_ORIGIN
|
|
||||||
value: "https://<后端origin直连地址>"
|
|
||||||
- name: RECONCILE_TOKEN
|
- name: RECONCILE_TOKEN
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef: {name: messageweave, key: reconcile-token}
|
secretKeyRef: {name: messageweave, key: reconcile-token}
|
||||||
|
|
@ -543,11 +546,12 @@ spec:
|
||||||
- 调度间隔按 `NFR-RECONCILE-INTERVAL` 取 **5–10 分钟**。Push 是主路径,对账是兜底;
|
- 调度间隔按 `NFR-RECONCILE-INTERVAL` 取 **5–10 分钟**。Push 是主路径,对账是兜底;
|
||||||
但**排空不是兜底项**——没有它,邮件只会一直进队列,一行通知都不会发出来。
|
但**排空不是兜底项**——没有它,邮件只会一直进队列,一行通知都不会发出来。
|
||||||
- **`/reconcile` 不会返回 400**:它没有请求体解析,传了 body 会被静默忽略,所以不要以为要传 `since`。
|
- **`/reconcile` 不会返回 400**:它没有请求体解析,传了 body 会被静默忽略,所以不要以为要传 `since`。
|
||||||
- 走 Worker 转发**无需特殊设置**:网关已为 `POST /reconcile` 单独覆盖超时与尝试次数——超时取
|
- 走 Worker 转发**无需特殊设置**:网关已为两个排空端点单独覆盖超时与尝试次数——`POST /reconcile` 取
|
||||||
`LB_RECONCILE_TIMEOUT_MS`(默认 `320000` ms,大于单飞锁租期 300 s + 心跳余量;锁续租逻辑见
|
`LB_RECONCILE_TIMEOUT_MS`(默认 `320000` ms,大于单飞锁租期 300 s + 心跳余量;锁续租逻辑见
|
||||||
`src/notify.rs:305`),且 `maxAttempts=1` **绝不故障转移**(故障转移只会让第二实例立刻返回
|
`src/notify.rs:305`),`POST /worker` 取 `LB_WORKER_TIMEOUT_MS`(默认 `300000` ms = 后端单条事件
|
||||||
`409`)。其余快路径仍用全局 `LB_REQUEST_TIMEOUT_MS`(默认 `10000` ms)与 `LB_MAX_ATTEMPTS`
|
下界,batch 拉大时按需上调),两者都 `maxAttempts=1` **绝不故障转移**(对 `/reconcile` 切实例会立刻
|
||||||
(默认 `2`),不受影响。`POST /worker` 走不了 Worker,不受这些参数影响。
|
返回 `409`,对 `/worker` 切实例只会重复排同一批消息)。其余快路径仍用全局 `LB_REQUEST_TIMEOUT_MS`
|
||||||
|
(默认 `10000` ms)与 `LB_MAX_ATTEMPTS`(默认 `2`),不受影响。
|
||||||
|
|
||||||
### 6.4 可靠性策略(Reliability)
|
### 6.4 可靠性策略(Reliability)
|
||||||
|
|
||||||
|
|
@ -748,9 +752,9 @@ Stalwart PushSub ────┼─▶ https://lb.<you>.workers.dev ← 唯
|
||||||
|
|
||||||
### 10.4 路由与故障转移
|
### 10.4 路由与故障转移
|
||||||
|
|
||||||
- **路由 safelist**(`C-LB-SINGLE-REG-URL`):Worker 只透传 **17 条**(`SAFE_ROUTES` at `backends.js:10-28` + `ROUTE_METHODS` at `index.js:47-65`):`GET /`、`/assets/config.js`、`/assets/styles.css`、`GET /api/status`、`GET|PUT /api/config`、`GET|PUT /api/business-config`、`POST /api/business-config/preflight`、`POST /api/admin/session`、`POST /api/admin/session/revoke`、`GET|PUT /api/enabled`、`POST /webhook/tg`、`POST /push/jmap`、`POST /api/push/register`、`POST /api/push/disable`、`POST /reconcile`、`GET /ready`、`GET /healthz`(源站自身健康检查;LB 自己的聚合探针在 `/healthz-worker`,不在列);**未知路径 404、method 不符 405**,不透传至后端。**后端另有 3 类不在 safelist**:`POST /api/bootstrap`(一次性信任引导,**不能**走 Worker 域名)、`POST /worker`(队列排空端点,由外部调度周期性调用而非人工触发,Bearer 鉴权,见 §6.3.1)、`/debug/*`(7 条远程联调面,`SAF-DEBUG-GATE`,只能直连后端 origin,见 §2.1)——**这 3 类都不承载外部业务流量,因此后端实例前不需要第二道入口**。`GET|PUT /api/enabled` 属于必须透传的例外:管理 SPA 本身只部署在 Worker 域名下,业务总开关由它读取与切换(`loadEnabled` 在 `web/config.js:676` 读、开关写入在 `:1048`),缺了这条白名单,SPA 里那个开关在后端直连模式下可用、经网关部署时恒 404;该路由已强制 admin-session Bearer 鉴权,暴露面与同在白名单内的 `/api/admin/session` 完全一致。`GET|PUT /api/business-config` 与 `POST /api/business-config/preflight` 同属此类必须透传的路由:SPA 由 Worker 服务,配置回读、提交前校验与保存都由它发起,缺了这两条,回读与预校验经网关部署时恒 404/405,整条配置管理页面只剩后端直连可用;两者同样强制 admin-session Bearer 鉴权,且回读只返回密钥存在性布尔(`SAF-NO-SECRET-ECHO`),不新增凭据暴露面。`/api/status` 只返回启动状态和缺少的环境变量名;所有管理 API 的 Bearer 鉴权由后端执行(`SAF-LB-PASSTHRU`)。
|
- **路由 safelist**(`C-LB-SINGLE-REG-URL`):Worker 只透传 **18 条**(`SAFE_ROUTES` at `backends.js:10-30` + `ROUTE_METHODS` at `index.js:54-73`):`GET /`、`/assets/config.js`、`/assets/styles.css`、`GET /api/status`、`GET|PUT /api/config`、`GET|PUT /api/business-config`、`POST /api/business-config/preflight`、`POST /api/admin/session`、`POST /api/admin/session/revoke`、`GET|PUT /api/enabled`、`POST /webhook/tg`、`POST /push/jmap`、`POST /api/push/register`、`POST /api/push/disable`、`POST /reconcile`、`POST /worker`(两条都是外部 cron 触发的排空端点,Bearer 鉴权,见 §6.3.1)、`GET /ready`、`GET /healthz`(源站自身健康检查;LB 自己的聚合探针在 `/healthz-worker`,不在列);**未知路径 404、method 不符 405**,不透传至后端。**后端另有 2 类不在 safelist**:`POST /api/bootstrap`(一次性信任引导,**不能**走 Worker 域名)、`/debug/*`(7 条远程联调面,`SAF-DEBUG-GATE`,只能直连后端 origin,见 §2.1)——**这 2 类都不承载外部业务流量,因此后端实例前不需要第二道入口**。`GET|PUT /api/enabled` 属于必须透传的例外:管理 SPA 本身只部署在 Worker 域名下,业务总开关由它读取与切换(`loadEnabled` 在 `web/config.js:676` 读、开关写入在 `:1048`),缺了这条白名单,SPA 里那个开关在后端直连模式下可用、经网关部署时恒 404;该路由已强制 admin-session Bearer 鉴权,暴露面与同在白名单内的 `/api/admin/session` 完全一致。`GET|PUT /api/business-config` 与 `POST /api/business-config/preflight` 同属此类必须透传的路由:SPA 由 Worker 服务,配置回读、提交前校验与保存都由它发起,缺了这两条,回读与预校验经网关部署时恒 404/405,整条配置管理页面只剩后端直连可用;两者同样强制 admin-session Bearer 鉴权,且回读只返回密钥存在性布尔(`SAF-NO-SECRET-ECHO`),不新增凭据暴露面。`/api/status` 只返回启动状态和缺少的环境变量名;所有管理 API 的 Bearer 鉴权由后端执行(`SAF-LB-PASSTHRU`)。
|
||||||
- **健康聚合(`MOD-HEALTH-AGG`)**:Worker 自行承载 `GET /healthz-worker`,按 TTL 缓存(默认 30s,`LB_HEALTH_TTL_MS` 可调)探测各后端 `/healthz`,返回 `{status, version, available, total, backends:[{origin,up,status}]}`(`version` 取 `LB_VERSION`,用于确认线上是哪次部署在回答);≥1 后端 up → 200,全 down → 503。`/ready` 透传给后端,做配置 + Redis + 出站只读探测(JMAP session、TG getMe,各 3s、并行,最坏约 3s),不触发业务副作用。
|
- **健康聚合(`MOD-HEALTH-AGG`)**:Worker 自行承载 `GET /healthz-worker`,按 TTL 缓存(默认 30s,`LB_HEALTH_TTL_MS` 可调)探测各后端 `/healthz`,返回 `{status, version, available, total, backends:[{origin,up,status}]}`(`version` 取 `LB_VERSION`,用于确认线上是哪次部署在回答);≥1 后端 up → 200,全 down → 503。`/ready` 透传给后端,做配置 + Redis + 出站只读探测(JMAP session、TG getMe,各 3s、并行,最坏约 3s),不触发业务副作用。
|
||||||
- **故障转移(`forwardWithFailover`)**:每次请求最多 `min(LB_MAX_ATTEMPTS, origins.length)` 次尝试;**仅**超时(AbortError)或 5xx 触发换下一个 origin;4xx/2xx/3xx 直接返回;默认 `LB_MAX_ATTEMPTS=2`(首次 + 1 次故障转移)。**例外:`POST /reconcile` 固定 `maxAttempts=1`,绝不故障转移**——它持集群级单飞锁,切实例只会立刻撞 `409`;其单请求超时取 `LB_RECONCILE_TIMEOUT_MS`(默认 `320000` ms),其余路由不受影响。
|
- **故障转移(`forwardWithFailover`)**:每次请求最多 `min(LB_MAX_ATTEMPTS, origins.length)` 次尝试;**仅**超时(AbortError)或 5xx 触发换下一个 origin;4xx/2xx/3xx 直接返回;默认 `LB_MAX_ATTEMPTS=2`(首次 + 1 次故障转移)。**例外:`POST /reconcile` 与 `POST /worker` 固定 `maxAttempts=1`,绝不故障转移**——前者切实例只会立刻撞 `409`(集群级单飞锁),后者切实例只会重复排同一批消息;这两条也各有独立超时:`LB_RECONCILE_TIMEOUT_MS`(默认 `320000` ms)与 `LB_WORKER_TIMEOUT_MS`(默认 `300000` ms = 后端单条事件下界),其余路由不受影响。
|
||||||
- **随机分摊**:起点 origin 按 `Math.random` 随机化,实现双活;单 origin 配置时退化为确定性。
|
- **随机分摊**:起点 origin 按 `Math.random` 随机化,实现双活;单 origin 配置时退化为确定性。
|
||||||
- **全失败兜底**:返回 `503 All Backends Unavailable`,交由 Telegram / Stalwart 自动重投(**不丢消息**)。
|
- **全失败兜底**:返回 `503 All Backends Unavailable`,交由 Telegram / Stalwart 自动重投(**不丢消息**)。
|
||||||
- **超时预算**(`LB_REQUEST_TIMEOUT_MS`,默认 10s)> 最坏 cold start。
|
- **超时预算**(`LB_REQUEST_TIMEOUT_MS`,默认 10s)> 最坏 cold start。
|
||||||
|
|
@ -797,6 +801,7 @@ npx wrangler deploy
|
||||||
npx wrangler secret put LB_REQUEST_TIMEOUT_MS # 默认 10000ms
|
npx wrangler secret put LB_REQUEST_TIMEOUT_MS # 默认 10000ms
|
||||||
npx wrangler secret put LB_MAX_ATTEMPTS # 默认 2(首次 + 1 次故障转移)
|
npx wrangler secret put LB_MAX_ATTEMPTS # 默认 2(首次 + 1 次故障转移)
|
||||||
npx wrangler secret put LB_RECONCILE_TIMEOUT_MS # 默认 320000ms,仅 POST /reconcile 生效
|
npx wrangler secret put LB_RECONCILE_TIMEOUT_MS # 默认 320000ms,仅 POST /reconcile 生效
|
||||||
|
npx wrangler secret put LB_WORKER_TIMEOUT_MS # 默认 300000ms,仅 POST /worker 生效
|
||||||
npx wrangler secret put LB_HEALTH_TTL_MS # 默认 30000ms
|
npx wrangler secret put LB_HEALTH_TTL_MS # 默认 30000ms
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -805,7 +810,7 @@ npx wrangler secret put LB_HEALTH_TTL_MS # 默认 30000ms
|
||||||
```toml
|
```toml
|
||||||
[vars]
|
[vars]
|
||||||
BACKEND_ORIGINS_JSON = '["https://messageweave-eu-1.motofans.club"]'
|
BACKEND_ORIGINS_JSON = '["https://messageweave-eu-1.motofans.club"]'
|
||||||
LB_VERSION = "2026.10.2"
|
LB_VERSION = "2026.10.3"
|
||||||
```
|
```
|
||||||
|
|
||||||
**明文例外及其边界**:origin 只是公开可达的 https 地址,不是凭据,所以可以随仓库走(好处是
|
**明文例外及其边界**:origin 只是公开可达的 https 地址,不是凭据,所以可以随仓库走(好处是
|
||||||
|
|
@ -829,7 +834,7 @@ npx wrangler secret put BACKEND_ORIGINS_JSON
|
||||||
> `cloudflare-worker/src/backends.js` 的 `parseBackendOrigin` 只认 `typeof raw === "string"` 的 origin,
|
> `cloudflare-worker/src/backends.js` 的 `parseBackendOrigin` 只认 `typeof raw === "string"` 的 origin,
|
||||||
> 对象项会让**整份**配置解析失败;权重也不存在——故障转移是
|
> 对象项会让**整份**配置解析失败;权重也不存在——故障转移是
|
||||||
> `cloudflare-worker/src/lb.js:107` 用 `rng()` 均匀选一个随机起始源再顺序尝试,没有加权。
|
> `cloudflare-worker/src/lb.js:107` 用 `rng()` 均匀选一个随机起始源再顺序尝试,没有加权。
|
||||||
> 症状值得留意:配置是 `cloudflare-worker/src/index.js:93` **每个请求**重新读取并解析的,
|
> 症状值得留意:配置是 `cloudflare-worker/src/index.js:105` **每个请求**重新读取并解析的,
|
||||||
> 所以写错不会「启动即 503」,而是**每个请求都返回 503**(`misconfigured backends`),
|
> 所以写错不会「启动即 503」,而是**每个请求都返回 503**(`misconfigured backends`),
|
||||||
> 日志里也没有异常栈可看。
|
> 日志里也没有异常栈可看。
|
||||||
|
|
||||||
|
|
@ -840,7 +845,7 @@ npx wrangler secret put BACKEND_ORIGINS_JSON
|
||||||
这条已修掉:`wrangler.toml` 现已全部展开成 `#` 行,`wrangler deploy --dry-run` 通过(上传 31.48 KiB)。
|
这条已修掉:`wrangler.toml` 现已全部展开成 `#` 行,`wrangler deploy --dry-run` 通过(上传 31.48 KiB)。
|
||||||
2. **`npm test` 在 Node 22.23 上原先跑不起来**:脚本写的是 `node --test test/`,Node 会把 `test/`
|
2. **`npm test` 在 Node 22.23 上原先跑不起来**:脚本写的是 `node --test test/`,Node 会把 `test/`
|
||||||
当成模块路径去 require,报 `MODULE_NOT_FOUND`。已改成 `node --test`(不加参数自动发现 `test/`),
|
当成模块路径去 require,报 `MODULE_NOT_FOUND`。已改成 `node --test`(不加参数自动发现 `test/`),
|
||||||
当前为 35 个用例全通过(`cloudflare-worker/test/backends.test.js` 6 / `cloudflare-worker/test/health.test.js` 5 / `cloudflare-worker/test/index.test.js` 12 / `cloudflare-worker/test/lb.test.js` 12;lb 含 2 条不可变响应头回归,health 含 1 条 `version` 回显)。
|
当前为 37 个用例全通过(`cloudflare-worker/test/backends.test.js` 6 / `cloudflare-worker/test/health.test.js` 5 / `cloudflare-worker/test/index.test.js` 14 / `cloudflare-worker/test/lb.test.js` 12;index 含 `/reconcile` 与 `/worker` 两条 per-route 长超时回归,lb 含 2 条不可变响应头回归,health 含 1 条 `version` 回显)。
|
||||||
|
|
||||||
**部署与验证**
|
**部署与验证**
|
||||||
|
|
||||||
|
|
@ -858,9 +863,9 @@ curl https://<your-worker>.workers.dev/healthz-worker
|
||||||
2. `npx wrangler deploy --env production` 后 `curl <worker-url>/healthz-worker` 看**响应体里的 `available`**:
|
2. `npx wrangler deploy --env production` 后 `curl <worker-url>/healthz-worker` 看**响应体里的 `available`**:
|
||||||
必须 `available ≥ 1` 才算接通;同时核对 `version` 等于 `wrangler.toml` 里的 `LB_VERSION`,
|
必须 `available ≥ 1` 才算接通;同时核对 `version` 等于 `wrangler.toml` 里的 `LB_VERSION`,
|
||||||
不等就说明跑的还是旧部署。
|
不等就说明跑的还是旧部署。
|
||||||
⚠️ **只看 HTTP 200 会误判**:`/healthz-worker` 是 Worker 自己生成的(不透传后端,`cloudflare-worker/src/index.js:78-82`),
|
⚠️ **只看 HTTP 200 会误判**:`/healthz-worker` 是 Worker 自己生成的(不透传后端,`cloudflare-worker/src/index.js:87-91`),
|
||||||
而且 `BACKEND_ORIGINS_JSON` 缺失、格式错或为空时,`handleHealth` 会吞掉解析异常并返回
|
而且 `BACKEND_ORIGINS_JSON` 缺失、格式错或为空时,`handleHealth` 会吞掉解析异常并返回
|
||||||
**HTTP 200 + `{"status":"no-backends","available":0,"total":0}`**(`cloudflare-worker/src/index.js:138` 的 catch)。
|
**HTTP 200 + `{"status":"no-backends","available":0,"total":0}`**(`cloudflare-worker/src/lb.js:155` 的 catch)。
|
||||||
所以「200」本身不证明配置正确,必须看 `available`。判据速查:
|
所以「200」本身不证明配置正确,必须看 `available`。判据速查:
|
||||||
`status=ok` 且 `available≥1` → 正常;`no-backends`(200)→ secret 没设好或 JSON 格式错;`down`(503)→ 后端全挂。
|
`status=ok` 且 `available≥1` → 正常;`no-backends`(200)→ secret 没设好或 JSON 格式错;`down`(503)→ 后端全挂。
|
||||||
3. `curl -X POST https://<worker>/webhook/tg -H "Content-Type: application/json" -d '{"test":1}'` 应得到后端返回(非 503)。
|
3. `curl -X POST https://<worker>/webhook/tg -H "Content-Type: application/json" -d '{"test":1}'` 应得到后端返回(非 503)。
|
||||||
|
|
|
||||||
|
|
@ -284,7 +284,7 @@ Response conventions:
|
||||||
## 4. Gateway vs backend route matrix
|
## 4. Gateway vs backend route matrix
|
||||||
|
|
||||||
> **Independently verified.** Source: `cloudflare-worker/src/backends.js` `SAFE_ROUTES`,
|
> **Independently verified.** Source: `cloudflare-worker/src/backends.js` `SAFE_ROUTES`,
|
||||||
> 16 entries, alongside `ROUTE_METHODS` (`index.js`)
|
> 18 entries, alongside `ROUTE_METHODS` (`index.js`)
|
||||||
> which fixes one method set per path. The worker entry point is `src/index.js`
|
> which fixes one method set per path. The worker entry point is `src/index.js`
|
||||||
> (`wrangler.toml`); `src/lb.js` performs forwarding and bounded failover (`SAF-LB-PASSTHRU`,
|
> (`wrangler.toml`); `src/lb.js` performs forwarding and bounded failover (`SAF-LB-PASSTHRU`,
|
||||||
> `C-NO-LONG-CONN`).
|
> `C-NO-LONG-CONN`).
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,9 @@
|
||||||
# --- 环境变量 ---
|
# --- 环境变量 ---
|
||||||
# MW_APP_URL 必填。对外平台 URL(走 LB 的域名),用于 GET /api/status、
|
# MW_APP_URL 必填。对外平台 URL(走 LB 的域名),用于 GET /api/status、
|
||||||
# GET /healthz、POST /reconcile。
|
# GET /healthz、POST /reconcile。
|
||||||
# MW_WORKER_URL 可选,默认 = MW_APP_URL。POST /worker 的地址。**重要**:
|
# MW_WORKER_URL 可选,默认 = MW_APP_URL。POST /worker 的地址。/worker 已在 LB
|
||||||
# /worker 不在 LB 的路由白名单内,前面有 LB 时必须指向源站
|
# 路由白名单内,默认直接走 MW_APP_URL 即可;要跳过 LB 直连源站
|
||||||
# 直连地址(如 https://messageweave-eu-1.motofans.club),
|
# 时才需要覆盖(如想绕过 LB 的排空超时,或源站另有独立域名)。
|
||||||
# 否则返回 404 route not forwarded。
|
|
||||||
# MW_DEBUG_URL 可选,默认 = MW_WORKER_URL。GET /debug/* 与 POST /debug/notify
|
# MW_DEBUG_URL 可选,默认 = MW_WORKER_URL。GET /debug/* 与 POST /debug/notify
|
||||||
# 的地址(需要后端以 --debug 启动且 DEBUG_TOKEN 非空,
|
# 的地址(需要后端以 --debug 启动且 DEBUG_TOKEN 非空,
|
||||||
# 且需直连源站)。
|
# 且需直连源站)。
|
||||||
|
|
@ -274,7 +273,8 @@ run_worker() {
|
||||||
404)
|
404)
|
||||||
if printf '%s' "$BODY" | grep -q 'route not forwarded'; then
|
if printf '%s' "$BODY" | grep -q 'route not forwarded'; then
|
||||||
log "worker=404 $WORKER_URL/worker 被 LB 拦截(route not forwarded)"
|
log "worker=404 $WORKER_URL/worker 被 LB 拦截(route not forwarded)"
|
||||||
log " /worker 不在 LB 白名单内,把 MW_WORKER_URL 指向源站直连地址"
|
log " LB 版本太旧:/worker 需要 LB_VERSION >= 2026.10.3(见 /healthz-worker)"
|
||||||
|
log " 升级 LB,或把 MW_WORKER_URL 指向源站直连地址绕过 LB"
|
||||||
else
|
else
|
||||||
log "worker=404 $WORKER_URL/worker 不存在(MW_WORKER_URL 可能指错)"
|
log "worker=404 $WORKER_URL/worker 不存在(MW_WORKER_URL 可能指错)"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue