LINGTU IDENTITY / DOCUMENTATION
接入登录、算力与支付
先完成应用登记与统一登录,再按需接入公共算力账务和统一支付。
GETTING STARTED
灵途造物账号为旗下产品提供统一身份与授权登录。用户使用一个账号登录,你的产品通过 OpenID Connect(OIDC)验证身份,并建立自己的登录会话。
账号中心负责统一身份认证与公共算力账务。课程、会员、推广关系、企业角色与业务权限,由接入产品自行管理。
| 接入约定 | 当前支持 |
|---|---|
| 应用类型 | 管理员登记的自有可信 Web 产品(具有后端) |
| 授权流程 | Authorization Code + PKCE S256 |
| 客户端认证 | client_secret_basic(后端 HTTP Basic) |
| 授权范围 | openid profile |
| 身份签名 | RS256,通过 JWKS 验证 |
| 外部身份唯一键 | (issuer, sub) |
浏览器只负责跳转和持有产品的会话 Cookie。client_secret、换码、令牌和验签均留在接入产品后端。当前不支持纯前端 SPA 直接换码、动态应用注册、密码授权或 Refresh Token。
01 / REGISTER
请联系账号中心管理员,提供应用名称和完整回调地址。管理员在「接入应用」中登记后,向你交付 client_id 与 client_secret;密钥仅保存在产品后端的环境变量或密钥管理服务中。
回调地址逐字精确匹配,不支持通配符。生产环境必须使用 HTTPS;HTTP 回调仅允许在非生产环境使用 localhost 或 127.0.0.1。开发与生产分别登记应用。
# 配置在接入产品后端,禁止使用 NEXT_PUBLIC_ 前缀
OIDC_ISSUER=https://login.lingtuzaowu.com/oauth
OIDC_CLIENT_ID=YOUR_CLIENT_ID
OIDC_CLIENT_SECRET=YOUR_CLIENT_SECRET
APP_ORIGIN=https://your-product.example
# 登记的回调:https://your-product.example/auth/callback
应用资料编辑、客户端停用和密钥轮换会使既有授权失效,产品应要求用户重新登录。
DISCOVERY
生产 Issuer 固定为 https://login.lingtuzaowu.com/oauth。在后端读取 Discovery,校验返回的 issuer 与配置完全相同,并校验端点来源,再使用其中的端点。
| 用途 | 方法 | 完整路径(域名均为 login.lingtuzaowu.com) |
|---|---|---|
| Discovery | GET | /oauth/.well-known/openid-configuration |
| 发起授权 | GET | /oauth/authorize |
| 兑换令牌 | POST | /oauth/token |
| 用户资料 | GET | /oauth/userinfo |
| 验签公钥 | GET | /oauth/jwks |
| 撤销令牌 | POST | /oauth/revoke |
| 账号状态 | POST | /api/client/account-status |
注意账号状态接口位于站点根路径 /api/client/account-status,不在 /oauth 下。
03 / CALLBACK
授权成功后,浏览器返回登记的回调地址,带上 code 与 state;失败时可能返回 error。回调必须由产品后端处理。
步骤 1 · 检查浏览器绑定和事务有效期,要求 state 唯一且匹配。原子消费事务,防止重复回调,即使后续换码失败也不可复用。
步骤 2 · 处理 error,要求 code 唯一且存在,然后在后端兑换令牌。授权码有效期仅 60 秒,只能使用一次。
# 后端请求示意;变量来自服务端配置和当前登录事务
curl --request POST 'https://login.lingtuzaowu.com/oauth/token' \
--user "$OIDC_CLIENT_ID:$OIDC_CLIENT_SECRET" \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode "code=$CODE" \
--data-urlencode 'redirect_uri=https://your-product.example/auth/callback' \
--data-urlencode "code_verifier=$CODE_VERIFIER"
client_secret_basic 的用户名、密码须按 OAuth 规则分别做表单编码后,再拼接为 client_id:client_secret 并 Base64 编码。当前颁发的客户端凭证使用 URL 安全字符;完整实现可参考下方 Node.js 示例。
步骤 3 · 使用 Discovery 的 jwks_uri 和成熟 JWT 库验证 id_token,限制算法为 RS256,校验固定 issuer、audience、sub、iat、exp、nonce 与整数 account_version;若存在 azp,必须为当前 client_id。仅解码 JWT 不等于验签。
步骤 4 · 使用 access_token 请求 userinfo,要求返回的 sub 和 account_version 与 ID Token 一致。
curl 'https://login.lingtuzaowu.com/oauth/userinfo' \
--header "Authorization: Bearer $ACCESS_TOKEN"
步骤 5 · 按 (issuer, sub) 查找或创建产品本地用户,将 account_version 绑定本地会话,并在允许受保护访问前校验账号状态。登录成功后更换会话 ID,浏览器只接收产品的 HttpOnly 会话 Cookie。
不要按手机号、昵称自动关联已有用户。不要把令牌写入 localStorage、浏览器 Cookie 或 URL,也不要在日志中输出授权码、令牌或密钥。
CLAIMS & TOKENS
请求 openid profile 后,可获得以下身份资料。nickname 与 picture 可能缺省,产品应提供默认显示。
{
"sub": "USER_SUBJECT",
"nickname": "灵途用户",
"picture": "https://example.com/avatar.png",
"account_version": 1
}
| 字段 | 含义 |
|---|---|
| sub | 账号主体标识,与 issuer 组合为唯一外部身份 |
| nickname | 显示昵称,不用作唯一键 |
| picture | 头像 URL,可选 |
| account_version | 账号安全版本,绑定产品会话并持续比对 |
| 凭证 | 有效期 | 用途 |
|---|---|---|
| 授权码 | 60 秒 | 一次性兑换令牌 |
| ID Token | 5 分钟 | 验证本次登录身份 |
| Access Token | 10 分钟 | 仅用于 userinfo |
资料不包含手机号、微信标识或产品权限。Access Token 不授予课程、会员或其他业务 API 的权限;产品会话寿命由产品管理,但必须执行下面的状态校验。
04 / SESSION
在产品的受保护访问中,由后端使用本应用的 Basic 凭证检查已获授权用户的状态。该接口只允许查询本应用已经获得授权的账号。
curl --request POST 'https://login.lingtuzaowu.com/api/client/account-status' \\
--user "$OIDC_CLIENT_ID:$OIDC_CLIENT_SECRET" \\
--header 'Content-Type: application/json' \\
--data '{"sub":"USER_SUBJECT"}'
{ "active": true, "account_version": 1 }
| 结果 | 产品应如何处理 |
|---|---|
| active=true,版本与会话一致 | 允许继续访问 |
| active=false 或版本不一致 | 清除本地会话,要求重新登录 |
| 401 / 403(客户端凭证失效或被停用) | 清除本地会话,检查应用配置 |
| 超时、中心不可用或其他失败 | 无有效缓存时返回 503,禁止沿用过期状态 |
可缓存成功状态,缓存最多 60 秒,从请求发起时计时。按应用与 sub 隔离缓存;流式连接也须在 60 秒内重新检查,并终止已撤销的连接。
产品普通退出只清除自己的会话,退出操作需防 CSRF。账号中心退出当前登录只清除中心会话;退出全部设备、禁用账号、密码修改和恢复会提升安全版本,使旧产品登录失效。不支持 OIDC RP-Initiated Logout;不要自行拼接 end_session_endpoint。
BUILD & VERIFY
项目提供独立的 Node.js 参考客户端 examples/oidc-client/server.mjs,包含 PKCE、一次性事务、后端换码、JWKS 验签、用户资料比对、状态查询与产品退出。
使用 Node.js 22 或更新版本。在非生产账号中心登记本地示例应用,回调地址为 http://localhost:4401/auth/callback,再运行:
cd examples/oidc-client
npm install
# 使用单独的开发账号中心,替换为实际开发 issuer
export OIDC_ISSUER=http://localhost:33887/oauth
export OIDC_CLIENT_ID=YOUR_DEV_CLIENT_ID
export OIDC_CLIENT_SECRET=YOUR_DEV_CLIENT_SECRET
export APP_ORIGIN=http://localhost:4401
export PORT=4401
npm start
打开 http://localhost:4401,点击登录。示例每次受保护访问均查询中心状态;用户、事务和会话保存在进程内存,重启即丢失,正式产品需改为持久化存储。示例需从项目源码运行,本页代码片段用于说明请求协议。
| 问题 | 检查方式 |
|---|---|
| 回调地址不匹配 | 比对登记地址、授权请求和换码请求,包括协议、端口、路径与尾部斜杠 |
| invalid_client / 401 | 检查应用是否启用、Basic 编码和密钥是否已轮换 |
| invalid_grant | 授权码是否超时、重复使用,verifier 是否属于原登录事务 |
| state / nonce 校验失败 | Cookie 是否回传、事务是否过期,是否错误复用其他浏览器的事务 |
| 验签失败 | 固定 issuer、audience、RS256、JWKS 和服务器时钟是否正确 |
| 状态失效 | 清除会话并重新授权;确认账号和应用状态 |
上线前验证完整登录、重放回调、错误 state、密钥轮换、退出全部设备和中心不可用的处理。各产品的本地用户、业务数据与权限仍由各自产品维护。
API KEY / INTEGRATION
API Key 用于 Agent、插件或命令行访问子系统 API。用户统一在账号中心创建、选择授权应用和撤销;子系统负责校验身份及执行自己的业务权限检查。
1. 子系统开通接入
管理员登记应用,子系统后端完成校验接口接入后,在「接入应用 → 编辑应用」开启「允许 API Key 接入」(默认关闭)。沿用应用的 client_id / client_secret,仅保存在子系统后端。
2. 用户统一创建密钥
子系统可提供「管理 API Key」入口,跳转到账号中心 /account/api-keys。用户登录账号中心后填写名称、选择有效期并勾选允许访问的应用;默认 90 天,也可选择 30 天、365 天或长期。默认不授权任何应用,新应用不会自动加入授权。
明文仅创建时展示一次,由用户保存到工具的安全凭证配置。创建、编辑授权与撤销接口只接受账号中心登录会话;子系统不能用应用 Basic 凭证或用户 API Key 代用户创建密钥,也不应把中心管理接口当作自己的跨站创建接口。
3. 从创建到子系统使用:完整链路
假设子系统地址是 https://product.example,要开放自己的 GET /api/me。账号中心不会自动替子系统开放这个接口,也不会主动把用户密钥推送给子系统。
管理员:登记子系统应用 → 子系统实现校验 → 开启该应用 API Key 接入
用户:在子系统正常登录一次,建立本地用户与中央 issuer/sub 的关联
用户:到账号中心创建 API Key,勾选该子系统 → 将密钥保存到工具
工具:携带 Bearer API Key → 请求子系统 GET /api/me
子系统后端:携带自身 Basic 凭证 + 用户 API Key → 请求中心 verify
中心:核验密钥、有效期、账号状态及是否授权当前子系统 → 返回身份
子系统后端:按 issuer/sub 查本地用户 → 检查业务权限 → 返回业务结果
同一密钥可以授权多个子系统,各子系统使用自己的应用凭证校验。例如密钥只授权 A,B 即使拿到该密钥,用 B 的凭证校验也会得到 active=false。子系统无需同步或保存用户密钥库;只处理本次请求的密钥,保存本地身份关联和必要的 keyId 审计信息。
子系统开发者需要实现什么
- 在子系统设置页增加「管理 API Key」链接,指向
https://login.lingtuzaowu.com/account/api-keys;当前由用户自行返回子系统或工具完成配置,没有自动回传明文密钥的跳转协议。 - 为需要开放的业务 API 接入 API Key 认证中间件。现有浏览器 Cookie 登录不会自动使这些接口支持 API Key。
- 将中心返回的
(issuer, sub)映射到 OIDC 登录时已建立的本地用户,复用自己的组织与业务权限检查;不要把 keyId 当用户 ID。 - 为每个开放接口定义允许的操作及输入,执行后在本地记录业务审计。涉及算力扣费时,继续使用既有算力接口与用户扣费授权。
用户 API Key 不放入子系统环境变量。子系统后端配置的是自己的应用凭证:
IDENTITY_ORIGIN=https://login.lingtuzaowu.com
OIDC_ISSUER=https://login.lingtuzaowu.com/oauth
OIDC_CLIENT_ID=当前子系统登记的应用ID
OIDC_CLIENT_SECRET=当前子系统自己的后端密钥
子系统后端接入示例
Node.js 22+ 可将 packages/api-key-sdk 作为独立包安装或发布到内部 npm,包名为 @lingtu/api-key-sdk;当前不假定它已经发布到公共 npm。以下为基于标准 Request/Response 的处理函数,接到子系统自己的 GET /api/me 路由即可。findLocalUser 是子系统需要提供的本地数据库适配函数,不是账号中心接口。
import { createApiKeyVerifier } from '@lingtu/api-key-sdk';
// 每个服务实例初始化一次;每次业务请求都调用 verify。
const verify = createApiKeyVerifier({
identityOrigin: process.env.IDENTITY_ORIGIN,
issuer: process.env.OIDC_ISSUER,
clientId: process.env.OIDC_CLIENT_ID,
clientSecret: process.env.OIDC_CLIENT_SECRET,
});
export async function getMe(request, { findLocalUser }) {
const json = (status, body) => Response.json(body, {
status, headers: { 'Cache-Control': 'no-store' },
});
const match = /^Bearer (ltak_[A-Za-z0-9_-]{43})$/i.exec(
request.headers.get('authorization') || '',
);
if (!match) return json(401, { message: '请提供用户 API Key' });
let identity;
try {
identity = await verify(match[1]);
} catch {
// 不记录或返回密钥、请求正文及底层异常。
return json(503, { message: '身份校验暂时不可用' });
}
if (!identity) return json(401, { message: '密钥无效或未授权本应用' });
const user = await findLocalUser(identity.issuer, identity.sub);
if (!user || user.disabled) {
return json(403, { message: '请先登录本产品关联账号,或联系管理员' });
}
// /api/me 只读取本人资料;组织/写入接口在此继续检查角色与数据归属。
return json(200, { userId: user.id, nickname: user.nickname });
}
如果子系统使用其他语言,无需 SDK,也可以按下文协议直接调用中心 verify。中心校验只判断身份和当前应用访问资格,不执行子系统的业务请求。
用户在工具中怎么调用
将创建结果保存到工具的安全凭证配置,并把 API 地址设置为子系统的业务地址。下面的 AGENT_API_KEY 由安全环境提供,避免将明文粘贴到命令历史:
curl 'https://product.example/api/me' \
--header "Authorization: Bearer $AGENT_API_KEY"
用户无需提供子系统的 client_secret,也无需先拿 API Key 换 OIDC Token。子系统收到请求后按上述流程校验;密钥管理页没有“填写子系统服务器密钥”的步骤。
接入完成后如何验收
- 用户已关联本地账号,密钥授权当前子系统:
/api/me返回本地用户资料。 - 密钥没勾选当前子系统、过期或撤销:业务接口返回 401,不执行业务。
- 中心校验成功,但本地用户未关联、被禁用或缺少组织权限:返回 403。
- 中心校验超时或不可用:返回 503,不沿用旧身份结果。
- 同一密钥授权 A、B 后分别调用:身份 issuer/sub 相同,业务权限分别由 A、B 判断。
可运行的双应用版本见 examples/api-key-client;其中本地用户与组织是测试夹具,接入真实子系统时需替换为自己的数据库查询。
4. 校验协议与错误处理
工具请求子系统 API 时携带 Authorization: Bearer <API Key>。子系统每次业务请求都在后端调用账号中心 POST /api/client/api-keys/verify:以本应用凭证做 HTTP Basic 认证,请求 JSON 为 { "apiKey": "ltak_..." }。
校验成功返回 active、issuer、sub、keyId、expiresAt;以 (issuer, sub) 查找产品本地用户,再检查组织成员、角色和数据归属。未关联本地用户时,引导用户完成产品正常注册/登录流程;不能直接相信请求自报的用户或角色。
active=true:继续本地权限检查,通过后才执行操作。active=false:返回 401,不执行操作。- 校验超时或不可用:返回 503,不使用旧结果继续执行。
不缓存校验结果,不记录密钥、Authorization 或请求正文。Node.js 22+ 可使用独立 SDK packages/api-key-sdk;双应用示例位于 examples/api-key-client。
5. 使用与撤销边界
API Key、OIDC 登录和算力扣费授权相互独立。密钥校验不创建产品账号、登录授权、扣费授权或会员权益;收费操作仍须核验已有扣费授权及额度。修改密码、退出全部设备不会撤销 API Key,用户需在账号中心单独撤销。
完整接口、SDK 示例、错误处理与生命周期见 API Key 接入契约。
CREDITS / SETUP
公共算力接入按应用单独开通。只使用统一登录的产品无需配置;登录成功并不代表用户允许扣费。此处管理余额与账务,模型 API、任务执行和业务定价由产品自行实现。
- 管理员在「接入应用」登记登录应用,并完成 OIDC 对接。
- 在该应用行点击「算力接入」,选择所需权限并勾选「允许新资金操作」。保存后复用该应用现有的 client_id / client_secret,无需新增密钥。
- 产品后端沿用 OIDC_CLIENT_ID 与 OIDC_CLIENT_SECRET,以 HTTP Basic 调用算力接口。密钥禁止使用 NEXT_PUBLIC_ 前缀;ID Token、Access Token 与浏览器 Cookie 不能代替应用认证。
- 按业务选择直接消费或划拨,接入下方接口并联调用户授权、余额不足与超时重试。
| 配置项 | 说明 |
|---|---|
| 查询 query | 查询用户余额和幂等操作结果;余额查询还要求用户已开通该产品扣费授权 |
| 消费 consume | 直接扣费、冻结、结算、取消及续期 |
| 划拨 transfer | 创建划拨意向、查单、补单与确认产品到账 |
| 到账通知地址(可选) | 接收划拨通知;不填时主动轮询,填写后仍需定时补单 |
| 产品入口(可选) | 用户从算力账户访问产品的入口 |
通知地址和产品入口须与该应用已登记的 OIDC 回调同源,生产环境使用 HTTPS,不跟随通知重定向。算力权限配置在应用的「算力接入」中完成,应用密钥统一在「编辑」中轮换;「算力账务」用于交易查询、对账和到账异常处理。
# 仅配置在接入产品后端,沿用现有登录配置
OIDC_CLIENT_ID=YOUR_CLIENT_ID
OIDC_CLIENT_SECRET=YOUR_CLIENT_SECRET
# 算力接口使用账号域名根地址,不带 /oauth
CREDITS_ORIGIN=https://login.lingtuzaowu.com
开发与生产分别开通,开发环境使用独立开发账号中心。应用密钥遗失可在「编辑」中轮换。轮换同时影响登录、算力接口和通知验签,旧密钥立即失效;产品同步更新 OIDC_CLIENT_SECRET,既有产品授权失效后须重新登录。关闭新资金操作不会删除原单据;处理未完成交易时应保留对应权限并使用当前有效应用密钥。
CREDITS / REQUIRED FLOW
产品页面必须先确认共享算力已开通,再查询余额与账单。不要用余额/流水接口的 403 来探测是否开通,也不要在页面挂载时无条件并发调用这两个接口。
“开通”分两层:管理员先为当前环境的应用配置启用的算力接入(灵途灵感使用 query、consume,不使用 transfer);用户再在账号中心选择不限额或每日额度并授权。统一登录成功不包含扣费授权。
页面进入时的调用顺序
- 产品后端校验本地 OIDC 会话及账号状态,从会话读取 sub。
- 后端用应用 Basic 调用
POST /api/credits/v1/product/grant-status,请求{sub}。该接口需要 consume 权限和登录关联,但不要求用户已授权,也不返回余额。 - 若返回
{authorized:false,dailyLimit:null},页面进入“未开通”状态,仅展示「开通共享算力」及说明。此时不请求 balance、transactions 或 debit,不显示伪造的 0 余额,也不将它渲染为通用加载失败。 - 用户点击开通后,产品后端创建并保存随机 state(绑定当前会话、sub、10 分钟有效),调用
POST /api/credits/v1/product/grant-requests,发送{sub,returnUri,state},再跳转到固定账号中心 origin + 返回的 confirmationPath。returnUri 必须是产品固定的回调路由,且与登记的 OIDC 回调同源。 - 用户在账号中心选择不限额或填写每日额度并明确确认;产品不能预填默认授权额度、静默授权或替用户确认。用户取消时保留“未开通”页面,不自动再次跳转。
- 返回产品后,后端校验并原子消费 state、检查有效期、当前会话与 sub,重新核验账号状态,再调用 grant-status。grant_result=authorized 只是回调提示,不能直接作为已授权证明。
- 只有中央明确返回
authorized:true,才加载余额与账单;此时两项查询可并发。进入页面、重新聚焦页面或授权返回后应重新确认状态,不能把已开通永久保存在浏览器里。 - 已开通后若查询/扣费返回
SPEND_NOT_AUTHORIZED(用户撤销或授权变化),清除展示中的旧余额/账单,回到“未开通”状态。不要自动重试扣费或静默恢复授权。资金操作仍沿用原幂等键及结果恢复规则。
校验登录 → grant-status
authorized=false → 显示开通按钮 → 账号中心明确授权
→ 产品回调校验 state → 再次 grant-status
authorized=true → 查询 balance + transactions → 展示余额/账单
区分应用配置与个人授权
| 中央结果 | 含义 | 产品应做什么 |
|---|---|---|
| grant-status 200,authorized=false | 当前用户尚未开通或已撤销 | 显示开通入口,停止余额和账单请求 |
| PRODUCT_UNAVAILABLE / PRODUCT_DISABLED / SCOPE_REQUIRED | 当前应用未开通、已停用或缺少权限 | 显示应用暂不可用/联系管理员;不能让用户在授权页里循环重试 |
| INVALID_CREDENTIAL | 应用凭证错误或失效 | 后端修复凭证;不能靠让用户反复登录解决 |
| USER_NOT_BOUND | 用户与当前应用没有有效登录关联 | 从产品重新发起 OIDC 登录后再检查 |
| SPEND_NOT_AUTHORIZED | 资金/查询接口发现用户授权已撤销或缺失 | 回到未开通状态;不伪造余额、不绕过授权 |
| 超时或 503 | 中央暂不可用,授权状态未知 | 展示可重试状态;不能当成已授权或未经确认的新扣费 |
给产品实现 AI 的改动清单
- 产品后端提供本产品的授权状态查询、开通发起及固定授权回调路由;中央真实接口为上面的 grant-status、grant-requests,不要臆造中央
/enable或/activate接口。 - 前端明确区分 checking(检查中)、needs-authorization(未开通)、ready(可查询)及 unavailable(不可用);只有 ready 才启用余额和账单查询,包括数据请求库的 enabled 条件。
- 浏览器只请求本产品后端,不持有 client_secret,也不直接携带用户 Cookie 调用中央用户授权写接口。用户每天的扣费额度只在账号中心确认。
- 保留授权取消、回调重放/过期、撤销授权、应用停用、密钥轮换及中央暂不可用的处理。不能为了消除控制台 403 放宽中央权限检查。
- 本地联调的浏览器授权、discovery、换码、JWKS、userinfo、账号状态及算力请求必须落在同一个开发账号中心。Whistle 只改变浏览器路由;产品后端需配置自身的开发通道。开发凭证用
.env.development.local,不得使用生产 client_id 去本地账号中心查询。
验收场景
- 新用户未开通:只发生状态检查,余额/账单请求次数均为 0,显示开通按钮。
- 用户取消开通:仍不查询余额和账单,不自动再次跳转。
- 授权成功:回调校验 state 并重新确认 authorized=true 后,两项查询正常返回。
- 用户撤销:下一次状态检查或 SPEND_NOT_AUTHORIZED 使页面返回未开通,并清除旧资金展示。
- 应用未配置/权限不足/凭证错误:显示管理员可定位的错误,不能伪装成用户未授权或无限跳转。
- state 错误、重复、过期或账号切换:拒绝回调,不查询资金数据;仅有 grant_result=authorized 也不能通过。
CREDITS / CONSENT
统一登录不会自动开通扣费。产品提供「开通共享算力」按钮,用户在灵途造物账号确认应用、账号和每日额度后返回产品。开通本身不扣费,各应用共享同一份公共余额。
- 产品后端从有效 OIDC 会话取得 sub,生成随机 state 并在服务端绑定当前会话和 sub,10 分钟过期,回调时原子消费。
- 使用应用 Basic 调用
POST /api/credits/v1/product/grant-requests,发送{sub,returnUri,state}。需要 consume 权限。returnUri 为产品固定的授权完成地址,须与已登记 OIDC 回调同源,生产使用 HTTPS。 - 响应
{confirmationPath,expiresAt},浏览器跳转固定账号中心 origin + confirmationPath。10 分钟有效,不记录完整票据地址。 - 用户在
/account/credits/authorize设置每日额度并明确确认;未登录时保留请求后登录,账号不符或请求过期时返回应用重新开通。取消不改变已有授权。 - 产品回调验证服务端 state、有效期、当前会话与 sub 后,消费 state。返回参数为
state和grant_result=authorized|cancelled,参数本身不证明授权。 - 后端重新核验账号状态,再以 Basic 调用
POST /api/credits/v1/product/grant-status,发送{sub};响应{userId,authorized,dailyLimit}。只有 authorized=true 才显示已开通。未授权 dailyLimit=null,已授权为最小单位字符串或 null(不限额),必须以 authorized 判断授权状态。
状态查询需要 consume,无需 query,不暴露未授权用户的余额。实际消费仍逐次检查中央授权。用户可在账号中心「算力流水」调整或撤销授权;撤销停止新扣费,已有冻结仍可按原金额结算。页面每日额度按北京时间计算,冻结也占用额度。
应用密钥轮换、应用停用、权限/回调来源变化、登录关联撤销或账号版本变化都会使旧请求失效。错误包括 INVALID_RETURN_URI、INVALID_GRANT_REQUEST、GRANT_REQUEST_EXPIRED 和 ACCOUNT_MISMATCH。独立产品后端示例见仓库 examples/credits-client。
CREDITS / CONSUME
1 元 = 100 算力,1 算力 = 10 个最小单位。接口金额使用整数十进制字符串,例如 "1000" 表示 100 算力(1 元),不接受小数、负数、前导零或科学计数法。单笔最大 9,000,000,000,000,000 最小单位。
所有产品接口位于账号域名的 /api/credits/v1/product 下,复用 OIDC 的 client_secret_basic,使用 Authorization: Basic BASE64(FORM_ENCODE(client_id) + ":" + FORM_ENCODE(client_secret))。sub 必须取自已验签且账号状态有效的产品后端会话,不能相信浏览器提交的付款人。
直接消费:用户先登录该产品,通过「开通共享算力」跳转到账号中心确认授权并设置每日额度,也可在「算力流水」管理授权。确定价格可直接扣费;任务成本不确定时先冻结,执行后结算实际金额,差额释放。中央统一管理余额,产品无需维护同一份余额副本。
| 方法与相对路径 | 权限 | 请求或用途 |
|---|---|---|
| POST /balance | query | {sub},查询已授权用户余额 |
| POST /debits | consume | {sub,amount},直接扣费 |
| POST /holds | consume | {sub,amount},冻结 30 分钟 |
| GET /holds/:id | consume | 查询当前冻结状态 |
| POST /holds/:id/extend | consume | {amount},追加冻结 |
| POST /holds/:id/renew | consume | {},续 30 分钟,最迟到创建后 24 小时 |
| POST /holds/:id/capture | consume | {amount},结算不超过冻结总额,可为 "0" |
| POST /holds/:id/cancel | consume | {},释放全部冻结 |
| GET /operations?operation=debit&key=... | query | 按操作与幂等键查询历史成功结果 |
所有金额写操作须提供 Idempotency-Key。同一业务操作始终复用同一键与参数;网络超时后按原键查询或重试,不能换键再次扣费。同键不同参数返回 IDEMPOTENCY_CONFLICT。键为 1–128 位 ASCII 字母、数字及 _ : . / -,区分大小写。
// 仅在产品后端执行;sub 来自有效会话,businessOrderId 持久保存
const formEncode = (value) => new URLSearchParams({ v: value }).toString().slice(2);
const basic = Buffer.from(`${formEncode(process.env.OIDC_CLIENT_ID)}:${formEncode(process.env.OIDC_CLIENT_SECRET)}`).toString('base64');
const response = await fetch(`${process.env.CREDITS_ORIGIN}/api/credits/v1/product/debits`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${basic}`,
'Idempotency-Key': `debit:${businessOrderId}`,
},
body: JSON.stringify({ sub: authenticatedSession.sub, amount: '1000' }),
});
const result = await response.json();
if (!response.ok) throw new Error(result.message);
每日额度按 Asia/Shanghai 计算,消费与冻结占用额度;差额、取消与过期释放原冻结日额度。产品必须在冻结有效期内执行任务,提前续期并在 24 小时内结束。过期冻结不能结算。撤销扣费授权或停用账号、产品后不能新增支出,已有冻结仍可按保留的权限结算或取消。
常见错误包括 SPEND_NOT_AUTHORIZED(未授权)、DAILY_LIMIT(每日额度不足)、INSUFFICIENT_BALANCE(余额不足)、USER_NOT_BOUND(未登录关联)、INVALID_CREDENTIAL(应用密钥失效)、SCOPE_REQUIRED(缺少权限)和 HOLD_EXPIRED(冻结到期)。错误返回 {code,message};503 或网络超时属于结果可能不确定的情况,保留原幂等键恢复。
CREDITS / TRANSFER
划拨到产品:适合产品自有个人或组织钱包。产品后端先核验当地接收权限,创建划拨意向,再将用户跳转到账号中心返回的 confirmationPath。用户逐笔确认后中央扣款,产品查单核实后在本地入账。这一路径使用逐笔确认,不以直接扣费授权代替确认。
| 方法与相对路径(前缀 /api/credits/v1/product) | 输入或用途 |
|---|---|
| POST /transfers | {sub,amount,recipientType,recipientId,recipientName},须 transfer 权限与幂等键 |
| GET /transfers/:id | 查询本产品划拨单,只有 state=committed 才能入账 |
| GET /transfers/pending | 待到账单,30 条一页,cursor 翻页 |
| POST /transfers/:id/ack | 本地事务提交后确认到账,重复调用安全,无需额外幂等键 |
recipientType 为 person 或 organization。创建后付款人、金额及接收方不可更改。产品只向浏览器提供账号中心跳转地址,不向浏览器暴露应用密钥。
到账通知正文为 {eventId,type:"credits.transfer.committed",transferId},请求头包含 x-credits-timestamp(Unix 秒)和 x-credits-signature。产品校验时间误差不超过 5 分钟,以恒定时间比较验签:签名为 HMAC-SHA256(hex(SHA256(secret)), timestamp + "." + rawBody) 的十六进制字符串;secret 为该应用当前的 client_secret。通知只提供线索,产品仍须携带应用 Basic 认证主动查单,核对产品、金额、接收对象与 committed 状态。
产品本地必须将唯一 inbox 记录与余额增加放在同一个数据库事务内,提交后再 ACK。不能先 ACK 再入账;入账成功但 ACK 丢失时,根据 inbox 去重并补发 ACK。HTTP 2xx 不代表已到账。即使配置通知,也须定时拉取 pending 补单。
中央已扣款、产品待到账时不会自动恢复余额。管理员在「算力账务 → 到账异常」核验并重试通知,不能直接标记到账。当前划拨是单向操作,不支持退回、提现、现金退款或用户间转赠。产品补贴、费用与组织关系由产品管理;中央不展示产品当地余额。
参考仓库中的 examples/credits-client,包含独立 MySQL inbox、接收权限检查、验签和补单。产品无需连接中央数据库。当前运行时未安装真实支付适配器,在线充值默认关闭;后台手工算力调整不代表真实支付。
PAYMENTS
产品后端复用应用凭证接入统一支付;各自有产品按配置商户收款,公共算力固定使用中央商户。会员价格、期限和权益仍由接入产品管理。
支持电脑扫码、微信内网页和微信外手机浏览器,会员采用逐次购买或续费。后台在「支付接入」配置主体、场景及应用权限,未验证渠道保持关闭。
商品支付支持在产品内确定金额,由产品后端下单,再跳转收银台直接付款。公共算力充值目前需在账号中心选择金额;产品后端直接创建算力充值单的接口尚未提供。
完整接口、通知验签和商户配置见 统一支付接入文档。Node.js 22+ 接入可直接使用以下公开文件:
可将上述文档链接提供给有联网能力的 AI 读取,或下载 SDK 安装到产品后端。