@@ -12,7 +12,7 @@ import { PROACTIVE_RULES } from '../lib/proactiveRules';
1212import { pickTemplate } from '../lib/messageTemplates' ;
1313import { getStatsCacheText , refreshStatsCache } from '../lib/progressStats' ;
1414import { useAssistantStore } from '../store/useAssistantStore' ;
15- import { MAX_TRIGGERS_PER_HOUR , MAX_CONSECUTIVE_IGNORES , PROGRESS_NARRATIVE_STORAGE_KEY } from '../constants' ;
15+ import { MAX_TRIGGERS_PER_HOUR , MAX_CONSECUTIVE_IGNORES , PROGRESS_NARRATIVE_STORAGE_KEY , PROGRESS_NARRATIVE_DELAY_MS } from '../constants' ;
1616import { getAuthToken } from '@/lib/ai/aiPluginProvider' ;
1717import type { TriggerContext , ProactiveRule } from '../types' ;
1818
@@ -21,6 +21,8 @@ const lastTriggeredMap = new Map<string, number>();
2121let hourlyCount = 0 ;
2222let hourlyResetAt = Date . now ( ) + 60 * 60 * 1000 ;
2323let consecutiveIgnores = 0 ;
24+ /** 最近一次气泡被关闭的时刻(延迟展示类规则用它判断用户是否已表达不想被打扰) */
25+ let lastDismissedAt = 0 ;
2426
2527/** A3 {stats} 占位符缓存未就绪时的通用正向兜底文案 */
2628const FALLBACK_STATS_TEXT = '你的每一点坚持都算数' ;
@@ -47,8 +49,12 @@ async function enhanceNarrative(statsText: string, ruleId: string): Promise<void
4749 authToken : authToken ?? undefined ,
4850 } ) as { narrative ?: string ; status ?: string } ;
4951 if ( res ?. narrative && res . status === 'success' ) {
50- // getState 读取避免闭包捕获旧的 showBubble 引用
51- useAssistantStore . getState ( ) . showBubble ( res . narrative , ruleId ) ;
52+ // 竞态守卫:AI 返回时用户可能已展开面板或关闭气泡——
53+ // 仅当当前仍在展示本规则的气泡时才覆盖,不抢占用户当前状态
54+ const s = useAssistantStore . getState ( ) ;
55+ if ( s . panelState === 'bubble' && s . bubbleTriggerId === ruleId ) {
56+ s . showBubble ( res . narrative , ruleId ) ;
57+ }
5258 }
5359 } catch { /* 静默降级:保留离线模板拼装的统计气泡 */ }
5460}
@@ -108,19 +114,31 @@ export function useProactiveEngine(): void {
108114 // 缓存未就绪(非 Electron/采集失败)时用通用正向文案兜底
109115 const statsText = getStatsCacheText ( ) ?? FALLBACK_STATS_TEXT ;
110116 const message = pickTemplate ( rule . id ) . replace ( '{stats}' , statsText ) ;
111- showBubble ( message , rule . id ) ;
117+ // progress-narrative 走下方延迟展示分支,其余规则立即展示
118+ if ( rule . id !== 'progress-narrative' ) {
119+ showBubble ( message , rule . id ) ;
120+ }
112121
113- // A3 微进展叙述:记录节奏 + 异步 AI 增强(成功后覆盖气泡)
122+ // A3 微进展叙述:同步记录节奏;气泡延迟展示——app:startup 同批的
123+ // 问候语先曝光,避免叙述瞬间覆盖问候;延迟期间用户已展开面板
124+ // 或关闭过气泡则放弃展示(奖赏回来 ≠ 抢回注意力)
114125 if ( rule . id === 'progress-narrative' ) {
115126 markNarrativeShown ( ) ;
116- void enhanceNarrative ( statsText , rule . id ) ;
127+ const scheduleAt = Date . now ( ) ;
128+ window . setTimeout ( ( ) => {
129+ const s = useAssistantStore . getState ( ) ;
130+ if ( s . panelState === 'expanded' || lastDismissedAt > scheduleAt ) return ;
131+ s . showBubble ( message , rule . id ) ;
132+ void enhanceNarrative ( statsText , rule . id ) ;
133+ } , PROGRESS_NARRATIVE_DELAY_MS ) ;
117134 }
118135 }
119136}
120137
121138/** 外部调用:标记用户忽略了气泡 */
122139export function reportBubbleDismissed ( ) : void {
123140 consecutiveIgnores ++ ;
141+ lastDismissedAt = Date . now ( ) ;
124142}
125143
126144/** 外部调用:标记用户回应了气泡(奖赏回来——重置退让计数) */
0 commit comments