refactor: 删除外置 skills 系统,全部工具转为内置
删除: - resources/skills/ (9个外置 skill, bash/python/node 脚本) - src/tools/registry.rs (SkillRegistry 加载/执行) - src/tools/parser.rs (SKILL.md 解析) - src/skills_importer.rs (skills 导入) - cli.rs Skills 子命令 - main.rs cmd_skills / global_skills_dir 保留: - src/tools/builtin.rs (内置工具注册 + 审批流程) - src/tools/weather.rs (天气查询) - src/tools/skill.rs (精简为 SkillSpec/RiskLevel/SkillResult/ExecutionContext) - src/tools/approval.rs (审批管理器) main.rs 简化: 移除 SkillRegistry 加载, executor 只走 BuiltinRegistry
This commit is contained in:
@@ -1,322 +0,0 @@
|
||||
use super::approval::{ApprovalDecision, ApprovalManager};
|
||||
use crate::tools::parser::parse_skill;
|
||||
use crate::tools::skill::*;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::process::Command;
|
||||
|
||||
/// 技能注册表
|
||||
pub struct SkillRegistry {
|
||||
skills: HashMap<String, Arc<Skill>>,
|
||||
}
|
||||
|
||||
impl SkillRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
skills: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 system_dir 和 user_dir 加载所有技能
|
||||
pub async fn load(system_dir: &str, user_dir: &str) -> (Self, Vec<String>) {
|
||||
let mut registry = Self::new();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
// 加载系统 skills
|
||||
if Path::new(system_dir).exists() {
|
||||
registry.load_from_dir(system_dir, SkillSource::System, &mut errors);
|
||||
}
|
||||
|
||||
// 加载全局 skills (~/.ias/skills/)
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
let global_dir = format!("{}/.ias/skills", home);
|
||||
if Path::new(&global_dir).exists() {
|
||||
registry.load_from_dir(&global_dir, SkillSource::User, &mut errors);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载用户 skills(项目目录)
|
||||
if Path::new(user_dir).exists() {
|
||||
registry.load_from_dir(user_dir, SkillSource::User, &mut errors);
|
||||
}
|
||||
|
||||
(registry, errors)
|
||||
}
|
||||
|
||||
fn load_from_dir(&mut self, dir: &str, source: SkillSource, errors: &mut Vec<String>) {
|
||||
let dir_path = Path::new(dir);
|
||||
let entries = match std::fs::read_dir(dir_path) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
errors.push(format!("读取目录 {} 失败: {}", dir, e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let skill_md = path.join("SKILL.md");
|
||||
if !skill_md.exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match parse_skill(&path) {
|
||||
Ok(mut skill) => {
|
||||
skill.source = source.clone();
|
||||
let name = skill.spec.name.clone();
|
||||
|
||||
// user 同名覆盖 system
|
||||
if source == SkillSource::User && self.skills.contains_key(&name) {
|
||||
tracing::info!("用户技能覆盖系统技能: {}", name);
|
||||
}
|
||||
self.skills.insert(name, Arc::new(skill));
|
||||
}
|
||||
Err(e) => {
|
||||
errors.push(format!("{}: {}", path.display(), e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 列出所有技能 specs(给 LLM function calling)
|
||||
pub fn list_specs(&self) -> Vec<&SkillSpec> {
|
||||
let mut specs: Vec<&SkillSpec> = self.skills.values().map(|s| &s.spec).collect();
|
||||
specs.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
specs
|
||||
}
|
||||
|
||||
/// 按风险等级过滤
|
||||
pub fn list_specs_by_risk(&self, level: RiskLevel) -> Vec<&SkillSpec> {
|
||||
self.skills
|
||||
.values()
|
||||
.filter(|s| s.spec.risk_level == level)
|
||||
.map(|s| &s.spec)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 按名称查找技能
|
||||
pub fn get(&self, name: &str) -> Option<&Skill> {
|
||||
self.skills.get(name).map(|s| s.as_ref())
|
||||
}
|
||||
|
||||
/// 获取技能数量
|
||||
pub fn count(&self) -> usize {
|
||||
self.skills.len()
|
||||
}
|
||||
|
||||
/// 动态注册一个技能(运行时添加,不持久化到文件)
|
||||
pub fn register_skill(&mut self, spec: SkillSpec, dir: std::path::PathBuf, execute_command: String) {
|
||||
let skill = Arc::new(Skill::new(spec, SkillSource::User, dir, execute_command));
|
||||
let name = skill.spec.name.clone();
|
||||
self.skills.insert(name, skill);
|
||||
}
|
||||
|
||||
// ─── 执行 ───
|
||||
|
||||
/// 执行技能(含审批流程)
|
||||
pub async fn execute(
|
||||
&self,
|
||||
name: &str,
|
||||
params: serde_json::Value,
|
||||
ctx: &ExecutionContext,
|
||||
) -> SkillResult {
|
||||
let skill = match self.skills.get(name) {
|
||||
Some(s) => s.clone(),
|
||||
None => {
|
||||
return SkillResult::error(format!("技能不存在: {}", name));
|
||||
}
|
||||
};
|
||||
|
||||
// High 风险 → 审批流程(透明模式)
|
||||
if skill.spec.risk_level.is_high() {
|
||||
match approval_flow(&skill, ctx).await {
|
||||
ApprovalResult::Approved => {
|
||||
// 继续执行
|
||||
}
|
||||
ApprovalResult::Rejected => {
|
||||
return SkillResult::rejected(name);
|
||||
}
|
||||
ApprovalResult::Expired => {
|
||||
return SkillResult::expired(name);
|
||||
}
|
||||
ApprovalResult::Error(msg) => {
|
||||
return SkillResult::error(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 执行技能
|
||||
run_skill(&skill, params, ctx).await
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 审批流程 ───
|
||||
|
||||
enum ApprovalResult {
|
||||
Approved,
|
||||
Rejected,
|
||||
Expired,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
async fn approval_flow(skill: &Skill, ctx: &ExecutionContext) -> ApprovalResult {
|
||||
let manager = match ctx.approval_manager.as_ref() {
|
||||
Some(m) => m.clone(),
|
||||
None => return ApprovalResult::Approved, // 测试模式
|
||||
};
|
||||
|
||||
// 创建审批请求(生成确认码 + 注册通道)
|
||||
let (code, rx) = match manager.create(&ctx.user_id, &skill.spec.name).await {
|
||||
Ok(pair) => pair,
|
||||
Err(e) => return ApprovalResult::Error(e),
|
||||
};
|
||||
tracing::debug!("审批确认码已生成: {}...", &code[..2]);
|
||||
|
||||
// 发送确认消息给用户
|
||||
let msg = format!(
|
||||
"⚠️ 操作确认\n\n技能:{}\n\n确认码:{}\n\n回复确认码以继续\n回复 0 或「取消」以取消\n(5分钟内有效,最多3次尝试)",
|
||||
skill.spec.name, code
|
||||
);
|
||||
if let Some(ref send) = ctx.send_wechat {
|
||||
if let Err(e) = send(&ctx.user_id, &msg).await {
|
||||
return ApprovalResult::Error(format!("发送确认消息失败: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
// 等待用户确认(通过 oneshot channel,由消息循环驱动)
|
||||
match tokio::time::timeout(Duration::from_secs(300), rx).await {
|
||||
Ok(Ok(ApprovalDecision::Approved)) => ApprovalResult::Approved,
|
||||
Ok(Ok(ApprovalDecision::Rejected)) => ApprovalResult::Rejected,
|
||||
Ok(Ok(ApprovalDecision::Expired)) => ApprovalResult::Expired,
|
||||
Ok(Err(_)) => ApprovalResult::Error("审批通道异常".to_string()),
|
||||
Err(_) => ApprovalResult::Expired,
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 技能执行器 ───
|
||||
|
||||
async fn run_skill(skill: &Skill, params: serde_json::Value, _ctx: &ExecutionContext) -> SkillResult {
|
||||
let timeout = Duration::from_secs(skill.spec.timeout_secs);
|
||||
|
||||
let cmd_str = &skill.execute_command;
|
||||
let parts: Vec<&str> = cmd_str.split_whitespace().collect();
|
||||
if parts.is_empty() {
|
||||
return SkillResult::error("执行命令为空");
|
||||
}
|
||||
|
||||
let mut cmd = Command::new(parts[0]);
|
||||
for arg in &parts[1..] {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
cmd.current_dir(&skill.dir);
|
||||
tracing::debug!(
|
||||
"执行命令: {:?} (cwd={:?})",
|
||||
&cmd_str, &skill.dir
|
||||
);
|
||||
|
||||
// 通过 stdin 传入参数
|
||||
let params_json = serde_json::to_string(¶ms).unwrap_or_default();
|
||||
cmd.stdin(std::process::Stdio::piped());
|
||||
cmd.stdout(std::process::Stdio::piped());
|
||||
cmd.stderr(std::process::Stdio::piped());
|
||||
cmd.kill_on_drop(true);
|
||||
|
||||
let mut child = match cmd.spawn() {
|
||||
Ok(c) => c,
|
||||
Err(e) => return SkillResult::error(format!("启动进程失败: {}", e)),
|
||||
};
|
||||
|
||||
// 写入参数到 stdin
|
||||
if let Some(mut stdin) = child.stdin.take() {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let _ = stdin.write_all(params_json.as_bytes()).await;
|
||||
// 关闭 stdin 让进程知道输入结束
|
||||
drop(stdin);
|
||||
}
|
||||
|
||||
// 等待完成(带超时)
|
||||
let result = tokio::time::timeout(timeout, child.wait_with_output()).await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(output)) => {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
let exit_code = output.status.code().unwrap_or(-1);
|
||||
|
||||
tracing::debug!(
|
||||
"🖥️ {} exit={} out={:.200} err={:.100}",
|
||||
skill.spec.name, exit_code,
|
||||
if stdout.is_empty() { "(空)" } else { &stdout },
|
||||
if stderr.is_empty() { "(空)" } else { &stderr }
|
||||
);
|
||||
|
||||
if output.status.success() {
|
||||
// 尝试解析为 JSON
|
||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&stdout) {
|
||||
SkillResult::ok_with_data(stdout, json)
|
||||
} else {
|
||||
SkillResult::ok(stdout)
|
||||
}
|
||||
} else {
|
||||
let msg = if !stderr.is_empty() {
|
||||
format!("退出码 {}: {}", output.status.code().unwrap_or(-1), stderr)
|
||||
} else if !stdout.is_empty() {
|
||||
format!("退出码 {}: {}", output.status.code().unwrap_or(-1), stdout)
|
||||
} else {
|
||||
format!("退出码 {}", output.status.code().unwrap_or(-1))
|
||||
};
|
||||
SkillResult::error(msg)
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => SkillResult::error(format!("进程错误: {}", e)),
|
||||
Err(_) => SkillResult::error(format!("执行超时 ({}s)", skill.spec.timeout_secs)),
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 执行上下文 ───
|
||||
|
||||
/// 发送微信消息的回调: (user_id, text) -> Result(异步)
|
||||
pub type WechatSender =
|
||||
Arc<dyn Fn(&str, &str) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send>> + Send + Sync>;
|
||||
|
||||
pub struct ExecutionContext {
|
||||
pub user_id: String,
|
||||
pub db: Option<Arc<sqlx::PgPool>>,
|
||||
/// 审批管理器
|
||||
pub approval_manager: Option<Arc<ApprovalManager>>,
|
||||
/// 发送微信消息的回调
|
||||
pub send_wechat: Option<WechatSender>,
|
||||
}
|
||||
|
||||
impl ExecutionContext {
|
||||
pub fn new(user_id: impl Into<String>) -> Self {
|
||||
Self {
|
||||
user_id: user_id.into(),
|
||||
db: None,
|
||||
approval_manager: None,
|
||||
send_wechat: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_db(mut self, db: Arc<sqlx::PgPool>) -> Self {
|
||||
self.db = Some(db);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_approval(mut self, mgr: Arc<ApprovalManager>) -> Self {
|
||||
self.approval_manager = Some(mgr);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_wechat(mut self, send: WechatSender) -> Self {
|
||||
self.send_wechat = Some(send);
|
||||
self
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user