|
|
|
@@ -1,6 +1,6 @@
|
|
|
|
|
import {FormEvent, useEffect, useRef, useState} from "react";
|
|
|
|
|
import "./App.css";
|
|
|
|
|
import {AgentDirectory, AgentLLMStatuses, AgentMCPStatuses, AgentOperationalDetails, AgentSkillStatuses, Agents, AuditTrail, CancelTask, ClearTaskHistory, ContinueTaskSession, CreatePairingCode, CreateTaskWithImages, DeleteMCPTool, DeployLLMProfile, DeploySkill, DispatchMentionsWithImages, ImportSkillsFromDirectory, LLMProfiles, MCPTools, OnlineAgents, RecentTasks, RenameAgent, RetryTask, RevokeAgent, SaveLLMProfile, SaveMCPTool, SaveSkill, SetAgentMCPTools, SetAgentSkills, SkillDocument, Skills, Snapshot, TaskDetails, TaskSessionTurns} from "../wailsjs/go/main/App";
|
|
|
|
|
import {AgentDirectory, AgentLLMStatuses, AgentMCPStatuses, AgentOperationalDetails, AgentSkillStatuses, Agents, AuditTrail, CancelTask, ClearTaskHistory, ContinueTaskSession, CreatePairingCode, CreateTaskWithImages, DeleteMCPTool, DeployLLMProfile, DeploySkill, DispatchMentionsWithImages, ImportSkillsFromDirectory, LLMProfiles, MCPTools, OnlineAgents, RecentTasks, RenameAgent, RestartAgent, RetryTask, RevokeAgent, SaveLLMProfile, SaveMCPTool, SaveSkill, SetAgentMCPTools, SetAgentSkills, SkillDocument, Skills, Snapshot, TaskDetails, TaskSessionTurns} from "../wailsjs/go/main/App";
|
|
|
|
|
import {controlplane, main} from "../wailsjs/go/models";
|
|
|
|
|
|
|
|
|
|
type Page = "overview" | "agents" | "batch" | "direct" | "skills" | "plugins" | "keys" | "audit" | "settings";
|
|
|
|
@@ -187,6 +187,14 @@ function App() {
|
|
|
|
|
finally { setLoading(false); }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const restartAgent = async (agent: controlplane.Agent) => {
|
|
|
|
|
if (agent.status !== "online") { setError("该 Agent 当前离线,无法通过已认证连接发送重启指令。"); return; }
|
|
|
|
|
if (!window.confirm(`重启“${agent.name}”?该 Agent 正在运行的任务将被安全取消。`)) return;
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try { await RestartAgent(agent.id); setNotice(`已请求 ${agent.name} 重启,正在等待其重新连接。`); await refresh(); }
|
|
|
|
|
catch (reason) { setError(String(reason)); } finally { setLoading(false); }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const saveLLMProfile = async (input: controlplane.LLMProfileInput) => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
@@ -277,7 +285,7 @@ function App() {
|
|
|
|
|
{error && <div className="message error">{error}</div>}
|
|
|
|
|
{notice && <div className="message notice">{notice}</div>}
|
|
|
|
|
{page === "overview" && <><Overview snapshot={snapshot} agents={agents} onNavigate={setPage}/><Tasks agents={agents} tasks={tasks} loading={loading} onCancel={(task) => void cancelTask(task)} onRetry={(task) => void retryTask(task)} onClearHistory={() => void clearTaskHistory()} onOpen={(task) => void showTaskDetail(task)}/></>}
|
|
|
|
|
{page === "agents" && <AgentManagement skills={skills} skillStatuses={agentSkillStatuses} mcpTools={mcpTools} mcpStatuses={agentMCPStatuses} loading={loading} onRename={(agentID, name) => void renameAgent(agentID, name)} onSetSkills={(targetAgentID, skillIDs) => void setAgentSkills(targetAgentID, skillIDs)} onSetMCPTools={(targetAgentID, toolIDs) => void setAgentMCPTools(targetAgentID, toolIDs)} onRevoke={(agent) => void revokeAgent(agent)} onPair={() => setPage("keys")}/>}
|
|
|
|
|
{page === "agents" && <AgentManagement skills={skills} skillStatuses={agentSkillStatuses} mcpTools={mcpTools} mcpStatuses={agentMCPStatuses} loading={loading} onRename={(agentID, name) => void renameAgent(agentID, name)} onSetSkills={(targetAgentID, skillIDs) => void setAgentSkills(targetAgentID, skillIDs)} onSetMCPTools={(targetAgentID, toolIDs) => void setAgentMCPTools(targetAgentID, toolIDs)} onRestart={(agent) => void restartAgent(agent)} onRevoke={(agent) => void revokeAgent(agent)} onPair={() => setPage("keys")}/>}
|
|
|
|
|
{page === "batch" && <MentionDispatch agents={agents} tasks={tasks} loading={loading} onSubmit={dispatchMentions} onOpen={(task) => void showTaskDetail(task)}/>}
|
|
|
|
|
{page === "direct" && <DirectAssignment agents={agents} tasks={tasks} loading={loading} onSubmit={dispatchDirectTask} onOpen={(task) => void showTaskDetail(task)}/>}
|
|
|
|
|
{page === "keys" && <><Security snapshot={snapshot} pairingCode={pairingCode} loading={loading} onCreate={() => void createPairingCode()}/><LLMConfiguration profiles={llmProfiles} agents={managedAgents} statuses={agentLLMStatuses} loading={loading} onSave={(input) => void saveLLMProfile(input)} onDeploy={(profileID, agentIDs) => void deployLLMProfile(profileID, agentIDs)}/></>}
|
|
|
|
@@ -300,7 +308,7 @@ function Overview({snapshot, agents, onNavigate}: {snapshot?: controlplane.Snaps
|
|
|
|
|
</>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function AgentManagement({skills, skillStatuses, mcpTools, mcpStatuses, loading, onRename, onSetSkills, onSetMCPTools, onRevoke, onPair}: {skills: controlplane.Skill[]; skillStatuses: controlplane.AgentSkillStatus[]; mcpTools: controlplane.MCPTool[]; mcpStatuses: controlplane.AgentMCPStatus[]; loading: boolean; onRename: (agentID: string, name: string) => void; onSetSkills: (agentID: string, skillIDs: string[]) => void; onSetMCPTools: (agentID: string, toolIDs: string[]) => void; onRevoke: (agent: controlplane.Agent) => void; onPair: () => void}) {
|
|
|
|
|
function AgentManagement({skills, skillStatuses, mcpTools, mcpStatuses, loading, onRename, onSetSkills, onSetMCPTools, onRestart, onRevoke, onPair}: {skills: controlplane.Skill[]; skillStatuses: controlplane.AgentSkillStatus[]; mcpTools: controlplane.MCPTool[]; mcpStatuses: controlplane.AgentMCPStatus[]; loading: boolean; onRename: (agentID: string, name: string) => void; onSetSkills: (agentID: string, skillIDs: string[]) => void; onSetMCPTools: (agentID: string, toolIDs: string[]) => void; onRestart: (agent: controlplane.Agent) => void; onRevoke: (agent: controlplane.Agent) => void; onPair: () => void}) {
|
|
|
|
|
const [query, setQuery] = useState("");
|
|
|
|
|
const [connectionStatus, setConnectionStatus] = useState("");
|
|
|
|
|
const [mcpStatus, setMCPStatus] = useState("");
|
|
|
|
@@ -334,7 +342,7 @@ function AgentManagement({skills, skillStatuses, mcpTools, mcpStatuses, loading,
|
|
|
|
|
<p className="management-intro">名称搜索和所有状态筛选均在总控服务端执行。点击卡片可查看最近任务、审计、部署轨迹和失败原因。</p>
|
|
|
|
|
<div className="agent-directory-toolbar"><label>按名称搜索<input type="search" value={query} onChange={(event) => updateFilter(() => setQuery(event.target.value))} placeholder="输入 Agent 名称"/></label><label>连接<select value={connectionStatus} onChange={event => updateFilter(() => setConnectionStatus(event.target.value))}><option value="">全部</option><option value="online">在线</option><option value="offline">离线</option></select></label><label>MCP<select value={mcpStatus} onChange={event => updateFilter(() => setMCPStatus(event.target.value))}><option value="">全部</option><option value="未启用">未启用</option><option value="pending">等待连接</option><option value="sent">下发中</option><option value="applied">已应用</option><option value="failed">失败</option></select></label><label>LLM<select value={llmStatus} onChange={event => updateFilter(() => setLLMStatus(event.target.value))}><option value="">全部</option><option value="未部署">未部署</option><option value="pending">等待连接</option><option value="sent">下发中</option><option value="applied">已应用</option><option value="failed">失败</option></select></label><label>Skill<select value={skillStatus} onChange={event => updateFilter(() => setSkillStatus(event.target.value))}><option value="">全部</option><option value="未选择">未选择</option><option value="pending">等待连接</option><option value="sent">下发中</option><option value="applied">已应用</option><option value="failed">失败</option></select></label><span>{directoryLoading ? "正在查询…" : `共 ${directory.totalItems} 个 Agent`}</span></div>
|
|
|
|
|
{directoryError && <p className="directory-error">{directoryError}</p>}{directory.totalItems === 0 ? <p className="empty">还没有符合筛选条件的 Agent。</p> : <><div className="agent-card-list">{directory.items.map(agent => <AgentCard key={agent.id} agent={agent} onOpen={() => void openAgent(agent)}/>)}</div><div className="directory-pagination"><button className="quiet" type="button" disabled={directoryLoading || page <= 1} onClick={() => setPage(current => current - 1)}>上一页</button><span>第 {directory.page} / {directory.totalPages} 页</span><button className="quiet" type="button" disabled={directoryLoading || page >= directory.totalPages} onClick={() => setPage(current => current + 1)}>下一页</button></div></>}
|
|
|
|
|
{selectedAgent && <AgentDetailModal agent={selectedDetail?.agent ?? new controlplane.Agent(selectedAgent)} detail={selectedDetail} skills={skills} selectedSkillIDs={skillStatuses.filter(status => status.agentID === selectedAgent.id).map(status => status.skillID)} deployment={selectedDetail?.deployment} mcpTools={mcpTools} selectedToolIDs={mcpStatuses.filter(status => status.agentID === selectedAgent.id).map(status => status.toolID)} toolStatuses={mcpStatuses.filter(status => status.agentID === selectedAgent.id)} loading={loading || detailLoading} onClose={() => { setSelectedAgent(undefined); setSelectedDetail(undefined); }} onRename={onRename} onSetSkills={onSetSkills} onSetMCPTools={onSetMCPTools} onRevoke={onRevoke}/>}
|
|
|
|
|
{selectedAgent && <AgentDetailModal agent={selectedDetail?.agent ?? new controlplane.Agent(selectedAgent)} detail={selectedDetail} skills={skills} selectedSkillIDs={skillStatuses.filter(status => status.agentID === selectedAgent.id).map(status => status.skillID)} deployment={selectedDetail?.deployment} mcpTools={mcpTools} selectedToolIDs={mcpStatuses.filter(status => status.agentID === selectedAgent.id).map(status => status.toolID)} toolStatuses={mcpStatuses.filter(status => status.agentID === selectedAgent.id)} loading={loading || detailLoading} onClose={() => { setSelectedAgent(undefined); setSelectedDetail(undefined); }} onRename={onRename} onSetSkills={onSetSkills} onSetMCPTools={onSetMCPTools} onRestart={onRestart} onRevoke={onRevoke}/>}
|
|
|
|
|
</section>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -355,12 +363,13 @@ function useCloseOnEscape(onClose: () => void) {
|
|
|
|
|
}, [onClose]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function AgentDetailModal({agent, detail, skills, selectedSkillIDs, deployment, mcpTools, selectedToolIDs, toolStatuses, loading, onClose, onRename, onSetSkills, onSetMCPTools, onRevoke}: {agent: controlplane.Agent; detail?: controlplane.AgentOperationalDetail; skills: controlplane.Skill[]; selectedSkillIDs: string[]; deployment?: controlplane.AgentDirectoryItem; mcpTools: controlplane.MCPTool[]; selectedToolIDs: string[]; toolStatuses: controlplane.AgentMCPStatus[]; loading: boolean; onClose: () => void; onRename: (agentID: string, name: string) => void; onSetSkills: (agentID: string, skillIDs: string[]) => void; onSetMCPTools: (agentID: string, toolIDs: string[]) => void; onRevoke: (agent: controlplane.Agent) => void}) {
|
|
|
|
|
function AgentDetailModal({agent, detail, skills, selectedSkillIDs, deployment, mcpTools, selectedToolIDs, toolStatuses, loading, onClose, onRename, onSetSkills, onSetMCPTools, onRestart, onRevoke}: {agent: controlplane.Agent; detail?: controlplane.AgentOperationalDetail; skills: controlplane.Skill[]; selectedSkillIDs: string[]; deployment?: controlplane.AgentDirectoryItem; mcpTools: controlplane.MCPTool[]; selectedToolIDs: string[]; toolStatuses: controlplane.AgentMCPStatus[]; loading: boolean; onClose: () => void; onRename: (agentID: string, name: string) => void; onSetSkills: (agentID: string, skillIDs: string[]) => void; onSetMCPTools: (agentID: string, toolIDs: string[]) => void; onRestart: (agent: controlplane.Agent) => void; onRevoke: (agent: controlplane.Agent) => void}) {
|
|
|
|
|
useCloseOnEscape(onClose);
|
|
|
|
|
return <div className="task-modal-backdrop" role="presentation" onMouseDown={(event) => { if (event.currentTarget === event.target) onClose(); }}><section className="task-modal agent-detail-modal" role="dialog" aria-modal="true" aria-labelledby="agent-detail-title"><div className="task-modal-heading"><div><p className="eyebrow">AGENT DETAIL</p><h2 id="agent-detail-title">{agent.name}</h2></div><button className="quiet" type="button" onClick={onClose}>关闭</button></div>{loading && !detail ? <p>正在读取 Agent 运行详情…</p> : <><AgentRow agent={agent} skills={skills} selectedSkillIDs={selectedSkillIDs} deployment={deployment} mcpTools={mcpTools} selectedToolIDs={selectedToolIDs} toolStatuses={toolStatuses} loading={loading} onRename={onRename} onSetSkills={onSetSkills} onSetMCPTools={onSetMCPTools} onRevoke={onRevoke}/><AgentOperationalSections detail={detail}/></>}</section></div>;
|
|
|
|
|
const restarting = agent.restartStatus === "restarting" || agent.restartStatus === "sent";
|
|
|
|
|
return <div className="task-modal-backdrop" role="presentation" onMouseDown={(event) => { if (event.currentTarget === event.target) onClose(); }}><section className="task-modal agent-detail-modal" role="dialog" aria-modal="true" aria-labelledby="agent-detail-title"><div className="task-modal-heading"><div><p className="eyebrow">AGENT DETAIL</p><h2 id="agent-detail-title">{agent.name}</h2></div><button className="quiet" type="button" onClick={onClose}>关闭</button></div>{loading && !detail ? <p>正在读取 Agent 运行详情…</p> : <><div className="agent-actions"><button className="quiet" type="button" disabled={loading || restarting || agent.status !== "online"} title={agent.status !== "online" ? "Agent 离线,无法通过已认证连接重启" : undefined} onClick={() => onRestart(agent)}>{agent.status !== "online" ? "Agent 离线,无法重启" : restarting ? "重启中…" : "重启 Agent"}</button><span>{agent.restartStatus === "reconnected" ? "已重连" : agent.restartStatus === "failed" ? `重启失败:${agent.restartFailure || "请查看审计日志"}` : restarting ? "重启中,正在等待 Agent 重新连接…" : ""}</span></div><AgentRow agent={agent} skills={skills} selectedSkillIDs={selectedSkillIDs} deployment={deployment} mcpTools={mcpTools} selectedToolIDs={selectedToolIDs} toolStatuses={toolStatuses} loading={loading} onRename={onRename} onSetSkills={onSetSkills} onSetMCPTools={onSetMCPTools} onRestart={onRestart} onRevoke={onRevoke}/><AgentOperationalSections detail={detail}/></>}</section></div>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function AgentRow({agent, skills, selectedSkillIDs, deployment, mcpTools, selectedToolIDs, toolStatuses, loading, onRename, onSetSkills, onSetMCPTools, onRevoke}: {agent: controlplane.Agent; skills: controlplane.Skill[]; selectedSkillIDs: string[]; deployment?: controlplane.AgentDirectoryItem; mcpTools: controlplane.MCPTool[]; selectedToolIDs: string[]; toolStatuses: controlplane.AgentMCPStatus[]; loading: boolean; onRename: (agentID: string, name: string) => void; onSetSkills: (agentID: string, skillIDs: string[]) => void; onSetMCPTools: (agentID: string, toolIDs: string[]) => void; onRevoke: (agent: controlplane.Agent) => void}) {
|
|
|
|
|
function AgentRow({agent, skills, selectedSkillIDs, deployment, mcpTools, selectedToolIDs, toolStatuses, loading, onRename, onSetSkills, onSetMCPTools, onRestart, onRevoke}: {agent: controlplane.Agent; skills: controlplane.Skill[]; selectedSkillIDs: string[]; deployment?: controlplane.AgentDirectoryItem; mcpTools: controlplane.MCPTool[]; selectedToolIDs: string[]; toolStatuses: controlplane.AgentMCPStatus[]; loading: boolean; onRename: (agentID: string, name: string) => void; onSetSkills: (agentID: string, skillIDs: string[]) => void; onSetMCPTools: (agentID: string, toolIDs: string[]) => void; onRestart: (agent: controlplane.Agent) => void; onRevoke: (agent: controlplane.Agent) => void}) {
|
|
|
|
|
const [name, setName] = useState(agent.name);
|
|
|
|
|
const [selected, setSelected] = useState(selectedSkillIDs);
|
|
|
|
|
const [selectedTools, setSelectedTools] = useState(selectedToolIDs);
|
|
|
|
|