arona-agent 1.2.1 → 1.2.2

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/gui/main.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  // ARONA GUI Electron 主进程:单窗口,与 Node 后端父进程经 stdin/stdout JSON lines 通信
2
2
  // (协议行前缀 ###GUI### 过滤 Electron 日志;与桌宠桥同模式)。
3
3
  const { app, BrowserWindow, Menu, ipcMain, nativeImage } = require("electron");
4
+ const crypto = require("crypto");
4
5
  const fs = require("fs");
6
+ const http = require("http");
5
7
  const path = require("path");
6
8
 
7
9
  // Windows:GUI 是纯 HTML/CSS(无 WebGL),默认硬件加速下页面加载/脚本均正常但不 paint(白屏,
@@ -290,7 +292,8 @@ ipcMain.on("gui-send", (_event, msg) => {
290
292
  send(msg);
291
293
  });
292
294
 
293
- // backend → renderer(行缓冲解析)
295
+ // backend → renderer(行缓冲解析;Windows 下此路不通——GUI 子系统 Electron 的 stdin 数据不可达,
296
+ // 见下方 HTTP 通道。保留作非 Windows 平台的备用通道)
294
297
  let buffer = "";
295
298
  process.stdin.on("data", (data) => {
296
299
  buffer += data.toString();
@@ -307,10 +310,41 @@ process.stdin.on("data", (data) => {
307
310
  }
308
311
  });
309
312
 
310
- // 握手:告知 backend 本进程的 stdin 解析器已就绪,可以开始下发 ###GUI### 事件。
311
- // Windows spawn 后立刻写 stdin 会丢数据(GUI 白屏根因:mode 事件从未到达本进程),
312
- // backend 收到 hello 前把事件全部入队,收到后按序下发。
313
- send({ type: "hello" });
313
+ // 本地 HTTP 通道(backend GUI 主方向):Windows 下 Electron 子进程 stdin 完全不可达(hello 握手
314
+ // 证明进程与 stdout 均正常、数据仍不到达),事件改走 127.0.0.1 随机端口 + 随机 token(随 hello 行
315
+ // 告知 backend,防本机其他进程伪造)。stdout(GUI backend)方向不受影响,继续走 ###GUI### 行。
316
+ const HTTP_TOKEN = crypto.randomBytes(16).toString("hex");
317
+ let helloSent = false;
318
+ function sendHello(httpPort) {
319
+ if (helloSent) return;
320
+ helloSent = true;
321
+ send({ type: "hello", httpPort: httpPort || 0, token: httpPort ? HTTP_TOKEN : "" });
322
+ }
323
+ const httpServer = http.createServer((req, res) => {
324
+ if (req.method !== "POST" || req.headers["x-arona-token"] !== HTTP_TOKEN) {
325
+ res.writeHead(403);
326
+ res.end();
327
+ return;
328
+ }
329
+ let body = "";
330
+ req.on("data", (c) => (body += c));
331
+ req.on("end", () => {
332
+ res.writeHead(204);
333
+ res.end();
334
+ try {
335
+ forward(JSON.parse(body));
336
+ } catch {
337
+ // 非 JSON,忽略
338
+ }
339
+ });
340
+ });
341
+ httpServer.on("error", (e) => {
342
+ console.error("[gui] http server error:", e.message, "→ 退回 stdin 通道");
343
+ sendHello(0); // HTTP 起不来也要发 hello(不带端口,backend 退回 stdin 写入)
344
+ });
345
+ httpServer.listen(0, "127.0.0.1", () => {
346
+ sendHello(httpServer.address().port);
347
+ });
314
348
 
315
349
  // 窗口全关:先发 exit 请求让后端走完整清理,再退出(延迟让协议行先 flush)
316
350
  app.on("window-all-closed", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arona-agent",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Terminal AI Agent with desktop pet Arona — eye-tracking pupils, voice cloning, Computer Use, TTS/STT, MCP.",
5
5
  "keywords": [
6
6
  "voice-clone",
package/src/gui/index.ts CHANGED
@@ -21,11 +21,15 @@ class GuiBridge {
21
21
  private controller: GuiController | null = null;
22
22
  private startingMain = false;
23
23
  private exited = false;
24
- // Windows 下 spawn 后立刻写 stdin 会丢数据(GUI 白屏根因:mode 事件从未到达 Electron 进程)——
25
- // GUI 进程回报 hello(stdin 解析器就绪的握手,gui/main.cjs 模块加载即发送)才真正下发,此前入队。
26
- // macOS/Linux 不受影响,但握手在所有平台统一走一遍(幂等,且顺带验证 stdout 协议链路通)。
24
+ // Windows 下 spawn 后立刻写 stdin 会丢数据,且实测 GUI 子系统 Electron 的 stdin **完全不可达**
25
+ //(hello 握手证明进程与 stdout 均正常,数据仍不到达)——GUI 进程起 127.0.0.1 随机端口 + 随机 token
26
+ // HTTP 服务,端口/token 随 hello 行告知;hello 前入队,收到后按序经 HTTP 下发。
27
+ // httpPort=0(HTTP 起失败)时退回 stdin 写入(非 Windows 平台 stdin 本来就通)。
27
28
  private helloReceived = false;
28
29
  private queuedEvents: GuiEvent[] = [];
30
+ private guiHttpPort = 0;
31
+ private guiHttpToken = "";
32
+ private httpChain: Promise<void> = Promise.resolve(); // 串行链保序(HTTP 异步,并发会乱序)
29
33
 
30
34
  emit(ev: GuiEvent): void {
31
35
  if (verbose) console.error(chalk.gray("[gui:verbose]"), "emit", ev.type, this.helloReceived ? "" : "(queued)");
@@ -34,6 +38,10 @@ class GuiBridge {
34
38
  return;
35
39
  }
36
40
  if (!this.proc || this.proc.killed) return;
41
+ if (this.guiHttpPort) {
42
+ this.httpChain = this.httpChain.then(() => this.postEvent(ev));
43
+ return;
44
+ }
37
45
  try {
38
46
  this.proc.stdin.write(formatGuiLine(ev));
39
47
  } catch {
@@ -41,6 +49,21 @@ class GuiBridge {
41
49
  }
42
50
  }
43
51
 
52
+ /** 经本地 HTTP 通道下发事件(gui/main.cjs 的 127.0.0.1 随机端口服务,token 鉴权) */
53
+ private async postEvent(ev: GuiEvent): Promise<void> {
54
+ try {
55
+ await fetch(`http://127.0.0.1:${this.guiHttpPort}/`, {
56
+ method: "POST",
57
+ headers: { "content-type": "application/json", "x-arona-token": this.guiHttpToken },
58
+ body: JSON.stringify(ev),
59
+ });
60
+ } catch (err) {
61
+ if (verbose) {
62
+ console.error(chalk.gray("[gui:verbose]"), "http emit failed:", err instanceof Error ? err.message : err);
63
+ }
64
+ }
65
+ }
66
+
44
67
  async start(): Promise<void> {
45
68
  const electronPath = await getElectronPath();
46
69
  if (!electronPath) {
@@ -187,13 +210,15 @@ class GuiBridge {
187
210
  private async handleRequest(req: GuiRequest): Promise<void> {
188
211
  switch (req.type) {
189
212
  case "hello": {
190
- // GUI 进程 stdin 解析器就绪:解锁排队中的事件(emit mode/setup_info 在 spawn 后立即调用,
191
- // Windows 下彼时写入会丢失——见 emit() 注释)
213
+ // GUI 进程握手:解锁排队中的事件(emit mode/setup_info 在 spawn 后立即调用,Windows 下彼时
214
+ // stdin 写入会丢失)。端口/token 就位后,后续事件经本地 HTTP 通道下发。
192
215
  this.helloReceived = true;
216
+ this.guiHttpPort = Number(req.httpPort) || 0;
217
+ this.guiHttpToken = String(req.token || "");
193
218
  const queued = this.queuedEvents;
194
219
  this.queuedEvents = [];
195
220
  if (verbose && queued.length) {
196
- console.error(chalk.gray("[gui:verbose]"), "hello received, flushing", queued.length, "queued events");
221
+ console.error(chalk.gray("[gui:verbose]"), "hello received (httpPort=" + this.guiHttpPort + "), flushing", queued.length, "queued events");
197
222
  }
198
223
  for (const ev of queued) this.emit(ev);
199
224
  break;
@@ -64,6 +64,8 @@ export interface GuiState {
64
64
  // gui → backend
65
65
  // ============================================================
66
66
  export type GuiRequest =
67
+ // GUI 进程握手(gui/main.cjs 起好 HTTP 通道后回报;httpPort=0 表示 HTTP 不可用,backend 退回 stdin)
68
+ | { type: "hello"; httpPort: number; token: string }
67
69
  // 用户消息(@file/!shell 由 backend 展开)
68
70
  | { type: "input"; text: string }
69
71
  // 斜杠命令(name 不含 "/")