feat: iPet → iAs 完整迁移,Rust 版微信 AI 助手
Phase 1 ✅ CLI 框架 + 配置系统 - clap 子命令: login / listen / send / whoami / usage - config.json + env var 替换 - tracing 日志系统 - state 持久化(auth/runtime 文件存 + PostgreSQL) Phase 2 ✅ 微信通道 - wechat::client — 完整 iLink Bot HTTP API 实现 - 扫码登录(终端二维码 + 轮询状态) - 长轮询 getupdates / 消息收发 / 监听注册 Phase 3 ✅ AI 对话(纯文本 + function calling) - LlmProvider trait: DeepSeek + LM Studio 实现 - SSE 流式解析(text / reasoning / tool_calls delta / usage) - Conversation: 消息历史 + chat / chat_with_tools 工具循环 Phase 4 ✅ PostgreSQL 集成 - app_state(认证 KV 存储) - chat_records(消息收发记录) - llm_usage(Token 用量统计缓存命中率) - user_memories(长期记忆持久化) - pending_approvals(审批确认码) - scheduled_tasks(定时任务表) Phase 5 ✅ 一切皆 Skill(工具系统) - SkillRegistry: 系统 + 用户 skills 双目录合并 - SKILL.md 解析器 + 子进程执行器(stdin JSON → stdout) - 9 个系统 Skills: datetime / weather / search / email / shell / schedule / memos / read_memories / read_summaries - ApprovalManager: High 风险技能 → 确认码审批(透明模式) - High 风险技能:确认码审批(透明模式) Phase 6 ✅ 定时任务调度器 上下文管理 - ChatSession: checkpoint + token budget (28K) + summaries - Token 估算器(中英文自适应) - 12h 空闲 → trigger_idle_summary(不入会话) - Budget 溢出 → trigger_overflow_summary(入会话 + drain 旧消息) - Summarizer: LLM 生成自然语言摘要(fallback 简单截断) - 长期记忆 / 摘要 通过 read_memories / read_summaries 工具按需读取 工具调用日志 + Token 统计 - INFO: 工具名 + 参数 + 结果摘要 - DEBUG: 子进程 exit/stdout/stderr - ias usage --since --until --model 查看用量和缓存命中率
This commit is contained in:
+635
-2
@@ -1,3 +1,636 @@
|
||||
fn main() {
|
||||
println!("Hello, world!");
|
||||
mod cli;
|
||||
mod config;
|
||||
mod context;
|
||||
mod db;
|
||||
mod llm;
|
||||
mod scheduler;
|
||||
mod state;
|
||||
mod tools;
|
||||
mod wechat;
|
||||
|
||||
use clap::Parser;
|
||||
use cli::{Cli, Commands};
|
||||
use context::MemoryStore;
|
||||
use config::AppConfig;
|
||||
use context::types::ChatSession as _;
|
||||
use db::Database;
|
||||
use llm::{Conversation, ConversationConfig, ToolExecutor, Usage, DEFAULT_SYSTEM_PROMPT};
|
||||
use std::sync::Arc;
|
||||
use tools::approval::ApprovalManager;
|
||||
use tools::registry::{ExecutionContext, SkillRegistry};
|
||||
use tracing::{error, info};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
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();
|
||||
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
let config = AppConfig::from_file("config.json");
|
||||
|
||||
// 初始化数据库(如果配置了 DATABASE_URL)
|
||||
let database = match Database::connect().await {
|
||||
Ok(db) => {
|
||||
info!("✅ 数据库连接成功");
|
||||
Some(Arc::new(db))
|
||||
}
|
||||
Err(e) => {
|
||||
info!("数据库不可用,使用文件存储: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// 长期记忆存储
|
||||
let memory_store = Arc::new(MemoryStore::new(database.as_ref().map(|db| Arc::new(db.pool().clone()))));
|
||||
memory_store.load().await;
|
||||
|
||||
// 文件状态管理器(降级方案)
|
||||
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;
|
||||
}
|
||||
Commands::Listen {
|
||||
llm: enable_llm,
|
||||
echo,
|
||||
} => {
|
||||
cmd_listen(enable_llm, echo, &database, &file_state, &config, &memory_store).await;
|
||||
}
|
||||
Commands::Send {
|
||||
to,
|
||||
text,
|
||||
context_token,
|
||||
} => {
|
||||
cmd_send(&to, &text, context_token.as_deref(), &database, &file_state).await;
|
||||
}
|
||||
Commands::Whoami => {
|
||||
cmd_whoami(&database, &file_state).await;
|
||||
}
|
||||
Commands::Usage { since, until, model } => {
|
||||
cmd_usage(&database, since, until, model).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 命令实现 ───
|
||||
|
||||
async fn cmd_login(
|
||||
timeout_secs: u64,
|
||||
database: &Option<Arc<Database>>,
|
||||
file_state: &state::StateManager,
|
||||
) {
|
||||
let client = WeChatClient::new(None);
|
||||
|
||||
match client
|
||||
.login(|url| println!("二维码链接: {}", url), timeout_secs)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
let auth = state::AuthState {
|
||||
token: result.token.clone(),
|
||||
account_id: result.account_id.clone(),
|
||||
base_url: result.base_url.clone(),
|
||||
};
|
||||
|
||||
// 优先存数据库
|
||||
if let Some(db) = database {
|
||||
if let Err(e) = db::models::save_auth(db.pool(), &auth).await {
|
||||
error!("保存 auth 到数据库失败: {}", e);
|
||||
file_state.save_auth(&auth);
|
||||
} else {
|
||||
info!("认证信息已保存到数据库");
|
||||
// 清理文件状态(迁移完成)
|
||||
file_state.clear_auth();
|
||||
}
|
||||
} else {
|
||||
file_state.save_auth(&auth);
|
||||
}
|
||||
|
||||
println!("\n✅ 登录成功!");
|
||||
println!(" 账号: {}", result.account_id);
|
||||
println!(" Token: {}...", &result.token[..std::cmp::min(16, result.token.len())]);
|
||||
println!(" API: {}", result.base_url);
|
||||
}
|
||||
Err(e) => error!("登录失败: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
async fn cmd_listen(
|
||||
enable_llm: bool,
|
||||
echo: bool,
|
||||
database: &Option<Arc<Database>>,
|
||||
file_state: &state::StateManager,
|
||||
config: &AppConfig,
|
||||
memory_store: &Arc<MemoryStore>,
|
||||
) {
|
||||
// 从数据库或文件加载认证
|
||||
let auth = 'load: {
|
||||
if let Some(db) = database {
|
||||
if let Some(a) = db::models::load_auth(db.pool()).await {
|
||||
break 'load Some(a);
|
||||
}
|
||||
// 尝试从文件迁移
|
||||
if let Some(fa) = file_state.load_auth() {
|
||||
let a = state::AuthState {
|
||||
token: fa.token,
|
||||
account_id: fa.account_id,
|
||||
base_url: fa.base_url,
|
||||
};
|
||||
let _ = db::models::save_auth(db.pool(), &a).await;
|
||||
break 'load Some(a);
|
||||
}
|
||||
} else if let Some(a) = file_state.load_auth() {
|
||||
break 'load Some(a);
|
||||
}
|
||||
break 'load None;
|
||||
};
|
||||
|
||||
let auth = match auth {
|
||||
Some(a) => a,
|
||||
None => {
|
||||
error!("未登录,请先执行 login 命令");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let client = WeChatClient::new(Some(auth.base_url.clone()));
|
||||
client
|
||||
.set_auth(&auth.token, &auth.account_id, &auth.base_url)
|
||||
.await;
|
||||
|
||||
// 从文件恢复 runtime 状态(get_updates_buf 暂时保留文件存储)
|
||||
if let Some(r) = file_state.load_runtime() {
|
||||
client.set_updates_buf(&r.get_updates_buf).await;
|
||||
}
|
||||
|
||||
if let Err(e) = client.notify_start().await {
|
||||
error!("注册监听器失败: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载系统 Skills
|
||||
let skill_registry = match SkillRegistry::load("resources/skills", "skills").await {
|
||||
Ok(r) => {
|
||||
info!("已加载 {} 个技能", r.count());
|
||||
Some(Arc::new(r))
|
||||
}
|
||||
Err(e) => {
|
||||
info!("技能加载结果: {:?}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// 审批管理器
|
||||
let approval_manager = database.as_ref().map(|db| {
|
||||
Arc::new(ApprovalManager::new(Some(Arc::new(db.pool().clone()))))
|
||||
});
|
||||
|
||||
let conversation = if enable_llm {
|
||||
let sys_prompt = std::env::var("WEIXIN_LLM_SYSTEM_PROMPT")
|
||||
.unwrap_or_else(|_| DEFAULT_SYSTEM_PROMPT.to_string());
|
||||
let model = config.llm.deepseek.model.clone();
|
||||
|
||||
let mut cfg = ConversationConfig {
|
||||
system_prompt: sys_prompt,
|
||||
model,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// 构建 LLM tools 列表(skills + 上下文工具)
|
||||
let mut tools_list: Vec<serde_json::Value> = Vec::new();
|
||||
|
||||
if let Some(ref registry) = skill_registry {
|
||||
for spec in registry.list_specs() {
|
||||
tools_list.push(serde_json::json!({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": spec.name,
|
||||
"description": spec.description,
|
||||
"parameters": spec.parameters,
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// 上下文工具(长期记忆 + 摘要)
|
||||
tools_list.push(serde_json::json!({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_memories",
|
||||
"description": "读取用户的长期记忆(跨会话持久保存的个人信息、偏好、约定等)",
|
||||
"parameters": { "type": "object", "properties": {} }
|
||||
}
|
||||
}));
|
||||
tools_list.push(serde_json::json!({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "write_memory",
|
||||
"description": "写入一条用户的长期记忆(当用户提到重要偏好、个人信息、约定时调用)",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": { "type": "string", "description": "记忆内容" }
|
||||
},
|
||||
"required": ["content"]
|
||||
}
|
||||
}
|
||||
}));
|
||||
tools_list.push(serde_json::json!({
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_summaries",
|
||||
"description": "读取历史会话摘要(包括因上下文压缩或空闲超时产生的摘要)",
|
||||
"parameters": { "type": "object", "properties": {} }
|
||||
}
|
||||
}));
|
||||
|
||||
cfg.tools = Some(tools_list);
|
||||
|
||||
// 创建 Conversation
|
||||
let mut conv = match Conversation::new(cfg) {
|
||||
Ok(c) => {
|
||||
info!("LLM 已初始化: {} / {}", c.provider_name(), c.model());
|
||||
c
|
||||
}
|
||||
Err(e) => {
|
||||
error!("初始化 LLM 失败: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 构建工具执行器
|
||||
let session = conv.session();
|
||||
let registry_opt = skill_registry.clone();
|
||||
let approval = approval_manager.clone();
|
||||
let memory_store = memory_store.clone();
|
||||
let send_wechat: tools::registry::WechatSender = {
|
||||
let client = client.clone();
|
||||
Arc::new(move |user_id: &str, text: &str| {
|
||||
let client = client.clone();
|
||||
let uid = user_id.to_string();
|
||||
let msg = text.to_string();
|
||||
Box::pin(async move {
|
||||
client.send_text(&uid, &msg, None).await.map(|_| ()).map_err(|e| e)
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
let executor: ToolExecutor = Arc::new(move |name: &str, args_json: &str| {
|
||||
let reg = registry_opt.clone();
|
||||
let approval = approval.clone();
|
||||
let send = send_wechat.clone();
|
||||
let session = session.clone();
|
||||
let memory_store = memory_store.clone();
|
||||
let n = name.to_string();
|
||||
let args = args_json.to_string();
|
||||
|
||||
Box::pin(async move {
|
||||
match n.as_str() {
|
||||
"read_memories" => return Ok(memory_store.read().await),
|
||||
"write_memory" => {
|
||||
let params: serde_json::Value = serde_json::from_str(&args).unwrap_or_default();
|
||||
let content = params.get("content").and_then(|v| v.as_str()).unwrap_or("");
|
||||
if content.is_empty() { return Ok("请提供 content 参数".to_string()); }
|
||||
return Ok(memory_store.write(content).await);
|
||||
}
|
||||
"read_summaries" => return Ok(context::tools::read_summaries(&session).await),
|
||||
_ => {}
|
||||
}
|
||||
if let Some(ref reg) = reg {
|
||||
let params: serde_json::Value = serde_json::from_str(&args).unwrap_or(serde_json::json!({}));
|
||||
let mut ctx = ExecutionContext::new("wechat_user");
|
||||
if let Some(a) = approval { ctx = ctx.with_approval(a); }
|
||||
ctx = ctx.with_wechat(send);
|
||||
let result = reg.execute(&n, params, &ctx).await;
|
||||
Ok(result.output)
|
||||
} else {
|
||||
Ok(format!("未知工具: {}", n))
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
conv.set_tool_executor(executor);
|
||||
|
||||
Some(conv)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let account_id = auth.account_id.clone();
|
||||
|
||||
// 启动调度器(如果数据库可用)
|
||||
if let Some(sched_db) = database.as_ref().map(|db| db.pool().clone()) {
|
||||
let sched_client = client.clone();
|
||||
tokio::spawn(async move {
|
||||
let scheduler = scheduler::Scheduler::new(Some(Arc::new(sched_db)));
|
||||
scheduler.run(move |to: &str, _name: &str, msg: &str| {
|
||||
let c = sched_client.clone();
|
||||
let uid = to.to_string();
|
||||
let text = msg.to_string();
|
||||
tokio::task::spawn(async move {
|
||||
if let Err(e) = c.send_text(&uid, &text, None).await {
|
||||
tracing::error!("发送调度结果失败: {}", e);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}).await;
|
||||
});
|
||||
info!("定时任务调度器已启动");
|
||||
}
|
||||
|
||||
info!("开始监听消息 (llm={}, echo={})...", enable_llm, echo);
|
||||
println!("已开始监听消息,按 Ctrl+C 退出");
|
||||
|
||||
let shutdown = async { tokio::signal::ctrl_c().await.expect("Ctrl+C 失败") };
|
||||
|
||||
tokio::select! {
|
||||
_ = shutdown => {
|
||||
info!("收到退出信号,注销监听器...");
|
||||
let _ = client.notify_stop().await;
|
||||
file_state.save_runtime(&client.get_updates_buf().await);
|
||||
info!("已退出");
|
||||
}
|
||||
_ = listen_loop(&client, enable_llm, echo, &account_id, database, file_state, conversation, approval_manager) => {}
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen_loop(
|
||||
client: &WeChatClient,
|
||||
enable_llm: bool,
|
||||
echo: bool,
|
||||
account_id: &str,
|
||||
database: &Option<Arc<Database>>,
|
||||
file_state: &state::StateManager,
|
||||
conversation: Option<Conversation>,
|
||||
approval_manager: Option<Arc<ApprovalManager>>,
|
||||
) {
|
||||
loop {
|
||||
match client.receive_messages().await {
|
||||
Ok(resp) => {
|
||||
// 持久化 runtime buf
|
||||
if !resp.get_updates_buf.is_empty() {
|
||||
file_state.save_runtime(&resp.get_updates_buf);
|
||||
}
|
||||
|
||||
for msg in &resp.msgs {
|
||||
if msg.msg_type != Some(wechat::types::WeixinMessage::TYPE_USER) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let from = msg.from_user_id.as_deref().unwrap_or("unknown");
|
||||
let text = msg.text_content().unwrap_or("(非文本消息)");
|
||||
let ctx_token = msg.context_token.as_deref();
|
||||
let msg_id = msg.message_id.unwrap_or(0).to_string();
|
||||
|
||||
info!("收到消息 from={}: {}", from, text);
|
||||
|
||||
// === 入库:收到的消息 ===
|
||||
if let Some(db) = database {
|
||||
let _ = db::models::insert_chat_record(
|
||||
db.pool(),
|
||||
"inbound",
|
||||
from,
|
||||
account_id,
|
||||
text,
|
||||
"wechat",
|
||||
ctx_token,
|
||||
&msg_id,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Echo 模式
|
||||
if echo {
|
||||
match client.send_text(from, text, ctx_token).await {
|
||||
Ok(sent_id) => {
|
||||
info!("回显成功 msg_id={}", sent_id);
|
||||
// === 入库:发送的回显 ===
|
||||
if let Some(db) = database {
|
||||
let _ = db::models::insert_chat_record(
|
||||
db.pool(),
|
||||
"outbound",
|
||||
from,
|
||||
account_id,
|
||||
text,
|
||||
"echo",
|
||||
ctx_token,
|
||||
&sent_id,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(e) => error!("回显失败: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// 审批回复检查:是否匹配待审批的确认码
|
||||
let is_approval_reply = if let Some(ref mgr) = approval_manager {
|
||||
mgr.handle_reply(from, text).await.is_some()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if is_approval_reply {
|
||||
info!("消息匹配审批确认码,不传给 LLM(已由审批系统处理)");
|
||||
continue;
|
||||
}
|
||||
|
||||
// LLM 回复
|
||||
if enable_llm {
|
||||
if let Some(ref conv) = conversation {
|
||||
let reply_result = if conv.spec().tools.is_some() {
|
||||
generate_reply_with_tools(conv, text.to_string(), database).await
|
||||
} else {
|
||||
generate_reply(conv, text.to_string(), database).await
|
||||
};
|
||||
|
||||
match reply_result {
|
||||
Ok(reply) => {
|
||||
match client.send_text(from, &reply, ctx_token).await {
|
||||
Ok(sent_id) => {
|
||||
info!("LLM 回复成功 msg_id={}", sent_id);
|
||||
if let Some(db) = database {
|
||||
let _ = db::models::insert_chat_record(
|
||||
db.pool(), "outbound", from, account_id,
|
||||
&reply, "llm", ctx_token, &sent_id,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
Err(e) => error!("发送 LLM 回复失败: {}", e),
|
||||
}
|
||||
}
|
||||
Err(e) => error!("生成 LLM 回复失败: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if !e.contains("超时") && !e.contains("timeout") {
|
||||
error!("接收消息失败: {}", e);
|
||||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn generate_reply(conv: &Conversation, user_text: String, db: &Option<Arc<Database>>) -> Result<String, String> {
|
||||
let handle = conv.chat(user_text).await?;
|
||||
if let Some(u) = handle.get_usage() {
|
||||
info!("📊 Token: {} 总 / {} 提示 / {} 生成 | 缓存命中 {} | 缓存未命中 {}",
|
||||
u.total_tokens, u.prompt_tokens, u.completion_tokens,
|
||||
u.prompt_cache_hit_tokens, u.prompt_cache_miss_tokens);
|
||||
store_usage(db, "", conv.model(), conv.provider_name(), u).await;
|
||||
}
|
||||
handle.consume().await
|
||||
}
|
||||
|
||||
async fn generate_reply_with_tools(
|
||||
conv: &Conversation,
|
||||
user_text: String,
|
||||
db: &Option<Arc<Database>>,
|
||||
) -> Result<String, String> {
|
||||
let (reply, _used_tools, usage) = conv.chat_with_tools(user_text).await?;
|
||||
if let Some(u) = &usage {
|
||||
info!("📊 Token: {} 总 / {} 提示 / {} 生成 | 缓存命中 {} | 缓存未命中 {}",
|
||||
u.total_tokens, u.prompt_tokens, u.completion_tokens,
|
||||
u.prompt_cache_hit_tokens, u.prompt_cache_miss_tokens);
|
||||
store_usage(db, "", conv.model(), conv.provider_name(), u).await;
|
||||
}
|
||||
Ok(reply)
|
||||
}
|
||||
|
||||
async fn store_usage(db: &Option<Arc<Database>>, user_id: &str, model: &str, provider: &str, u: &Usage) {
|
||||
if let Some(db) = db {
|
||||
let _ = db::models::insert_llm_usage(
|
||||
db.pool(), user_id, model, provider,
|
||||
u.prompt_tokens, u.completion_tokens,
|
||||
u.prompt_cache_hit_tokens, u.prompt_cache_miss_tokens,
|
||||
).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn cmd_whoami(
|
||||
database: &Option<Arc<Database>>,
|
||||
file_state: &state::StateManager,
|
||||
) {
|
||||
let auth: Option<state::AuthState> = if let Some(db) = database {
|
||||
db::models::load_auth(db.pool()).await
|
||||
} else {
|
||||
file_state.load_auth()
|
||||
};
|
||||
|
||||
match auth {
|
||||
Some(a) => {
|
||||
println!("账号: {}", a.account_id);
|
||||
println!("Token: {}...", &a.token[..std::cmp::min(16, a.token.len())]);
|
||||
println!("API: {}", a.base_url);
|
||||
if database.is_some() {
|
||||
println!("存储: PostgreSQL");
|
||||
} else {
|
||||
println!("存储: 文件 ({})", file_state.state_dir().display());
|
||||
}
|
||||
}
|
||||
None => println!("未登录,请先执行 login 命令"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn cmd_usage(
|
||||
database: &Option<Arc<Database>>,
|
||||
since: Option<String>,
|
||||
until: Option<String>,
|
||||
model: Option<String>,
|
||||
) {
|
||||
let db = match database {
|
||||
Some(d) => d,
|
||||
None => { println!("数据库未配置,无法查询用量"); return; }
|
||||
};
|
||||
|
||||
let since_dt = since.and_then(|s| {
|
||||
chrono::DateTime::parse_from_rfc3339(&s).ok().map(|d| d.with_timezone(&chrono::Utc))
|
||||
});
|
||||
let until_dt = until.and_then(|s| {
|
||||
chrono::DateTime::parse_from_rfc3339(&s).ok().map(|d| d.with_timezone(&chrono::Utc))
|
||||
});
|
||||
let model_ref = model.as_deref();
|
||||
|
||||
match db::models::query_llm_usage_stats(db.pool(), since_dt, until_dt, model_ref).await {
|
||||
Ok(stats) => {
|
||||
let hit_rate = if stats.total_cache_hit + stats.total_cache_miss > 0 {
|
||||
stats.total_cache_hit as f64 / (stats.total_cache_hit + stats.total_cache_miss) as f64 * 100.0
|
||||
} else { 0.0 };
|
||||
|
||||
println!("📊 LLM Token 使用统计");
|
||||
println!("{:=<40}", "");
|
||||
println!("调用次数: {}", stats.total_calls);
|
||||
println!("Prompt Tokens: {} ({:.1}K)", stats.total_prompt_tokens, stats.total_prompt_tokens as f64 / 1000.0);
|
||||
println!("生成 Tokens: {} ({:.1}K)", stats.total_completion_tokens, stats.total_completion_tokens as f64 / 1000.0);
|
||||
println!("总 Tokens: {} ({:.1}K)", stats.total_tokens, stats.total_tokens as f64 / 1000.0);
|
||||
println!("缓存命中: {} ({:.1}%)", stats.total_cache_hit, hit_rate);
|
||||
println!("缓存未命中: {} ({:.1}%)", stats.total_cache_miss, 100.0 - hit_rate);
|
||||
}
|
||||
Err(e) => println!("查询失败: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
async fn cmd_send(
|
||||
to: &str,
|
||||
text: &str,
|
||||
context_token: Option<&str>,
|
||||
database: &Option<Arc<Database>>,
|
||||
file_state: &state::StateManager,
|
||||
) {
|
||||
let auth: Option<state::AuthState> = if let Some(db) = database {
|
||||
db::models::load_auth(db.pool()).await
|
||||
} else {
|
||||
file_state.load_auth()
|
||||
};
|
||||
|
||||
let auth = match auth {
|
||||
Some(a) => a,
|
||||
None => {
|
||||
error!("未登录,请先执行 login 命令");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let base_url = auth.base_url.clone();
|
||||
let account_id = auth.account_id.clone();
|
||||
let client = WeChatClient::new(Some(auth.base_url));
|
||||
client
|
||||
.set_auth(&auth.token, &auth.account_id, &base_url)
|
||||
.await;
|
||||
|
||||
match client.send_text(to, text, context_token).await {
|
||||
Ok(msg_id) => {
|
||||
println!("✅ 消息已发送, msg_id={}", msg_id);
|
||||
|
||||
// 入库:发送的消息
|
||||
if let Some(db) = database {
|
||||
let _ = db::models::insert_chat_record(
|
||||
db.pool(),
|
||||
"outbound",
|
||||
to,
|
||||
&account_id,
|
||||
text,
|
||||
"manual",
|
||||
context_token,
|
||||
&msg_id,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(e) => error!("发送失败: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user