feat: add first-run runtime initialization
This commit is contained in:
@@ -303,6 +303,18 @@ function resolveOpenClawRuntime(): OpenClawRuntimeResolution {
|
||||
return cachedOpenClawRuntime;
|
||||
}
|
||||
|
||||
if (hasRequiredOpenClawContextModules(managedDir)) {
|
||||
cachedOpenClawRuntime = {
|
||||
dir: managedDir,
|
||||
source: 'managed',
|
||||
version: readOpenClawVersion(managedDir),
|
||||
bundledDir,
|
||||
managedDir,
|
||||
};
|
||||
logOpenClawRuntime('[openclaw-runtime] Using managed OpenClaw runtime', cachedOpenClawRuntime);
|
||||
return cachedOpenClawRuntime;
|
||||
}
|
||||
|
||||
const externalDir = findExternalOpenClawDir([bundledDir, managedDir]);
|
||||
if (externalDir) {
|
||||
cachedOpenClawRuntime = {
|
||||
@@ -312,7 +324,7 @@ function resolveOpenClawRuntime(): OpenClawRuntimeResolution {
|
||||
bundledDir,
|
||||
managedDir,
|
||||
};
|
||||
logOpenClawRuntime('[openclaw-runtime] Using existing OpenClaw installation', cachedOpenClawRuntime);
|
||||
logOpenClawRuntime('[openclaw-runtime] Using existing OpenClaw installation before managed runtime is installed', cachedOpenClawRuntime);
|
||||
return cachedOpenClawRuntime;
|
||||
}
|
||||
|
||||
@@ -391,6 +403,30 @@ export function getOpenClawDir(): string {
|
||||
return resolveOpenClawRuntime().dir;
|
||||
}
|
||||
|
||||
export function reinstallManagedOpenClawRuntime(): OpenClawRuntimeResolution {
|
||||
cachedOpenClawRuntime = null;
|
||||
|
||||
const bundledDir = getBundledOpenClawDir();
|
||||
const managedDir = getManagedOpenClawDir();
|
||||
rmSync(fsPath(managedDir), { recursive: true, force: true });
|
||||
|
||||
let installedFromBundled = false;
|
||||
if (isValidOpenClawPackageDir(bundledDir)) {
|
||||
installedFromBundled = installBundledOpenClawToManagedRuntime(bundledDir, managedDir);
|
||||
}
|
||||
|
||||
cachedOpenClawRuntime = {
|
||||
dir: hasRequiredOpenClawContextModules(managedDir) ? managedDir : bundledDir,
|
||||
source: hasRequiredOpenClawContextModules(managedDir) ? 'managed' : isValidOpenClawPackageDir(bundledDir) ? 'bundled' : 'missing',
|
||||
version: readOpenClawVersion(hasRequiredOpenClawContextModules(managedDir) ? managedDir : bundledDir),
|
||||
bundledDir,
|
||||
managedDir,
|
||||
installedFromBundled,
|
||||
};
|
||||
logOpenClawRuntime('[openclaw-runtime] Reinstalled managed OpenClaw runtime for first-run initialization', cachedOpenClawRuntime);
|
||||
return cachedOpenClawRuntime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OpenClaw package directory resolved to a real path.
|
||||
* Useful when consumers need deterministic module resolution under pnpm symlinks.
|
||||
|
||||
@@ -52,6 +52,10 @@ export interface AppSettings {
|
||||
sidebarCollapsed: boolean;
|
||||
devModeUnlocked: boolean;
|
||||
|
||||
// First-run initialization
|
||||
setupComplete: boolean;
|
||||
openclawInitializedAt?: number;
|
||||
|
||||
// Presets
|
||||
selectedBundles: string[];
|
||||
enabledSkills: string[];
|
||||
@@ -95,7 +99,7 @@ function createDefaultSettings(): AppSettings {
|
||||
|
||||
// Update
|
||||
updateChannel: 'stable',
|
||||
autoCheckUpdate: true,
|
||||
autoCheckUpdate: false,
|
||||
autoDownloadUpdate: false,
|
||||
skippedVersions: [],
|
||||
|
||||
@@ -103,6 +107,10 @@ function createDefaultSettings(): AppSettings {
|
||||
sidebarCollapsed: false,
|
||||
devModeUnlocked: false,
|
||||
|
||||
// First-run initialization
|
||||
setupComplete: false,
|
||||
openclawInitializedAt: undefined,
|
||||
|
||||
// Presets
|
||||
selectedBundles: ['productivity', 'developer'],
|
||||
enabledSkills: [],
|
||||
|
||||
213
electron/utils/yinian-initializer.ts
Normal file
213
electron/utils/yinian-initializer.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
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<string, unknown>;
|
||||
|
||||
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<YinianInitializationStatus> | 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<YinianInitializationStatus> {
|
||||
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<YinianInitializationStatus> {
|
||||
if (initializationInFlight) return initializationInFlight;
|
||||
initializationInFlight = runYinianRuntimeInitialization();
|
||||
try {
|
||||
return await initializationInFlight;
|
||||
} finally {
|
||||
initializationInFlight = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function runYinianRuntimeInitialization(): Promise<YinianInitializationStatus> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<JsonObject> {
|
||||
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
|
||||
: {};
|
||||
}
|
||||
Reference in New Issue
Block a user