架构与边界:Pages / Node Functions / Edge Functions / KV

2026年1月25日
3 分钟阅读
作者:ixNieStudio

架构与边界:Pages / Node Functions / Edge Functions / KV

做“推送服务”最容易掉进一个坑:你以为难点是“发消息”,实际上难点是 在一个受限的运行环境里,把状态、鉴权、外部 API 的不确定性收敛住

这一篇不讲具体接口细节,只回答三个问题:

  • 这套系统在 EdgeOne 上到底怎么跑起来?
  • 为什么一定要多一层 Edge Functions 代理 KV?
  • 哪些边界一旦没划清,后期会极难维护?

先讲清楚目标(以及我刻意不做什么)

我想要的是:

  • 0 成本可运行(尽量不引入长期服务器)。
  • 一行 webhook 调用就能推送(脚本友好)。
  • 有 UI 管理(不想每次改配置都上 KV 控制台)。
  • 数据自托管(配置、绑定关系、历史都归自己)。

我刻意不做的是:

  • 不把它做成“强安全边界”的公网 API(例如 OAuth、复杂权限模型)。
  • 不追求“消息必达”的可靠投递(至少在当前版本,失败记录下来就够了)。

约束:边缘环境里,很多“理所当然”都不成立

在传统服务里,我们很习惯:

  • 后端直接连 Redis / DB
  • 进程内缓存 token
  • 用固定域名回调

但在 EdgeOne 的组合里,这些假设会频繁失效。你必须接受:

  • 运行形态分裂:Pages / Node Functions / Edge Functions
  • 请求可能来自不同域名(自定义域名、默认域名、甚至同源推断失败)
  • 外部 API(微信)会带来 token 失效、错误码、以及“成功但没送达”的灰度状态

这也是为什么,这个项目的“架构”不是为了炫技,而是为了把约束显式化。

一张图:三层运行形态 + 一条 KV 生命线

flowchart TB subgraph UI[前端(Pages)] WEB[管理后台 UI] end subgraph NF[Node Functions] API["管理 API
/channels /apps /openids /messages"] SEND["推送入口
/send/appKey"] SVC["Services
push/app/channel/openid/message"] end subgraph EF[Edge Functions] KVAPI["/api/kv/*
KV 代理 API"] end KV[("EdgeOne KV
CONFIG/CHANNELS/APPS/OPENIDS/MESSAGES")] WX["微信/企业微信 API"] WEB --> API API --> SVC SEND --> SVC SVC -->|HTTP| KVAPI KVAPI <--> KV SVC -->|HTTPS| WX

读图的方式:

  • 所有业务请求最终都落在 Node Functions。
  • Node Functions 不能直接碰 KV,于是所有 KV 操作都绕一圈到 Edge Functions。
  • 推送调用与管理调用共用同一套 service,只是鉴权边界不同。

关键决策 1:Node Functions 不直接访问 KV(而是通过 Edge Functions 代理)

这是这个项目最重要的一个决策。

在代码里,它落在:

  • node-functions/shared/kv-client.ts

这里的思路不是“写一个 SDK”,而是把 KV 当成一个 HTTP 服务

  • Node Functions 只负责拼 URL、带上内部鉴权 header
  • Edge Functions 负责真正的 KV get/put/delete/list

这样做的收益:

  • 运行环境差异被隔离:业务层不需要知道 KV 的访问细节。
  • 排障路径清晰:KV 问题就盯住 /api/kv/* 这一跳。
  • 安全边界更清楚:KV 代理 API 只对“内部调用”开放。

代价也很明确:

  • 多一次网络 hop(但在边缘上通常可接受)
  • 必须处理 baseUrl/域名推断问题

关键决策 2:KV_BASE_URL —— 用显式配置替代隐式推断

边缘环境最怕“我以为你知道我是谁”。

当请求直接打到 /send/* 时,Node Functions 在某些场景下并不能稳定推断出正确的“对外域名”,于是它拼出来的 /api/kv/* 地址可能指向错误的 host。

所以这里不赌运气,直接引入:

  • KV_BASE_URL=https://your-domain.com

这是一种很工程化的做法:

  • 让配置暴露在部署层
  • 让不确定性停止蔓延到业务逻辑

一条典型请求链路(推送)

推送看起来是 curl 一下,但内部有不少步骤:

sequenceDiagram autonumber participant C as Caller participant N as Node Functions (/send) participant K as Edge Functions (/api/kv) participant V as KV participant W as WeChat API C->>N: GET/POST /send/appKey N->>K: get(appKey->appId) K->>V: KV get V-->>K: appId K-->>N: appId N->>K: get(app, channel, openids) K->>V: KV get/list V-->>K: records K-->>N: records N->>W: send message (token cached) W-->>N: result N->>K: put(message history) K->>V: KV put V-->>K: ok K-->>N: ok N-->>C: {code:0,data:{success/failed}}

鉴权边界:两个世界,两种规则

这个项目有两个“外部入口”,它们的安全策略完全不同:

  • 管理端 API:需要管理员令牌(控制面,必须收紧)。
  • 推送入口 send:通过 appKey 做弱认证(数据面,追求接入简单)。

把这两个边界混在一起,会导致:

  • 你要么把 send 做得很难用(强鉴权、脚本不好接入)
  • 要么把管理 API 暴露得很危险(弱鉴权、容易被扫)

所以架构上就应该明确:

  • 业务编排可以复用
  • 鉴权策略不要复用

踩坑与排障(这类问题通常不是代码 bug)

  • KV 访问失败:优先查 KV_BASE_URL 是否为完整 https 域名,其次查 /api/kv/* 是否可访问。
  • 内部鉴权失败X-Internal-Key 不一致时,表现通常像“所有 KV 操作都报错”。
  • 微信 token 相关错误:不要只看“发送失败”,要看 token 状态缓存(它决定你是凭证错还是网络错)。

启发:把约束显式化,比“写得优雅”更重要

  • 显式边界能减少认知负担:Node 不碰 KV,把复杂度集中到一处。
  • 显式配置能减少线上玄学:KV_BASE_URL 这种看似“多此一举”的参数,能节省大量排障时间。
  • 把外部不确定性翻译成内部可观测状态:token status、message history 都是典型手段。