chore: stabilize Zhinian pilot delivery

This commit is contained in:
inman
2026-05-12 19:44:44 +08:00
parent 45389855e1
commit 20b5aff4ad
174 changed files with 41428 additions and 784 deletions

View File

@@ -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];
}