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

@@ -0,0 +1,355 @@
import { app } from 'electron';
import { spawn } from 'node:child_process';
import { createRequire } from 'node:module';
import { existsSync } from 'node:fs';
import path from 'node:path';
import { getUvMirrorEnv } from './uv-env';
import {
findManagedPythonPath,
resolveUvBin,
setupManagedPython,
} from './uv-setup';
import { logger } from './logger';
import { getOpenClawConfigDir, needsWinShell, quoteForCmd } from './paths';
import { buildDotnetEnv, resolveDotnetExecutable } from './dotnet-runtime';
export type OfficeRuntimeStatus = 'ok' | 'warning' | 'error';
export interface OfficeRuntimeCheck {
id: string;
label: string;
status: OfficeRuntimeStatus;
detail: string;
}
export interface OfficeSkillRuntimeDiagnostics {
capturedAt: number;
ok: boolean;
repairAttempted: boolean;
python: {
executable: string | null;
packages: Array<{
packageName: string;
importName: string;
installed: boolean;
}>;
};
node: {
modules: Array<{
name: string;
installed: boolean;
resolvedPath: string | null;
}>;
};
dotnet: {
available: boolean;
version: string | null;
};
checks: OfficeRuntimeCheck[];
}
type CommandResult = {
ok: boolean;
stdout: string;
stderr: string;
code: number | null;
error?: string;
};
const PYTHON_PACKAGES = [
{ packageName: 'pypdf', importName: 'pypdf' },
{ packageName: 'reportlab', importName: 'reportlab' },
{ packageName: 'pandas', importName: 'pandas' },
{ packageName: 'openpyxl', importName: 'openpyxl' },
{ packageName: 'matplotlib', importName: 'matplotlib' },
{ packageName: 'beautifulsoup4', importName: 'bs4' },
] as const;
const NODE_MODULES = [
'docx',
'pptxgenjs',
'react-icons',
'react',
'react-dom/server',
'sharp',
] as const;
const PYTHON_INSTALL_TIMEOUT_MS = 10 * 60_000;
const COMMAND_TIMEOUT_MS = 20_000;
let warmupPromise: Promise<OfficeSkillRuntimeDiagnostics> | null = null;
function appendOutput(current: string, chunk: Buffer): string {
const next = current + chunk.toString('utf8');
return next.length <= 12_000 ? next : next.slice(next.length - 12_000);
}
function runCommand(
command: string,
args: string[],
options: {
env?: Record<string, string | undefined>;
timeoutMs?: number;
shell?: boolean;
} = {},
): Promise<CommandResult> {
return new Promise((resolve) => {
let settled = false;
let stdout = '';
let stderr = '';
const finish = (result: CommandResult) => {
if (settled) return;
settled = true;
resolve(result);
};
try {
const child = spawn(command, args, {
env: options.env,
shell: options.shell,
windowsHide: true,
});
const timeout = setTimeout(() => {
try {
child.kill();
} catch {
// ignore
}
finish({
ok: false,
stdout,
stderr,
code: null,
error: `Timed out after ${options.timeoutMs ?? COMMAND_TIMEOUT_MS}ms`,
});
}, options.timeoutMs ?? COMMAND_TIMEOUT_MS);
child.stdout?.on('data', (data) => {
stdout = appendOutput(stdout, data);
});
child.stderr?.on('data', (data) => {
stderr = appendOutput(stderr, data);
});
child.on('error', (error) => {
clearTimeout(timeout);
finish({
ok: false,
stdout,
stderr,
code: null,
error: error.message,
});
});
child.on('close', (code) => {
clearTimeout(timeout);
finish({
ok: code === 0,
stdout,
stderr,
code,
});
});
} catch (error) {
finish({
ok: false,
stdout,
stderr,
code: null,
error: error instanceof Error ? error.message : String(error),
});
}
});
}
function buildRequireCandidates(): NodeRequire[] {
const candidates: string[] = [
path.join(process.cwd(), 'package.json'),
path.join(app.getAppPath(), 'package.json'),
path.join(process.resourcesPath || '', 'app.asar', 'package.json'),
process.resourcesPath ? path.join(process.resourcesPath, 'openclaw', 'package.json') : '',
path.join(getOpenClawConfigDir(), 'runtime', 'openclaw', 'package.json'),
].filter(Boolean);
const requires: NodeRequire[] = [createRequire(import.meta.url)];
for (const candidate of candidates) {
try {
if (existsSync(candidate)) {
requires.push(createRequire(candidate));
}
} catch {
// ignore invalid require roots
}
}
return requires;
}
function resolveNodeModule(name: string): string | null {
for (const req of buildRequireCandidates()) {
try {
return req.resolve(name);
} catch {
// try next candidate
}
}
return null;
}
async function checkPythonPackage(pythonPath: string | null, importName: string): Promise<boolean> {
if (!pythonPath) return false;
const result = await runCommand(pythonPath, ['-c', `__import__(${JSON.stringify(importName)})`]);
return result.ok;
}
async function installPythonPackages(pythonPath: string, packageNames: string[]): Promise<void> {
if (packageNames.length === 0) return;
const { bin: uvBin } = resolveUvBin();
const useShell = needsWinShell(uvBin);
const uvEnv = await getUvMirrorEnv();
const args = ['pip', 'install', '--python', pythonPath, '--break-system-packages', ...packageNames];
const baseEnv = { ...process.env };
const first = await runCommand(useShell ? quoteForCmd(uvBin) : uvBin, args, {
shell: useShell,
env: { ...baseEnv, ...uvEnv },
timeoutMs: PYTHON_INSTALL_TIMEOUT_MS,
});
if (first.ok) return;
if (Object.keys(uvEnv).length > 0) {
const second = await runCommand(useShell ? quoteForCmd(uvBin) : uvBin, args, {
shell: useShell,
env: baseEnv,
timeoutMs: PYTHON_INSTALL_TIMEOUT_MS,
});
if (second.ok) return;
throw new Error(second.stderr || second.stdout || second.error || 'uv pip install failed');
}
throw new Error(first.stderr || first.stdout || first.error || 'uv pip install failed');
}
async function checkDotnet(): Promise<{ available: boolean; version: string | null }> {
const env = buildDotnetEnv(process.env as Record<string, string | undefined>);
const dotnetBin = resolveDotnetExecutable() ?? 'dotnet';
const result = await runCommand(dotnetBin, ['--version'], { env });
return {
available: result.ok,
version: result.ok ? result.stdout.trim() || null : null,
};
}
export async function buildOfficeSkillRuntimeDiagnostics(options: {
repair?: boolean;
} = {}): Promise<OfficeSkillRuntimeDiagnostics> {
const repairAttempted = options.repair === true;
if (repairAttempted) {
await setupManagedPython();
}
let pythonPath = await findManagedPythonPath();
let packageResults = await Promise.all(
PYTHON_PACKAGES.map(async (pkg) => ({
...pkg,
installed: await checkPythonPackage(pythonPath, pkg.importName),
})),
);
if (repairAttempted && pythonPath) {
const missing = packageResults
.filter((pkg) => !pkg.installed)
.map((pkg) => pkg.packageName);
if (missing.length > 0) {
logger.info(`[office-runtime] Installing missing Python document packages: ${missing.join(', ')}`);
await installPythonPackages(pythonPath, missing);
pythonPath = await findManagedPythonPath();
packageResults = await Promise.all(
PYTHON_PACKAGES.map(async (pkg) => ({
...pkg,
installed: await checkPythonPackage(pythonPath, pkg.importName),
})),
);
}
}
const nodeModules = NODE_MODULES.map((name) => {
const resolvedPath = resolveNodeModule(name);
return {
name,
installed: Boolean(resolvedPath),
resolvedPath,
};
});
const dotnet = await checkDotnet();
const missingPythonPackages = packageResults.filter((pkg) => !pkg.installed);
const missingNodeModules = nodeModules.filter((mod) => !mod.installed);
const checks: OfficeRuntimeCheck[] = [
{
id: 'python',
label: 'Python 运行环境',
status: pythonPath ? 'ok' : 'error',
detail: pythonPath ?? '未找到 uv 管理的 Python 3.12',
},
{
id: 'python-packages',
label: 'PDF/表格依赖',
status: missingPythonPackages.length === 0 ? 'ok' : 'error',
detail: missingPythonPackages.length === 0
? '已安装常用 PDF 与表格处理依赖'
: `缺少 ${missingPythonPackages.map((pkg) => pkg.packageName).join(', ')}`,
},
{
id: 'node-modules',
label: '文档生成依赖',
status: missingNodeModules.length === 0 ? 'ok' : 'error',
detail: missingNodeModules.length === 0
? '已找到项目内置文档生成依赖'
: `缺少 ${missingNodeModules.map((mod) => mod.name).join(', ')}`,
},
{
id: 'dotnet',
label: 'Word 高级生成',
status: dotnet.available ? 'ok' : 'warning',
detail: dotnet.available
? `.NET ${dotnet.version}`
: '未找到 .NETdocx 高级 OpenXML 生成可能受限',
},
];
return {
capturedAt: Date.now(),
ok: checks.every((check) => check.status !== 'error'),
repairAttempted,
python: {
executable: pythonPath,
packages: packageResults,
},
node: {
modules: nodeModules,
},
dotnet,
checks,
};
}
export async function ensureOfficeSkillRuntimeReady(): Promise<OfficeSkillRuntimeDiagnostics> {
return await buildOfficeSkillRuntimeDiagnostics({ repair: true });
}
export function warmupOfficeSkillRuntimeReadiness(): void {
if (warmupPromise) return;
warmupPromise = ensureOfficeSkillRuntimeReady()
.then((diagnostics) => {
const summary = diagnostics.checks.map((check) => `${check.id}:${check.status}`).join(', ');
logger.info(`[office-runtime] Warmup complete (${summary})`);
return diagnostics;
})
.catch((error) => {
logger.warn('[office-runtime] Warmup failed:', error);
throw error;
})
.finally(() => {
warmupPromise = null;
});
}