arona-agent 1.0.0

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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +149 -0
  3. package/assets/blue-archive/arona/spine/arona_spr.atlas.txt +1168 -0
  4. package/assets/blue-archive/arona/spine/arona_spr.png +0 -0
  5. package/assets/blue-archive/arona/spine/arona_spr.skel +0 -0
  6. package/assets/blue-archive/arona/voice.mp3 +0 -0
  7. package/assets/blue-archive/hoshino/hoshino_spr.atlas.txt +174 -0
  8. package/assets/blue-archive/hoshino/hoshino_spr.json +15385 -0
  9. package/assets/blue-archive/hoshino/hoshino_spr.png +0 -0
  10. package/assets/blue-archive/hoshino/hoshino_spr.skel +0 -0
  11. package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.atlas.txt +111 -0
  12. package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.png +0 -0
  13. package/assets/blue-archive/hoshino/robber/hoshino_robber_spr.skel +0 -0
  14. package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.atlas.txt +251 -0
  15. package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.png +0 -0
  16. package/assets/blue-archive/hoshino/swimsuit/hoshino_swimsuit_spr.skel +0 -0
  17. package/assets/blue-archive/hoshino/voice.mp3 +0 -0
  18. package/assets/blue-archive/plana/spine/plana_spr.atlas.txt +1266 -0
  19. package/assets/blue-archive/plana/spine/plana_spr.png +0 -0
  20. package/assets/blue-archive/plana/spine/plana_spr.skel +0 -0
  21. package/assets/blue-archive/plana/voice.mp3 +0 -0
  22. package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.atlas.txt +97 -0
  23. package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.png +0 -0
  24. package/assets/blue-archive/shiroko/ridingsuit/shiroko_ridingsuit_spr.skel +0 -0
  25. package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.atlas.txt +174 -0
  26. package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.png +0 -0
  27. package/assets/blue-archive/shiroko/robber/shiroko_robber_spr.skel +0 -0
  28. package/assets/blue-archive/shiroko/shiroko_spr.atlas.txt +167 -0
  29. package/assets/blue-archive/shiroko/shiroko_spr.json +10702 -0
  30. package/assets/blue-archive/shiroko/shiroko_spr.png +0 -0
  31. package/assets/blue-archive/shiroko/shiroko_spr.skel +0 -0
  32. package/assets/blue-archive/shiroko/voice.mp3 +0 -0
  33. package/bin/arona.mjs +43 -0
  34. package/package.json +37 -0
  35. package/pet/agents.cjs +197 -0
  36. package/pet/main.cjs +462 -0
  37. package/pet/preload.cjs +21 -0
  38. package/pet/renderer/fx.html +30 -0
  39. package/pet/renderer/fx.js +41 -0
  40. package/pet/renderer/gallery.html +18 -0
  41. package/pet/renderer/gallery.js +31 -0
  42. package/pet/renderer/index.html +18 -0
  43. package/pet/renderer/renderer.js +382 -0
  44. package/pet/renderer/spine_layer.js +609 -0
  45. package/pet/renderer/spinetest.html +24 -0
  46. package/pet/renderer/spinetest.js +295 -0
  47. package/pet/renderer/style.css +63 -0
  48. package/pet/tools/gallery_capture.cjs +267 -0
  49. package/pet/tools/gen_sway.cjs +170 -0
  50. package/pet/tools/meshify_region.cjs +105 -0
  51. package/pet/tools/skel_inspect.cjs +112 -0
  52. package/pet/tools/skel_to_json.cjs +410 -0
  53. package/pet/tools/spine_node.cjs +69 -0
  54. package/pet/tools/visual_test.cjs +262 -0
  55. package/pet/vendor/spine/spine-canvas.js +8472 -0
  56. package/pet/vendor/spine/spine-webgl.js +10839 -0
  57. package/python/__pycache__/_i18n.cpython-313.pyc +0 -0
  58. package/python/__pycache__/_i18n.cpython-314.pyc +0 -0
  59. package/python/__pycache__/computer_use.cpython-314.pyc +0 -0
  60. package/python/__pycache__/hotkey.cpython-313.pyc +0 -0
  61. package/python/__pycache__/hotkey.cpython-314.pyc +0 -0
  62. package/python/__pycache__/stt.cpython-313.pyc +0 -0
  63. package/python/__pycache__/stt.cpython-314.pyc +0 -0
  64. package/python/__pycache__/tts.cpython-313.pyc +0 -0
  65. package/python/__pycache__/tts.cpython-314.pyc +0 -0
  66. package/python/__pycache__/tts_stream.cpython-313.pyc +0 -0
  67. package/python/__pycache__/tts_stream.cpython-314.pyc +0 -0
  68. package/python/__pycache__/voice_clone.cpython-314.pyc +0 -0
  69. package/python/_i18n.py +18 -0
  70. package/python/computer_use.py +190 -0
  71. package/python/hotkey.py +196 -0
  72. package/python/stt.py +240 -0
  73. package/python/tts_say.py +177 -0
  74. package/python/voice_clone.py +98 -0
  75. package/requirements.txt +6 -0
  76. package/src/agent.ts +791 -0
  77. package/src/agent_registry.ts +109 -0
  78. package/src/commands.ts +641 -0
  79. package/src/config.ts +245 -0
  80. package/src/in_memory_credentials.ts +61 -0
  81. package/src/index.ts +78 -0
  82. package/src/locale.ts +131 -0
  83. package/src/logo.ts +120 -0
  84. package/src/mcp.ts +192 -0
  85. package/src/memory.ts +303 -0
  86. package/src/pet.ts +346 -0
  87. package/src/renderer.ts +350 -0
  88. package/src/repl.ts +902 -0
  89. package/src/setup.ts +371 -0
  90. package/src/skills.ts +96 -0
  91. package/src/slash_menu.ts +371 -0
  92. package/src/slash_registry.ts +53 -0
  93. package/src/speaker_context.ts +41 -0
  94. package/src/text_split.ts +79 -0
  95. package/src/tools/computer_use.ts +142 -0
  96. package/src/tools/emotion_tool.ts +55 -0
  97. package/src/tools/keep_silent_tool.ts +26 -0
  98. package/src/tools/memory_tool.ts +24 -0
  99. package/src/tools/skill_tools.ts +100 -0
  100. package/src/tools/tavily_tools.ts +431 -0
  101. package/src/tools/voice_tools.ts +28 -0
  102. package/src/tts_stream.ts +260 -0
  103. package/src/tui_select.ts +146 -0
  104. package/src/undo.ts +451 -0
  105. package/src/utils/python.ts +211 -0
  106. package/src/utils/spawn.ts +56 -0
  107. package/src/voice.ts +83 -0
  108. package/src/voice_cli.ts +198 -0
  109. package/src/voices.ts +135 -0
package/pet/main.cjs ADDED
@@ -0,0 +1,462 @@
1
+ // ARONA 桌宠 Electron 主进程
2
+ // 与主 Agent 进程通过 stdin/stdout JSON lines 通信(协议行前缀 ###PET### 过滤 Electron 日志)
3
+ // 支持多角色同屏:ARONA_AGENT = 主 Agent,ARONA_SUB_AGENTS = 逗号分隔的子 Agent 列表;
4
+ // 每个角色一个 BrowserWindow,共享一个全屏特效窗。
5
+ const { app, BrowserWindow, ipcMain, screen } = require("electron");
6
+ const path = require("path");
7
+ const fs = require("fs");
8
+ const os = require("os");
9
+ const { AGENTS } = require("./agents.cjs");
10
+
11
+ // Windows 透明无边框窗口在部分显卡驱动下会触发渲染进程崩溃;禁 GPU 硬件加速(须在 app ready 前调用)。
12
+ // 注意:不能用 app.disableHardwareAcceleration()——Electron 43 里它会顺带 --disable-software-rasterizer,
13
+ // 把软件 WebGL 一并堵死(实测 getGPUFeatureStatus().webgl === "disabled_off",Spine 直接白屏)。
14
+ // 改用细粒度 flag:disable-gpu 只禁硬件 GPU(保留"防透明窗口崩溃"的初衷),
15
+ // enable-unsafe-swiftshader 放行 SwiftShader 软件 WebGL(仍失败则 spine_layer 自动降级 Canvas 2D,见 §B12)。
16
+ if (process.platform === "win32") {
17
+ app.commandLine.appendSwitch("disable-gpu");
18
+ app.commandLine.appendSwitch("enable-unsafe-swiftshader");
19
+ }
20
+
21
+ // 角色选择:主 Agent + 子 Agent 列表
22
+ const MAIN_AGENT_ID = process.env.ARONA_AGENT && AGENTS[process.env.ARONA_AGENT] ? process.env.ARONA_AGENT : "arona";
23
+ const SUB_AGENT_IDS = (process.env.ARONA_SUB_AGENTS || "")
24
+ .split(",")
25
+ .map((s) => s.trim())
26
+ .filter((s) => s && AGENTS[s]);
27
+ const AGENT_IDS = [MAIN_AGENT_ID, ...SUB_AGENT_IDS];
28
+
29
+ const PREFIX = "###PET###";
30
+ const POS_FILE = path.join(os.homedir(), ".arona", "pet.json");
31
+ const WIN_W = 580; // 窗口总宽:左侧 320px 为 Spine 角色渲染区,右侧 260px 为文字气泡专用区(避免气泡遮脸)
32
+ const WIN_H = 674;
33
+ const SUB_OFFSET_X = 620; // 子窗口默认横向错开(> WIN_W,避免角色+气泡区重叠)
34
+ const SUB_OFFSET_Y = 40;
35
+
36
+ // --verbose(src/pet.ts 注入 ARONA_PET_VERBOSE=1 + --enable-logging):
37
+ // 主进程详细日志走 stderr 原样转发回终端,用于锁定 Windows 白屏等"进程活着但无画面"问题。
38
+ const VERBOSE = process.env.ARONA_PET_VERBOSE === "1";
39
+ function vlog(...args) {
40
+ if (VERBOSE) console.error("[pet:verbose]", ...args);
41
+ }
42
+
43
+ /** agentId -> BrowserWindow;主 Agent 始终在列表首位 */
44
+ const petWins = new Map();
45
+ let fxWin = null; // 全屏透明特效窗口(点击/拖尾),覆盖所有显示器、鼠标穿透
46
+
47
+ function send(msg) {
48
+ try {
49
+ process.stdout.write(PREFIX + JSON.stringify(msg) + "\n");
50
+ } catch {
51
+ // stdout 已关闭,忽略
52
+ }
53
+ }
54
+
55
+ // ---- 位置记忆(多角色格式:{ [agentId]: { x, y } };兼容旧 { x, y } → arona) ----
56
+ function loadPositions() {
57
+ try {
58
+ const data = JSON.parse(fs.readFileSync(POS_FILE, "utf-8"));
59
+ if (data && typeof data === "object" && !Array.isArray(data)) return data;
60
+ } catch {
61
+ // 无记录或损坏,忽略
62
+ }
63
+ return {};
64
+ }
65
+
66
+ function loadPosition(agentId) {
67
+ const data = loadPositions();
68
+ let pos = data[agentId];
69
+ // 旧版单角色格式兼容迁移:{x,y} 视为 arona 的位置
70
+ if (!pos && agentId === MAIN_AGENT_ID && Number.isFinite(data.x) && Number.isFinite(data.y)) {
71
+ pos = { x: data.x, y: data.y };
72
+ }
73
+ if (!pos || !Number.isFinite(pos.x) || !Number.isFinite(pos.y)) return null;
74
+ // 屏幕边界检查:记忆坐标须落在任一显示器 bounds 内(中心点判定),否则视为失效。
75
+ // 防分辨率变化 / Mac→Win 迁移 / 负坐标导致窗口创建在屏幕外"看似没显示"。
76
+ const W = WIN_W, H = WIN_H;
77
+ for (const d of screen.getAllDisplays()) {
78
+ const { x, y, width, height } = d.bounds;
79
+ const cx = pos.x + W / 2, cy = pos.y + H / 2;
80
+ if (cx >= x && cx <= x + width && cy >= y && cy <= y + height) return pos;
81
+ }
82
+ return null;
83
+ }
84
+
85
+ function savePosition(agentId, x, y) {
86
+ try {
87
+ const data = loadPositions();
88
+ // 旧版 {x,y} 迁移:一旦保存新格式,就不再有顶层 x/y
89
+ delete data.x;
90
+ delete data.y;
91
+ data[agentId] = { x, y };
92
+ fs.mkdirSync(path.dirname(POS_FILE), { recursive: true });
93
+ fs.writeFileSync(POS_FILE, JSON.stringify(data, null, 2));
94
+ } catch {
95
+ // 写入失败不影响功能
96
+ }
97
+ }
98
+
99
+ // ---- 窗口 ----
100
+ function defaultPosition(index) {
101
+ if (index === 0) return null;
102
+ // 找不到记忆坐标时使用相对主显示器的默认错开位(macOS 菜单栏下方安全区起步)
103
+ const primary = screen.getPrimaryDisplay().bounds;
104
+ return {
105
+ x: primary.x + 40 + SUB_OFFSET_X * (index - 1),
106
+ y: primary.y + 80 + SUB_OFFSET_Y * (index - 1),
107
+ };
108
+ }
109
+
110
+ function createPetWindow(agentId, index) {
111
+ const agent = AGENTS[agentId];
112
+ const bw = new BrowserWindow({
113
+ width: WIN_W,
114
+ height: WIN_H,
115
+ transparent: true,
116
+ frame: false,
117
+ hasShadow: false,
118
+ alwaysOnTop: true,
119
+ skipTaskbar: true,
120
+ resizable: false,
121
+ focusable: false, // 不抢终端焦点
122
+ webPreferences: {
123
+ preload: path.join(__dirname, "preload.cjs"),
124
+ contextIsolation: true,
125
+ nodeIntegration: false,
126
+ },
127
+ });
128
+ bw.setAlwaysOnTop(true, "screen-saver");
129
+ if (process.platform === "darwin") {
130
+ bw.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
131
+ }
132
+ const pos = loadPosition(agentId) || defaultPosition(index);
133
+ if (pos) bw.setPosition(pos.x, pos.y);
134
+ vlog("window created", JSON.stringify({ agentId, pos, alwaysOnTop: bw.isAlwaysOnTop(), visible: bw.isVisible(), bounds: bw.getBounds() }));
135
+
136
+ // PET_PAGE=spinetest → 加载 Spine 调试页(默认仍 index.html,零风险)
137
+ const page = process.env.PET_PAGE === "spinetest" ? "spinetest.html" : "index.html";
138
+ // ?agent=<id>:渲染层(spine_layer.js/renderer.js)据此读取对应角色配置
139
+ const url = path.join(__dirname, "renderer", page);
140
+ bw.loadFile(url, { search: "agent=" + agentId });
141
+ vlog("loadFile", url, "?agent=" + agentId);
142
+ if (process.env.PET_PAGE) {
143
+ // 调试页:附带 devtools 便于人工查看 console / 网络加载
144
+ bw.webContents.openDevTools({ mode: "detach" });
145
+ }
146
+ bw.webContents.on("did-finish-load", () => {
147
+ vlog("did-finish-load", bw.webContents.getURL());
148
+ send({ type: "ready", agent: agentId });
149
+ });
150
+ // ---- GL 驱动消息缓冲 / 报错解锁(2026-08-20)----
151
+ // Windows SwiftShader 下 Chromium 把 GL 驱动性能提示(如
152
+ // "GPU stall due to ReadPixels",Chromium gl_utils.cc 的 Performance 级消息)
153
+ // 作为 console 消息打到渲染进程。非 verbose 时是纯噪音——平时静默缓冲;
154
+ // 一旦程序真报错(error 级 console / did-fail-load / render-process-gone),
155
+ // 补打缓冲行并解锁后续不过滤,保住排障信息。VERBOSE 全量输出不走此逻辑。
156
+ const GL_DRIVER_MSG = "GL Driver Message";
157
+ let glDroppedBuf = []; // 被过滤的 GL 驱动消息(环形,上限 20)
158
+ let glUnlocked = false; // 报错后不再过滤
159
+ const unlockGlLogs = () => {
160
+ if (glUnlocked) return;
161
+ glUnlocked = true;
162
+ if (glDroppedBuf.length) {
163
+ for (const m of glDroppedBuf) console.error(`[pet:render:${agentId}] ${m}`);
164
+ glDroppedBuf = [];
165
+ }
166
+ };
167
+ // 页面加载失败(资源 404 / file:// 路径错 / 加载被拦):必然白屏,任何模式都要转发
168
+ bw.webContents.on("did-fail-load", (_e, code, desc, url2, isMain) => {
169
+ if (!isMain) return;
170
+ unlockGlLogs();
171
+ console.error(`[pet:render] did-fail-load agent=${agentId} code=${code} desc=${desc} url=${url2}`);
172
+ });
173
+ // renderer console 转发到 stderr(src/pet.ts 已打印 [pet] 前缀):页面 JS 错误默认静默
174
+ // (不进 stderr),Windows 白屏等故障全靠它定位。Electron 43 规范签名为单事件对象:
175
+ // event.{message, level, lineNumber, sourceId}(老双参数签名会打 deprecation 警告)。
176
+ bw.webContents.on("console-message", (event) => {
177
+ const msg = typeof event.message === "string" ? event.message : "";
178
+ if (!msg) return;
179
+ const line = event.lineNumber ? `:${event.lineNumber}` : "";
180
+ if (VERBOSE) {
181
+ const src = event.sourceId ? ` (${event.sourceId}${line})` : "";
182
+ console.error(`[pet:render:${event.level ?? "?"}:${agentId}]${src} ${msg}`);
183
+ return;
184
+ }
185
+ // 非 verbose:严格判断——GL 驱动性能提示平时静默,仅报错后放行
186
+ if (event.level === 3) {
187
+ unlockGlLogs(); // 报错:补打被缓冲的 GL 消息,之后不再过滤
188
+ console.error(`[pet:render:${agentId}]${line} ${msg}`);
189
+ return;
190
+ }
191
+ if (!glUnlocked && msg.includes(GL_DRIVER_MSG)) {
192
+ if (glDroppedBuf.length >= 20) glDroppedBuf.shift();
193
+ glDroppedBuf.push(msg);
194
+ return;
195
+ }
196
+ console.error(`[pet:render:${agentId}]${line} ${msg}`);
197
+ });
198
+ // 渲染进程崩溃上报(Windows 透明窗口崩溃的主因;reason/exitCode 可精确定位)
199
+ bw.webContents.on("render-process-gone", (_e, details) => {
200
+ unlockGlLogs();
201
+ send({ type: "crash", agent: agentId, kind: "render", reason: details.reason, exitCode: details.exitCode, url: bw.webContents.getURL() });
202
+ vlog("render-process-gone", JSON.stringify({ agentId, ...details }));
203
+ });
204
+ // 窗口关闭诊断(verbose):window-all-closed 会因任一窗口被系统/驱动关闭而触发,导致 code 0 退出
205
+ bw.on("closed", () => {
206
+ vlog("window closed", agentId);
207
+ petWins.delete(agentId);
208
+ });
209
+ petWins.set(agentId, bw);
210
+ }
211
+
212
+ // ---- 全屏特效窗口(点击/拖尾铺满整个屏幕) ----
213
+ // 覆盖所有显示器的并集 bounds;透明、鼠标穿透(setIgnoreMouseEvents),
214
+ // 层级高于桌宠(screen-saver + 1),这样拖尾能盖在角色之上跨屏延伸。
215
+ function getAllDisplaysBounds() {
216
+ const ds = screen.getAllDisplays();
217
+ if (!ds.length) return screen.getPrimaryDisplay().bounds;
218
+ let x = Infinity, y = Infinity, x2 = -Infinity, y2 = -Infinity;
219
+ for (const d of ds) {
220
+ x = Math.min(x, d.bounds.x);
221
+ y = Math.min(y, d.bounds.y);
222
+ x2 = Math.max(x2, d.bounds.x + d.bounds.width);
223
+ y2 = Math.max(y2, d.bounds.y + d.bounds.height);
224
+ }
225
+ return { x, y, width: x2 - x, height: y2 - y };
226
+ }
227
+
228
+ function createFxWindow() {
229
+ const b = getAllDisplaysBounds();
230
+ fxWin = new BrowserWindow({
231
+ x: b.x,
232
+ y: b.y,
233
+ width: b.width,
234
+ height: b.height,
235
+ transparent: true,
236
+ frame: false,
237
+ hasShadow: false,
238
+ alwaysOnTop: true,
239
+ skipTaskbar: true,
240
+ resizable: false,
241
+ focusable: false,
242
+ fullscreenable: false,
243
+ webPreferences: {
244
+ preload: path.join(__dirname, "preload.cjs"),
245
+ contextIsolation: true,
246
+ nodeIntegration: false,
247
+ },
248
+ });
249
+ fxWin.setAlwaysOnTop(true, "screen-saver", 1); // 高于桌宠窗口的 screen-saver
250
+ fxWin.setIgnoreMouseEvents(true); // 鼠标穿透,不拦任何点击
251
+ if (process.platform === "darwin") {
252
+ fxWin.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
253
+ }
254
+ fxWin.loadFile(path.join(__dirname, "renderer", "fx.html"));
255
+ fxWin.on("closed", () => vlog("fxWin closed"));
256
+ vlog("fx window created", JSON.stringify({ x: b.x, y: b.y, width: b.width, height: b.height }));
257
+ }
258
+
259
+ // ---- 工具:根据 webContents 找所属 agent ----
260
+ function agentBySender(sender) {
261
+ for (const [agentId, bw] of petWins) {
262
+ if (bw.webContents === sender) return agentId;
263
+ }
264
+ return MAIN_AGENT_ID;
265
+ }
266
+
267
+ function windowByAgent(agentId) {
268
+ return petWins.get(agentId) || null;
269
+ }
270
+
271
+ // ---- 特效坐标转发:桌宠窗口(全局 DIP) → 特效窗口(本地 CSS px) ----
272
+ // screenX/screenY 是相对主显示器原点的全局 DIP,减去 fxWin 左上角即得本地坐标。
273
+ function relayFx(channel, sx, sy) {
274
+ if (!fxWin || fxWin.isDestroyed()) return;
275
+ const [wx, wy] = fxWin.getPosition();
276
+ fxWin.webContents.send(channel, sx - wx, sy - wy);
277
+ }
278
+
279
+ ipcMain.on("pet:fx-down", (_e, sx, sy) => relayFx("fx:down", sx, sy));
280
+ ipcMain.on("pet:fx-move", (_e, sx, sy) => relayFx("fx:move", sx, sy));
281
+ ipcMain.on("pet:fx-up", () => {
282
+ if (fxWin && !fxWin.isDestroyed()) fxWin.webContents.send("fx:up");
283
+ });
284
+
285
+ // ---- 全局光标轮询(~60Hz,DIP 坐标系内运算,供渲染层瞳孔跟随 + 按住期间晃动检测补采样) ----
286
+ // 对每个桌宠窗口分别发送窗口本地坐标 + 全局坐标(gx/gy:renderer 按住期间用于晃动检测——
287
+ // 光标快速甩动划出窗口时 mousemove 断流,本轮询不断流,轻微出窗仍能采到晃动)
288
+ const CURSOR_POLL_MS = 16;
289
+ let cursorTimer = null;
290
+ const lastCursors = new Map();
291
+
292
+ function startCursorTracking() {
293
+ if (cursorTimer) return;
294
+ cursorTimer = setInterval(() => {
295
+ const p = screen.getCursorScreenPoint(); // 全局 DIP
296
+ for (const [agentId, bw] of petWins) {
297
+ if (bw.isDestroyed() || !bw.isVisible()) continue;
298
+ const [wx, wy] = bw.getPosition();
299
+ const x = p.x - wx;
300
+ const y = p.y - wy; // 窗口本地 CSS px(可在窗口外,注视效果所需)
301
+ const last = lastCursors.get(agentId);
302
+ if (Math.abs(x - (last?.x ?? NaN)) < 1 && Math.abs(y - (last?.y ?? NaN)) < 1) continue; // 静止节流
303
+ lastCursors.set(agentId, { x, y });
304
+ bw.webContents.send("pet:cursor", x, y, p.x, p.y);
305
+ }
306
+ }, CURSOR_POLL_MS);
307
+ }
308
+
309
+ // ---- 拖动 IPC(16ms 节流,按窗口独立) ----
310
+ const dragPendingMap = new Map();
311
+ const dragTimerMap = new Map();
312
+
313
+ ipcMain.on("pet:drag", (e, dx, dy) => {
314
+ const agentId = agentBySender(e.sender);
315
+ const bw = windowByAgent(agentId);
316
+ if (!bw) return;
317
+ const prev = dragPendingMap.get(agentId) || { x: bw.getPosition()[0], y: bw.getPosition()[1] };
318
+ const pending = { x: prev.x + dx, y: prev.y + dy };
319
+ dragPendingMap.set(agentId, pending);
320
+ if (!dragTimerMap.has(agentId)) {
321
+ dragTimerMap.set(agentId, setTimeout(() => {
322
+ dragTimerMap.delete(agentId);
323
+ const p = dragPendingMap.get(agentId);
324
+ if (p && windowByAgent(agentId)) {
325
+ windowByAgent(agentId).setPosition(Math.round(p.x), Math.round(p.y));
326
+ dragPendingMap.delete(agentId);
327
+ }
328
+ }, 16));
329
+ }
330
+ });
331
+
332
+ ipcMain.on("pet:dragend", (e) => {
333
+ const agentId = agentBySender(e.sender);
334
+ const bw = windowByAgent(agentId);
335
+ if (!bw) return;
336
+ const timer = dragTimerMap.get(agentId);
337
+ if (timer) {
338
+ clearTimeout(timer);
339
+ dragTimerMap.delete(agentId);
340
+ }
341
+ const pending = dragPendingMap.get(agentId);
342
+ if (pending) {
343
+ bw.setPosition(Math.round(pending.x), Math.round(pending.y));
344
+ dragPendingMap.delete(agentId);
345
+ }
346
+ const [x, y] = bw.getPosition();
347
+ savePosition(agentId, x, y);
348
+ send({ type: "moved", agent: agentId, x, y });
349
+ });
350
+
351
+ ipcMain.on("pet:shake", (e) => send({ type: "shake", agent: agentBySender(e.sender) }));
352
+ ipcMain.on("pet:dizzy", (e) => send({ type: "dizzy", agent: agentBySender(e.sender) }));
353
+
354
+ ipcMain.handle("pet:get-agent-config", (e) => {
355
+ const agentId = agentBySender(e.sender);
356
+ // isMain:renderer 据此让"仅主 Agent 响应"的交互(大幅晃动 dizzy)跳过子窗口
357
+ return { id: agentId, isMain: agentId === MAIN_AGENT_ID, ...AGENTS[agentId] };
358
+ });
359
+
360
+ // ---- stdin 协议 ----
361
+ let buffer = "";
362
+
363
+ function sendToAgent(agentId, channel, payload) {
364
+ const bw = windowByAgent(agentId);
365
+ if (!bw || bw.isDestroyed()) return false;
366
+ bw.webContents.send(channel, payload);
367
+ return true;
368
+ }
369
+
370
+ function handleMessage(msg) {
371
+ vlog("stdin msg", JSON.stringify(msg));
372
+ switch (msg.type) {
373
+ case "set_emotion": {
374
+ const id = msg.agent && AGENTS[msg.agent] ? msg.agent : MAIN_AGENT_ID;
375
+ if (AGENTS[id].emotions[msg.name]) {
376
+ if (!sendToAgent(id, "pet:emotion", msg.name)) send({ type: "error", message: `no window for ${id}` });
377
+ } else {
378
+ send({ type: "error", message: `unknown emotion: ${msg.name}` });
379
+ }
380
+ break;
381
+ }
382
+ case "reset":
383
+ // 所有角色窗口统一恢复默认待机
384
+ for (const [id] of petWins) sendToAgent(id, "pet:reset", null);
385
+ break;
386
+ case "text": {
387
+ const id = msg.agent && AGENTS[msg.agent] ? msg.agent : MAIN_AGENT_ID;
388
+ sendToAgent(id, "pet:text", { kind: msg.kind, data: msg.data });
389
+ break;
390
+ }
391
+ case "quit":
392
+ app.quit();
393
+ break;
394
+ }
395
+ }
396
+
397
+ process.stdin.on("data", (data) => {
398
+ buffer += data.toString();
399
+ const lines = buffer.split("\n");
400
+ buffer = lines.pop() || "";
401
+ for (const line of lines) {
402
+ const trimmed = line.trim();
403
+ if (!trimmed) continue;
404
+ try {
405
+ handleMessage(JSON.parse(trimmed));
406
+ } catch {
407
+ // 非 JSON 行,忽略
408
+ }
409
+ }
410
+ });
411
+
412
+ process.stdin.on("end", () => {
413
+ // Windows:stdin 会提前 EOF(GUI 子系统 + 父进程 stdio 链),直接 app.quit() 会把刚启动、
414
+ // 尚未 did-finish-load 的桌宠误杀(症状:code 0 静默退出、窗口闪一下不见)。
415
+ // 桌宠退出由父进程(src/pet.ts)的 quit 指令 + SIGTERM 兜底驱动,不依赖 stdin EOF。
416
+ // 其他平台:stdin EOF 可靠表示父进程退出,跟随退出防孤儿进程。
417
+ vlog("stdin END", process.platform === "win32" ? "(ignored on win32)" : "(quit)");
418
+ if (process.platform !== "win32") app.quit();
419
+ });
420
+
421
+ app.whenReady().then(() => {
422
+ if (VERBOSE) {
423
+ // GPU 功能状态:确认 webgl/webgl2 是 hardware/software/disabled(Windows 白屏排查关键)
424
+ try {
425
+ vlog("GPU feature status:", JSON.stringify(app.getGPUFeatureStatus()));
426
+ } catch (e) {
427
+ vlog("getGPUFeatureStatus failed:", e.message);
428
+ }
429
+ vlog("platform:", process.platform, "electron:", process.versions.electron, "chrome:", process.versions.chrome);
430
+ vlog("displays:", JSON.stringify(screen.getAllDisplays().map((d) => d.bounds)));
431
+ vlog("agents:", JSON.stringify(AGENT_IDS));
432
+ }
433
+ AGENT_IDS.forEach((id, i) => createPetWindow(id, i));
434
+ createFxWindow();
435
+ startCursorTracking();
436
+ });
437
+
438
+ app.on("window-all-closed", () => {
439
+ vlog("window-all-closed → app.quit()");
440
+ if (cursorTimer) {
441
+ clearInterval(cursorTimer);
442
+ cursorTimer = null;
443
+ }
444
+ app.quit();
445
+ });
446
+
447
+ // 退出链路诊断(verbose):定位"进程 code 0 退出但无 did-finish-load"的退出源
448
+ app.on("before-quit", () => vlog("before-quit"));
449
+ app.on("will-quit", () => vlog("will-quit"));
450
+
451
+ // ---- 崩溃日志:GPU/utility 子进程崩溃 + 主进程 JS 异常(配合 src/pet.ts 的 crash 处理定位问题) ----
452
+ app.on("child-process-gone", (_e, details) => {
453
+ send({ type: "crash", kind: details.type || "child", reason: details.reason, exitCode: details.exitCode });
454
+ vlog("child-process-gone", JSON.stringify(details));
455
+ });
456
+ process.on("uncaughtException", (err) => {
457
+ console.error("[pet:crash] uncaughtException:", err && err.stack ? err.stack : err);
458
+ process.exit(1); // 主进程状态已不可靠,退出让父进程按崩溃路径重启
459
+ });
460
+ process.on("unhandledRejection", (reason) => {
461
+ console.error("[pet:crash] unhandledRejection:", reason);
462
+ });
@@ -0,0 +1,21 @@
1
+ const { contextBridge, ipcRenderer } = require("electron");
2
+
3
+ contextBridge.exposeInMainWorld("petAPI", {
4
+ onEmotion: (cb) => ipcRenderer.on("pet:emotion", (_e, name) => cb(name)),
5
+ onReset: (cb) => ipcRenderer.on("pet:reset", () => cb()),
6
+ onText: (cb) => ipcRenderer.on("pet:text", (_e, p) => cb(p)),
7
+ getAgentConfig: () => ipcRenderer.invoke("pet:get-agent-config"),
8
+ drag: (dx, dy) => ipcRenderer.send("pet:drag", dx, dy),
9
+ dragEnd: () => ipcRenderer.send("pet:dragend"),
10
+ shake: () => ipcRenderer.send("pet:shake"),
11
+ dizzy: () => ipcRenderer.send("pet:dizzy"),
12
+ // gx/gy:全局屏幕坐标(按住期间 renderer 用于晃动检测补采样,出窗不断流)
13
+ onCursor: (cb) => ipcRenderer.on("pet:cursor", (_e, x, y, gx, gy) => cb(x, y, gx, gy)),
14
+ // 点击/拖尾特效:桌宠窗口发全局屏幕坐标 → 主进程 → 全屏特效窗口收
15
+ fxDown: (x, y) => ipcRenderer.send("pet:fx-down", x, y),
16
+ fxMove: (x, y) => ipcRenderer.send("pet:fx-move", x, y),
17
+ fxUp: () => ipcRenderer.send("pet:fx-up"),
18
+ onFxDown: (cb) => ipcRenderer.on("fx:down", (_e, x, y) => cb(x, y)),
19
+ onFxMove: (cb) => ipcRenderer.on("fx:move", (_e, x, y) => cb(x, y)),
20
+ onFxUp: (cb) => ipcRenderer.on("fx:up", () => cb()),
21
+ });
@@ -0,0 +1,30 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self' file: data: blob:; script-src 'self' file:; style-src 'self' 'unsafe-inline'; img-src 'self' file: data: blob:; font-src 'self' file:; connect-src 'self' file: data: blob:">
6
+ <style>
7
+ html, body {
8
+ margin: 0;
9
+ padding: 0;
10
+ width: 100%;
11
+ height: 100%;
12
+ background: transparent;
13
+ overflow: hidden;
14
+ }
15
+ /* 全屏特效画布:不拦截鼠标(manual 输入 + 主进程穿透),透明背景 */
16
+ #fx {
17
+ position: absolute;
18
+ inset: 0;
19
+ width: 100%;
20
+ height: 100%;
21
+ pointer-events: none;
22
+ }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <div id="fx"></div>
27
+ <script src="../../node_modules/ba-click-fx/dist/ba-click-fx.iife.js"></script>
28
+ <script src="fx.js"></script>
29
+ </body>
30
+ </html>
@@ -0,0 +1,41 @@
1
+ // ARONA 全屏特效渲染层:ba-click-fx(manual 输入),覆盖整个屏幕画点击特效 + 拖尾。
2
+ // 桌宠窗口只负责检测「窗口内长按拖动」,主进程把全局屏幕坐标转发到这里。
3
+ // 透明窗口关键:outputCompositing 'browser-overlay' + hostCompositingSurface 'transparent-window'。
4
+ let fx = null;
5
+
6
+ function initFx() {
7
+ if (typeof BAClickFX === "undefined") return;
8
+ try {
9
+ fx = new BAClickFX.BAClickFX({
10
+ target: "#fx",
11
+ inputSource: "manual",
12
+ effectBackend: "webgl2",
13
+ bloomBackend: "webgl2",
14
+ outputCompositing: "browser-overlay",
15
+ overlayAlphaPolicy: "coverage",
16
+ overlayColorCompensation: "none",
17
+ overlayAlphaLimit: 250 / 255,
18
+ hostCompositing: "source-over",
19
+ hostCompositingSurface: "transparent-window",
20
+ lightBackgroundContrastAlpha: 0,
21
+ clickEnabled: true,
22
+ trailEnabled: true,
23
+ trailAlways: false,
24
+ });
25
+ } catch {
26
+ fx = null;
27
+ }
28
+ }
29
+
30
+ // 主进程已把全局屏幕坐标换算成本窗口(全屏覆盖)本地 CSS px,这里直接喂给 fx。
31
+ window.petAPI.onFxDown((x, y) => {
32
+ if (fx) fx.pointerDown({ x, y, pointerId: 1, pointerType: "mouse" });
33
+ });
34
+ window.petAPI.onFxMove((x, y) => {
35
+ if (fx) fx.pointerMove({ x, y, pointerId: 1, pointerType: "mouse" });
36
+ });
37
+ window.petAPI.onFxUp(() => {
38
+ if (fx) fx.pointerUp(1);
39
+ });
40
+
41
+ initFx();
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self' file: data: blob:; script-src 'self' file:; style-src 'self' 'unsafe-inline'; img-src 'self' file: data: blob:; font-src 'self' file:; connect-src 'self' file: data: blob:">
6
+ <title>Spine 表情目视图库</title>
7
+ <style>
8
+ html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: transparent; }
9
+ #spine { position: absolute; inset: 0; width: 100%; height: 100%; }
10
+ </style>
11
+ </head>
12
+ <body>
13
+ <canvas id="spine"></canvas>
14
+ <script src="../vendor/spine/spine-webgl.js"></script>
15
+ <script src="spine_layer.js"></script>
16
+ <script src="gallery.js"></script>
17
+ </body>
18
+ </html>
@@ -0,0 +1,31 @@
1
+ // 表情目视图库(gallery.js):加载当前 Agent 骨架,逐帧切换数字情绪预设,供人工目视选号。
2
+ // 由 pet/tools/gallery_capture.cjs 驱动截图。
3
+ // 与 spinetest 不同:这里不调度眨眼/注视/拖动,保证截图干净。
4
+ // 注意:截图内不带任何水印(旧 #label 水印已删——DOM 文本与 WebGL 画布是两条合成路径,
5
+ // 曾出现水印滞后于骨架的过期纹理帧,编号与脸错位误导选号;编号一律以 gallery.html 文字为准)。
6
+ const canvas = document.getElementById("spine");
7
+
8
+ async function init() {
9
+ await window.SpineLayer.init(canvas);
10
+ // 冻结 track0 Idle_01 相位:不同截图/不同批次身体姿势完全一致,可像素级对比(编号↔脸一一对应)
11
+ window.SpineLayer.pinBody(1.0);
12
+ window.__gallery = {
13
+ /**
14
+ * 切换到指定预设(数字动画名)。
15
+ * 数字预设 3.333s 非循环、残留末帧持有 → 外部按 settledMs 节奏等动画落定后再截图。
16
+ */
17
+ show(name) {
18
+ window.SpineLayer.setEmotionPreset(String(name), false, true);
19
+ },
20
+ /** 情绪确认模式:按情绪名渲染其映射预设 */
21
+ showEmotion(_emotionName, preset) {
22
+ window.SpineLayer.setEmotionPreset(String(preset), false, true);
23
+ },
24
+ /** 数字预设动画时长 + 余量(等落定到末帧——settled face 才是表情本体,见 CLAUDE.md B5) */
25
+ settledMs: 3600,
26
+ };
27
+ }
28
+
29
+ init().catch((err) => {
30
+ console.error("gallery init failed:", err);
31
+ });
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self' file: data: blob:; script-src 'self' file:; style-src 'self' 'unsafe-inline'; img-src 'self' file: data: blob:; font-src 'self' file:; connect-src 'self' file: data: blob:">
6
+ <link rel="stylesheet" href="style.css">
7
+ </head>
8
+ <body>
9
+ <div id="stage">
10
+ <canvas id="spine"></canvas>
11
+ <div id="pet-bubble" class="hidden"></div>
12
+ </div>
13
+ <script src="../vendor/spine/spine-webgl.js"></script>
14
+ <script src="../vendor/spine/spine-canvas.js"></script>
15
+ <script src="spine_layer.js"></script>
16
+ <script src="renderer.js"></script>
17
+ </body>
18
+ </html>