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:
2026-06-01 21:08:38 +08:00
parent a31afaba80
commit f51b5c100f
7 changed files with 342 additions and 81 deletions
+148 -58
View File
@@ -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),
}