This commit is contained in:
toot
2026-09-16 18:11:48 +08:00
parent 9e01cbbec0
commit ada97a7497
27 changed files with 661 additions and 45 deletions
+16 -7
View File
@@ -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);
+2
View File
@@ -55,6 +55,8 @@ export function RecentTasks():Promise<Array<controlplane.Task>>;
export function RenameAgent(arg1:string,arg2:string):Promise<void>;
export function RestartAgent(arg1:string):Promise<controlplane.AgentRestart>;
export function RetryTask(arg1:string):Promise<string>;
export function RevokeAgent(arg1:string):Promise<void>;
@@ -106,6 +106,10 @@ export function RenameAgent(arg1, arg2) {
return window['go']['main']['App']['RenameAgent'](arg1, arg2);
}
export function RestartAgent(arg1) {
return window['go']['main']['App']['RestartAgent'](arg1);
}
export function RetryTask(arg1) {
return window['go']['main']['App']['RetryTask'](arg1);
}
+30
View File
@@ -5,6 +5,8 @@ export namespace controlplane {
name: string;
status: string;
lastSeenAt: string;
restartStatus: string;
restartFailure: string;
static createFrom(source: any = {}) {
return new Agent(source);
@@ -16,6 +18,8 @@ export namespace controlplane {
this.name = source["name"];
this.status = source["status"];
this.lastSeenAt = source["lastSeenAt"];
this.restartStatus = source["restartStatus"];
this.restartFailure = source["restartFailure"];
}
}
export class AgentDirectoryFilter {
@@ -57,6 +61,8 @@ export namespace controlplane {
mcpStatus: string;
mcpFailure: string;
lastDeploymentAt: string;
restartStatus: string;
restartFailure: string;
static createFrom(source: any = {}) {
return new AgentDirectoryItem(source);
@@ -78,6 +84,8 @@ export namespace controlplane {
this.mcpStatus = source["mcpStatus"];
this.mcpFailure = source["mcpFailure"];
this.lastDeploymentAt = source["lastDeploymentAt"];
this.restartStatus = source["restartStatus"];
this.restartFailure = source["restartFailure"];
}
}
export class AgentDirectoryPage {
@@ -310,6 +318,28 @@ export namespace controlplane {
return a;
}
}
export class AgentRestart {
requestID: string;
agentID: string;
status: string;
requestedAt: string;
confirmedAt: string;
failure: string;
static createFrom(source: any = {}) {
return new AgentRestart(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.requestID = source["requestID"];
this.agentID = source["agentID"];
this.status = source["status"];
this.requestedAt = source["requestedAt"];
this.confirmedAt = source["confirmedAt"];
this.failure = source["failure"];
}
}
export class AgentSkillStatus {
agentID: string;
agentName: string;