diff --git a/web/src/api/client.ts b/web/src/api/client.ts index c7c0a03..ba30d3d 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -397,12 +397,28 @@ export interface AgentStats { sent_total: number; /** 参与的未归档会话数,配合默认值判断设多少合适 */ active_sessions: number; + /** online / offline / disabled —— 管理页靠它区分状态并决定显示停用/恢复按钮 */ + status?: string; } export async function adminListAgentStats() { return request<{ quotas: AgentStats[] }>('GET', '/admin/quotas'); } +/** 停用(true)或恢复(false)一个 Agent。返回撤销的密钥数等信息。 */ +export async function adminSetAgentStatus(agentName: string, disabled: boolean) { + return request<{ + agent_name: string; + disabled: boolean; + keys_revoked?: number; + detail: string; + }>( + 'PUT', + `/admin/agents/${encodeURIComponent(agentName)}/status`, + { disabled } + ); +} + /** 改该 Agent 的新任务默认预算(0 = 不限)。 */ export async function adminSetDefaultRounds(agentName: string, defaultRounds: number) { return request<{ quota: AgentStats }>( diff --git a/web/src/components/QuotaPanel.tsx b/web/src/components/QuotaPanel.tsx index 917770a..c1b7405 100644 --- a/web/src/components/QuotaPanel.tsx +++ b/web/src/components/QuotaPanel.tsx @@ -1,24 +1,19 @@ import { useCallback, useEffect, useState } from 'react'; import * as api from '../api/client'; -import { BotIcon, CheckIcon } from './icons'; +import { BotIcon, CheckIcon, CloseIcon } from './icons'; /** - * Agent 新任务默认预算(管理员)。 + * Agent 管理面板(管理员):默认预算 + 停用/恢复。 * - * **这里配的是默认值,不是额度。** - * - * 额度(往返预算)属于具体任务 —— 在写信时给、在对话页里随时改。 - * 这个面板只决定「派给某个 Agent 的新任务,如果没人显式指定,默认给几个来回」: - * 跑测试的小工具与重构整个模块的 Agent,合理来回数差一个量级,所以分开设。 - * - * 累计发信数只是观测数据,不拦任何请求 —— 之前它是「终身额度」, - * 但终身额度跑满要人工重置才能再干活,而 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 load = useCallback(async () => { try { @@ -52,11 +47,40 @@ export default function QuotaPanel() { } }; + const toggleStatus = async (name: string, currentDisabled: boolean) => { + setBusy(name); + setError(null); + try { + const result = await api.adminSetAgentStatus(name, !currentDisabled); + await load(); + setConfirming(null); + // 停用时显示撤销了几把密钥,这是操作结果里最有信息量的部分 + if (result.detail) setError(null); // 清掉旧错误,让 detail 独占提示区 + setBusy(null); + return; + } 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 新任务默认预算

+

Agent 管理

{stats.length}
@@ -67,7 +91,7 @@ export default function QuotaPanel() { 插件自动转发的最终总结与权限询问不占用预算。

- {error &&
{error}
} + {error &&
{error}
} {stats.length === 0 ? (
暂无已注册的 Agent
@@ -77,12 +101,17 @@ export default function QuotaPanel() { 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} 个来回`} @@ -107,6 +136,39 @@ export default function QuotaPanel() { > + + {/* 停用/恢复按钮 */} + {isConfirming ? ( +
+ 确认? + + +
+ ) : ( + + )}
); })}