chore: stabilize Zhinian pilot delivery
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { getAllSettings, setSetting } from './store';
|
||||
import { getOpenClawConfigDir, reinstallManagedOpenClawRuntime } from './paths';
|
||||
import { logger } from './logger';
|
||||
import { ensureOfficeSkillRuntimeReady } from './office-skill-runtime';
|
||||
|
||||
type JsonObject = Record<string, unknown>;
|
||||
|
||||
@@ -28,8 +29,17 @@ export interface YinianInitializationStatus {
|
||||
const INTERNAL_PROVIDER_KEY = 'minimax';
|
||||
const INTERNAL_MODEL_ID = 'MiniMax-M2.7';
|
||||
const INTERNAL_MODEL_REF = `${INTERNAL_PROVIDER_KEY}/${INTERNAL_MODEL_ID}`;
|
||||
const INTERNAL_MODEL_TIMEOUT_SECONDS = 300;
|
||||
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', 'AGENTS.md'),
|
||||
join('docs', 'reference', 'templates', 'TOOLS.md'),
|
||||
join('docs', 'reference', 'templates', 'HEARTBEAT.md'),
|
||||
join('node_modules', 'openclaw', 'package.json'),
|
||||
] as const;
|
||||
let initializationInFlight: Promise<YinianInitializationStatus> | null = null;
|
||||
|
||||
const DEFAULT_STEPS: YinianInitializationStep[] = [
|
||||
@@ -41,17 +51,65 @@ const DEFAULT_STEPS: YinianInitializationStep[] = [
|
||||
|
||||
export async function getYinianInitializationStatus(): Promise<YinianInitializationStatus> {
|
||||
const settings = await getAllSettings();
|
||||
const initialized = settings.setupComplete === true;
|
||||
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 officeMarkerReady = existsSync(join(getOpenClawConfigDir(), 'runtime', 'yinian-initialized.json'));
|
||||
const initialized = settings.setupComplete === true
|
||||
&& runtimeStatus.ok
|
||||
&& workspaceReady
|
||||
&& modelReady
|
||||
&& officeMarkerReady;
|
||||
|
||||
if (settings.setupComplete === true && !initialized) {
|
||||
await setSetting('setupComplete', false);
|
||||
logger.warn('[yinian-init] Existing setupComplete flag was reset because runtime verification failed', {
|
||||
runtimeMissing: runtimeStatus.missing,
|
||||
workspaceReady,
|
||||
modelConfigReady,
|
||||
modelAuthReady,
|
||||
officeMarkerReady,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
initialized,
|
||||
initializedAt: settings.openclawInitializedAt,
|
||||
openclawDir: initialized ? join(getOpenClawConfigDir(), 'runtime', 'openclaw') : undefined,
|
||||
openclawDir: runtimeStatus.runtimeDir,
|
||||
model: initialized ? INTERNAL_MODEL_REF : undefined,
|
||||
steps: DEFAULT_STEPS.map((step) => ({
|
||||
...step,
|
||||
status: initialized ? 'success' : 'pending',
|
||||
message: initialized ? '已完成' : undefined,
|
||||
})),
|
||||
steps: DEFAULT_STEPS.map((step) => {
|
||||
if (step.id === 'runtime') {
|
||||
return {
|
||||
...step,
|
||||
status: runtimeStatus.ok ? 'success' : (settings.setupComplete === true ? 'error' : 'pending'),
|
||||
message: runtimeStatus.ok
|
||||
? runtimeStatus.runtimeDir
|
||||
: runtimeStatus.missing.length > 0
|
||||
? `缺少运行文件:${runtimeStatus.missing.join(', ')}`
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
if (step.id === 'workspace') {
|
||||
return { ...step, status: workspaceReady ? 'success' : 'pending', message: workspaceReady ? '本地工作区已准备' : undefined };
|
||||
}
|
||||
if (step.id === 'model') {
|
||||
return {
|
||||
...step,
|
||||
status: modelReady ? 'success' : (settings.setupComplete === true && modelConfigReady ? 'error' : 'pending'),
|
||||
message: modelReady
|
||||
? INTERNAL_MODEL_REF
|
||||
: modelConfigReady && !modelAuthReady
|
||||
? '缺少内测模型调用凭据'
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
if (step.id === 'python') {
|
||||
return { ...step, status: officeMarkerReady ? 'success' : 'pending', message: officeMarkerReady ? '文档处理环境已准备' : undefined };
|
||||
}
|
||||
return { ...step };
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,6 +144,10 @@ async function runYinianRuntimeInitialization(): Promise<YinianInitializationSta
|
||||
if (runtime.source === 'missing') {
|
||||
throw new Error('内置 OpenClaw 运行环境不可用');
|
||||
}
|
||||
const runtimeStatus = getManagedRuntimeVerification();
|
||||
if (!runtimeStatus.ok) {
|
||||
throw new Error(`OpenClaw 运行环境不完整:${runtimeStatus.missing.join(', ')}`);
|
||||
}
|
||||
setStep('runtime', 'success', runtime.dir);
|
||||
|
||||
setStep('workspace', 'running', '正在创建本地工作区');
|
||||
@@ -94,11 +156,22 @@ async function runYinianRuntimeInitialization(): Promise<YinianInitializationSta
|
||||
|
||||
setStep('model', 'running', '正在写入内测模型配置');
|
||||
await seedInternalModelConfig();
|
||||
await seedInternalModelAuthProfiles();
|
||||
if (!(await hasYinianModelAuthProfile())) {
|
||||
throw new Error('内测模型调用凭据未配置,请使用内测包或联系管理员配置模型服务');
|
||||
}
|
||||
setStep('model', 'success', INTERNAL_MODEL_REF);
|
||||
|
||||
setStep('python', 'running', '正在准备文档处理环境');
|
||||
await ensureAuxiliaryRuntimeMarkers();
|
||||
setStep('python', 'success', '文档处理环境已准备');
|
||||
setStep('python', 'running', '正在准备文档与办公能力环境');
|
||||
const officeRuntime = await ensureAuxiliaryRuntimeMarkers();
|
||||
if (!officeRuntime.ok) {
|
||||
const failed = officeRuntime.checks.filter((check) => check.status === 'error');
|
||||
throw new Error(`文档处理环境未完成:${failed.map((check) => check.detail).join(';')}`);
|
||||
}
|
||||
const dotnetCheck = officeRuntime.checks.find((check) => check.id === 'dotnet');
|
||||
setStep('python', 'success', dotnetCheck?.status === 'warning'
|
||||
? `文档处理环境已准备;${dotnetCheck.detail}`
|
||||
: '文档处理环境已准备');
|
||||
|
||||
const initializedAt = Date.now();
|
||||
await setSetting('setupComplete', true);
|
||||
@@ -117,6 +190,7 @@ async function runYinianRuntimeInitialization(): Promise<YinianInitializationSta
|
||||
steps,
|
||||
};
|
||||
} catch (error) {
|
||||
await setSetting('setupComplete', false);
|
||||
const running = steps.find((step) => step.status === 'running');
|
||||
if (running) {
|
||||
running.status = 'error';
|
||||
@@ -130,6 +204,18 @@ async function runYinianRuntimeInitialization(): Promise<YinianInitializationSta
|
||||
}
|
||||
}
|
||||
|
||||
function getManagedRuntimeVerification(): { runtimeDir: string; ok: boolean; missing: string[] } {
|
||||
const runtimeDir = join(getOpenClawConfigDir(), 'runtime', 'openclaw');
|
||||
const missing = REQUIRED_RUNTIME_FILES.filter((relativePath) => (
|
||||
!existsSync(join(runtimeDir, relativePath))
|
||||
));
|
||||
return {
|
||||
runtimeDir,
|
||||
ok: missing.length === 0,
|
||||
missing,
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureWorkspaceFiles(): Promise<void> {
|
||||
const openclawDir = getOpenClawConfigDir();
|
||||
const workspaceDir = join(openclawDir, 'workspace');
|
||||
@@ -145,12 +231,10 @@ async function seedInternalModelConfig(): Promise<void> {
|
||||
const config = await readJsonFile(configPath);
|
||||
const models = asObject(config.models);
|
||||
const providers = asObject(models.providers);
|
||||
const pricing = asObject(models.pricing);
|
||||
providers[INTERNAL_PROVIDER_KEY] = {
|
||||
baseUrl: 'https://api.minimaxi.com/anthropic',
|
||||
api: 'anthropic-messages',
|
||||
authHeader: true,
|
||||
timeoutSeconds: INTERNAL_MODEL_TIMEOUT_SECONDS,
|
||||
models: [
|
||||
{
|
||||
id: INTERNAL_MODEL_ID,
|
||||
@@ -162,10 +246,9 @@ async function seedInternalModelConfig(): Promise<void> {
|
||||
},
|
||||
],
|
||||
};
|
||||
pricing.enabled = false;
|
||||
models.mode = 'merge';
|
||||
models.providers = providers;
|
||||
models.pricing = pricing;
|
||||
delete models.pricing;
|
||||
config.models = models;
|
||||
|
||||
const tools = asObject(config.tools);
|
||||
@@ -177,12 +260,13 @@ 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'],
|
||||
};
|
||||
defaults.workspace = join(homedir(), '.openclaw', 'workspace');
|
||||
defaults.skills = [];
|
||||
defaults.skills = enabledSkillIds;
|
||||
defaults.heartbeat = {
|
||||
...asObject(defaults.heartbeat),
|
||||
every: '0m',
|
||||
@@ -196,7 +280,7 @@ async function seedInternalModelConfig(): Promise<void> {
|
||||
default: true,
|
||||
workspace: join(homedir(), '.openclaw', 'workspace'),
|
||||
agentDir: '~/.openclaw/agents/main/agent',
|
||||
skills: [],
|
||||
skills: enabledSkillIds,
|
||||
tools: { profile: DESKTOP_TOOLS_PROFILE },
|
||||
},
|
||||
];
|
||||
@@ -204,9 +288,12 @@ async function seedInternalModelConfig(): Promise<void> {
|
||||
agents.list = agents.list.map((entry) => {
|
||||
const agent = asObject(entry);
|
||||
if (agent.id !== 'main') return entry;
|
||||
const currentSkills = Array.isArray(agent.skills)
|
||||
? agent.skills.filter((value): value is string => typeof value === 'string')
|
||||
: [];
|
||||
return {
|
||||
...agent,
|
||||
skills: Array.isArray(agent.skills) ? agent.skills : [],
|
||||
skills: arraysEqual(currentSkills, enabledSkillIds) ? currentSkills : enabledSkillIds,
|
||||
tools: {
|
||||
...asObject(agent.tools),
|
||||
profile: DESKTOP_TOOLS_PROFILE,
|
||||
@@ -219,13 +306,112 @@ async function seedInternalModelConfig(): Promise<void> {
|
||||
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
async function ensureAuxiliaryRuntimeMarkers(): Promise<void> {
|
||||
async function seedInternalModelAuthProfiles(): Promise<void> {
|
||||
const bundledAuthPath = resolveBundledModelAuthPath();
|
||||
if (!bundledAuthPath) return;
|
||||
|
||||
const bundled = await readJsonFile(bundledAuthPath);
|
||||
if (bundled.bundled !== true) return;
|
||||
|
||||
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 authProfilesPath = join(getOpenClawConfigDir(), 'agents', 'main', 'agent', 'auth-profiles.json');
|
||||
await mkdir(dirname(authProfilesPath), { recursive: true });
|
||||
|
||||
const current = await readJsonFile(authProfilesPath);
|
||||
const profiles = asObject(current.profiles);
|
||||
let changed = false;
|
||||
|
||||
for (const [profileId, profile] of Object.entries(bundledProfiles)) {
|
||||
const bundledProfile = asObject(profile);
|
||||
if (!isUsableMinimaxAuthProfile(bundledProfile)) continue;
|
||||
if (!isUsableMinimaxAuthProfile(asObject(profiles[profileId]))) {
|
||||
profiles[profileId] = {
|
||||
type: 'api_key',
|
||||
provider: INTERNAL_PROVIDER_KEY,
|
||||
key: bundledProfile.key,
|
||||
};
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
: [];
|
||||
if (!minimaxOrder.includes(INTERNAL_AUTH_PROFILE_ID)) {
|
||||
order[INTERNAL_PROVIDER_KEY] = [
|
||||
INTERNAL_AUTH_PROFILE_ID,
|
||||
...minimaxOrder.filter((profileId) => profileId !== INTERNAL_AUTH_PROFILE_ID),
|
||||
];
|
||||
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;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed) return;
|
||||
current.version = typeof current.version === 'number' ? current.version : 1;
|
||||
current.profiles = profiles;
|
||||
current.order = order;
|
||||
current.lastGood = lastGood;
|
||||
await writeFile(authProfilesPath, `${JSON.stringify(current, null, 2)}\n`, 'utf8');
|
||||
}
|
||||
|
||||
function resolveBundledModelAuthPath(): string | undefined {
|
||||
const candidates = [
|
||||
process.resourcesPath
|
||||
? join(process.resourcesPath, 'resources', 'yinian-internal', 'model-auth-profiles.json')
|
||||
: '',
|
||||
join(process.cwd(), 'build', 'yinian-internal', 'model-auth-profiles.json'),
|
||||
].filter(Boolean);
|
||||
|
||||
return candidates.find((candidate) => existsSync(candidate));
|
||||
}
|
||||
|
||||
async function hasYinianModelAuthProfile(): 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)));
|
||||
}
|
||||
|
||||
function isUsableMinimaxAuthProfile(profile: JsonObject): boolean {
|
||||
return profile.type === 'api_key'
|
||||
&& profile.provider === INTERNAL_PROVIDER_KEY
|
||||
&& typeof profile.key === 'string'
|
||||
&& profile.key.trim().length >= 8;
|
||||
}
|
||||
|
||||
async function ensureAuxiliaryRuntimeMarkers(): Promise<Awaited<ReturnType<typeof ensureOfficeSkillRuntimeReady>>> {
|
||||
const dir = join(getOpenClawConfigDir(), 'runtime');
|
||||
await mkdir(dir, { recursive: true });
|
||||
const officeRuntime = await ensureOfficeSkillRuntimeReady();
|
||||
await writeFile(join(dir, 'yinian-initialized.json'), JSON.stringify({
|
||||
initializedAt: Date.now(),
|
||||
documentRuntime: 'bundled',
|
||||
officeRuntime: {
|
||||
ok: officeRuntime.ok,
|
||||
checks: officeRuntime.checks,
|
||||
python: {
|
||||
executable: officeRuntime.python.executable,
|
||||
packages: officeRuntime.python.packages.map((pkg) => ({
|
||||
packageName: pkg.packageName,
|
||||
installed: pkg.installed,
|
||||
})),
|
||||
},
|
||||
dotnet: officeRuntime.dotnet,
|
||||
},
|
||||
}, null, 2), 'utf8');
|
||||
return officeRuntime;
|
||||
}
|
||||
|
||||
async function readJsonFile(filePath: string): Promise<JsonObject> {
|
||||
@@ -244,3 +430,22 @@ function asObject(value: unknown): JsonObject {
|
||||
? value as JsonObject
|
||||
: {};
|
||||
}
|
||||
|
||||
function arraysEqual(left: string[], right: string[]): boolean {
|
||||
return left.length === right.length && left.every((value, index) => value === right[index]);
|
||||
}
|
||||
|
||||
function resolveYinianEnabledSkillIds(config: JsonObject): string[] {
|
||||
const skills = asObject(config.skills);
|
||||
const entries = asObject(skills.entries);
|
||||
const enabled = Object.entries(entries)
|
||||
.filter(([, value]) => asObject(value).enabled !== false)
|
||||
.map(([id]) => id.trim())
|
||||
.filter(Boolean);
|
||||
const unique = [...new Set(enabled)];
|
||||
const ordered = [
|
||||
...YINIAN_FALLBACK_SKILL_IDS.filter((id) => unique.includes(id)),
|
||||
...unique.filter((id) => !YINIAN_FALLBACK_SKILL_IDS.includes(id)).sort((left, right) => left.localeCompare(right)),
|
||||
];
|
||||
return ordered.length > 0 ? ordered : [...YINIAN_FALLBACK_SKILL_IDS];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user