Files
writer-work-flow/apps/web/components/settings/KimiCodeOauth.tsx
2026-06-28 07:31:20 +02:00

145 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 CodeOAuth 订阅 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 CodeOAuth"
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 CodeOAuth"
: "连接 Kimi CodeOAuth"}
</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>
);
}