arona-agent 1.2.0 → 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,6 +310,42 @@ process.stdin.on("data", (data) => {
307
310
  }
308
311
  });
309
312
 
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
+ });
348
+
310
349
  // 窗口全关:先发 exit 请求让后端走完整清理,再退出(延迟让协议行先 flush)
311
350
  app.on("window-all-closed", () => {
312
351
  send({ type: "exit" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arona-agent",
3
- "version": "1.2.0",
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,10 +21,27 @@ class GuiBridge {
21
21
  private controller: GuiController | null = null;
22
22
  private startingMain = false;
23
23
  private exited = false;
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 本来就通)。
28
+ private helloReceived = false;
29
+ private queuedEvents: GuiEvent[] = [];
30
+ private guiHttpPort = 0;
31
+ private guiHttpToken = "";
32
+ private httpChain: Promise<void> = Promise.resolve(); // 串行链保序(HTTP 异步,并发会乱序)
24
33
 
25
34
  emit(ev: GuiEvent): void {
26
- if (verbose) console.error(chalk.gray("[gui:verbose]"), "emit", ev.type); // 白屏排查:后端→GUI 事件流向
35
+ if (verbose) console.error(chalk.gray("[gui:verbose]"), "emit", ev.type, this.helloReceived ? "" : "(queued)");
36
+ if (!this.helloReceived) {
37
+ this.queuedEvents.push(ev);
38
+ return;
39
+ }
27
40
  if (!this.proc || this.proc.killed) return;
41
+ if (this.guiHttpPort) {
42
+ this.httpChain = this.httpChain.then(() => this.postEvent(ev));
43
+ return;
44
+ }
28
45
  try {
29
46
  this.proc.stdin.write(formatGuiLine(ev));
30
47
  } catch {
@@ -32,6 +49,21 @@ class GuiBridge {
32
49
  }
33
50
  }
34
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
+
35
67
  async start(): Promise<void> {
36
68
  const electronPath = await getElectronPath();
37
69
  if (!electronPath) {
@@ -56,6 +88,16 @@ class GuiBridge {
56
88
  stdio: ["pipe", "pipe", "pipe"],
57
89
  });
58
90
 
91
+ // 握手超时告警:hello 不来 = GUI 进程的 stdin/stdout 协议链路不通(页面白屏只是其结果)
92
+ setTimeout(() => {
93
+ if (!this.helloReceived && !this.exited && this.proc) {
94
+ console.error(chalk.red(t(
95
+ "GUI:15s 未收到 GUI 进程握手(hello),###GUI### stdin 协议链路不通,界面将保持白屏。",
96
+ "GUI: no handshake (hello) from the GUI process within 15s; the ###GUI### stdin protocol is broken and the window will stay blank.",
97
+ )));
98
+ }
99
+ }, 15000);
100
+
59
101
  this.proc.stdout.on("data", (data) => {
60
102
  this.buffer += data.toString();
61
103
  const lines = this.buffer.split("\n");
@@ -167,6 +209,20 @@ class GuiBridge {
167
209
 
168
210
  private async handleRequest(req: GuiRequest): Promise<void> {
169
211
  switch (req.type) {
212
+ case "hello": {
213
+ // GUI 进程握手:解锁排队中的事件(emit mode/setup_info 在 spawn 后立即调用,Windows 下彼时
214
+ // stdin 写入会丢失)。端口/token 就位后,后续事件经本地 HTTP 通道下发。
215
+ this.helloReceived = true;
216
+ this.guiHttpPort = Number(req.httpPort) || 0;
217
+ this.guiHttpToken = String(req.token || "");
218
+ const queued = this.queuedEvents;
219
+ this.queuedEvents = [];
220
+ if (verbose && queued.length) {
221
+ console.error(chalk.gray("[gui:verbose]"), "hello received (httpPort=" + this.guiHttpPort + "), flushing", queued.length, "queued events");
222
+ }
223
+ for (const ev of queued) this.emit(ev);
224
+ break;
225
+ }
170
226
  case "setup_submit": {
171
227
  const ok = await runGuiSetup(req.form as unknown as GuiSetupForm, (ev) => this.emit(ev));
172
228
  if (ok) await this.startMain();
@@ -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 不含 "/")