"""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)