AI 对接 Skill

把下面这段提示词发给你的 AI 编程助手,它会读取完整对接规范并完成接入。

---

name: rekit-integration

description: >-

Rekit(开放平台)完整对接规范:供产品站或其它 AI agent 直接实现托管登录、

站点 API Key 开放能力(邮件/图床/公众号通知含查关注/支付 Checkout)。

用户提到 Rekit、REKIT、开放平台、X-Api-Key、access_token、subscribe-status、

login/mp、对接 skill 时使用。先完整阅读本文件(已含文末接口参考)。

---

Rekit · 开放平台对接(AI Agent 完整版)

本 skill 面向 其它 AI agent / 产品站开发:读完后应能写出可运行的对接代码,不要自建登录页、自配公众号网页授权、自发明鉴权。

公开直链(无需登录,含本文件 + reference):

https://rekitdev.com/skill.md

实现时优先遵循下文「最小实现」;字段级契约以文末「接口参考」为准(已拼入本文档)。

---

0. Agent 开工前必须拿到的材料

运营在 Rekit 后台配好站点后,应交给对接方(或你)至少:

| 材料 | 示例 | 用途 |

|------|------|------|

| 站点 slug | weibo / rekitdev | 登录跳转 site=;与 Key 绑定 |

| 站点 API Key | sk_... | 仅服务端 X-Api-Key |

| 已开通权限 | email_notify / storage / wechat_notify / payments / cdn / captcha | 未开通 → HTTP 403 |

| 产品站对外 URL / 域名白名单 | https://wb.example.com | 登录回跳、支付 success/cancel 校验 |

| 基址(可默认) | 见下表 | 环境变量 |

生产基址(默认):

| 变量 | 值 | 谁用 |

|------|-----|------|

| NEXT_PUBLIC_REKIT_AUTH_API_URL | https://login.rekitdev.com | 浏览器跳转登录(须 NEXT_PUBLIC_) |

| REKIT_API_URL | https://api.rekitdev.com | 服务端开放 API |

| NEXT_PUBLIC_REKIT_CAPTCHA_API_URL | https://api.rekitdev.com | 浏览器加载 captcha SDK / 获取 token |

| REKIT_PAYMENTS_API_URL | https://pay.rekitdev.com | 服务端支付 |

| REKIT_SITE_API_KEY | 站点 Key | 服务端;禁止进前端 bundle |

| NEXT_PUBLIC_BASE_URL | https://你的站 | 拼 callback / return_to |

本地端口:login 31401,pay 31402,api 31403,admin API 31400

仅有 Key、没有 slug / 域名白名单 / 权限绑定:登录回不了 token,开放接口会 403/503。

---

1. 硬规则(违反即对接错误)

1. 两套鉴权不要混

• 用户身份:Authorization: Bearer <access_token> → 只打 login.(如 /api/v1/me

• 站点能力:X-Api-Key: <站点 Key> → 打 api. / pay.(邮件、图床开放预签名、公众号通知、支付)

2. 登录只跳托管门户,产品站不自建注册/登录页,不自配公众号网页授权 AppID。

3. 微信内 H5/login/mp;浏览器通用用 /login。不要用产品站域去做微信 OAuth。

4. Key 永不进浏览器;开放能力请求放在服务端 / BFF。

5. 图床:拿预签名后客户端 直传 COS;读图用返回的 url / static. CDN,文件不经产品站中转。

6. 支付:优先 Checkout Session;success_url / cancel_url 的 host 必须在该站域名白名单内。

---

2. 子域名一览

| 子域 | 用途 | 产品站怎么用 |

|------|------|--------------|

| login. | 托管登录 + 用户 JWT | 浏览器 302;Bearer |

| api. | 邮件 / 图床 / 公众号通知 | 服务端 X-Api-Key |

| pay. | 支付 Checkout / 下单 | 服务端 X-Api-Key |

| static. | CDN 读图 | 公开 URL |

---

3. 登录对接(必做)

3.1 流程

用户点登录
  → 302 {LOGIN}/login?return_to={CALLBACK}&site={slug}&lang=zh|en
     (微信内 H5 → /login/mp,参数相同)
  → 用户在 login. 完成邮箱或微信
  → login. 302 → {CALLBACK}?login_code=...
  → 本站 callback 服务端 POST {LOGIN}/api/v1/auth/exchange 换 access_token
  → 写 HttpOnly Cookie,再 302 到业务 path(returnTo)

> 协议变更提醒login. 不再把签好的 JWT 明文放进跳转 URL(会留在浏览器历史 /

> 服务端访问日志 / Referer,且没有来源校验时任何人拿到别人的回跳链接就能把自己的

> 登录态嫁接到受害者浏览器上)。改为回传一次性 login_code(单次消费、90 秒内有效),

> 由 产品站服务端(不是浏览器)拿它换真正的 access_token。仍在用旧版

> ?access_token=... 直读方式的接入方必须升级 callback 实现,否则登录会失败。

{CALLBACK} 推荐:https://你的站/api/auth/callback?returnTo=/account

returnTo 为本站相对路径,须以 / 开头、禁止 //。)

site=<slug>要回传登录态时必填return_to 的 host 必须在该站「域名 / 对外 URL」白名单;否则登录成功但 不会 302 带回 login_code

3.2 跳转 URL 模板

# 浏览器 / 通用
{LOGIN}/login?return_to={encodeURIComponent(CALLBACK)}&site={slug}&lang=zh

# 微信内 H5(专用;非微信打开只提示「请在微信内打开」)
{LOGIN}/login/mp?return_to={encodeURIComponent(CALLBACK)}&site={slug}&lang=zh

# 微信内 H5 · 静默(snsapi_base,无授权页;推荐微信内首选)
{LOGIN}/login/mp?silent=1&return_to={encodeURIComponent(CALLBACK)}&site={slug}&lang=zh

检测微信内:/MicroMessenger/i.test(navigator.userAgent)

silent=1(静默登录):走 snsapi_base 网页授权,微信内不弹授权页——已在平台注册过且

已有 unionid 的用户零点击直接回跳 ?login_code=...;平台全新用户、以及库里还没有

unionid 的老用户,由平台回调自动跳一次 snsapi_userinfo 补 unionid / 昵称头像后回跳。

若用户拒绝授权,或 userinfo 之后仍无 unionid,平台不发 login_code,回到

/login/mp?need_consent=1 提示用户点「允许授权」再走一次 snsapi_userinfo

接入方只需带 silent=1,其余

(callback 收 login_codePOST /api/v1/auth/exchange 换 token)与非静默完全一致。仅微信内可用。

静默登录会把用户的公众号 openid 落到平台账号上,这正是内联 JSAPI 支付所依赖的 openid 来源(见 §8)

3.3 本站最小实现

1. 登录入口:按上表 302(或 window.location.href)。

2. GET /api/auth/callback(本站,服务端处理,不能是纯前端页面):

• 读 query:login_code(必填)、returnTo(相对路径)

• 服务端(不经浏览器)POST {LOGIN}/api/v1/auth/exchange,body {"code": login_code}

成功返回 {access_token, expires_in, user},失败(code 过期/已用过)返回 400,需引导用户重新登录

• 写 Cookie(参考名 rekit_user_token):HttpOnly; Path=/; SameSite=Lax;(HTTPS 加 Secure;可按主域设 Domain

Max-Age = max(60, expires_in)

• 302 到 returnTo

3. 鉴权:服务端用 Cookie 里的 token 调

GET {LOGIN}/api/v1/me,头:Authorization: Bearer <token>

响应:{ "user": { id, nickname, avatar_url, background_url, email, has_password, has_wechat_web, has_mp, unionid } }

没有 mp_openid 字段;只有 has_mp + unionid。发模板消息用的公众号 openid 见 §5。)

4. 登出:清 Cookie。

参考实现(本仓库):rekitdev/site.go 的 callback/profile/checkout handler 与 rekitdev/public/site.js 的同源调用。

3.4 平台侧微信配置(对接方知悉,一般由运营做)

• 开放平台「网站应用」授权回调域、公众号「网页授权域名」均填 login.你的域名(如 login.rekitdev.com),不是产品站域名。

• 使用 /login/mp 时后台须启用「公众号网页授权」。

• 小程序与公众号须同开放平台主体,才能用 unionid 对齐账号。

3.5 用户资料 / 账号绑定(Bearer)

| 方法 | 路径 | 说明 |

|------|------|------|

| GET | /api/v1/me | 当前用户 |

| PATCH | /api/v1/me | {nickname?, avatar_url?, background_url?};URL 须 http(s) |

| POST | /api/v1/me/bind-email/code | {email} |

| POST | /api/v1/me/bind-email | {email, code, password};密码 ≥6;邮箱占用 409 |

| POST | /api/v1/me/unbind-email | 解绑前须已绑微信 |

| POST | /api/v1/me/password | {current_password, new_password} |

| GET | /api/v1/me/bind-wechat/web/start?return_to= | 返回 {state, authorize_url, redirect_uri} |

| POST | /api/v1/me/unbind-wechat | {channel:"web"};解绑前须有邮箱密码 |

完整表见 reference。

---

4. 开放 API 通用约定

X-Api-Key: <REKIT_SITE_API_KEY>
Content-Type: application/json

| HTTP | 含义 |

|------|------|

| 401 | 缺 Key / Key 无效 / Bearer 无效 |

| 403 | 站点停用或未开通该权限 |

| 400 | 参数错误 / 业务拒绝(见 detail) |

| 409 | 冲突:JSAPI payer_mp_openid_missing / jsapi_appid_mismatch,应回退扫码 |

| 422 | 请求体校验失败 |

| 429 | 超限(平台不再对通知类接口设站点级配额;此码仅来自登录/发码等端点) |

| 502 | 上游失败(如 SMTP) |

| 503 | 平台或站点未绑定配置(SMTP / COS / 支付 / 微信等) |

统一错误外壳: 所有对外错误响应体恒为 {"detail": {"code": "...", "message": "..."}}(平台网关层统一)。code 为稳定机器码(如 api_key_missingpermission_deniedsite_provider_unboundvalidation_error),可用于多语言映射;message 为兜底人读文案。接入方只需解析这一种形状。

权限 ID:storagecdnwechat_notifyemail_notifypaymentscaptcha

站点自检(对接第一步): GET {REKIT_API_URL}/api/v1/site,只需 X-Api-Key,无权限门槛。返回各能力的 permission(权限位)与 ready(权限位 后台已绑配置,才是真正可调用);ready: falsemissing 精确说明缺什么——可直接转给运营当工单,不必逐个功能试出 503。存储另带 read_modepublic = 存 unsigned_url 永久链;signed = 带 CDN 鉴权、仅即传即用)与 cdn_auth_enabled(后台原始开关);支付另带 providers(可用渠道,与下单同一判定)与 webhook_configuredfalse = 到账不推送;后台配了 webhook_url 才有,且后配不补历史)。完整响应形状见文末接口参考。

---

5. Captcha(captcha)— api.

Rekit 托管登录页已内置 captcha。产品站如果自有表单也要防注册/登录/敏感动作,可复用同一套。

浏览器侧只使用公开站点 slug,不暴露 API Key:

<script src="https://api.rekitdev.com/rc/v1/rekit.min.js"></script>
<div id="captcha"></div>
<script>
  const rk = ReKitCaptcha.init({
    api: "https://api.rekitdev.com/rc/v1/s/你的站点slug"
  });
  rk.render(document.getElementById("captcha"), {
    onToken(token) {
      // 随业务表单一起提交给你的服务端
    }
  });
</script>

服务端校验 token(必须用 X-Api-Key,且 token 只能消费一次):

POST /api/v1/captcha/verify
X-Api-Key: <REKIT_SITE_API_KEY>
Content-Type: application/json

{ "token": "<browser token>" }

成功:{"ok":true,"site":"你的站点slug","sub":"rekit-pass|rekit-verify","score":0.9,"expires_at":...}

失败或重放:400 captcha_invalid。站点未开权限:403 permission_denied

---

6. 公众号通知(wechat_notify)— api.

基址: {REKIT_API_URL}

鉴权: X-Api-Key + 权限 wechat_notify

收件人身份: 统一用 unionid(接入方共享同一开放平台主体,只拿得到 unionid;公众号 openid 只有公众号自己能拿)。平台内部用 members 表映射到投递 openid。

限流: 平台侧不设站点配额;实际速率上限由下游渠道决定(微信模板消息配额 / SMTP 服务商频控)。

路径: /api/v1/notify/*

5.0 前置:拿到 unionid + 用户须关注

通知与查关注以 unionid唯一收件人身份。能用 unionid 的前提是:

你站点的公众号、你的小程序/网站应用,绑定在同一个微信开放平台账号下

(同一用户在各应用里才会拿到同一 unionid)。这是接入前一次性配置——

小程序/网站应用侧由你自己绑(开放平台「管理中心」),平台公众号侧由运营绑;

没绑齐,unionid 拿不到或对不上,查关注/通知对该用户永久不可用且报错与

"未关注"不可区分(只能靠平台后台告警发现)。

三条渠道拿到 unionid(按你的接入形态选一条):

| 接入形态 | 怎么拿 unionid |

|---|---|

| 小程序自有登录(主画像) | 小程序 wx.login() → 你的服务端 jscode2session → 响应直接含 unionid(前提:小程序已绑开放平台) |

| 网站自有微信登录 | 你的网站应用 OAuth(snsapi_login)→ access_token + openidsns/userinfo 响应含 unionid |

| 全托管 Rekit 登录 | 登录后 GET /api/v1/me(或 auth/exchange 响应)的 user.unionid,平台已替你落库 |

关注是投递前提: 模板消息只能发给已关注该站点绑定服务号的用户。

• 用户该关注哪个号:运营在后台给你站点绑定的那套公众号——向运营要公众号名称/二维码(平台不托管关注页;/wechat/subscribe/{app_id} 是一次性订阅消息授权页,与关注无关)。

• 用户关注后平台回调自动落库 unionid→openid,随后 subscribe-status 即可查到(通常秒级)。

• 产品站推荐形态:发通知前 subscribe-status 预检;subscribed !== true 时在通知设置/订单页展示该服务号的关注入口(公众号资料卡/二维码),关注后再发。

• 用户明明已关注却始终 subscribed:false / wechat_member_not_found:几乎必然是公众号未绑开放平台(平台拿不到 unionid),联系运营核查绑定。

5.1 查是否关注

GET /api/v1/notify/subscribe-status?unionid=oYYYY

Query 传 unionid

成功:

{
  "ok": true,
  "openid": "公众号openid",
  "subscribed": true,
  "nickname": "昵称或null",
  "unionid": "unionid或null"
}

关键约束:

• 平台用 members 表把 unionid → 公众号 openid(用户须曾关注过,并由公众号事件/回填写入)。

• 库中无该 unionid(从未关注)→ 返回 {"subscribed": false}(不再报错),据此判断即可。

• 实时结果以微信 user/infosubscribe==1 为准,不是只读本地缓存。

推荐用法(产品站):

1. 直接用登录用户的 unionid 查。

2. subscribed !== true 时不要发模板消息(发也发不出去)。

5.2 发「工单状态」模板消息

POST /api/v1/notify/work-order-status
Content-Type: application/json

{
  "unionid": "oYYYY",
  "project_name": "你的产品名",
  "status": "账号过期",
  "customer_name": "用户昵称",
  "time": "2026-07-17 18:00",
  "url": "https://你的站/notify?scene=expired",
  "client_msg_id": "expired:123"
}

| 字段 | 必填 | 说明 |

|------|------|------|

| unionid | 是 | 接入方共享 unionid;平台内部映射到投递 openid |

| project_name | 是 | ≤100 |

| status | 是 | ≤100;映射到 short_thing 时微信侧常 ≤5 字 |

| customer_name | 是 | ≤100 |

| time | 否 | YYYY-MM-DD HH:MM 或带秒;不传用服务器当前时间 |

| url | 否 | 点击跳转 |

| client_msg_id | 否 | 防重 |

成功:{ "ok": true, "msgid": "..." }

收件人 unionid 从未关注公众号:400 wechat_member_not_found(发送前用 5.1 预检)

模板字段映射由运营在公众号配置里设置;产品站只传上述语义字段。

---

7. 邮件(email_notify)— api.

POST /api/v1/notify/email
{
  "to": "a@b.com",
  "subject": "主题",
  "html": "<p>正文</p>"
}

成功:{ "ok": true }

to 必须是单个合法邮箱地址:不接受显示名(张三 <a@b.com>)、逗号/分号分隔的多地址、含空格或换行的值 → 400 email_invalid(入参问题,重试无用,改正地址再发)。

站点须在后台「站点接入」绑定 SMTP;未绑定 → 503 site_email_unbound

注册验证码由 login 服务自己发信,不要走本接口。

---

8. 图床(storage)— api.

7.1 站点开放预签名(Api-Key,不计入用户配额)

POST /api/v1/storage/open/presign
{
  "filename": "a.webp",
  "content_type": "image/webp",
  "content_length": 12345,
  "subdir": "products/thumbs"
}

content_type 仅:image/jpeg | image/png | image/webp | image/gif

content_length必填(字节,与 PUT body 长度一致);缺失 → 400 content_length_required(不传则预签名 URL 不带长度约束,客户端可 PUT 任意大小对象)。用户 Bearer 预签名同样必填

• 单文件 ≤ 5MB

• 响应:{ key, put_url, url, unsigned_url, expires_in }(秒,通常 300)

• 客户端对 put_urlHTTP PUT,Header:Content-Type 与申请时一致;若预签名绑定了长度则 body 长度必须等于 content_length

• 读图用 url(可能带 CDN 鉴权)或 unsigned_url

7.2 登录用户资料图(Bearer,计入用户配额 ≤15MB)

POST /api/v1/storage/presign
Authorization: Bearer <access_token>
{ "filename":"a.webp", "content_type":"image/webp", "content_length":12345, "subdir":"avatars" }

subdiravatars / backgrounds 时,换绑后由 PATCH /api/v1/me 删除旧对象。

---

9. 支付 Checkout(payments)— pay.

POST /api/v1/payments/open/checkout/sessions
{
  "mode": "payment",
  "success_url": "https://你的站/checkout/success?session_id={CHECKOUT_SESSION_ID}",
  "cancel_url": "https://你的站/pricing",
  "user_id": 123,
  "external_customer_id": "站点自管客户标识(与 user_id 二选一)",
  "client_reference_id": "本地订单号",
  "customer_email": "a@b.com",
  "metadata": { "plan_type": "personal-monthly" },
  "line_items": [{
    "quantity": 1,
    "price_data": {
      "currency": "cny",
      "unit_amount": 4900,
      "product_data": {
        "name": "个人版 - 月付",
        "description": "有效期1年,不自动续费",
        "unit_label": "/ 月",
        "features": ["权益1", "权益2"]
      }
    }
  }]
}

• 付款方双轨:user_id(login. /me 的平台用户 id)与 external_customer_id(站点自管客户标识)二选一;未接 Rekit 登录用后者即可

• 目前 仅支持恰好 1 条 line_itemsunit_amount 单位为

• 响应含 idurlstatuspayment_statusamount_total

• 浏览器 302 到 url(如 https://pay.rekitdev.com/c/pay/cs_...

• 回站后履约:

GET /api/v1/payments/open/checkout/sessions/{id}

仅当 payment_status === "paid" 时履约。可手动过期:POST .../sessions/{id}/expire

底层直连(兼容,一般不必用):

POST /api/v1/payments/open/orders
{ "provider": "wechat", "amount_cents": 100, "subject": "标题", "external_customer_id": "cust_123" }

providerwechat | alipay。付款方 user_idexternal_customer_id 二选一(都没传 → 422)。

站点须绑定对应支付配置。响应含 order_id / out_trade_no /

code_url(微信 NATIVE 扫码)/ pay_url(支付宝)。

查订单状态(站点 Key,webhook 之外的轮询兜底):

GET /api/v1/payments/open/orders/{order_id}

返回 statuspending | paid | ...)等;仅当 paid 时履约。

8.2 微信内联 JSAPI(产品站自己页面直接拉起付款,不跳转、不扫码)

微信内(MicroMessenger)希望在产品站自己的页面点一下直接弹微信付款,用这个。前提:站点

微信支付配置的 appid 必须等于平台公众号 appid(同一服务号),运营在后台绑定即可。

JSAPI 需 Rekit 微信身份:付款人 openid 由平台按 user_idUser.mp_openid 解析(产品站不接触

openid),前置该用户先完成 Rekit 微信登录(§3 的 silent=1 即可)。external_customer_id 的外部客户

拿不到公众号 openid,JSAPI 不可用,请走 NATIVE 扫码 / 支付宝 / §8 的 Checkout Session。

POST /api/v1/payments/open/orders
X-Api-Key: <站点 Key>
{ "provider": "wechat", "jsapi": true, "amount_cents": 100, "subject": "标题", "user_id": 123 }

成功响应含 prepay_params,产品站在自己页面拉起:

// 必须在微信内置浏览器中;prepay_params 为服务端返回的对象
WeixinJSBridge.invoke('getBrandWCPayRequest', order.prepay_params, function (res) {
  if (res.err_msg === 'get_brand_wcpay_request:ok') {
    // 用户已支付:以服务端为准履约——等 webhook order.paid,或轮询 GET /open/orders/{id}
  } else {
    // 取消或失败,提示重试
  }
});

• 到账仍以服务端为准:靠 webhook order.paid(§8.1)+ GET /open/orders/{order_id} 兜底,

不要只信前端 getBrandWCPayRequest:ok

409 {code:"payer_mp_openid_missing"}:该用户无公众号 openid(如仅邮箱登录)→ 引导其先做

微信登录,或回退 NATIVE 扫码 / 跳转 §8 的 Checkout Session。

409 {code:"jsapi_appid_mismatch"}:站点支付 appid≠公众号 appid,运营需在后台改配;此前回退扫码。

• 不传 jsapi → 维持 NATIVE 扫码(返回 code_url);alipay 忽略该字段。

8.1 Webhook —— 到账主动推送(推荐主力,轮询作兜底)

配置:后台「站点接入」→ 站点详情填 Webhook URL(https)、复制 Webhook Secret(站点详情随时可查看复制)。留空则不推送、退回轮询。

到账时 Rekit 向该地址 POST 一个签名事件(门铃);产品站验签后即发货,不必守着轮询。轮询 GET .../sessions/{id} 仍保留作兜底(保险锁)——万一 webhook 漏收,靠它补齐。两条腿都用,不丢单。

请求头:

| 头 | 说明 |

|---|---|

| X-Rekit-Event-Id | 事件唯一 id(evt_...),用于去重 |

| X-Rekit-Event-Type | order.paidping 为后台测试事件) |

| X-Rekit-Timestamp | Unix 秒,用于防重放 |

| X-Rekit-Signature | sha256=<hmac>,见下方验签 |

Body(order.paid):

{ "id":"evt_ab12…", "type":"order.paid", "created":1710000000,
  "data":{ "order_id":123, "out_trade_no":"…", "provider":"wechat",
           "amount_cents":4900, "currency":"cny", "user_id":456, "external_customer_id":null, "site_id":1,
           "checkout_session_id":"cs_…", "client_reference_id":"你的本地单号",
           "metadata":{…}, "paid_at":"2026-07-18T09:00:00+00:00" } }

验签hex( HMAC_SHA256(webhook_secret, "<X-Rekit-Timestamp>.<原始 body>") ) 应等于 X-Rekit-Signature 去掉 sha256= 前缀。务必用原始请求体字节验签,勿先 JSON 反序列化再拼回。

// Node.js / Express(express.raw 拿原始 body)
import crypto from "crypto";
app.post("/api/rekit/webhook", express.raw({ type: "*/*" }), (req, res) => {
  const ts = req.get("X-Rekit-Timestamp") || "";
  const sig = (req.get("X-Rekit-Signature") || "").replace(/^sha256=/, "");
  const expect = crypto.createHmac("sha256", process.env.REKIT_WEBHOOK_SECRET)
                       .update(`${ts}.${req.body.toString("utf8")}`).digest("hex");
  if (sig.length !== expect.length ||
      !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expect))) {
    return res.status(400).end();            // 验签失败
  }
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.status(400).end(); // 防重放
  const evt = JSON.parse(req.body.toString("utf8"));
  // 幂等:按 evt.id 或 data.order_id 去重后再发货
  fulfillOnce(evt);
  res.status(200).end();                      // 必须回 2xx,否则会重试
});

重试与幂等:非 2xx / 超时 → 按退避重试(30s→2m→10m→30m→2h→6h→12h,约 8 次后置 failed,后台可「重发」)。同一笔到账只推一次事件,但重试可能让同一 event_id 到达多次——产品站务必按 event_idorder_id 幂等发货(至少一次语义)。

联调:后台站点详情「发送测试事件」会推一条 type:"ping",可用来验证地址连通 + 验签逻辑。

---

10. 多租户边界

• 用户账号是 平台级 SSO(邮箱/微信全局唯一;JWT 不含 site_id)。

• 开放能力按 站点 Key + 权限 隔离;支付订单带 site_id

• 登录回跳 / Checkout URL 按站点域名白名单校验。

• 微信模板消息使用该站点绑定的公众号配置取 access_token。

---

11. 运营后台一次性配置(对接方 checklist)

1. 「站点接入」:建站 → 填域名/对外 URL → 复制 API Key → 开权限 → 绑定邮件/存储/支付/公众号(按需)。

2. 「邮件通知」:SMTP → 站点选用。

3. 「对象存储」:COS;public_base = CDN(如 https://static.你的域名);直传域名勿填 CDN。

4. 支付:商户配置 → 站点绑定;(推荐)填 Webhook URL + 复制 Webhook Secret,让到账主动推送产品站(§8.1)。

5. 登录:微信回调域 = login.;系统设置 secret_key / auth_public_base_url 等各服务共用。

---

12. 给其它 AI 的提示词

请先完整读取对接 skill(含文末接口参考),再按其中流程实现产品站对接,不要自建登录或公众号网页授权:
https://rekitdev.com/skill.md

我将提供:站点 slug、API Key、已开通权限、产品站 BASE_URL。
请实现:登录跳转 + callback 写 Cookie、/me、以及我开通权限对应的开放 API。

---

13. 实现检查清单

• [ ] 对接第一步调 GET /api/v1/site 自检;ready: false 的项按 missing 找运营补齐

• [ ] 登录只跳 login./login/login/mp,带 site + return_to

• [ ] callback 收 login_code,服务端 POST /api/v1/auth/exchangeaccess_token / expires_in,再写 HttpOnly Cookie(不要在浏览器端/前端代码里直接读 URL 上的 token)

• [ ] 用户 API 用 Bearer;开放 API 用服务端 X-Api-Key

• [ ] Key 未进前端;权限已开

• [ ] 查关注 / 发模板:路径与字段按 §5;对外传 unionid(推荐),无需公众号 openid

• [ ] 发模板前用 subscribe-status 预检;unionid 从未关注 → subscribed:false / 发模板 400 wechat_member_not_found

• [ ] 支付付款方:接了登录传 user_id,否则传 external_customer_id(二选一)

• [ ] 邮件/存储/支付站点已绑定配置

• [ ] 图床直传对象存储;支付用 Checkout 且 success/cancel host 在白名单

• [ ] (推荐)配 Webhook:验签 X-Rekit-Signature、按 event_id/order_id 幂等发货、回 2xx;轮询作兜底

• [ ] 微信授权回调域均为 login. 域名

• [ ] 软件版本分发:presign 直传 → 注册版本(§14);客户端走公开 latest / download,下载后用 sha256 校验

---

14. 软件版本管理(releases)— api.

给自己的软件(桌面端 / 移动端安装包等)做版本分发。写面要 API Key,读面公开——客户端软件做更新检查不能内置 API Key。

发布流程(CI / 发布脚本)

# 1) 预签名直传(文件不经平台服务器带宽)
POST /api/v1/releases/open/presign
X-Api-Key: <站点 Key>
{ "content_type": "application/zip", "content_length": 12345678 }

# → { "key": "img/<slug>/releases/<uuid>.bin", "put_url": "https://...", "expires_in": 300, "max_bytes": 1073741824 }

# 2) 客户端对 put_url 做 HTTP PUT(Content-Type / Content-Length 与申请一致)

# 3) 注册版本
POST /api/v1/releases
X-Api-Key: <站点 Key>
{ "version": "1.2.0", "channel": "stable", "notes": "更新说明",
  "storage_key": "<第 1 步返回的 key>", "size_bytes": 12345678,
  "sha256": "<64 位 hex,客户端下载后校验用>" }

content_length 必填、≤ 1GB;version 须 semver(1.2.3 / 1.2.3-beta.1);channel 默认 stablesha256 64 位 hex

• 注册前平台 HEAD 校验对象已上传且大小一致;同通道同版本重复注册 → 409 release_exists

• 站点未绑定存储 → 503 site_storage_unbound(与图床同语义,不做全局兜底)

客户端读面(公开,无需 Key)

GET /api/v1/releases/{site_slug}/{channel}/latest
GET /api/v1/releases/{site_slug}?channel=stable&limit=20
GET /api/v1/releases/{site_slug}/{channel}/{version}/download   # 302 → CDN(鉴权开启时签名),下载计数 +1

响应含 version / notes / size_bytes / sha256 / published_at / download_url;无可用版本 → 404 release_not_found

「下架」由运营后台操作,下架后公开查询与下载立即 404。

---

接口参考(完整契约)

产品站 / AI agent 按需查阅。鉴权:用户接口用 Authorization: Bearer;带权限 ID 的接口用 X-Api-Key

基址:

• login. → https://login.rekitdev.com(或 NEXT_PUBLIC_REKIT_AUTH_API_URL

• api. → https://api.rekitdev.com(或 REKIT_API_URL

• pay. → https://pay.rekitdev.com(或 REKIT_PAYMENTS_API_URL

错误体统一形态:{"detail": {"code": "...", "message": "..."}}(平台在网关层统一外壳,接入方只需解析这一种)。

---

login. — 浏览器门户(非 JSON)

| 方法 | 路径 | Query | 说明 |

|------|------|-------|------|

| GET | /login | return_to(回跳 URL)、site(slug)、lang=zh\|en | 托管登录(邮箱/微信门户) |

| GET | /login/mp | 同上 + silent=1(可选) | 微信内公众号 H5 登录;非微信仅提示。silent=1→snsapi_base 静默(已有 unionid 的老用户零点击;新用户或缺 unionid 的老用户自动一次 userinfo)。拒绝授权或仍无 unionid 时不发 login_code,回门户 need_consent=1 再点允许。回传同样是 login_code |

| GET | /login/wechat | 同上 | 门户混入口(微信内→公众号,否则→PC 扫码);产品站 H5 请用 /login/mp |

| GET | /register | 同上 | 托管注册(回传同样是 login_code) |

| GET | /forgot-password | return_to 等 | 托管找回密码 |

登录成功后(site 有效且 return_to host 在白名单):

302 → {return_to}?login_code={一次性码}

return_to 可自带 query(如 .../callback?returnTo=%2Fmember),平台仅追加 login_code。产品站服务端再用 POST /api/v1/auth/exchange {code: login_code}access_token + user(一次性、90s 有效)——原始 token 不出现在浏览器 URL 里。

---

login. — JSON API

邮箱认证(一般由托管页使用;产品站通常不必直调)

| 方法 | 路径 | Body | 成功要点 |

|------|------|------|----------|

| POST | /api/v1/auth/email-code | {email, purpose:"register"\|"reset"} | 发验证码 |

| POST | /api/v1/auth/register | {email, code, password} | 含 access_tokenexpires_inuser |

| POST | /api/v1/auth/login | {email, password} | 同上 |

| POST | /api/v1/auth/reset-password | {email, code, password} | 重置密码 |

密码长度 ≥ 6。

配置与微信登录(唯一入口:托管门户)

| 方法 | 路径 | 说明 |

|------|------|------|

| GET | /api/v1/app-config | 微信登录/支付是否启用 |

| GET | /login?return_to=&site= | 托管登录门户(PC 扫码 / 邮箱);登录后 302 回 return_to?login_code=… |

| GET | /login/mp?return_to=&site=&silent=1 | 微信内 H5 公众号登录;silent=1 走 snsapi_base 静默(无授权页) |

| POST | /api/v1/auth/exchange | {code}(回跳带回的 login_code)→ access_token + user(一次性换票) |

平台回调 /api/v1/auth/wechat/{web,mp,mp/silent}/callback 由门户内部使用,产品站不直接调用

唯一路子: 浏览器 /login,微信内 H5 /login/mp?silent=1,回跳拿 login_codePOST /api/v1/auth/exchange 换 token。不要在产品站自拼微信授权 URL,也不存在 state/authorize-url 这类底层入口。

当前用户(Bearer)

#### GET /api/v1/me

{
  "user": {
    "id": 1,
    "nickname": "昵称",
    "avatar_url": "https://...",
    "background_url": "https://...",
    "email": "a@b.com",
    "has_password": true,
    "has_wechat_web": false,
    "has_mp": true,
    "unionid": "oUnion..."
  }
}

注意:响应 不含 mp_openid;仅有 has_mp。公众号 openid 请用 api. subscribe-status 解析并自行缓存。

#### PATCH /api/v1/me

Body(均可选,至少一项有意义):

{ "nickname": "...", "avatar_url": "https://...", "background_url": "https://..." }

avatar_url / background_urlhttp://https://,或空串清空

nickname 非空

• 换绑资料图时平台会删除旧 COS 对象(若属于本平台存储)

#### 邮箱绑定 / 密码 / 微信

| 方法 | 路径 | Body / Query | 说明 |

|------|------|--------------|------|

| POST | /api/v1/me/bind-email/code | {email} | 返回 {expires_in} |

| POST | /api/v1/me/bind-email | {email, code, password} | 邮箱占用 409 |

| POST | /api/v1/me/unbind-email | — | 须已绑微信 |

| POST | /api/v1/me/password | {current_password, new_password} | 须已设密码 |

| GET | /api/v1/me/bind-wechat/web/start | return_to= | {state, authorize_url, redirect_uri} |

| POST | /api/v1/me/unbind-wechat | {channel:"web"} | 须已有邮箱密码 |

---

api. — 站点自检

权限: 无(只需 X-Api-Key 有效、站点启用)

GET /api/v1/site

返回本站点身份与各能力就绪状态——对接第一步先调它。permission = 权限位;ready = 权限位 后台已绑定可用配置;missing = 未就绪的精确原因(可直接作为给运营的工单)。

{
  "site": {"slug": "yoursite", "name": "YourSite", "domain": "your.site.com"},
  "capabilities": {
    "storage": {
      "permission": true,
      "ready": true,
      "missing": null,
      "read_mode": "public",
      "cdn_auth_enabled": false,
      "public_base": "https://static.example.com"
    },
    "releases": {"permission": true, "ready": true, "missing": null},
    "wechat_notify": {
      "permission": true,
      "ready": false,
      "missing": "未绑定公众号配置,或所绑公众号已停用,请联系运营在后台「站点接入」绑定"
    },
    "email_notify": {"permission": true, "ready": true, "missing": null},
    "captcha": {"permission": true, "ready": true, "missing": null},
    "payments": {
      "permission": true,
      "ready": true,
      "missing": null,
      "providers": ["wechat", "alipay"],
      "webhook_configured": true
    }
  },
  "docs": "https://rekitdev.com/skill.md"
}

| 字段 | 说明 |

|------|------|

| capabilities.*.ready | 权限位 + 后台已绑配置且启用,才是真正可调用 |

| storage.read_mode | publicunsigned_url 是永久链;signedurl 带 CDN 鉴权(默认 3600s 过期、无续期端点,仅即传即用) |

| storage.cdn_auth_enabled | 后台原始开关,与 read_mode 可能不一致(开了鉴权但站点无 cdn 权限时,生效仍是 public) |

| releases | 软件版本管理:ready = releases 权限 + 存储已绑定(读面公开查询/下载随时可用,写面须 ready) |

| payments.providers | 可用渠道(绑定 + 启用 + 配置完整,与下单运行时同一判定) |

| payments.webhook_configured | false = 到账不推送(后台配 webhook_url 后才有;后配不补历史,靠轮询兜底) |

---

api. — 邮件

api. — Captcha

权限: captcha

浏览器协议: /rc/v1/s/{site_slug}(SDK 自动调用 handshake / assess / challenge / verify)

SDK: {REKIT_API_URL}/rc/v1/rekit.min.js

POST /api/v1/captcha/verify

服务端用站点 Key 校验浏览器拿到的 token。默认 consume:true,同一 token 只能成功一次。

{ "token": "...", "action": "可选", "consume": true }

成功:

{ "ok": true, "site": "yoursite", "sub": "rekit-pass", "score": 0.92, "expires_at": 1710000000 }

失败:400 captcha_invalid;未开权限:403 permission_denied

---

权限: email_notify

头: X-Api-Key

路径: /api/v1/notify/*

POST /api/v1/notify/email

{ "to": "a@b.com", "subject": "主题", "html": "<p>正文</p>" }

| 字段 | 约束 |

|------|------|

| to | 单个合法邮箱,3–254;显示名 / 多地址 / 含空格换行一律拒 |

| subject | 1–200 |

| html | 非空 |

成功:{"ok": true}

收件人不合法:400 email_invalid(改地址,别重试)

未绑定 SMTP:503 site_email_unbound

SMTP 失败:502 email_send_failed

---

api. — 公众号通知

权限: wechat_notify

头: X-Api-Key

收件人身份: 统一用 unionid(接入方共享同一开放平台主体,只拿得到 unionid;公众号 openid 只有公众号自己拿得到)。平台内部用 members 表映射到投递 openid。

限流: 平台侧不设站点配额;实际速率上限由下游渠道决定(微信模板消息配额 / SMTP 服务商频控)。

路径: /api/v1/notify/*

GET /api/v1/notify/subscribe-status

Query:

| 参数 | 说明 |

|------|------|

| unionid | 平台用 members 表解析到公众号 openid |

成功:

{
  "ok": true,
  "openid": "oMp...",
  "subscribed": true,
  "nickname": "昵称",
  "unionid": "oUnion..."
}

失败 / 边界:

| 情况 | HTTP / 返回 |

|------|------|

| 未传 unionid | 422 |

| unionid 从未关注(members 无记录) | 200 {"subscribed": false}(不再报错) |

| 微信 API 错误 | 400 |

| Key / 权限 | 401 / 403 |

subscribed:微信 cgi-bin/user/infosubscribe == 1(实时)。

POST /api/v1/notify/work-order-status

{
  "unionid": "oUnion...",
  "project_name": "你的产品名",
  "status": "账号过期",
  "customer_name": "用户昵称",
  "time": "2026-07-17 18:00",
  "url": "https://站/notify?scene=expired",
  "client_msg_id": "可选防重ID"
}

| 字段 | 必填 | 约束 |

|------|------|------|

| unionid | 是 | 接入方共享 unionid;平台内部映射到投递 openid |

| project_name | 是 | ≤100 |

| status | 是 | ≤100;short_thing 模板常限制约 5 字 |

| customer_name | 是 | ≤100 |

| time | 否 | YYYY-MM-DD HH:MM / HH:MM:SS / YYYY/MM/DD HH:MM |

| url | 否 | 点击跳转 |

| client_msg_id | 否 | 防重 |

成功:{"ok": true, "msgid": "..."}

time 格式非法:400 invalid_time_format

收件人 unionid 从未关注公众号:400 wechat_member_not_found(先用 subscribe-status 预检)

---

api. — 图床

GET /api/v1/storage/config(可无 Key)

{
  "enabled": true,
  "public_base": "https://static.example.com",
  "cdn_auth_enabled": false,
  "path_prefix": "...",
  "max_single_upload_bytes": 5242880,
  "max_user_storage_bytes": 15728640
}

POST /api/v1/storage/open/presign — 权限 storage + Api-Key

请求:

{
  "filename": "a.webp",
  "content_type": "image/webp",
  "content_length": 12345,
  "subdir": "products/thumbs"
}

| 字段 | 必填 | 说明 |

|------|------|------|

| filename | 否 | 默认 upload.webp |

| content_type | 否 | 默认 image/webp;仅 jpeg/png/webp/gif |

| content_length | 是 | 字节;与 PUT body 一致;缺失 → 400 content_length_required;校验 ≤5MB |

| subdir | 否 | ≤128;落在 {path_prefix}/{site_slug}/ 下 |

响应:

{
  "key": "prefix/slug/subdir/uuid.webp",
  "put_url": "https://cos.../...",
  "url": "https://static.../...",
  "unsigned_url": "https://static.../...",
  "expires_in": 300
}

站点未绑定存储:503 site_storage_unbound

客户端:PUT put_urlContent-Type 与申请一致。

POST /api/v1/storage/presign — Bearer 用户上传

同上 body(content_length 同样必填);计入用户配额(总 ≤15MB)。

响应额外含 quota: {used_bytes, max_bytes, max_single_bytes}

GET /api/v1/storage/quota — Bearer

{ "used_bytes": 0, "max_bytes": 15728640, "max_single_bytes": 5242880 }

---

api. — 版本管理

写权限: releases写头: X-Api-Key读接口公开(按 {site_slug} 定位站点,客户端更新检查不带 Key)。

POST /api/v1/releases/open/presign — 权限 releases + Api-Key

请求:

{ "content_type": "application/zip", "content_length": 12345678 }

| 字段 | 必填 | 说明 |

|------|------|------|

| content_type | 否 | 默认 application/octet-stream |

| content_length | 是 | 字节;≤ 1GB;缺失 → 422 content_length: Field required |

响应:

{ "key": "prefix/slug/releases/uuid.bin", "put_url": "https://cos.../...", "expires_in": 300, "max_bytes": 1073741824 }

客户端 PUT put_urlContent-Type 与申请一致。站点未绑定存储:503 site_storage_unbound

POST /api/v1/releases — 权限 releases + Api-Key

请求:

{
  "version": "1.2.0",
  "channel": "stable",
  "notes": "更新说明(可选)",
  "storage_key": "<presign 返回的 key>",
  "content_type": "application/zip",
  "size_bytes": 12345678,
  "sha256": "<64 位十六进制>"
}

| 字段 | 必填 | 说明 |

|------|------|------|

| version | 是 | semver,如 1.2.3 / 1.2.3-beta.1;非法 → 422。注册时规范化存储:剥 v 前缀与 +build 元数据,各段不允许前导零 |

| channel | 否 | 默认 stable;字母数字开头,可含 -_.,≤32 |

| notes | 否 | ≤2000 字符 |

| storage_key | 是 | 须位于 {path_prefix}/{site_slug}/releases/ 命名空间,否则 400 storage_key_invalid |

| content_type | 否 | 默认 application/octet-stream;须 type/subtype 形式 |

| size_bytes | 是 | 须与对象实际大小一致(平台 HEAD 校验) |

| sha256 | 是 | 64 位 hex |

错误:对象未找到 / 大小不符 → 400 storage_object_missing;同通道同版本已存在 → 409 release_exists

响应 201:{ id, channel, version, notes, content_type, size_bytes, sha256, download_count, published_at, download_url }

GET /api/v1/releases/{site_slug}/{channel}/latest — 公开

通道内 semver 最新(仅未下架)。无 → 404 release_not_found

GET /api/v1/releases/{site_slug}?channel=&limit= — 公开

channel 默认 stablelimit 默认 20、≤100。响应 { channel, items: [...] },按 semver 降序。

GET /api/v1/releases/{site_slug}/{channel}/{version}/download — 公开

302 → CDN 链接(站点开 cdn 权限且启用 CDN 鉴权时带签名),download_count +1。已下架 → 404 release_not_found

---

pay. — 支付

权限: payments(开放接口)

头: X-Api-Key(开放接口)

POST /api/v1/payments/open/checkout/sessions

请求:

{
  "mode": "payment",
  "success_url": "https://站/success?session_id={CHECKOUT_SESSION_ID}",
  "cancel_url": "https://站/pricing",
  "user_id": 123,
  "external_customer_id": "站点自管客户标识(与 user_id 二选一)",
  "client_reference_id": "local-order-1",
  "customer_email": "a@b.com",
  "metadata": { "plan_type": "monthly" },
  "line_items": [
    {
      "quantity": 1,
      "price_data": {
        "currency": "cny",
        "unit_amount": 4900,
        "product_data": {
          "name": "商品名",
          "description": "描述",
          "unit_label": "/ 月",
          "features": ["特性"],
          "validity": "",
          "activate_note": "",
          "renewal_note": ""
        }
      }
    }
  ]
}

约束:

mode 目前仅 "payment"

line_items 恰好 1 条unit_amount > 0,单位

• 付款方双轨:user_id(Rekit 登录用户)与 external_customer_id(站点自管客户标识)二选一;未接 Rekit 登录用后者即可

success_url / cancel_url host 须在站点白名单

metadata 值为 string 字典

响应要点(Stripe 风格):

{
  "id": "cs_...",
  "object": "checkout.session",
  "url": "https://pay.../c/pay/cs_...",
  "status": "open",
  "payment_status": "unpaid",
  "mode": "payment",
  "amount_total": 4900,
  "currency": "cny",
  "success_url": "...",
  "cancel_url": "...",
  "client_reference_id": "...",
  "customer_email": "...",
  "metadata": {},
  "line_items": [],
  "expires_at": 1710000000,
  "created": 1710000000
}

浏览器跳转 url。履约前再查 Session。

GET /api/v1/payments/open/checkout/sessions/{id}

同站点 Key 可查。payment_status === "paid" 时履约。

POST /api/v1/payments/open/checkout/sessions/{id}/expire

手动过期未支付 Session。

托管页(浏览器,非 JSON)

| 路径 | 说明 |

|------|------|

| GET /c/pay/{session_id} | 单页收银台:确认订单 / 选支付方式 / 内联二维码 / 就地切换(/wait 旧链接 302 回本页) |

Webhook —— 到账主动推送(order.paid

站点配 webhook_url(后台「站点接入」)后,到账时 Rekit POST 签名事件到该地址。头 X-Rekit-Event-Id / X-Rekit-Event-Type / X-Rekit-Timestamp / X-Rekit-Signature: sha256=<hmac>;签名 = HMAC_SHA256(webhook_secret, "<timestamp>.<raw_body>") hex。Body dataorder_id / out_trade_no / provider / amount_cents / currency / user_id / external_customer_id / site_id / checkout_session_id / client_reference_id / metadata / paid_at。非 2xx 退避重试(~8 次),按 event_id/order_id 幂等发货。详见 SKILL §8.1(含验签示例)。轮询接口仍作兜底。

POST /api/v1/payments/open/orders(兼容直连)

{
  "provider": "wechat",
  "amount_cents": 100,
  "subject": "订单标题",
  "user_id": 123,
  "external_customer_id": "站点自管客户标识(与 user_id 二选一)"
}

providerwechat | alipay。付款方 user_idexternal_customer_id 二选一(都没传 → 422)。

传了 user_id 但用户不存在:404 user_not_registered(用 external_customer_id 则不涉及)。未绑定渠道:503

响应含 order_id / out_trade_no / code_url(微信 NATIVE)/ pay_url(支付宝)。

微信内联 JSAPI:加 "jsapi": true(provider=wechat)→ 响应含 prepay_params{appId,timeStamp,

nonceStr,package,signType,paySign}),产品站在自己页面 WeixinJSBridge.invoke('getBrandWCPayRequest',

prepay_params, cb) 拉起。付款人 openid 只能来自 Rekit 微信登录(平台按 user_idUser.mp_openid

解析,见 §3 silent=1;该用户须已通过 Rekit 微信登录)。开放下单 body 没有 payer_openid 字段;用 external_customer_id 的外部客户拿不到公众号 openid,

JSAPI 不可用,请走 NATIVE 扫码 / 支付宝 / 托管收银台。错误:409 payer_mp_openid_missing

409 jsapi_appid_mismatch(支付 appid≠公众号 appid)→ 均应回退扫码。

GET /api/v1/payments/open/orders/{order_id}(站点 Key,轮询兜底)

site_id 隔离取单 → 返回 statuspending|paid|...)等;仅 paid 履约。webhook order.paid

主推之外的兜底腿。订单不存在或跨站:404 order_not_found

POST /api/v1/payments/open/orders/{order_id}/cancel(站点 Key)

取消 pending 订单(与下单/查单同一条开放路)。按 site_id 隔离,防跨站。订单不存在或跨站:404 order_not_found

> 下单 / 查单 / 取消对外只有 /api/v1/payments/open/* 一套(站点 Key)。无 Bearer 版订单接口。

支付结果异步通知路径(平台内部验签,产品站一般不对接):

/api/v1/payments/notify/{provider}/{config_id}

---

权限目录

| id | 标签 | 服务 |

|----|------|------|

| storage | 图床上传 | api |

| cdn | CDN 鉴权(影响返回的 url 是否签名) | api |

| wechat_notify | 公众号查关注 / 模板消息 | api |

| email_notify | 邮件通知 | api |

| payments | 支付 | pay |

| captcha | 人机验证 | api |

| releases | 软件版本管理 | api |

---

产品站 Cookie / Callback 约定(参考)

| Cookie | 用途 |

|--------|------|

| rekit_user_token | 产品站 HttpOnly 存 access_token(参考站约定名) |

| rekit_auth_return | 可选,暂存登录后相对路径 |

说明:login. 服务自身也可能 Set-Cookie 名为 user_token;产品站应以 callback 写入本站 Cookie 为准,不要依赖跨域读 login. 的 Cookie。

Callback query:login_code(一次性消费、90s 有效)、returnTo(相对路径)。站点服务端用 login_codePOST /api/v1/auth/exchange 换取 access_token(旧 ?access_token= 直回协议已废止)。

参考代码:rekitdev/site.go/api/auth/callback/api/user/profile/api/checkout/session handler。