diff --git a/frontend/app/(app)/operations-agents/page.tsx b/frontend/app/(app)/operations-agents/page.tsx
index f3efedbd..89432446 100644
--- a/frontend/app/(app)/operations-agents/page.tsx
+++ b/frontend/app/(app)/operations-agents/page.tsx
@@ -1,17 +1,21 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
-import Link from 'next/link'
-import { ArrowUp, Bell, Bot, CalendarClock, ChevronDown, CircleDot, Cloud, Code2, FileSearch, Pause, Play, Plus, Repeat2, Sparkles, Terminal } from 'lucide-react'
+import { ArrowUp, Bell, Bot, CalendarClock, ChevronDown, CircleDot, FileSearch, Pause, Play, Plus, Repeat2, Terminal } from 'lucide-react'
import { toast } from 'sonner'
import AgentAvatar from '@/components/smoothui/agent-avatar'
import SwitchboardCard from '@/components/smoothui/switchboard-card'
-import { useAutomations, useCreateAutomation, useGovernedWorkspaces, useInstallAutomationStarters, useOperationsAgentActivity, useOperationsAgentDraft, useOperationsAgents, useOperationsAgentVersions, usePatchAutomation, usePublishOperationsAgentVersion, useStartOperationsAgentRun, useUpdateOperationsAgentDraft } from '@/lib/api/hooks'
-import type { Automation, OperationsAgent, OperationsAgentMode } from '@/lib/api/types'
+import { useAutomations, useCreateAutomation, useCreateOperationsAgent, useGovernedWorkspaces, useInstallAutomationStarters, useNodes, useOperationsAgentActivity, useOperationsAgentDraft, useOperationsAgents, useOperationsAgentTeams, useOperationsAgentVersions, usePatchAutomation, usePublishOperationsAgentVersion, useStartAutomationRun, useStartOperationsAgentRun, useUpdateOperationsAgentDraft } from '@/lib/api/hooks'
+import type { AgentRuntimeBindingV1, Automation, OperationsAgent, OperationsAgentMode } from '@/lib/api/types'
+import { latestOperationsAgentRuns } from '@/lib/automations/activity'
+import { AUTOMATION_APPROVALS as APPROVALS } from '@/lib/automations/approval'
+import { compatibleOperationsAgents, runnableOperationsAgents } from '@/lib/automations/binding'
+import { automationExecutorMeta as executorMeta } from '@/lib/automations/executors'
+import { automationScheduleText, automationScheduleValue } from '@/lib/automations/schedule'
import { cn } from '@/lib/utils'
import { BACKEND_HINT, EmptyState, ErrorState, LoadingState } from '@/components/shell/data-states'
-import { Button, buttonVariants } from '@/components/ui/button'
+import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
@@ -53,27 +57,7 @@ type AgentStarterInput = {
executor?: string
}
-const EXECUTORS = [
- { id: 'codex', name: 'Codex', icon: Code2, color: 'text-sky-400' },
- { id: 'claude', name: 'Claude', icon: Sparkles, color: 'text-orange-400' },
- { id: 'chatcloud', name: 'ChatCloud', icon: Cloud, color: 'text-violet-400' },
- { id: 'custom', name: '自定义', icon: Terminal, color: 'text-emerald-400' },
-] as const
-
-const APPROVALS: Array<{ id: OperationsAgentMode; label: string; detail: string }> = [
- { id: 'observe_only', label: '仅观察', detail: '不提出或执行变更' },
- { id: 'suggest_changes', label: '建议需批准', detail: '送入 Inbox 后由人决定' },
- { id: 'low_risk_automatic', label: '低风险自动', detail: '白名单外仍需批准' },
-]
-
-function executorMeta(id: string) {
- return EXECUTORS.find((item) => item.id === id) ?? EXECUTORS[3]
-}
-function scheduleText(value: string) {
- const [kind, time] = value.split('@')
- return `${kind === 'daily' ? '每天' : kind === 'weekdays' ? '工作日' : kind === 'weekly' ? '每周' : kind}${time ? ` ${time}` : ''}`
-}
const EMPTY_SCHEMA = { type: 'object', properties: {} }
@@ -87,16 +71,26 @@ function ContractEditor({ workspaceId, agent }: { workspaceId: string; agent: Op
const draft = useOperationsAgentDraft(workspaceId, agent.id)
const versions = useOperationsAgentVersions(workspaceId, agent.id)
const updateDraft = useUpdateOperationsAgentDraft()
+ const nodes = useNodes()
const publishVersion = usePublishOperationsAgentVersion()
const [instructions, setInstructions] = useState('')
const [inputSchema, setInputSchema] = useState('')
const [outputSchema, setOutputSchema] = useState('')
const [stateSchema, setStateSchema] = useState('')
const [agentUrl, setAgentUrl] = useState('')
+ const [runtime, setRuntime] = useState
('codex')
const [workflow, setWorkflow] = useState('')
const [dispatchTimeout, setDispatchTimeout] = useState(1800)
const [runtimeConfig, setRuntimeConfig] = useState('')
const [reason, setReason] = useState('')
+ const runtimeNodes = useMemo(
+ () => [...(nodes.data?.data ?? [])].reverse().filter(
+ (node) => node.protocol === 'ws' && node.status === 'online' && node.runtimes?.length,
+ ),
+ [nodes.data],
+ )
+ const selectedRuntimeNode = runtimeNodes.find((node) => node.url === agentUrl)
+ const runtimeOptions = selectedRuntimeNode?.runtimes ?? []
useEffect(() => {
if (!draft.data) return
@@ -107,11 +101,31 @@ function ContractEditor({ workspaceId, agent }: { workspaceId: string; agent: Op
setOutputSchema(JSON.stringify(contract?.output_schema ?? EMPTY_SCHEMA, null, 2))
setStateSchema(JSON.stringify(contract?.state_schema ?? EMPTY_SCHEMA, null, 2))
setAgentUrl(binding?.agent_url ?? '')
+ setRuntime(binding?.runtime ?? 'codex')
setWorkflow(binding?.workflow ?? '')
setDispatchTimeout(binding?.dispatch_timeout_seconds ?? 1800)
setRuntimeConfig(JSON.stringify(binding?.config ?? { timeout_seconds: 1800 }, null, 2))
}, [draft.data])
+ useEffect(() => {
+ if (agentUrl || !runtimeNodes.length) return
+ const node = runtimeNodes[0]
+ const firstRuntime = node.runtimes?.[0]
+ setAgentUrl(node.url)
+ if (firstRuntime) {
+ setRuntime(firstRuntime)
+ if (!workflow) {
+ setWorkflow(
+ firstRuntime === 'miniflow'
+ ? 'builtin.read_only_readiness'
+ : firstRuntime === 'codex'
+ ? 'exec'
+ : 'default',
+ )
+ }
+ }
+ }, [agentUrl, runtimeNodes, workflow])
+
async function saveDraft() {
if (!draft.data) return
try {
@@ -132,7 +146,7 @@ function ContractEditor({ workspaceId, agent }: { workspaceId: string; agent: Op
runtime_binding: {
schema_version: 'agent.runtime-binding.v1',
agent_url: agentUrl.trim(),
- runtime: 'pi',
+ runtime,
workflow: workflow.trim(),
dispatch_timeout_seconds: dispatchTimeout,
config: parseJsonObject(runtimeConfig, 'Runtime config'),
@@ -172,7 +186,7 @@ function ContractEditor({ workspaceId, agent }: { workspaceId: string; agent: Op
Agent Contract
Draft r{draft.data?.revision ?? '—'} · 当前发布 v{currentPublishedVersion || '—'}
-
+
@@ -212,10 +227,13 @@ export default function OperationsAgentsPage() {
const [view, setView] = useState<'automations' | 'agents'>('automations')
const automations = useAutomations(workspaceId)
const agents = useOperationsAgents(workspaceId)
+ const teams = useOperationsAgentTeams(workspaceId)
const activity = useOperationsAgentActivity(workspaceId)
const installStarterPack = useInstallAutomationStarters()
+ const createOperationsAgent = useCreateOperationsAgent()
const createAutomation = useCreateAutomation()
const patchAutomation = usePatchAutomation()
+ const startAutomationRun = useStartAutomationRun()
const startRunMutation = useStartOperationsAgentRun()
const [open, setOpen] = useState(false)
const [selectedAgent, setSelectedAgent] = useState
(null)
@@ -224,7 +242,7 @@ export default function OperationsAgentsPage() {
const [name, setName] = useState('')
const [prompt, setPrompt] = useState('')
const [precheck, setPrecheck] = useState('')
- const [executor, setExecutor] = useState('codex')
+ const [automationAgentId, setAutomationAgentId] = useState('')
const [projectPath, setProjectPath] = useState('')
const [branch, setBranch] = useState('main')
const [scheduleKind, setScheduleKind] = useState('weekdays')
@@ -235,11 +253,47 @@ export default function OperationsAgentsPage() {
const [runInput, setRunInput] = useState('{}')
const [runState, setRunState] = useState('{}')
const [runTargetType, setRunTargetType] = useState('manual')
- const latestRun = useMemo(() => new Map(activity.data?.map((run) => [run.operations_agent_id, run]) ?? []), [activity.data])
+ const [agentCreateOpen, setAgentCreateOpen] = useState(false)
+ const [agentName, setAgentName] = useState('')
+ const [agentDescription, setAgentDescription] = useState('')
+ const [owningTeamId, setOwningTeamId] = useState('')
+ const [automationToBind, setAutomationToBind] = useState(null)
+ const [automationToRun, setAutomationToRun] = useState(null)
+ const [bindingAgentId, setBindingAgentId] = useState('')
+ const latestRun = useMemo(
+ () => latestOperationsAgentRuns(activity.data ?? []),
+ [activity.data],
+ )
+ const runnableAgents = useMemo(
+ () => runnableOperationsAgents(agents.data ?? []),
+ [agents.data],
+ )
+ const compatibleAgents = useMemo(
+ () => compatibleOperationsAgents(agents.data ?? [], approvalMode),
+ [agents.data, approvalMode],
+ )
+
+ useEffect(() => {
+ if (!workspaceId && workspaces.data?.length) {
+ setWorkspaceId(workspaces.data[0].id)
+ }
+ }, [workspaceId, workspaces.data])
+
+ useEffect(() => {
+ if (agentCreateOpen && teams.data?.length === 1 && !owningTeamId) {
+ setOwningTeamId(teams.data[0].id)
+ }
+ }, [agentCreateOpen, owningTeamId, teams.data])
+
+ useEffect(() => {
+ if (!open) return
+ const selected = compatibleAgents.find((agent) => agent.id === automationAgentId)
+ if (!selected) setAutomationAgentId(compatibleAgents[0]?.id ?? '')
+ }, [automationAgentId, compatibleAgents, open])
function startCreate(preset?: AgentStarterInput) {
setName(preset?.name ?? '')
setPrompt(preset?.prompt ?? '')
- setExecutor(preset?.executor ?? 'codex')
+ setAutomationAgentId('')
if (preset) {
const [kind, presetTime] = preset.schedule.split('@')
setScheduleKind(kind)
@@ -252,7 +306,7 @@ export default function OperationsAgentsPage() {
if (!workspaceId || installStarterPack.isPending) return
try {
const result = await installStarterPack.mutateAsync({ workspaceId })
- toast.success(`已支起 ${result.created_count} 个 Agent Starter,跳过 ${result.skipped_count} 个`)
+ toast.success(`已安装 ${result.created_count} 个自动化模板,跳过 ${result.skipped_count} 个`)
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Agent Starter 创建失败')
}
@@ -266,17 +320,49 @@ export default function OperationsAgentsPage() {
setOpen(true)
}
+ function startAgentCreate(starter?: AgentStarterInput) {
+ setAgentName(starter?.name ?? '')
+ setAgentDescription(starter?.prompt ?? '')
+ setOwningTeamId(teams.data?.length === 1 ? teams.data[0].id : '')
+ setAgentCreateOpen(true)
+ }
+
+ async function submitAgentCreate() {
+ if (!workspaceId || !agentName.trim() || !owningTeamId) return
+ try {
+ const agent = await createOperationsAgent.mutateAsync({
+ workspaceId,
+ data: {
+ name: agentName.trim(),
+ description: agentDescription.trim() || null,
+ owning_team_id: owningTeamId,
+ },
+ })
+ setAgentCreateOpen(false)
+ setView('agents')
+ setSelectedAgent(agent)
+ setAgentDetailView('contract')
+ toast.success('Operations Agent 已创建;请确认 Runtime Contract 后发布')
+ } catch (error) {
+ toast.error(error instanceof Error ? error.message : 'Operations Agent 创建失败')
+ }
+ }
+
async function submitCreate() {
if (!workspaceId) return
+ const boundAgent = compatibleAgents.find((agent) => agent.id === automationAgentId)
+ if (!boundAgent?.current_published_version) return
try {
await createAutomation.mutateAsync({ workspaceId, data: {
+ operations_agent_id: boundAgent.id,
+ operations_agent_version: boundAgent.current_published_version,
name: name.trim(), prompt: prompt.trim(), precheck: precheck.trim() || null,
- executor, schedule: `${scheduleKind}@${time}`, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
- session_mode: sessionMode, approval_mode: approvalMode,
+ executor: 'operations-agent', schedule: automationScheduleValue(scheduleKind, time), timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+ session_mode: sessionMode, approval_mode: boundAgent.current_profile.mode,
project: { path: projectPath.trim() || null, branch: branch.trim() || null }, enabled: true,
} })
setOpen(false)
- toast.success('自动化已创建')
+ toast.success('自动化已创建并绑定已发布智能体')
} catch (error) {
toast.error(error instanceof Error ? error.message : '创建失败')
}
@@ -292,6 +378,60 @@ export default function OperationsAgentsPage() {
}
}
+ function openAutomationRun(automation: Automation) {
+ setAutomationToRun(automation)
+ }
+
+ async function runAutomationNow() {
+ if (!workspaceId || !automationToRun) return
+ try {
+ const run = await startAutomationRun.mutateAsync({
+ workspaceId,
+ automationId: automationToRun.id,
+ })
+ const agent = agents.data?.find(
+ (candidate) => candidate.id === automationToRun.operations_agent_id,
+ )
+ setAutomationToRun(null)
+ if (agent) {
+ setSelectedAgent(agent)
+ setAgentDetailView('activity')
+ setView('agents')
+ }
+ toast.success(`Automation Run ${run.id} 已提交`)
+ } catch (error) {
+ toast.error(error instanceof Error ? error.message : 'Automation Run 启动失败')
+ }
+ }
+
+ function openAutomationBinding(automation: Automation) {
+ setAutomationToBind(automation)
+ setBindingAgentId(automation.operations_agent_id ?? runnableAgents[0]?.id ?? '')
+ }
+
+ async function bindAutomation() {
+ if (!workspaceId || !automationToBind) return
+ const agent = runnableAgents.find((candidate) => candidate.id === bindingAgentId)
+ if (!agent?.current_published_version) return
+ try {
+ await patchAutomation.mutateAsync({
+ workspaceId,
+ automationId: automationToBind.id,
+ data: {
+ operations_agent_id: agent.id,
+ operations_agent_version: agent.current_published_version,
+ approval_mode: agent.current_profile.mode,
+ executor: 'operations-agent',
+ enabled: true,
+ },
+ })
+ setAutomationToBind(null)
+ toast.success(`已绑定 ${agent.name} v${agent.current_published_version}`)
+ } catch (error) {
+ toast.error(error instanceof Error ? error.message : '智能体绑定失败')
+ }
+ }
+
async function startRun() {
if (!workspaceId || !selectedAgent || !runTargetType.trim() || !runTargetId.trim()) return
try {
@@ -326,20 +466,20 @@ export default function OperationsAgentsPage() {
-
-
-
+
+
+
SmoothUI / Agent starters
-
先把这三个 Agent 支起来
-
三套可直接创建的自动化模板;创建后会进入我的自动化并按日程执行。
+
添加 Operations Agent
+
Starter 卡片创建真实 Agent 身份;随后确认 Runtime Contract 并发布即可运行。
-
@@ -352,7 +492,7 @@ export default function OperationsAgentsPage() {
rows={5}
gridPattern={[...starter.pattern]}
className="h-[260px] p-4"
- onButtonClick={() => startCreate(starter)}
+ onButtonClick={() => startAgentCreate(starter)}
/>
))}
@@ -368,17 +508,53 @@ export default function OperationsAgentsPage() {
- {SUGGESTIONS.map((item) => { const Icon = item.icon; return
startCreate(item)} className="group flex w-full items-start gap-4 rounded-xl px-3 py-3 text-left transition-colors hover:bg-white/[0.04]">{item.name} {scheduleText(item.schedule)}{item.prompt} })}
+ {SUGGESTIONS.map((item) => { const Icon = item.icon; return
startCreate(item)} className="group flex w-full items-start gap-4 rounded-xl px-3 py-3 text-left transition-colors hover:bg-white/[0.04]">{item.name} {automationScheduleText(item.schedule)}{item.prompt} })}
我的自动化
startCreate()}>手动配置
- {automations.isLoading ? : automations.isError ? : !automations.data?.length ? : {automations.data.map((automation) => { const meta = executorMeta(automation.executor); const Icon = meta.icon; return
{automation.name}{automation.enabled ? '等待首次运行' : '已暂停'}{scheduleText(automation.schedule)}{meta.name}
{automation.prompt}
void toggleAutomation(automation)}>{automation.enabled ? : } })}
}
+ {automations.isLoading ? (
+
+ ) : automations.isError ? (
+
+ ) : !automations.data?.length ? (
+
+ ) : (
+
+ {automations.data.map((automation) => {
+ const meta = executorMeta(automation.executor)
+ const Icon = meta.icon
+ const boundAgent = agents.data?.find(
+ (agent) => agent.id === automation.operations_agent_id,
+ )
+ return (
+
+
+
+
+ {automation.name}
+ {automation.enabled ? '已启用' : '已暂停'}
+ {automationScheduleText(automation.schedule)}
+ {meta.name}
+
+
{automation.prompt}
+
{boundAgent ? `绑定 ${boundAgent.name} · Agent v${automation.operations_agent_version} · Automation r${automation.revision}` : '尚未绑定已发布 Operations Agent;调度保持暂停'}
+
+
+
openAutomationBinding(automation)}>{boundAgent ? '更换绑定' : '绑定智能体'}
+
openAutomationRun(automation)}>立即运行
+
void toggleAutomation(automation)}>{automation.enabled ? : }
+
+
+ )
+ })}
+
+ )}
)}
@@ -390,13 +566,66 @@ export default function OperationsAgentsPage() {
@@ -405,7 +634,7 @@ export default function OperationsAgentsPage() {
{selectedAgent ? <>