persistent auth in SQLite, API chat/logs, agent completion via channel

- Auth: move from state.json to SQLite authed_chats table, with memory cache
- Remove Persistent/state.json, all state now in noc.db
- HTTP API: POST /api/chat (end-to-end LLM), GET /api/logs (failed API requests)
- API logging: store raw request/response for 400 errors in api_log table
- Agent completion: spawn_agent sends LifeEvent::AgentDone via channel,
  life loop picks up with full conversation context and responds
- Config structs: derive Clone for HTTP server
- System prompt: instruct LLM not to add timestamps
- Makefile: rsync without --delete to preserve VPS-only tools
This commit is contained in:
Fam Zheng
2026-04-11 09:31:48 +01:00
parent f7bcdf9b4b
commit 55e9b2f50f
11 changed files with 230 additions and 71 deletions

View File

@@ -77,10 +77,12 @@ async fn main() {
gitea.resolve_token();
}
let state_path = std::env::var("NOC_STATE")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("state.json"));
let state = Arc::new(AppState::load(state_path));
// channel: http/agents → life loop
let (life_tx, life_rx) = tokio::sync::mpsc::channel(16);
let config_path = std::env::var("NOC_CONFIG").unwrap_or_else(|_| "config.yaml".into());
let db_dir = Path::new(&config_path).parent().unwrap_or(Path::new("."));
let state = Arc::new(AppState::load(db_dir, life_tx.clone()));
let _ = std::fs::create_dir_all(incoming_dir());
@@ -93,18 +95,15 @@ async fn main() {
let config = Arc::new(config);
// channel: http server → life loop
let (life_tx, life_rx) = tokio::sync::mpsc::channel(16);
// start life loop
tokio::spawn(life::life_loop(bot.clone(), state.clone(), config.clone(), life_rx));
// start http server (API + gitea webhook)
{
let srv_config = config.clone();
let http_config = config.as_ref().clone();
let srv_state = state.clone();
tokio::spawn(async move {
http::start_http_server(&srv_config, srv_state, life_tx).await;
http::start_http_server(&http_config, srv_state, life_tx).await;
});
}
@@ -173,20 +172,10 @@ async fn handle(
let is_private = msg.chat.is_private();
let text = msg.text().or(msg.caption()).unwrap_or("").to_string();
let raw_id = chat_id.0;
let date = session_date(config.session.refresh_hour);
let is_authed = {
let p = state.persist.read().await;
p.authed.get(&raw_id) == Some(&date)
};
if !is_authed {
if !state.is_authed(raw_id).await {
if text.trim() == config.auth.passphrase {
{
let mut p = state.persist.write().await;
p.authed.insert(raw_id, date);
}
state.save().await;
state.set_authed(raw_id).await;
bot.send_message(chat_id, "authenticated").await?;
info!(chat = raw_id, "authed");
} else {