import { useEffect, useRef, useState, type FormEvent } from 'react'; import { NativeApiError, type NativeApi } from '@api/native'; import { Button } from '@components/ui/button'; import { Input } from '@components/ui/input'; import { Label } from '@components/ui/label'; import { Badge } from '@components/ui/badge'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@components/ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@components/ui/alert-dialog'; import type { NativeClientKey } from '@typings/native'; function metadata(key: NativeClientKey): NativeClientKey { // 创建响应中的明文 key 不得随元数据列表保留,关闭展示窗口后只保留可公开的前缀。 return { id: key.id, name: key.name, prefix: key.prefix, created_at: key.created_at, expires_at: key.expires_at, last_used_at: key.last_used_at, revoked_at: key.revoked_at }; } function date(value: string | null) { return value ? new Date(value).toLocaleString('zh-CN') : '—'; } export function NativeProfileKeys({ api, profileId, onError }: { api: NativeApi; profileId: string; onError: (value: unknown) => void }) { const [keys, setKeys] = useState([]); const [loading, setLoading] = useState(true); const [name, setName] = useState(''); const [days, setDays] = useState('30'); const [pending, setPending] = useState(false); const [refresh, setRefresh] = useState(0); const [secret, setSecret] = useState<{ name: string; key: string } | null>(null); const [copied, setCopied] = useState(false); const [copyError, setCopyError] = useState(''); const [revoke, setRevoke] = useState(null); const request = useRef(); const mounted = useRef(true); useEffect(() => { mounted.current = true; return () => { mounted.current = false; request.current?.abort(); }; }, []); useEffect(() => { const controller = new AbortController(); setLoading(true); void api.keys(profileId, controller.signal).then(result => { if (!controller.signal.aborted) setKeys(result.items.map(metadata)); }).catch(value => { if (!controller.signal.aborted) onError(value); }) .finally(() => { if (!controller.signal.aborted) setLoading(false); }); return () => controller.abort(); }, [api, profileId, refresh, onError]); const create = async (event: FormEvent) => { event.preventDefault(); const expiry = Number(days); if (!name.trim() || request.current || !Number.isInteger(expiry) || expiry < 1 || expiry > 365) return; const controller = new AbortController(); request.current = controller; setPending(true); setCopied(false); setCopyError(''); try { const created = await api.createKey(profileId, name.trim(), expiry, controller.signal); if (controller.signal.aborted || !mounted.current) return; setKeys(value => [metadata(created), ...value]); setName(''); if (!created.key) throw new Error('服务器未返回新 API key。请撤销刚创建的记录后重新创建。'); // 明文仅用于当前一次展示;切换页面、profile 或登出都会卸载组件并清除它。 setSecret({ name: created.name, key: created.key }); } catch (value) { if (!controller.signal.aborted) onError(value instanceof NativeApiError && value.status === 0 ? new Error('未确认 API key 是否创建。请刷新 keys 检查记录;若记录已存在但未收到明文,请先撤销该记录再重新创建。') : value); } finally { request.current = undefined; if (mounted.current) setPending(false); } }; const confirmRevoke = async () => { if (!revoke || request.current) return; const selected = revoke; const controller = new AbortController(); request.current = controller; setPending(true); setRevoke(null); try { await api.revokeKey(profileId, selected.id, controller.signal); if (!controller.signal.aborted && mounted.current) setRefresh(value => value + 1); } catch (value) { if (!controller.signal.aborted) onError(value); } finally { request.current = undefined; if (mounted.current) setPending(false); } }; return

Profile API keys

每个 key 仅允许访问 {profileId} 的 MCP 接口。为每个客户端命名、设置有效期,并在不再需要时撤销。

setName(event.target.value)} disabled={pending} required className="mt-2" />
setDays(event.target.value)} disabled={pending} required className="mt-2" />

明文仅在创建后显示一次。请立即复制到客户端,关闭后无法再次查看;若丢失,请撤销并重新创建。

已签发的 API keys

{loading ?

正在读取 API keys…

: keys.length === 0 ?

此 profile 尚未创建 API key

:
    {keys.map(key => { const expired = Boolean(key.expires_at && new Date(key.expires_at).getTime() <= Date.now()); return
  • {key.name}

    {key.revoked_at ? '已撤销' : expired ? '已到期' : '有效'}

    {key.prefix}…

    创建:
    {date(key.created_at)}
    到期:
    {key.expires_at ? date(key.expires_at) : '未设置到期时间'}
    最近使用:
    {key.last_used_at ? date(key.last_used_at) : '尚未使用'}
    {key.revoked_at &&
    撤销:
    {date(key.revoked_at)}
    }
  • ; })}
} { if (!open) { setSecret(null); setCopied(false); setCopyError(''); } }}>保存 API key:{secret?.name}仅显示这一次。关闭后无法再次查看,请立即复制到对应客户端。{secret && <>