"""FastAPI × Tokin:Web 多用户接入的最小可跑示例(可直接抄进你的项目)。
跑起来:
pip install tokincloud fastapi uvicorn
python examples/fastapi_web.py
浏览器打开 http://127.0.0.1:8000/
这个示例用 loopback 回调 + dev_ 前缀,所以**不需要在 Tokin 注册**就能跑通。
上线时换两处即可(其余代码不动):
APP_ID → 控制台创建应用拿到的 app_xxx
REDIRECT_URI → https://你的域名/oauth/callback(不用登记,首次授权自动绑定)
要点:
- 每个「本站用户」各自授权、各自扣自己的钱包,你不垫付、也拿不到对方的主 Key。
- 凭证由 TokenStore 落库,进程重启不丢;过期自动续期并回存,你不用管。
- 生产多进程(gunicorn -w 4)请把 pending 换成 Redis 实现,见文末注释。
"""
import uvicorn
from fastapi import Cookie, FastAPI, Request, Response
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from tokincloud import SqliteTokenStore, TokinAuthError, TokinError, TokinWebAuth
BASE_URL = "https://api.tokincloud.com"
APP_ID = "dev_myapp" # 上线换成 app_xxx
# 必须是**固定常量**,不要从 request.base_url / Host 头推导。
# 平台按「首次授权自动绑定、之后只认已绑定的」来防回调被换掉,而同一台服务器上
# http://IP:8321、https://IP:8443、以后的域名**各算一个绑定**——从请求推导的话,
# 你自己的几个变体就会把绑定位占满,之后所有人都授权不了,且现场看不出原因
# (授权页正常、回调正常,只是不认了)。上线就改这一行。
REDIRECT_URI = "http://127.0.0.1:8000/oauth/callback" # 上线换成 https://你的域名/oauth/callback
app = FastAPI()
auth = TokinWebAuth(
base_url=BASE_URL,
app_id=APP_ID,
redirect_uri=REDIRECT_URI,
store=SqliteTokenStore("tokin_tokens.db"), # 换成你自己的库:实现 TokenStore 三个方法即可
)
def current_user(request: Request) -> str:
"""把这里换成你自己的登录态。示例用 cookie 里的 uid 冒充「本站用户ID」。"""
return request.cookies.get("uid") or "demo-user"
@app.get("/", response_class=HTMLResponse)
def home(request: Request):
uid = current_user(request)
if not auth.is_authorized(uid):
return f'
用户 {uid}
连接我的 Tokin 账户
'
return (
f'用户 {uid} 已连接
'
f'问一个问题 · '
f'看余额 · 解除绑定
'
)
# ① 发起授权:跳到 Tokin 授权页,用户在那边登录并同意
@app.get("/connect")
def connect(request: Request):
return RedirectResponse(auth.start(user_id=current_user(request)))
# ② 回调:换取凭证并落库。user_id 由 state 还原,不用自己在 URL 里传(传了也不可信)
@app.get("/oauth/callback")
def callback(code: str = None, state: str = None, error: str = None):
try:
uid = auth.callback(code=code, state=state, error=error)
except TokinAuthError as e:
return HTMLResponse(f"连接失败:{e}
重试
", status_code=400)
resp = RedirectResponse("/")
resp.set_cookie("uid", uid) # 示例用;真实项目这里应是你自己的会话
return resp
# ③ 之后随时用:凭证过期自动续,你只管发请求
@app.get("/ask")
def ask(request: Request, q: str = "你好"):
uid = current_user(request)
try:
r = auth.client_for(uid).chat(
model="deepseek-v4-flash",
messages=[{"role": "user", "content": q}],
)
return JSONResponse({"answer": r["choices"][0]["message"]["content"], "usage": r.get("usage")})
except TokinAuthError:
# 授权被用户吊销 / refresh 失效 → 引导重新连接(这是唯一需要你处理的凭证异常)
return RedirectResponse("/connect")
except TokinError as e:
# 余额不足是 402,前端可据此弹「去充值」
return JSONResponse({"error": str(e), "status": e.status}, status_code=e.status or 500)
@app.get("/balance")
def balance(request: Request):
uid = current_user(request)
try:
return {"balance": auth.balance_of(uid)}
except TokinAuthError:
return RedirectResponse("/connect")
@app.get("/disconnect")
def disconnect(request: Request, response: Response):
auth.forget(current_user(request)) # 只删本地凭证;用户在 Tokin 控制台也能自行吊销
return RedirectResponse("/")
# ── 多进程部署(gunicorn -w 4 / 多实例)──
# 默认 pending 是进程内内存,A 进程发起、B 进程收回调会判 state 无效。换成 Redis:
#
# from tokincloud import PendingStore
# import json, redis
#
# class RedisPending(PendingStore):
# def __init__(self, r): self.r = r
# def put(self, state, data, ttl): self.r.setex(f"tokin:st:{state}", ttl, json.dumps(data))
# def pop(self, state):
# raw = self.r.getdel(f"tokin:st:{state}") # 一次性,防重放
# return json.loads(raw) if raw else None
#
# auth = TokinWebAuth(..., pending=RedisPending(redis.Redis()))
if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8000)