arona-agent 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  > 谁不想在电脑上养一只 ~~香香软软~~ 可可爱爱的阿洛娜呢?
8
8
 
9
- ![示意图](https://cdn.jsdelivr.net/gh/SRC114514/arona-agent/intro.png)
9
+ ![示意图](https://cdn.jsdelivr.net/gh/SRC114514/arona-agent/intro.jpeg)
10
10
 
11
11
  基于 [Pi SDK](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) 构建的终端对话式 AI Agent,集成 Computer Use、TTS / STT、桌面宠物与持久记忆。
12
12
 
@@ -38,6 +38,9 @@ npm u -g arona-agent
38
38
  # 禁用TTS+STT并启动
39
39
  arona --no-voice
40
40
 
41
+ # 以命令行启动
42
+ arona --cli
43
+
41
44
  # 补全/重新克隆某角色音色
42
45
  arona voice add [<角色名>] # 不带角色名则进入 TUI 选择未补全的角色
43
46
 
@@ -93,6 +96,7 @@ arona doctor
93
96
  | `tavilyApiKey` | Tavily API Key | — |
94
97
  | `pythonPath` | Python 路径 | `python3` |
95
98
  | `autoLoadSkills` | 启动时自动从 `~/.agents` 补全缺失的Skill | `true` |
99
+ | `CLIEnabled` | 裸 `arona` 启动时进入命令行 | `false` |
96
100
  | `mcpServers` | MCP 服务器 JSON | `{}` |
97
101
 
98
102
  模型名前缀自动检测:若 `model` 不含 `/`,按模型名前缀或 `apiBaseUrl` 域名自动补 `provider/`。
package/README_en.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
6
6
 
7
- ![Screenshot](https://cdn.jsdelivr.net/gh/SRC114514/arona-agent/intro.png)
7
+ ![Screenshot](https://cdn.jsdelivr.net/gh/SRC114514/arona-agent/intro.jpeg)
8
8
 
9
9
  A terminal-based conversational AI Agent built on the [Pi SDK](https://www.npmjs.com/package/@earendil-works/pi-coding-agent), featuring Computer Use, TTS / STT, desktop pets and persistent memory.
10
10
 
@@ -36,6 +36,9 @@ npm u -g arona-agent
36
36
  # Launch with TTS/STT disabled
37
37
  arona --no-voice
38
38
 
39
+ # Use --cli for the command line
40
+ arona --cli
41
+
39
42
  # Clone/re-clone a character's voice
40
43
  arona voice add [<character-name>] # omit the name to enter the TUI for missing voices
41
44
 
@@ -90,6 +93,7 @@ All configuration lives in the JSON file `~/.arona/settings.json`, mostly genera
90
93
  | `tavilyApiKey` | Tavily API key | — |
91
94
  | `pythonPath` | Python path | `python3` |
92
95
  | `autoLoadSkills` | Automatically load missing Skills from `~/.agents` on startup | `true` |
96
+ | `CLIEnabled` | Launch the CLI when running bare `arona` | `false` |
93
97
  | `mcpServers` | MCP server JSON | `{}` |
94
98
 
95
99
  Model prefix auto-detection: if `model` contains no `/`, a `provider/` prefix is added automatically based on the model name prefix or the `apiBaseUrl` domain.
package/bin/arona.mjs CHANGED
@@ -6,41 +6,49 @@
6
6
  import { spawn } from 'node:child_process';
7
7
  import { existsSync } from 'node:fs';
8
8
  import { fileURLToPath } from 'node:url';
9
- import { dirname, join } from 'node:path';
9
+ import { dirname, join, delimiter } from 'node:path';
10
10
 
11
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
12
  const root = join(__dirname, '..');
13
13
  const args = process.argv.slice(2);
14
14
 
15
+ // 自包含运行时(npm run package 产物 runtime/):存在则前置 PATH,
16
+ // 让 tsx 的 `#!/usr/bin/env node` 及一切后续子进程都解析到包内 Node/Python,
17
+ // 即使不经 arona.sh 直接用系统 node 启动本文件也一样自包含。
18
+ const runtimePathEntries = process.platform === 'win32'
19
+ ? [join(root, 'runtime', 'node'), join(root, 'runtime', 'python')]
20
+ : [join(root, 'runtime', 'node', 'bin'), join(root, 'runtime', 'python', 'bin')];
21
+ const bundledEntries = runtimePathEntries.filter((p) => existsSync(p));
22
+ if (bundledEntries.length) {
23
+ process.env.PATH = [...bundledEntries, process.env.PATH].join(delimiter);
24
+ }
25
+
15
26
  const isSetup = args[0] === 'setup';
16
27
  const isVoice = args[0] === 'voice';
17
28
  const isDoctor = args[0] === 'doctor';
29
+ // 裸 `arona` 默认进 GUI:src/index.ts 依据 --cli / settings.json CLIEnabled 自行分流
18
30
  const target = isSetup ? 'src/setup.ts' : isVoice ? 'src/voice_cli.ts' : isDoctor ? 'src/doctor.ts' : 'src/index.ts';
19
31
  const passArgs = (isSetup || isVoice || isDoctor) ? args.slice(1) : args;
20
32
 
21
- // tsx ships a CLI binary alongside the package. We prefer the local install.
22
- const tsxBin = process.platform === 'win32'
23
- ? join(root, 'node_modules', '.bin', 'tsx.cmd')
24
- : join(root, 'node_modules', '.bin', 'tsx');
33
+ // tsx 以库形式随包内置:直接用当前 node 运行其 CLI,避免依赖 node_modules/.bin
34
+ // (Windows 包里没有 .cmd shim,且跨平台无需 PATH 里能找到 node)。
35
+ const tsxCli = join(root, 'node_modules', 'tsx', 'dist', 'cli.mjs');
25
36
 
26
- if (!existsSync(tsxBin)) {
37
+ if (!existsSync(tsxCli)) {
27
38
  // 此文件无法 import TS,内联判定语言(仅环境变量)
28
39
  const isEn = /^en/i.test(process.env.LANG ?? "");
29
40
  console.error(
30
41
  isEn
31
- ? `Error: tsx not found at ${tsxBin}.\nRun \`npm install\` in ${root} (or run \`arona setup\` to install dependencies).`
32
- : `错误:未找到 tsx(${tsxBin})。\n请在 ${root} 运行 \`npm install\`(或运行 \`arona setup\` 安装依赖)。`,
42
+ ? `Error: tsx not found at ${tsxCli}.\nRun \`npm install\` in ${root} (or run \`arona setup\` to install dependencies).`
43
+ : `错误:未找到 tsx(${tsxCli})。\n请在 ${root} 运行 \`npm install\`(或运行 \`arona setup\` 安装依赖)。`,
33
44
  );
34
45
  process.exit(1);
35
46
  }
36
47
 
37
- const child = spawn(tsxBin, [join(root, target), ...passArgs], {
48
+ const child = spawn(process.execPath, [tsxCli, join(root, target), ...passArgs], {
38
49
  // 保持用户调用 arona 时所在的目录作为工作目录(Workspace = CWD)。
39
50
  // 之前固定 cwd: root 会让全局安装的 arona 把包目录当工作区,文件工具/项目文档/undo 全部错位。
40
51
  cwd: process.cwd(),
41
52
  stdio: 'inherit',
42
- // Windows: tsxBin 是 .cmd 批处理文件。Node >=20.12.2/18.20.2(CVE-2024-27980 修复)
43
- // 起,spawn .bat/.cmd 且不设 shell 会直接抛 EINVAL,导致 `arona setup` 在 Windows 上无法启动。
44
- shell: process.platform === 'win32',
45
53
  });
46
54
  child.on('exit', (code) => process.exit(code ?? 0));
package/gui/main.cjs ADDED
@@ -0,0 +1,292 @@
1
+ // ARONA GUI Electron 主进程:单窗口,与 Node 后端父进程经 stdin/stdout JSON lines 通信
2
+ // (协议行前缀 ###GUI### 过滤 Electron 日志;与桌宠桥同模式)。
3
+ const { app, BrowserWindow, ipcMain, nativeImage } = require("electron");
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const APP_TITLE = "Arona Agent";
8
+ const ICON_PATH = path.join(__dirname, "renderer", "assets", "icon.png");
9
+
10
+ const PREFIX = "###GUI###";
11
+ const VERBOSE = process.env.ARONA_GUI_VERBOSE === "1";
12
+
13
+ function send(msg) {
14
+ try {
15
+ process.stdout.write(PREFIX + JSON.stringify(msg) + "\n");
16
+ } catch {
17
+ // stdout 已关闭,忽略
18
+ }
19
+ }
20
+
21
+ let win = null;
22
+ let rendererReady = false;
23
+ // backend 在 spawn 后立刻写 mode/ready 行,此时页面尚未 loadFile 完成——
24
+ // 直接 send 会静默丢弃,两页 .page 均保持 hidden → 白屏。就绪前入缓冲,did-finish-load 后 flush。
25
+ const pending = [];
26
+
27
+ function forward(msg) {
28
+ if (win && !win.isDestroyed() && rendererReady) {
29
+ win.webContents.send("gui-event", msg);
30
+ } else {
31
+ pending.push(msg);
32
+ }
33
+ }
34
+
35
+ function flushPending() {
36
+ if (!win || win.isDestroyed()) return;
37
+ while (pending.length) {
38
+ win.webContents.send("gui-event", pending.shift());
39
+ }
40
+ }
41
+
42
+ // ARONA_GUI_SMOKE=1:loadFile + flush 后探测 DOM(页面可见性 / preload / 渲染层脚本),
43
+ // 结果打 SMOKE_RESULT 行后退出——冒烟验证 mode 事件不丢(无需人工看窗口)。
44
+ // 另起 SMOKE_UI 探测(8s 后,等 startMain 就绪):欢迎页 LOGO / 浅色背景 / 斜杠菜单过滤 / 工具行样式。
45
+ function smokeProbe(skipQuit) {
46
+ const js = '(function(){var p=document.querySelectorAll(".page:not(.hidden)");'
47
+ + 'return JSON.stringify({visible:p.length?p[0].id:null,'
48
+ + 'api:!!(window.guiAPI&&window.guiAPI.send&&window.guiAPI.on),'
49
+ + 'setup:!!(window.SetupUI&&window.SetupUI.handle)});})()';
50
+ setTimeout(() => {
51
+ win.webContents.executeJavaScript(js)
52
+ // stderr 会被 backend 转发到终端(stdout 被 ###GUI### 协议解析占用)
53
+ .then((r) => { console.error("SMOKE_RESULT " + r); if (!skipQuit) app.quit(); })
54
+ .catch((e) => { console.error("SMOKE_ERROR " + e); if (!skipQuit) app.quit(); });
55
+ }, 300);
56
+ }
57
+
58
+ function smokeProbeUI() {
59
+ const js = `(function(){
60
+ var p=document.querySelectorAll(".page:not(.hidden)");
61
+ var logo=document.querySelector(".wl-logo");
62
+ var input=document.getElementById("input");
63
+ var menu=document.getElementById("slash-menu");
64
+ var names=[];
65
+ if(input&&menu){
66
+ input.value="/"; input.dispatchEvent(new Event("input"));
67
+ menu.querySelectorAll(".slash-item .name").forEach(function(n){names.push(n.textContent);});
68
+ input.value=""; input.dispatchEvent(new Event("input"));
69
+ }
70
+ var probe=document.createElement("div"); probe.className="msg-tool";
71
+ probe.innerHTML='<span class="t-icon"></span><span class="t-label">终端</span><span class="t-status run"></span>';
72
+ document.body.appendChild(probe);
73
+ var cs=getComputedStyle(probe);
74
+ var out={visible:p.length?p[0].id:null,
75
+ logo:logo?{loaded:logo.naturalWidth>0,h:logo.clientHeight}:null,
76
+ welcome:!!document.getElementById("welcome"),
77
+ chatKids:document.getElementById("chat")?document.getElementById("chat").children.length:-1,
78
+ bg:getComputedStyle(document.body).backgroundColor,
79
+ menu:names,
80
+ toolDisplay:cs.display,toolFlex:cs.alignItems};
81
+ probe.remove();
82
+ return JSON.stringify(out);})()`;
83
+ setTimeout(() => {
84
+ win.webContents.executeJavaScript(js)
85
+ .then((r) => { console.error("SMOKE_UI " + r); app.quit(); })
86
+ .catch((e) => { console.error("SMOKE_UI_ERROR " + e); app.quit(); });
87
+ }, 8000);
88
+ }
89
+
90
+ // ARONA_GUI_DEMO=1:不调用 LLM,向前端注入一段脚本化 agent_event 序列(思考 / 工具行 /
91
+ // 编码子代理实时过程 / 总结文本),预览渲染效果;结束时 capturePage 截图到 /tmp/arona_gui_demo.png。
92
+ function demoScenario() {
93
+ const send = (m) => { if (win && !win.isDestroyed()) win.webContents.send("gui-event", m); };
94
+ const ev = (agentId, type, extra = {}) => send({ type: "agent_event", agentId, event: { type, ...extra } });
95
+ const delta = (agentId, kind, text) => ev(agentId, "message_update", { assistantMessageEvent: { type: kind, delta: text } });
96
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
97
+
98
+ return (async () => {
99
+ // —— 回放测试:ARONA_GUI_DEMO_SESSION=<会话路径> 时读取真实会话 + coding sidecar 注入前端 ——
100
+ const sessPath = process.env.ARONA_GUI_DEMO_SESSION;
101
+ if (sessPath) {
102
+ try {
103
+ const sidecar = sessPath.replace(/\.jsonl$/, ".coding.jsonl");
104
+ const runs = [];
105
+ if (fs.existsSync(sidecar)) {
106
+ for (const line of fs.readFileSync(sidecar, "utf-8").split("\n")) {
107
+ if (!line.trim()) continue;
108
+ const p = JSON.parse(line);
109
+ if (p.type !== "arona-coding-log") runs.push(p);
110
+ }
111
+ }
112
+ send({ type: "coding_runs", runs });
113
+ // 用 marked 官方 hooks 记录 parse 入参 + 调用栈(定位报告文本的渲染路径)
114
+ await win.webContents.executeJavaScript(
115
+ 'window.__mdCalls=[]; window.marked.use({hooks:{preprocess:function(t){'
116
+ + 'if(String(t).indexOf("入口")>=0){window.__mdCalls.push(String(t).slice(0,40)+"\\nSTACK:"+new Error().stack.split("\\n").slice(1,5).join(" | "));}'
117
+ + 'return t;}}}); true',
118
+ ).catch(() => {});
119
+ const messages = fs.readFileSync(sessPath, "utf-8").split("\n")
120
+ .filter((l) => l.trim())
121
+ .map((l) => JSON.parse(l))
122
+ .filter((m) => m.type !== "arona-session" && m.role);
123
+ send({ type: "history", messages });
124
+ console.error("DEMO_SESSION_REPLAY " + sessPath + " runs=" + runs.length);
125
+ await sleep(2000);
126
+ // 滚动到报告中第一个 Markdown 表格附近(文本 "2.1 入口"),预览表格渲染
127
+ await win.webContents.executeJavaScript(
128
+ 'var els=Array.from(document.querySelectorAll(".msg-body h2,.msg-body h3,.msg-body h4,.msg-body p,.msg-body strong"));'
129
+ + 'var t=els.find(function(e){return e.textContent.indexOf("2.1")>=0;});'
130
+ + 'if(t) t.scrollIntoView({block:"start"}); true',
131
+ ).catch(() => {});
132
+ await sleep(300);
133
+ const check = await win.webContents.executeJavaScript(
134
+ '(function(){var out=window.marked.parse("| `a` | 挂载 `<App />` 到 `#root` |",{breaks:true,gfm:true,async:false});'
135
+ + 'return JSON.stringify(String(out).slice(0,300));})()',
136
+ ).catch((e) => "CHECK_ERROR " + e);
137
+ console.error("DEMO_CHECK " + check);
138
+ const calls = await win.webContents.executeJavaScript('JSON.stringify(window.__mdCalls)').catch((e) => "ERR");
139
+ console.error("DEMO_MDCALLS " + calls);
140
+ // 自动展开第一条 Bash 工具行,预览输入 + 输出(Markdown 渲染)效果
141
+ await win.webContents.executeJavaScript(
142
+ 'var r=document.querySelector(".msg-tool[data-tool=\\"bash\\"]"); if(r) r.classList.add("open"); true',
143
+ ).catch(() => {});
144
+ await sleep(300);
145
+ const imgTop = await win.webContents.capturePage();
146
+ fs.writeFileSync("/tmp/arona_gui_demo.png", imgTop.toPNG());
147
+ console.error("DEMO_CAPTURE /tmp/arona_gui_demo.png");
148
+ return;
149
+ } catch (e) {
150
+ console.error("DEMO_SESSION_ERROR " + e);
151
+ }
152
+ }
153
+
154
+ // —— 主 Agent:思考 + 派出编码子代理 ——
155
+ ev("arona", "message_start");
156
+ for (const chunk of ["用户想让我看看项目结构。", "这个任务适合派 millennium 去探索,", "我先设置情绪,然后调用 create_subagent。"]) {
157
+ delta("arona", "thinking_delta", chunk);
158
+ await sleep(260);
159
+ }
160
+ ev("arona", "message_end");
161
+ ev("arona", "tool_execution_start", { toolName: "change_emotion", input: { emotion: "curious" } });
162
+ await sleep(400);
163
+ ev("arona", "tool_execution_end", { isError: false, result: "已切换情绪为 curious" });
164
+ ev("arona", "tool_execution_start", { toolName: "create_subagent", input: { task: "探索 /Users/sunrongchen/Desktop/Projects/m2_her_webui 这个项目的源码结构。简要列出:1. 主要目录结构 2. 核心文件及其职责 3. 关键技术栈", agent: "millennium" } });
165
+
166
+ // —— 编码子代理 millennium:实时过程(思考 / 工具调用 / 报告文本)——
167
+ await sleep(500);
168
+ ev("millennium", "message_start");
169
+ for (const chunk of ["收到探索任务。", "先看顶层目录,再深入 src/。"]) {
170
+ delta("millennium", "thinking_delta", chunk);
171
+ await sleep(240);
172
+ }
173
+ ev("millennium", "message_end");
174
+ ev("millennium", "tool_execution_start", { toolName: "bash", input: { command: "ls src/ && find src -name '*.tsx' | head -20" } });
175
+ await sleep(600);
176
+ ev("millennium", "tool_execution_end", {
177
+ isError: false,
178
+ result: "App.tsx\ncomponents/\n ChatInput.tsx\n MessageBubble.tsx\ncontext/\n ChatContext.tsx\n SettingsContext.tsx\n\n共 **20** 个 `.tsx` 文件,全部位于 `src/` 下的二级目录中。",
179
+ });
180
+ ev("millennium", "tool_execution_start", { toolName: "read", input: { file_path: "src/App.tsx" } });
181
+ await sleep(500);
182
+ ev("millennium", "tool_execution_end", { isError: false, result: "import React from 'react';\nimport ChatContext from './context/ChatContext';\n\nexport default function App() {\n return <ChatContext.Provider>…</ChatContext.Provider>;\n}" });
183
+ ev("millennium", "tool_execution_start", { toolName: "web_search", input: { query: "React 18 release notes" } });
184
+ await sleep(500);
185
+ ev("millennium", "tool_execution_end", { isError: false, result: "React 18.3.1 稳定版;并发特性默认可用。" });
186
+ ev("millennium", "message_start");
187
+ for (const chunk of ["探索完成。这是一个 React 18 + Vite + Tailwind 的纯前端调试台,", "25 个文件约 3500 行,状态管理用 Context + localStorage,无第三方状态库。"]) {
188
+ delta("millennium", "text_delta", chunk);
189
+ await sleep(240);
190
+ }
191
+ ev("millennium", "message_end");
192
+
193
+ // —— 主 Agent:收回结果并总结(同轮次复用说话人标签)——
194
+ ev("arona", "tool_execution_end", { isError: false, result: "子Agent任务完成,最终报告已返回。" });
195
+ ev("arona", "message_start");
196
+ for (const chunk of ["子Agent报告收到啦~ 给 Sensei 划个重点:\n\n", "**项目本质**:纯前端 React SPA,无后端,状态全靠 Context + localStorage。\n\n", "下次需要深挖代码,直接叫它就行~"]) {
197
+ delta("arona", "text_delta", chunk);
198
+ await sleep(240);
199
+ }
200
+ ev("arona", "message_end");
201
+ send({
202
+ type: "ready",
203
+ state: {
204
+ model: "demo", mainAgent: "arona", mainAgentLabel: "阿洛娜", subAgents: [],
205
+ ttsEnabled: false, sttEnabled: false, noVoice: true,
206
+ processing: false, recording: false, currentSessionPath: null,
207
+ },
208
+ });
209
+
210
+ await sleep(600);
211
+ try {
212
+ const img = await win.webContents.capturePage();
213
+ fs.writeFileSync("/tmp/arona_gui_demo.png", img.toPNG());
214
+ console.error("DEMO_CAPTURE /tmp/arona_gui_demo.png");
215
+ } catch (e) {
216
+ console.error("DEMO_CAPTURE_ERROR " + e);
217
+ }
218
+ })();
219
+ }
220
+
221
+ function createWindow() {
222
+ win = new BrowserWindow({
223
+ width: 1100,
224
+ height: 760,
225
+ minWidth: 720,
226
+ minHeight: 480,
227
+ title: APP_TITLE,
228
+ backgroundColor: "#f6f7f9",
229
+ titleBarStyle: "hiddenInset",
230
+ trafficLightPosition: { x: 16, y: 16 },
231
+ icon: ICON_PATH,
232
+ webPreferences: {
233
+ preload: path.join(__dirname, "preload.cjs"),
234
+ contextIsolation: true,
235
+ nodeIntegration: false,
236
+ },
237
+ });
238
+ // 先挂监听再 loadFile:避免加载完成事件在挂监听前触发导致 rendererReady 永不置位
239
+ win.webContents.once("did-finish-load", () => {
240
+ rendererReady = true;
241
+ flushPending();
242
+ if (process.env.ARONA_GUI_SMOKE === "1") { smokeProbe(true); smokeProbeUI(); }
243
+ if (process.env.ARONA_GUI_DEMO === "1") setTimeout(demoScenario, 2000);
244
+ });
245
+ win.loadFile(path.join(__dirname, "renderer", "index.html"));
246
+ win.on("closed", () => { win = null; rendererReady = false; });
247
+ win.webContents.on("did-fail-load", (_e, code, desc, url) => {
248
+ console.error(`[gui] did-fail-load ${code} ${desc} ${url}`);
249
+ });
250
+ if (VERBOSE) win.webContents.openDevTools({ mode: "detach" });
251
+ }
252
+
253
+ // renderer → backend
254
+ ipcMain.on("gui-send", (_event, msg) => {
255
+ send(msg);
256
+ });
257
+
258
+ // backend → renderer(行缓冲解析)
259
+ let buffer = "";
260
+ process.stdin.on("data", (data) => {
261
+ buffer += data.toString();
262
+ const lines = buffer.split("\n");
263
+ buffer = lines.pop() || "";
264
+ for (const line of lines) {
265
+ const trimmed = line.trim();
266
+ if (!trimmed.startsWith(PREFIX)) continue;
267
+ try {
268
+ forward(JSON.parse(trimmed.slice(PREFIX.length)));
269
+ } catch {
270
+ // 非 JSON,忽略
271
+ }
272
+ }
273
+ });
274
+
275
+ // 窗口全关:先发 exit 请求让后端走完整清理,再退出(延迟让协议行先 flush)
276
+ app.on("window-all-closed", () => {
277
+ send({ type: "exit" });
278
+ setTimeout(() => app.quit(), 200);
279
+ });
280
+
281
+ app.whenReady().then(() => {
282
+ // macOS 开发模式下 Dock 图标默认是 Electron 图标,用 LOGO 替换(打包后由应用 bundle 提供)
283
+ if (process.platform === "darwin" && app.dock) {
284
+ const icon = nativeImage.createFromPath(ICON_PATH);
285
+ if (!icon.isEmpty()) app.dock.setIcon(icon);
286
+ }
287
+ createWindow();
288
+ });
289
+
290
+ app.on("render-process-gone", (_e, details) => {
291
+ console.error(`[gui] render-process-gone reason=${details.reason} exitCode=${details.exitCode}`);
292
+ });
@@ -0,0 +1,11 @@
1
+ // GUI preload:contextBridge 暴露双向协议通道(send / on)
2
+ const { contextBridge, ipcRenderer } = require("electron");
3
+
4
+ contextBridge.exposeInMainWorld("guiAPI", {
5
+ send(msg) {
6
+ ipcRenderer.send("gui-send", msg);
7
+ },
8
+ on(cb) {
9
+ ipcRenderer.on("gui-event", (_e, msg) => cb(msg));
10
+ },
11
+ });