wip: 完成了最初版本,实现了安装并简单解析skill的功能
This commit is contained in:
parent
337158f4b5
commit
2c8205e5cc
|
|
@ -1,2 +1,3 @@
|
|||
/target
|
||||
.idea
|
||||
.cache
|
||||
|
|
@ -181,6 +181,12 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
|
|
@ -265,12 +271,28 @@ dependencies = [
|
|||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indicator"
|
||||
version = "0.4.4"
|
||||
|
|
@ -474,11 +496,34 @@ dependencies = [
|
|||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "ruff_python_ast"
|
||||
|
|
@ -549,6 +594,12 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
|
|
@ -601,6 +652,19 @@ dependencies = [
|
|||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.2"
|
||||
|
|
@ -764,6 +828,12 @@ dependencies = [
|
|||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
|
|
@ -779,10 +849,12 @@ dependencies = [
|
|||
"console",
|
||||
"indicator",
|
||||
"path_abs",
|
||||
"regex",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_parser",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -14,3 +14,5 @@ console = "0.16.3"
|
|||
indicator = "0.4.4"
|
||||
ruff_python_parser = { git = "https://github.com/astral-sh/ruff", rev = "v0.4.0" }
|
||||
ruff_python_ast = { git = "https://github.com/astral-sh/ruff", rev = "v0.4.0" }
|
||||
regex = "1.12.3"
|
||||
serde_yaml = "0.9.34"
|
||||
23
README.md
23
README.md
|
|
@ -1,10 +1,25 @@
|
|||
<div align="center">
|
||||
|
||||
# Viceroy (总督)
|
||||
|
||||
Pretor的插件管理工具
|
||||
|
||||
|
||||
</div>
|
||||
---
|
||||
*"你们搞大模型的就是码奸,你们已经害死前端兄弟了,还要害死后端兄弟,测试兄弟,运维兄弟,害死网安兄弟,害死ic兄弟,最后害死自己害死全人类"*
|
||||
>*"你们搞大模型的就是码奸,你们已经害死前端兄弟了,还要害死后端兄弟,测试兄弟,运维兄弟,害死网安兄弟,害死ic兄弟,最后害死自己害死全人类"*
|
||||
|
||||
viceroy 是一个由rust编写的安装工具,用于pretor的插件管理
|
||||
|
||||
---
|
||||
## 目前支持对象
|
||||
- skill: 安装skill并进行简单的解析到目标文件夹下
|
||||
|
||||
---
|
||||
## 使用方法
|
||||
#### Skill
|
||||
**Skill** 是一个由指令、脚本和资源组成的集合,Agent通过动态加载这些内容,以在特定任务上提升表现。**Skill** 教会 **Agent** 如何以可重复的方式完成特定任务,例如按照公司品牌指南创建文档、使用组织特定的工作流程分析数据,或自动化个人任务。
|
||||
目标仓库:https://github.com/anthropics/skills
|
||||
```Bash
|
||||
./viceroy install (github仓库名) [-p (仓库内SKILL.md所在目录的相对路径)] -o (输出路径)
|
||||
```
|
||||
**viceroy**将在skill根目录下产生 **skill.json** 和 **metadata.json**两个文件。
|
||||
**skill.json**包括SKILL.md的**name**,**description**,**instructions**。
|
||||
**metadata**包含整个skill的文件树和架构和python脚本工具的函数信息。
|
||||
|
|
@ -11,9 +11,7 @@ pub struct GitInstaller{
|
|||
}
|
||||
|
||||
impl GitInstaller {
|
||||
// 1. 增加一个关联函数 new,负责“一键初始化”
|
||||
pub fn new(git_repo_url: &str, root_cache_path: &str) -> Self {
|
||||
// 逻辑:从 URL 提取仓库名(总督的直觉)
|
||||
let repo_name = git_repo_url
|
||||
.split('/')
|
||||
.last()
|
||||
|
|
@ -21,8 +19,6 @@ impl GitInstaller {
|
|||
.trim_end_matches(".git");
|
||||
let mut path_buf = PathBuf::from(root_cache_path);
|
||||
path_buf.push(repo_name);
|
||||
|
||||
// 返回实例
|
||||
Self {
|
||||
git_repo_url: git_repo_url.to_owned(),
|
||||
cache_path: Some(path_buf.display().to_string()),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,30 @@
|
|||
use anyhow::Result;
|
||||
use std::path::Path;
|
||||
use std::fs;
|
||||
|
||||
pub trait Installer {
|
||||
fn download(&self) -> Result<()>;
|
||||
}
|
||||
|
||||
pub fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
|
||||
if !dst.exists() {
|
||||
fs::create_dir_all(dst)?;
|
||||
}
|
||||
for entry in fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let ty = entry.file_type()?;
|
||||
let target = dst.join(entry.file_name());
|
||||
|
||||
// Skip .git directory to avoid unnecessary weight
|
||||
if entry.file_name() == ".git" {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ty.is_dir() {
|
||||
copy_dir_recursive(&entry.path(), &target)?;
|
||||
} else {
|
||||
fs::copy(&entry.path(), &target)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
71
src/main.rs
71
src/main.rs
|
|
@ -1,6 +1,69 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
use viceroy::manifest::skill::analysis::process_and_save_skill;
|
||||
use viceroy::manifest::skill::model::SkillModel;
|
||||
use anyhow::Result;
|
||||
|
||||
|
||||
fn main() {
|
||||
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "viceroy")]
|
||||
#[command(about = "Pretor's plugin management tool", long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Parse a skill directory, extracting SKILL.md and analyzing python files.
|
||||
Parse {
|
||||
/// The path to the skill directory
|
||||
#[arg(short, long, value_name = "DIR")]
|
||||
path: PathBuf,
|
||||
},
|
||||
/// Install a skill from a Git repository and parse it
|
||||
Install {
|
||||
/// The Git repository URL
|
||||
url: String,
|
||||
|
||||
/// Subdirectory path inside the repo (default is root)
|
||||
#[arg(short = 'p', long, default_value = "")]
|
||||
path: String,
|
||||
|
||||
/// Root cache directory to clone into
|
||||
#[arg(short = 'c', long, default_value = ".cache")]
|
||||
cache_dir: String,
|
||||
|
||||
/// Output directory to move the final skill into
|
||||
#[arg(short = 'o', long)]
|
||||
output: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match &cli.command {
|
||||
Commands::Parse { path } => {
|
||||
if !path.exists() || !path.is_dir() {
|
||||
anyhow::bail!("Error: path {:?} does not exist or is not a directory", path);
|
||||
}
|
||||
println!("Parsing skill directory: {:?}", path);
|
||||
process_and_save_skill(path)?;
|
||||
println!("Done.");
|
||||
}
|
||||
Commands::Install { url, path, cache_dir, output } => {
|
||||
let mut final_url = url.clone();
|
||||
if !final_url.starts_with("http://") && !final_url.starts_with("https://") && !final_url.starts_with("git@") {
|
||||
final_url = format!("https://github.com/{}", final_url);
|
||||
}
|
||||
|
||||
println!("Installing skill from {} into {}", final_url, cache_dir);
|
||||
let skill = SkillModel::install(final_url, cache_dir.clone(), path.clone(), output.clone());
|
||||
println!("Analyzing installed skill at {}", skill.skill_path);
|
||||
skill.analysis()?;
|
||||
println!("Done.");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
mod skill;
|
||||
pub mod skill;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,39 @@
|
|||
use crate::manifest::skill::skill_structure_tree::{SkillNode,PythonFuncNode,PythonFileNode};
|
||||
use crate::manifest::skill::skill_structure_tree::{SkillNode, SkillJson, PythonFuncNode, PythonFileNode};
|
||||
use walkdir::WalkDir;
|
||||
use std::path::Path;
|
||||
use std::collections::HashMap;
|
||||
use ruff_python_parser::{parse, Mode};
|
||||
use ruff_python_ast::{Mod, Stmt};
|
||||
use regex::Regex;
|
||||
|
||||
pub fn parse_skill_md(content: &str) -> SkillJson {
|
||||
let mut metadata = SkillJson::default();
|
||||
|
||||
// Pattern to match YAML frontmatter between `---` and `---`
|
||||
let re_frontmatter = Regex::new(r"(?s)^---\s*(.*?)\s*---").unwrap();
|
||||
if let Some(caps) = re_frontmatter.captures(content) {
|
||||
let frontmatter = caps.get(1).map_or("", |m| m.as_str());
|
||||
|
||||
// Parse frontmatter properly using serde_yaml to support multiline values like `|`
|
||||
if let Ok(yaml_data) = serde_yaml::from_str::<serde_json::Value>(frontmatter) {
|
||||
if let Some(name) = yaml_data.get("name").and_then(|v| v.as_str()) {
|
||||
metadata.name = name.to_string();
|
||||
}
|
||||
if let Some(desc) = yaml_data.get("description").and_then(|v| v.as_str()) {
|
||||
metadata.description = desc.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Extract instructions (everything after the frontmatter)
|
||||
let body = re_frontmatter.replace(content, "").trim().to_string();
|
||||
metadata.instructions = body;
|
||||
} else {
|
||||
// No frontmatter found, whole file is instructions
|
||||
metadata.instructions = content.trim().to_string();
|
||||
}
|
||||
|
||||
metadata
|
||||
}
|
||||
|
||||
fn analyze_python_file(code: &str) -> PythonFileNode {
|
||||
let mut func_dict = HashMap::new();
|
||||
|
|
@ -29,25 +59,62 @@ fn analyze_python_file(code: &str) -> PythonFileNode {
|
|||
}
|
||||
}
|
||||
|
||||
fn main_scan_logic(root_path: &Path) {
|
||||
pub fn analyze_skill_directory(root_path: &Path) -> (Option<SkillJson>, SkillNode) {
|
||||
let mut root_node = SkillNode::new_folder("root");
|
||||
let mut skill_metadata = None;
|
||||
|
||||
for entry in WalkDir::new(root_path).into_iter().filter_map(|e| e.ok()) {
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let file_name_str = path.file_name().unwrap().to_string_lossy().to_string();
|
||||
|
||||
// Skip useless files
|
||||
let lower_name = file_name_str.to_lowercase();
|
||||
if lower_name.starts_with("license") || file_name_str.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let rel_path = path.strip_prefix(root_path).unwrap();
|
||||
let segments: Vec<&str> = rel_path.iter().map(|s| s.to_str().unwrap()).collect();
|
||||
let node = if path.extension().and_then(|s| s.to_str()) == Some("py") {
|
||||
|
||||
if file_name_str.to_lowercase() == "skill.md" && segments.len() == 1 {
|
||||
let code = std::fs::read_to_string(path).expect("读取文件失败");
|
||||
skill_metadata = Some(parse_skill_md(&code));
|
||||
// We don't add SKILL.md to the tree since it will be in skill.json
|
||||
continue;
|
||||
}
|
||||
|
||||
let node = if path.extension().and_then(|s| s.to_str()) == Some("py") {
|
||||
let code = std::fs::read_to_string(path).unwrap_or_default();
|
||||
let mut py_node = analyze_python_file(&code);
|
||||
py_node.file_name = path.file_stem().unwrap().to_string_lossy().into();
|
||||
py_node.file_name = file_name_str.clone();
|
||||
SkillNode::Python(py_node)
|
||||
} else {
|
||||
let file_name = path.file_name().unwrap().to_string_lossy().into();
|
||||
SkillNode::File(file_name)
|
||||
SkillNode::File(file_name_str)
|
||||
};
|
||||
root_node.insert_recursive(&segments, node);
|
||||
}
|
||||
}
|
||||
let json = serde_json::to_string_pretty(&root_node).unwrap();
|
||||
println!("{}", json);
|
||||
|
||||
(skill_metadata, root_node)
|
||||
}
|
||||
|
||||
pub fn process_and_save_skill(root_path: &Path) -> anyhow::Result<()> {
|
||||
let (metadata_opt, tree) = analyze_skill_directory(root_path);
|
||||
|
||||
// Save skill.json
|
||||
if let Some(metadata) = metadata_opt {
|
||||
let skill_json_path = root_path.join("skill.json");
|
||||
let skill_json_content = serde_json::to_string_pretty(&metadata)?;
|
||||
std::fs::write(&skill_json_path, skill_json_content)?;
|
||||
println!("Saved {:?}", skill_json_path);
|
||||
}
|
||||
|
||||
// Save metadata.json
|
||||
let metadata_json_path = root_path.join("metadata.json");
|
||||
let tree_json_content = serde_json::to_string_pretty(&tree)?;
|
||||
std::fs::write(&metadata_json_path, tree_json_content)?;
|
||||
println!("Saved {:?}", metadata_json_path);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use crate::manifest::skill::model::SkillModel;
|
|||
use std::path::PathBuf;
|
||||
|
||||
impl SkillModel{
|
||||
fn install(git_repo_url: String, root_cache_path: String, relative_path: String) -> Self{
|
||||
pub fn install(git_repo_url: String, root_cache_path: String, relative_path: String, output_dir: Option<String>) -> Self{
|
||||
let git_installer = git::GitInstaller::new(&git_repo_url, &root_cache_path);
|
||||
if let Err(e) = git_installer.download() {
|
||||
eprintln!("安装失败: {}", e);
|
||||
|
|
@ -17,12 +17,36 @@ impl SkillModel{
|
|||
let mut path_builder = PathBuf::from(&root_cache_path);
|
||||
path_builder.push(git_repo_name);
|
||||
path_builder.push(&relative_path);
|
||||
let skill_path = path_builder.to_string_lossy().to_string();
|
||||
|
||||
let mut final_path = path_builder.to_string_lossy().to_string();
|
||||
|
||||
if let Some(out_dir) = output_dir {
|
||||
// Determine the name of the skill directory to create inside the output directory.
|
||||
// e.g. if relative_path is "skills/skill-creator", skill_dir_name is "skill-creator".
|
||||
// If relative_path is empty, use the repo name.
|
||||
let skill_dir_name = if relative_path.is_empty() {
|
||||
git_repo_name
|
||||
} else {
|
||||
relative_path.split('/').last().unwrap_or(git_repo_name)
|
||||
};
|
||||
|
||||
let target_dst = std::path::Path::new(&out_dir).join(skill_dir_name);
|
||||
|
||||
// Copy the contents to the new target directory
|
||||
if let Err(e) = crate::installer::install::copy_dir_recursive(&path_builder, &target_dst) {
|
||||
eprintln!("复制到目标文件夹失败: {}", e);
|
||||
} else {
|
||||
final_path = target_dst.to_string_lossy().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
Self{
|
||||
skill_path,
|
||||
skill_path: final_path,
|
||||
}
|
||||
}
|
||||
fn analysis(&self){
|
||||
pub fn analysis(&self) -> anyhow::Result<()> {
|
||||
use std::path::Path;
|
||||
use crate::manifest::skill::analysis::process_and_save_skill;
|
||||
process_and_save_skill(Path::new(&self.skill_path))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use std::collections::HashMap;
|
||||
use serde::Serialize;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct PythonFuncNode{
|
||||
|
|
@ -45,3 +45,10 @@ impl SkillNode {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct SkillJson {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub instructions: String,
|
||||
}
|
||||
Loading…
Reference in New Issue