feat: manage_skill + 全局 skills 目录
新增: - manage_skill: LLM 可调用的 Skill 创建/删除/列出工具 (High) - 全局 skills 支持: ~/.ias/skills/ 自动加载 - SkillRegistry::register_skill() 动态注册 - SkillRegistry::load() 三元加载: system → global → user manage_skill 操作: create: 写 SKILL.md + 执行脚本到 skills/<name>/ delete: 移除 skills/<name>/ list: 列出所有自定义 Skill 目前一共 9 个系统 Skills
This commit is contained in:
+148
-58
@@ -11,16 +11,14 @@ mod wechat;
|
||||
|
||||
use clap::Parser;
|
||||
use cli::{Cli, Commands};
|
||||
use context::MemoryStore;
|
||||
use config::AppConfig;
|
||||
use context::types::ChatSession as _;
|
||||
use context::MemoryStore;
|
||||
use db::Database;
|
||||
use llm::{Conversation, ConversationConfig, ToolExecutor, Usage, DEFAULT_SYSTEM_PROMPT};
|
||||
use llm::{Conversation, ConversationConfig, DEFAULT_SYSTEM_PROMPT, ToolExecutor, Usage};
|
||||
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]
|
||||
@@ -46,7 +44,9 @@ async fn main() {
|
||||
};
|
||||
|
||||
// 长期记忆存储
|
||||
let memory_store = Arc::new(MemoryStore::new(database.as_ref().map(|db| Arc::new(db.pool().clone()))));
|
||||
let memory_store = Arc::new(MemoryStore::new(
|
||||
database.as_ref().map(|db| Arc::new(db.pool().clone())),
|
||||
));
|
||||
memory_store.load().await;
|
||||
|
||||
// 文件状态管理器(降级方案)
|
||||
@@ -60,7 +60,15 @@ async fn main() {
|
||||
llm: enable_llm,
|
||||
echo,
|
||||
} => {
|
||||
cmd_listen(enable_llm, echo, &database, &file_state, &config, &memory_store).await;
|
||||
cmd_listen(
|
||||
enable_llm,
|
||||
echo,
|
||||
&database,
|
||||
&file_state,
|
||||
&config,
|
||||
&memory_store,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Commands::Send {
|
||||
to,
|
||||
@@ -72,7 +80,11 @@ async fn main() {
|
||||
Commands::Whoami => {
|
||||
cmd_whoami(&database, &file_state).await;
|
||||
}
|
||||
Commands::Usage { since, until, model } => {
|
||||
Commands::Usage {
|
||||
since,
|
||||
until,
|
||||
model,
|
||||
} => {
|
||||
cmd_usage(&database, since, until, model).await;
|
||||
}
|
||||
Commands::Service => {
|
||||
@@ -117,7 +129,10 @@ async fn cmd_login(
|
||||
|
||||
println!("\n✅ 登录成功!");
|
||||
println!(" 账号: {}", result.account_id);
|
||||
println!(" Token: {}...", &result.token[..std::cmp::min(16, result.token.len())]);
|
||||
println!(
|
||||
" Token: {}...",
|
||||
&result.token[..std::cmp::min(16, result.token.len())]
|
||||
);
|
||||
println!(" API: {}", result.base_url);
|
||||
}
|
||||
Err(e) => error!("登录失败: {}", e),
|
||||
@@ -190,9 +205,9 @@ async fn cmd_listen(
|
||||
};
|
||||
|
||||
// 审批管理器
|
||||
let approval_manager = database.as_ref().map(|db| {
|
||||
Arc::new(ApprovalManager::new(Some(Arc::new(db.pool().clone()))))
|
||||
});
|
||||
let approval_manager = database
|
||||
.as_ref()
|
||||
.map(|db| Arc::new(ApprovalManager::new(Some(Arc::new(db.pool().clone())))));
|
||||
|
||||
// 共享的当前用户 ID(用于审批消息)
|
||||
let current_user_id = Arc::new(tokio::sync::Mutex::new(String::new()));
|
||||
@@ -260,7 +275,7 @@ async fn cmd_listen(
|
||||
|
||||
// 创建 Conversation
|
||||
let mut conv = match Conversation::new(cfg) {
|
||||
Ok(mut c) => {
|
||||
Ok(c) => {
|
||||
// 设置 db_pool 并加载历史消息
|
||||
if let Some(db) = database {
|
||||
let session = c.session();
|
||||
@@ -291,7 +306,11 @@ async fn cmd_listen(
|
||||
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)
|
||||
client
|
||||
.send_text(&uid, &msg, None)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|e| e)
|
||||
})
|
||||
})
|
||||
};
|
||||
@@ -311,9 +330,12 @@ async fn cmd_listen(
|
||||
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 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()); }
|
||||
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),
|
||||
@@ -321,9 +343,12 @@ async fn cmd_listen(
|
||||
}
|
||||
let user_id = current_user.lock().await.clone();
|
||||
if let Some(ref reg) = reg {
|
||||
let params: serde_json::Value = serde_json::from_str(&args).unwrap_or(serde_json::json!({}));
|
||||
let params: serde_json::Value =
|
||||
serde_json::from_str(&args).unwrap_or(serde_json::json!({}));
|
||||
let mut ctx = ExecutionContext::new(&user_id);
|
||||
if let Some(a) = approval { ctx = ctx.with_approval(a); }
|
||||
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)
|
||||
@@ -347,17 +372,19 @@ async fn cmd_listen(
|
||||
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;
|
||||
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!("定时任务调度器已启动");
|
||||
}
|
||||
@@ -483,14 +510,24 @@ async fn listen_loop(
|
||||
generate_reply(&conv, text_owned, &database).await
|
||||
};
|
||||
let reply = reply_result.ok().unwrap_or_default();
|
||||
match client.send_text(&from_owned, &reply, ctx_owned.as_deref()).await {
|
||||
match client
|
||||
.send_text(&from_owned, &reply, ctx_owned.as_deref())
|
||||
.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_owned, &aid,
|
||||
&reply, "llm", ctx_owned.as_deref(), &sent_id,
|
||||
).await;
|
||||
db.pool(),
|
||||
"outbound",
|
||||
&from_owned,
|
||||
&aid,
|
||||
&reply,
|
||||
"llm",
|
||||
ctx_owned.as_deref(),
|
||||
&sent_id,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(e) => error!("发送 LLM 回复失败: {}", e),
|
||||
@@ -510,12 +547,21 @@ async fn listen_loop(
|
||||
}
|
||||
}
|
||||
|
||||
async fn generate_reply(conv: &Conversation, user_text: String, db: &Option<Arc<Database>>) -> Result<String, String> {
|
||||
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);
|
||||
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
|
||||
@@ -528,28 +574,42 @@ async fn generate_reply_with_tools(
|
||||
) -> 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);
|
||||
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) {
|
||||
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;
|
||||
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,
|
||||
) {
|
||||
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 {
|
||||
@@ -579,31 +639,61 @@ async fn cmd_usage(
|
||||
) {
|
||||
let db = match database {
|
||||
Some(d) => d,
|
||||
None => { println!("数据库未配置,无法查询用量"); return; }
|
||||
None => {
|
||||
println!("数据库未配置,无法查询用量");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let since_dt = since.and_then(|s| {
|
||||
chrono::DateTime::parse_from_rfc3339(&s).ok().map(|d| d.with_timezone(&chrono::Utc))
|
||||
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))
|
||||
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 };
|
||||
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);
|
||||
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),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user