feat: add first-run runtime initialization
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user