import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { homedir } from 'node:os'; import { join } from 'node:path'; import { existsSync } from 'node:fs'; import { getAllSettings, setSetting } from './store'; import { getOpenClawConfigDir, reinstallManagedOpenClawRuntime } from './paths'; import { logger } from './logger'; type JsonObject = Record; export type YinianInitializationStepStatus = 'pending' | 'running' | 'success' | 'error'; export interface YinianInitializationStep { id: 'runtime' | 'workspace' | 'model' | 'python'; label: string; status: YinianInitializationStepStatus; message?: string; } export interface YinianInitializationStatus { initialized: boolean; initializedAt?: number; openclawDir?: string; model?: string; steps: YinianInitializationStep[]; } const INTERNAL_PROVIDER_KEY = 'minimax'; const INTERNAL_MODEL_ID = 'MiniMax-M2.7'; const INTERNAL_MODEL_REF = `${INTERNAL_PROVIDER_KEY}/${INTERNAL_MODEL_ID}`; let initializationInFlight: Promise | null = null; const DEFAULT_STEPS: YinianInitializationStep[] = [ { id: 'runtime', label: '安装运行环境', status: 'pending' }, { id: 'workspace', label: '准备本地工作区', status: 'pending' }, { id: 'model', label: '写入内测模型配置', status: 'pending' }, { id: 'python', label: '准备文档处理环境', status: 'pending' }, ]; export async function getYinianInitializationStatus(): Promise { const settings = await getAllSettings(); const initialized = settings.setupComplete === true; return { initialized, initializedAt: settings.openclawInitializedAt, openclawDir: initialized ? join(getOpenClawConfigDir(), 'runtime', 'openclaw') : undefined, model: initialized ? INTERNAL_MODEL_REF : undefined, steps: DEFAULT_STEPS.map((step) => ({ ...step, status: initialized ? 'success' : 'pending', message: initialized ? '已完成' : undefined, })), }; } export async function initializeYinianRuntime(): Promise { if (initializationInFlight) return initializationInFlight; initializationInFlight = runYinianRuntimeInitialization(); try { return await initializationInFlight; } finally { initializationInFlight = null; } } async function runYinianRuntimeInitialization(): Promise { const steps = DEFAULT_STEPS.map((step) => ({ ...step })); const setStep = ( id: YinianInitializationStep['id'], status: YinianInitializationStepStatus, message?: string, ) => { const step = steps.find((item) => item.id === id); if (step) { step.status = status; step.message = message; } }; try { setStep('runtime', 'running', '正在重装内置运行环境'); const runtime = reinstallManagedOpenClawRuntime(); if (runtime.source === 'missing') { throw new Error('内置 OpenClaw 运行环境不可用'); } setStep('runtime', 'success', runtime.dir); setStep('workspace', 'running', '正在创建本地工作区'); await ensureWorkspaceFiles(); setStep('workspace', 'success', '本地工作区已准备'); setStep('model', 'running', '正在写入内测模型配置'); await seedInternalModelConfig(); setStep('model', 'success', INTERNAL_MODEL_REF); setStep('python', 'running', '正在准备文档处理环境'); await ensureAuxiliaryRuntimeMarkers(); setStep('python', 'success', '文档处理环境已准备'); const initializedAt = Date.now(); await setSetting('setupComplete', true); await setSetting('openclawInitializedAt', initializedAt); logger.info('[yinian-init] First-run initialization completed', { openclawDir: runtime.dir, model: INTERNAL_MODEL_REF, }); return { initialized: true, initializedAt, openclawDir: runtime.dir, model: INTERNAL_MODEL_REF, steps, }; } catch (error) { const running = steps.find((step) => step.status === 'running'); if (running) { running.status = 'error'; running.message = error instanceof Error ? error.message : String(error); } logger.error('[yinian-init] First-run initialization failed', error); return { initialized: false, steps, }; } } async function ensureWorkspaceFiles(): Promise { const openclawDir = getOpenClawConfigDir(); const workspaceDir = join(openclawDir, 'workspace'); await mkdir(workspaceDir, { recursive: true }); await mkdir(join(openclawDir, 'agents', 'main', 'agent'), { recursive: true }); } async function seedInternalModelConfig(): Promise { const configDir = getOpenClawConfigDir(); const configPath = join(configDir, 'openclaw.json'); await mkdir(configDir, { recursive: true }); const config = await readJsonFile(configPath); const models = asObject(config.models); const providers = asObject(models.providers); providers[INTERNAL_PROVIDER_KEY] = { baseUrl: 'https://api.minimaxi.com/anthropic', api: 'anthropic-messages', authHeader: true, models: [ { id: INTERNAL_MODEL_ID, name: 'MiniMax M2.7', reasoning: true, input: ['text', 'image'], contextWindow: 204800, maxTokens: 131072, }, ], }; models.mode = 'merge'; models.providers = providers; config.models = models; const agents = asObject(config.agents); const defaults = asObject(agents.defaults); defaults.model = { primary: INTERNAL_MODEL_REF, fallbacks: ['minimax/MiniMax-M2.5'], }; defaults.workspace = join(homedir(), '.openclaw', 'workspace'); agents.defaults = defaults; if (!Array.isArray(agents.list)) { agents.list = [ { id: 'main', name: '智念助手', default: true, workspace: join(homedir(), '.openclaw', 'workspace'), agentDir: '~/.openclaw/agents/main/agent', }, ]; } config.agents = agents; await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8'); } async function ensureAuxiliaryRuntimeMarkers(): Promise { const dir = join(getOpenClawConfigDir(), 'runtime'); await mkdir(dir, { recursive: true }); await writeFile(join(dir, 'yinian-initialized.json'), JSON.stringify({ initializedAt: Date.now(), documentRuntime: 'bundled', }, null, 2), 'utf8'); } async function readJsonFile(filePath: string): Promise { if (!existsSync(filePath)) return {}; try { const raw = await readFile(filePath, 'utf8'); const parsed = JSON.parse(raw) as unknown; return asObject(parsed); } catch { return {}; } } function asObject(value: unknown): JsonObject { return typeof value === 'object' && value !== null && !Array.isArray(value) ? value as JsonObject : {}; }