Skip to main content

aios_core/
context_builder.rs

1//! 上下文构建器 — 将脱敏事件按时间窗口聚合
2//!
3//! 将 `SanitizedEvent` 序列聚合成 `StructuredContext`,
4//! 这是发送给 Cloud LLM 前的最后一步。
5
6use std::collections::HashMap;
7
8use aios_spec::{
9    AppTransition, ContextSummary, ExtensionCategory, SanitizedEvent, SanitizedEventType,
10    SemanticHint, SourceTier, StructuredContext, SystemStatusSnapshot,
11};
12use uuid::Uuid;
13
14/// 窗口聚合器
15///
16/// 按时间窗口收集 SanitizedEvent, 在窗口关闭时构建 StructuredContext。
17#[derive(Debug)]
18pub struct WindowAggregator {
19    /// 当前窗口的事件缓冲区
20    buffer: Vec<SanitizedEvent>,
21    /// 窗口时长 (秒)
22    window_secs: u64,
23    /// 窗口起始时间 (epoch ms)
24    window_start_ms: i64,
25}
26
27impl WindowAggregator {
28    /// 创建新的窗口聚合器
29    ///
30    /// `window_secs` 为窗口长度, 默认 10 秒。
31    /// `now_ms` 为当前 epoch 毫秒时间戳。
32    pub fn new(window_secs: u64, now_ms: i64) -> Self {
33        Self {
34            buffer: Vec::new(),
35            window_secs,
36            window_start_ms: now_ms,
37        }
38    }
39
40    /// 向当前窗口追加一个脱敏事件
41    pub fn push(&mut self, event: SanitizedEvent) {
42        self.buffer.push(event);
43    }
44
45    /// 返回当前窗口内的已收集事件数
46    pub fn len(&self) -> usize {
47        self.buffer.len()
48    }
49
50    /// 返回 true 表示窗口为空
51    pub fn is_empty(&self) -> bool {
52        self.buffer.is_empty()
53    }
54
55    /// 窗口是否已到期
56    pub fn is_expired(&self, now_ms: i64) -> bool {
57        let elapsed_ms = now_ms.saturating_sub(self.window_start_ms);
58        elapsed_ms >= (self.window_secs * 1000) as i64
59    }
60
61    /// 关闭当前窗口, 构建 StructuredContext, 并重置缓冲区。
62    ///
63    /// `now_ms` 为窗口结束时间 (epoch ms)。
64    /// 返回 Some(StructuredContext) 如果窗口非空, 否则返回 None。
65    pub fn close(&mut self, now_ms: i64) -> Option<StructuredContext> {
66        if self.buffer.is_empty() {
67            self.window_start_ms = now_ms;
68            return None;
69        }
70
71        let events = std::mem::take(&mut self.buffer);
72        let summary = build_summary(&events);
73        let context = StructuredContext {
74            window_id: new_id(),
75            window_start_ms: self.window_start_ms,
76            window_end_ms: now_ms,
77            duration_secs: ((now_ms - self.window_start_ms).max(0) / 1000) as u32,
78            events,
79            summary,
80        };
81
82        self.window_start_ms = now_ms;
83
84        Some(context)
85    }
86}
87
88/// 从事件序列构建 ContextSummary
89fn build_summary(events: &[SanitizedEvent]) -> ContextSummary {
90    let mut foreground_apps: Vec<String> = Vec::new();
91    let mut notified_apps: Vec<String> = Vec::new();
92    let mut all_semantic_hints: Vec<SemanticHint> = Vec::new();
93    let mut file_activity_counts: HashMap<ExtensionCategory, u32> = HashMap::new();
94    let mut latest_system_status: Option<SystemStatusSnapshot> = None;
95    let mut source_tier = SourceTier::PublicApi;
96
97    for event in events {
98        if event.source_tier == SourceTier::Daemon {
99            source_tier = SourceTier::Daemon;
100        }
101
102        match &event.event_type {
103            SanitizedEventType::AppTransition {
104                package_name,
105                transition: AppTransition::Foreground,
106                ..
107            } if !foreground_apps.contains(package_name) => {
108                foreground_apps.push(package_name.clone());
109            },
110            SanitizedEventType::ProcessResource {
111                package_name: Some(pkg),
112                ..
113            } if !foreground_apps.contains(pkg) => {
114                foreground_apps.push(pkg.clone());
115            },
116            SanitizedEventType::Notification {
117                source_package,
118                semantic_hints,
119                ..
120            } => {
121                if !notified_apps.contains(source_package) {
122                    notified_apps.push(source_package.clone());
123                }
124                for hint in semantic_hints {
125                    if !all_semantic_hints.contains(hint) {
126                        all_semantic_hints.push(hint.clone());
127                    }
128                }
129            },
130            SanitizedEventType::FileActivity {
131                extension_category, ..
132            } => {
133                *file_activity_counts
134                    .entry(extension_category.clone())
135                    .or_insert(0) += 1;
136            },
137            SanitizedEventType::SystemStatus {
138                battery_pct,
139                is_charging,
140                network,
141                ringer_mode,
142                location_type,
143                headphone_connected,
144            } => {
145                latest_system_status = Some(SystemStatusSnapshot {
146                    battery_pct: *battery_pct,
147                    is_charging: *is_charging,
148                    network: network.clone(),
149                    ringer_mode: ringer_mode.clone(),
150                    location_type: location_type.clone(),
151                    headphone_connected: *headphone_connected,
152                });
153            },
154            SanitizedEventType::InterAppInteraction {
155                source_package: Some(pkg),
156                ..
157            } if !foreground_apps.contains(pkg) => {
158                foreground_apps.push(pkg.clone());
159            },
160            _ => {},
161        }
162    }
163
164    let file_activity: Vec<(ExtensionCategory, u32)> = file_activity_counts.into_iter().collect();
165
166    ContextSummary {
167        foreground_apps,
168        notified_apps,
169        all_semantic_hints,
170        file_activity,
171        latest_system_status,
172        source_tier,
173    }
174}
175
176fn new_id() -> String {
177    Uuid::new_v4().to_string()
178}