fix: 提升Windows分发包启动兼容性

This commit is contained in:
yu 2026-10-27 16:43:28 +08:00
parent 5de47ee420
commit cb68af5a0f
3 changed files with 92 additions and 5 deletions

View File

@ -13,6 +13,8 @@ const releaseRoot = path.resolve(root, "release");
const productFolderName = `RPA-Studio-${version}-win-x64`;
const productFolder = path.join(releaseRoot, productFolderName);
const zipPath = path.join(releaseRoot, `${productFolderName}.zip`);
const setupPath = path.join(releaseRoot, `RPA-Studio-Setup-${version}-win-x64.exe`);
const setupArchive = path.join(releaseRoot, `${productFolderName}.7z`);
function assertInsideRelease(target) {
const relative = path.relative(releaseRoot, path.resolve(target));
@ -27,7 +29,7 @@ function run(command, args) {
}
fs.mkdirSync(releaseRoot, { recursive: true });
for (const target of [productFolder, zipPath]) {
for (const target of [productFolder, zipPath, setupPath, setupArchive]) {
assertInsideRelease(target);
fs.rmSync(target, { recursive: true, force: true });
}
@ -97,17 +99,61 @@ fs.writeFileSync(path.join(productFolder, "使用说明.txt"), [
"3. 任务、流程和运行记录默认保存在当前 Windows 用户的 AppData\\Roaming\\rpa-studio。",
"4. Word 自动化需要本机安装 Microsoft Word;其他文件级能力无需安装 Office。",
"5. 首次启动如被 Windows SmartScreen 提示,请核对同目录 SHA256SUMS.txt;当前构建未进行商业代码签名。",
"6. 如果双击没有窗口,请运行 启动诊断.cmd,并把自动打开的 startup.log 发给开发人员。",
"",
`版本:${version}`,
`源码提交:${revision}`,
`构建时间:${builtAt}`
].join("\r\n"), "utf8");
fs.writeFileSync(path.join(productFolder, "系统要求.txt"), [
"RPA Studio 系统要求",
"",
"- Windows 10 64 位或 Windows 11 64 位",
"- 建议至少 4 GB 内存和 500 MB 可用磁盘空间",
"- 必须先完整解压,不能直接在 ZIP 压缩包中双击 exe",
"- Word 自动化需要本机安装 Microsoft Word",
"- 当前版本未购买商业代码签名证书,SmartScreen 可能显示未知发布者",
"- 软件默认关闭 GPU 硬件加速,以兼容远程桌面、虚拟机和旧显卡驱动"
].join("\r\n"), "utf8");
fs.writeFileSync(path.join(productFolder, "启动诊断.cmd"), [
"@echo off",
"chcp 65001 >nul",
"set ELECTRON_ENABLE_LOGGING=1",
"set LOG=%APPDATA%\\rpa-studio\\logs\\startup.log",
"start \"RPA Studio\" \"%~dp0RPA Studio.exe\" --enable-logging --v=1",
"timeout /t 8 /nobreak >nul",
"if exist \"%LOG%\" (start notepad.exe \"%LOG%\") else (echo 未生成启动日志,请检查安全软件拦截记录。 & pause)"
].join("\r\n"), "utf8");
fs.writeFileSync(path.join(productFolder, "安装到本机.cmd"), [
"@echo off",
"chcp 65001 >nul",
"set \"TARGET=%LOCALAPPDATA%\\Programs\\RPA Studio\"",
"if not exist \"%TARGET%\" mkdir \"%TARGET%\"",
"robocopy \"%~dp0\" \"%TARGET%\" /E /R:2 /W:1 /NFL /NDL /NJH /NJS /NP >nul",
"powershell -NoProfile -ExecutionPolicy Bypass -Command \"$w=New-Object -ComObject WScript.Shell;$s=$w.CreateShortcut([IO.Path]::Combine([Environment]::GetFolderPath('Desktop'),'RPA Studio.lnk'));$s.TargetPath=[IO.Path]::Combine($env:LOCALAPPDATA,'Programs','RPA Studio','RPA Studio.exe');$s.WorkingDirectory=[IO.Path]::Combine($env:LOCALAPPDATA,'Programs','RPA Studio');$s.Save()\"",
"start \"RPA Studio\" \"%TARGET%\\RPA Studio.exe\"",
"exit /b 0"
].join("\r\n"), "utf8");
console.log("[package] zip");
const zip = new AdmZip();
zip.addLocalFolder(productFolder, productFolderName);
zip.writeZip(zipPath);
const sha256 = createHash("sha256").update(fs.readFileSync(zipPath)).digest("hex");
fs.writeFileSync(path.join(releaseRoot, "SHA256SUMS.txt"), `${sha256} ${path.basename(zipPath)}\r\n`, "utf8");
const zipSha256 = createHash("sha256").update(fs.readFileSync(zipPath)).digest("hex");
console.log(JSON.stringify({ productFolder, zipPath, sha256 }, null, 2));
let setupSha256 = "";
const sevenZip = "C:/Program Files/7-Zip/7z.exe";
const sfxModule = "C:/Program Files/7-Zip/7z.sfx";
if (fs.existsSync(sevenZip) && fs.existsSync(sfxModule)) {
console.log("[package] setup sfx");
const archived = spawnSync(sevenZip, ["a", "-t7z", setupArchive, ".", "-mx=9"], { cwd: productFolder, stdio: "inherit" });
if (archived.status !== 0) throw new Error(`7-Zip 打包失败,退出码 ${archived.status}`);
const config = Buffer.from(`;!@Install@!UTF-8!\nTitle=\"RPA Studio ${version}\"\nBeginPrompt=\"将 RPA Studio 安装到当前用户并创建桌面快捷方式?\"\nRunProgram=\"安装到本机.cmd\"\nGUIMode=\"1\"\n;!@InstallEnd@!\n`, "utf8");
fs.writeFileSync(setupPath, Buffer.concat([fs.readFileSync(sfxModule), config, fs.readFileSync(setupArchive)]));
setupSha256 = createHash("sha256").update(fs.readFileSync(setupPath)).digest("hex");
}
const sums = [`${zipSha256} ${path.basename(zipPath)}`];
if (setupSha256) sums.push(`${setupSha256} ${path.basename(setupPath)}`);
fs.writeFileSync(path.join(releaseRoot, "SHA256SUMS.txt"), `${sums.join("\r\n")}\r\n`, "utf8");
console.log(JSON.stringify({ productFolder, zipPath, setupPath: setupSha256 ? setupPath : null, zipSha256, setupSha256: setupSha256 || null }, null, 2));

View File

@ -7,11 +7,17 @@ import { createDemoFlow, createExampleFlows, createFlow, deleteFlow, getFlow, li
import { capabilityCount, capabilityIds } from "./engine/registry";
import { closeBrowserSession, startRun, stopRun } from "./run/manager";
import { fixtureUrl, startFixtureServer, stopFixtureServer } from "./fixture/server";
import { logMain } from "./logger";
let mainWindow: BrowserWindow | null = null;
if (process.env.RPA_ENABLE_GPU !== "1") app.disableHardwareAcceleration();
process.on("uncaughtException", (error) => logMain("uncaughtException", error.stack || error.message));
process.on("unhandledRejection", (reason) => logMain("unhandledRejection", reason instanceof Error ? reason.stack || reason.message : String(reason)));
function createWindow(): void {
const e2e = process.env.RPA_E2E === "1";
logMain("createWindow", { packaged: app.isPackaged, appPath: app.getAppPath(), userData: app.getPath("userData") });
mainWindow = new BrowserWindow({
width: 1440,
height: 900,
@ -34,7 +40,18 @@ function createWindow(): void {
}
});
mainWindow.on("ready-to-show", () => { if (!e2e) mainWindow?.show(); });
let shown = false;
const showWindow = (source: string) => {
if (e2e || shown || !mainWindow || mainWindow.isDestroyed()) return;
shown = true;
logMain("showWindow", source);
mainWindow.show();
mainWindow.focus();
};
const showFallback = setTimeout(() => showWindow("fallback-timeout"), 5000);
mainWindow.on("ready-to-show", () => { clearTimeout(showFallback); showWindow("ready-to-show"); });
mainWindow.on("unresponsive", () => logMain("window-unresponsive"));
mainWindow.on("responsive", () => logMain("window-responsive"));
mainWindow.on("closed", () => {
mainWindow = null;
});
@ -42,6 +59,14 @@ function createWindow(): void {
if (/^https?:/i.test(url)) void shell.openExternal(url);
return { action: "deny" };
});
mainWindow.webContents.on("did-fail-load", (_event, code, description, url) => {
logMain("did-fail-load", { code, description, url });
showWindow("did-fail-load");
});
mainWindow.webContents.on("render-process-gone", (_event, details) => logMain("render-process-gone", details));
mainWindow.webContents.on("console-message", (_event, level, message, line, sourceId) => {
if (level >= 2) logMain("renderer-console", { level, message, line, sourceId });
});
const devUrl = process.env.ELECTRON_RENDERER_URL;
if (devUrl) {
@ -121,10 +146,12 @@ if (!gotLock) {
});
app.whenReady().then(async () => {
logMain("app-ready", { electron: process.versions.electron, chrome: process.versions.chrome, node: process.versions.node });
try {
await startFixtureServer(Number(process.env.RPA_FIXTURE_PORT || 17863));
} catch (error) {
console.warn("local fixture server unavailable", error);
logMain("fixture-server-failed", error instanceof Error ? error.stack || error.message : String(error));
}
registerIpc();
createWindow();

14
src/main/logger.ts Normal file
View File

@ -0,0 +1,14 @@
import { app } from "electron";
import fs from "node:fs";
import path from "node:path";
export function logMain(event: string, detail: unknown = ""): void {
try {
const directory = path.join(app.getPath("userData"), "logs");
fs.mkdirSync(directory, { recursive: true });
const content = typeof detail === "string" ? detail : JSON.stringify(detail);
fs.appendFileSync(path.join(directory, "startup.log"), `${new Date().toISOString()} [${event}] ${content}\r\n`, "utf8");
} catch {
// Logging must never prevent the app from starting.
}
}