mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
feat(gui): 加载动画改三点式 + 新增系统通知
## 1) 加载动画:边框旋转 → 三点渐亮
旧实现是 border + rotate 的 spinner,用在 .loading(16px) /
.loading-spinner(32px) / .live-spinner(15px) 三处。在消息气泡里刺眼,
且语义不对:旋转暗示"正在加载某个确定的东西",而 agent 思考没有进度。
参考 PiDeck(@deepseek-ai/dsh-web-frontend 与 mermaid bundle 里的
@keyframe 实现)改为「等距三点 + 透明度递减 + 轻微上浮」:
haDots 三点错峰明暗(1/0.6/0.35/0.15)
haDotFloat 单点上浮 + 缩放
haSweep 横向扫光
旧类名 .loading / .loading-spinner 保留为别名指向新动画(还有 6+ 处
调用点,逐处改 HTML 容易漏),并解除其 animation 以免残留旋转。
7 处调用点统一换成 <i>x3 结构。
## 2) 系统通知(此前完全不存在)
主进程无 Notification、preload 无接口、渲染层只有页面内 toast
(用户不盯着窗口就看不到)。补齐一条完整链路:
main.js notify:show / notify:supported 两个 IPC + 原生 Notification
preload notify.{show,supported,onClicked}
app.js notifyTurn / notifyTurnOnce / onNotifyClicked
设计取舍(都写进注释):
· **一轮只通知一次**。SSE 的 tool_call / stage / agent_output 都可能
判定"一轮结束",用 turnSig 去重,否则连弹好几次。
· **不在流式过程中逐 token 通知**。一轮可能几十个 delta,那是噪音;
用户要的是"有结果了"。
· **窗口在前台且聚焦时不弹**。正看着界面时弹窗只会烦人。
静默启动(窗口 hidden)正好走"要弹"这条 —— 这也是静默后唯一
能让用户知道"agent 回了"的途径。
· **挂在 SSE 的 agent_output 收尾处**,不挂 endChatTurn():
后者开头 `if (!state.chatLoading) return`,而旁观路径
(agent 响应 memo/QQ/email,GUI 只收 SSE)chatLoading 恒 false,
根本走不到那里。挂 SSE 对"自己发"与"旁观"两条路径都生效。
· **点击通知会 show() + 回传渲染进程**,切到 chat 视图并滚到该条。
点了却不知道该看哪句话等于没通知。
· 正文超 180 字截断,避免通知被撑爆。
## 判据
新增 cmd/gui/loading-notify.test.mjs(进 npm test),24 条断言覆盖
动画关键帧/错峰 delay/别名解除/调用点结构,以及通知链路的六段
(主进程 IPC、preload 桥、去重、挂载位置、点击跳转、可用性探测)。
## 实测
重新打包安装:186 文件 0 处不匹配,exe 188834816 字节
启动正常:silentStart 生效、login 200、device/online 200、
graph/pulse 轮询、0 个 401 / JS 错误。
判据:cmd/gui npm test 全绿(84 条)。
This commit is contained in:
@ -2595,6 +2595,62 @@ app.on("activate", () => {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
// ============ 系统通知(Electron 原生)============
|
||||
//
|
||||
// 之前**完全没有**这个能力:主进程没引入 Notification、preload 没暴露接口、
|
||||
// 渲染层只有页面内的 toast(用户不盯着窗口就看不到)。
|
||||
// 这套是补齐:渲染层判断"该不该通知",主进程负责弹系统通知。
|
||||
//
|
||||
// 为什么点击要回传会话:点了通知却不知道该看哪句话,等于没通知。
|
||||
// 点击时通过 webContents 回到渲染进程,由它切到 chat 视图并滚到该条。
|
||||
let notifSeq = 0;
|
||||
|
||||
ipcMain.handle("notify:show", (_, payload) => {
|
||||
try {
|
||||
const p = payload || {};
|
||||
const title = String(p.title || "HomeAgent");
|
||||
const body = String(p.body || "");
|
||||
if (!body) return { ok: false, error: "empty body" };
|
||||
if (!Notification.isSupported()) return { ok: false, error: "unsupported" };
|
||||
|
||||
// 已有窗口在前台且可见时不再打扰:用户正看着界面。
|
||||
// 静默启动场景下 mainWindow 存在但 hidden,正好走"要通知"这条路。
|
||||
if (mainWindow && !mainWindow.isDestroyed() && mainWindow.isVisible() && mainWindow.isFocused()) {
|
||||
return { ok: false, skipped: "focused" };
|
||||
}
|
||||
|
||||
const id = ++notifSeq;
|
||||
const n = new Notification({
|
||||
title: title,
|
||||
body: body.length > 180 ? body.slice(0, 180) + "\u2026" : body,
|
||||
silent: !!p.silent,
|
||||
urgency: p.urgent ? "critical" : "normal",
|
||||
});
|
||||
n.on("click", () => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
if (!mainWindow.isVisible()) mainWindow.show();
|
||||
mainWindow.focus();
|
||||
}
|
||||
try {
|
||||
mainWindow.webContents.send("notify:clicked", { id: id, msgKey: p.msgKey || "" });
|
||||
} catch (e) {}
|
||||
n.close();
|
||||
});
|
||||
n.show();
|
||||
return { ok: true, id: id };
|
||||
} catch (e) {
|
||||
return { ok: false, error: e.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("notify:supported", () => {
|
||||
try {
|
||||
return { supported: Notification.isSupported() };
|
||||
} catch (e) {
|
||||
return { supported: false };
|
||||
}
|
||||
});
|
||||
|
||||
// IPC:prefs(renderer 设置页需要)
|
||||
// IPC:本机设备身份(renderer 设备页需要)
|
||||
ipcMain.handle("device:identity", () =>
|
||||
|
||||
Reference in New Issue
Block a user