import { useEffect, useMemo, useState } from "react"; import { AlertCircle, Bot, CheckCircle2, Loader2, Play, RefreshCcw, ShieldAlert, Wand2, } from "lucide-react"; import { aiStewardApi, type OpenClawCommandDispatchResponse } from "@/api/aiSteward"; import { projectsApi, type ProjectSummary } from "@/api/projects"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { formatProjectNameWithId, toZhStatus } from "@/lib/display"; const EXAMPLE_COMMANDS = [ "把当前未分配的高优先级任务分给最合适的人,并通知相关成员。", "根据当前负载重新平衡本项目任务,先 dry run 给出调整方案。", "检查 blocked 任务,能恢复推进的直接切状态,不能恢复的通知负责人补充信息。", ]; const MANAGER_ROLES = new Set(["BOSS", "ADMIN"]); function resultTone(status: string): string { const key = String(status || "").trim().toLowerCase(); if (key === "applied") return "bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-300 dark:border-green-800"; if (key === "planned") return "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-300 dark:border-blue-800"; if (key === "skipped") return "bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-900/20 dark:text-amber-300 dark:border-amber-800"; return "bg-red-50 text-red-700 border-red-200 dark:bg-red-900/20 dark:text-red-300 dark:border-red-800"; } function renderOperationLabel(operationCode: string): string { const code = String(operationCode || "").trim().toLowerCase(); if (code === "task.create") return "任务创建"; if (code === "task.assign") return "任务分配"; if (code === "task.prioritize") return "优先级调整"; if (code === "task.status_transition") return "状态迁移"; if (code === "notification.create") return "通知创建"; if (code === "personnel.workload_update") return "工时容量调整"; return operationCode || "-"; } interface OpenClawCommanderPanelProps { currentUserRole?: string | null; } export function OpenClawCommanderPanel({ currentUserRole }: OpenClawCommanderPanelProps) { const [projects, setProjects] = useState([]); const [projectLoading, setProjectLoading] = useState(false); const [projectId, setProjectId] = useState(""); const [instruction, setInstruction] = useState(""); const [dryRun, setDryRun] = useState(true); const [maxOperations, setMaxOperations] = useState("6"); const [dispatching, setDispatching] = useState(false); const [error, setError] = useState(null); const [result, setResult] = useState(null); const canDispatch = MANAGER_ROLES.has(String(currentUserRole || "").trim().toUpperCase()); const selectedProject = useMemo( () => projects.find((item) => String(item.id) === String(projectId)) || null, [projectId, projects] ); const loadProjects = async () => { setProjectLoading(true); try { const payload = await projectsApi.getSummary(); const items = payload.projects || []; setProjects(items); if (!projectId && items[0]?.id) { setProjectId(String(items[0].id)); } } catch (loadError) { console.error("load commander projects failed", loadError); setError("加载项目列表失败"); } finally { setProjectLoading(false); } }; useEffect(() => { if (!canDispatch) { return; } void loadProjects(); }, [canDispatch]); const handleDispatch = async () => { const parsedProjectId = Number(projectId); const parsedMaxOperations = Number(maxOperations); if (!Number.isInteger(parsedProjectId) || parsedProjectId <= 0) { setError("请选择有效项目"); return; } if (!instruction.trim()) { setError("请输入调度指令"); return; } if (!Number.isInteger(parsedMaxOperations) || parsedMaxOperations < 1 || parsedMaxOperations > 20) { setError("max operations 仅支持 1-20"); return; } setDispatching(true); setError(null); try { const payload = await aiStewardApi.dispatchCommand({ project_id: parsedProjectId, instruction: instruction.trim(), dry_run: dryRun, max_operations: parsedMaxOperations, }); setResult(payload); if (!dryRun) { void loadProjects(); } } catch (dispatchError: any) { const text = typeof dispatchError?.message === "string" ? dispatchError.message : "指挥调度失败"; setError(text); } finally { setDispatching(false); } }; if (!canDispatch) { return (
Commander 仅开放给管理角色
当前接口要求 `BOSS / ADMIN`,普通成员仍可通过任务看板或 AI 问答拿建议,但不能直接下发执行。
); } return (
OpenClaw Commander 直接下达自然语言指令,让指挥层规划并驱动 AI 管家执行。
{projects.length === 0 ? (
{projectLoading ? "正在加载项目..." : "暂无可调度项目"}
) : ( projects.map((project) => { const active = String(project.id) === String(projectId); return ( ); }) )}
setProjectId(event.target.value)} placeholder="例如 1" />
setMaxOperations(event.target.value)} inputMode="numeric" placeholder="6" />
{selectedProject ? (
{formatProjectNameWithId(selectedProject)}
Active {selectedProject.in_progress_tasks} / Pending {selectedProject.pending_tasks} / Blocked 由执行结果返回。
) : null}