import { useState } from 'react'; import * as api from '../api/client'; import { KeyIcon, CopyIcon, TrashIcon, PlusIcon, CheckIcon } from './icons'; /** 密钥类型的中文说明,创建表单与列表共用一份文案 */ export const KEY_TYPE_LABEL: Record = { permanent: '长期', one_time: '一次性', timed: '限时' }; const KEY_TYPE_HINT: Record = { permanent: '永不过期,可重复使用', one_time: '首次使用后立即失效', timed: '指定小时数后过期' }; /** 一条密钥在列表里的状态:过期/已用完/可用 */ function keyState(k: { key_type: api.KeyType; expires_at: string | null; used_at: string | null }) { if (k.key_type === 'one_time' && k.used_at) return { text: '已使用', cls: 'text-gray-400' }; if (k.key_type === 'timed' && k.expires_at && new Date(k.expires_at) < new Date()) return { text: '已过期', cls: 'text-red-500' }; return { text: '可用', cls: 'text-green-600' }; } /** * 新签发密钥的一次性展示条。 * * 密钥全文只在创建响应里出现一次,服务端之后只返回前 8 位, * 所以这里必须明确提示「关掉就再也看不到」,而不是让用户以为随时能回来复制。 */ function NewKeyBanner({ token, onDismiss }: { token: string; onDismiss: () => void }) { const [copied, setCopied] = useState(false); const copy = async () => { try { await navigator.clipboard.writeText(token); setCopied(true); setTimeout(() => setCopied(false), 1500); } catch { /* 无剪贴板权限时用户可手动选中 */ } }; return (
密钥已创建。全文仅显示这一次,关闭后无法再次查看。
{token}
); } interface CreateFormProps { /** Agent 密钥面板会多出「绑定 Agent」与「登记已有密钥」两项 */ variant: 'agent' | 'user'; busy: boolean; onSubmit: (payload: api.CreateKeyPayload) => void; } function CreateForm({ variant, busy, onSubmit }: CreateFormProps) { const [open, setOpen] = useState(false); const [keyType, setKeyType] = useState('permanent'); const [label, setLabel] = useState(''); const [hours, setHours] = useState(24); const [agentName, setAgentName] = useState(''); const [keyToken, setKeyToken] = useState(''); if (!open) { return ( ); } const submit = () => { const payload: api.CreateKeyPayload = { key_type: keyType, label: label.trim() }; if (keyType === 'timed') payload.expires_hours = hours; if (variant === 'agent') { if (agentName.trim()) payload.agent_name = agentName.trim(); if (keyToken.trim()) payload.key_token = keyToken.trim(); } onSubmit(payload); setOpen(false); setLabel(''); setAgentName(''); setKeyToken(''); }; return (
{(Object.keys(KEY_TYPE_LABEL) as api.KeyType[]).map(t => ( ))}
setLabel(e.target.value)} placeholder="备注(如 我的笔记本 / CI 机器)" className="flex-1 text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-100" /> {keyType === 'timed' && (
setHours(Math.max(1, Number(e.target.value) || 1))} className="w-20 text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-100" /> 小时后过期
)}
{variant === 'agent' && (
setAgentName(e.target.value)} placeholder="绑定到 Agent(留空 = 首次注册时自动落定)" className="w-full text-xs border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-100" /> setKeyToken(e.target.value)} placeholder="登记插件本地生成的密钥(留空 = 由服务器生成)" className="w-full text-xs font-mono border border-gray-300 rounded px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-blue-100" />
)}
); } /** * 密钥面板。Agent 密钥(管理员)与用户连接密钥共用同一套渲染, * 差异用 variant 表达:只有 Agent 密钥能绑定 Agent 名、能登记客户端已生成的密钥。 */ export default function KeyPanel({ variant, keys, loading, error, newToken, onCreate, onDelete, onBind, onDismissToken }: { variant: 'agent' | 'user'; keys: (api.AgentKey | api.UserKey)[]; loading: boolean; error: string | null; newToken: string | null; onCreate: (payload: api.CreateKeyPayload) => void; onDelete: (id: string) => void; onBind?: (id: string, agentName: string) => void; onDismissToken: () => void; }) { const [bindingID, setBindingID] = useState(null); const [bindName, setBindName] = useState(''); return (

{variant === 'agent' ? 'Agent 接入密钥' : '客户端连接密钥'}

{variant === 'agent' ? 'Agent 用该密钥注册、收发邮件与订阅通知。插件首次安装会在本地生成一把密钥并打印出来,把它填到「登记」框即可。' : '第三方客户端用该密钥访问自己的邮箱(Authorization: Bearer)。它不能用于注册 Agent。'}

{error &&
{error}
} {newToken && } {keys.length === 0 ? (
暂无密钥
) : (
{keys.map(k => { const st = keyState(k); const agentKey = variant === 'agent' ? (k as api.AgentKey) : null; return (
{k.token_hint}
{k.label || (无备注)}
{KEY_TYPE_LABEL[k.key_type]} {k.expires_at && ` · ${new Date(k.expires_at).toLocaleString()} 过期`} {agentKey && (agentKey.agent_name ? ` · ${agentKey.agent_name}` : ' · 待绑定')}
{st.text} {agentKey && onBind && bindingID === k.key_id ? (
setBindName(e.target.value)} placeholder="Agent 名" className="w-28 text-[11px] border border-gray-300 rounded px-1.5 py-1" />
) : ( agentKey && onBind && ( ) )}
); })}
)}
); }