mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
## 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 条)。
62 lines
2.1 KiB
JavaScript
62 lines
2.1 KiB
JavaScript
const { contextBridge, ipcRenderer } = require("electron");
|
|
|
|
contextBridge.exposeInMainWorld("homeagent", {
|
|
win: {
|
|
minimize: () => ipcRenderer.invoke("window:minimize"),
|
|
toggleMaximize: () => ipcRenderer.invoke("window:toggleMaximize"),
|
|
close: () => ipcRenderer.invoke("window:close"),
|
|
},
|
|
connections: {
|
|
list: () => ipcRenderer.invoke("connections:list"),
|
|
add: (conn) => ipcRenderer.invoke("connections:add", conn),
|
|
update: (id, updates) =>
|
|
ipcRenderer.invoke("connections:update", { id, updates }),
|
|
delete: (id) => ipcRenderer.invoke("connections:delete", id),
|
|
setCurrent: (id) => ipcRenderer.invoke("connections:setCurrent", id),
|
|
},
|
|
cli: {
|
|
request: (socketPath, apiKey, line) =>
|
|
ipcRenderer.invoke("cli:request", { socketPath, apiKey, line }),
|
|
},
|
|
webui: {
|
|
setAuth: (url, cookie, headers, username, password) =>
|
|
ipcRenderer.invoke("webui:setAuth", {
|
|
url,
|
|
cookie,
|
|
headers,
|
|
username,
|
|
password,
|
|
}),
|
|
openLogin: (url, username, password) =>
|
|
ipcRenderer.invoke("webui:openLogin", { url, username, password }),
|
|
onLoginResult: (cb) =>
|
|
ipcRenderer.on("webui:login-result", (_e, d) => cb(d)),
|
|
},
|
|
cacheBg: (src) => ipcRenderer.invoke("bg:cache", { src }),
|
|
log: (m) => ipcRenderer.invoke("log:r", m),
|
|
notify: {
|
|
show: (payload) => ipcRenderer.invoke("notify:show", payload),
|
|
supported: () => ipcRenderer.invoke("notify:supported"),
|
|
onClicked: (cb) => ipcRenderer.on("notify:clicked", (_e, d) => cb(d)),
|
|
},
|
|
prefs: {
|
|
get: () => ipcRenderer.invoke("prefs:get"),
|
|
set: (p) => ipcRenderer.invoke("prefs:set", p),
|
|
},
|
|
device: {
|
|
identity: () => ipcRenderer.invoke("device:identity"),
|
|
},
|
|
deviceBridge: {
|
|
get: () => ipcRenderer.invoke("device-bridge:get"),
|
|
set: (cfg) => ipcRenderer.invoke("device-bridge:set", cfg),
|
|
setAuthorized: (auth) =>
|
|
ipcRenderer.invoke("device-bridge:setAuthorized", auth),
|
|
},
|
|
displays: {
|
|
list: () => ipcRenderer.invoke("displays:list"),
|
|
},
|
|
audio: {
|
|
list: () => ipcRenderer.invoke("audio:list"),
|
|
},
|
|
});
|