feat(lb): /healthz 透传到源站,LB 聚合探针改挂 /healthz-worker
源站自己也暴露 /healthz;两者同路径时走 Worker 域名只能看到 LB 的聚合 结果,看不到源站自己的健康状态。现在拆开: - GET /healthz 透传到源站(源站自己的健康检查) - GET /healthz-worker LB 聚合探针(LB_VERSION + 后端汇总) SAFE_ROUTES 增列 /healthz、ROUTE_METHODS 增列 /healthz: [GET],白名单 16 -> 17。 LB 探测源站的路径仍是 /healthz,不变。LB_VERSION 2026.10.1 -> 2026.10.2。 GATE-DOCS 6/6;node --check 全部通过。
This commit is contained in:
parent
15fa7f1e35
commit
89dfe5cf3d
|
|
@ -3,7 +3,7 @@
|
||||||
统一 HTTPS 入口 + 多后端 origin 故障转移(HA/LB 子项目)。
|
统一 HTTPS 入口 + 多后端 origin 故障转移(HA/LB 子项目)。
|
||||||
**透传模型**:Worker 不感知业务,原样转发请求到多个 https 后端 origin;仅「超时 / 5xx」做有界故障转移。
|
**透传模型**:Worker 不感知业务,原样转发请求到多个 https 后端 origin;仅「超时 / 5xx」做有界故障转移。
|
||||||
|
|
||||||
> 本组件是 **safelist 受限的边缘负载均衡器(edge load balancer)**:路由固定 16 条白名单、
|
> 本组件是 **safelist 受限的边缘负载均衡器(edge load balancer)**:路由固定 17 条白名单、
|
||||||
> 后端 origin 在部署期固定且仅允许 https、未知路径一律 404。它只对固定后端做请求转发与
|
> 后端 origin 在部署期固定且仅允许 https、未知路径一律 404。它只对固定后端做请求转发与
|
||||||
> 故障转移,不接受任意目标主机,也不提供任何形式的流量中转或访问隐藏能力。
|
> 故障转移,不接受任意目标主机,也不提供任何形式的流量中转或访问隐藏能力。
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
| `LB_MAX_ATTEMPTS` | 否 | 每请求最多 origin 尝试次数;默认 `2`(= 1 次故障转移)。`POST /reconcile` 固定为 `1`。 |
|
| `LB_MAX_ATTEMPTS` | 否 | 每请求最多 origin 尝试次数;默认 `2`(= 1 次故障转移)。`POST /reconcile` 固定为 `1`。 |
|
||||||
| `LB_RECONCILE_TIMEOUT_MS` | 否 | 仅 `POST /reconcile` 的单 origin 超时覆盖;默认 `320000`(须大于后端锁初租 300s + 心跳余量)。 |
|
| `LB_RECONCILE_TIMEOUT_MS` | 否 | 仅 `POST /reconcile` 的单 origin 超时覆盖;默认 `320000`(须大于后端锁初租 300s + 心跳余量)。 |
|
||||||
| `LB_HEALTH_TTL_MS` | 否 | 健康探测缓存 TTL;默认 `30000`。 |
|
| `LB_HEALTH_TTL_MS` | 否 | 健康探测缓存 TTL;默认 `30000`。 |
|
||||||
| `LB_VERSION` | 否 | `/healthz` 响应体里的 `version` 字段,用于确认线上是哪次部署在回答;取自 `wrangler.toml` 的 `[vars]`(当前 `2026.10.1`),改 LB 逻辑的提交同时 bump |
|
| `LB_VERSION` | 否 | `/healthz-worker` 响应体里的 `version` 字段,用于确认线上是哪次部署在回答;取自 `wrangler.toml` 的 `[vars]`(当前 `2026.10.2`),改 LB 逻辑的提交同时 bump |
|
||||||
|
|
||||||
> 默认写在 `wrangler.toml` 的 `[vars]`(**明文例外**,因为当前值只是公开可达的 https origin,
|
> 默认写在 `wrangler.toml` 的 `[vars]`(**明文例外**,因为当前值只是公开可达的 https origin,
|
||||||
> 不是凭据)。一旦要放内网地址、带内嵌凭据的 URL、或不想公开的 staging 主机名,
|
> 不是凭据)。一旦要放内网地址、带内嵌凭据的 URL、或不想公开的 staging 主机名,
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
* 仅 ES2022,零依赖,可 node 直接执行。
|
* 仅 ES2022,零依赖,可 node 直接执行。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** 允许透传的入站路由 safelist(C-LB-SINGLE-REG-URL;/healthz 由 Worker 自身聚合,不在列):未知路径 → 404。
|
/** 允许透传的入站路由 safelist(C-LB-SINGLE-REG-URL):未知路径 → 404。
|
||||||
|
* `/healthz` 透传到源站(源站自己的健康检查);LB 自己的聚合探针在 `/healthz-worker`,不在列。
|
||||||
* SPA 静态资源为公开 GET;所有管理 API 仍由后端校验 Redis bootstrap 凭据或 admin session。 */
|
* SPA 静态资源为公开 GET;所有管理 API 仍由后端校验 Redis bootstrap 凭据或 admin session。 */
|
||||||
export const SAFE_ROUTES = Object.freeze([
|
export const SAFE_ROUTES = Object.freeze([
|
||||||
"/",
|
"/",
|
||||||
|
|
@ -22,6 +23,7 @@ export const SAFE_ROUTES = Object.freeze([
|
||||||
"/api/push/register",
|
"/api/push/register",
|
||||||
"/api/push/disable",
|
"/api/push/disable",
|
||||||
"/reconcile",
|
"/reconcile",
|
||||||
|
"/healthz",
|
||||||
"/ready",
|
"/ready",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
* 健康聚合探针(稳定 ID:MOD-HEALTH-AGG │ C-NO-DB │ C-REDIS-ONLY-STATE)。
|
* 健康聚合探针(稳定 ID:MOD-HEALTH-AGG │ C-NO-DB │ C-REDIS-ONLY-STATE)。
|
||||||
*
|
*
|
||||||
* 语义:
|
* 语义:
|
||||||
* - Worker 暴露 LB 级 `GET /healthz`,聚合探测各后端 origin 的 `/healthz`(后端公开探针,无敏感信息)。
|
* - Worker 暴露 LB 级 `GET /healthz-worker`,聚合探测各后端 origin 的 `/healthz`
|
||||||
|
* (后端公开探针,无敏感信息);入站 `/healthz` 保留给源站自己的健康检查(透传)。
|
||||||
* - 缓存 TTL(默认 30秒)避免健康探测放大;TTL 内复用上次结果,过期后才重探。
|
* - 缓存 TTL(默认 30秒)避免健康探测放大;TTL 内复用上次结果,过期后才重探。
|
||||||
* - 探测失败/超时 → 该 origin 记为 down;`available` 统计存活数。
|
* - 探测失败/超时 → 该 origin 记为 down;`available` 统计存活数。
|
||||||
* - 仅 HTTP 层健康(<500 记为 up):**不**检查/代理 Redis、JMAP(C-NO-DB / C-REDIS-ONLY-STATE),
|
* - 仅 HTTP 层健康(<500 记为 up):**不**检查/代理 Redis、JMAP(C-NO-DB / C-REDIS-ONLY-STATE),
|
||||||
|
|
@ -65,7 +66,7 @@ export async function aggregateHealth(origins, ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 组装 LB 级 `/healthz` 响应:有 ≥1 后端存活 → 200;全 down → 503;无配置 → 200 + 明确标记。
|
* 组装 LB 级 `/healthz-worker` 响应:有 ≥1 后端存活 → 200;全 down → 503;无配置 → 200 + 明确标记。
|
||||||
* @param {{available:number,total:number,backends:unknown[]}} summary
|
* @param {{available:number,total:number,backends:unknown[]}} summary
|
||||||
* @param {string} [version] 版本标识(env.LB_VERSION);便于确认线上是哪次部署在回答。
|
* @param {string} [version] 版本标识(env.LB_VERSION);便于确认线上是哪次部署在回答。
|
||||||
* 省略时 JSON 不含该字段(JSON.stringify 丢弃 undefined),保持向后兼容。
|
* 省略时 JSON 不含该字段(JSON.stringify 丢弃 undefined),保持向后兼容。
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* - SAF-LB-PASSTHRU:透传 headers/body(含鉴权头),不做鉴权改写。
|
* - SAF-LB-PASSTHRU:透传 headers/body(含鉴权头),不做鉴权改写。
|
||||||
* - C-HTTPS-INBOUND:仅 https 后端 origin。
|
* - C-HTTPS-INBOUND:仅 https 后端 origin。
|
||||||
* - ARCH-LB-WORKER / C-LB-SINGLE-REG-URL:safelist 路由,未知路径 404、method 不符 405。
|
* - ARCH-LB-WORKER / C-LB-SINGLE-REG-URL:safelist 路由,未知路径 404、method 不符 405。
|
||||||
* - MOD-HEALTH-AGG:/healthz 由 Worker 聚合,不落后端。
|
* - MOD-HEALTH-AGG:/healthz-worker 由 Worker 聚合;/healthz 透传到源站。
|
||||||
* - C-NO-DB / C-REDIS-ONLY-STATE:不代理 Redis/JMAP;不检查数据库侧可用性。
|
* - C-NO-DB / C-REDIS-ONLY-STATE:不代理 Redis/JMAP;不检查数据库侧可用性。
|
||||||
* - C-NO-LONG-CONN:纯请求-响应,body 一次性读入回灌;无 WS/SSE/长轮询。
|
* - C-NO-LONG-CONN:纯请求-响应,body 一次性读入回灌;无 WS/SSE/长轮询。
|
||||||
* - C-NO-SECRET-IN-IMAGE:日志仅「方法/路径/origin/失败类别」,不含 secret;origin 清单走 wrangler secret。
|
* - C-NO-SECRET-IN-IMAGE:日志仅「方法/路径/origin/失败类别」,不含 secret;origin 清单走 wrangler secret。
|
||||||
|
|
@ -16,7 +16,8 @@
|
||||||
* - GET|PUT /api/config、GET|PUT /api/business-config、POST /api/business-config/preflight、
|
* - GET|PUT /api/config、GET|PUT /api/business-config、POST /api/business-config/preflight、
|
||||||
* POST /api/admin/session[/revoke]
|
* POST /api/admin/session[/revoke]
|
||||||
* → 透传至后端;后端校验 bootstrap 凭据或短期 admin session
|
* → 透传至后端;后端校验 bootstrap 凭据或短期 admin session
|
||||||
* - GET /healthz → LB 聚合健康(MOD-HEALTH-AGG)
|
* - GET /healthz → 透传到源站(源站自己的健康检查)
|
||||||
|
* - GET /healthz-worker → LB 聚合健康(MOD-HEALTH-AGG),响应体带 LB_VERSION
|
||||||
* - POST /webhook/tg|/push/jmap|/api/push/register|/api/push/disable|/reconcile、GET /ready → 透传 + 有界故障转移
|
* - POST /webhook/tg|/push/jmap|/api/push/register|/api/push/disable|/reconcile、GET /ready → 透传 + 有界故障转移
|
||||||
* - 其它 → 404 / 405
|
* - 其它 → 404 / 405
|
||||||
*
|
*
|
||||||
|
|
@ -38,7 +39,7 @@ const DEFAULT_RECONCILE_TIMEOUT_MS = 320_000;
|
||||||
/**
|
/**
|
||||||
* 健康探测缓存(模块作用域 = CF Worker isolate 内跨请求复用)。
|
* 健康探测缓存(模块作用域 = CF Worker isolate 内跨请求复用)。
|
||||||
* 仅存「origin → {at,up,status}」,不含任何 header/body/secret(SAF-LOG-PURITY 精神)。
|
* 仅存「origin → {at,up,status}」,不含任何 header/body/secret(SAF-LOG-PURITY 精神)。
|
||||||
* TTL 由 LB_HEALTH_TTL_MS 控制;isolate 冷启时为空,首个 /healthz 全量探测。
|
* TTL 由 LB_HEALTH_TTL_MS 控制;isolate 冷启时为空,首个 /healthz-worker 全量探测。
|
||||||
*/
|
*/
|
||||||
const healthCache = new Map();
|
const healthCache = new Map();
|
||||||
|
|
||||||
|
|
@ -59,6 +60,7 @@ const ROUTE_METHODS = Object.freeze({
|
||||||
"/api/push/register": ["POST"],
|
"/api/push/register": ["POST"],
|
||||||
"/api/push/disable": ["POST"],
|
"/api/push/disable": ["POST"],
|
||||||
"/reconcile": ["POST"],
|
"/reconcile": ["POST"],
|
||||||
|
"/healthz": ["GET"],
|
||||||
"/ready": ["GET"],
|
"/ready": ["GET"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -72,9 +74,11 @@ export async function handleFetch(request, env) {
|
||||||
const path = new URL(request.url).pathname;
|
const path = new URL(request.url).pathname;
|
||||||
|
|
||||||
// LB 级健康聚合探针(不含敏感信息,SAF-PROBE-PUBLIC 精神)。
|
// LB 级健康聚合探针(不含敏感信息,SAF-PROBE-PUBLIC 精神)。
|
||||||
if (path === "/healthz") {
|
// 挂在 /healthz-worker:/healthz 保留给源站自己的健康检查(透传),这样入站域名上
|
||||||
|
// 两个健康检查互不遮蔽、能分别看到 LB 与源站的版本。
|
||||||
|
if (path === "/healthz-worker") {
|
||||||
if (request.method !== "GET") {
|
if (request.method !== "GET") {
|
||||||
return text("method not allowed for /healthz", 405, { allow: "GET" });
|
return text("method not allowed for /healthz-worker", 405, { allow: "GET" });
|
||||||
}
|
}
|
||||||
return handleHealth(env);
|
return handleHealth(env);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,16 +42,16 @@ test("BACKEND_ORIGINS_JSON parses, dedups, validates all entries", () => {
|
||||||
assert.throws(() => parseBackendOrigins("[]"), /non-empty/);
|
assert.throws(() => parseBackendOrigins("[]"), /non-empty/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("route safelist excludes /healthz and unknown paths (C-LB-SINGLE-REG-URL)", () => {
|
test("route safelist forwards /healthz and blocks unknown paths (C-LB-SINGLE-REG-URL)", () => {
|
||||||
for (const r of [
|
for (const r of [
|
||||||
"/", "/assets/config.js", "/assets/styles.css", "/api/status", "/ready", "/webhook/tg", "/push/jmap", "/reconcile",
|
"/", "/assets/config.js", "/assets/styles.css", "/api/status", "/ready", "/webhook/tg", "/push/jmap", "/reconcile",
|
||||||
"/api/push/register", "/api/push/disable",
|
"/api/push/register", "/api/push/disable",
|
||||||
"/api/config", "/api/business-config", "/api/admin/session", "/api/admin/session/revoke",
|
"/api/config", "/api/business-config", "/api/admin/session", "/api/admin/session/revoke",
|
||||||
"/api/enabled",
|
"/api/enabled", "/healthz",
|
||||||
]) {
|
]) {
|
||||||
assert.ok(LB_ROUTES.includes(r), `forwarded: ${r}`);
|
assert.ok(LB_ROUTES.includes(r), `forwarded: ${r}`);
|
||||||
}
|
}
|
||||||
for (const r of ["/healthz", "/admin", "/debug", "/jmap/session", "/ready/extra"]) {
|
for (const r of ["/healthz-worker", "/admin", "/debug", "/jmap/session", "/ready/extra"]) {
|
||||||
assert.ok(!LB_ROUTES.includes(r), `must be blocked: ${r}`);
|
assert.ok(!LB_ROUTES.includes(r), `must be blocked: ${r}`);
|
||||||
assert.ok(!SAFE_ROUTES.includes(r), `must not be a safe route: ${r}`);
|
assert.ok(!SAFE_ROUTES.includes(r), `must not be a safe route: ${r}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/**
|
/**
|
||||||
* 集成测试:Cloudflare Worker 主入口 handle fetch(ARCH-LB-WORKER / SAF-LB-PASSTHRU)。
|
* 集成测试:Cloudflare Worker 主入口 handle fetch(ARCH-LB-WORKER / SAF-LB-PASSTHRU)。
|
||||||
* 验证:路由分发、/healthz 聚合、未白名单 404、method 拦截、配置异常 fail-closed、鉴权头透传。
|
* 验证:路由分发、/healthz-worker 聚合、/healthz 透传、未白名单 404、method 拦截、配置异常 fail-closed、鉴权头透传。
|
||||||
* 运行:cd cloudflare-worker && node --test
|
* 运行:cd cloudflare-worker && node --test
|
||||||
*/
|
*/
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
|
|
@ -25,10 +25,10 @@ function stubFetch(impl) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test("Worker GET /healthz aggregates backend probes without touching forwarded routes", async () => {
|
test("Worker GET /healthz-worker aggregates backend probes without touching forwarded routes", async () => {
|
||||||
const restore = stubFetch(async (url) => new Response("ok", { status: 200 }));
|
const restore = stubFetch(async (url) => new Response("ok", { status: 200 }));
|
||||||
try {
|
try {
|
||||||
const res = await handleFetch(new Request("https://lb.example/healthz", { method: "GET" }), makeEnv());
|
const res = await handleFetch(new Request("https://lb.example/healthz-worker", { method: "GET" }), makeEnv());
|
||||||
assert.equal(res.status, 200);
|
assert.equal(res.status, 200);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
assert.equal(body.status, "ok");
|
assert.equal(body.status, "ok");
|
||||||
|
|
@ -40,7 +40,7 @@ test("Worker GET /healthz aggregates backend probes without touching forwarded r
|
||||||
|
|
||||||
// 配置后原样回显:确认线上是哪次部署在回答。
|
// 配置后原样回显:确认线上是哪次部署在回答。
|
||||||
const res2 = await handleFetch(
|
const res2 = await handleFetch(
|
||||||
new Request("https://lb.example/healthz", { method: "GET" }),
|
new Request("https://lb.example/healthz-worker", { method: "GET" }),
|
||||||
makeEnv({ LB_VERSION: "2026.10.1" }),
|
makeEnv({ LB_VERSION: "2026.10.1" }),
|
||||||
);
|
);
|
||||||
const body2 = await res2.json();
|
const body2 = await res2.json();
|
||||||
|
|
@ -50,15 +50,30 @@ test("Worker GET /healthz aggregates backend probes without touching forwarded r
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Worker: wrong method on the local /healthz probe => 405, not 404", async () => {
|
test("Worker: wrong method on the local /healthz-worker probe => 405, not 404", async () => {
|
||||||
const res = await handleFetch(
|
const res = await handleFetch(
|
||||||
new Request("https://lb.example/healthz", { method: "POST", body: "{}" }),
|
new Request("https://lb.example/healthz-worker", { method: "POST", body: "{}" }),
|
||||||
makeEnv(),
|
makeEnv(),
|
||||||
);
|
);
|
||||||
assert.equal(res.status, 405);
|
assert.equal(res.status, 405);
|
||||||
assert.equal(res.headers.get("allow"), "GET");
|
assert.equal(res.headers.get("allow"), "GET");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Worker GET /healthz is forwarded to the origin, not answered by the LB", async () => {
|
||||||
|
// 两个健康检查共存:/healthz 属于源站,/healthz-worker 属于 LB。
|
||||||
|
const restore = stubFetch(async () => new Response("origin-ok", { status: 200 }));
|
||||||
|
try {
|
||||||
|
const res = await handleFetch(
|
||||||
|
new Request("https://lb.example/healthz", { method: "GET" }),
|
||||||
|
makeEnv(),
|
||||||
|
);
|
||||||
|
assert.equal(res.status, 200);
|
||||||
|
assert.equal(await res.text(), "origin-ok");
|
||||||
|
} finally {
|
||||||
|
restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("Worker: unknown route => 404 (C-LB-SINGLE-REG-URL)", async () => {
|
test("Worker: unknown route => 404 (C-LB-SINGLE-REG-URL)", async () => {
|
||||||
const res = await handleFetch(
|
const res = await handleFetch(
|
||||||
new Request("https://lb.example/unknown", { method: "POST", body: "{}" }),
|
new Request("https://lb.example/unknown", { method: "POST", body: "{}" }),
|
||||||
|
|
|
||||||
|
|
@ -46,4 +46,4 @@ compatibility_flags = ["nodejs_compat_v2"]
|
||||||
# LB_VERSION 是例外——它是版本标识,必须跟代码同版本、可 diff,写进 dashboard 反而容易漂。
|
# LB_VERSION 是例外——它是版本标识,必须跟代码同版本、可 diff,写进 dashboard 反而容易漂。
|
||||||
[vars]
|
[vars]
|
||||||
BACKEND_ORIGINS_JSON = '["https://messageweave-eu-1.motofans.club"]'
|
BACKEND_ORIGINS_JSON = '["https://messageweave-eu-1.motofans.club"]'
|
||||||
LB_VERSION = "2026.10.1"
|
LB_VERSION = "2026.10.2"
|
||||||
|
|
|
||||||
|
|
@ -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/*` 不在网关 16 条安全路由内,Worker 一律 `404 route not forwarded`;只能直连后端 origin,公网不可达 | 安全 |
|
| `SAF-DEBUG-ORIGIN-ONLY` | docs/deployment.md §2.1 / docs/reference.md §4 | `/debug/*` 不在网关 17 条安全路由内,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**:网关的 16 条安全路由(`cloudflare-worker/src/backends.js:9-26`)不含任何 `/debug/*`,Worker 对未白名单路径返回 `404 route not forwarded: /debug/...`(`cloudflare-worker/src/index.js:82-85`)。因此 `/debug/*` 只能通过直连后端 origin 访问;若必须经代理,请自行在代理层加鉴权,不要让公网可达。
|
- **不在 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 访问;若必须经代理,请自行在代理层加鉴权,不要让公网可达。
|
||||||
|
|
||||||
> **建议**:生产环境不开启。需要远程联调时在控制台设置 `DEBUG_ENABLED=1` 与一次性
|
> **建议**:生产环境不开启。需要远程联调时在控制台设置 `DEBUG_ENABLED=1` 与一次性
|
||||||
> `DEBUG_TOKEN`,重新部署后联调;结束立即把 `DEBUG_ENABLED` 置空并轮换 `DEBUG_TOKEN`。
|
> `DEBUG_TOKEN`,重新部署后联调;结束立即把 `DEBUG_ENABLED` 置空并轮换 `DEBUG_TOKEN`。
|
||||||
|
|
@ -138,7 +138,7 @@
|
||||||
- 非 root 用户运行(保留既有约定)
|
- 非 root 用户运行(保留既有约定)
|
||||||
- **公网 HTTPS 入口(`C-HTTPS-URL` 已确认)**:运维在平台上给 bot 配一个公网 HTTPS URL(如 `https://bot.example.com`),平台 ingress/反代把 `https://…/webhook/tg`、`/push/jmap`、`/reconcile` 路由到容器 `PORT`。**bot 自身不申请证书、不监听 443**;证书由平台/反代管理(`C-HTTPS-INBOUND`)。
|
- **公网 HTTPS 入口(`C-HTTPS-URL` 已确认)**:运维在平台上给 bot 配一个公网 HTTPS URL(如 `https://bot.example.com`),平台 ingress/反代把 `https://…/webhook/tg`、`/push/jmap`、`/reconcile` 路由到容器 `PORT`。**bot 自身不申请证书、不监听 443**;证书由平台/反代管理(`C-HTTPS-INBOUND`)。
|
||||||
- 需要在 Stalwart 与 Telegram 两侧使用这个公网 URL:Telegram `setWebhook` 指向 `/webhook/tg`;管理员调用 `POST /api/push/register` 并提交该 URL 的 `/push/jmap` 路径,后端负责执行 `PushSubscription/set create`。服务不会自动猜测平台公网域名。
|
- 需要在 Stalwart 与 Telegram 两侧使用这个公网 URL:Telegram `setWebhook` 指向 `/webhook/tg`;管理员调用 `POST /api/push/register` 并提交该 URL 的 `/push/jmap` 路径,后端负责执行 `PushSubscription/set create`。服务不会自动猜测平台公网域名。
|
||||||
- **出站 egress 是 `/ready` 的硬依赖**:就绪探针向配置的 JMAP session host 与 `api.telegram.org:443` 发起只读 `GET`。该 egress 不通时 `/ready` 会长期 `503`,即使配置与 Redis 都正常。若 egress 必须经 HTTP(S) 代理则**无法使用**:应用内 `reqwest` 以 `default-features = false` 编译(`Cargo.toml:12`),未启用 `proxy` feature,**不解析 `HTTPS_PROXY`/`HTTP_PROXY`**;这种情况下请用网关聚合的 `/healthz`(无条件 200)作为存活监控,`/ready` 仅作人工排查。
|
- **出站 egress 是 `/ready` 的硬依赖**:就绪探针向配置的 JMAP session host 与 `api.telegram.org:443` 发起只读 `GET`。该 egress 不通时 `/ready` 会长期 `503`,即使配置与 Redis 都正常。若 egress 必须经 HTTP(S) 代理则**无法使用**:应用内 `reqwest` 以 `default-features = false` 编译(`Cargo.toml:12`),未启用 `proxy` feature,**不解析 `HTTPS_PROXY`/`HTTP_PROXY`**;这种情况下请用网关聚合的 `/healthz-worker`(无条件 200)作为存活监控,`/ready` 仅作人工排查。
|
||||||
- **多实例 LB/HA 时登记的是 Worker URL 而非各后端 URL**(`C-LB-SINGLE-REG-URL`,§10):Telegram / Stalwart / Cron 只认 Worker 的稳定域名;后端平台入口不对外登记。
|
- **多实例 LB/HA 时登记的是 Worker URL 而非各后端 URL**(`C-LB-SINGLE-REG-URL`,§10):Telegram / Stalwart / Cron 只认 Worker 的稳定域名;后端平台入口不对外登记。
|
||||||
- Secrets 运行期注入(环境变量/容器平台 secret),见 §5(同文件内章节链接)
|
- Secrets 运行期注入(环境变量/容器平台 secret),见 §5(同文件内章节链接)
|
||||||
|
|
||||||
|
|
@ -736,8 +736,8 @@ Stalwart PushSub ────┼─▶ https://lb.<you>.workers.dev ← 唯
|
||||||
|
|
||||||
### 10.4 路由与故障转移
|
### 10.4 路由与故障转移
|
||||||
|
|
||||||
- **路由 safelist**(`C-LB-SINGLE-REG-URL`):Worker 只透传 **16 条**(`SAFE_ROUTES` at `backends.js:9-26` + `ROUTE_METHODS` at `index.js:46-63`):`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`;**未知路径 404、method 不符 405**,不透传至后端。**后端另有 4 类不在 safelist**:`POST /api/bootstrap`(一次性信任引导,**不能**走 Worker 域名)、`POST /worker`(队列排空端点,由外部调度周期性调用而非人工触发,Bearer 鉴权,见 §6.3.1)、`GET /healthz`(网关自行聚合,不转发)、`/debug/*`(7 条远程联调面,`SAF-DEBUG-GATE`,只能直连后端 origin,见 §2.1)——**这 4 类都不承载外部业务流量,因此后端实例前不需要第二道入口**。`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 只透传 **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`)。
|
||||||
- **健康聚合(`MOD-HEALTH-AGG`)**:Worker 自行承载 `GET /healthz`,按 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` 固定 `maxAttempts=1`,绝不故障转移**——它持集群级单飞锁,切实例只会立刻撞 `409`;其单请求超时取 `LB_RECONCILE_TIMEOUT_MS`(默认 `320000` ms),其余路由不受影响。
|
||||||
- **随机分摊**:起点 origin 按 `Math.random` 随机化,实现双活;单 origin 配置时退化为确定性。
|
- **随机分摊**:起点 origin 按 `Math.random` 随机化,实现双活;单 origin 配置时退化为确定性。
|
||||||
- **全失败兜底**:返回 `503 All Backends Unavailable`,交由 Telegram / Stalwart 自动重投(**不丢消息**)。
|
- **全失败兜底**:返回 `503 All Backends Unavailable`,交由 Telegram / Stalwart 自动重投(**不丢消息**)。
|
||||||
|
|
@ -793,7 +793,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.1"
|
LB_VERSION = "2026.10.2"
|
||||||
```
|
```
|
||||||
|
|
||||||
**明文例外及其边界**:origin 只是公开可达的 https 地址,不是凭据,所以可以随仓库走(好处是
|
**明文例外及其边界**:origin 只是公开可达的 https 地址,不是凭据,所以可以随仓库走(好处是
|
||||||
|
|
@ -836,17 +836,17 @@ npx wrangler secret put BACKEND_ORIGINS_JSON
|
||||||
cd cloudflare-worker
|
cd cloudflare-worker
|
||||||
npx wrangler deploy --env production
|
npx wrangler deploy --env production
|
||||||
# 验证 LB 健康聚合
|
# 验证 LB 健康聚合
|
||||||
curl https://<your-worker>.workers.dev/healthz
|
curl https://<your-worker>.workers.dev/healthz-worker
|
||||||
```
|
```
|
||||||
|
|
||||||
**Telegram / Stalwart / Cron 登记的唯一 URL** 改为 `https://<your-worker>.workers.dev/webhook/tg` 等(`C-LB-SINGLE-REG-URL`);**各后端平台的 HTTPS 入口不再对外登记**(小平台入口可能被公网直连,故**后端鉴权不可省**,`SAF-LB-PASSTHRU`)。
|
**Telegram / Stalwart / Cron 登记的唯一 URL** 改为 `https://<your-worker>.workers.dev/webhook/tg` 等(`C-LB-SINGLE-REG-URL`);**各后端平台的 HTTPS 入口不再对外登记**(小平台入口可能被公网直连,故**后端鉴权不可省**,`SAF-LB-PASSTHRU`)。
|
||||||
|
|
||||||
**验证(发布前必做)**
|
**验证(发布前必做)**
|
||||||
1. `cd cloudflare-worker && npm test`(Node 单测,代理逻辑 + 健康聚合 + 超时/失败重试 + safelist)。
|
1. `cd cloudflare-worker && npm test`(Node 单测,代理逻辑 + 健康聚合 + 超时/失败重试 + safelist)。
|
||||||
2. `npx wrangler deploy --env production` 后 `curl <worker-url>/healthz` 看**响应体里的 `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 自己生成的(不透传后端,`cloudflare-worker/src/index.js:75`),
|
⚠️ **只看 HTTP 200 会误判**:`/healthz-worker` 是 Worker 自己生成的(不透传后端,`cloudflare-worker/src/index.js:78-82`),
|
||||||
而且 `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/index.js:138` 的 catch)。
|
||||||
所以「200」本身不证明配置正确,必须看 `available`。判据速查:
|
所以「200」本身不证明配置正确,必须看 `available`。判据速查:
|
||||||
|
|
@ -903,7 +903,7 @@ Git 集成**必须改的四个字段**(Cloudflare 的默认值对不上本仓
|
||||||
(8787 是 `wrangler dev` 的默认端口,`--ip 0.0.0.0` 让 Cloudflare 的预览代理能连进来)。
|
(8787 是 `wrangler dev` 的默认端口,`--ip 0.0.0.0` 让 Cloudflare 的预览代理能连进来)。
|
||||||
⚠️ 但预览环境**拿不到** dashboard 里加的 secret:现在 `BACKEND_ORIGINS_JSON` 写在
|
⚠️ 但预览环境**拿不到** dashboard 里加的 secret:现在 `BACKEND_ORIGINS_JSON` 写在
|
||||||
`wrangler.toml` 的 `[vars]` 里,预览因此也能跑通转发;**一旦改回 secret 注入**,预览就只剩
|
`wrangler.toml` 的 `[vars]` 里,预览因此也能跑通转发;**一旦改回 secret 注入**,预览就只剩
|
||||||
「Worker 能被拉起 + 路由表生效 + `/healthz` 返回 200 但 `status` 是 `no-backends`」,
|
「Worker 能被拉起 + 路由表生效 + `/healthz-worker` 返回 200 但 `status` 是 `no-backends`」,
|
||||||
**不能**验证后端转发,转发只能看生产。(仓库里也不该有 `.dev.vars`,它只对 `wrangler dev` 生效。)
|
**不能**验证后端转发,转发只能看生产。(仓库里也不该有 `.dev.vars`,它只对 `wrangler dev` 生效。)
|
||||||
|
|
||||||
**Node 版本不用管。** Cloudflare 环境实测给的是 `nodejs@24.18.0`,本地在 Node 24.21.0 与
|
**Node 版本不用管。** Cloudflare 环境实测给的是 `nodejs@24.18.0`,本地在 Node 24.21.0 与
|
||||||
|
|
@ -925,7 +925,7 @@ Encrypt(填 secret)。改 secret **即时生效,不需要重新部署**。
|
||||||
任何明文 secret 提交进仓库(本仓库里没有,`.gitignore` 也排除了)。
|
任何明文 secret 提交进仓库(本仓库里没有,`.gitignore` 也排除了)。
|
||||||
|
|
||||||
**验证方式与 CLI 完全相同**,见上方「验证(发布前必做)」,尤其第 2 条:
|
**验证方式与 CLI 完全相同**,见上方「验证(发布前必做)」,尤其第 2 条:
|
||||||
看 `/healthz` 响应体的 `available`,不要只看 HTTP 200。
|
看 `/healthz-worker` 响应体的 `available`,不要只看 HTTP 200。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -577,7 +577,7 @@ pub enum BotError {
|
||||||
|
|
||||||
**运行监控(`GATE-UPTIME-KUMA`):**
|
**运行监控(`GATE-UPTIME-KUMA`):**
|
||||||
- 使用 Uptime Kuma HTTP(s) Monitor 检查 `/healthz`(进程存活)和 `/ready`(配置 / Redis / 上游可达就绪),分别期望 HTTP 200;`/ready` 最坏约 3s,探针超时需设 ≥10s;`/ready` 不就绪时返回 `503` 与标准错误 envelope(`service_unavailable` + `Retry-After: 30`),Uptime Kuma 仍按状态码判定,不受响应体变化影响。
|
- 使用 Uptime Kuma HTTP(s) Monitor 检查 `/healthz`(进程存活)和 `/ready`(配置 / Redis / 上游可达就绪),分别期望 HTTP 200;`/ready` 最坏约 3s,探针超时需设 ≥10s;`/ready` 不就绪时返回 `503` 与标准错误 envelope(`service_unavailable` + `Retry-After: 30`),Uptime Kuma 仍按状态码判定,不受响应体变化影响。
|
||||||
- `/healthz` 是纯 liveness(无条件 `200`);`/ready` 检查配置完整性 + Redis 可达性 + 出站只读探测(JMAP session `GET`、TG `getMe`,各 3s、并行,最坏约 3s),探针只读、不回显 token 或第三方响应、不触发业务副作用,报告体不含敏感信息;由于 `/ready` 是最重的一环(可能 3s),平台侧应优先使用网关聚合的 `/healthz` 作为存活探测,避免高频出站请求。
|
- `/healthz` 是纯 liveness(无条件 `200`);`/ready` 检查配置完整性 + Redis 可达性 + 出站只读探测(JMAP session `GET`、TG `getMe`,各 3s、并行,最坏约 3s),探针只读、不回显 token 或第三方响应、不触发业务副作用,报告体不含敏感信息;由于 `/ready` 是最重的一环(可能 3s),平台侧应优先使用网关聚合的 `/healthz-worker` 作为存活探测,避免高频出站请求。
|
||||||
- 不引入 Prometheus、Exporter 或额外指标端口;真实 Stalwart/Telegram 端到端链路仍需单独联调。
|
- 不引入 Prometheus、Exporter 或额外指标端口;真实 Stalwart/Telegram 端到端链路仍需单独联调。
|
||||||
|
|
||||||
**生产红线(贯穿所有阶段)**:
|
**生产红线(贯穿所有阶段)**:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue