跳转至文档正文
灵途造物开发文档
开发文档 / 产品接入

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;线上与本地账号服务均允许 localhost 或 127.0.0.1 的 HTTP 回调,其他 HTTP 主机不允许。开发与生产分别登记应用。

# 配置在接入产品后端,禁止使用 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 下。

02 / AUTHORIZE

发起授权

用户点击“使用灵途造物账号登录”后,先进入产品自己的后端登录路由,再由后端创建登录事务并跳转至授权端点。

  1. 用安全随机数生成 state、nonce 和 code_verifier,每次登录重新生成。
  2. 计算 code_challenge = BASE64URL(SHA256(code_verifier)),不带填充字符。
  3. 在后端保存事务,包含 state、nonce、verifier、浏览器绑定与站内 returnTo,十分钟过期。用 HttpOnly、SameSite=Lax 的 Cookie 绑定浏览器,生产环境加 Secure。
  4. 跳转至以下地址,所有参数通过 URLSearchParams 编码。
https://login.lingtuzaowu.com/oauth/authorize
  ?client_id=YOUR_CLIENT_ID
  &redirect_uri=https%3A%2F%2Fyour-product.example%2Fauth%2Fcallback
  &response_type=code
  &scope=openid%20profile
  &state=RANDOM_STATE
  &nonce=RANDOM_NONCE
  &code_challenge=BASE64URL_SHA256_VERIFIER
  &code_challenge_method=S256

以上为分行展示,实际跳转使用单行 URL。returnTo 只允许产品站内路径,不要把任意外部地址作为登录后的跳转目标。用户已登录账号中心时,可以复用有效的中心登录。

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 可能缺省,产品应提供默认显示。phone_number 与兼容字段 phone 为当前绑定手机号,未绑定时省略。

{
  "sub": "USER_SUBJECT",
  "nickname": "灵途用户",
  "picture": "https://example.com/avatar.png",
  "phone_number": "13800000000",
  "phone": "13800000000",
  "account_version": 1
}
字段 含义
sub 账号主体标识,与 issuer 组合为唯一外部身份
nickname 显示昵称,不用作唯一键
picture 头像 URL,可选
phone_number 当前绑定手机号,未绑定时省略
phone phone_number 的兼容字段,值相同
account_version 账号安全版本,绑定产品会话并持续比对
凭证 有效期 用途
授权码 60 秒 一次性兑换令牌
ID Token 5 分钟 验证本次登录身份
Access Token 10 分钟 仅用于 userinfo

ID Token 与 userinfo 均可返回手机号;产品可读取 profile.phone_number ?? profile.phone ?? null。已有 ID Token 不会更新,重新请求 userinfo 或重新登录可同步最新资料。手机号暂未经过短信验证,不返回 phone_number_verified,不能据此认定号码已验证或自动合并账号。资料不包含微信标识或产品权限,公开访客接口不返回手机号。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 审计信息。

子系统开发者需要实现什么

  1. 在子系统设置页增加「管理 API Key」链接,指向 https://login.lingtuzaowu.com/account/api-keys;当前由用户自行返回子系统或工具完成配置,没有自动回传明文密钥的跳转协议。
  2. 为需要开放的业务 API 接入 API Key 认证中间件。现有浏览器 Cookie 登录不会自动使这些接口支持 API Key。
  3. 将中心返回的 (issuer, sub) 映射到 OIDC 登录时已建立的本地用户,复用自己的组织与业务权限检查;不要把 keyId 当用户 ID。
  4. 为每个开放接口定义允许的操作及输入,执行后在本地记录业务审计。涉及算力扣费时,继续使用既有算力接口与用户扣费授权。

用户 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 接入契约。

学堂接入与密钥来源授权

学堂技能接口已接入中央 API Key,旧 l2sk_ 及平台发布口令停用,不再要求专用 CLI。密钥在灵途造物生成并授权学堂;来源范围在学堂 /my-skills/api-keys 单独配置,默认没有机构来源权限。学堂实时校验后按 issuer/sub 映射原用户,继续检查投稿资格、作品归属和来源成员资格。

产品后端可用 POST /api/client/api-keys/metadata 读取本人已授权本应用的 Key 元信息,输入 sub/page/keyId,应用 Basic 认证;不返回密钥明文、摘要或其他应用权限。sub 由产品的已验证网页登录与身份绑定产生,不能相信浏览器提交。仅用于产品配置本地业务范围,实际调用仍每次 verify。

详见 API Key 接入契约 与 SDK 0.2.0 使用说明。

CREDITS / SETUP

开通算力接入

公共算力接入按应用单独开通。只使用统一登录的产品无需配置;登录成功并不代表用户允许扣费。此处管理余额与账务,模型 API、任务执行和业务定价由产品自行实现。

  1. 管理员在「接入应用」登记登录应用,并完成 OIDC 对接。
  2. 在该应用行点击「算力接入」,选择所需权限并勾选「允许新资金操作」。保存后复用该应用现有的 client_id / client_secret,无需新增密钥。
  3. 产品后端沿用 OIDC_CLIENT_ID 与 OIDC_CLIENT_SECRET,以 HTTP Basic 调用算力接口。密钥禁止使用 NEXT_PUBLIC_ 前缀;ID Token、Access Token 与浏览器 Cookie 不能代替应用认证。
  4. 按业务选择直接消费或划拨,接入下方接口并联调用户授权、余额不足与超时重试。
配置项 说明
查询 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);用户再在账号中心选择不限额或每日额度并授权。统一登录成功不包含扣费授权。

页面进入时的调用顺序

  1. 产品后端校验本地 OIDC 会话及账号状态,从会话读取 sub。
  2. 后端用应用 Basic 调用 POST /api/credits/v1/product/grant-status,请求 {sub}。该接口需要 consume 权限和登录关联,但不要求用户已授权,也不返回余额。
  3. 若返回 {authorized:false,dailyLimit:null},页面进入“未开通”状态,仅展示「开通共享算力」及说明。此时不请求 balance、transactions 或 debit,不显示伪造的 0 余额,也不将它渲染为通用加载失败。
  4. 用户点击开通后,产品后端创建并保存随机 state(绑定当前会话、sub、10 分钟有效),调用 POST /api/credits/v1/product/grant-requests,发送 {sub,returnUri,state},再跳转到固定账号中心 origin + 返回的 confirmationPath。returnUri 必须是产品固定的回调路由,且与登记的 OIDC 回调同源。
  5. 用户在账号中心选择不限额或填写每日额度并明确确认;产品不能预填默认授权额度、静默授权或替用户确认。用户取消时保留“未开通”页面,不自动再次跳转。
  6. 返回产品后,后端校验并原子消费 state、检查有效期、当前会话与 sub,重新核验账号状态,再调用 grant-status。grant_result=authorized 只是回调提示,不能直接作为已授权证明。
  7. 只有中央明确返回 authorized:true,才加载余额与账单;此时两项查询可并发。进入页面、重新聚焦页面或授权返回后应重新确认状态,不能把已开通永久保存在浏览器里。
  8. 已开通后若查询/扣费返回 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 去本地账号中心查询。

验收场景

  1. 新用户未开通:只发生状态检查,余额/账单请求次数均为 0,显示开通按钮。
  2. 用户取消开通:仍不查询余额和账单,不自动再次跳转。
  3. 授权成功:回调校验 state 并重新确认 authorized=true 后,两项查询正常返回。
  4. 用户撤销:下一次状态检查或 SPEND_NOT_AUTHORIZED 使页面返回未开通,并清除旧资金展示。
  5. 应用未配置/权限不足/凭证错误:显示管理员可定位的错误,不能伪装成用户未授权或无限跳转。
  6. state 错误、重复、过期或账号切换:拒绝回调,不查询资金数据;仅有 grant_result=authorized 也不能通过。

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

产品后端复用应用凭证接入统一支付;各自有产品按配置商户收款,公共算力固定使用中央商户。商品名称、价格、期限、规格和售卖状态可统一登记在灵途造物,由接入产品后台编辑,中心后台只读查看;实际业务权益仍由接入产品交付。

支持电脑扫码、微信内网页和微信外手机浏览器,采用逐次购买或续费,不自动续费。中心管理员在「支付接入」配置主体、场景及应用权限,未验证渠道保持关闭。商品可先登记,登记本身不会开通收款。

使用中央商品目录的应用,先生成固定商品版本的报价,再按 quoteId 创建支付单;启用 catalogOnly 后禁止任意金额下单。其他已有应用继续兼容服务端金额下单。公共算力充值仍需在账号中心选择金额;产品后端直接创建算力充值单的接口尚未提供。

商品登记、版本冲突、检查同步及“将学堂配置发布到统一登录”见下一节 商品登记与同步。完整支付接口、通知验签和商户配置见 统一支付接入文档。Node.js 22+ 接入可使用:

可将文档链接提供给有联网能力的 AI 读取,或下载 SDK 安装到产品后端。

PRODUCT CATALOG / SYNC

接入产品保留商品编辑入口,灵途造物统一存储商品名称、介绍、价格、期限、规格及售卖状态。中心后台「支付接入 → 商品目录」按应用只读查看商品、版本与关联订单;课程内容、系列范围、学习权益、赠课领取和推广归因仍由接入产品负责。公共算力继续使用独立的中央钱包流程。

先登记商品,再配置收款

应用完成登记并取得 client_id / client_secret 后,即可登记、读取自己的商品,不必先绑定商户或创建支付应用配置。登记商品不会开通收款;报价和支付单仍需要中心管理员配置并启用应用支付权限及收款场景,真实渠道验收完成前保持关闭。

应用密钥只由产品后端持有。浏览器先请求产品后台,产品后台校验当前操作者的管理权限,再调用中心接口。开发和正式环境分别使用各自的应用凭证与服务地址,不能混用。

商品接口

前缀为 https://login.lingtuzaowu.com/api/payments/v1/product,使用与 OIDC 相同的 HTTP Basic 应用认证。

方法 路径 用途
GET /catalog?after=<productId> 分页读取本应用当前商品,返回 records 和 next,每页最多 100 条
GET /catalog/:productId 读取商品当前版本及完整 document;未登记返回 404 NOT_FOUND
PUT /catalog/:productId 提交 { expectedVersion, document },创建时 expectedVersion 为 0

商品编号在 URL 中编码,并使用产品自己的稳定编号。例如学堂系列 series:starter-kit、整课 course:<课程ID>;规格沿用系列 ID 或原 SKU ID,不因改价重新生成编号。所有商品按应用隔离。

商品文档包含名称、介绍、售卖状态、默认单集规则与完整规格数组。规格包含价格(整数分)、期限(天,null 为不限时)、启用状态、是否允许赠送,以及关联目标。整课默认单集价格和继承该默认值的单集在同一商品版本中保存;单集独立定价继续保留。零价规格可登记,免费领取由产品本地交付,不创建零元支付报价。

每次实际变更创建不可变版本。保存必须携带读取到的当前版本;409 CATALOG_CONFLICT 要求重新读取差异并由操作者确认。完全相同的当前内容不会额外生成版本;原保存响应丢失时,只有内容完全相同且期望版本恰好落后一个版本的重试会返回已保存版本。

Node.js SDK 登记示例

使用 SDK 使用说明 中的 0.2.0 或更高版本。以下代码仅在产品后端执行:

import { PaymentClient } from "@lingtu/payment-sdk";

const payments = new PaymentClient({
  origin: "https://login.lingtuzaowu.com",
  clientId: process.env.OIDC_CLIENT_ID,
  clientSecret: process.env.OIDC_CLIENT_SECRET,
});
const productId = "series:starter-kit";
const document = {
  name: "AI Agent 上手体验包",
  description: "装好自己的 AI Agent,完成第一项真实任务。",
  enabled: true,
  defaultSingle: null,
  skus: [{
    id: "starter-kit",
    targetType: "series",
    targetId: "starter-kit",
    name: "AI Agent 上手体验包",
    priceFen: 1990,
    validityDays: null,
    billingPeriodMonths: null,
    enabled: true,
    giftable: false,
    inheritsDefault: false,
  }],
};
// 仅首次确认未登记时使用 0;修改时使用 getCatalog 返回的 version。
const registered = await payments.saveCatalog(productId, {
  expectedVersion: 0,
  document,
});
// 中心确认成功后,产品再更新本地展示缓存及中央版本。
console.log(registered.productId, registered.version);

应用停用后不能发布配置,凭证轮换立即生效。认证、网络或权限失败不能当成“未登记”,不得据此自动创建或覆盖商品。

后台检查、差异确认与双向同步

学堂系列管理提供“检查同步”和“发布到统一登录”两个入口:

  1. 检查时读取双方当前配置,比对名称、介绍、价格、期限、售卖状态、规格及版本;完全一致显示 “与灵途造物登记信息一致”。
  2. 存在差异时并列显示“学堂当前值”和“灵途造物登记值”,不自动覆盖。
  3. 从灵途造物同步:确认后使用中心登记信息更新学堂展示缓存,不修改中心商品。
  4. 将学堂配置发布到统一登录:先展示差异并确认,再按中心当前版本提交完整配置;尚未登记的商品确认后创建登记。配置一致时无需重复发布。
  5. 执行前重新检查双方配置和所属应用;任一边变化都要求重新检查。中心版本倒退、已登记商品丢失或规格无法映射时停止直接同步,先核对应用配置。

中心保存成功、本地缓存更新失败时,不应提示本地缓存已经同步;使用相同保存参数重试或重新读取中心最新版本恢复缓存。中心保存失败时保留表单内容,不将本地编辑标记为已生效。本地展示缓存不能用于最终定价。

学堂 API 的 CENTRAL_CATALOG_WRITES=enabled 可单独开启商品登记和编辑,不开启新支付;未配置或留空时沿用 CENTRAL_COMMERCE_WRITES。新下单继续由 CENTRAL_COMMERCE_WRITES 控制。切换前冻结时两个开关均设为 disabled。检查读取与从中心恢复缓存不受写入开关限制。

学堂服务端请求使用已配置的 IDENTITY_INTERNAL_ORIGIN,并保留公开域名 Host;它仅决定后端请求的上游,不替换公开 issuer、收银台地址或订单返回地址。开发时指向本机账号服务,避免将开发凭证发送到正式环境。

同步后按中央报价付款

配置 catalogOnly=true 的应用必须先调用 createQuote(),再使用 quoteId 调用 createOrder(),不能提交任意金额。中心报价固定用户、商品版本、规格、数量、金额与期限,有效 10 分钟。

新价格只影响新报价;已有有效报价和支付单保留原价。报价过期且尚未创建支付单时,重新报价并让用户确认;下单超时或响应丢失时使用原业务单号与稳定幂等键恢复,不创建替代订单。

支付通知仍需先验签、后查单,核对应用、中央用户 sub、业务单号、金额、币种、报价及完整商品快照,再在产品本地事务中幂等交付权益。退款处理中不提前撤权,成功退款后不得因重复或乱序通知重新开通权益。详见 统一支付接入文档。