import { useEffect, useRef, useState } from 'react'; import { Button } from '../../components/ui'; import { WEBSITE_MODE } from '../../app/websiteConfig'; import { downloadBytes } from '../../project/cachedFiles'; import type { SimulationSession } from '../../simulation/SimulationSession'; import { AgentTaskController } from './AgentTaskController'; import { DecisionClient } from './DecisionClient'; import { DeterministicBaseline } from './DeterministicBaseline'; import { MockDecisionProvider } from './MockDecisionProvider'; import { ModelConnectionPanel } from './ModelConnectionPanel'; import { PICK_PLACE, TASK_GOAL_LIMITS, seedPositions, type PickPlaceTaskConfig, } from './PickPlaceScene'; export interface AgentWorkbenchBridge { loadScene(config: PickPlaceTaskConfig): Promise; getSession(): SimulationSession | undefined; isTaskScene(): boolean; publish(): void; } const states = { idle: '准备', planning: '等待 LLM 规划', deciding: '等待 Jev 判定', executing: '执行技能', paused: '任务已暂停', succeeded: '物理验收成功', failed: '失败 / 安全停止', cancelled: '已取消', }; export function AgentTaskPanel({ bridge, loading, }: { bridge: AgentWorkbenchBridge; loading: boolean; }) { const liveBridge = useRef(bridge); useEffect(() => { liveBridge.current = bridge; }, [bridge]); const task = useRef(undefined); const ownSession = useRef(undefined); const [snapshot, setSnapshot] = useState>(); const [client, setClient] = useState(); const [settingsOpen, setSettingsOpen] = useState(false); const [mode, setMode] = useState<'mock' | 'api'>('mock'); const [instruction, setInstruction] = useState( '把方块抓起,使用底盘搬运到指定支撑台,放下、松开并撤离。', ); const [goal, setGoal] = useState([...PICK_PLACE.goal.position]); const [busy, setBusy] = useState(false); const [message, setMessage] = useState('需要 A / lekiwi-v1 资产;不会自动下载或替换机器人。'); const epoch = useRef(0); const sceneDirty = useRef(true); const alive = useRef(true); function publish() { const current = task.current; if (current) setSnapshot(current.snapshot()); liveBridge.current.publish(); } function stop(reason = '用户停止任务') { epoch.current++; const current = task.current, session = ownSession.current; if (current) current.cancel(reason); if ( session && session === liveBridge.current.getSession() && session.snapshot().controlOwner === 'agent' ) { session.stopAgent(reason); session.setPaused(true); } if (alive.current) publish(); } useEffect(() => { alive.current = true; const timer = setInterval(() => { const current = task.current, session = ownSession.current; if (!current) return; setSnapshot(current.snapshot()); if ( current.terminal && session === liveBridge.current.getSession() && session?.snapshot().controlOwner === 'agent' ) { session.stopAgent('任务已结束'); session.setPaused(true); liveBridge.current.publish(); } }, 250); const invalidatePending = () => { epoch.current++; }; return () => { alive.current = false; invalidatePending(); clearInterval(timer); const session = ownSession.current; if ( session === liveBridge.current.getSession() && session?.snapshot().controlOwner === 'agent' ) { session.stopAgent('任务面板已卸载'); session.setPaused(true); } task.current?.dispose(); }; }, []); async function load() { stop('任务场景重新加载'); const generation = epoch.current; setBusy(true); try { seedPositions(0, goal); const session = await liveBridge.current.loadScene({ seed: 0, goal: [...goal] }); if (generation !== epoch.current || !alive.current) return; ownSession.current = session; task.current = undefined; sceneDirty.current = false; setSnapshot(undefined); setMessage('A 抓放场景已加载并暂停;用户地图草稿保留,但不用于本次空旷平地任务。'); } catch (error) { if (generation === epoch.current && alive.current) setMessage(error instanceof Error ? error.message : '加载失败'); } finally { if (alive.current) setBusy(false); } } async function start(single = false) { if (busy || loading) return; if (mode === 'api' && !client) { setMessage( WEBSITE_MODE ? '请在模型设置中保存 LLM 与 Jev 连接;不会自动转为演示。' : '真实模式必须先连接本机模型服务;不会转为 mock。', ); return; } if (task.current) { stop('开始新回合'); task.current.dispose(); task.current = undefined; sceneDirty.current = true; } setSnapshot(undefined); const generation = ++epoch.current; setBusy(true); try { seedPositions(0, goal); let session = liveBridge.current.getSession(); if (sceneDirty.current || task.current || !liveBridge.current.isTaskScene()) session = await liveBridge.current.loadScene({ seed: 0, goal: [...goal] }); if (generation !== epoch.current || !alive.current) return; if (!session) throw new Error('尚未加载任务场景'); const provider = mode === 'api' ? client! : new MockDecisionProvider(); session.startAgent((write) => { const skills = new DeterministicBaseline(session!, 0, write, true, goal); try { const controller = new AgentTaskController(skills, provider, instruction); if (single) controller.resume(true); task.current = controller; return controller; } catch (error) { skills.dispose(); throw error; } }); ownSession.current = session; sceneDirty.current = false; session.setPaused(false); setMessage( mode === 'mock' ? '显式 mock 决策;执行与验收为真实 MuJoCo 物理。' : '真实模型模式,可能计费;失败不降级规则。', ); publish(); } catch (error) { if (generation === epoch.current && alive.current) setMessage(error instanceof Error ? error.message : '启动失败'); } finally { if (alive.current) setBusy(false); } } function resume(single = false) { const current = task.current; if (current && !current.terminal && ownSession.current === liveBridge.current.getSession()) { current.resume(single); publish(); } else void start(single); } function changeGoal(axis: number, value: number) { stop('目标编辑使旧计划失效'); sceneDirty.current = true; setGoal((previous) => previous.map((v, i) => (i === axis ? value : v))); // Any edit requires a new scene before starting, even if values are subsequently restored. task.current?.dispose(); setMessage('目标已编辑;开始时会重新加载场景与方块,旧计划不可复用。'); } const running = Boolean( snapshot && !['succeeded', 'failed', 'cancelled', 'paused'].includes(snapshot.state), ); const locked = busy || loading; const receipts = client?.receipts.filter((r) => r.stamp.runId === snapshot?.runId) ?? []; const costs = receipts.filter((r) => r.usage.cost !== undefined); const tokens = receipts.filter((r) => ['total_tokens', 'input_tokens', 'prompt_tokens'].some((key) => r.usage[key] !== undefined), ); return (

仅仿真 · MuJoCo 结构化真值(非视觉) · A / lekiwi-v1。空旷平地单方块,不承诺障碍导航或任意目标泛化。

任务模式:{mode === 'mock' ? 'MOCK(不调用模型 API)' : '真实 LLM + Jev(可能计费)'}