hajimi-claw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +2602 -0
- package/Cargo.toml +57 -0
- package/README.md +73 -0
- package/bin/hajimi-claw.js +28 -0
- package/config.example.toml +32 -0
- package/crates/hajimi-claw-agent/Cargo.toml +25 -0
- package/crates/hajimi-claw-agent/src/lib.rs +351 -0
- package/crates/hajimi-claw-bot/Cargo.toml +18 -0
- package/crates/hajimi-claw-bot/src/lib.rs +305 -0
- package/crates/hajimi-claw-daemon/Cargo.toml +24 -0
- package/crates/hajimi-claw-daemon/src/lib.rs +173 -0
- package/crates/hajimi-claw-exec/Cargo.toml +21 -0
- package/crates/hajimi-claw-exec/src/lib.rs +419 -0
- package/crates/hajimi-claw-gateway/Cargo.toml +27 -0
- package/crates/hajimi-claw-gateway/src/lib.rs +747 -0
- package/crates/hajimi-claw-llm/Cargo.toml +19 -0
- package/crates/hajimi-claw-llm/src/lib.rs +367 -0
- package/crates/hajimi-claw-policy/Cargo.toml +14 -0
- package/crates/hajimi-claw-policy/src/lib.rs +381 -0
- package/crates/hajimi-claw-store/Cargo.toml +17 -0
- package/crates/hajimi-claw-store/src/lib.rs +730 -0
- package/crates/hajimi-claw-tools/Cargo.toml +21 -0
- package/crates/hajimi-claw-tools/src/lib.rs +758 -0
- package/crates/hajimi-claw-types/Cargo.toml +16 -0
- package/crates/hajimi-claw-types/src/lib.rs +300 -0
- package/package.json +26 -0
- package/scripts/npm-install.js +45 -0
- package/src/main.rs +4 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
use std::sync::Arc;
|
|
2
|
+
|
|
3
|
+
use anyhow::{Context, Result};
|
|
4
|
+
use hajimi_claw_gateway::{
|
|
5
|
+
Gateway, GatewayRequest, InlineKeyboard, SessionDirective, parse_gateway_command,
|
|
6
|
+
};
|
|
7
|
+
use serde::Deserialize;
|
|
8
|
+
use tokio::sync::Mutex;
|
|
9
|
+
use tracing::{error, info, warn};
|
|
10
|
+
|
|
11
|
+
#[derive(Debug, Clone)]
|
|
12
|
+
pub struct TelegramConfig {
|
|
13
|
+
pub token: String,
|
|
14
|
+
pub poll_timeout_secs: u64,
|
|
15
|
+
pub admin_user_id: i64,
|
|
16
|
+
pub admin_chat_id: i64,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pub struct TelegramBot {
|
|
20
|
+
client: reqwest::Client,
|
|
21
|
+
config: TelegramConfig,
|
|
22
|
+
gateway: Arc<dyn Gateway>,
|
|
23
|
+
current_session: Mutex<Option<String>>,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl TelegramBot {
|
|
27
|
+
pub fn new(config: TelegramConfig, gateway: Arc<dyn Gateway>) -> Self {
|
|
28
|
+
Self {
|
|
29
|
+
client: reqwest::Client::new(),
|
|
30
|
+
config,
|
|
31
|
+
gateway,
|
|
32
|
+
current_session: Mutex::new(None),
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub async fn run(&self) -> Result<()> {
|
|
37
|
+
let mut offset = 0_i64;
|
|
38
|
+
loop {
|
|
39
|
+
match self.get_updates(offset).await {
|
|
40
|
+
Ok(updates) => {
|
|
41
|
+
for update in updates {
|
|
42
|
+
offset = update.update_id + 1;
|
|
43
|
+
if let Err(err) = self.handle_update(update).await {
|
|
44
|
+
error!(error = %err, "failed to handle telegram update");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
Err(err) => {
|
|
49
|
+
warn!(error = %err, "telegram polling failed");
|
|
50
|
+
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async fn handle_update(&self, update: Update) -> Result<()> {
|
|
57
|
+
if let Some(message) = update.message {
|
|
58
|
+
if !self.is_authorized(
|
|
59
|
+
message.chat.id,
|
|
60
|
+
message
|
|
61
|
+
.from
|
|
62
|
+
.as_ref()
|
|
63
|
+
.map(|user| user.id)
|
|
64
|
+
.unwrap_or_default(),
|
|
65
|
+
) {
|
|
66
|
+
warn!("ignored telegram message from unauthorized actor");
|
|
67
|
+
return Ok(());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let Some(text) = message.text else {
|
|
71
|
+
return Ok(());
|
|
72
|
+
};
|
|
73
|
+
let reply = self.dispatch_command(&text).await;
|
|
74
|
+
self.send_message(message.chat.id, &reply.text, reply.keyboard)
|
|
75
|
+
.await?;
|
|
76
|
+
return Ok(());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if let Some(callback_query) = update.callback_query {
|
|
80
|
+
let chat_id = callback_query
|
|
81
|
+
.message
|
|
82
|
+
.as_ref()
|
|
83
|
+
.map(|message| message.chat.id)
|
|
84
|
+
.unwrap_or(self.config.admin_chat_id);
|
|
85
|
+
if !self.is_authorized(chat_id, callback_query.from.id) {
|
|
86
|
+
warn!("ignored telegram callback from unauthorized actor");
|
|
87
|
+
return Ok(());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if let Some(data) = callback_query.data {
|
|
91
|
+
let reply = self.dispatch_command(&data).await;
|
|
92
|
+
self.send_message(chat_id, &reply.text, reply.keyboard)
|
|
93
|
+
.await?;
|
|
94
|
+
}
|
|
95
|
+
self.answer_callback_query(&callback_query.id).await?;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
Ok(())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async fn dispatch_command(&self, text: &str) -> BotReply {
|
|
102
|
+
let current_session_id = self.current_session.lock().await.clone();
|
|
103
|
+
match self
|
|
104
|
+
.gateway
|
|
105
|
+
.handle(GatewayRequest {
|
|
106
|
+
actor_user_id: self.config.admin_user_id,
|
|
107
|
+
actor_chat_id: self.config.admin_chat_id,
|
|
108
|
+
raw_text: text.to_string(),
|
|
109
|
+
command: parse_gateway_command(text),
|
|
110
|
+
current_session_id,
|
|
111
|
+
})
|
|
112
|
+
.await
|
|
113
|
+
{
|
|
114
|
+
Ok(response) => {
|
|
115
|
+
match response.session {
|
|
116
|
+
SessionDirective::Keep => {}
|
|
117
|
+
SessionDirective::Set(session_id) => {
|
|
118
|
+
*self.current_session.lock().await = Some(session_id);
|
|
119
|
+
}
|
|
120
|
+
SessionDirective::Clear => {
|
|
121
|
+
*self.current_session.lock().await = None;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
BotReply {
|
|
125
|
+
text: response.text,
|
|
126
|
+
keyboard: response.keyboard,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
Err(err) => BotReply {
|
|
130
|
+
text: format!("error: {err}"),
|
|
131
|
+
keyboard: None,
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async fn get_updates(&self, offset: i64) -> Result<Vec<Update>> {
|
|
137
|
+
let response = self
|
|
138
|
+
.client
|
|
139
|
+
.post(self.api_url("getUpdates"))
|
|
140
|
+
.json(&serde_json::json!({
|
|
141
|
+
"timeout": self.config.poll_timeout_secs,
|
|
142
|
+
"offset": offset,
|
|
143
|
+
}))
|
|
144
|
+
.send()
|
|
145
|
+
.await
|
|
146
|
+
.context("getUpdates request")?;
|
|
147
|
+
|
|
148
|
+
let payload: TelegramEnvelope<Vec<Update>> =
|
|
149
|
+
response.json().await.context("decode updates")?;
|
|
150
|
+
if !payload.ok {
|
|
151
|
+
anyhow::bail!("telegram getUpdates returned ok=false");
|
|
152
|
+
}
|
|
153
|
+
Ok(payload.result)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async fn send_message(
|
|
157
|
+
&self,
|
|
158
|
+
chat_id: i64,
|
|
159
|
+
text: &str,
|
|
160
|
+
keyboard: Option<InlineKeyboard>,
|
|
161
|
+
) -> Result<()> {
|
|
162
|
+
let payload = if let Some(keyboard) = keyboard {
|
|
163
|
+
serde_json::json!({
|
|
164
|
+
"chat_id": chat_id,
|
|
165
|
+
"text": clamp_text(text),
|
|
166
|
+
"reply_markup": to_reply_markup(keyboard),
|
|
167
|
+
})
|
|
168
|
+
} else {
|
|
169
|
+
serde_json::json!({
|
|
170
|
+
"chat_id": chat_id,
|
|
171
|
+
"text": clamp_text(text),
|
|
172
|
+
})
|
|
173
|
+
};
|
|
174
|
+
let response = self
|
|
175
|
+
.client
|
|
176
|
+
.post(self.api_url("sendMessage"))
|
|
177
|
+
.json(&payload)
|
|
178
|
+
.send()
|
|
179
|
+
.await
|
|
180
|
+
.context("sendMessage request")?;
|
|
181
|
+
let payload: TelegramEnvelope<serde_json::Value> =
|
|
182
|
+
response.json().await.context("decode sendMessage")?;
|
|
183
|
+
if !payload.ok {
|
|
184
|
+
anyhow::bail!("telegram sendMessage returned ok=false");
|
|
185
|
+
}
|
|
186
|
+
info!("sent telegram message");
|
|
187
|
+
Ok(())
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async fn answer_callback_query(&self, callback_query_id: &str) -> Result<()> {
|
|
191
|
+
let response = self
|
|
192
|
+
.client
|
|
193
|
+
.post(self.api_url("answerCallbackQuery"))
|
|
194
|
+
.json(&serde_json::json!({
|
|
195
|
+
"callback_query_id": callback_query_id,
|
|
196
|
+
}))
|
|
197
|
+
.send()
|
|
198
|
+
.await
|
|
199
|
+
.context("answerCallbackQuery request")?;
|
|
200
|
+
let payload: TelegramEnvelope<serde_json::Value> = response
|
|
201
|
+
.json()
|
|
202
|
+
.await
|
|
203
|
+
.context("decode answerCallbackQuery")?;
|
|
204
|
+
if !payload.ok {
|
|
205
|
+
anyhow::bail!("telegram answerCallbackQuery returned ok=false");
|
|
206
|
+
}
|
|
207
|
+
Ok(())
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fn api_url(&self, method: &str) -> String {
|
|
211
|
+
format!(
|
|
212
|
+
"https://api.telegram.org/bot{}/{}",
|
|
213
|
+
self.config.token, method
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
fn is_authorized(&self, chat_id: i64, user_id: i64) -> bool {
|
|
218
|
+
chat_id == self.config.admin_chat_id && user_id == self.config.admin_user_id
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#[derive(Debug, Deserialize)]
|
|
223
|
+
struct TelegramEnvelope<T> {
|
|
224
|
+
ok: bool,
|
|
225
|
+
result: T,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#[derive(Debug, Deserialize)]
|
|
229
|
+
struct Update {
|
|
230
|
+
update_id: i64,
|
|
231
|
+
message: Option<Message>,
|
|
232
|
+
callback_query: Option<CallbackQuery>,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
#[derive(Debug, Deserialize)]
|
|
236
|
+
struct Message {
|
|
237
|
+
chat: Chat,
|
|
238
|
+
from: Option<User>,
|
|
239
|
+
text: Option<String>,
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#[derive(Debug, Deserialize)]
|
|
243
|
+
struct Chat {
|
|
244
|
+
id: i64,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#[derive(Debug, Deserialize)]
|
|
248
|
+
struct User {
|
|
249
|
+
id: i64,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#[derive(Debug, Deserialize)]
|
|
253
|
+
struct CallbackQuery {
|
|
254
|
+
id: String,
|
|
255
|
+
from: User,
|
|
256
|
+
data: Option<String>,
|
|
257
|
+
message: Option<Message>,
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
struct BotReply {
|
|
261
|
+
text: String,
|
|
262
|
+
keyboard: Option<InlineKeyboard>,
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
fn clamp_text(text: &str) -> String {
|
|
266
|
+
const MAX: usize = 3500;
|
|
267
|
+
if text.len() <= MAX {
|
|
268
|
+
return text.to_string();
|
|
269
|
+
}
|
|
270
|
+
format!("{}...\n[truncated]", &text[..MAX])
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fn to_reply_markup(keyboard: InlineKeyboard) -> serde_json::Value {
|
|
274
|
+
let rows = keyboard
|
|
275
|
+
.rows
|
|
276
|
+
.into_iter()
|
|
277
|
+
.map(|row| {
|
|
278
|
+
row.into_iter()
|
|
279
|
+
.map(|button| {
|
|
280
|
+
serde_json::json!({
|
|
281
|
+
"text": button.text,
|
|
282
|
+
"callback_data": button.data,
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
.collect::<Vec<_>>()
|
|
286
|
+
})
|
|
287
|
+
.collect::<Vec<_>>();
|
|
288
|
+
serde_json::json!({ "inline_keyboard": rows })
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#[cfg(test)]
|
|
292
|
+
mod tests {
|
|
293
|
+
use hajimi_claw_gateway::{GatewayCommand, parse_gateway_command};
|
|
294
|
+
|
|
295
|
+
#[test]
|
|
296
|
+
fn parses_elevated_start() {
|
|
297
|
+
assert_eq!(
|
|
298
|
+
parse_gateway_command("/elevated start 15 maintenance"),
|
|
299
|
+
GatewayCommand::ElevatedStart {
|
|
300
|
+
minutes: 15,
|
|
301
|
+
reason: "maintenance".into(),
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "hajimi-claw-daemon"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
license.workspace = true
|
|
6
|
+
authors.workspace = true
|
|
7
|
+
|
|
8
|
+
[dependencies]
|
|
9
|
+
anyhow.workspace = true
|
|
10
|
+
chrono.workspace = true
|
|
11
|
+
serde.workspace = true
|
|
12
|
+
tokio.workspace = true
|
|
13
|
+
toml.workspace = true
|
|
14
|
+
tracing.workspace = true
|
|
15
|
+
tracing-subscriber.workspace = true
|
|
16
|
+
hajimi-claw-agent = { path = "../hajimi-claw-agent" }
|
|
17
|
+
hajimi-claw-bot = { path = "../hajimi-claw-bot" }
|
|
18
|
+
hajimi-claw-exec = { path = "../hajimi-claw-exec" }
|
|
19
|
+
hajimi-claw-gateway = { path = "../hajimi-claw-gateway" }
|
|
20
|
+
hajimi-claw-llm = { path = "../hajimi-claw-llm" }
|
|
21
|
+
hajimi-claw-policy = { path = "../hajimi-claw-policy" }
|
|
22
|
+
hajimi-claw-store = { path = "../hajimi-claw-store" }
|
|
23
|
+
hajimi-claw-tools = { path = "../hajimi-claw-tools" }
|
|
24
|
+
hajimi-claw-types = { path = "../hajimi-claw-types" }
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
use std::path::PathBuf;
|
|
2
|
+
use std::sync::Arc;
|
|
3
|
+
|
|
4
|
+
use anyhow::{Context, Result};
|
|
5
|
+
use hajimi_claw_agent::AgentRuntime;
|
|
6
|
+
use hajimi_claw_bot::{TelegramBot, TelegramConfig};
|
|
7
|
+
use hajimi_claw_exec::{LocalExecutor, PlatformMode};
|
|
8
|
+
use hajimi_claw_gateway::InProcessGateway;
|
|
9
|
+
use hajimi_claw_llm::{StaticBackend, StoreBackedBackend};
|
|
10
|
+
use hajimi_claw_policy::{PolicyConfig, PolicyEngine};
|
|
11
|
+
use hajimi_claw_store::{SecretCipher, Store};
|
|
12
|
+
use hajimi_claw_tools::ToolRegistry;
|
|
13
|
+
use serde::Deserialize;
|
|
14
|
+
use tracing_subscriber::EnvFilter;
|
|
15
|
+
|
|
16
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
17
|
+
pub struct AppConfig {
|
|
18
|
+
pub telegram: TelegramSection,
|
|
19
|
+
pub llm: LlmSection,
|
|
20
|
+
pub storage: StorageSection,
|
|
21
|
+
pub security: SecuritySection,
|
|
22
|
+
pub policy: PolicyConfig,
|
|
23
|
+
pub execution: ExecutionSection,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
27
|
+
pub struct TelegramSection {
|
|
28
|
+
pub bot_token: String,
|
|
29
|
+
pub poll_timeout_secs: Option<u64>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
33
|
+
pub struct LlmSection {
|
|
34
|
+
pub base_url: Option<String>,
|
|
35
|
+
pub api_key: Option<String>,
|
|
36
|
+
pub model: Option<String>,
|
|
37
|
+
pub static_fallback_response: Option<String>,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
41
|
+
pub struct StorageSection {
|
|
42
|
+
pub sqlite_path: PathBuf,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
46
|
+
pub struct SecuritySection {
|
|
47
|
+
pub master_key_env: Option<String>,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
51
|
+
pub struct ExecutionSection {
|
|
52
|
+
pub mode: Option<String>,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pub async fn run_from_env() -> Result<()> {
|
|
56
|
+
init_tracing();
|
|
57
|
+
let config_path = std::env::var("HAJIMI_CLAW_CONFIG").unwrap_or_else(|_| "config.toml".into());
|
|
58
|
+
let config = load_config(PathBuf::from(config_path))?;
|
|
59
|
+
run(config).await
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
pub async fn run(config: AppConfig) -> Result<()> {
|
|
63
|
+
if let Some(parent) = config.storage.sqlite_path.parent() {
|
|
64
|
+
std::fs::create_dir_all(parent)
|
|
65
|
+
.with_context(|| format!("create storage directory {}", parent.display()))?;
|
|
66
|
+
}
|
|
67
|
+
let policy = Arc::new(PolicyEngine::new(config.policy.clone()));
|
|
68
|
+
let master_key_env = config
|
|
69
|
+
.security
|
|
70
|
+
.master_key_env
|
|
71
|
+
.clone()
|
|
72
|
+
.unwrap_or_else(|| "HAJIMI_CLAW_MASTER_KEY".into());
|
|
73
|
+
let master_key = std::env::var(&master_key_env).with_context(|| {
|
|
74
|
+
format!(
|
|
75
|
+
"missing provider encryption key env `{master_key_env}`; set it before starting hajimi-claw"
|
|
76
|
+
)
|
|
77
|
+
})?;
|
|
78
|
+
let cipher = Arc::new(SecretCipher::from_passphrase(&master_key)?);
|
|
79
|
+
let store = Arc::new(Store::open_with_cipher(
|
|
80
|
+
&config.storage.sqlite_path,
|
|
81
|
+
Some(cipher),
|
|
82
|
+
)?);
|
|
83
|
+
let executor = Arc::new(LocalExecutor::new(
|
|
84
|
+
policy.clone(),
|
|
85
|
+
select_platform_mode(config.execution.mode.as_deref()),
|
|
86
|
+
));
|
|
87
|
+
let tools = Arc::new(ToolRegistry::default(executor, policy.clone()));
|
|
88
|
+
let fallback = Arc::new(StaticBackend::new(
|
|
89
|
+
config
|
|
90
|
+
.llm
|
|
91
|
+
.static_fallback_response
|
|
92
|
+
.clone()
|
|
93
|
+
.unwrap_or_else(|| "LLM backend not configured.".into()),
|
|
94
|
+
));
|
|
95
|
+
if let (Some(base_url), Some(api_key), Some(model)) = (
|
|
96
|
+
config.llm.base_url.clone(),
|
|
97
|
+
config.llm.api_key.clone(),
|
|
98
|
+
config.llm.model.clone(),
|
|
99
|
+
) {
|
|
100
|
+
let bootstrap_record = hajimi_claw_types::ProviderRecord {
|
|
101
|
+
config: hajimi_claw_types::ProviderConfig {
|
|
102
|
+
id: "bootstrap".into(),
|
|
103
|
+
label: "Bootstrap".into(),
|
|
104
|
+
kind: hajimi_claw_types::ProviderKind::OpenAiCompatible,
|
|
105
|
+
base_url,
|
|
106
|
+
api_key,
|
|
107
|
+
model,
|
|
108
|
+
enabled: true,
|
|
109
|
+
extra_headers: vec![],
|
|
110
|
+
created_at: chrono::Utc::now(),
|
|
111
|
+
},
|
|
112
|
+
is_default: store.get_default_provider()?.is_none(),
|
|
113
|
+
};
|
|
114
|
+
store.upsert_provider(&bootstrap_record)?;
|
|
115
|
+
}
|
|
116
|
+
let llm: Arc<dyn hajimi_claw_types::LlmBackend> =
|
|
117
|
+
Arc::new(StoreBackedBackend::new(store.clone(), Some(fallback)));
|
|
118
|
+
|
|
119
|
+
let runtime = Arc::new(AgentRuntime::new(llm, tools, store.clone(), policy.clone()));
|
|
120
|
+
let gateway = Arc::new(InProcessGateway::new(
|
|
121
|
+
runtime,
|
|
122
|
+
policy.clone(),
|
|
123
|
+
store.clone(),
|
|
124
|
+
));
|
|
125
|
+
let bot = TelegramBot::new(
|
|
126
|
+
TelegramConfig {
|
|
127
|
+
token: config.telegram.bot_token,
|
|
128
|
+
poll_timeout_secs: config.telegram.poll_timeout_secs.unwrap_or(30),
|
|
129
|
+
admin_user_id: config.policy.admin_user_id,
|
|
130
|
+
admin_chat_id: config.policy.admin_chat_id,
|
|
131
|
+
},
|
|
132
|
+
gateway,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
bot.run().await
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
pub fn load_config(path: PathBuf) -> Result<AppConfig> {
|
|
139
|
+
let raw = std::fs::read_to_string(&path)
|
|
140
|
+
.with_context(|| format!("read config file {}", path.display()))?;
|
|
141
|
+
let config: AppConfig = toml::from_str(&raw).context("parse config.toml")?;
|
|
142
|
+
Ok(config)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fn init_tracing() {
|
|
146
|
+
let _ = tracing_subscriber::fmt()
|
|
147
|
+
.with_env_filter(EnvFilter::from_default_env())
|
|
148
|
+
.try_init();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
fn select_platform_mode(mode: Option<&str>) -> PlatformMode {
|
|
152
|
+
match mode.unwrap_or("auto") {
|
|
153
|
+
"windows-safe" => PlatformMode::WindowsSafe,
|
|
154
|
+
"windows-elevated" => PlatformMode::WindowsElevated,
|
|
155
|
+
"unix" => PlatformMode::Unix,
|
|
156
|
+
_ if cfg!(windows) => PlatformMode::WindowsSafe,
|
|
157
|
+
_ => PlatformMode::Unix,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#[cfg(test)]
|
|
162
|
+
mod tests {
|
|
163
|
+
use super::select_platform_mode;
|
|
164
|
+
use hajimi_claw_exec::PlatformMode;
|
|
165
|
+
|
|
166
|
+
#[test]
|
|
167
|
+
fn selects_explicit_mode() {
|
|
168
|
+
assert!(matches!(
|
|
169
|
+
select_platform_mode(Some("windows-safe")),
|
|
170
|
+
PlatformMode::WindowsSafe
|
|
171
|
+
));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "hajimi-claw-exec"
|
|
3
|
+
version.workspace = true
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
license.workspace = true
|
|
6
|
+
authors.workspace = true
|
|
7
|
+
|
|
8
|
+
[dependencies]
|
|
9
|
+
anyhow.workspace = true
|
|
10
|
+
async-trait.workspace = true
|
|
11
|
+
chrono.workspace = true
|
|
12
|
+
hajimi-claw-policy = { path = "../hajimi-claw-policy" }
|
|
13
|
+
hajimi-claw-types = { path = "../hajimi-claw-types" }
|
|
14
|
+
serde.workspace = true
|
|
15
|
+
tokio.workspace = true
|
|
16
|
+
tracing.workspace = true
|
|
17
|
+
uuid.workspace = true
|
|
18
|
+
windows.workspace = true
|
|
19
|
+
|
|
20
|
+
[dev-dependencies]
|
|
21
|
+
tempfile.workspace = true
|