145 lines
4.9 KiB
TypeScript
145 lines
4.9 KiB
TypeScript
"use client";
|
||
|
||
import { ExternalLink, Link2Off, PlugZap } from "lucide-react";
|
||
|
||
import { Button } from "@/components/ui/Button";
|
||
import { SectionHeader } from "@/components/ui/SectionHeader";
|
||
import { StatusNote } from "@/components/ui/StatusNote";
|
||
import { authOpenUrl, formatExpiresAt } from "@/lib/settings/kimiOauth";
|
||
import { useKimiOauth } from "@/lib/settings/useKimiOauth";
|
||
|
||
interface KimiCodeOauthProps {
|
||
initialConnected: boolean;
|
||
initialExpiresAt: string | null;
|
||
}
|
||
|
||
// Kimi Code(OAuth 订阅 plan)连接区(K1.4)。
|
||
// 与 API-key 凭据行分离:无 key 输入,连接经 device flow(显示 user_code + 开授权页 + 轮询 job)。
|
||
export function KimiCodeOauth({
|
||
initialConnected,
|
||
initialExpiresAt,
|
||
}: KimiCodeOauthProps) {
|
||
const oauth = useKimiOauth({ initialConnected, initialExpiresAt });
|
||
const { phase, device, expiresAt, busy, error } = oauth;
|
||
const expiresLabel = formatExpiresAt(expiresAt);
|
||
|
||
return (
|
||
<section>
|
||
<SectionHeader
|
||
title="Kimi Code(OAuth)"
|
||
description={
|
||
<>
|
||
订阅 plan 经 OAuth 设备授权连接(无需 API Key)。连接后可在上方档位路由里选用
|
||
<span className="font-mono"> kimi-code · kimi-for-coding</span>。
|
||
</>
|
||
}
|
||
className="mb-3"
|
||
/>
|
||
|
||
<div className="rounded border border-line bg-panel p-4">
|
||
<div className="flex flex-wrap items-center gap-3">
|
||
<span
|
||
className={`h-2 w-2 rounded ${
|
||
phase === "connected" ? "bg-pass" : "bg-line"
|
||
}`}
|
||
aria-hidden="true"
|
||
/>
|
||
<span className="text-sm text-ink">
|
||
{phase === "connected" ? "已连接" : "未连接"}
|
||
</span>
|
||
{phase === "connected" && expiresLabel ? (
|
||
<span className="font-mono text-xs text-ink-soft">
|
||
凭据有效至 {expiresLabel}(到期自动刷新)
|
||
</span>
|
||
) : null}
|
||
|
||
<div className="ml-auto flex gap-2">
|
||
{phase === "connected" ? (
|
||
<Button
|
||
onClick={() => void oauth.disconnect()}
|
||
disabled={busy}
|
||
variant="secondary"
|
||
size="sm"
|
||
>
|
||
<Link2Off className="h-4 w-4" aria-hidden="true" />
|
||
{busy ? "处理中…" : "断开"}
|
||
</Button>
|
||
) : (
|
||
<Button
|
||
onClick={() => void oauth.connect()}
|
||
disabled={busy}
|
||
variant="primary"
|
||
size="sm"
|
||
>
|
||
<PlugZap className="h-4 w-4" aria-hidden="true" />
|
||
{busy
|
||
? "等待授权…"
|
||
: phase === "error"
|
||
? "重新连接 Kimi Code(OAuth)"
|
||
: "连接 Kimi Code(OAuth)"}
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{phase === "awaiting" && device ? (
|
||
<DeviceInstructions
|
||
userCode={device.userCode}
|
||
openUrl={authOpenUrl(device)}
|
||
/>
|
||
) : null}
|
||
|
||
{phase !== "awaiting" ? (
|
||
<StatusNote className="mt-3" variant="info">
|
||
OAuth 连接只保存加密后的访问凭据;模型选择仍在“档位路由”里统一配置。
|
||
</StatusNote>
|
||
) : null}
|
||
|
||
{phase === "error" && error ? (
|
||
<StatusNote className="mt-3" variant="danger" role="alert">
|
||
{error}
|
||
</StatusNote>
|
||
) : null}
|
||
</div>
|
||
</section>
|
||
);
|
||
}
|
||
|
||
interface DeviceInstructionsProps {
|
||
userCode: string;
|
||
openUrl: string;
|
||
}
|
||
|
||
// 设备授权指引:突出展示 user_code(可读、可选、可聚焦)+ 打开授权页按钮。
|
||
function DeviceInstructions({ userCode, openUrl }: DeviceInstructionsProps) {
|
||
const openAuth = (): void => {
|
||
if (typeof window !== "undefined") {
|
||
window.open(openUrl, "_blank", "noopener,noreferrer");
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="motion-safe:transition-opacity mt-4 rounded border border-dashed border-line bg-bg p-4">
|
||
<StatusNote className="mb-3" variant="info">
|
||
请在浏览器中打开授权页,并确认页面显示的设备码与下方一致。
|
||
</StatusNote>
|
||
<div className="grid gap-3 sm:grid-cols-[minmax(0,1fr)_auto] sm:items-center">
|
||
<output
|
||
aria-label="设备授权码"
|
||
tabIndex={0}
|
||
className="block select-all rounded bg-panel px-4 py-3 text-center font-mono text-2xl tracking-[0.3em] text-ink"
|
||
>
|
||
{userCode}
|
||
</output>
|
||
<Button onClick={openAuth} variant="outline" size="sm">
|
||
<ExternalLink className="h-4 w-4" aria-hidden="true" />
|
||
打开授权页
|
||
</Button>
|
||
</div>
|
||
<p className="mt-2 text-xs text-ink-soft">
|
||
授权完成后本页会自动检测连接状态(设备码过期后请重新连接)。
|
||
</p>
|
||
</div>
|
||
);
|
||
}
|