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 条)。
127 lines
5.7 KiB
JavaScript
127 lines
5.7 KiB
JavaScript
// 加载动画 + 系统通知的判据。
|
||
//
|
||
// ## 要判的是什么
|
||
//
|
||
// 1) 加载动画:旧实现是 border + rotate 的 spinner(.loading /
|
||
// .loading-spinner / .live-spinner 三处),在消息气泡里刺眼,
|
||
// 且"持续旋转"语义不对 —— 旋转暗示在加载某个确定的东西,
|
||
// 而 agent 思考本身没有进度。改为 PiDeck 用的「等距三点 +
|
||
// 透明度递减 + 轻微上浮」。
|
||
//
|
||
// 2) 系统通知:**此前完全不存在**(主进程无 Notification、preload 无接口、
|
||
// 渲染层只有页面内 toast)。这是新增能力,容易被后续改动静默删掉。
|
||
//
|
||
// 运行:node loading-notify.test.mjs
|
||
|
||
import { readFileSync } from "node:fs";
|
||
import { fileURLToPath } from "node:url";
|
||
import { dirname, join } from "node:path";
|
||
|
||
const here = dirname(fileURLToPath(import.meta.url));
|
||
const css = readFileSync(join(here, "renderer", "style.css"), "utf8");
|
||
const app = readFileSync(join(here, "renderer", "app.js"), "utf8");
|
||
const main = readFileSync(join(here, "main.js"), "utf8");
|
||
const preload = readFileSync(join(here, "preload.js"), "utf8");
|
||
|
||
let failures = 0;
|
||
const check = (name, ok, detail) => {
|
||
if (ok) console.log(` ✓ ${name}`);
|
||
else {
|
||
failures++;
|
||
console.log(` ✗ ${name}${detail ? " — " + detail : ""}`);
|
||
}
|
||
};
|
||
|
||
// ── 1) 加载动画:三点而非旋转 ────────────────────────────────────────
|
||
check("定义 haDots 关键帧", /@keyframes haDots/.test(css), "缺 @keyframes haDots");
|
||
check("定义 haDotFloat 关键帧", /@keyframes haDotFloat/.test(css));
|
||
check("定义 haSweep 关键帧", /@keyframes haSweep/.test(css));
|
||
|
||
// 三点的错峰 delay 是"追逐感"的来源,缺了就变成同步闪烁
|
||
check(
|
||
"三点有错峰 delay(nth-child 2/3)",
|
||
/\.ha-dots\s*>?\s*i:nth-child\(2\)/.test(css) ||
|
||
/\.ha-dots > i:nth-child\(2\)/.test(css),
|
||
"缺 nth-child delay ⇒ 三点同步闪,不像追逐",
|
||
);
|
||
|
||
// 旧类名必须仍然可用(还有调用点用它们)
|
||
check(
|
||
".loading 别名仍指向新动画",
|
||
/\.loading,\s*\.loading-spinner\s*{[^}]*animation:\s*none/.test(css),
|
||
"别名没解除旋转 ⇒ 旧调用点仍是转圈",
|
||
);
|
||
check(
|
||
"live-spinner 已是 flex 三点容器",
|
||
/\.msg-bubble \.live-spinner\s*{[^}]*display:\s*inline-flex/.test(css),
|
||
"live-spinner 仍是 inline-block 圆环",
|
||
);
|
||
|
||
// 旧的旋转实现不应再被这三类使用
|
||
const usesOldSpin = /\.loading[^{]*\{[^}]*border-top-color/.test(css);
|
||
check("没有残留的 border 旋转 spinner", !usesOldSpin, "仍有 border-top-color 旋转实现");
|
||
|
||
// ── 2) 调用点都换成了三点结构 ──────────────────────────────────────
|
||
const dotMarkup = (app.match(/class="ha-dots[^"]*"><i><\/i><i><\/i><i><\/i>/g) || []).length;
|
||
check(
|
||
"★ 面板/列表加载已换成三点结构",
|
||
dotMarkup >= 4,
|
||
`只找到 ${dotMarkup} 处 <i>x3 结构(预期 >=4:星图 2 + 列表 3 + 其他)`,
|
||
);
|
||
check(
|
||
"★ 消息内 live-spinner 带三个点",
|
||
/class="live-spinner"><i><\/i><i><\/i><i><\/i>/.test(app),
|
||
"live-spinner 仍是空的(会看不到任何点)",
|
||
);
|
||
check(
|
||
"旧的空 <div class=\"loading\"></div> 已清理",
|
||
!/class="loading"><\/div>/.test(app),
|
||
"仍有裸 <div class=loading>(无点)",
|
||
);
|
||
|
||
// ── 3) 系统通知:主进程 ────────────────────────────────────────────
|
||
check("main 引入 Notification", /new Notification\(/.test(main), "未调用原生 Notification");
|
||
check("有 notify:show IPC", /ipcMain\.handle\("notify:show"/.test(main));
|
||
check("有 notify:supported IPC", /ipcMain\.handle\("notify:supported"/.test(main));
|
||
check(
|
||
"★ 前台聚焦时不打扰用户",
|
||
/isFocused\(\)/.test(main) && /skipped:\s*"focused"/.test(main),
|
||
"没有「窗口在前台就不弹」判定 ⇒ 正看着界面时会被弹窗打断",
|
||
);
|
||
check(
|
||
"★ 点击通知会回传并唤起窗口",
|
||
/notify:clicked/.test(main) && /mainWindow\.show\(\)/.test(main),
|
||
"点击通知后没有 show()+回传,点了等于没点",
|
||
);
|
||
check(
|
||
"正文超长截断(避免通知被撑爆)",
|
||
/body\.length\s*>\s*\d+/.test(main),
|
||
"未截断正文",
|
||
);
|
||
|
||
// ── 4) preload 桥 ──────────────────────────────────────────────────
|
||
check("preload 暴露 notify.show", /notify:\s*{[\s\S]{0,200}show:/.test(preload));
|
||
check("preload 暴露 notify.supported", /supported:\s*\(\)\s*=>\s*ipcRenderer\.invoke\("notify:supported"/.test(preload));
|
||
check("preload 暴露 notify.onClicked", /onClicked:/.test(preload));
|
||
|
||
// ── 5) 渲染层 ──────────────────────────────────────────────────────
|
||
check("渲染层有 notifyTurnOnce 去重", /function notifyTurnOnce\(/.test(app), "缺去重 ⇒ 一轮会弹多次");
|
||
check(
|
||
"去重基于 turnSig",
|
||
/turnSig/.test(app),
|
||
"去重没按轮次签名",
|
||
);
|
||
check(
|
||
"★ 通知挂在 SSE agent_output 上(覆盖旁观路径)",
|
||
/notifyTurnOnce\(/.test(app) && /starmapPulse\("output"[\s\S]{0,400}notifyTurnOnce/.test(app),
|
||
"没挂在 SSE 收尾处 ⇒ 旁观其他渠道的回复时不会通知",
|
||
);
|
||
check("注册了通知点击跳转", /onNotifyClicked/.test(app) && /onClicked\(onNotifyClicked\)/.test(app));
|
||
check("启动时探测通知可用性", /notify\.supported\(\)/.test(app), "未探测 Notification.isSupported");
|
||
|
||
console.log("");
|
||
if (failures > 0) {
|
||
console.log(`全部失败:${failures} 条`);
|
||
process.exit(1);
|
||
}
|
||
console.log("全部通过"); |