Refine desktop setup and remove bundled app center apps

This commit is contained in:
inman
2026-06-04 09:58:58 +08:00
parent 6153579b90
commit 84128dbe23
73 changed files with 3888 additions and 2024 deletions

View File

@@ -6,8 +6,27 @@ import { getAllSettings, setSetting } from './store';
import { getOpenClawConfigDir, reinstallManagedOpenClawRuntime } from './paths';
import { logger } from './logger';
import { ensureOfficeSkillRuntimeReady } from './office-skill-runtime';
import {
YINIAN_MODEL_ENTRY,
YINIAN_MODEL_PROVIDER_KEY,
} from '../../shared/yinian-model';
type JsonObject = Record<string, unknown>;
type InternalModelAuthSeedResult =
| { status: 'seeded'; path: string; config: ModelRuntimeConfig }
| { status: 'skipped'; path?: string; reason: string; modelRef?: string };
interface ModelRuntimeConfig {
providerKey: string;
modelId: string;
modelName: string;
modelRef: string;
baseUrl: string;
api: string;
authHeader?: boolean;
fallbackModelRefs: string[];
authProfileId: string;
}
export type YinianInitializationStepStatus = 'pending' | 'running' | 'success' | 'error';
@@ -26,18 +45,18 @@ export interface YinianInitializationStatus {
steps: YinianInitializationStep[];
}
const INTERNAL_PROVIDER_KEY = 'minimax';
const INTERNAL_MODEL_ID = 'MiniMax-M2.7';
const INTERNAL_MODEL_REF = `${INTERNAL_PROVIDER_KEY}/${INTERNAL_MODEL_ID}`;
const INTERNAL_AUTH_PROFILE_ID = 'minimax:default';
const DESKTOP_TOOLS_PROFILE = 'coding';
const YINIAN_FALLBACK_SKILL_IDS = ['docx', 'pdf', 'pptx', 'xlsx', 'design', 'image-search', 'web-search'];
const REQUIRED_RUNTIME_FILES = [
'package.json',
'openclaw.mjs',
join('docs', 'reference', 'templates', 'SOUL.md'),
join('docs', 'reference', 'templates', 'IDENTITY.md'),
join('docs', 'reference', 'templates', 'USER.md'),
join('docs', 'reference', 'templates', 'AGENTS.md'),
join('docs', 'reference', 'templates', 'TOOLS.md'),
join('docs', 'reference', 'templates', 'HEARTBEAT.md'),
join('docs', 'reference', 'templates', 'BOOT.md'),
join('node_modules', 'openclaw', 'package.json'),
] as const;
let initializationInFlight: Promise<YinianInitializationStatus> | null = null;
@@ -45,7 +64,7 @@ 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: 'model', label: '准备模型 API 配置', status: 'pending' },
{ id: 'python', label: '准备文档处理环境', status: 'pending' },
];
@@ -54,8 +73,8 @@ export async function getYinianInitializationStatus(): Promise<YinianInitializat
const runtimeStatus = getManagedRuntimeVerification();
const workspaceReady = existsSync(join(getOpenClawConfigDir(), 'workspace'));
const modelConfigReady = existsSync(join(getOpenClawConfigDir(), 'openclaw.json'));
const modelAuthReady = await hasYinianModelAuthProfile();
const modelReady = modelConfigReady && modelAuthReady;
const currentModelRef = modelConfigReady ? await getConfiguredPrimaryModelRef() : undefined;
const modelReady = modelConfigReady;
const officeMarkerReady = existsSync(join(getOpenClawConfigDir(), 'runtime', 'yinian-initialized.json'));
const initialized = settings.setupComplete === true
&& runtimeStatus.ok
@@ -69,7 +88,6 @@ export async function getYinianInitializationStatus(): Promise<YinianInitializat
runtimeMissing: runtimeStatus.missing,
workspaceReady,
modelConfigReady,
modelAuthReady,
officeMarkerReady,
});
}
@@ -78,7 +96,7 @@ export async function getYinianInitializationStatus(): Promise<YinianInitializat
initialized,
initializedAt: settings.openclawInitializedAt,
openclawDir: runtimeStatus.runtimeDir,
model: initialized ? INTERNAL_MODEL_REF : undefined,
model: initialized ? currentModelRef : undefined,
steps: DEFAULT_STEPS.map((step) => {
if (step.id === 'runtime') {
return {
@@ -97,12 +115,10 @@ export async function getYinianInitializationStatus(): Promise<YinianInitializat
if (step.id === 'model') {
return {
...step,
status: modelReady ? 'success' : (settings.setupComplete === true && modelConfigReady ? 'error' : 'pending'),
status: modelReady ? 'success' : 'pending',
message: modelReady
? INTERNAL_MODEL_REF
: modelConfigReady && !modelAuthReady
? '缺少内测模型调用凭据'
: undefined,
? currentModelRef || '模型 API 可在设置中配置'
: undefined,
};
}
if (step.id === 'python') {
@@ -154,13 +170,12 @@ async function runYinianRuntimeInitialization(): Promise<YinianInitializationSta
await ensureWorkspaceFiles();
setStep('workspace', 'success', '本地工作区已准备');
setStep('model', 'running', '正在写入内测模型配置');
await seedInternalModelConfig();
await seedInternalModelAuthProfiles();
if (!(await hasYinianModelAuthProfile())) {
throw new Error('内测模型调用凭据未配置,请使用内测包或联系管理员配置模型服务');
}
setStep('model', 'success', INTERNAL_MODEL_REF);
setStep('model', 'running', '正在准备模型 API 配置');
const authSeedResult = await seedModelApiConfiguration();
const modelRef = authSeedResult.status === 'seeded'
? authSeedResult.config.modelRef
: authSeedResult.modelRef;
setStep('model', 'success', modelRef || '模型 API 可在设置中配置');
setStep('python', 'running', '正在准备文档与办公能力环境');
const officeRuntime = await ensureAuxiliaryRuntimeMarkers();
@@ -179,14 +194,15 @@ async function runYinianRuntimeInitialization(): Promise<YinianInitializationSta
logger.info('[yinian-init] First-run initialization completed', {
openclawDir: runtime.dir,
model: INTERNAL_MODEL_REF,
model: modelRef,
modelSeedStatus: authSeedResult.status,
});
return {
initialized: true,
initializedAt,
openclawDir: runtime.dir,
model: INTERNAL_MODEL_REF,
model: modelRef,
steps,
};
} catch (error) {
@@ -223,7 +239,88 @@ async function ensureWorkspaceFiles(): Promise<void> {
await mkdir(join(openclawDir, 'agents', 'main', 'agent'), { recursive: true });
}
async function seedInternalModelConfig(): Promise<void> {
async function seedModelApiConfiguration(): Promise<InternalModelAuthSeedResult> {
const bundledAuthPath = resolveBundledModelAuthPath();
if (!bundledAuthPath) {
logger.warn('[yinian-init] Model API auth bundle was not found');
const modelRef = await ensureBaseOpenClawConfig();
return {
status: 'skipped',
reason: '安装包缺少模型 API 凭据资源',
modelRef,
};
}
const bundled = await readJsonFile(bundledAuthPath);
if (bundled.bundled !== true) {
const reason = typeof bundled.reason === 'string' && bundled.reason.trim()
? bundled.reason.trim()
: '构建时未启用模型 API 凭据打包';
logger.warn('[yinian-init] Model API auth bundle is disabled', {
bundledAuthPath,
reason,
});
const modelRef = await ensureBaseOpenClawConfig();
return {
status: 'skipped',
path: bundledAuthPath,
reason,
modelRef,
};
}
let runtimeConfig: ModelRuntimeConfig;
try {
runtimeConfig = resolveModelRuntimeConfig(bundled);
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
logger.warn('[yinian-init] Model API runtime config bundle is incomplete; skipping bundled model seed', {
bundledAuthPath,
reason,
});
const modelRef = await ensureBaseOpenClawConfig();
return {
status: 'skipped',
path: bundledAuthPath,
reason,
modelRef,
};
}
const hasBundledAuth = hasUsableBundledModelAuthProfile(bundled, runtimeConfig);
const hasExistingAuth = await hasYinianModelAuthProfile(runtimeConfig.providerKey);
if (!hasBundledAuth && !hasExistingAuth) {
logger.warn('[yinian-init] Model API auth bundle is incomplete; skipping bundled model seed', { bundledAuthPath });
const modelRef = await ensureBaseOpenClawConfig();
return {
status: 'skipped',
path: bundledAuthPath,
reason: '模型 API 凭据资源不完整',
modelRef,
};
}
await seedModelApiConfig(runtimeConfig);
if (hasBundledAuth) {
await seedModelApiAuthProfiles(bundledAuthPath, bundled, runtimeConfig);
}
return {
status: 'seeded',
path: bundledAuthPath,
config: runtimeConfig,
};
}
async function seedModelApiConfig(runtimeConfig: ModelRuntimeConfig): Promise<void> {
await writeBaseOpenClawConfig(runtimeConfig);
}
async function ensureBaseOpenClawConfig(): Promise<string | undefined> {
return writeBaseOpenClawConfig();
}
async function writeBaseOpenClawConfig(runtimeConfig?: ModelRuntimeConfig): Promise<string | undefined> {
const configDir = getOpenClawConfigDir();
const configPath = join(configDir, 'openclaw.json');
await mkdir(configDir, { recursive: true });
@@ -231,21 +328,20 @@ async function seedInternalModelConfig(): Promise<void> {
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,
},
],
};
if (runtimeConfig) {
providers[runtimeConfig.providerKey] = {
baseUrl: runtimeConfig.baseUrl,
api: runtimeConfig.api,
...(typeof runtimeConfig.authHeader === 'boolean' ? { authHeader: runtimeConfig.authHeader } : {}),
models: [
{
...YINIAN_MODEL_ENTRY,
id: runtimeConfig.modelId,
name: runtimeConfig.modelName,
},
],
};
}
models.mode = 'merge';
models.providers = providers;
delete models.pricing;
@@ -261,10 +357,12 @@ async function seedInternalModelConfig(): Promise<void> {
const agents = asObject(config.agents);
const defaults = asObject(agents.defaults);
const enabledSkillIds = resolveYinianEnabledSkillIds(config);
defaults.model = {
primary: INTERNAL_MODEL_REF,
fallbacks: ['minimax/MiniMax-M2.5'],
};
if (runtimeConfig) {
defaults.model = {
primary: runtimeConfig.modelRef,
fallbacks: [...runtimeConfig.fallbackModelRefs],
};
}
defaults.workspace = join(homedir(), '.openclaw', 'workspace');
defaults.skills = enabledSkillIds;
defaults.heartbeat = {
@@ -304,20 +402,23 @@ async function seedInternalModelConfig(): Promise<void> {
config.agents = agents;
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
return getNonEmptyString(asObject(defaults.model).primary);
}
async function seedInternalModelAuthProfiles(): Promise<void> {
const bundledAuthPath = resolveBundledModelAuthPath();
if (!bundledAuthPath) return;
const bundled = await readJsonFile(bundledAuthPath);
if (bundled.bundled !== true) return;
async function seedModelApiAuthProfiles(
bundledAuthPath: string,
bundled: JsonObject,
runtimeConfig: ModelRuntimeConfig,
): Promise<void> {
const bundledStore = asObject(bundled.store);
const bundledProfiles = asObject(bundledStore.profiles);
const bundledDefault = asObject(bundledProfiles[INTERNAL_AUTH_PROFILE_ID]);
if (!isUsableMinimaxAuthProfile(bundledDefault)) {
throw new Error('内测模型凭据资源不完整');
const bundledDefault = asObject(bundledProfiles[runtimeConfig.authProfileId]);
const fallbackBundledDefault = Object.values(bundledProfiles)
.map(asObject)
.find((profile) => isUsableModelApiKeyProfile(profile));
if (!isUsableModelApiKeyProfile(bundledDefault) && !fallbackBundledDefault) {
logger.warn('[yinian-init] Model API auth bundle is incomplete', { bundledAuthPath });
throw new Error('模型 API 凭据资源不完整');
}
const authProfilesPath = join(getOpenClawConfigDir(), 'agents', 'main', 'agent', 'auth-profiles.json');
@@ -329,11 +430,16 @@ async function seedInternalModelAuthProfiles(): Promise<void> {
for (const [profileId, profile] of Object.entries(bundledProfiles)) {
const bundledProfile = asObject(profile);
if (!isUsableMinimaxAuthProfile(bundledProfile)) continue;
if (!isUsableMinimaxAuthProfile(asObject(profiles[profileId]))) {
profiles[profileId] = {
if (!isUsableModelApiKeyProfile(bundledProfile)) continue;
const targetProfileId = profileId === runtimeConfig.authProfileId
? runtimeConfig.authProfileId
: profileId.startsWith(`${runtimeConfig.providerKey}:`)
? profileId
: runtimeConfig.authProfileId;
if (!isUsableModelApiKeyProfile(asObject(profiles[targetProfileId]), runtimeConfig.providerKey)) {
profiles[targetProfileId] = {
type: 'api_key',
provider: INTERNAL_PROVIDER_KEY,
provider: runtimeConfig.providerKey,
key: bundledProfile.key,
};
changed = true;
@@ -341,24 +447,26 @@ async function seedInternalModelAuthProfiles(): Promise<void> {
}
const order = asObject(current.order) as Record<string, unknown>;
const minimaxOrder = Array.isArray(order[INTERNAL_PROVIDER_KEY])
? (order[INTERNAL_PROVIDER_KEY] as unknown[]).filter((value): value is string => typeof value === 'string')
const currentOrder = Array.isArray(order[runtimeConfig.providerKey])
? (order[runtimeConfig.providerKey] as unknown[]).filter((value): value is string => typeof value === 'string')
: [];
if (!minimaxOrder.includes(INTERNAL_AUTH_PROFILE_ID)) {
order[INTERNAL_PROVIDER_KEY] = [
INTERNAL_AUTH_PROFILE_ID,
...minimaxOrder.filter((profileId) => profileId !== INTERNAL_AUTH_PROFILE_ID),
if (!currentOrder.includes(runtimeConfig.authProfileId)) {
order[runtimeConfig.providerKey] = [
runtimeConfig.authProfileId,
...currentOrder.filter((profileId) => profileId !== runtimeConfig.authProfileId),
];
changed = true;
}
const lastGood = asObject(current.lastGood) as Record<string, unknown>;
if (lastGood[INTERNAL_PROVIDER_KEY] !== INTERNAL_AUTH_PROFILE_ID) {
lastGood[INTERNAL_PROVIDER_KEY] = INTERNAL_AUTH_PROFILE_ID;
if (lastGood[runtimeConfig.providerKey] !== runtimeConfig.authProfileId) {
lastGood[runtimeConfig.providerKey] = runtimeConfig.authProfileId;
changed = true;
}
if (!changed) return;
if (!changed) {
return;
}
current.version = typeof current.version === 'number' ? current.version : 1;
current.profiles = profiles;
current.order = order;
@@ -377,16 +485,89 @@ function resolveBundledModelAuthPath(): string | undefined {
return candidates.find((candidate) => existsSync(candidate));
}
async function hasYinianModelAuthProfile(): Promise<boolean> {
function hasUsableBundledModelAuthProfile(bundle: JsonObject, runtimeConfig: ModelRuntimeConfig): boolean {
const bundledStore = asObject(bundle.store);
const bundledProfiles = asObject(bundledStore.profiles);
const bundledDefault = asObject(bundledProfiles[runtimeConfig.authProfileId]);
if (isUsableModelApiKeyProfile(bundledDefault)) {
return true;
}
return Object.values(bundledProfiles)
.map(asObject)
.some((profile) => isUsableModelApiKeyProfile(profile));
}
function resolveModelRuntimeConfig(bundle: JsonObject): ModelRuntimeConfig {
const model = asObject(bundle.model);
const providerKey = getNonEmptyString(model.providerKey);
const modelId = getNonEmptyString(model.modelId);
const baseUrl = getNonEmptyString(model.baseUrl);
const api = getNonEmptyString(model.api);
const missingFields = [
!providerKey ? 'model.providerKey' : '',
!modelId ? 'model.modelId' : '',
!baseUrl ? 'model.baseUrl' : '',
!api ? 'model.api' : '',
].filter(Boolean);
if (missingFields.length > 0) {
throw new Error(`模型 API 配置资源不完整:缺少 ${missingFields.join(', ')}`);
}
const modelName = getNonEmptyString(model.modelName) || getNonEmptyString(model.name) || modelId;
const authProfileId = getNonEmptyString(model.authProfileId) || `${providerKey}:default`;
const fallbackModelRefs = readModelRefList(model.fallbackModelRefs)
.concat(readModelRefList(model.fallbacks))
.filter((value, index, list) => list.indexOf(value) === index);
return {
providerKey,
modelId,
modelName,
modelRef: `${providerKey}/${modelId}`,
baseUrl,
api,
authHeader: typeof model.authHeader === 'boolean' ? model.authHeader : undefined,
fallbackModelRefs,
authProfileId,
};
}
function getNonEmptyString(value: unknown): string | undefined {
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
}
function readModelRefList(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value
.map((item) => typeof item === 'string' ? item.trim() : '')
.filter(Boolean);
}
async function getConfiguredPrimaryModelRef(): Promise<string | undefined> {
const configPath = join(getOpenClawConfigDir(), 'openclaw.json');
const config = await readJsonFile(configPath);
const agents = asObject(config.agents);
const defaults = asObject(agents.defaults);
const model = asObject(defaults.model);
return getNonEmptyString(model.primary);
}
function splitProviderKey(modelRef: string): string | undefined {
const separatorIndex = modelRef.indexOf('/');
if (separatorIndex <= 0) return undefined;
return modelRef.slice(0, separatorIndex);
}
async function hasYinianModelAuthProfile(providerKey = YINIAN_MODEL_PROVIDER_KEY): Promise<boolean> {
const authProfilesPath = join(getOpenClawConfigDir(), 'agents', 'main', 'agent', 'auth-profiles.json');
const store = await readJsonFile(authProfilesPath);
const profiles = asObject(store.profiles);
return Object.values(profiles).some((profile) => isUsableMinimaxAuthProfile(asObject(profile)));
return Object.values(profiles).some((profile) => isUsableModelApiKeyProfile(asObject(profile), providerKey));
}
function isUsableMinimaxAuthProfile(profile: JsonObject): boolean {
function isUsableModelApiKeyProfile(profile: JsonObject, providerKey?: string): boolean {
return profile.type === 'api_key'
&& profile.provider === INTERNAL_PROVIDER_KEY
&& (!providerKey || profile.provider === providerKey)
&& typeof profile.key === 'string'
&& profile.key.trim().length >= 8;
}