/** * 多账号管理(账号页里的一段):列出已添加账号、添加新账号、删除。 * * # 添加时**真的去验证**连通性 * * 粘贴一个错的密钥不会报错——它只会表现为"这个账号的收件箱一直是空的"。 * 所以添加流程必须调一次 `GET /auth/me`: * 成功 → 显示用户名(用户能确认"这是我要的那个账号") * 401 → 明确说"密钥无效",不写进账号列表 * 网络错 → 说清是哪一种(连不上 / 超时),因为这两种的修法不同 * * 用「显式认证」发这个请求(`fetchWithAuth`),不碰当前账号的认证单例 —— * 否则验证一个坏账号的过程会把当前账号的令牌换掉。 */ import { useState } from 'react'; import { fetchWithAuth } from '../api/config'; import { deriveDisplayName, normalizeGateway } from '../lib/accounts'; import { activeAccount, useAccountStore } from '../stores/accountStore'; export default function AccountList() { const accounts = useAccountStore(s => s.accounts); const activeId = useAccountStore(s => s.activeId); const ephemeral = useAccountStore(s => s.ephemeralStorage); const storageFile = useAccountStore(s => s.storageFile); const addAccount = useAccountStore(s => s.add); const removeAccount = useAccountStore(s => s.remove); const setActive = useAccountStore(s => s.setActive); const current = useAccountStore(activeAccount); const [open, setOpen] = useState(false); const [gateway, setGateway] = useState(''); const [token, setToken] = useState(''); const [name, setName] = useState(''); const [busy, setBusy] = useState(false); const [msg, setMsg] = useState<{ kind: 'ok' | 'err'; text: string } | null>(null); const submit = async () => { setMsg(null); const g = normalizeGateway(gateway); const t = token.trim(); if (!g) return setMsg({ kind: 'err', text: '请填写 Gateway 地址' }); if (!t) return setMsg({ kind: 'err', text: '请填写用户密钥' }); setBusy(true); try { // 先验证,再入库 —— 顺序反过来就会出现"列表里有个永远空的账号" const res = await fetchWithAuth({ base: `${g}/api/v1`, token: t }, '/auth/me'); const body = await res.json().catch(() => ({})); if (res.status === 401 || res.status === 403) { setBusy(false); return setMsg({ kind: 'err', text: '密钥无效(服务端返回 401)——请确认用的是用户密钥' }); } if (!res.ok) { setBusy(false); return setMsg({ kind: 'err', text: `服务端返回 ${res.status}${body?.error ? `:${body.error}` : ''}` }); } const username = body?.user?.username || body?.username || ''; const r = await addAccount({ gateway: g, token: t, username, displayName: name.trim() || deriveDisplayName(g, username) }); setBusy(false); if (!r.ok) return setMsg({ kind: 'err', text: r.error || '添加失败' }); setMsg({ kind: 'ok', text: `已添加${username ? `(${username})` : ''}` }); setGateway(''); setToken(''); setName(''); setOpen(false); } catch (e) { setBusy(false); // 连不上 / 超时 / CORS 都要能分辨:这里至少把原始错误原样说出来 setMsg({ kind: 'err', text: `连不上:${(e as Error)?.message || e}` }); } }; return (
{ephemeral ? '账号保存在浏览器 localStorage(当前不在桌面端,这不是加密存储)' : `账号保存在本机文件:${storageFile || '(未知路径)'}(权限 600)`}
添加前会调用一次 /auth/me 验证密钥,通过才写入。
{msg.text}
)}