import { useCallback, useEffect, useState } from 'react'; import * as api from '../api/client'; import { BotIcon, CheckIcon, ArchiveIcon } from './icons'; /** * Agent 管理面板(管理员):默认预算 + 停用/恢复。 * * 预算决定「派给某个 Agent 的新任务默认多少个来回」。 * 停用/恢复控制 Agent 的准入:停用后密钥撤销、注册被拒,邮件与会话保留。 */ export default function QuotaPanel() { const [stats, setStats] = useState([]); const [error, setError] = useState(null); const [busy, setBusy] = useState(null); const [drafts, setDrafts] = useState>({}); const [confirming, setConfirming] = useState(null); const [confirmAction, setConfirmAction] = useState<'toggle' | 'delete' | null>(null); const [notice, setNotice] = useState(null); const load = useCallback(async () => { try { const r = await api.adminListAgentStats(); setStats(r.quotas); setError(null); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }, []); useEffect(() => { load(); }, [load]); const apply = async (name: string, defaultRounds: number) => { setBusy(name); setError(null); try { await api.adminSetDefaultRounds(name, defaultRounds); await load(); setDrafts(d => { const next = { ...d }; delete next[name]; return next; }); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(null); } }; const deleteAgent = async (name: string) => { setBusy(name); setError(null); setNotice(null); try { const result = await api.adminDeleteAgent(name); await load(); setConfirming(null); setConfirmAction(null); const revoked = typeof result.keys_revoked === 'number' && result.keys_revoked > 0 ? `(已撤销 ${result.keys_revoked} 把密钥)` : ''; setNotice(`${name} 已删除${revoked}`); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(null); } }; const toggleStatus = async (name: string, currentDisabled: boolean) => { setBusy(name); setError(null); setNotice(null); try { const result = await api.adminSetAgentStatus(name, !currentDisabled); await load(); setConfirming(null); // 撤销了几把密钥是停用操作里最有信息量的部分 —— 恢复后要重新签发几把, // 只说「已停用」的话用户不知道还有这一步。 const revoked = typeof result.keys_revoked === 'number' && result.keys_revoked > 0 ? `(已撤销 ${result.keys_revoked} 把密钥)` : ''; // 恢复路径必须把「密钥不会自动回来」说出口。否则用户点完恢复就算完事, // 而插件拿着已撤销的密钥无限重试并被 401 —— 本会话就踩过: // opencode 被停用后拿旧密钥重试了 18 小时,gateway 日志里 2690 次 401。 setNotice(result.disabled ? `${name} 已停用${revoked}` : `${name} 已恢复为离线。密钥不会自动回来 —— 请到「密钥」面板重新签发一把并写进插件配置,否则它会一直被拒(401)。`); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(null); } }; const statusBadge = (s: api.AgentStats) => { const st = s.status ?? 'offline'; const cls = st === 'online' ? 'bg-green-50 text-green-700' : st === 'disabled' ? 'bg-red-50 text-red-600' : 'bg-gray-50 text-gray-500'; const label = st === 'online' ? '在线' : st === 'disabled' ? '已停用' : '离线'; return {label}; }; return (

Agent 管理

{stats.length}

默认预算:派给某个 Agent 的新任务默认多少个来回(填 0 = 不限)。 这只是默认值 —— 写信时可以单独指定,之后在对话页里还能随时调整。 插件自动转发的最终总结与权限询问不占用预算。
停用:撤销该 Agent 的全部密钥、 从地址补全与联系人里隐藏,拒绝它重新注册,并让别人发信给它时收到 409。 邮件、会话、模型范围全部保留,随时可恢复 —— 但密钥不会自动回来,恢复后需重新签发。
删除:在停用的基础上清掉运行态(日历事件置 cancelled)。 邮件与会话保留(历史是审计凭据),此名字今后不可再注册。

{error &&
{error}
} {notice &&
{notice}
} {stats.length === 0 ? (
暂无已注册的 Agent
) : (
{stats.map(s => { const draft = drafts[s.agent_name] ?? String(s.default_rounds); const dirty = draft !== String(s.default_rounds); const invalid = draft.trim() !== '' && !/^\d+$/.test(draft.trim()); const isDisabled = s.status === 'disabled'; const isConfirming = confirming === s.agent_name; return (
{s.agent_name} {statusBadge(s)}
{s.default_rounds === 0 ? '默认不限来回' : `默认 ${s.default_rounds} 个来回`} {' · '}进行中 {s.active_sessions} 个任务 · 累计发信 {s.sent_total}
setDrafts(d => ({ ...d, [s.agent_name]: e.target.value }))} inputMode="numeric" title="新任务默认往返数;0 = 不限" className={`w-16 text-xs border rounded px-1.5 py-1 shrink-0 focus:outline-none focus:ring-2 focus:ring-blue-100 ${ invalid ? 'border-red-300' : 'border-gray-300' }`} /> {/* 停用/恢复/删除 按钮 */} {isConfirming ? (
确认{confirmAction === 'delete' ? '删除' : ''}?
) : (
)}
); })}
)}
); }