1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value};
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7pub struct DaemonConfig {
8 #[serde(default)]
9 pub quiet_restart: bool,
10 #[serde(default = "default_working_dirs")]
11 pub working_dirs: Vec<String>,
12}
13
14fn default_working_dirs() -> Vec<String> {
15 vec!["~".to_string()]
16}
17
18impl Default for DaemonConfig {
19 fn default() -> Self {
20 Self {
21 quiet_restart: false,
22 working_dirs: default_working_dirs(),
23 }
24 }
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28pub struct WebConfig {
29 #[serde(default = "default_web_host")]
30 pub host: String,
31 #[serde(default = "default_proxy_base_port")]
32 pub proxy_base_port: u16,
33}
34
35fn default_web_host() -> String {
36 "0.0.0.0".to_string()
37}
38
39fn default_proxy_base_port() -> u16 {
40 8800
41}
42
43impl Default for WebConfig {
44 fn default() -> Self {
45 Self {
46 host: default_web_host(),
47 proxy_base_port: default_proxy_base_port(),
48 }
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
53pub struct Config {
54 #[serde(default)]
55 pub daemon: DaemonConfig,
56 #[serde(default)]
57 pub web: WebConfig,
58 #[serde(default)]
59 pub lark: LarkConfig,
60 #[serde(default, rename = "screenAnalyzer")]
61 pub screen_analyzer: ScreenAnalyzerConfig,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65pub struct BotConfig {
66 #[serde(default)]
67 pub name: Option<String>,
68 #[serde(rename = "larkAppId")]
69 pub lark_app_id: String,
70 #[serde(rename = "larkAppSecret")]
71 pub lark_app_secret: String,
72 #[serde(rename = "cliId")]
73 pub cli_id: String,
74 #[serde(rename = "cliBin", default)]
75 pub cli_bin: Option<String>,
76 #[serde(rename = "cliArgs", default)]
77 pub cli_args: Vec<String>,
78 #[serde(default)]
79 pub model: Option<String>,
80 #[serde(rename = "workingDir", default)]
81 pub working_dir: Option<String>,
82 #[serde(rename = "skipWorkingDirPrompt", default)]
83 pub skip_working_dir_prompt: bool,
84 #[serde(rename = "larkEncryptKey", default)]
85 pub lark_encrypt_key: Option<String>,
86 #[serde(rename = "larkVerificationToken", default)]
87 pub lark_verification_token: Option<String>,
88 #[serde(rename = "allowedUsers", default)]
89 pub allowed_users: Vec<String>,
90 #[serde(rename = "privateCard", default)]
91 pub private_card: bool,
92 #[serde(rename = "allowedChatGroups", default)]
93 pub allowed_chat_groups: Vec<String>,
94 #[serde(rename = "chatGrants", default)]
95 pub chat_grants: std::collections::HashMap<String, Vec<String>>,
96 #[serde(rename = "globalGrants", default)]
97 pub global_grants: Vec<String>,
98 #[serde(rename = "oncallChats", default)]
99 pub oncall_chats: Vec<OncallChatBinding>,
100 #[serde(rename = "restrictGrantCommands", default)]
101 pub restrict_grant_commands: bool,
102 #[serde(rename = "messageQuota", default)]
103 pub message_quota: Option<MessageQuotaConfig>,
104 #[serde(rename = "quotaState", default)]
105 pub quota_state: std::collections::HashMap<String, QuotaEntry>,
106 #[serde(rename = "customTriggers", default)]
110 pub custom_triggers: Vec<CustomTrigger>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
114pub struct OncallChatBinding {
115 #[serde(rename = "chatId")]
116 pub chat_id: String,
117 #[serde(rename = "workingDir", default)]
118 pub working_dir: Option<String>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
122pub struct CustomTrigger {
123 #[serde(rename = "trigger")]
128 pub trigger: String,
129 #[serde(rename = "prompt", default)]
132 pub prompt: Option<String>,
133 #[serde(rename = "skipDirSelect", default)]
136 pub skip_dir_select: bool,
137 #[serde(rename = "workingDir", default)]
140 pub working_dir: Option<String>,
141 #[serde(rename = "ackMessage", default)]
145 pub ack_message: Option<String>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
149pub struct MessageQuotaConfig {
150 #[serde(rename = "defaultLimit", default)]
151 pub default_limit: Option<u32>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
155pub struct QuotaEntry {
156 pub limit: u32,
157 pub used: u32,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
161pub struct LarkConfig {
162 #[serde(default)]
163 pub verification_token: Option<String>,
164 #[serde(default)]
165 pub encrypt_key: Option<String>,
166}
167
168#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
169pub struct ScreenAnalyzerConfig {
170 #[serde(default)]
171 pub enabled: bool,
172 #[serde(default)]
173 pub base_url: String,
174 #[serde(default)]
175 pub api_key: String,
176 #[serde(default)]
177 pub model: String,
178 #[serde(default = "default_screen_analyzer_interval_ms")]
179 pub interval_ms: u64,
180 #[serde(default = "default_screen_analyzer_stable_count")]
181 pub stable_count: u32,
182 #[serde(default = "default_screen_analyzer_snapshot_max_chars")]
183 pub snapshot_max_chars: usize,
184 #[serde(default)]
185 pub extra_headers: HashMap<String, String>,
186 #[serde(default)]
187 pub extra_body: Map<String, Value>,
188}
189
190fn default_screen_analyzer_interval_ms() -> u64 {
191 2_000
192}
193
194fn default_screen_analyzer_stable_count() -> u32 {
195 6
196}
197
198fn default_screen_analyzer_snapshot_max_chars() -> usize {
199 8_000
200}
201
202impl Default for ScreenAnalyzerConfig {
203 fn default() -> Self {
204 Self {
205 enabled: false,
206 base_url: String::new(),
207 api_key: String::new(),
208 model: String::new(),
209 interval_ms: default_screen_analyzer_interval_ms(),
210 stable_count: default_screen_analyzer_stable_count(),
211 snapshot_max_chars: default_screen_analyzer_snapshot_max_chars(),
212 extra_headers: HashMap::new(),
213 extra_body: Map::new(),
214 }
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::BotConfig;
221
222 #[test]
223 fn bot_config_defaults_missing_cli_args_and_skip_prompt_fields() {
224 let raw = r#"{
225 "larkAppId":"app-1",
226 "larkAppSecret":"secret",
227 "cliId":"codex"
228 }"#;
229 let bot: BotConfig = serde_json::from_str(raw).expect("deserialize bot");
230 assert!(bot.cli_args.is_empty());
231 assert!(!bot.skip_working_dir_prompt);
232 }
233
234 #[test]
235 fn bot_config_deserializes_traex_cli_args() {
236 let raw = r#"{
237 "larkAppId":"app-1",
238 "larkAppSecret":"secret",
239 "cliId":"traex",
240 "cliArgs":["-y"]
241 }"#;
242 let bot: BotConfig = serde_json::from_str(raw).expect("deserialize bot");
243 assert_eq!(bot.cli_args, vec!["-y".to_string()]);
244 }
245}