import type { PiOAuthMode, PiOAuthStartResult, PiOAuthState, PiProviderInfo, PiUpstream } from "@umt/contracts"; import { Check, Copy, ExternalLink, LoaderCircle } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { completePiOAuth, useInvalidatePi, useStartPiOAuth } from "~/api/pi.ts"; import { FormField } from "~/components/console/form-field.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Input } from "~/components/ui/input.tsx"; import { errorMessage } from "~/lib/api.ts"; import { cn } from "~/lib/utils.ts"; // 订阅登录:start → browser 粘回地址栏 URL / device 前端轮询 complete → 终态。 // 登录会话只在服务端内存里,15 分钟过期;失败、过期、被取消都可以重新开始。 const MODE_LABEL: Record = { device: "验证码", browser: "浏览器授权" }; const END_LABEL: Record< Exclude, { label: string; tone: "success" | "destructive" | "warning" } > = { succeeded: { label: "已登录", tone: "success" }, failed: { label: "登录失败", tone: "destructive" }, expired: { label: "登录会话已过期", tone: "warning" }, cancelled: { label: "登录已取消", tone: "warning" }, }; function CopyButton({ text, label }: { text: string; label: string }) { const [done, setDone] = useState(false); return ( ); } function useCountdown(expiresAt: string | null) { const [now, setNow] = useState(() => Date.now()); useEffect(() => { if (!expiresAt) return; const t = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(t); }, [expiresAt]); if (!expiresAt) return null; const left = Math.max(0, Math.floor((new Date(expiresAt).getTime() - now) / 1000)); return left; } export function PiOAuthLogin({ upstream, provider, onSucceeded, }: { upstream: PiUpstream; provider: PiProviderInfo | undefined; onSucceeded?: () => void; }) { const start = useStartPiOAuth(); const invalidate = useInvalidatePi(); const modes = provider?.oauth?.modes ?? ["browser"]; const [mode, setMode] = useState(modes[0] ?? "browser"); const [session, setSession] = useState(null); const [state, setState] = useState(null); const [message, setMessage] = useState(null); const [progress, setProgress] = useState([]); const [pasted, setPasted] = useState(""); const [submitting, setSubmitting] = useState(false); const abortRef = useRef(null); const left = useCountdown(state === "pending" ? (session?.expiresAt ?? null) : null); const termsNote = provider?.oauth?.termsNote ?? provider?.termsNote ?? null; useEffect(() => () => abortRef.current?.abort(), []); const finish = (s: PiOAuthState, msg: string | null) => { setState(s); setMessage(msg); invalidate(); if (s === "succeeded") { toast.success(`${upstream.name}已登录`); onSucceeded?.(); } }; // device:反复调 complete(每次服务端最多等 25 秒)直到终态或本地判定过期 const poll = async (s: PiOAuthStartResult) => { abortRef.current?.abort(); const ac = new AbortController(); abortRef.current = ac; while (!ac.signal.aborted) { if (Date.now() > new Date(s.expiresAt).getTime()) { finish("expired", "授权已超时。"); return; } try { const r = await completePiOAuth(upstream.id, { sessionId: s.sessionId, waitMs: 25_000 }, ac.signal); setProgress(r.progress); if (r.state !== "pending") { finish(r.state, r.message); return; } } catch (err) { if (ac.signal.aborted) return; finish("failed", errorMessage(err, "轮询登录状态失败")); return; } } }; const begin = async () => { abortRef.current?.abort(); setMessage(null); setProgress([]); setPasted(""); try { const r = await start.mutateAsync({ id: upstream.id, body: { mode } }); setSession(r); setState(r.state); setMessage(r.message); if (r.state === "pending" && r.mode === "device") void poll(r); } catch (err) { setSession(null); setState(null); toast.error(errorMessage(err, "无法开始登录")); } }; const submitPasted = async (e: React.FormEvent) => { e.preventDefault(); if (!session || !pasted.trim()) return; setSubmitting(true); try { const r = await completePiOAuth(upstream.id, { sessionId: session.sessionId, pastedUrlOrCode: pasted.trim(), waitMs: 25_000, }); setProgress(r.progress); if (r.state === "pending") setMessage("还在处理,稍后再提交一次。"); else finish(r.state, r.message); } catch (err) { toast.error(errorMessage(err, "提交失败")); } finally { setSubmitting(false); } }; const pending = state === "pending" && !!session; const ended = state && state !== "pending" ? END_LABEL[state] : null; const mmss = left === null ? "" : `${Math.floor(left / 60)}:${String(left % 60).padStart(2, "0")}`; return (
{termsNote ? (

{termsNote}

) : null} {!pending ? (
{modes.length > 1 ? (
登录方式
{modes.map((m) => ( ))}
) : null} {ended ? (
{ended.label} {message ? {message} : null}
) : null} {state !== "succeeded" ? (
) : null}
) : session.mode === "device" ? (

在任意设备打开验证地址,输入下面的验证码。授权后这里自动完成。

{session.userCode} {session.userCode ? : null}
{session.verificationUrl ? ( {session.verificationUrl} ) : null} {session.instructions ?

{session.instructions}

: null}

等待授权 · 剩余 {mmss} {progress.length ? ` · ${progress[progress.length - 1]}` : ""}

) : (

在浏览器打开授权链接。授权后会跳到 localhost 地址,页面打不开属正常,把地址栏里的完整 URL 粘贴到下面。

{session.authorizeUrl ? ( ) : null} setPasted(e.target.value)} placeholder="http://localhost:…/callback?code=…&state=…" autoComplete="off" disabled={submitting} /> {message ?

{message}

: null}
)}
); }