fix(backthink): 她主动找你终于看得见 —— 主动消息落主会话 + 前端消费 + 非前台浏览器通知
修主人报的硬故障「后台思考的主动推送一个都没有推出来」(根因两条,缺一不可): - 后端 gateway/proactive.go 只广播 proactive_message 帧、**从不写会话历史** ⇒ 刷新补不回来 - 前端**零消费点** ⇒ 帧被静默丢弃(既无气泡也无通知) 后端:新增 proactive_persist.go(**先落库、再发帧**;web/assistant/mode=chat;未接线时空操作且不谎称已落库) 载荷新增 id/createdAt(落库行权威 id);PushResult.Detail 三态如实分开报告「投递到几个连接」与「是否落库」 前端:handlers/proactive.ts(气泡**恒落** + 按落库 id 去重 + **非前台才补通知** + 拒绝授权/畸形载荷可见降级) + lib/notify/proactive.ts(纯逻辑 isBackground/decideProactiveNotice/有界去重表) + handlers/task.ts(另三个被丢的帧 task_done/automation_result/subagent_result) + ws/resync.ts + useWsResync.ts(**重连重跑本页引导读取**,首次连接不跑) 双份核查(带三层证据):现状不双份 —— 路由分叉「一次 Push 至多一个 sink 成功」,且 IM 路由本来就把主动消息镜像回主会话恰好一次 契约 v0.72;变异实测:只发帧不落库 / 写成 user 角色 / 去重失效 / 前台也弹 / IM 成功再打活跃 ⇒ 全部打红
This commit is contained in:
1 parent
1bf85a955b
commit
84c124f5a9
18 files changed
+1545
-46
No files matched your search
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* 主动消息的**浏览器通知**判定(切片 P0「她主动找你」)。
|
||||
*
|
||||
* 本模块是**纯逻辑**:不碰 DOM、不碰 store、不读全局变量,因此可以在没有 jsdom 的
|
||||
* 纯 node 下逐条钉住主人裁定的口径 ——
|
||||
*
|
||||
* 1. **气泡恒落,通知可省**:气泡(消息 store)与前后台无关,永远落;浏览器通知
|
||||
* 只是「人不在看的时候补一发」,是提醒、不是记录。
|
||||
* 2. **只在不在前台时补一发**:判定 = Page Visibility **且** 焦点。只判可见性会漏掉
|
||||
* 「窗口被别的窗口盖住、visibilityState 仍是 visible」这一半 —— 那种时候主人
|
||||
* 根本看不见,正该提醒。
|
||||
* 3. **同一条只打扰一次**:去重表按消息 id 记(含「同一条既走实时帧、又被 IM 镜像
|
||||
* 回主会话」这种同时到达的情形)。气泡**不参与**提醒去重 —— 它是记录,去重由
|
||||
* 消息 store 按 id 负责。
|
||||
* 4. **拒绝授权 ⇒ 如实降级**:没授权就只留气泡,并让调用方留一句可见说明;
|
||||
* **绝不静默丢弃**,也绝不当成故障崩掉。
|
||||
*/
|
||||
|
||||
/** 浏览器通知权限(`Notification.permission` 的三态;运行时不支持该 API 时由调用方
|
||||
* 归到 `"denied"` —— 拿不到通知就是没授权,绝不假装已授权)。 */
|
||||
export type NotifyPermission = "granted" | "denied" | "default";
|
||||
|
||||
/** 一次提醒的判定结果。 */
|
||||
export type NoticeDecision =
|
||||
/** 不在前台且已授权 ⇒ 补一发系统通知。 */
|
||||
| "show"
|
||||
/** 同一条已经提醒过(或已在前台露过面)⇒ 不打扰第二次。 */
|
||||
| "duplicate"
|
||||
/** 页面可见且聚焦 ⇒ 按裁定不弹(气泡已经落了)。 */
|
||||
| "foreground"
|
||||
/** 通知权限未授权/被拒 ⇒ 如实降级为只留气泡。 */
|
||||
| "not-granted";
|
||||
|
||||
/** 「不在前台」的判定:**可见且聚焦**才算前台。`visibility` 用
|
||||
* `document.visibilityState` 的原值("visible" | "hidden" | …),未知取值一律按
|
||||
* 「不在前台」处理 —— 提醒多发一次不可怕,人错过了才可怕。 */
|
||||
export function isBackground(visibility: string, focused: boolean): boolean {
|
||||
return !(visibility === "visible" && focused);
|
||||
}
|
||||
|
||||
/** 决定这一刻要不要为这条主动消息补一发通知。纯函数:同样的输入永远同样的结论。 */
|
||||
export function decideProactiveNotice(input: {
|
||||
/** 同一条消息此前是否已经提醒过(调用方查去重表)。 */
|
||||
seen: boolean;
|
||||
/** `document.visibilityState`。 */
|
||||
visibility: string;
|
||||
/** `document.hasFocus()`。 */
|
||||
focused: boolean;
|
||||
/** 当前通知权限。 */
|
||||
permission: NotifyPermission;
|
||||
}): NoticeDecision {
|
||||
if (input.seen) return "duplicate";
|
||||
if (!isBackground(input.visibility, input.focused)) return "foreground";
|
||||
if (input.permission !== "granted") return "not-granted";
|
||||
return "show";
|
||||
}
|
||||
|
||||
/** 有界的「已提醒」表。它是**本页存活期间的打扰记录**,不是消息台账:超过上限就
|
||||
* 淘汰最旧的一条(一条很老的重复消息至多多提醒一次,绝不会让表无限长)。 */
|
||||
export function createSeenNotices(limit: number): {
|
||||
has(id: string): boolean;
|
||||
add(id: string): void;
|
||||
size(): number;
|
||||
} {
|
||||
const order: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
return {
|
||||
has(id) {
|
||||
return seen.has(id);
|
||||
},
|
||||
add(id) {
|
||||
if (!id || seen.has(id)) return;
|
||||
seen.add(id);
|
||||
order.push(id);
|
||||
while (order.length > limit) {
|
||||
const oldest = order.shift();
|
||||
if (oldest !== undefined) seen.delete(oldest);
|
||||
}
|
||||
},
|
||||
size() {
|
||||
return seen.size;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user