extract Output trait: decouple AI core from Telegram
- Add src/output.rs with Output trait and 3 implementations: TelegramOutput (streaming via draft/edit), GiteaOutput (comments), BufferOutput (for worker/tests) - Refactor run_openai_with_tools and execute_tool to use &mut dyn Output - Remove run_claude_streaming, invoke_claude_streaming, run_openai_streaming (dead code — only OpenAI-compatible backend is used now) - Remove BackendConfig::Claude code path from handler - stream.rs: 790 → 150 lines
This commit is contained in:
209
src/main.rs
209
src/main.rs
@@ -2,6 +2,7 @@ mod config;
|
||||
mod display;
|
||||
mod gitea;
|
||||
mod life;
|
||||
mod output;
|
||||
mod state;
|
||||
mod stream;
|
||||
mod tools;
|
||||
@@ -21,12 +22,9 @@ use uuid::Uuid;
|
||||
|
||||
use config::{BackendConfig, Config};
|
||||
use display::build_user_content;
|
||||
use output::TelegramOutput;
|
||||
use state::{AppState, MAX_WINDOW, SLIDE_SIZE};
|
||||
use stream::{
|
||||
build_system_prompt, invoke_claude_streaming, run_claude_streaming, run_openai_with_tools,
|
||||
summarize_messages,
|
||||
};
|
||||
use tools::discover_tools;
|
||||
use stream::{build_system_prompt, run_openai_with_tools, summarize_messages};
|
||||
|
||||
// ── helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -315,7 +313,7 @@ async fn handle_inner(
|
||||
let count = state.message_count(&sid).await;
|
||||
let persona = state.get_config("persona").await.unwrap_or_default();
|
||||
let scratch = state.get_scratch().await;
|
||||
let tools = discover_tools();
|
||||
let tools = tools::discover_tools();
|
||||
let empty = vec![];
|
||||
let tools_arr = tools.as_array().unwrap_or(&empty);
|
||||
|
||||
@@ -373,126 +371,97 @@ async fn handle_inner(
|
||||
}
|
||||
}
|
||||
|
||||
// handle "cc" prefix: pass directly to claude -p, no session, no history
|
||||
if let Some(cc_prompt) = text.strip_prefix("cc").map(|s| s.trim_start()) {
|
||||
if !cc_prompt.is_empty() {
|
||||
info!(%sid, "cc passthrough");
|
||||
let prompt = build_prompt(cc_prompt, &uploaded, &download_errors, &transcriptions);
|
||||
match run_claude_streaming(&[], &prompt, bot, chat_id).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!(%sid, "cc claude: {e:#}");
|
||||
let _ = bot.send_message(chat_id, format!("[error] {e:#}")).await;
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let prompt = build_prompt(text, &uploaded, &download_errors, &transcriptions);
|
||||
|
||||
match &config.backend {
|
||||
BackendConfig::Claude => {
|
||||
let known = state.persist.read().await.known_sessions.contains(&sid);
|
||||
let result =
|
||||
invoke_claude_streaming(&sid, &prompt, known, bot, chat_id).await;
|
||||
match &result {
|
||||
Ok(_) => {
|
||||
if !known {
|
||||
state.persist.write().await.known_sessions.insert(sid.clone());
|
||||
state.save().await;
|
||||
let BackendConfig::OpenAI {
|
||||
endpoint,
|
||||
model,
|
||||
api_key,
|
||||
} = &config.backend
|
||||
else {
|
||||
let _ = bot.send_message(chat_id, "Only OpenAI backend is supported").await;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let conv = state.load_conv(&sid).await;
|
||||
let persona = state.get_config("persona").await.unwrap_or_default();
|
||||
let memory_slots = state.get_memory_slots().await;
|
||||
let inner = state.get_inner_state().await;
|
||||
let system_msg = build_system_prompt(&conv.summary, &persona, &memory_slots, &inner);
|
||||
|
||||
let mut api_messages = vec![system_msg];
|
||||
api_messages.extend(conv.messages);
|
||||
|
||||
let scratch = state.get_scratch().await;
|
||||
let user_content = build_user_content(&prompt, &scratch, &uploaded);
|
||||
api_messages.push(serde_json::json!({"role": "user", "content": user_content}));
|
||||
|
||||
let mut tg_output = TelegramOutput::new(bot.clone(), chat_id, is_private);
|
||||
|
||||
match run_openai_with_tools(
|
||||
endpoint, model, api_key, api_messages, &mut tg_output, state, &sid, config, chat_id.0,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
state.push_message(&sid, "user", &prompt).await;
|
||||
if !response.is_empty() {
|
||||
state.push_message(&sid, "assistant", &response).await;
|
||||
}
|
||||
|
||||
// sliding window
|
||||
let count = state.message_count(&sid).await;
|
||||
if count >= MAX_WINDOW {
|
||||
info!(%sid, "sliding window: {count} messages, summarizing oldest {SLIDE_SIZE}");
|
||||
let _ = bot
|
||||
.send_message(chat_id, "[整理记忆中...]")
|
||||
.await;
|
||||
|
||||
let to_summarize =
|
||||
state.get_oldest_messages(&sid, SLIDE_SIZE).await;
|
||||
let current_summary = {
|
||||
let db = state.db.lock().await;
|
||||
db.query_row(
|
||||
"SELECT summary FROM conversations WHERE session_id = ?1",
|
||||
[&sid],
|
||||
|row| row.get::<_, String>(0),
|
||||
)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
match summarize_messages(
|
||||
endpoint,
|
||||
model,
|
||||
api_key,
|
||||
¤t_summary,
|
||||
&to_summarize,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(new_summary) => {
|
||||
state.slide_window(&sid, &new_summary, SLIDE_SIZE).await;
|
||||
let remaining = state.message_count(&sid).await;
|
||||
info!(%sid, "window slid, {remaining} messages remain, summary {} chars", new_summary.len());
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(%sid, "summarize failed: {e:#}, keeping all messages");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(%sid, "claude: {e:#}");
|
||||
let _ = bot.send_message(chat_id, format!("[error] {e:#}")).await;
|
||||
}
|
||||
}
|
||||
|
||||
// auto-reflect every 10 messages
|
||||
let count = state.message_count(&sid).await;
|
||||
if count % 10 == 0 && count > 0 {
|
||||
let state_c = state.clone();
|
||||
let config_c = config.clone();
|
||||
tokio::spawn(async move {
|
||||
crate::life::reflect(&state_c, &config_c).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
BackendConfig::OpenAI {
|
||||
endpoint,
|
||||
model,
|
||||
api_key,
|
||||
} => {
|
||||
let conv = state.load_conv(&sid).await;
|
||||
let persona = state.get_config("persona").await.unwrap_or_default();
|
||||
let memory_slots = state.get_memory_slots().await;
|
||||
let inner = state.get_inner_state().await;
|
||||
let system_msg = build_system_prompt(&conv.summary, &persona, &memory_slots, &inner);
|
||||
|
||||
let mut api_messages = vec![system_msg];
|
||||
api_messages.extend(conv.messages);
|
||||
|
||||
let scratch = state.get_scratch().await;
|
||||
let user_content = build_user_content(&prompt, &scratch, &uploaded);
|
||||
api_messages.push(serde_json::json!({"role": "user", "content": user_content}));
|
||||
|
||||
match run_openai_with_tools(
|
||||
endpoint, model, api_key, api_messages, bot, chat_id, state, &sid, config, is_private,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
state.push_message(&sid, "user", &prompt).await;
|
||||
if !response.is_empty() {
|
||||
state.push_message(&sid, "assistant", &response).await;
|
||||
}
|
||||
|
||||
// sliding window
|
||||
let count = state.message_count(&sid).await;
|
||||
if count >= MAX_WINDOW {
|
||||
info!(%sid, "sliding window: {count} messages, summarizing oldest {SLIDE_SIZE}");
|
||||
let _ = bot
|
||||
.send_message(chat_id, "[整理记忆中...]")
|
||||
.await;
|
||||
|
||||
let to_summarize =
|
||||
state.get_oldest_messages(&sid, SLIDE_SIZE).await;
|
||||
let current_summary = {
|
||||
let db = state.db.lock().await;
|
||||
db.query_row(
|
||||
"SELECT summary FROM conversations WHERE session_id = ?1",
|
||||
[&sid],
|
||||
|row| row.get::<_, String>(0),
|
||||
)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
match summarize_messages(
|
||||
endpoint,
|
||||
model,
|
||||
api_key,
|
||||
¤t_summary,
|
||||
&to_summarize,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(new_summary) => {
|
||||
state.slide_window(&sid, &new_summary, SLIDE_SIZE).await;
|
||||
let remaining = state.message_count(&sid).await;
|
||||
info!(%sid, "window slid, {remaining} messages remain, summary {} chars", new_summary.len());
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(%sid, "summarize failed: {e:#}, keeping all messages");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// auto-reflect every 10 messages
|
||||
let count = state.message_count(&sid).await;
|
||||
if count % 10 == 0 && count > 0 {
|
||||
let state_c = state.clone();
|
||||
let config_c = config.clone();
|
||||
tokio::spawn(async move {
|
||||
crate::life::reflect(&state_c, &config_c).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(%sid, "openai: {e:#}");
|
||||
let _ = bot.send_message(chat_id, format!("[error] {e:#}")).await;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(%sid, "openai: {e:#}");
|
||||
let _ = bot.send_message(chat_id, format!("[error] {e:#}")).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user