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_code → POST /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_missing、permission_denied、site_provider_unbound、validation_error),可用于多语言映射;message 为兜底人读文案。接入方只需解析这一种形状。
权限 ID:storage、cdn、wechat_notify、email_notify、payments、captcha。
站点自检(对接第一步): GET {REKIT_API_URL}/api/v1/site,只需 X-Api-Key,无权限门槛。返回各能力的 permission(权限位)与 ready(权限位 且 后台已绑配置,才是真正可调用);ready: false 时 missing 精确说明缺什么——可直接转给运营当工单,不必逐个功能试出 503。存储另带 read_mode(public = 存 unsigned_url 永久链;signed = 带 CDN 鉴权、仅即传即用)与 cdn_auth_enabled(后台原始开关);支付另带 providers(可用渠道,与下单同一判定)与 webhook_configured(false = 到账不推送;后台配了 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 + openid → sns/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/info 的 subscribe==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_url 做 HTTP 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" }
subdir 为 avatars / 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_items;unit_amount 单位为 分
• 响应含 id、url、status、payment_status、amount_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" }
provider:wechat | alipay。付款方 user_id 与 external_customer_id 二选一(都没传 → 422)。
站点须绑定对应支付配置。响应含 order_id / out_trade_no /
code_url(微信 NATIVE 扫码)/ pay_url(支付宝)。
查订单状态(站点 Key,webhook 之外的轮询兜底):
GET /api/v1/payments/open/orders/{order_id}
返回 status(pending | paid | ...)等;仅当 paid 时履约。
8.2 微信内联 JSAPI(产品站自己页面直接拉起付款,不跳转、不扫码)
微信内(MicroMessenger)希望在产品站自己的页面点一下直接弹微信付款,用这个。前提:站点
微信支付配置的 appid 必须等于平台公众号 appid(同一服务号),运营在后台绑定即可。
JSAPI 需 Rekit 微信身份:付款人 openid 由平台按 user_id 从 User.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.paid(ping 为后台测试事件) |
| 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_id 或 order_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/exchange 换 access_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 默认 stable;sha256 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_token、expires_in、user |
| 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_code → POST /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_url 须 http:// 或 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 | public:unsigned_url 是永久链;signed:url 带 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/info 的 subscribe == 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_url,Content-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_url,Content-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 默认 stable;limit 默认 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 data:order_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 二选一)"
}
provider:wechat | alipay。付款方 user_id 与 external_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_id 从 User.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 隔离取单 → 返回 status(pending|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_code 调 POST /api/v1/auth/exchange 换取 access_token(旧 ?access_token= 直回协议已废止)。
参考代码:rekitdev/site.go 的 /api/auth/callback、/api/user/profile、/api/checkout/session handler。