Compare commits
5 Commits
feature/pa
...
feature/20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2081f583e3 | ||
|
|
90e02636c7 | ||
|
|
1667688931 | ||
|
|
f8c9ca2dd3 | ||
|
|
dd211056dd |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -53,3 +53,5 @@ dist/
|
||||
dist-electron/
|
||||
release/
|
||||
*.local
|
||||
|
||||
build/
|
||||
@@ -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",
|
||||
|
||||
@@ -1,7 +1,2 @@
|
||||
"use strict";
|
||||
require("electron");
|
||||
require("./main-Cg0vBGtL.js");
|
||||
require("electron-squirrel-startup");
|
||||
require("electron-log");
|
||||
require("bytenode");
|
||||
require("axios");
|
||||
require('bytenode')
|
||||
require('./main.jsc')
|
||||
@@ -1,128 +1 @@
|
||||
"use strict";
|
||||
const electron = require("electron");
|
||||
var IPC_EVENTS = /* @__PURE__ */ ((IPC_EVENTS2) => {
|
||||
IPC_EVENTS2["HOST_API_FETCH"] = "hostapi:fetch";
|
||||
IPC_EVENTS2["HOST_API_TOKEN"] = "hostapi:token";
|
||||
IPC_EVENTS2["EXTERNAL_OPEN"] = "external-open";
|
||||
IPC_EVENTS2["APP_SET_FRAMELESS"] = "app:set-frameless";
|
||||
IPC_EVENTS2["APP_LOAD_PAGE"] = "app:load-page";
|
||||
IPC_EVENTS2["LOG_TO_MAIN"] = "log-to-main";
|
||||
IPC_EVENTS2["READ_FILE"] = "read-file";
|
||||
IPC_EVENTS2["INVOKE"] = "ipc:invoke";
|
||||
IPC_EVENTS2["INVOKE_ASYNC"] = "ipc:invokeAsync";
|
||||
IPC_EVENTS2["APP_MINIMIZE"] = "app:minimize";
|
||||
IPC_EVENTS2["APP_MAXIMIZE"] = "app:maximize";
|
||||
IPC_EVENTS2["APP_QUIT"] = "app:quit";
|
||||
IPC_EVENTS2["FILE_READ"] = "file:read";
|
||||
IPC_EVENTS2["FILE_WRITE"] = "file:write";
|
||||
IPC_EVENTS2["CUSTOM_EVENT"] = "custom:event";
|
||||
IPC_EVENTS2["TIME_UPDATE"] = "time:update";
|
||||
IPC_EVENTS2["RENDERER_IS_READY"] = "renderer-ready";
|
||||
IPC_EVENTS2["SHOW_CONTEXT_MENU"] = "show-context-menu";
|
||||
IPC_EVENTS2["START_A_DIALOGUE"] = "start-a-dialogue";
|
||||
IPC_EVENTS2["OPEN_WINDOW"] = "open-window";
|
||||
IPC_EVENTS2["LOG_DEBUG"] = "log-debug";
|
||||
IPC_EVENTS2["LOG_INFO"] = "log-info";
|
||||
IPC_EVENTS2["LOG_WARN"] = "log-warn";
|
||||
IPC_EVENTS2["LOG_ERROR"] = "log-error";
|
||||
IPC_EVENTS2["CONFIG_UPDATED"] = "config-updated";
|
||||
IPC_EVENTS2["SET_CONFIG"] = "set-config";
|
||||
IPC_EVENTS2["GET_CONFIG"] = "get-config";
|
||||
IPC_EVENTS2["UPDATE_CONFIG"] = "update-config";
|
||||
IPC_EVENTS2["SET_THEME_MODE"] = "set-theme-mode";
|
||||
IPC_EVENTS2["GET_THEME_MODE"] = "get-theme-mode";
|
||||
IPC_EVENTS2["IS_DARK_THEME"] = "is-dark-theme";
|
||||
IPC_EVENTS2["THEME_MODE_UPDATED"] = "theme-mode-updated";
|
||||
IPC_EVENTS2["EXECUTE_SCRIPT"] = "execute-script";
|
||||
IPC_EVENTS2["TASK_PROGRESS"] = "task:progress";
|
||||
IPC_EVENTS2["TASK_STARTED"] = "task:started";
|
||||
IPC_EVENTS2["TASK_COMPLETED"] = "task:completed";
|
||||
IPC_EVENTS2["OPEN_CHANNEL"] = "open-channel";
|
||||
IPC_EVENTS2["SCRIPT_LIST"] = "script:list";
|
||||
IPC_EVENTS2["SCRIPT_GET"] = "script:get";
|
||||
IPC_EVENTS2["SCRIPT_SAVE"] = "script:save";
|
||||
IPC_EVENTS2["SCRIPT_DELETE"] = "script:delete";
|
||||
IPC_EVENTS2["SCRIPT_TOGGLE"] = "script:toggle";
|
||||
IPC_EVENTS2["SCRIPT_RUN"] = "script:run";
|
||||
IPC_EVENTS2["SCRIPT_RECORD_START"] = "script:record-start";
|
||||
IPC_EVENTS2["SCRIPT_RECORD_STOP"] = "script:record-stop";
|
||||
IPC_EVENTS2["SCRIPT_CODEGEN"] = "script:codegen";
|
||||
IPC_EVENTS2["GATEWAY_RPC"] = "gateway:rpc";
|
||||
IPC_EVENTS2["GATEWAY_EVENT"] = "gateway:event";
|
||||
IPC_EVENTS2["UPDATE_CHECK"] = "update:check";
|
||||
IPC_EVENTS2["UPDATE_DOWNLOAD"] = "update:download";
|
||||
IPC_EVENTS2["UPDATE_INSTALL"] = "update:install";
|
||||
IPC_EVENTS2["UPDATE_VERSION"] = "update:version";
|
||||
IPC_EVENTS2["UPDATE_STATUS_CHANGED"] = "update:status-changed";
|
||||
return IPC_EVENTS2;
|
||||
})(IPC_EVENTS || {});
|
||||
const api = {
|
||||
versions: process.versions,
|
||||
external: {
|
||||
open: (url) => electron.ipcRenderer.invoke("external-open", url)
|
||||
},
|
||||
platform: process.platform,
|
||||
windowMinimize: () => electron.ipcRenderer.invoke("window:minimize"),
|
||||
windowMaximize: () => electron.ipcRenderer.invoke("window:maximize"),
|
||||
windowClose: () => electron.ipcRenderer.invoke("window:close"),
|
||||
windowIsMaximized: () => electron.ipcRenderer.invoke("window:isMaximized"),
|
||||
viewIsReady: () => electron.ipcRenderer.send(IPC_EVENTS.RENDERER_IS_READY),
|
||||
app: {
|
||||
setFrameless: (route) => electron.ipcRenderer.invoke(IPC_EVENTS.APP_SET_FRAMELESS, route),
|
||||
loadPage: (page) => electron.ipcRenderer.invoke(IPC_EVENTS.APP_LOAD_PAGE, page)
|
||||
},
|
||||
// 通过 IPC 调用主进程
|
||||
readFile: (filePath) => electron.ipcRenderer.invoke(IPC_EVENTS.READ_FILE, filePath),
|
||||
// 异步调用(映射为 electron 的 invoke)
|
||||
invoke: (channel, ...args) => electron.ipcRenderer.invoke(channel, ...args),
|
||||
// 异步调用(为了兼容老代码)
|
||||
invokeAsync: (channel, ...args) => electron.ipcRenderer.invoke(channel, ...args),
|
||||
// 监听主进程消息
|
||||
on: (event, callback) => {
|
||||
const subscription = (_event, ...args) => callback(...args);
|
||||
electron.ipcRenderer.on(event, subscription);
|
||||
return () => electron.ipcRenderer.removeListener(event, subscription);
|
||||
},
|
||||
// 发送消息到主进程
|
||||
send: (channel, ...args) => electron.ipcRenderer.send(channel, ...args),
|
||||
// 发送日志
|
||||
logger: {
|
||||
debug: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_DEBUG, message, ...meta),
|
||||
info: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_INFO, message, ...meta),
|
||||
warn: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_WARN, message, ...meta),
|
||||
error: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_ERROR, message, ...meta)
|
||||
},
|
||||
// 执行脚本
|
||||
executeScript: (params) => electron.ipcRenderer.invoke(IPC_EVENTS.EXECUTE_SCRIPT, params),
|
||||
// 任务事件
|
||||
onTaskProgress: (cb) => {
|
||||
const subscription = (_event, payload) => cb(payload);
|
||||
electron.ipcRenderer.on(IPC_EVENTS.TASK_PROGRESS, subscription);
|
||||
return () => electron.ipcRenderer.removeListener(IPC_EVENTS.TASK_PROGRESS, subscription);
|
||||
},
|
||||
onTaskStarted: (cb) => {
|
||||
const subscription = (_event, payload) => cb(payload);
|
||||
electron.ipcRenderer.on(IPC_EVENTS.TASK_STARTED, subscription);
|
||||
return () => electron.ipcRenderer.removeListener(IPC_EVENTS.TASK_STARTED, subscription);
|
||||
},
|
||||
onTaskCompleted: (cb) => {
|
||||
const subscription = (_event, payload) => cb(payload);
|
||||
electron.ipcRenderer.on(IPC_EVENTS.TASK_COMPLETED, subscription);
|
||||
return () => electron.ipcRenderer.removeListener(IPC_EVENTS.TASK_COMPLETED, subscription);
|
||||
},
|
||||
// 打开渠道
|
||||
openChannel: (channels) => electron.ipcRenderer.invoke(IPC_EVENTS.OPEN_CHANNEL, channels),
|
||||
// 脚本管理
|
||||
scriptApi: {
|
||||
list: () => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_LIST),
|
||||
get: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_GET, id),
|
||||
save: (input) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_SAVE, input),
|
||||
delete: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_DELETE, id),
|
||||
toggle: (id, enabled) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_TOGGLE, id, enabled),
|
||||
run: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RUN, id),
|
||||
startRecording: (url) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_START, url),
|
||||
stopRecording: () => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_STOP),
|
||||
codegen: (id, url) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_CODEGEN, id, url)
|
||||
}
|
||||
};
|
||||
electron.contextBridge.exposeInMainWorld("api", api);
|
||||
"use strict";const r=require("electron");var i=(e=>(e.HOST_API_FETCH="hostapi:fetch",e.HOST_API_TOKEN="hostapi:token",e.EXTERNAL_OPEN="external-open",e.APP_SET_FRAMELESS="app:set-frameless",e.APP_LOAD_PAGE="app:load-page",e.LOG_TO_MAIN="log-to-main",e.READ_FILE="read-file",e.INVOKE="ipc:invoke",e.INVOKE_ASYNC="ipc:invokeAsync",e.APP_MINIMIZE="app:minimize",e.APP_MAXIMIZE="app:maximize",e.APP_QUIT="app:quit",e.FILE_READ="file:read",e.FILE_WRITE="file:write",e.CUSTOM_EVENT="custom:event",e.TIME_UPDATE="time:update",e.RENDERER_IS_READY="renderer-ready",e.SHOW_CONTEXT_MENU="show-context-menu",e.START_A_DIALOGUE="start-a-dialogue",e.OPEN_WINDOW="open-window",e.LOG_DEBUG="log-debug",e.LOG_INFO="log-info",e.LOG_WARN="log-warn",e.LOG_ERROR="log-error",e.CONFIG_UPDATED="config-updated",e.SET_CONFIG="set-config",e.GET_CONFIG="get-config",e.UPDATE_CONFIG="update-config",e.SET_THEME_MODE="set-theme-mode",e.GET_THEME_MODE="get-theme-mode",e.IS_DARK_THEME="is-dark-theme",e.THEME_MODE_UPDATED="theme-mode-updated",e.EXECUTE_SCRIPT="execute-script",e.TASK_PROGRESS="task:progress",e.TASK_STARTED="task:started",e.TASK_COMPLETED="task:completed",e.OPEN_CHANNEL="open-channel",e.SCRIPT_LIST="script:list",e.SCRIPT_GET="script:get",e.SCRIPT_SAVE="script:save",e.SCRIPT_DELETE="script:delete",e.SCRIPT_TOGGLE="script:toggle",e.SCRIPT_RUN="script:run",e.SCRIPT_RECORD_START="script:record-start",e.SCRIPT_RECORD_STOP="script:record-stop",e.SCRIPT_CODEGEN="script:codegen",e.GATEWAY_RPC="gateway:rpc",e.GATEWAY_EVENT="gateway:event",e.UPDATE_CHECK="update:check",e.UPDATE_DOWNLOAD="update:download",e.UPDATE_INSTALL="update:install",e.UPDATE_VERSION="update:version",e.UPDATE_STATUS_CHANGED="update:status-changed",e))(i||{});const p={versions:process.versions,external:{open:e=>r.ipcRenderer.invoke("external-open",e)},platform:process.platform,windowMinimize:()=>r.ipcRenderer.invoke("window:minimize"),windowMaximize:()=>r.ipcRenderer.invoke("window:maximize"),windowClose:()=>r.ipcRenderer.invoke("window:close"),windowIsMaximized:()=>r.ipcRenderer.invoke("window:isMaximized"),viewIsReady:()=>r.ipcRenderer.send(i.RENDERER_IS_READY),app:{setFrameless:e=>r.ipcRenderer.invoke(i.APP_SET_FRAMELESS,e),loadPage:e=>r.ipcRenderer.invoke(i.APP_LOAD_PAGE,e)},readFile:e=>r.ipcRenderer.invoke(i.READ_FILE,e),invoke:(e,...n)=>r.ipcRenderer.invoke(e,...n),invokeAsync:(e,...n)=>r.ipcRenderer.invoke(e,...n),on:(e,n)=>{const t=(o,...R)=>n(...R);return r.ipcRenderer.on(e,t),()=>r.ipcRenderer.removeListener(e,t)},send:(e,...n)=>r.ipcRenderer.send(e,...n),logger:{debug:(e,...n)=>r.ipcRenderer.send(i.LOG_DEBUG,e,...n),info:(e,...n)=>r.ipcRenderer.send(i.LOG_INFO,e,...n),warn:(e,...n)=>r.ipcRenderer.send(i.LOG_WARN,e,...n),error:(e,...n)=>r.ipcRenderer.send(i.LOG_ERROR,e,...n)},executeScript:e=>r.ipcRenderer.invoke(i.EXECUTE_SCRIPT,e),onTaskProgress:e=>{const n=(t,o)=>e(o);return r.ipcRenderer.on(i.TASK_PROGRESS,n),()=>r.ipcRenderer.removeListener(i.TASK_PROGRESS,n)},onTaskStarted:e=>{const n=(t,o)=>e(o);return r.ipcRenderer.on(i.TASK_STARTED,n),()=>r.ipcRenderer.removeListener(i.TASK_STARTED,n)},onTaskCompleted:e=>{const n=(t,o)=>e(o);return r.ipcRenderer.on(i.TASK_COMPLETED,n),()=>r.ipcRenderer.removeListener(i.TASK_COMPLETED,n)},openChannel:e=>r.ipcRenderer.invoke(i.OPEN_CHANNEL,e),scriptApi:{list:()=>r.ipcRenderer.invoke(i.SCRIPT_LIST),get:e=>r.ipcRenderer.invoke(i.SCRIPT_GET,e),save:e=>r.ipcRenderer.invoke(i.SCRIPT_SAVE,e),delete:e=>r.ipcRenderer.invoke(i.SCRIPT_DELETE,e),toggle:(e,n)=>r.ipcRenderer.invoke(i.SCRIPT_TOGGLE,e,n),run:e=>r.ipcRenderer.invoke(i.SCRIPT_RUN,e),startRecording:e=>r.ipcRenderer.invoke(i.SCRIPT_RECORD_START,e),stopRecording:()=>r.ipcRenderer.invoke(i.SCRIPT_RECORD_STOP),codegen:(e,n)=>r.ipcRenderer.invoke(i.SCRIPT_CODEGEN,e,n)}};r.contextBridge.exposeInMainWorld("api",p);
|
||||
|
||||
4
dist/index.html
vendored
4
dist/index.html
vendored
@@ -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-BTjWBXhl.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DQz7_fOL.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>
|
||||
|
||||
344
docs/Desktop-Packaging-Workflow.md
Normal file
344
docs/Desktop-Packaging-Workflow.md
Normal 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
|
||||
```
|
||||
@@ -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
|
||||
@@ -102,13 +111,15 @@ nsis:
|
||||
createStartMenuShortcut: true
|
||||
shortcutName: NIANXX
|
||||
uninstallDisplayName: NIANXX
|
||||
license: LICENSE
|
||||
include: scripts/installer.nsh
|
||||
installerIcon: resources/icons/icon.ico
|
||||
uninstallerIcon: resources/icons/icon.ico
|
||||
|
||||
# Linux Configuration
|
||||
linux:
|
||||
extraResources:
|
||||
- from: resources/cli/posix/
|
||||
to: cli/
|
||||
icon: resources/icons/
|
||||
target:
|
||||
- target: AppImage
|
||||
@@ -129,5 +140,3 @@ linux:
|
||||
Categories: Utility;Network;
|
||||
Keywords: ai;assistant;automation;chat;
|
||||
StartupWMClass: zn-ai
|
||||
appImage:
|
||||
license: LICENSE
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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')}`;
|
||||
|
||||
@@ -266,7 +266,7 @@ class WindowService {
|
||||
public focus(target: BrowserWindow | void | null) {
|
||||
if (!target) return;
|
||||
const name = this.getName(target);
|
||||
if (target?.isMaximized()) {
|
||||
if (target?.isMinimized()) {
|
||||
target?.restore();
|
||||
logManager.debug(`Window ${name} restored and focused`);
|
||||
} else {
|
||||
|
||||
66
electron/utils/log-helpers.ts
Normal file
66
electron/utils/log-helpers.ts
Normal 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;
|
||||
}
|
||||
160
electron/utils/runtime-diagnostics.ts
Normal file
160
electron/utils/runtime-diagnostics.ts
Normal 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(),
|
||||
});
|
||||
}
|
||||
6
global.d.ts
vendored
6
global.d.ts
vendored
@@ -147,9 +147,9 @@ declare global {
|
||||
open: (url: string) => Promise<void>
|
||||
},
|
||||
platform: string;
|
||||
windowMinimize: () => void;
|
||||
windowMaximize: () => void;
|
||||
windowClose: () => void;
|
||||
windowMinimize: () => Promise<void>;
|
||||
windowMaximize: () => Promise<void>;
|
||||
windowClose: () => Promise<void>;
|
||||
windowIsMaximized: () => Promise<boolean>;
|
||||
viewIsReady: () => void;
|
||||
app: {
|
||||
|
||||
@@ -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": {
|
||||
|
||||
54
resources/cli/posix/openclaw
Normal file
54
resources/cli/posix/openclaw
Normal 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" "$@"
|
||||
26
resources/cli/win32/openclaw
Normal file
26
resources/cli/win32/openclaw
Normal 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" "$@"
|
||||
37
resources/cli/win32/openclaw.cmd
Normal file
37
resources/cli/win32/openclaw.cmd
Normal 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%
|
||||
150
resources/cli/win32/update-user-path.ps1
Normal file
150
resources/cli/win32/update-user-path.ps1
Normal 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
|
||||
@@ -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');
|
||||
|
||||
7
scripts/configure-zx-shell.mjs
Normal file
7
scripts/configure-zx-shell.mjs
Normal file
@@ -0,0 +1,7 @@
|
||||
import { usePowerShell } from 'zx';
|
||||
|
||||
export function configureZxShellForPlatform() {
|
||||
if (process.platform === 'win32') {
|
||||
usePowerShell();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
75
scripts/patch-electron-builder-nsis.mjs
Normal file
75
scripts/patch-electron-builder-nsis.mjs
Normal 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}`);
|
||||
@@ -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');
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Copy, Minus, Square, X } from 'lucide-react';
|
||||
import { useI18n } from '../../i18n';
|
||||
|
||||
type TitleBarProps = {
|
||||
variant?: 'default' | 'light';
|
||||
controlsTheme?: 'default' | 'white';
|
||||
};
|
||||
|
||||
export default function TitleBar({ variant = 'default' }: TitleBarProps) {
|
||||
type WindowsTitleBarProps = {
|
||||
variant: TitleBarProps['variant'];
|
||||
controlsTheme: NonNullable<TitleBarProps['controlsTheme']>;
|
||||
closeLabel: string;
|
||||
maximizeLabel: string;
|
||||
minimizeLabel: string;
|
||||
restoreLabel: string;
|
||||
};
|
||||
|
||||
export default function TitleBar({ variant = 'default', controlsTheme }: TitleBarProps) {
|
||||
const { t } = useI18n();
|
||||
const platform = (window as any).api?.platform ?? '';
|
||||
|
||||
if (platform === 'linux') return null;
|
||||
|
||||
const iconColorClass =
|
||||
variant === 'light' ? 'text-white' : 'text-[#525866] dark:text-gray-300';
|
||||
const platform = window.api?.platform ?? '';
|
||||
const borderColorClass =
|
||||
variant === 'light' ? 'border-b-white/30' : 'border-b-gray-300 dark:border-gray-700';
|
||||
|
||||
@@ -24,6 +31,64 @@ export default function TitleBar({ variant = 'default' }: TitleBarProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (platform !== 'win32') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<WindowsTitleBar
|
||||
variant={variant}
|
||||
controlsTheme={controlsTheme ?? (variant === 'light' ? 'white' : 'default')}
|
||||
closeLabel={t('window.close')}
|
||||
maximizeLabel={t('window.maximize')}
|
||||
minimizeLabel={t('window.minimize')}
|
||||
restoreLabel={t('window.restore')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function WindowsTitleBar({
|
||||
variant = 'default',
|
||||
controlsTheme,
|
||||
closeLabel,
|
||||
maximizeLabel,
|
||||
minimizeLabel,
|
||||
restoreLabel,
|
||||
}: WindowsTitleBarProps) {
|
||||
const [maximized, setMaximized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
void syncMaximizedState();
|
||||
}, []);
|
||||
|
||||
const syncMaximizedState = async () => {
|
||||
const nextState = await window.api.windowIsMaximized();
|
||||
setMaximized(Boolean(nextState));
|
||||
};
|
||||
|
||||
const handleMinimize = () => {
|
||||
void window.api.windowMinimize();
|
||||
};
|
||||
|
||||
const handleMaximize = async () => {
|
||||
await window.api.windowMaximize();
|
||||
await syncMaximizedState();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
void window.api.windowClose();
|
||||
};
|
||||
|
||||
const useWhiteControls = controlsTheme === 'white';
|
||||
const iconColorClass = useWhiteControls ? 'text-white/95' : 'text-[#525866] dark:text-gray-300';
|
||||
const borderColorClass =
|
||||
variant === 'light' ? 'border-b-white/30' : 'border-b-gray-300 dark:border-gray-700';
|
||||
const buttonClass = [
|
||||
'flex h-full w-11 items-center justify-center transition-colors',
|
||||
useWhiteControls ? 'hover:bg-white/15 hover:text-white' : 'hover:bg-[#999] hover:text-white',
|
||||
iconColorClass,
|
||||
].join(' ');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={['drag-region flex h-10 shrink-0 items-center justify-end border-b', borderColorClass].join(
|
||||
@@ -34,39 +99,32 @@ export default function TitleBar({ variant = 'default' }: TitleBarProps) {
|
||||
<div className="no-drag flex h-full">
|
||||
<button
|
||||
type="button"
|
||||
className={['flex h-full w-11 items-center justify-center hover:bg-[#999] hover:text-white transition-colors', iconColorClass].join(
|
||||
' ',
|
||||
)}
|
||||
title={t('window.minimize')}
|
||||
onClick={() => {
|
||||
(window as any).api?.windowMinimize?.();
|
||||
}}
|
||||
className={buttonClass}
|
||||
title={minimizeLabel}
|
||||
onClick={handleMinimize}
|
||||
>
|
||||
<span className="text-base leading-none">−</span>
|
||||
<Minus className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={['flex h-full w-11 items-center justify-center hover:bg-[#999] hover:text-white transition-colors', iconColorClass].join(
|
||||
' ',
|
||||
)}
|
||||
title={t('window.maximize')}
|
||||
className={buttonClass}
|
||||
title={maximized ? restoreLabel : maximizeLabel}
|
||||
onClick={() => {
|
||||
(window as any).api?.windowMaximize?.();
|
||||
void handleMaximize();
|
||||
}}
|
||||
>
|
||||
<span className="text-[14px] leading-none">▢</span>
|
||||
{maximized ? <Copy className="h-3.5 w-3.5" /> : <Square className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={['flex h-full w-11 items-center justify-center hover:bg-[#ff0000] hover:text-white transition-colors', iconColorClass].join(
|
||||
' ',
|
||||
)}
|
||||
title={t('window.close')}
|
||||
onClick={() => {
|
||||
(window as any).api?.windowClose?.();
|
||||
}}
|
||||
className={[
|
||||
'flex h-full w-11 items-center justify-center transition-colors hover:bg-[#ff0000] hover:text-white',
|
||||
iconColorClass,
|
||||
].join(' ')}
|
||||
title={closeLabel}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<span className="text-base leading-none">×</span>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -144,7 +144,9 @@ export default function LoginPage() {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<TitleBar variant="light" />
|
||||
<div className="relative z-20">
|
||||
<TitleBar variant="light" controlsTheme="white" />
|
||||
</div>
|
||||
|
||||
<main
|
||||
className={['relative z-10 box-border flex flex-auto pl-2 pr-2 pb-2', platform !== 'linux' ? 'pt-2' : 'pt-11'].join(' ')}
|
||||
|
||||
46
tests/log-helpers.test.ts
Normal file
46
tests/log-helpers.test.ts
Normal 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',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user