feat: add first-run runtime initialization
This commit is contained in:
@@ -44,6 +44,7 @@ import {
|
|||||||
import { updateSkillConfig, getSkillConfig, getAllSkillConfigs } from '../utils/skill-config';
|
import { updateSkillConfig, getSkillConfig, getAllSkillConfigs } from '../utils/skill-config';
|
||||||
import { whatsAppLoginManager } from '../utils/whatsapp-login';
|
import { whatsAppLoginManager } from '../utils/whatsapp-login';
|
||||||
import { getProviderConfig } from '../utils/provider-registry';
|
import { getProviderConfig } from '../utils/provider-registry';
|
||||||
|
import { getYinianInitializationStatus, initializeYinianRuntime } from '../utils/yinian-initializer';
|
||||||
import { deviceOAuthManager, OAuthProviderType } from '../utils/device-oauth';
|
import { deviceOAuthManager, OAuthProviderType } from '../utils/device-oauth';
|
||||||
import { browserOAuthManager, type BrowserOAuthProviderType } from '../utils/browser-oauth';
|
import { browserOAuthManager, type BrowserOAuthProviderType } from '../utils/browser-oauth';
|
||||||
import { applyProxySettings } from './proxy';
|
import { applyProxySettings } from './proxy';
|
||||||
@@ -98,6 +99,9 @@ export function registerIpcHandlers(
|
|||||||
// OpenClaw handlers
|
// OpenClaw handlers
|
||||||
registerOpenClawHandlers(gatewayManager);
|
registerOpenClawHandlers(gatewayManager);
|
||||||
|
|
||||||
|
// YINIAN first-run initialization
|
||||||
|
registerYinianSetupHandlers();
|
||||||
|
|
||||||
// Provider handlers
|
// Provider handlers
|
||||||
registerProviderHandlers(gatewayManager);
|
registerProviderHandlers(gatewayManager);
|
||||||
|
|
||||||
@@ -144,6 +148,11 @@ export function registerIpcHandlers(
|
|||||||
registerFileHandlers();
|
registerFileHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function registerYinianSetupHandlers(): void {
|
||||||
|
ipcMain.handle('yinian:setup:status', async () => getYinianInitializationStatus());
|
||||||
|
ipcMain.handle('yinian:setup:initialize', async () => initializeYinianRuntime());
|
||||||
|
}
|
||||||
|
|
||||||
function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
|
function registerUnifiedRequestHandlers(gatewayManager: GatewayManager): void {
|
||||||
const providerService = getProviderService();
|
const providerService = getProviderService();
|
||||||
const handleProxySettingsChange = async () => {
|
const handleProxySettingsChange = async () => {
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ import { logger } from '../utils/logger';
|
|||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { setQuitting } from './app-state';
|
import { setQuitting } from './app-state';
|
||||||
|
|
||||||
/** Base CDN URL (without trailing channel path) */
|
/** Update feed URL. Disabled unless explicitly configured for an environment. */
|
||||||
const OSS_BASE_URL = 'https://oss.intelli-spectrum.com';
|
const UPDATE_FEED_BASE_URL = process.env.YINIAN_UPDATE_FEED_URL?.trim() || '';
|
||||||
|
const UPDATES_DISABLED_MESSAGE = '内测阶段暂未启用在线更新,请使用服务端下发的新版安装包。';
|
||||||
|
|
||||||
export interface UpdateStatus {
|
export interface UpdateStatus {
|
||||||
status: 'idle' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
|
status: 'idle' | 'disabled' | 'checking' | 'available' | 'not-available' | 'downloading' | 'downloaded' | 'error';
|
||||||
info?: UpdateInfo;
|
info?: UpdateInfo;
|
||||||
progress?: ProgressInfo;
|
progress?: ProgressInfo;
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -43,7 +44,9 @@ function detectChannel(version: string): string {
|
|||||||
|
|
||||||
export class AppUpdater extends EventEmitter {
|
export class AppUpdater extends EventEmitter {
|
||||||
private mainWindow: BrowserWindow | null = null;
|
private mainWindow: BrowserWindow | null = null;
|
||||||
private status: UpdateStatus = { status: 'idle' };
|
private status: UpdateStatus = UPDATE_FEED_BASE_URL
|
||||||
|
? { status: 'idle' }
|
||||||
|
: { status: 'disabled', error: UPDATES_DISABLED_MESSAGE };
|
||||||
private autoInstallTimer: NodeJS.Timeout | null = null;
|
private autoInstallTimer: NodeJS.Timeout | null = null;
|
||||||
private autoInstallCountdown = 0;
|
private autoInstallCountdown = 0;
|
||||||
|
|
||||||
@@ -69,13 +72,16 @@ export class AppUpdater extends EventEmitter {
|
|||||||
debug: (msg: string) => logger.debug('[Updater]', msg),
|
debug: (msg: string) => logger.debug('[Updater]', msg),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Override feed URL for prerelease channels so that
|
|
||||||
// alpha -> /alpha/alpha-mac.yml, beta -> /beta/beta-mac.yml, etc.
|
|
||||||
const version = app.getVersion();
|
const version = app.getVersion();
|
||||||
const channel = detectChannel(version);
|
const channel = detectChannel(version);
|
||||||
const feedUrl = `${OSS_BASE_URL}/${channel}`;
|
const feedUrl = UPDATE_FEED_BASE_URL ? `${UPDATE_FEED_BASE_URL.replace(/\/+$/, '')}/${channel}` : '';
|
||||||
|
|
||||||
logger.info(`[Updater] Version: ${version}, channel: ${channel}, feedUrl: ${feedUrl}`);
|
logger.info(`[Updater] Version: ${version}, channel: ${channel}, feedUrl: ${feedUrl || 'disabled'}`);
|
||||||
|
if (!UPDATE_FEED_BASE_URL) {
|
||||||
|
autoUpdater.autoDownload = false;
|
||||||
|
autoUpdater.autoInstallOnAppQuit = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Set channel so electron-updater requests the correct yml filename.
|
// Set channel so electron-updater requests the correct yml filename.
|
||||||
// e.g. channel "alpha" → requests alpha-mac.yml, channel "latest" → requests latest-mac.yml
|
// e.g. channel "alpha" → requests alpha-mac.yml, channel "latest" → requests latest-mac.yml
|
||||||
@@ -174,6 +180,11 @@ export class AppUpdater extends EventEmitter {
|
|||||||
* final status so the UI never gets stuck in 'checking'.
|
* final status so the UI never gets stuck in 'checking'.
|
||||||
*/
|
*/
|
||||||
async checkForUpdates(): Promise<UpdateInfo | null> {
|
async checkForUpdates(): Promise<UpdateInfo | null> {
|
||||||
|
if (!UPDATE_FEED_BASE_URL) {
|
||||||
|
this.updateStatus({ status: 'disabled', error: UPDATES_DISABLED_MESSAGE });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await autoUpdater.checkForUpdates();
|
const result = await autoUpdater.checkForUpdates();
|
||||||
|
|
||||||
@@ -205,6 +216,11 @@ export class AppUpdater extends EventEmitter {
|
|||||||
* Download available update
|
* Download available update
|
||||||
*/
|
*/
|
||||||
async downloadUpdate(): Promise<void> {
|
async downloadUpdate(): Promise<void> {
|
||||||
|
if (!UPDATE_FEED_BASE_URL) {
|
||||||
|
this.updateStatus({ status: 'disabled', error: UPDATES_DISABLED_MESSAGE });
|
||||||
|
throw new Error(UPDATES_DISABLED_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await autoUpdater.downloadUpdate();
|
await autoUpdater.downloadUpdate();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -225,6 +241,11 @@ export class AppUpdater extends EventEmitter {
|
|||||||
* the window cleanly while ShipIt runs independently to replace the app.
|
* the window cleanly while ShipIt runs independently to replace the app.
|
||||||
*/
|
*/
|
||||||
quitAndInstall(): void {
|
quitAndInstall(): void {
|
||||||
|
if (!UPDATE_FEED_BASE_URL) {
|
||||||
|
this.updateStatus({ status: 'disabled', error: UPDATES_DISABLED_MESSAGE });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('[Updater] quitAndInstall called');
|
logger.info('[Updater] quitAndInstall called');
|
||||||
setQuitting();
|
setQuitting();
|
||||||
autoUpdater.quitAndInstall();
|
autoUpdater.quitAndInstall();
|
||||||
@@ -273,7 +294,7 @@ export class AppUpdater extends EventEmitter {
|
|||||||
* Set auto-download preference
|
* Set auto-download preference
|
||||||
*/
|
*/
|
||||||
setAutoDownload(enable: boolean): void {
|
setAutoDownload(enable: boolean): void {
|
||||||
autoUpdater.autoDownload = enable;
|
autoUpdater.autoDownload = UPDATE_FEED_BASE_URL ? enable : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ const electronAPI = {
|
|||||||
'yinian:skills:listLocal',
|
'yinian:skills:listLocal',
|
||||||
'yinian:skills:getRegistry',
|
'yinian:skills:getRegistry',
|
||||||
'yinian:server:status',
|
'yinian:server:status',
|
||||||
|
'yinian:setup:status',
|
||||||
|
'yinian:setup:initialize',
|
||||||
// Window controls
|
// Window controls
|
||||||
'window:minimize',
|
'window:minimize',
|
||||||
'window:maximize',
|
'window:maximize',
|
||||||
|
|||||||
@@ -303,6 +303,18 @@ function resolveOpenClawRuntime(): OpenClawRuntimeResolution {
|
|||||||
return cachedOpenClawRuntime;
|
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]);
|
const externalDir = findExternalOpenClawDir([bundledDir, managedDir]);
|
||||||
if (externalDir) {
|
if (externalDir) {
|
||||||
cachedOpenClawRuntime = {
|
cachedOpenClawRuntime = {
|
||||||
@@ -312,7 +324,7 @@ function resolveOpenClawRuntime(): OpenClawRuntimeResolution {
|
|||||||
bundledDir,
|
bundledDir,
|
||||||
managedDir,
|
managedDir,
|
||||||
};
|
};
|
||||||
logOpenClawRuntime('[openclaw-runtime] Using existing OpenClaw installation', cachedOpenClawRuntime);
|
logOpenClawRuntime('[openclaw-runtime] Using existing OpenClaw installation before managed runtime is installed', cachedOpenClawRuntime);
|
||||||
return cachedOpenClawRuntime;
|
return cachedOpenClawRuntime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,6 +403,30 @@ export function getOpenClawDir(): string {
|
|||||||
return resolveOpenClawRuntime().dir;
|
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.
|
* Get OpenClaw package directory resolved to a real path.
|
||||||
* Useful when consumers need deterministic module resolution under pnpm symlinks.
|
* Useful when consumers need deterministic module resolution under pnpm symlinks.
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ export interface AppSettings {
|
|||||||
sidebarCollapsed: boolean;
|
sidebarCollapsed: boolean;
|
||||||
devModeUnlocked: boolean;
|
devModeUnlocked: boolean;
|
||||||
|
|
||||||
|
// First-run initialization
|
||||||
|
setupComplete: boolean;
|
||||||
|
openclawInitializedAt?: number;
|
||||||
|
|
||||||
// Presets
|
// Presets
|
||||||
selectedBundles: string[];
|
selectedBundles: string[];
|
||||||
enabledSkills: string[];
|
enabledSkills: string[];
|
||||||
@@ -95,7 +99,7 @@ function createDefaultSettings(): AppSettings {
|
|||||||
|
|
||||||
// Update
|
// Update
|
||||||
updateChannel: 'stable',
|
updateChannel: 'stable',
|
||||||
autoCheckUpdate: true,
|
autoCheckUpdate: false,
|
||||||
autoDownloadUpdate: false,
|
autoDownloadUpdate: false,
|
||||||
skippedVersions: [],
|
skippedVersions: [],
|
||||||
|
|
||||||
@@ -103,6 +107,10 @@ function createDefaultSettings(): AppSettings {
|
|||||||
sidebarCollapsed: false,
|
sidebarCollapsed: false,
|
||||||
devModeUnlocked: false,
|
devModeUnlocked: false,
|
||||||
|
|
||||||
|
// First-run initialization
|
||||||
|
setupComplete: false,
|
||||||
|
openclawInitializedAt: undefined,
|
||||||
|
|
||||||
// Presets
|
// Presets
|
||||||
selectedBundles: ['productivity', 'developer'],
|
selectedBundles: ['productivity', 'developer'],
|
||||||
enabledSkills: [],
|
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
|
||||||
|
: {};
|
||||||
|
}
|
||||||
@@ -167,6 +167,13 @@ function App() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!setupComplete && !location.pathname.startsWith('/setup')) {
|
||||||
|
navigate('/setup', { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!setupComplete) return;
|
||||||
|
|
||||||
if (!yinianSession.authenticated && !location.pathname.startsWith('/login')) {
|
if (!yinianSession.authenticated && !location.pathname.startsWith('/login')) {
|
||||||
navigate('/login', { replace: true });
|
navigate('/login', { replace: true });
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ export function UpdateSettings() {
|
|||||||
return <Rocket className="h-4 w-4 text-primary" />;
|
return <Rocket className="h-4 w-4 text-primary" />;
|
||||||
case 'error':
|
case 'error':
|
||||||
return <RefreshCw className="h-4 w-4 text-destructive" />;
|
return <RefreshCw className="h-4 w-4 text-destructive" />;
|
||||||
|
case 'disabled':
|
||||||
|
return <RefreshCw className="h-4 w-4 text-muted-foreground" />;
|
||||||
default:
|
default:
|
||||||
return <RefreshCw className="h-4 w-4 text-muted-foreground" />;
|
return <RefreshCw className="h-4 w-4 text-muted-foreground" />;
|
||||||
}
|
}
|
||||||
@@ -76,6 +78,8 @@ export function UpdateSettings() {
|
|||||||
return t('updates.status.downloaded', { version: updateInfo?.version });
|
return t('updates.status.downloaded', { version: updateInfo?.version });
|
||||||
case 'error':
|
case 'error':
|
||||||
return error || t('updates.status.failed');
|
return error || t('updates.status.failed');
|
||||||
|
case 'disabled':
|
||||||
|
return error || t('updates.status.disabled');
|
||||||
case 'not-available':
|
case 'not-available':
|
||||||
return t('updates.status.latest');
|
return t('updates.status.latest');
|
||||||
default:
|
default:
|
||||||
@@ -128,6 +132,12 @@ export function UpdateSettings() {
|
|||||||
{t('updates.action.retry')}
|
{t('updates.action.retry')}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
case 'disabled':
|
||||||
|
return (
|
||||||
|
<Button disabled variant="outline" size="sm">
|
||||||
|
{t('updates.action.disabled')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<Button onClick={handleCheckForUpdates} variant="outline" size="sm">
|
<Button onClick={handleCheckForUpdates} variant="outline" size="sm">
|
||||||
|
|||||||
@@ -196,6 +196,7 @@
|
|||||||
"downloaded": "Ready to install: v{{version}}",
|
"downloaded": "Ready to install: v{{version}}",
|
||||||
"autoInstalling": "Restarting to install update in {{seconds}}s...",
|
"autoInstalling": "Restarting to install update in {{seconds}}s...",
|
||||||
"failed": "Update check failed",
|
"failed": "Update check failed",
|
||||||
|
"disabled": "Online updates are disabled during internal testing",
|
||||||
"latest": "You have the latest version",
|
"latest": "You have the latest version",
|
||||||
"check": "Check for updates to get the latest features"
|
"check": "Check for updates to get the latest features"
|
||||||
},
|
},
|
||||||
@@ -205,13 +206,14 @@
|
|||||||
"download": "Download Update",
|
"download": "Download Update",
|
||||||
"install": "Install & Restart",
|
"install": "Install & Restart",
|
||||||
"cancelAutoInstall": "Cancel",
|
"cancelAutoInstall": "Cancel",
|
||||||
|
"disabled": "Unavailable",
|
||||||
"retry": "Retry",
|
"retry": "Retry",
|
||||||
"check": "Check for Updates"
|
"check": "Check for Updates"
|
||||||
},
|
},
|
||||||
"currentVersion": "Current Version",
|
"currentVersion": "Current Version",
|
||||||
"whatsNew": "What's New:",
|
"whatsNew": "What's New:",
|
||||||
"errorDetails": "Error Details:",
|
"errorDetails": "Error Details:",
|
||||||
"help": "When auto-update is enabled, updates are downloaded and installed automatically."
|
"help": "During internal testing, new versions are distributed by the service team. Online updates will stay disabled until the update feed is configured."
|
||||||
},
|
},
|
||||||
"advanced": {
|
"advanced": {
|
||||||
"title": "Advanced",
|
"title": "Advanced",
|
||||||
|
|||||||
@@ -196,6 +196,7 @@
|
|||||||
"downloaded": "准备安装:v{{version}}",
|
"downloaded": "准备安装:v{{version}}",
|
||||||
"autoInstalling": "将在 {{seconds}} 秒后重启并安装更新...",
|
"autoInstalling": "将在 {{seconds}} 秒后重启并安装更新...",
|
||||||
"failed": "检查更新失败",
|
"failed": "检查更新失败",
|
||||||
|
"disabled": "内测阶段暂未启用在线更新",
|
||||||
"latest": "您已拥有最新版本",
|
"latest": "您已拥有最新版本",
|
||||||
"check": "检查更新以获取最新功能"
|
"check": "检查更新以获取最新功能"
|
||||||
},
|
},
|
||||||
@@ -205,13 +206,14 @@
|
|||||||
"download": "下载更新",
|
"download": "下载更新",
|
||||||
"install": "安装并重启",
|
"install": "安装并重启",
|
||||||
"cancelAutoInstall": "取消",
|
"cancelAutoInstall": "取消",
|
||||||
|
"disabled": "暂不可用",
|
||||||
"retry": "重试",
|
"retry": "重试",
|
||||||
"check": "检查更新"
|
"check": "检查更新"
|
||||||
},
|
},
|
||||||
"currentVersion": "当前版本",
|
"currentVersion": "当前版本",
|
||||||
"whatsNew": "更新内容:",
|
"whatsNew": "更新内容:",
|
||||||
"errorDetails": "错误详情:",
|
"errorDetails": "错误详情:",
|
||||||
"help": "开启自动更新后,更新将自动下载并安装。"
|
"help": "内测阶段更新将由服务端统一下发新版安装包,在线更新接入前不会自动下载或安装。"
|
||||||
},
|
},
|
||||||
"advanced": {
|
"advanced": {
|
||||||
"title": "高级",
|
"title": "高级",
|
||||||
|
|||||||
@@ -97,7 +97,9 @@ export function Settings() {
|
|||||||
const updateDesktopUserName = useYinianStore((state) => state.updateDesktopUserName);
|
const updateDesktopUserName = useYinianStore((state) => state.updateDesktopUserName);
|
||||||
const updateWorkspaceDisplayName = useYinianStore((state) => state.updateWorkspaceDisplayName);
|
const updateWorkspaceDisplayName = useYinianStore((state) => state.updateWorkspaceDisplayName);
|
||||||
const currentVersion = useUpdateStore((state) => state.currentVersion);
|
const currentVersion = useUpdateStore((state) => state.currentVersion);
|
||||||
|
const updateStatus = useUpdateStore((state) => state.status);
|
||||||
const updateSetAutoDownload = useUpdateStore((state) => state.setAutoDownload);
|
const updateSetAutoDownload = useUpdateStore((state) => state.setAutoDownload);
|
||||||
|
const updatesDisabled = updateStatus === 'disabled';
|
||||||
const [controlUiInfo, setControlUiInfo] = useState<ControlUiInfo | null>(null);
|
const [controlUiInfo, setControlUiInfo] = useState<ControlUiInfo | null>(null);
|
||||||
const [openclawCliCommand, setOpenclawCliCommand] = useState('');
|
const [openclawCliCommand, setOpenclawCliCommand] = useState('');
|
||||||
const [openclawCliError, setOpenclawCliError] = useState<string | null>(null);
|
const [openclawCliError, setOpenclawCliError] = useState<string | null>(null);
|
||||||
@@ -690,6 +692,7 @@ export function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={autoCheckUpdate}
|
checked={autoCheckUpdate}
|
||||||
|
disabled={updatesDisabled}
|
||||||
onCheckedChange={setAutoCheckUpdate}
|
onCheckedChange={setAutoCheckUpdate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -703,6 +706,7 @@ export function Settings() {
|
|||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={autoDownloadUpdate}
|
checked={autoDownloadUpdate}
|
||||||
|
disabled={updatesDisabled}
|
||||||
onCheckedChange={(value) => {
|
onCheckedChange={(value) => {
|
||||||
setAutoDownloadUpdate(value);
|
setAutoDownloadUpdate(value);
|
||||||
updateSetAutoDownload(value);
|
updateSetAutoDownload(value);
|
||||||
|
|||||||
@@ -37,9 +37,8 @@ interface SetupStep {
|
|||||||
|
|
||||||
const STEP = {
|
const STEP = {
|
||||||
WELCOME: 0,
|
WELCOME: 0,
|
||||||
RUNTIME: 1,
|
INSTALLING: 1,
|
||||||
INSTALLING: 2,
|
COMPLETE: 2,
|
||||||
COMPLETE: 3,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const getSteps = (t: TFunction): SetupStep[] => [
|
const getSteps = (t: TFunction): SetupStep[] => [
|
||||||
@@ -48,11 +47,6 @@ const getSteps = (t: TFunction): SetupStep[] => [
|
|||||||
title: t('steps.welcome.title'),
|
title: t('steps.welcome.title'),
|
||||||
description: t('steps.welcome.description'),
|
description: t('steps.welcome.description'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'runtime',
|
|
||||||
title: t('steps.runtime.title'),
|
|
||||||
description: t('steps.runtime.description'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'installing',
|
id: 'installing',
|
||||||
title: t('steps.installing.title'),
|
title: t('steps.installing.title'),
|
||||||
@@ -88,14 +82,15 @@ import zhinianLogo from '@/assets/logo.svg';
|
|||||||
export function Setup() {
|
export function Setup() {
|
||||||
const { t } = useTranslation(['setup', 'channels']);
|
const { t } = useTranslation(['setup', 'channels']);
|
||||||
const navigate = useNavigate();
|
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);
|
const [currentStep, setCurrentStep] = useState<number>(STEP.WELCOME);
|
||||||
|
|
||||||
// Setup state
|
// Setup state
|
||||||
// Installation state for the Installing step
|
// Installation state for the Installing step
|
||||||
const [installedSkills, setInstalledSkills] = useState<string[]>([]);
|
const [installedSkills, setInstalledSkills] = useState<string[]>([]);
|
||||||
// Runtime check status
|
|
||||||
const [runtimeChecksPassed, setRuntimeChecksPassed] = useState(false);
|
|
||||||
|
|
||||||
const steps = getSteps(t);
|
const steps = getSteps(t);
|
||||||
const safeStepIndex = Number.isInteger(currentStep)
|
const safeStepIndex = Number.isInteger(currentStep)
|
||||||
? Math.min(Math.max(currentStep, STEP.WELCOME), steps.length - 1)
|
? Math.min(Math.max(currentStep, STEP.WELCOME), steps.length - 1)
|
||||||
@@ -111,8 +106,6 @@ export function Setup() {
|
|||||||
switch (safeStepIndex) {
|
switch (safeStepIndex) {
|
||||||
case STEP.WELCOME:
|
case STEP.WELCOME:
|
||||||
return true;
|
return true;
|
||||||
case STEP.RUNTIME:
|
|
||||||
return runtimeChecksPassed;
|
|
||||||
case STEP.INSTALLING:
|
case STEP.INSTALLING:
|
||||||
return false; // Cannot manually proceed, auto-proceeds when done
|
return false; // Cannot manually proceed, auto-proceeds when done
|
||||||
case STEP.COMPLETE:
|
case STEP.COMPLETE:
|
||||||
@@ -120,14 +113,14 @@ export function Setup() {
|
|||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}, [safeStepIndex, runtimeChecksPassed]);
|
}, [safeStepIndex]);
|
||||||
|
|
||||||
const handleNext = async () => {
|
const handleNext = async () => {
|
||||||
if (isLastStep) {
|
if (isLastStep) {
|
||||||
// Complete setup
|
// Complete setup
|
||||||
markSetupComplete();
|
markSetupComplete();
|
||||||
toast.success(t('complete.title'));
|
toast.success(t('complete.title'));
|
||||||
navigate('/');
|
navigate('/login', { replace: true });
|
||||||
} else {
|
} else {
|
||||||
setCurrentStep((i) => i + 1);
|
setCurrentStep((i) => i + 1);
|
||||||
}
|
}
|
||||||
@@ -139,7 +132,7 @@ export function Setup() {
|
|||||||
|
|
||||||
const handleSkip = () => {
|
const handleSkip = () => {
|
||||||
markSetupComplete();
|
markSetupComplete();
|
||||||
navigate('/');
|
navigate('/login', { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Auto-proceed when installation is complete
|
// Auto-proceed when installation is complete
|
||||||
@@ -207,7 +200,6 @@ export function Setup() {
|
|||||||
{/* Step-specific content */}
|
{/* Step-specific content */}
|
||||||
<div className="rounded-xl bg-card text-card-foreground border shadow-sm p-8 mb-8">
|
<div className="rounded-xl bg-card text-card-foreground border shadow-sm p-8 mb-8">
|
||||||
{safeStepIndex === STEP.WELCOME && <WelcomeContent />}
|
{safeStepIndex === STEP.WELCOME && <WelcomeContent />}
|
||||||
{safeStepIndex === STEP.RUNTIME && <RuntimeContent onStatusChange={setRuntimeChecksPassed} />}
|
|
||||||
{safeStepIndex === STEP.INSTALLING && (
|
{safeStepIndex === STEP.INSTALLING && (
|
||||||
<InstallingContent
|
<InstallingContent
|
||||||
skills={getDefaultSkills(t)}
|
skills={getDefaultSkills(t)}
|
||||||
@@ -234,7 +226,7 @@ export function Setup() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{!isLastStep && safeStepIndex !== STEP.RUNTIME && (
|
{isE2EMode && !isLastStep && (
|
||||||
<Button data-testid="setup-skip-button" variant="ghost" onClick={handleSkip}>
|
<Button data-testid="setup-skip-button" variant="ghost" onClick={handleSkip}>
|
||||||
{t('nav.skipSetup')}
|
{t('nav.skipSetup')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -638,11 +630,12 @@ function RuntimeContent({ onStatusChange }: RuntimeContentProps) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
void RuntimeContent;
|
||||||
|
|
||||||
// NOTE: ProviderContent component removed - configure providers via Settings > AI Providers
|
// 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';
|
type InstallStatus = 'pending' | 'installing' | 'completed' | 'failed';
|
||||||
|
|
||||||
interface SkillInstallState {
|
interface SkillInstallState {
|
||||||
@@ -650,6 +643,7 @@ interface SkillInstallState {
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
status: InstallStatus;
|
status: InstallStatus;
|
||||||
|
message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface InstallingContentProps {
|
interface InstallingContentProps {
|
||||||
@@ -658,10 +652,16 @@ interface InstallingContentProps {
|
|||||||
onSkip: () => void;
|
onSkip: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function InstallingContent({ skills, onComplete, onSkip }: InstallingContentProps) {
|
function InstallingContent({ skills: _skills, onComplete, onSkip }: InstallingContentProps) {
|
||||||
const { t } = useTranslation('setup');
|
const { t } = useTranslation('setup');
|
||||||
|
const canSkip = new URLSearchParams(window.location.search).get('e2e') === '1';
|
||||||
const [skillStates, setSkillStates] = useState<SkillInstallState[]>(
|
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 [overallProgress, setOverallProgress] = useState(0);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
@@ -674,36 +674,58 @@ function InstallingContent({ skills, onComplete, onSkip }: InstallingContentProp
|
|||||||
|
|
||||||
const runRealInstall = async () => {
|
const runRealInstall = async () => {
|
||||||
try {
|
try {
|
||||||
// Step 1: Initialize all skills to 'installing' state for UI
|
setSkillStates(prev => prev.map((s, index) => ({ ...s, status: index === 0 ? 'installing' : 'pending' })));
|
||||||
setSkillStates(prev => prev.map(s => ({ ...s, status: 'installing' })));
|
|
||||||
setOverallProgress(10);
|
setOverallProgress(10);
|
||||||
|
|
||||||
// Step 2: Call the backend to install uv and setup Python
|
const result = await invokeIpc('yinian:setup:initialize') as {
|
||||||
const result = await invokeIpc('uv:install-all') as {
|
initialized: boolean;
|
||||||
success: boolean;
|
openclawDir?: string;
|
||||||
error?: string
|
model?: string;
|
||||||
|
steps?: Array<{
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
status: 'pending' | 'running' | 'success' | 'error';
|
||||||
|
message?: string;
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (result.success) {
|
const mappedSteps = (result.steps ?? []).map((step) => ({
|
||||||
setSkillStates(prev => prev.map(s => ({ ...s, status: 'completed' })));
|
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);
|
setOverallProgress(100);
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||||
onComplete(skills.map(s => s.id));
|
onComplete((result.steps ?? []).map((s) => s.id));
|
||||||
} else {
|
} else {
|
||||||
setSkillStates(prev => prev.map(s => ({ ...s, status: 'failed' })));
|
setSkillStates(prev => prev.map(s => s.status === 'completed' ? s : { ...s, status: 'failed' }));
|
||||||
setErrorMessage(result.error || 'Unknown error during installation');
|
setErrorMessage('初始化未完成,请查看失败项后重试。');
|
||||||
toast.error('Environment setup failed');
|
toast.error('初始化失败');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSkillStates(prev => prev.map(s => ({ ...s, status: 'failed' })));
|
setSkillStates(prev => prev.map(s => ({ ...s, status: 'failed' })));
|
||||||
setErrorMessage(String(err));
|
setErrorMessage(String(err));
|
||||||
toast.error('Installation error');
|
toast.error('初始化失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
runRealInstall();
|
runRealInstall();
|
||||||
}, [skills, onComplete]);
|
}, [onComplete]);
|
||||||
|
|
||||||
const getStatusIcon = (status: InstallStatus) => {
|
const getStatusIcon = (status: InstallStatus) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@@ -773,7 +795,7 @@ function InstallingContent({ skills, onComplete, onSkip }: InstallingContentProp
|
|||||||
{getStatusIcon(skill.status)}
|
{getStatusIcon(skill.status)}
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">{skill.name}</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{getStatusText(skill)}
|
{getStatusText(skill)}
|
||||||
@@ -809,18 +831,20 @@ function InstallingContent({ skills, onComplete, onSkip }: InstallingContentProp
|
|||||||
|
|
||||||
{!errorMessage && (
|
{!errorMessage && (
|
||||||
<p className="text-sm text-slate-400 text-center">
|
<p className="text-sm text-slate-400 text-center">
|
||||||
{t('installing.wait')}
|
正在准备智念助手运行环境,请勿关闭应用。
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-end">
|
{canSkip && (
|
||||||
<Button
|
<div className="flex justify-end">
|
||||||
variant="ghost"
|
<Button
|
||||||
className="text-muted-foreground"
|
variant="ghost"
|
||||||
onClick={onSkip}
|
className="text-muted-foreground"
|
||||||
>
|
onClick={onSkip}
|
||||||
{t('installing.skip')}
|
>
|
||||||
</Button>
|
{t('installing.skip')}
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -830,33 +854,25 @@ interface CompleteContentProps {
|
|||||||
|
|
||||||
function CompleteContent({ installedSkills }: CompleteContentProps) {
|
function CompleteContent({ installedSkills }: CompleteContentProps) {
|
||||||
const { t } = useTranslation(['setup', 'settings']);
|
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 (
|
return (
|
||||||
<div className="text-center space-y-6">
|
<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>
|
<h2 className="text-xl font-semibold">{t('complete.title')}</h2>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
{t('complete.subtitle')}
|
智念助手已经完成运行环境、工作区和内测模型配置,下一步请登录账号。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="space-y-3 text-left max-w-md mx-auto">
|
<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">
|
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
|
||||||
<span>{t('complete.components')}</span>
|
<span>初始化项目</span>
|
||||||
<span className="text-green-400">
|
<span className="text-green-400">{installedSkills.length || 4} 项已完成</span>
|
||||||
{installedSkillNames || `${installedSkills.length} ${t('installing.status.installed')}`}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
|
<div className="flex items-center justify-between p-3 rounded-lg bg-muted/50">
|
||||||
<span>{t('complete.gateway')}</span>
|
<span>默认模型</span>
|
||||||
<span className={gatewayStatus.state === 'running' ? 'text-green-400' : 'text-yellow-400'}>
|
<span className="text-green-400">MiniMax M2.7</span>
|
||||||
{gatewayStatus.state === 'running' ? `✓ ${t('complete.running')}` : gatewayStatus.state}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const defaultSettings = {
|
|||||||
proxyAllServer: '',
|
proxyAllServer: '',
|
||||||
proxyBypassRules: '<local>;localhost;127.0.0.1;::1',
|
proxyBypassRules: '<local>;localhost;127.0.0.1;::1',
|
||||||
updateChannel: 'stable' as UpdateChannel,
|
updateChannel: 'stable' as UpdateChannel,
|
||||||
autoCheckUpdate: true,
|
autoCheckUpdate: false,
|
||||||
autoDownloadUpdate: false,
|
autoDownloadUpdate: false,
|
||||||
sidebarCollapsed: false,
|
sidebarCollapsed: false,
|
||||||
devModeUnlocked: false,
|
devModeUnlocked: false,
|
||||||
@@ -179,8 +179,20 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
setProxyAllServer: (proxyAllServer) => set({ proxyAllServer }),
|
setProxyAllServer: (proxyAllServer) => set({ proxyAllServer }),
|
||||||
setProxyBypassRules: (proxyBypassRules) => set({ proxyBypassRules }),
|
setProxyBypassRules: (proxyBypassRules) => set({ proxyBypassRules }),
|
||||||
setUpdateChannel: (updateChannel) => set({ updateChannel }),
|
setUpdateChannel: (updateChannel) => set({ updateChannel }),
|
||||||
setAutoCheckUpdate: (autoCheckUpdate) => set({ autoCheckUpdate }),
|
setAutoCheckUpdate: (autoCheckUpdate) => {
|
||||||
setAutoDownloadUpdate: (autoDownloadUpdate) => set({ autoDownloadUpdate }),
|
set({ autoCheckUpdate });
|
||||||
|
void hostApiFetch('/api/settings/autoCheckUpdate', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ value: autoCheckUpdate }),
|
||||||
|
}).catch(() => { });
|
||||||
|
},
|
||||||
|
setAutoDownloadUpdate: (autoDownloadUpdate) => {
|
||||||
|
set({ autoDownloadUpdate });
|
||||||
|
void hostApiFetch('/api/settings/autoDownloadUpdate', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ value: autoDownloadUpdate }),
|
||||||
|
}).catch(() => { });
|
||||||
|
},
|
||||||
setSidebarCollapsed: (sidebarCollapsed) => set({ sidebarCollapsed }),
|
setSidebarCollapsed: (sidebarCollapsed) => set({ sidebarCollapsed }),
|
||||||
setDevModeUnlocked: (devModeUnlocked) => {
|
setDevModeUnlocked: (devModeUnlocked) => {
|
||||||
set({ devModeUnlocked });
|
set({ devModeUnlocked });
|
||||||
@@ -189,7 +201,13 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
body: JSON.stringify({ value: devModeUnlocked }),
|
body: JSON.stringify({ value: devModeUnlocked }),
|
||||||
}).catch(() => { });
|
}).catch(() => { });
|
||||||
},
|
},
|
||||||
markSetupComplete: () => set({ setupComplete: true }),
|
markSetupComplete: () => {
|
||||||
|
set({ setupComplete: true });
|
||||||
|
void hostApiFetch('/api/settings/setupComplete', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ value: true }),
|
||||||
|
}).catch(() => { });
|
||||||
|
},
|
||||||
resetSettings: () => {
|
resetSettings: () => {
|
||||||
const language = applyLanguage(defaultSettings.language);
|
const language = applyLanguage(defaultSettings.language);
|
||||||
set({ ...defaultSettings, language });
|
set({ ...defaultSettings, language });
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface ProgressInfo {
|
|||||||
|
|
||||||
export type UpdateStatus =
|
export type UpdateStatus =
|
||||||
| 'idle'
|
| 'idle'
|
||||||
|
| 'disabled'
|
||||||
| 'checking'
|
| 'checking'
|
||||||
| 'available'
|
| 'available'
|
||||||
| 'not-available'
|
| 'not-available'
|
||||||
@@ -139,7 +140,7 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Auto-check for updates on startup (respects user toggle)
|
// Auto-check for updates on startup (respects user toggle)
|
||||||
if (autoCheckUpdate) {
|
if (autoCheckUpdate && get().status !== 'disabled') {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
get().checkForUpdates().catch(() => {});
|
get().checkForUpdates().catch(() => {});
|
||||||
}, 10000);
|
}, 10000);
|
||||||
@@ -147,6 +148,7 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
checkForUpdates: async () => {
|
checkForUpdates: async () => {
|
||||||
|
if (get().status === 'disabled') return;
|
||||||
set({ status: 'checking', error: null });
|
set({ status: 'checking', error: null });
|
||||||
let completedWithoutUpdaterEvent = false;
|
let completedWithoutUpdaterEvent = false;
|
||||||
|
|
||||||
@@ -189,6 +191,7 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
downloadUpdate: async () => {
|
downloadUpdate: async () => {
|
||||||
|
if (get().status === 'disabled') return;
|
||||||
set({ status: 'downloading', error: null });
|
set({ status: 'downloading', error: null });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -206,6 +209,7 @@ export const useUpdateStore = create<UpdateState>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
installUpdate: () => {
|
installUpdate: () => {
|
||||||
|
if (get().status === 'disabled') return;
|
||||||
void invokeIpc('update:install');
|
void invokeIpc('update:install');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user