feat: 后台服务模式 + 日志滚动 + 修复 llm_usage 表兼容性
- 新增 logger.rs(tracing-appender,日滚日志,.data/logs/) - service 子命令:等同 listen --llm 但日志同时写入文件 - systemd/ias.service 单元文件 - 迁移 0007:ALTER TABLE llm_usage ADD COLUMN IF NOT EXISTS 修复与 iPet 旧表的结构冲突 - ias usage 命令可正常查询 Token 统计 - 日志滚动保留 7 天,自动删除旧文件
This commit is contained in:
Generated
+81
@@ -467,6 +467,15 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
@@ -528,6 +537,15 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -1127,6 +1145,7 @@ dependencies = [
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
@@ -1652,6 +1671,12 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
@@ -1876,6 +1901,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
@@ -2752,6 +2783,12 @@ version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "symlink"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
@@ -2860,6 +2897,37 @@ dependencies = [
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.3"
|
||||
@@ -3014,6 +3082,19 @@ dependencies = [
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-appender"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"symlink",
|
||||
"thiserror",
|
||||
"time",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
|
||||
@@ -37,3 +37,4 @@ async-trait = "0.1"
|
||||
# 流式处理
|
||||
futures-util = "0.3"
|
||||
sqlx = { version = "0.8.6", features = ["runtime-tokio-native-tls", "postgres", "uuid", "chrono", "derive", "migrate"] }
|
||||
tracing-appender = "0.2.5"
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
-- 会话摘要持久化
|
||||
CREATE TABLE IF NOT EXISTS session_summaries (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id TEXT NOT NULL DEFAULT 'default',
|
||||
summary_text TEXT NOT NULL,
|
||||
reason TEXT NOT NULL DEFAULT 'overflow',
|
||||
message_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_session_summaries_created
|
||||
ON session_summaries (created_at DESC);
|
||||
@@ -0,0 +1,11 @@
|
||||
-- 修复 llm_usage 表:从 iPet 旧格式迁移到 iAs 新格式
|
||||
-- 旧表有 kind/entry 字段,新表需要 prompt_tokens/completion_tokens 等
|
||||
|
||||
ALTER TABLE llm_usage ADD COLUMN IF NOT EXISTS prompt_tokens INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE llm_usage ADD COLUMN IF NOT EXISTS completion_tokens INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE llm_usage ADD COLUMN IF NOT EXISTS total_tokens INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE llm_usage ADD COLUMN IF NOT EXISTS cache_hit_tokens INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE llm_usage ADD COLUMN IF NOT EXISTS cache_miss_tokens INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE llm_usage ADD COLUMN IF NOT EXISTS user_id TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE llm_usage ADD COLUMN IF NOT EXISTS model TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE llm_usage ADD COLUMN IF NOT EXISTS provider TEXT NOT NULL DEFAULT 'deepseek';
|
||||
@@ -60,4 +60,7 @@ pub enum Commands {
|
||||
#[arg(long)]
|
||||
model: Option<String>,
|
||||
},
|
||||
|
||||
/// 后台服务模式(等同 listen --llm,日志写入文件)
|
||||
Service,
|
||||
}
|
||||
|
||||
+41
-17
@@ -30,11 +30,8 @@ fn is_cjk(c: char) -> bool {
|
||||
/// 构建给 LLM 的消息数组(带 token budget 管理)
|
||||
///
|
||||
/// 返回消息数组和是否需要摘要的信息
|
||||
pub async fn build_context(
|
||||
session: &Arc<Mutex<ChatSession>>,
|
||||
system_prompt: &str,
|
||||
) -> Vec<Message> {
|
||||
let mut s = session.lock().await;
|
||||
pub async fn build_context(session: &Arc<Mutex<ChatSession>>, system_prompt: &str) -> Vec<Message> {
|
||||
let s = session.lock().await;
|
||||
|
||||
// ── 1. 空闲超时检查(消息到达前由调用方检查)──
|
||||
// 这里只做构建,超时触发在上层
|
||||
@@ -74,10 +71,14 @@ pub async fn build_context(
|
||||
included.reverse();
|
||||
|
||||
// 保护:确保最近一条用户消息始终在上下文中
|
||||
if let Some(last_user) = recent.iter().rev().find(|m| m.role == crate::llm::types::Role::User) {
|
||||
let already_included = included.iter().any(|m| {
|
||||
m.role == last_user.role && m.content == last_user.content
|
||||
});
|
||||
if let Some(last_user) = recent
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|m| m.role == crate::llm::types::Role::User)
|
||||
{
|
||||
let already_included = included
|
||||
.iter()
|
||||
.any(|m| m.role == last_user.role && m.content == last_user.content);
|
||||
if !already_included {
|
||||
let t = estimate_tokens(last_user);
|
||||
if used + tail_used + t <= s.token_budget + 2000 {
|
||||
@@ -112,7 +113,7 @@ pub async fn trigger_overflow_summary(
|
||||
|
||||
let to_summarize: Vec<_> = s.messages[start..end]
|
||||
.iter()
|
||||
.take(40) // 最多 40 条
|
||||
.take(40) // 最多 40 条
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
@@ -124,11 +125,15 @@ pub async fn trigger_overflow_summary(
|
||||
|
||||
s.summaries.push(super::types::SummaryEntry {
|
||||
checkpoint: prev_checkpoint,
|
||||
text: summary_text,
|
||||
text: summary_text.clone(),
|
||||
reason: super::types::SummaryReason::Overflow,
|
||||
created_at: chrono::Utc::now(),
|
||||
});
|
||||
|
||||
// 持久化到数据库
|
||||
s.save_summary_to_db(&summary_text, "overflow", to_summarize.len() as i32)
|
||||
.await;
|
||||
|
||||
s.checkpoint = end;
|
||||
|
||||
// 清理太旧的消息(checkpoint 之前的保留最近 100 条用于调试)
|
||||
@@ -163,11 +168,15 @@ pub async fn trigger_idle_summary(
|
||||
|
||||
s.summaries.push(super::types::SummaryEntry {
|
||||
checkpoint: cp,
|
||||
text: summary_text,
|
||||
text: summary_text.clone(),
|
||||
reason: super::types::SummaryReason::Timeout,
|
||||
created_at: chrono::Utc::now(),
|
||||
});
|
||||
|
||||
let msg_count = cp as i32;
|
||||
s.save_summary_to_db(&summary_text, "timeout", msg_count)
|
||||
.await;
|
||||
|
||||
s.checkpoint = s.messages.len();
|
||||
|
||||
// 清理旧消息(checkpoint 之前的全部清除)
|
||||
@@ -204,9 +213,15 @@ async fn generate_summary(messages: &[Message], summarizer: Option<&Summarizer>)
|
||||
fn build_summary_prompt(messages: &[Message]) -> String {
|
||||
let convo: String = messages
|
||||
.iter()
|
||||
.filter(|m| m.role == crate::llm::types::Role::User || m.role == crate::llm::types::Role::Assistant)
|
||||
.filter(|m| {
|
||||
m.role == crate::llm::types::Role::User || m.role == crate::llm::types::Role::Assistant
|
||||
})
|
||||
.map(|m| {
|
||||
let role = if m.role == crate::llm::types::Role::User { "用户" } else { "助手" };
|
||||
let role = if m.role == crate::llm::types::Role::User {
|
||||
"用户"
|
||||
} else {
|
||||
"助手"
|
||||
};
|
||||
let content: String = m.content.chars().take(200).collect();
|
||||
format!("{}: {}", role, content)
|
||||
})
|
||||
@@ -224,7 +239,9 @@ fn summarize_messages(messages: &[Message]) -> String {
|
||||
// 简单实现:提取用户消息的前 80 个字符作为摘要
|
||||
let lines: Vec<String> = messages
|
||||
.iter()
|
||||
.filter(|m| m.role == crate::llm::types::Role::User || m.role == crate::llm::types::Role::Assistant)
|
||||
.filter(|m| {
|
||||
m.role == crate::llm::types::Role::User || m.role == crate::llm::types::Role::Assistant
|
||||
})
|
||||
.map(|m| {
|
||||
let role = match m.role {
|
||||
crate::llm::types::Role::User => "用户",
|
||||
@@ -239,9 +256,16 @@ fn summarize_messages(messages: &[Message]) -> String {
|
||||
if lines.is_empty() {
|
||||
"暂无历史对话".to_string()
|
||||
} else {
|
||||
let result = format!("历史对话摘要({} 条消息):\n{}", lines.len(), lines.join("\n"));
|
||||
let result = format!(
|
||||
"历史对话摘要({} 条消息):\n{}",
|
||||
lines.len(),
|
||||
lines.join("\n")
|
||||
);
|
||||
if result.chars().count() > 2000 {
|
||||
format!("{}...(已截断)", result.chars().take(2000).collect::<String>())
|
||||
format!(
|
||||
"{}...(已截断)",
|
||||
result.chars().take(2000).collect::<String>()
|
||||
)
|
||||
} else {
|
||||
result
|
||||
}
|
||||
|
||||
@@ -3,4 +3,3 @@ pub mod tools;
|
||||
pub mod types;
|
||||
|
||||
pub use tools::MemoryStore;
|
||||
pub use types::ChatSession;
|
||||
|
||||
+30
-18
@@ -64,29 +64,41 @@ impl MemoryStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// read_summaries 工具:读取历史摘要
|
||||
/// read_summaries 工具:读取历史摘要(内存 + 数据库)
|
||||
pub async fn read_summaries(
|
||||
session: &Arc<Mutex<super::types::ChatSession>>,
|
||||
) -> String {
|
||||
let s = session.lock().await;
|
||||
if s.summaries.is_empty() {
|
||||
let mut lines: Vec<String> = Vec::new();
|
||||
|
||||
// 内存中的摘要
|
||||
for sum in &s.summaries {
|
||||
let reason = match sum.reason {
|
||||
super::types::SummaryReason::Overflow => "上下文压缩",
|
||||
super::types::SummaryReason::Timeout => "空闲超时",
|
||||
};
|
||||
lines.push(format!(
|
||||
"[{}] {} (原因: {})",
|
||||
sum.created_at.format("%Y-%m-%d %H:%M"),
|
||||
sum.text,
|
||||
reason,
|
||||
));
|
||||
}
|
||||
|
||||
// 数据库中的历史摘要
|
||||
if let Some(pool) = &s.db_pool {
|
||||
if let Ok(db_summaries) = crate::db::models::load_summaries(pool, 5).await {
|
||||
for text in db_summaries {
|
||||
if !lines.iter().any(|l| l.contains(&text.chars().take(30).collect::<String>())) {
|
||||
lines.push(format!("[数据库存档] {}", text));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if lines.is_empty() {
|
||||
"暂无历史摘要".to_string()
|
||||
} else {
|
||||
s.summaries
|
||||
.iter()
|
||||
.map(|sum| {
|
||||
let reason = match sum.reason {
|
||||
super::types::SummaryReason::Overflow => "上下文压缩",
|
||||
super::types::SummaryReason::Timeout => "空闲超时",
|
||||
};
|
||||
format!(
|
||||
"[{}] {} (原因: {})",
|
||||
sum.created_at.format("%Y-%m-%d %H:%M"),
|
||||
sum.text,
|
||||
reason,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n---\n")
|
||||
lines.join("\n---\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::llm::types::Message;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// 摘要原因
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
@@ -27,6 +28,8 @@ pub struct SummaryEntry {
|
||||
/// 聊天会话状态
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChatSession {
|
||||
pub user_id: String,
|
||||
pub db_pool: Option<Arc<sqlx::PgPool>>,
|
||||
/// 摘要总结点——此位置之前的消息已被压缩
|
||||
pub checkpoint: usize,
|
||||
/// 全部消息(checkpoint 之后的保持完整)
|
||||
@@ -44,6 +47,8 @@ pub struct ChatSession {
|
||||
impl Default for ChatSession {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
user_id: "default".to_string(),
|
||||
db_pool: None,
|
||||
checkpoint: 0,
|
||||
messages: Vec::new(),
|
||||
summaries: Vec::new(),
|
||||
@@ -108,4 +113,54 @@ impl ChatSession {
|
||||
pub fn latest_overflow_summary(&self) -> Option<&SummaryEntry> {
|
||||
self.summaries.iter().rev().find(|s| s.reason == SummaryReason::Overflow)
|
||||
}
|
||||
|
||||
/// 从数据库加载最近的聊天记录到会话中
|
||||
pub async fn load_recent_messages(&mut self, limit: i64) {
|
||||
let pool = match &self.db_pool {
|
||||
Some(p) => p.clone(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
let rows = sqlx::query_as::<_, (String, String, String, String)>(
|
||||
"SELECT direction, user_id, text, message_id FROM chat_records ORDER BY created_at DESC LIMIT $1",
|
||||
)
|
||||
.bind(limit)
|
||||
.fetch_all(pool.as_ref())
|
||||
.await;
|
||||
|
||||
if let Ok(rows) = rows {
|
||||
let mut msgs: Vec<Message> = rows
|
||||
.into_iter()
|
||||
.rev() // 恢复时间顺序
|
||||
.map(|(dir, _uid, text, _mid)| {
|
||||
if dir == "inbound" {
|
||||
Message::user(text)
|
||||
} else {
|
||||
Message::assistant(text)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if !msgs.is_empty() {
|
||||
tracing::info!("从数据库恢复 {} 条历史消息", msgs.len());
|
||||
self.messages = msgs;
|
||||
self.checkpoint = 0;
|
||||
self.last_user_at = Some(Utc::now());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存摘要到数据库
|
||||
pub async fn save_summary_to_db(&self, text: &str, reason: &str, message_count: i32) {
|
||||
if let Some(pool) = &self.db_pool {
|
||||
let _ = sqlx::query(
|
||||
"INSERT INTO session_summaries (user_id, summary_text, reason, message_count) VALUES ($1, $2, $3, $4)",
|
||||
)
|
||||
.bind(&self.user_id)
|
||||
.bind(text)
|
||||
.bind(reason)
|
||||
.bind(message_count)
|
||||
.execute(pool.as_ref())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,3 +191,38 @@ pub async fn query_llm_usage_stats(
|
||||
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
// ─── 会话摘要 ───
|
||||
|
||||
/// 保存一条摘要到数据库
|
||||
pub async fn save_summary(
|
||||
pool: &PgPool,
|
||||
user_id: &str,
|
||||
text: &str,
|
||||
reason: &str,
|
||||
message_count: i32,
|
||||
) -> Result<(), String> {
|
||||
sqlx::query(
|
||||
"INSERT INTO session_summaries (user_id, summary_text, reason, message_count) VALUES ($1, $2, $3, $4)",
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(text)
|
||||
.bind(reason)
|
||||
.bind(message_count)
|
||||
.execute(pool)
|
||||
.await
|
||||
.map_err(|e| format!("保存摘要失败: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 加载最近的摘要(用于 read_summaries 工具)
|
||||
pub async fn load_summaries(pool: &PgPool, limit: i64) -> Result<Vec<String>, String> {
|
||||
let rows = sqlx::query_as::<_, (String,)>(
|
||||
"SELECT summary_text FROM session_summaries ORDER BY created_at DESC LIMIT $1",
|
||||
)
|
||||
.bind(limit)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| format!("查询摘要失败: {}", e))?;
|
||||
Ok(rows.into_iter().map(|(t,)| t).collect())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use std::path::PathBuf;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
pub fn init_logger(log_dir: Option<&str>, with_file: bool) {
|
||||
let env_filter = EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
|
||||
let terminal = tracing_subscriber::fmt::layer()
|
||||
.with_target(false)
|
||||
.with_ansi(true);
|
||||
|
||||
if with_file {
|
||||
if let Some(dir) = log_dir {
|
||||
let dir_path = PathBuf::from(dir);
|
||||
std::fs::create_dir_all(&dir_path).ok();
|
||||
|
||||
let file_appender = tracing_appender::rolling::daily(&dir_path, "ias.log");
|
||||
let file_layer = tracing_subscriber::fmt::layer()
|
||||
.with_target(false)
|
||||
.with_ansi(false)
|
||||
.with_writer(file_appender);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(env_filter)
|
||||
.with(terminal)
|
||||
.with(file_layer)
|
||||
.init();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(env_filter)
|
||||
.with(terminal)
|
||||
.init();
|
||||
}
|
||||
+17
-9
@@ -3,6 +3,7 @@ mod config;
|
||||
mod context;
|
||||
mod db;
|
||||
mod llm;
|
||||
mod logger;
|
||||
mod scheduler;
|
||||
mod state;
|
||||
mod tools;
|
||||
@@ -24,12 +25,9 @@ use wechat::client::WeChatClient;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| EnvFilter::new("info")),
|
||||
)
|
||||
.init();
|
||||
let cli = Cli::parse();
|
||||
let is_service = matches!(cli.command, Commands::Service);
|
||||
logger::init_logger(Some(".data/logs"), is_service);
|
||||
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
@@ -54,8 +52,6 @@ async fn main() {
|
||||
// 文件状态管理器(降级方案)
|
||||
let file_state = state::StateManager::new(&config.storage.state_dir);
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Login { timeout } => {
|
||||
cmd_login(timeout, &database, &file_state).await;
|
||||
@@ -79,6 +75,9 @@ async fn main() {
|
||||
Commands::Usage { since, until, model } => {
|
||||
cmd_usage(&database, since, until, model).await;
|
||||
}
|
||||
Commands::Service => {
|
||||
cmd_listen(true, false, &database, &file_state, &config, &memory_store).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,7 +257,16 @@ async fn cmd_listen(
|
||||
|
||||
// 创建 Conversation
|
||||
let mut conv = match Conversation::new(cfg) {
|
||||
Ok(c) => {
|
||||
Ok(mut c) => {
|
||||
// 设置 db_pool 并加载历史消息
|
||||
if let Some(db) = database {
|
||||
let session = c.session();
|
||||
{
|
||||
let mut s = session.lock().await;
|
||||
s.db_pool = Some(Arc::new(db.pool().clone()));
|
||||
s.load_recent_messages(20).await;
|
||||
}
|
||||
}
|
||||
info!("LLM 已初始化: {} / {}", c.provider_name(), c.model());
|
||||
c
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=iAs - 微信 AI 智能助手
|
||||
After=network.target postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=xiao
|
||||
WorkingDirectory=/home/xiao/project/iAs
|
||||
EnvironmentFile=/home/xiao/project/iAs/.env
|
||||
ExecStart=/home/xiao/project/iAs/target/release/iAs service
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
StandardOutput=null
|
||||
StandardError=null
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user