feat: add first-run runtime initialization

This commit is contained in:
inman
2026-04-29 17:24:37 +08:00
parent 3b252250cd
commit cddaf37016
14 changed files with 432 additions and 80 deletions

View File

@@ -97,7 +97,9 @@ export function Settings() {
const updateDesktopUserName = useYinianStore((state) => state.updateDesktopUserName);
const updateWorkspaceDisplayName = useYinianStore((state) => state.updateWorkspaceDisplayName);
const currentVersion = useUpdateStore((state) => state.currentVersion);
const updateStatus = useUpdateStore((state) => state.status);
const updateSetAutoDownload = useUpdateStore((state) => state.setAutoDownload);
const updatesDisabled = updateStatus === 'disabled';
const [controlUiInfo, setControlUiInfo] = useState<ControlUiInfo | null>(null);
const [openclawCliCommand, setOpenclawCliCommand] = useState('');
const [openclawCliError, setOpenclawCliError] = useState<string | null>(null);
@@ -690,6 +692,7 @@ export function Settings() {
</div>
<Switch
checked={autoCheckUpdate}
disabled={updatesDisabled}
onCheckedChange={setAutoCheckUpdate}
/>
</div>
@@ -703,6 +706,7 @@ export function Settings() {
</div>
<Switch
checked={autoDownloadUpdate}
disabled={updatesDisabled}
onCheckedChange={(value) => {
setAutoDownloadUpdate(value);
updateSetAutoDownload(value);

View File

@@ -37,9 +37,8 @@ interface SetupStep {
const STEP = {
WELCOME: 0,
RUNTIME: 1,
INSTALLING: 2,
COMPLETE: 3,
INSTALLING: 1,
COMPLETE: 2,
} as const;
const getSteps = (t: TFunction): SetupStep[] => [
@@ -48,11 +47,6 @@ const getSteps = (t: TFunction): SetupStep[] => [
title: t('steps.welcome.title'),
description: t('steps.welcome.description'),
},
{
id: 'runtime',
title: t('steps.runtime.title'),
description: t('steps.runtime.description'),
},
{
id: 'installing',
title: t('steps.installing.title'),
@@ -88,14 +82,15 @@ import zhinianLogo from '@/assets/logo.svg';
export function Setup() {
const { t } = useTranslation(['setup', 'channels']);
const navigate = useNavigate();
const rendererQuery = typeof window !== 'undefined'
? new URLSearchParams(window.location.search)
: new URLSearchParams();
const isE2EMode = rendererQuery.get('e2e') === '1';
const [currentStep, setCurrentStep] = useState<number>(STEP.WELCOME);
// Setup state
// Installation state for the Installing step
const [installedSkills, setInstalledSkills] = useState<string[]>([]);
// Runtime check status
const [runtimeChecksPassed, setRuntimeChecksPassed] = useState(false);
const steps = getSteps(t);
const safeStepIndex = Number.isInteger(currentStep)
? Math.min(Math.max(currentStep, STEP.WELCOME), steps.length - 1)
@@ -111,8 +106,6 @@ export function Setup() {
switch (safeStepIndex) {
case STEP.WELCOME:
return true;
case STEP.RUNTIME:
return runtimeChecksPassed;
case STEP.INSTALLING:
return false; // Cannot manually proceed, auto-proceeds when done
case STEP.COMPLETE:
@@ -120,14 +113,14 @@ export function Setup() {
default:
return true;
}
}, [safeStepIndex, runtimeChecksPassed]);
}, [safeStepIndex]);
const handleNext = async () => {
if (isLastStep) {
// Complete setup
markSetupComplete();
toast.success(t('complete.title'));
navigate('/');
navigate('/login', { replace: true });
} else {
setCurrentStep((i) => i + 1);
}
@@ -139,7 +132,7 @@ export function Setup() {
const handleSkip = () => {
markSetupComplete();
navigate('/');
navigate('/login', { replace: true });
};
// Auto-proceed when installation is complete
@@ -207,7 +200,6 @@ export function Setup() {
{/* Step-specific content */}
<div className="rounded-xl bg-card text-card-foreground border shadow-sm p-8 mb-8">
{safeStepIndex === STEP.WELCOME && <WelcomeContent />}
{safeStepIndex === STEP.RUNTIME && <RuntimeContent onStatusChange={setRuntimeChecksPassed} />}
{safeStepIndex === STEP.INSTALLING && (
<InstallingContent
skills={getDefaultSkills(t)}
@@ -234,7 +226,7 @@ export function Setup() {
)}
</div>
<div className="flex gap-2">
{!isLastStep && safeStepIndex !== STEP.RUNTIME && (
{isE2EMode && !isLastStep && (
<Button data-testid="setup-skip-button" variant="ghost" onClick={handleSkip}>
{t('nav.skipSetup')}
</Button>
@@ -638,11 +630,12 @@ function RuntimeContent({ onStatusChange }: RuntimeContentProps) {
</div>
);
}
void RuntimeContent;
// NOTE: ProviderContent component removed - configure providers via Settings > AI Providers
// Installation status for each skill
// Initialization status for each first-run task
type InstallStatus = 'pending' | 'installing' | 'completed' | 'failed';
interface SkillInstallState {
@@ -650,6 +643,7 @@ interface SkillInstallState {
name: string;
description: string;
status: InstallStatus;
message?: string;
}
interface InstallingContentProps {
@@ -658,10 +652,16 @@ interface InstallingContentProps {
onSkip: () => void;
}
function InstallingContent({ skills, onComplete, onSkip }: InstallingContentProps) {
function InstallingContent({ skills: _skills, onComplete, onSkip }: InstallingContentProps) {
const { t } = useTranslation('setup');
const canSkip = new URLSearchParams(window.location.search).get('e2e') === '1';
const [skillStates, setSkillStates] = useState<SkillInstallState[]>(
skills.map((s) => ({ ...s, status: 'pending' as InstallStatus }))
[
{ id: 'runtime', name: '安装 OpenClaw', description: '重装内置运行环境,避免客户本机残留版本影响启动', status: 'pending' as InstallStatus },
{ id: 'workspace', name: '准备本地工作区', description: '创建智念助手所需的本地工作区与运行目录', status: 'pending' as InstallStatus },
{ id: 'model', name: '写入内测模型', description: '使用当前开发环境的默认模型配置', status: 'pending' as InstallStatus },
{ id: 'python', name: '准备文档能力', description: '准备知识库与文档处理所需的本地环境标记', status: 'pending' as InstallStatus },
]
);
const [overallProgress, setOverallProgress] = useState(0);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
@@ -674,36 +674,58 @@ function InstallingContent({ skills, onComplete, onSkip }: InstallingContentProp
const runRealInstall = async () => {
try {
// Step 1: Initialize all skills to 'installing' state for UI
setSkillStates(prev => prev.map(s => ({ ...s, status: 'installing' })));
setSkillStates(prev => prev.map((s, index) => ({ ...s, status: index === 0 ? 'installing' : 'pending' })));
setOverallProgress(10);
// Step 2: Call the backend to install uv and setup Python
const result = await invokeIpc('uv:install-all') as {
success: boolean;
error?: string
const result = await invokeIpc('yinian:setup:initialize') as {
initialized: boolean;
openclawDir?: string;
model?: string;
steps?: Array<{
id: string;
label: string;
status: 'pending' | 'running' | 'success' | 'error';
message?: string;
}>;
};
if (result.success) {
setSkillStates(prev => prev.map(s => ({ ...s, status: 'completed' })));
const mappedSteps = (result.steps ?? []).map((step) => ({
id: step.id,
name: step.label,
description: step.message || '',
status: step.status === 'success'
? 'completed' as const
: step.status === 'error'
? 'failed' as const
: step.status === 'running'
? 'installing' as const
: 'pending' as const,
message: step.message,
}));
if (mappedSteps.length > 0) {
setSkillStates(mappedSteps);
}
if (result.initialized) {
setOverallProgress(100);
await new Promise((resolve) => setTimeout(resolve, 800));
onComplete(skills.map(s => s.id));
onComplete((result.steps ?? []).map((s) => s.id));
} else {
setSkillStates(prev => prev.map(s => ({ ...s, status: 'failed' })));
setErrorMessage(result.error || 'Unknown error during installation');
toast.error('Environment setup failed');
setSkillStates(prev => prev.map(s => s.status === 'completed' ? s : { ...s, status: 'failed' }));
setErrorMessage('初始化未完成,请查看失败项后重试。');
toast.error('初始化失败');
}
} catch (err) {
setSkillStates(prev => prev.map(s => ({ ...s, status: 'failed' })));
setErrorMessage(String(err));
toast.error('Installation error');
toast.error('初始化失败');
}
};
runRealInstall();
}, [skills, onComplete]);
}, [onComplete]);
const getStatusIcon = (status: InstallStatus) => {
switch (status) {
@@ -773,7 +795,7 @@ function InstallingContent({ skills, onComplete, onSkip }: InstallingContentProp
{getStatusIcon(skill.status)}
<div>
<p className="font-medium">{skill.name}</p>
<p className="text-xs text-muted-foreground">{skill.description}</p>
<p className="text-xs text-muted-foreground">{skill.message || skill.description}</p>
</div>
</div>
{getStatusText(skill)}
@@ -809,18 +831,20 @@ function InstallingContent({ skills, onComplete, onSkip }: InstallingContentProp
{!errorMessage && (
<p className="text-sm text-slate-400 text-center">
{t('installing.wait')}
</p>
)}
<div className="flex justify-end">
<Button
variant="ghost"
className="text-muted-foreground"
onClick={onSkip}
>
{t('installing.skip')}
</Button>
</div>
{canSkip && (
<div className="flex justify-end">
<Button
variant="ghost"
className="text-muted-foreground"
onClick={onSkip}
>
{t('installing.skip')}
</Button>
</div>
)}
</div>
);
}
@@ -830,33 +854,25 @@ interface CompleteContentProps {
function CompleteContent({ installedSkills }: CompleteContentProps) {
const { t } = useTranslation(['setup', 'settings']);
const gatewayStatus = useGatewayStore((state) => state.status);
const installedSkillNames = getDefaultSkills(t)
.filter((s: DefaultSkill) => installedSkills.includes(s.id))
.map((s: DefaultSkill) => s.name)
.join(', ');
return (
<div className="text-center space-y-6">
<div className="text-6xl mb-4">🎉</div>
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-emerald-500/10">
<CheckCircle2 className="h-8 w-8 text-emerald-500" />
</div>
<h2 className="text-xl font-semibold">{t('complete.title')}</h2>
<p className="text-muted-foreground">
{t('complete.subtitle')}
</p>
<div className="space-y-3 text-left max-w-md mx-auto">
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
<span>{t('complete.components')}</span>
<span className="text-green-400">
{installedSkillNames || `${installedSkills.length} ${t('installing.status.installed')}`}
</span>
<span></span>
<span className="text-green-400">{installedSkills.length || 4} </span>
</div>
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
<span>{t('complete.gateway')}</span>
<span className={gatewayStatus.state === 'running' ? 'text-green-400' : 'text-yellow-400'}>
{gatewayStatus.state === 'running' ? `${t('complete.running')}` : gatewayStatus.state}
</span>
<span></span>
<span className="text-green-400">MiniMax M2.7</span>
</div>
</div>