feat(training): release V0.8 自调参 Agent
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled

This commit is contained in:
2026-09-02 13:49:34 +08:00
parent cffac29a03
commit deead17a9a
47 changed files with 4986 additions and 96 deletions
@@ -1,28 +1,25 @@
import { useEffect, useState, type ReactNode } from 'react';
import { Download, Link, Play, Server, Square } from 'lucide-react';
import { Download, ExternalLink, Link, Play, Server, Square } from 'lucide-react';
import { Badge, Button, ProgressBar, PropertyRow, Select } from '../components/ui';
import { LocalTrainingClient } from './LocalTrainingClient';
import type { TrainingDevice, TrainingJob, TrainingServerInfo, WandbMode } from './types';
import type {
RewardPreset,
TrainingDevice,
TrainingJob,
TrainingServerInfo,
WandbMode,
} from './types';
import {
DEFAULT_TRAINING_ENDPOINT,
localStored,
rememberTrainingConnection,
sessionStored,
TRAINING_ENDPOINT_KEY,
TRAINING_JOB_KEY,
TRAINING_TOKEN_KEY,
} from './storage';
const ENDPOINT_KEY = 'mujoco-local-training-endpoint',
JOB_KEY = 'mujoco-local-training-job',
TOKEN_KEY = 'mujoco-local-training-token';
const DEFAULT_ENDPOINT = 'http://127.0.0.1:8765';
const ACTIVE_STATES = new Set(['queued', 'running']);
function stored(key: string, fallback = ''): string {
try {
return localStorage.getItem(key) ?? fallback;
} catch {
return fallback;
}
}
function sessionStored(key: string): string {
try {
return sessionStorage.getItem(key) ?? '';
} catch {
return '';
}
}
function errorText(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
@@ -37,10 +34,14 @@ function stateLabel(state: TrainingJob['state']): string {
}
export function LocalTrainingPanel({ onPolicyReady }: { onPolicyReady(file: File): void }) {
const [endpoint, setEndpoint] = useState(() => stored(ENDPOINT_KEY, DEFAULT_ENDPOINT));
const [token, setToken] = useState(() => sessionStored(TOKEN_KEY));
const [endpoint, setEndpoint] = useState(() =>
localStored(TRAINING_ENDPOINT_KEY, DEFAULT_TRAINING_ENDPOINT),
);
const [token, setToken] = useState(() => sessionStored(TRAINING_TOKEN_KEY));
const [server, setServer] = useState<TrainingServerInfo>();
const [job, setJob] = useState<TrainingJob>();
const [presets, setPresets] = useState<RewardPreset[]>([]);
const [rewardPresetId, setRewardPresetId] = useState('');
const [busy, setBusy] = useState(false),
[error, setError] = useState<string>();
const [taskId, setTaskId] = useState('Unitree-Go2-Flat'),
@@ -60,26 +61,30 @@ export function LocalTrainingPanel({ onPolicyReady }: { onPolicyReady(file: File
info = await client.health();
setServer(info);
try {
localStorage.setItem(ENDPOINT_KEY, client.endpoint);
sessionStorage.setItem(TOKEN_KEY, client.token);
setPresets(await client.presets());
} catch {
setPresets([]);
}
try {
rememberTrainingConnection(client.endpoint, client.token);
} catch {
/* 当前会话仍可连接 */
}
if (info.tasks.length && !info.tasks.includes(taskId)) setTaskId(info.tasks[0]);
const remembered = info.activeJobId ?? stored(JOB_KEY);
const remembered = info.activeJobId ?? localStored(TRAINING_JOB_KEY);
if (remembered) {
try {
const recovered = await client.job(remembered);
setJob(recovered);
try {
localStorage.setItem(JOB_KEY, recovered.id);
localStorage.setItem(TRAINING_JOB_KEY, recovered.id);
} catch {
/* ignore */
}
} catch {
setJob(undefined);
try {
localStorage.removeItem(JOB_KEY);
localStorage.removeItem(TRAINING_JOB_KEY);
} catch {
/* ignore */
}
@@ -116,6 +121,37 @@ export function LocalTrainingPanel({ onPolicyReady }: { onPolicyReady(file: File
};
}, [endpoint, jobId, jobState, token]);
useEffect(() => {
const receive = (event: MessageEvent) => {
if (
event.origin !== window.location.origin ||
!event.source ||
typeof event.data !== 'object'
)
return;
const data = event.data as { type?: string; sessionId?: string };
if (data.type === 'mujoco-tuning-ready') {
(event.source as Window).postMessage(
{ type: 'mujoco-tuning-credentials', endpoint, token },
event.origin,
);
}
if (data.type === 'mujoco-tuning-import-policy' && data.sessionId) {
void new LocalTrainingClient(endpoint, token)
.downloadBestPolicy(data.sessionId)
.then(onPolicyReady)
.catch((value: unknown) => setError(errorText(value)));
}
};
window.addEventListener('message', receive);
return () => window.removeEventListener('message', receive);
}, [endpoint, onPolicyReady, token]);
const openTuningDashboard = () => {
rememberTrainingConnection(endpoint, token);
window.open(new URL('tuning.html', document.baseURI), '_blank');
};
const start = async () => {
setBusy(true);
setError(undefined);
@@ -138,10 +174,11 @@ export function LocalTrainingPanel({ onPolicyReady }: { onPolicyReady(file: File
device,
gpuIds: ids,
wandbMode,
rewardPresetId: rewardPresetId || undefined,
});
setJob(next);
try {
localStorage.setItem(JOB_KEY, next.id);
localStorage.setItem(TRAINING_JOB_KEY, next.id);
} catch {
/* ignore */
}
@@ -217,6 +254,15 @@ export function LocalTrainingPanel({ onPolicyReady }: { onPolicyReady(file: File
{server?.ready ? '可用' : '离线'}
</Badge>
</div>
{server?.ready && (
<Button
className="mt-2 w-full"
icon={<ExternalLink className="h-3.5 w-3.5" />}
onClick={openTuningDashboard}
>
打开自调参 Agent 工作台
</Button>
)}
{server?.ready && !job && (
<div className="mt-3 space-y-2">
<Field label="训练任务">
@@ -286,6 +332,21 @@ export function LocalTrainingPanel({ onPolicyReady }: { onPolicyReady(file: File
/>
</Field>
</div>
<Field label="奖励配置">
<Select
aria-label="奖励配置 preset"
className="w-full"
value={rewardPresetId}
onChange={(event) => setRewardPresetId(event.target.value)}
>
<option value="">仓库默认奖励</option>
{presets.map((preset) => (
<option key={preset.id} value={preset.id}>
{preset.name}
</option>
))}
</Select>
</Field>
<Field label="实验记录">
<Select
aria-label="W&B 模式"
@@ -368,7 +429,7 @@ export function LocalTrainingPanel({ onPolicyReady }: { onPolicyReady(file: File
onClick={() => {
setJob(undefined);
try {
localStorage.removeItem(JOB_KEY);
localStorage.removeItem(TRAINING_JOB_KEY);
} catch {
/* ignore */
}