Compare commits

2 Commits

23 changed files with 1086 additions and 39 deletions

View File

@@ -1,45 +1,45 @@
{
"generatedAt": "2026-04-19T12:02:30.404Z",
"generatedAt": "2026-04-27T07:58:41.747Z",
"skills": [
{
"slug": "pdf",
"version": "2c7ec5e78b8e5d43ea02e90bb8826f6b9f147b0c",
"version": "5128e1865d670f5d6c9cef000e6dfc4e951fb5b9",
"repo": "anthropics/skills",
"repoPath": "skills/pdf",
"ref": "main",
"commit": "2c7ec5e78b8e5d43ea02e90bb8826f6b9f147b0c"
"commit": "5128e1865d670f5d6c9cef000e6dfc4e951fb5b9"
},
{
"slug": "xlsx",
"version": "2c7ec5e78b8e5d43ea02e90bb8826f6b9f147b0c",
"version": "5128e1865d670f5d6c9cef000e6dfc4e951fb5b9",
"repo": "anthropics/skills",
"repoPath": "skills/xlsx",
"ref": "main",
"commit": "2c7ec5e78b8e5d43ea02e90bb8826f6b9f147b0c"
"commit": "5128e1865d670f5d6c9cef000e6dfc4e951fb5b9"
},
{
"slug": "docx",
"version": "2c7ec5e78b8e5d43ea02e90bb8826f6b9f147b0c",
"version": "5128e1865d670f5d6c9cef000e6dfc4e951fb5b9",
"repo": "anthropics/skills",
"repoPath": "skills/docx",
"ref": "main",
"commit": "2c7ec5e78b8e5d43ea02e90bb8826f6b9f147b0c"
"commit": "5128e1865d670f5d6c9cef000e6dfc4e951fb5b9"
},
{
"slug": "pptx",
"version": "2c7ec5e78b8e5d43ea02e90bb8826f6b9f147b0c",
"version": "5128e1865d670f5d6c9cef000e6dfc4e951fb5b9",
"repo": "anthropics/skills",
"repoPath": "skills/pptx",
"ref": "main",
"commit": "2c7ec5e78b8e5d43ea02e90bb8826f6b9f147b0c"
"commit": "5128e1865d670f5d6c9cef000e6dfc4e951fb5b9"
},
{
"slug": "find-skills",
"version": "bc21a37a12b90fcb5aec051c91baf5b227b704b1",
"version": "5516b8ad07393f35af4b50238b9d3f7cceca5f1e",
"repo": "vercel-labs/skills",
"repoPath": "skills/find-skills",
"ref": "main",
"commit": "bc21a37a12b90fcb5aec051c91baf5b227b704b1"
"commit": "5516b8ad07393f35af4b50238b9d3f7cceca5f1e"
},
{
"slug": "self-improving-agent",

4
dist/index.html vendored
View File

@@ -8,8 +8,8 @@
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: http://8.138.234.141 https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com; connect-src 'self' http://8.138.234.141 https://api.iconify.design wss://onefeel.brother7.cn"
/>
<script type="module" crossorigin src="./assets/index-Dup_ea5v.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DEZ3n45h.css">
<script type="module" crossorigin src="./assets/index-B3Rs2JJn.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-s_BVsofG.css">
</head>
<body>
<div id="app"></div>

View File

@@ -0,0 +1,344 @@
# 桌面端打包流程
本文用于梳理 `zn-ai` 当前仓库的 Windows / macOS 打包流程,说明应该先执行哪个脚本、每个脚本会自动带哪些前置步骤,以及常见使用场景。
## 1. 先记结论
如果你只是想正常出包,不需要手动先跑 `bundle-openclaw``bundle:preinstalled-skills``uv:download:*`
直接按目标平台执行:
- Windows 安装包:`pnpm run package:win`
- mac 双架构包:`pnpm run package:mac`
- mac Intel 单架构包:`pnpm run package:mac:x64`
- mac Apple Silicon 单架构包:`pnpm run package:mac:arm64`
也就是说:
- 要打 Windows 包,优先跑 `package:win`
- 要打 mac 包,优先跑 `package:mac` 或具体架构脚本
- `package` 只是“准备打包产物”,不是最终安装包命令
- `build` 是“当前宿主机默认 electron-builder 打包”,不如平台脚本直观,日常不建议优先用
## 2. 推荐执行顺序
### 2.1 第一次打包或切分支后
1. `pnpm install`
2. `pnpm typecheck`
3. 按目标平台执行对应脚本
推荐命令:
```powershell
pnpm install
pnpm typecheck
pnpm run package:win
```
或:
```powershell
pnpm install
pnpm typecheck
pnpm run package:mac
```
### 2.2 日常出 Windows 包
```powershell
pnpm run package:win
```
### 2.3 日常出 mac 包
如果希望一次出 x64 + arm64
```powershell
pnpm run package:mac
```
如果只想出单一架构:
```powershell
pnpm run package:mac:x64
```
或:
```powershell
pnpm run package:mac:arm64
```
## 3. 各脚本职责
### 3.1 `pnpm run package`
这是“打包前准备阶段”,会自动执行:
1. `prepackage`
2. `vite build`
3. `node scripts/bundle-openclaw.mjs`
4. `zx scripts/bundle-preinstalled-skills.mjs`
所以它会完成:
- 检查并补齐平台所需的 bundled runtime binaries
- 生成 `dist``dist-electron`
- 生成 `build/openclaw`
- 生成 `build/preinstalled-skills`
但是它不会直接产出最终安装包。
### 3.2 `pnpm run package:win`
等价于:
```powershell
pnpm run package
electron-builder --win --publish never
```
用途:
- 产出 Windows NSIS 安装包
- 当前配置只打 `x64`
### 3.3 `pnpm run package:mac`
等价于:
```powershell
pnpm run package
electron-builder --mac --publish never
```
用途:
- 产出 macOS 包
- 当前配置目标包含 `dmg``zip`
- 当前配置默认覆盖 `x64``arm64`
### 3.4 `pnpm run build`
等价于:
```powershell
pnpm run package
electron-builder
```
特点:
- 使用 `electron-builder.yml` 默认目标
- 更适合“按宿主机默认配置整体打一遍”
- 不如 `package:win` / `package:mac` 明确
## 4. 打包时自动发生的事
### 4.1 `prepackage`
`prepackage` 会自动执行:
```powershell
node scripts/ensure-bundled-runtime-binaries.mjs
```
这一步会按当前平台检查 `resources/bin` 下的运行时文件是否齐全。
当前规则:
- Windows检查 `uv.exe``node.exe`
- macOS检查 `uv`
- Linux检查 `uv`
如果缺失,会自动调用对应下载脚本:
- Windows`uv:download:win``node:download:win`
- macOS`uv:download:mac`
- Linux`uv:download:linux`
因此正常情况下,你不需要手动先跑这些下载脚本。
### 4.2 `afterPack`
`electron-builder` 打包后会自动执行:
```powershell
scripts/after-pack.cjs
```
它会继续完成这些动作:
-`resources/bin/<platform-arch>/...` 平铺复制到安装包里的 `resources/bin/`
-`electron/scripts` 处理到包内
- 补拷 `playwright``playwright-core``chromium-bidi``bytenode`
- 补拷 `build/openclaw`
- 清理不必要的开发文件,缩小包体
所以如果你是正常走 `package:win` / `package:mac`,这些都已经自动串好了。
## 5. 什么时候手动跑单独脚本
### 5.1 手动预拉 Windows 运行时
如果你只是想提前把 Windows 运行时二进制准备好,可以执行:
```powershell
pnpm run prep:win-binaries
```
它会跑:
```powershell
pnpm run uv:download:win
pnpm run node:download:win
```
适合场景:
- 网络较慢,想在正式打包前先把依赖下载好
- 排查 `resources/bin/win32-*` 是否齐全
### 5.2 手动更新预装 skills
如果你只想刷新预装 skills不是正式出包可以执行
```powershell
pnpm run bundle:preinstalled-skills
```
### 5.3 手动更新 OpenClaw 运行时
如果你只想刷新 `build/openclaw`,可以执行:
```powershell
pnpm run bundle:openclaw
```
## 6. 产物位置
最终安装包 / 压缩包输出目录:
```text
release/
```
打包中间产物目录:
```text
dist/
dist-electron/
build/openclaw/
build/preinstalled-skills/
```
## 7. Windows / mac 实操建议
### 7.1 Windows
推荐最简流程:
```powershell
pnpm install
pnpm typecheck
pnpm run package:win
```
说明:
- 当前 `electron-builder.yml` 的 Windows 目标是 `nsis`
- 当前只配置了 `x64`
### 7.2 macOS
推荐最简流程:
```powershell
pnpm install
pnpm typecheck
pnpm run package:mac
```
如果只打单架构:
```powershell
pnpm run package:mac:x64
```
或:
```powershell
pnpm run package:mac:arm64
```
如果只是本地快速验包,不追求最大压缩率:
```powershell
pnpm run package:mac:fast
```
如果想保留 `afterPack` 清理前的更多内容,便于本地排查:
```powershell
pnpm run package:mac:local
```
说明:
- `package:mac:local` 会设置 `SKIP_AFTERPACK_CLEANUP=1`
- 它不是跳过打包,而是跳过 `afterPack` 里的清理步骤
## 8. 常见误区
### 误区 1先手动跑 `bundle-openclaw` 才能打包
不是必须。
因为 `package` 已经会自动执行:
- `node scripts/bundle-openclaw.mjs`
- `zx scripts/bundle-preinstalled-skills.mjs`
### 误区 2先手动跑 `uv:download:win` 才能打 Windows 包
也不是必须。
因为 `prepackage` 会自动执行 `ensure-bundled-runtime-binaries.mjs`,缺失时会自动下载。
### 误区 3`package` 就是最终出包命令
不是。
`package` 只做准备,不直接生成最终安装包;真正平台出包应使用:
- `package:win`
- `package:mac`
- `package:mac:x64`
- `package:mac:arm64`
## 9. 建议你平时直接记住的命令
### Windows
```powershell
pnpm run package:win
```
### macOS
```powershell
pnpm run package:mac
```
### 只做前置产物准备
```powershell
pnpm run package
```
### 只想检查类型
```powershell
pnpm typecheck
```

View File

@@ -40,14 +40,20 @@ asarUnpack:
npmRebuild: false
afterPack: ./scripts/after-pack.cjs
# Auto-update configuration (to be configured later)
# Auto-update configuration
# publish:
# - provider: generic
# url: https://your-update-server.com/latest
# useMultipleRangeRequest: false
# - provider: s3
# bucket: one-feel-bucket
# region: oss-cn-guangzhou
# endpoint: https://oss-cn-guangzhou.aliyuncs.com
# path: setupPackage
# acl: public-read
# macOS Configuration
mac:
extraResources:
- from: resources/cli/posix/
to: cli/
category: public.app-category.productivity
icon: resources/icons/icon.icns
target:
@@ -86,6 +92,9 @@ dmg:
win:
verifyUpdateCodeSignature: false
signAndEditExecutable: false
extraResources:
- from: resources/cli/win32/
to: cli/
icon: resources/icons/icon.ico
target:
- target: nsis
@@ -108,6 +117,9 @@ nsis:
# Linux Configuration
linux:
extraResources:
- from: resources/cli/posix/
to: cli/
icon: resources/icons/
target:
- target: AppImage

View File

@@ -25,6 +25,8 @@ export async function handleLogRoutes(
try {
return ok({
content: await logManager.readRecentLogText(parseTailLines(url.searchParams.get('tailLines'))),
directory: logManager.getLogDirectoryPath(),
currentFile: logManager.getCurrentLogFilePath(),
});
} catch (error) {
return fail(500, error instanceof Error ? error.message : String(error));

View File

@@ -7,7 +7,7 @@ import configManager from '@electron/service/config-service'
import themeManager from '@electron/service/theme-service'
import { runTaskOperationService } from '@electron/process/runTaskOperationService'
import { initScriptStoreService } from '@electron/service/script-store-service'
import log from 'electron-log';
import logManager from '@electron/service/logger';
import 'bytenode'; // Ensure bytenode is bundled/externalized correctly
import { appUpdater } from '@electron/service/updater';
import axios from 'axios';
@@ -21,9 +21,19 @@ import { applyLaunchAtStartupSetting, syncLaunchAtStartupSettingFromConfig } fro
import { ensureBuiltinSkillsInstalled, ensurePreinstalledSkillsInstalled } from '@electron/utils/skill-config';
import { initTelemetry, shutdownTelemetry } from '@electron/utils/telemetry';
import { syncGatewayConfigBeforeLaunch } from '@electron/gateway/config-sync';
import { installRuntimeDiagnostics, logStartupCheckpoint, startLocalCrashReporter } from '@electron/utils/runtime-diagnostics';
// 初始化 updater确保在 app ready 之前或者之中注册好 IPC
appUpdater.init();
startLocalCrashReporter();
installRuntimeDiagnostics();
logStartupCheckpoint('main-module-loaded');
try {
appUpdater.init();
logStartupCheckpoint('app-updater-initialized');
} catch (error) {
logManager.captureException('Failed to initialize app updater', error);
}
// 注册 hostapi:fetch IPC 代理
// 模型管理相关接口在本地处理(对齐 ClawX其余接口代理到远端后端
@@ -48,7 +58,7 @@ function refreshProviderRuntime(): { warnings: string[] } {
try {
return syncProviderRuntimeSnapshot();
} catch (error) {
log.error('provider runtime sync failed', error);
logManager.captureException('provider runtime sync failed', error);
return {
warnings: [error instanceof Error ? error.message : String(error)],
};
@@ -237,12 +247,12 @@ function bindGatewayLifecycleEvents(): void {
}
function requestQuitOnSignal(signal: NodeJS.Signals): void {
log.info(`Received ${signal}; requesting app quit`);
logManager.info(`Received ${signal}; requesting app quit`);
app.quit();
}
function emergencyGatewayCleanup(reason: string, error: unknown): void {
log.error(`${reason}:`, error);
logManager.captureException(reason, error);
hostEventBus.closeAll();
void closeHostApiServer().catch(() => {
// ignore host API server close failures during emergency cleanup
@@ -321,14 +331,14 @@ app.on('before-quit', (event) => {
gatewayQuitCleanupInProgress = true;
hostEventBus.closeAll();
const closeServerPromise = closeHostApiServer().catch((error) => {
log.warn('Host API server close failed during quit:', error);
logManager.captureException('Host API server close failed during quit', error);
});
const stopPromise = Promise.all([
closeServerPromise,
gatewayManager.stop(),
]).catch((error) => {
log.warn('gatewayManager.stop() error during quit:', error);
logManager.captureException('gatewayManager.stop() error during quit', error);
});
const timeoutPromise = new Promise<'timeout'>((resolve) => {
setTimeout(() => resolve('timeout'), GATEWAY_QUIT_TIMEOUT_MS);
@@ -339,38 +349,43 @@ app.on('before-quit', (event) => {
timeoutPromise,
]).then(async (result) => {
if (result === 'timeout') {
log.warn('Gateway shutdown timed out during app quit; proceeding with forced quit');
logManager.warn('Gateway shutdown timed out during app quit; proceeding with forced quit');
try {
const terminated = await gatewayManager.forceTerminateOwnedProcessForQuit();
if (terminated) {
log.warn('Forced gateway process termination completed after quit timeout');
logManager.warn('Forced gateway process termination completed after quit timeout');
}
} catch (error) {
log.warn('Forced gateway termination failed after quit timeout:', error);
logManager.captureException('Forced gateway termination failed after quit timeout', error);
}
}
try {
await shutdownTelemetry();
} catch (error) {
log.warn('Telemetry shutdown failed during app quit:', error);
logManager.captureException('Telemetry shutdown failed during app quit', error);
}
gatewayQuitCleanupCompleted = true;
app.quit();
}).catch((error) => {
gatewayQuitCleanupInProgress = false;
log.warn('Gateway quit cleanup failed:', error);
logManager.captureException('Gateway quit cleanup failed', error);
gatewayQuitCleanupCompleted = true;
app.quit();
});
});
app.whenReady().then(async () => {
logStartupCheckpoint('app-ready');
await configManager.init();
logStartupCheckpoint('config-initialized');
await syncLaunchAtStartupSettingFromConfig();
logStartupCheckpoint('launch-at-startup-synced');
await themeManager.init();
logStartupCheckpoint('theme-initialized');
await initTelemetry();
logStartupCheckpoint('telemetry-initialized');
bindGatewayLifecycleEvents();
try {
@@ -386,8 +401,9 @@ app.whenReady().then(async () => {
);
},
});
logStartupCheckpoint('host-api-server-started');
} catch (error) {
log.error('Failed to start Host API server:', error);
logManager.captureException('Failed to start Host API server', error);
}
let launchAtStartup = Boolean(configManager.get(CONFIG_KEYS.LAUNCH_AT_STARTUP));
@@ -409,23 +425,25 @@ app.whenReady().then(async () => {
});
void ensureBuiltinSkillsInstalled().catch((error) => {
log.warn('Failed to install built-in skills:', error);
logManager.captureException('Failed to install built-in skills', error);
});
void ensurePreinstalledSkillsInstalled().catch((error) => {
log.warn('Failed to install preinstalled skills:', error);
logManager.captureException('Failed to install preinstalled skills', error);
});
try {
await syncGatewayConfigBeforeLaunch();
logStartupCheckpoint('gateway-config-synced');
} catch (error) {
log.warn('Failed to sync OpenClaw config before launch:', error);
logManager.captureException('Failed to sync OpenClaw config before launch', error);
}
refreshProviderRuntime();
void gatewayManager.init().catch((error) => {
log.warn('Failed to initialize GatewayManager:', error);
logManager.captureException('Failed to initialize GatewayManager', error);
});
logStartupCheckpoint('gateway-init-requested');
onProviderChange(() => {
const runtimeSync = refreshProviderRuntime();
@@ -437,14 +455,20 @@ app.whenReady().then(async () => {
});
setupMainWindow();
logStartupCheckpoint('main-window-requested');
// 初始化脚本存储服务
initScriptStoreService()
logStartupCheckpoint('script-store-initialized');
// 开启任务操作子进程
runTaskOperationService()
logStartupCheckpoint('task-operation-service-started');
// 开启subagent子进程
}).catch((error) => {
logManager.captureException('Fatal error during app bootstrap', error);
emergencyGatewayCleanup('Fatal error during app bootstrap', error);
});
// Quit when all windows are closed, except on macOS. There, it's common
@@ -452,7 +476,7 @@ app.whenReady().then(async () => {
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin' && !configManager.get(CONFIG_KEYS.MINIMIZE_TO_TRAY)) {
log.info('app closing due to all windows being closed');
logManager.info('app closing due to all windows being closed');
app.quit();
}
});

View File

@@ -1,10 +1,11 @@
import { IPC_EVENTS } from '@runtime/lib/constants';
import { promisify } from 'util';
import { ipcMain } from 'electron';
import { app, ipcMain } from 'electron';
import log from 'electron-log';
import * as path from 'path';
import * as fs from 'fs';
import { getUserDataDir } from '@electron/utils/paths';
import { serializeUnknownError } from '@electron/utils/log-helpers';
// 转换为Promise形式的fs方法
const readdirAsync = promisify(fs.readdir);
@@ -35,6 +36,12 @@ class LogService {
this.error('Failed to create log directory:', err);
}
try {
app.setAppLogsPath(logPath);
} catch (err) {
this.error('Failed to set app logs path:', err);
}
// 配置electron-log
log.transports.file.resolvePathFn = () => {
// 使用当前日期作为日志文件名,格式为 YYYY-MM-DD.log
@@ -48,6 +55,7 @@ class LogService {
// 配置日志文件大小限制默认10MB
log.transports.file.maxSize = 10 * 1024 * 1024; // 10MB
log.transports.file.sync = true;
// 配置控制台日志级别开发环境可以设置为debug生产环境可以设置为info
log.transports.console.level = process.env.NODE_ENV === 'development' ? 'debug' : 'info';
@@ -160,6 +168,13 @@ class LogService {
log.error(message, ...meta);
}
public captureException(message: string, error: unknown, extra: Record<string, unknown> = {}): void {
this.error(message, {
...extra,
error: serializeUnknownError(error),
});
}
public logApiRequest(endpoint: string, data: any = {}, method: string = 'POST'): void {
this.info(`API Request: ${endpoint}, Method: ${method}, Request: ${JSON.stringify(data)}`);
@@ -219,6 +234,14 @@ class LogService {
}
}
public getLogDirectoryPath(): string {
return this.logDirPath;
}
public getCurrentLogFilePath(): string {
return this._getCurrentLogFilePath();
}
private _getCurrentLogFilePath(): string {
const today = new Date();
const formattedDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;

View File

@@ -0,0 +1,66 @@
export type SerializedError = {
name?: string;
message: string;
stack?: string;
code?: string | number;
cause?: unknown;
};
function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
export function serializeError(error: Error): SerializedError {
const code = (error as Error & { code?: string | number }).code;
const cause = (error as Error & { cause?: unknown }).cause;
return {
name: error.name,
message: error.message,
stack: error.stack,
...(typeof code !== 'undefined' ? { code } : {}),
...(typeof cause !== 'undefined' ? { cause: serializeUnknownForLog(cause) } : {}),
};
}
export function serializeUnknownError(error: unknown): SerializedError {
if (error instanceof Error) {
return serializeError(error);
}
if (typeof error === 'string') {
return { message: error };
}
if (isPlainObject(error) && typeof error.message === 'string') {
return {
message: error.message,
...(typeof error.name === 'string' ? { name: error.name } : {}),
...(typeof error.stack === 'string' ? { stack: error.stack } : {}),
...(typeof error.code === 'string' || typeof error.code === 'number' ? { code: error.code } : {}),
...(typeof error.cause !== 'undefined' ? { cause: serializeUnknownForLog(error.cause) } : {}),
};
}
return {
message: `Non-Error value thrown: ${String(error)}`,
};
}
export function serializeUnknownForLog(value: unknown): unknown {
if (value instanceof Error) {
return serializeError(value);
}
if (Array.isArray(value)) {
return value.map((item) => serializeUnknownForLog(item));
}
if (isPlainObject(value)) {
return Object.fromEntries(
Object.entries(value).map(([key, entryValue]) => [key, serializeUnknownForLog(entryValue)]),
);
}
return value;
}

View File

@@ -0,0 +1,160 @@
import { app, crashReporter, type BrowserWindow, type WebContents } from 'electron';
import path from 'node:path';
import logManager from '@electron/service/logger';
import { ensureDir, getUserDataDir } from './paths';
import { serializeUnknownError, serializeUnknownForLog } from './log-helpers';
let diagnosticsInstalled = false;
let crashReporterStarted = false;
function describeWebContents(webContents: WebContents): Record<string, unknown> {
return {
webContentsId: webContents.id,
type: webContents.getType(),
url: webContents.getURL(),
loading: webContents.isLoading(),
destroyed: webContents.isDestroyed(),
};
}
function describeWindow(window: BrowserWindow): Record<string, unknown> {
return {
windowId: window.id,
title: window.getTitle(),
visible: window.isVisible(),
minimized: window.isMinimized(),
maximized: window.isMaximized(),
destroyed: window.isDestroyed(),
...describeWebContents(window.webContents),
};
}
function getCrashDumpsDirectoryPath(): string {
return ensureDir(path.join(getUserDataDir(), 'logs', 'crashDumps'));
}
function attachWindowDiagnostics(window: BrowserWindow): void {
logManager.info('BrowserWindow created', describeWindow(window));
window.on('unresponsive', () => {
logManager.warn('BrowserWindow became unresponsive', describeWindow(window));
});
window.on('responsive', () => {
logManager.info('BrowserWindow became responsive again', describeWindow(window));
});
window.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => {
logManager.error('WebContents failed to load', {
...describeWindow(window),
errorCode,
errorDescription,
validatedURL,
isMainFrame,
frameProcessId,
frameRoutingId,
});
});
window.webContents.on('render-process-gone', (_event, details) => {
logManager.error('Window render process exited unexpectedly', {
...describeWindow(window),
reason: details.reason,
exitCode: details.exitCode,
});
});
}
export function logStartupCheckpoint(stage: string, details: Record<string, unknown> = {}): void {
logManager.info(`startup:${stage}`, {
pid: process.pid,
packaged: app.isPackaged,
...details,
});
}
export function startLocalCrashReporter(): void {
if (crashReporterStarted) {
return;
}
const crashDumpsDir = getCrashDumpsDirectoryPath();
try {
app.setPath('crashDumps', crashDumpsDir);
} catch (error) {
logManager.captureException('Failed to set crashDumps path', error, { crashDumpsDir });
}
try {
crashReporter.start({
companyName: 'ZhiNian Team',
productName: app.getName() || 'NIANXX',
submitURL: 'https://127.0.0.1/disabled-crash-upload',
uploadToServer: false,
compress: true,
ignoreSystemCrashHandler: false,
rateLimit: false,
globalExtra: {
appVersion: app.getVersion(),
packaged: String(app.isPackaged),
platform: process.platform,
arch: process.arch,
},
});
crashReporterStarted = true;
logManager.info('Crash reporter started for local dump collection', {
crashDumpsDir,
uploadToServer: false,
});
} catch (error) {
logManager.captureException('Failed to start crash reporter', error, { crashDumpsDir });
}
}
export function installRuntimeDiagnostics(): void {
if (diagnosticsInstalled) {
return;
}
diagnosticsInstalled = true;
process.on('warning', (warning) => {
logManager.warn('Node.js process warning', serializeUnknownError(warning));
});
process.on('uncaughtExceptionMonitor', (error) => {
logManager.captureException('uncaughtExceptionMonitor', error);
});
process.on('unhandledRejection', (reason) => {
logManager.error('unhandledRejection observed by runtime diagnostics', {
reason: serializeUnknownForLog(reason),
});
});
app.on('browser-window-created', (_event, window) => {
attachWindowDiagnostics(window);
});
app.on('render-process-gone', (_event, webContents, details) => {
logManager.error('App render process exited unexpectedly', {
...describeWebContents(webContents),
reason: details.reason,
exitCode: details.exitCode,
});
});
app.on('child-process-gone', (_event, details) => {
logManager.error('Child process exited unexpectedly', serializeUnknownForLog(details));
});
logManager.info('Runtime diagnostics installed', {
platform: process.platform,
arch: process.arch,
pid: process.pid,
userDataDir: getUserDataDir(),
logsDir: logManager.getLogDirectoryPath(),
});
}

View File

@@ -6,7 +6,7 @@
"main": "dist-electron/main/main.js",
"scripts": {
"init": "pnpm install && pnpm run uv:download",
"prepackage": "node scripts/ensure-bundled-runtime-binaries.mjs",
"prepackage": "node scripts/ensure-bundled-runtime-binaries.mjs && node scripts/patch-electron-builder-nsis.mjs",
"predev": "zx scripts/prepare-preinstalled-skills-dev.mjs",
"prestart": "zx scripts/prepare-preinstalled-skills-dev.mjs",
"dev": "vite",
@@ -40,7 +40,7 @@
"generate-prod-entry": "node build/scripts/generateProdEntry.js",
"clean": "node build/scripts/clean.js",
"build:encrypt": "pnpm run clean && pnpm run build:vite && pnpm run package",
"postinstall": "electron-builder install-app-deps"
"postinstall": "electron-builder install-app-deps && node scripts/patch-electron-builder-nsis.mjs"
},
"keywords": [],
"author": {

View File

@@ -0,0 +1,54 @@
#!/bin/sh
# OpenClaw CLI - managed by NIANXX
# Do not edit manually. Regenerated on NIANXX updates.
# Resolve the real path of this script (follow symlinks)
SCRIPT="$0"
while [ -L "$SCRIPT" ]; do
SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT")" && pwd)"
SCRIPT="$(readlink "$SCRIPT")"
[ "${SCRIPT#/}" = "$SCRIPT" ] && SCRIPT="$SCRIPT_DIR/$SCRIPT"
done
SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT")" && pwd)"
if [ "$(uname)" = "Darwin" ]; then
# macOS: <App>.app/Contents/Resources/cli/openclaw
# SCRIPT_DIR = .../Contents/Resources/cli
CONTENTS_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
ELECTRON="$CONTENTS_DIR/MacOS/NIANXX"
CLI="$CONTENTS_DIR/Resources/openclaw/openclaw.mjs"
else
# Linux: /opt/NIANXX/resources/cli/openclaw
# SCRIPT_DIR = .../resources/cli
INSTALL_DIR="$(dirname "$(dirname "$SCRIPT_DIR")")"
CLI="$INSTALL_DIR/resources/openclaw/openclaw.mjs"
if [ -x "$INSTALL_DIR/nianxx" ]; then
ELECTRON="$INSTALL_DIR/nianxx"
elif [ -x "$INSTALL_DIR/NIANXX" ]; then
ELECTRON="$INSTALL_DIR/NIANXX"
else
ELECTRON="$INSTALL_DIR/zn-ai"
fi
fi
if [ ! -f "$ELECTRON" ]; then
echo "Error: NIANXX executable not found at $ELECTRON" >&2
echo "Please reinstall NIANXX or remove this script: $0" >&2
exit 1
fi
case "$1" in
update)
echo "openclaw is managed by NIANXX (bundled version)."
echo ""
echo "To update openclaw, update NIANXX:"
echo " Open NIANXX > Settings > Check for Updates"
echo ""
ELECTRON_RUN_AS_NODE=1 "$ELECTRON" "$CLI" --version 2>/dev/null || true
exit 0
;;
esac
export OPENCLAW_EMBEDDED_IN="NIANXX"
ELECTRON_RUN_AS_NODE=1 exec "$ELECTRON" "$CLI" "$@"

View File

@@ -0,0 +1,26 @@
#!/bin/sh
# OpenClaw CLI wrapper for Git Bash / MSYS2 on Windows
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INSTALL_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
case "$1" in
update)
echo "openclaw is managed by NIANXX (bundled version)."
echo ""
echo "To update openclaw, update NIANXX:"
echo " Open NIANXX > Settings > Check for Updates"
exit 0
;;
esac
export OPENCLAW_EMBEDDED_IN="NIANXX"
NODE_EXE="$INSTALL_DIR/resources/bin/node.exe"
OPENCLAW_ENTRY="$INSTALL_DIR/resources/openclaw/openclaw.mjs"
if [ -f "$NODE_EXE" ]; then
if "$NODE_EXE" -e 'const [maj,min]=process.versions.node.split(".").map(Number);process.exit((maj>22||maj===22&&min>=16)?0:1)' >/dev/null 2>&1; then
exec "$NODE_EXE" "$OPENCLAW_ENTRY" "$@"
fi
fi
ELECTRON_RUN_AS_NODE=1 exec "$INSTALL_DIR/NIANXX.exe" "$OPENCLAW_ENTRY" "$@"

View File

@@ -0,0 +1,37 @@
@echo off
setlocal
if /i "%1"=="update" (
echo openclaw is managed by NIANXX ^(bundled version^).
echo.
echo To update openclaw, update NIANXX:
echo Open NIANXX ^> Settings ^> Check for Updates
exit /b 0
)
rem Switch console to UTF-8 so Unicode box-drawing and CJK text render correctly
rem on non-English Windows (e.g. Chinese CP936). Save the previous codepage to restore later.
for /f "tokens=2 delims=:." %%a in ('chcp') do set /a "_CP=%%a" 2>nul
chcp 65001 >nul 2>&1
set OPENCLAW_EMBEDDED_IN=NIANXX
set "NODE_EXE=%~dp0..\bin\node.exe"
set "OPENCLAW_ENTRY=%~dp0..\openclaw\openclaw.mjs"
set "_USE_BUNDLED_NODE=0"
if exist "%NODE_EXE%" (
"%NODE_EXE%" -e "const [maj,min]=process.versions.node.split('.').map(Number);process.exit((maj>22||maj===22&&min>=16)?0:1)" >nul 2>&1
if not errorlevel 1 set "_USE_BUNDLED_NODE=1"
)
if "%_USE_BUNDLED_NODE%"=="1" (
"%NODE_EXE%" "%OPENCLAW_ENTRY%" %*
) else (
set ELECTRON_RUN_AS_NODE=1
"%~dp0..\..\NIANXX.exe" "%OPENCLAW_ENTRY%" %*
)
set _EXIT=%ERRORLEVEL%
if defined _CP chcp %_CP% >nul 2>&1
endlocal & exit /b %_EXIT%

View File

@@ -0,0 +1,150 @@
param(
[Parameter(Mandatory = $true)]
[ValidateSet('add', 'remove')]
[string]$Action,
[Parameter(Mandatory = $true)]
[string]$CliDir
)
$ErrorActionPreference = 'Stop'
function Get-UserPathRegistryValue {
$raw = [Environment]::GetEnvironmentVariable('Path', 'User')
$kind = [Microsoft.Win32.RegistryValueKind]::ExpandString
try {
$key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $false)
if ($null -ne $key) {
try {
$stored = $key.GetValue('Path', $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
if ($null -ne $stored) {
$raw = [string]$stored
}
} catch {
# Fallback to Environment API value
}
try {
$kind = $key.GetValueKind('Path')
} catch {
# Keep default ExpandString
}
$key.Close()
}
} catch {
# Fallback to Environment API value
}
return @{
Raw = $raw
Kind = $kind
}
}
function Normalize-PathEntry {
param([string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) {
return ''
}
return $Value.Trim().Trim('"').TrimEnd('\').ToLowerInvariant()
}
$pathMeta = Get-UserPathRegistryValue
$current = $pathMeta.Raw
$entries = @()
if (-not [string]::IsNullOrWhiteSpace($current)) {
$entries = $current -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
}
$target = Normalize-PathEntry $CliDir
$seen = [System.Collections.Generic.HashSet[string]]::new()
$nextEntries = New-Object System.Collections.Generic.List[string]
foreach ($entry in $entries) {
$normalized = Normalize-PathEntry $entry
if ([string]::IsNullOrWhiteSpace($normalized)) {
continue
}
if ($normalized -eq $target) {
continue
}
if ($seen.Add($normalized)) {
$nextEntries.Add($entry.Trim().Trim('"'))
}
}
$status = 'already-present'
if ($Action -eq 'add') {
if ($seen.Add($target)) {
$nextEntries.Add($CliDir)
$status = 'updated'
}
} elseif ($entries.Count -ne $nextEntries.Count) {
$status = 'updated'
}
$isLikelyCorruptedWrite = (
$Action -eq 'add' -and
$entries.Count -gt 1 -and
$nextEntries.Count -le 1
)
if ($isLikelyCorruptedWrite) {
throw "Refusing to rewrite user PATH: input had $($entries.Count) entries but output has $($nextEntries.Count)."
}
$newPath = if ($nextEntries.Count -eq 0) { $null } else { $nextEntries -join ';' }
try {
$key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true)
if ($null -eq $key) {
throw 'Unable to open HKCU\Environment for write.'
}
if ([string]::IsNullOrWhiteSpace($newPath)) {
$key.DeleteValue('Path', $false)
} else {
$kind = if ($pathMeta.Kind -eq [Microsoft.Win32.RegistryValueKind]::String) {
[Microsoft.Win32.RegistryValueKind]::String
} else {
[Microsoft.Win32.RegistryValueKind]::ExpandString
}
$key.SetValue('Path', $newPath, $kind)
}
$key.Close()
} catch {
throw "Failed to write HKCU\\Environment\\Path: $($_.Exception.Message)"
}
try {
Add-Type -Namespace OpenClaw -Name NativeMethods -MemberDefinition @"
[System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true, CharSet = System.Runtime.InteropServices.CharSet.Auto)]
public static extern System.IntPtr SendMessageTimeout(
System.IntPtr hWnd,
int Msg,
System.IntPtr wParam,
string lParam,
int fuFlags,
int uTimeout,
out System.IntPtr lpdwResult
);
"@
$result = [IntPtr]::Zero
[OpenClaw.NativeMethods]::SendMessageTimeout(
[IntPtr]0xffff,
0x001A,
[IntPtr]::Zero,
'Environment',
0x0002,
5000,
[ref]$result
) | Out-Null
} catch {
Write-Warning "PATH updated but failed to broadcast environment change: $($_.Exception.Message)"
}
Write-Output $status

View File

@@ -1,10 +1,13 @@
#!/usr/bin/env zx
import 'zx/globals';
import { configureZxShellForPlatform } from './configure-zx-shell.mjs';
import { readFileSync, existsSync, mkdirSync, rmSync, cpSync, writeFileSync } from 'node:fs';
import { join, dirname, basename } from 'node:path';
import { fileURLToPath } from 'node:url';
configureZxShellForPlatform();
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const MANIFEST_PATH = join(ROOT, 'resources', 'skills', 'preinstalled-manifest.json');

View File

@@ -0,0 +1,7 @@
import { usePowerShell } from 'zx';
export function configureZxShellForPlatform() {
if (process.platform === 'win32') {
usePowerShell();
}
}

View File

@@ -1,6 +1,9 @@
#!/usr/bin/env zx
import 'zx/globals';
import { configureZxShellForPlatform } from './configure-zx-shell.mjs';
configureZxShellForPlatform();
const ROOT_DIR = path.resolve(__dirname, '..');
const NODE_VERSION = '22.16.0';
@@ -130,4 +133,4 @@ try {
echo(chalk.yellow` Packaging will continue without external Node.js binary.`);
// Exit with code 0 to allow packaging to continue
process.exit(0);
}
}

View File

@@ -1,6 +1,9 @@
#!/usr/bin/env zx
import 'zx/globals';
import { configureZxShellForPlatform } from './configure-zx-shell.mjs';
configureZxShellForPlatform();
const ROOT_DIR = path.resolve(__dirname, '..');
const UV_VERSION = '0.10.0';

View File

@@ -1,10 +1,13 @@
#!/usr/bin/env zx
import 'zx/globals';
import { configureZxShellForPlatform } from './configure-zx-shell.mjs';
import sharp from 'sharp';
import png2icons from 'png2icons';
import { fileURLToPath } from 'url';
configureZxShellForPlatform();
// Calculate paths
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

View File

@@ -0,0 +1,75 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const targetFile = require.resolve('app-builder-lib/out/targets/nsis/NsisTarget.js');
const patchMarker = 'zn-ai patch: retry NSIS stub execution on Windows';
const helperSnippet = `
// ${patchMarker}
function isRetryableWindowsSpawnError(error) {
const code = error != null && typeof error.code === "string" ? error.code : "";
const message = error != null && typeof error.message === "string" ? error.message : "";
return (code === "EPERM" ||
code === "EACCES" ||
message.includes("spawn EPERM") ||
message.includes("spawn EACCES") ||
message.includes("used by another process"));
}
async function execWineWithRetry(file, file64 = null, appArgs = [], options = {}) {
const maxAttempts = process.platform === "win32" ? 12 : 1;
let attempt = 0;
while (true) {
attempt++;
try {
return await (0, wine_1.execWine)(file, file64, appArgs, options);
}
catch (error) {
const shouldRetry = process.platform === "win32" &&
attempt < maxAttempts &&
isRetryableWindowsSpawnError(error);
if (!shouldRetry) {
throw error;
}
builder_util_1.log.warn({
attempt,
maxAttempts,
file,
reason: error.message || String(error),
}, "retrying NSIS stub execution after transient Windows spawn failure");
await new Promise(resolve => setTimeout(resolve, attempt * 1000));
}
}
}
`;
const originalCall = 'await (0, wine_1.execWine)(installerPath, null, [], { env: { __COMPAT_LAYER: "RunAsInvoker" } });';
const patchedCall = 'await execWineWithRetry(installerPath, null, [], { env: { __COMPAT_LAYER: "RunAsInvoker" } });';
const source = fs.readFileSync(targetFile, 'utf8');
if (source.includes(patchMarker)) {
console.log(`[patch-electron-builder-nsis] already patched: ${targetFile}`);
process.exit(0);
}
if (!source.includes(originalCall)) {
console.error(`[patch-electron-builder-nsis] target call not found in ${targetFile}`);
process.exit(1);
}
const classToken = 'class NsisTarget extends core_1.Target {';
if (!source.includes(classToken)) {
console.error(`[patch-electron-builder-nsis] class token not found in ${targetFile}`);
process.exit(1);
}
const patchedSource = source
.replace(classToken, `${helperSnippet}\n${classToken}`)
.replace(originalCall, patchedCall);
fs.writeFileSync(targetFile, patchedSource, 'utf8');
console.log(`[patch-electron-builder-nsis] patched ${targetFile}`);

View File

@@ -1,10 +1,13 @@
#!/usr/bin/env zx
import 'zx/globals';
import { configureZxShellForPlatform } from './configure-zx-shell.mjs';
import { existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
configureZxShellForPlatform();
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
const lockPath = join(ROOT, 'build', 'preinstalled-skills', '.preinstalled-lock.json');

46
tests/log-helpers.test.ts Normal file
View File

@@ -0,0 +1,46 @@
import { describe, expect, it } from 'vitest';
import { serializeError, serializeUnknownError, serializeUnknownForLog } from '../electron/utils/log-helpers';
describe('log helpers', () => {
it('serializes Error instances with code and cause', () => {
const cause = new Error('inner failure');
const error = new Error('outer failure') as Error & { code?: string; cause?: unknown };
error.code = 'E_TEST';
error.cause = cause;
expect(serializeError(error)).toMatchObject({
name: 'Error',
message: 'outer failure',
code: 'E_TEST',
cause: {
name: 'Error',
message: 'inner failure',
},
});
});
it('normalizes string and object rejection values', () => {
expect(serializeUnknownError('boom')).toEqual({ message: 'boom' });
expect(serializeUnknownError({ message: 'failed', code: 500 })).toEqual({
message: 'failed',
code: 500,
});
});
it('recursively serializes nested error values for structured logging', () => {
const payload = serializeUnknownForLog({
task: 'bootstrap',
errors: [new Error('bad dependency')],
});
expect(payload).toEqual({
task: 'bootstrap',
errors: [
expect.objectContaining({
name: 'Error',
message: 'bad dependency',
}),
],
});
});
});

View File

@@ -16,6 +16,12 @@ const projectAliases = {
function isMainProcessExternal(id: string): boolean {
if (!id || id.startsWith('\0')) return false;
if (id.startsWith('.') || id.startsWith('/') || /^[A-Za-z]:[\\/]/.test(id)) return false;
// Keep ws bundled into the Electron main chunk.
// The packaged app imports it directly from gateway runtime code,
// and relying on transitive/peer installation causes "Cannot find module 'ws'"
// in production builds.
if (id === 'ws' || id.startsWith('ws/')) return false;
// Project-specific aliases that should be bundled (not external)
const internalAliases = [