wip: 完成了最初版本,实现了安装并简单解析skill的功能

This commit is contained in:
朝夕 2026-04-15 20:18:35 +08:00
parent 337158f4b5
commit 2c8205e5cc
11 changed files with 302 additions and 30 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
/target
.idea
.cache

72
Cargo.lock generated
View File

@ -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",
]

View File

@ -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"

View File

@ -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脚本工具的函数信息。

View File

@ -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()),

View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -1 +1 @@
mod skill;
pub mod skill;

View File

@ -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(())
}

View File

@ -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();
Self{
skill_path,
}
}
fn analysis(&self){
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: final_path,
}
}
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))
}
}

View File

@ -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,
}