agentic_warden/core/
models.rs

1//! 统一数据模型定义
2//!
3//! 定义系统中使用的所有核心数据结构
4
5#![allow(dead_code)] // 数据模型定义,部分结构和函数是公共API
6
7use crate::error::{AgenticResult, AgenticWardenError};
8use chrono::{DateTime, Duration, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::{HashMap, HashSet};
11use std::path::PathBuf;
12use std::time::SystemTime;
13
14/// AI CLI 类型
15#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
16pub enum AiType {
17    #[serde(rename = "codex")]
18    Codex,
19    #[serde(rename = "claude")]
20    Claude,
21    #[serde(rename = "gemini")]
22    Gemini,
23    #[serde(rename = "all")]
24    All,
25}
26
27/// 任务唯一标识符
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub struct TaskId(u64);
30
31impl TaskId {
32    #[allow(clippy::new_without_default)]
33    pub fn new() -> Self {
34        Self(
35            SystemTime::now()
36                .duration_since(SystemTime::UNIX_EPOCH)
37                .unwrap_or_else(|_| {
38                    // Fallback: Use a pseudo-random value if system time is before UNIX_EPOCH
39                    // This should never happen on properly configured systems
40                    use std::time::Duration;
41                    Duration::from_nanos(std::process::id() as u64)
42                })
43                .as_nanos() as u64,
44        )
45    }
46}
47
48/// 进程树信息,包含完整进程链与AI CLI元数据
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub struct ProcessTreeInfo {
51    /// 进程链:[current_pid, parent_pid, grandparent_pid, ..., root_pid]
52    #[serde(default)]
53    pub process_chain: Vec<u32>,
54    /// AI CLI根进程PID(如果没有找到则为传统根父进程)
55    #[serde(default)]
56    pub root_parent_pid: Option<u32>,
57    /// 进程树深度
58    #[serde(default, alias = "process_tree_depth")]
59    pub depth: usize,
60    /// 是否找到AI CLI根进程
61    #[serde(default)]
62    pub has_ai_cli_root: bool,
63    /// AI CLI类型
64    #[serde(default)]
65    pub ai_cli_type: Option<String>,
66    /// 可选的AI CLI进程信息
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub ai_cli_process: Option<AiCliProcessInfo>,
69}
70
71impl ProcessTreeInfo {
72    /// 创建新的进程树信息
73    pub fn new(process_chain: Vec<u32>) -> Self {
74        let depth = process_chain.len();
75        let root_parent_pid = process_chain.last().copied();
76        Self {
77            process_chain,
78            root_parent_pid,
79            depth,
80            has_ai_cli_root: false,
81            ai_cli_type: None,
82            ai_cli_process: None,
83        }
84    }
85
86    /// 附加 AI CLI 元数据
87    pub fn with_ai_cli_process(mut self, ai_cli_process: Option<AiCliProcessInfo>) -> Self {
88        if let Some(info) = ai_cli_process {
89            self.root_parent_pid = Some(info.pid);
90            self.ai_cli_type = Some(info.ai_type.clone());
91            self.has_ai_cli_root = true;
92            self.ai_cli_process = Some(info);
93        }
94        self
95    }
96
97    /// 获取AI CLI根进程PID
98    pub fn get_ai_cli_root(&self) -> Option<u32> {
99        if self.has_ai_cli_root {
100            self.ai_cli_process
101                .as_ref()
102                .map(|info| info.pid)
103                .or(self.root_parent_pid)
104        } else {
105            self.root_parent_pid
106        }
107    }
108
109    /// 检查进程链中是否包含指定PID
110    pub fn contains_process(&self, pid: u32) -> bool {
111        self.process_chain.contains(&pid)
112    }
113
114    /// 获取当前进程到AI CLI根进程的子链
115    pub fn get_chain_to_ai_cli_root(&self) -> Vec<u32> {
116        if let Some(root_pid) = self.get_ai_cli_root() {
117            if let Some(pos) = self.process_chain.iter().position(|pid| *pid == root_pid) {
118                return self.process_chain[..=pos].to_vec();
119            }
120        }
121        self.process_chain.clone()
122    }
123
124    /// 校验数据完整性
125    pub fn validate(&self) -> AgenticResult<()> {
126        if self.process_chain.is_empty() {
127            return Err(validation_error(
128                "process_tree.process_chain",
129                "process chain cannot be empty",
130            ));
131        }
132
133        if self.depth != self.process_chain.len() {
134            return Err(validation_error(
135                "process_tree.depth",
136                format!(
137                    "depth ({}) must equal process_chain length ({})",
138                    self.depth,
139                    self.process_chain.len()
140                ),
141            ));
142        }
143
144        let mut seen = HashSet::new();
145        for pid in &self.process_chain {
146            if !seen.insert(pid) {
147                return Err(validation_error(
148                    "process_tree.process_chain",
149                    format!("duplicate pid {} detected", pid),
150                ));
151            }
152        }
153
154        if self.has_ai_cli_root {
155            if self.ai_cli_type.is_none() {
156                return Err(validation_error(
157                    "process_tree.ai_cli_type",
158                    "ai_cli_type required when has_ai_cli_root=true",
159                ));
160            }
161            if self.ai_cli_process.is_none() {
162                return Err(validation_error(
163                    "process_tree.ai_cli_process",
164                    "ai_cli_process required when has_ai_cli_root=true",
165                ));
166            }
167        }
168
169        Ok(())
170    }
171}
172
173/// AI CLI进程的详细信息
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
175pub struct AiCliProcessInfo {
176    /// 进程PID
177    pub pid: u32,
178    /// AI CLI类型
179    pub ai_type: String,
180    /// 进程名称
181    #[serde(default)]
182    pub process_name: String,
183    /// 命令行
184    #[serde(default)]
185    pub command_line: String,
186    /// 是否为NPM包形式
187    pub is_npm_package: bool,
188    /// 检测时间
189    pub detected_at: DateTime<Utc>,
190    /// 可选的可执行路径
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub executable_path: Option<PathBuf>,
193}
194
195impl AiCliProcessInfo {
196    /// 创建新的AI CLI进程信息
197    pub fn new(pid: u32, ai_type: impl Into<String>) -> Self {
198        Self {
199            pid,
200            ai_type: ai_type.into(),
201            process_name: String::new(),
202            command_line: String::new(),
203            is_npm_package: false,
204            detected_at: Utc::now(),
205            executable_path: None,
206        }
207    }
208
209    pub fn with_process_name(mut self, name: impl Into<String>) -> Self {
210        self.process_name = name.into();
211        self
212    }
213
214    pub fn with_command_line(mut self, command_line: impl Into<String>) -> Self {
215        self.command_line = command_line.into();
216        self
217    }
218
219    pub fn with_is_npm_package(mut self, is_npm_package: bool) -> Self {
220        self.is_npm_package = is_npm_package;
221        self
222    }
223
224    pub fn with_executable_path(mut self, path: Option<PathBuf>) -> Self {
225        self.executable_path = path;
226        self
227    }
228
229    /// 检查是否为有效的AI CLI进程
230    pub fn is_valid_ai_cli(&self) -> bool {
231        self.pid > 0 && !self.ai_type.is_empty() && !self.process_name.is_empty()
232    }
233
234    /// 获取进程描述
235    pub fn get_description(&self) -> String {
236        let mut description = format!("{} (pid {})", self.ai_type, self.pid);
237        if !self.process_name.is_empty() {
238            description.push_str(&format!(" via {}", self.process_name));
239        }
240        if self.is_npm_package {
241            description.push_str(" [npm]");
242        }
243        description
244    }
245
246    /// 校验AI CLI进程信息
247    pub fn validate(&self) -> AgenticResult<()> {
248        if self.pid == 0 {
249            return Err(validation_error(
250                "ai_cli_process.pid",
251                "pid must be a non-zero value",
252            ));
253        }
254        if self.ai_type.trim().is_empty() {
255            return Err(validation_error(
256                "ai_cli_process.ai_type",
257                "ai_type cannot be empty",
258            ));
259        }
260        if self.process_name.trim().is_empty() {
261            return Err(validation_error(
262                "ai_cli_process.process_name",
263                "process_name cannot be empty",
264            ));
265        }
266        Ok(())
267    }
268}
269
270/// 进程信息
271#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct ProcessInfo {
273    /// 进程 ID
274    pub pid: u32,
275    /// 父进程 ID
276    pub ppid: u32,
277    /// 进程名称
278    pub name: String,
279    /// 进程路径
280    pub path: Option<PathBuf>,
281    /// 命令行
282    pub command_line: String,
283    /// 启动时间
284    pub start_time: SystemTime,
285    /// 用户 ID
286    pub user_id: Option<u32>,
287    /// 是否为根进程
288    pub is_root: bool,
289    /// 进程树深度
290    pub depth: u32,
291}
292
293/// Provider 配置
294#[derive(Debug, Clone, Serialize, Deserialize)]
295pub struct Provider {
296    /// Provider 唯一标识符
297    pub name: String,
298    /// Provider 描述信息
299    pub description: String,
300    /// 兼容的 AI CLI 类型列表
301    pub compatible_with: Vec<AiType>,
302    /// 环境变量映射
303    pub env: HashMap<String, String>,
304    /// 是否为内置 Provider
305    #[serde(default)]
306    pub builtin: bool,
307    /// 创建时间
308    #[serde(default = "default_now")]
309    pub created_at: DateTime<Utc>,
310    /// 更新时间
311    #[serde(default = "default_now")]
312    pub updated_at: DateTime<Utc>,
313    /// 元数据
314    #[serde(default)]
315    pub metadata: HashMap<String, serde_json::Value>,
316}
317
318fn default_now() -> DateTime<Utc> {
319    Utc::now()
320}
321
322/// Provider 配置文件
323#[derive(Debug, Serialize, Deserialize)]
324pub struct ProviderConfig {
325    /// JSON Schema 版本
326    #[serde(rename = "$schema")]
327    pub schema: String,
328    /// Provider 映射表
329    pub providers: HashMap<String, Provider>,
330    /// 默认 Provider 名称
331    pub default_provider: String,
332    /// 配置文件版本
333    #[serde(default = "default_config_version")]
334    pub version: String,
335    /// 配置文件格式版本
336    #[serde(default = "default_format_version")]
337    pub format_version: u32,
338    /// 配置设置
339    #[serde(default)]
340    pub settings: ProviderSettings,
341}
342
343fn default_config_version() -> String {
344    "1.0.0".to_string()
345}
346
347fn default_format_version() -> u32 {
348    1
349}
350
351/// Provider 设置
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct ProviderSettings {
354    /// 是否自动刷新
355    #[serde(default = "default_true")]
356    pub auto_refresh: bool,
357    /// 健康检查间隔(秒)
358    #[serde(default = "default_health_check_interval")]
359    pub health_check_interval: u64,
360    /// 连接超时(秒)
361    #[serde(default = "default_connection_timeout")]
362    pub connection_timeout: u64,
363    /// 最大重试次数
364    #[serde(default = "default_max_retries")]
365    pub max_retries: u32,
366    /// 启动时验证
367    #[serde(default = "default_true")]
368    pub validate_on_startup: bool,
369}
370
371fn default_true() -> bool {
372    true
373}
374
375fn default_health_check_interval() -> u64 {
376    300
377}
378
379fn default_connection_timeout() -> u64 {
380    30
381}
382
383fn default_max_retries() -> u32 {
384    3
385}
386
387impl Default for ProviderSettings {
388    fn default() -> Self {
389        Self {
390            auto_refresh: true,
391            health_check_interval: 300,
392            connection_timeout: 30,
393            max_retries: 3,
394            validate_on_startup: true,
395        }
396    }
397}
398
399/// OAuth Token 信息
400#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct TokenInfo {
402    /// 访问令牌
403    pub access_token: String,
404    /// 刷新令牌
405    pub refresh_token: String,
406    /// 令牌类型
407    pub token_type: String,
408    /// 过期时间(秒)
409    pub expires_in: u64,
410    /// 实际过期时间戳
411    #[serde(default = "calculate_expiry")]
412    pub expiry_time: DateTime<Utc>,
413    /// 令牌获取时间
414    #[serde(default = "default_now")]
415    pub obtained_at: DateTime<Utc>,
416    /// 令牌范围
417    pub scope: Option<String>,
418}
419
420fn calculate_expiry() -> DateTime<Utc> {
421    Utc::now() + Duration::seconds(3600) // 默认 1 小时
422}
423
424/// 实例注册信息
425#[derive(Debug, Serialize, Deserialize)]
426pub struct InstanceRegistry {
427    /// 实例 ID
428    pub instance_id: usize,
429    /// 实例启动时间
430    pub start_time: SystemTime,
431    /// 主进程 PID
432    pub main_pid: u32,
433    /// 用户名
434    pub username: String,
435    /// 主机名
436    pub hostname: String,
437    /// 工作目录
438    pub working_directory: PathBuf,
439    /// agentic-warden 版本
440    pub version: String,
441    /// 最后心跳时间
442    pub last_heartbeat: SystemTime,
443    /// 任务计数
444    pub task_count: usize,
445    /// 活跃任务数
446    pub active_task_count: usize,
447}
448
449fn validation_error(field: &str, message: impl Into<String>) -> AgenticWardenError {
450    AgenticWardenError::Validation {
451        message: message.into(),
452        field: Some(field.to_string()),
453        value: None,
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    use super::*;
460
461    #[test]
462    fn process_tree_info_roundtrip_includes_ai_cli_metadata() {
463        let ai_info = AiCliProcessInfo::new(42, "claude")
464            .with_process_name("claude-cli")
465            .with_command_line("claude ask --debug")
466            .with_is_npm_package(false);
467        let tree = ProcessTreeInfo::new(vec![4242, 1337, 42]).with_ai_cli_process(Some(ai_info));
468        tree.validate().expect("tree should be valid");
469
470        let serialized = serde_json::to_string(&tree).expect("serialize tree");
471        let restored: ProcessTreeInfo =
472            serde_json::from_str(&serialized).expect("deserialize tree");
473
474        assert_eq!(restored.depth, 3);
475        assert!(restored.has_ai_cli_root);
476        assert_eq!(restored.get_ai_cli_root(), Some(42));
477        assert!(restored.ai_cli_process.is_some());
478    }
479
480    #[test]
481    fn process_tree_info_accepts_legacy_depth_field() {
482        let json = r#"{
483            "process_chain": [100, 50],
484            "process_tree_depth": 2,
485            "root_parent_pid": 50
486        }"#;
487
488        let tree: ProcessTreeInfo =
489            serde_json::from_str(json).expect("legacy depth should deserialize");
490        assert_eq!(tree.depth, 2);
491        tree.validate().expect("tree should remain valid");
492    }
493
494    #[test]
495    fn ai_cli_process_requires_non_empty_name() {
496        let ai = AiCliProcessInfo::new(1, "codex").with_process_name("codex-cli");
497        assert!(ai.validate().is_ok());
498
499        let invalid = AiCliProcessInfo::new(0, "").with_process_name("");
500        assert!(invalid.validate().is_err());
501    }
502}