Skip to main content

beam_core/
config.rs

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    /// Group-chat keywords that activate the bot without an @mention.
107    /// A matching trigger can also supply the initial prompt for the
108    /// session it creates.
109    #[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    /// Keyword that activates the bot. Matched at the start of a group
124    /// message when it is followed by a word boundary (whitespace,
125    /// punctuation, or end of text), so a short keyword does not match
126    /// inside longer words.
127    #[serde(rename = "trigger")]
128    pub trigger: String,
129    /// Initial prompt used when this trigger creates a new session.
130    /// Trailing user text after the keyword is appended after the prompt.
131    #[serde(rename = "prompt", default)]
132    pub prompt: Option<String>,
133    /// When true, a session created by this trigger skips the directory
134    /// selection card and uses `workingDir` (or the bot's default).
135    #[serde(rename = "skipDirSelect", default)]
136    pub skip_dir_select: bool,
137    /// Working directory used when this trigger creates a session directly.
138    /// Takes precedence over the bot's `workingDir`.
139    #[serde(rename = "workingDir", default)]
140    pub working_dir: Option<String>,
141    /// Message replied immediately (as a reply to the triggering message)
142    /// when this trigger creates a session, so users know the task was
143    /// accepted before the longer-running work produces output.
144    #[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}