feat: bundle desktop skills

This commit is contained in:
boos
2026-06-26 14:28:13 +08:00
parent 424aaa9b1f
commit cd08ac931d
18 changed files with 719 additions and 17 deletions
+1
View File
@@ -1184,6 +1184,7 @@ dependencies = [
"sea-orm",
"serde",
"serde_json",
"serde_yaml",
"tauri",
"tauri-build",
"tauri-plugin-autostart",
+1
View File
@@ -47,6 +47,7 @@ tauri-plugin-process = "2"
tauri-plugin-updater = "2"
serde = { workspace = true }
serde_json = { workspace = true }
serde_yaml = "0.9"
boosapi-core = { path = "crates/core" }
boosapi-providers = { path = "crates/providers" }
boosapi-agent = { path = "crates/agent" }
@@ -0,0 +1,32 @@
---
name: cad-ai-agent
description: CAD/DXF drawing intelligence workflow for summaries, compliance checks, quantity takeoffs, RFIs, health reports, revision comparison, and safe structured edit planning.
---
# CAD AI Agent Skill
Use this skill when a user asks for CAD/DXF drawing intelligence: summaries, compliance checks, quantity takeoffs, RFIs, health reports, revision comparison, or safe edit planning.
Upstream project: https://github.com/jeremylongshore/cad-ai-agent
## Setup
Clone and install the upstream project when the user wants to run the full local pipeline:
```bash
git clone https://github.com/jeremylongshore/cad-ai-agent.git
cd cad-ai-agent
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```
## Workflow
1. Identify the drawing format: DXF is directly supported by the upstream project; DWG/PDF may need conversion or a different inspection pipeline.
2. Ask what the user wants: summarize, edit, compare, query, generate documentation, compliance, coordination, estimation, or quality.
3. Preserve originals. Prefer save-as outputs and structured reports.
4. For edits, produce structured operation plans rather than modifying raw CAD data directly.
5. For analysis, return layer/entity summaries, issues, measurements, and concrete next actions.
This bundled skill is a BoosAPI Desktop wrapper around the upstream project and keeps CAD operations explicit and reversible.
@@ -0,0 +1,35 @@
---
name: dwg-mcp-server
description: Inspect AutoCAD DWG files through DWG MCP Server. Use when users ask to open, inspect, query, summarize, or extract objects/properties from DWG drawings.
---
# DWG MCP Server Skill
Use this skill when a user wants to inspect AutoCAD DWG drawings with an AI agent.
Upstream project: https://github.com/dimitrovakulenko/dwg-mcp-server
## Setup
Install the MCP server in your MCP client or run it with npx:
```bash
npx -y @dmytro-prototypes/dwg-mcp-server
```
For clients without MCP roots, configure explicit allowed roots:
```bash
DWG_MCP_ALLOWED_ROOTS="$HOME" npx -y @dmytro-prototypes/dwg-mcp-server
```
## Workflow
1. Ask the user for the DWG file path and confirm it is under an allowed root.
2. Use `dwg.list_roots` to inspect accessible folders.
3. Use `dwg.open_file` to open the drawing and get a `documentId`.
4. Use `dwg.list_file_types`, `dwg.describe_type`, `dwg.get_objects`, and `dwg.query_objects` to inspect contents.
5. Summarize entities, layers, blocks, layouts, references, and notable properties.
6. Close the document with `dwg.close_file` when done.
This bundled skill is a BoosAPI Desktop wrapper around the upstream MCP server. It does not edit DWG files directly.
@@ -0,0 +1,20 @@
---
name: gorden-ppt-skill
description: 中文 PPT 生成与编辑 Skill,支持工作汇报、年终总结、述职竞聘、项目复盘、商务提案、教学课件、模板套用、版式保持和 PowerPoint 成品导出。
---
# Gorden PPT Skill
Use this skill when the user wants to create or edit a PowerPoint / PPT / slides / `.pptx` deck.
Upstream full skill: https://github.com/GordenSun/GordenPPTSkill
## Workflow
1. Clarify deck purpose, audience, language, page count, format, and whether the user has a template.
2. If using a template, preserve layout, typography, colors, and spacing; replace content without damaging the design.
3. If creating from scratch, propose a concise structure first: cover, agenda, context, key points, data pages, conclusion, and appendix as needed.
4. Keep slide text short and presentation-ready.
5. Check text overflow, same-level heading consistency, and visual hierarchy before final export.
Install `GordenSun/GordenPPTSkill` from the Skills marketplace for the full template library and update scripts.
@@ -0,0 +1,26 @@
---
name: jianying-editor
description: 剪映 AI 自动化剪辑 Skill,支持草稿生成、素材导入、字幕、配音、特效、转场、录屏和导出规划。Use when the user wants to automate JianYing/CapCut editing, create a draft, assemble clips, add captions, voiceover, music, transitions, or export a video.
---
# JianYing Editor Skill
Use this skill when the user wants to automate video editing with JianYing/CapCut.
Upstream full skill: https://github.com/luoluoluo22/jianying-editor-skill
## Workflow
1. Clarify the target: platform, duration, aspect ratio, source assets, narration, subtitle style, music, and export format.
2. Build an edit plan before scripting: timeline structure, clips, captions, voiceover, music, effects, transitions, and export path.
3. Keep generated scripts in the user's project/workspace, not inside the skill directory.
4. Preserve original media. Generate a new draft/project for each requested edit.
5. When the full upstream skill is installed, follow its detailed `rules/` and `docs/agent-playbook.md`.
## Install Full Upstream Version
```bash
git clone https://github.com/luoluoluo22/jianying-editor-skill.git
```
In BoosAPI Desktop, install `luoluoluo22/jianying-editor-skill` from the Skills marketplace to replace this bundled quick-start with the full implementation.
@@ -0,0 +1,30 @@
---
name: seedance-prompt-en
description: English Seedance 2.0 multimodal AI video prompt guide. Use for text/image/video/audio reference prompts with @Image, @Video, and @Audio syntax.
---
# Seedance 2.0 English Prompt Skill
Use this skill when the user wants English prompts for Jimeng / Seedance 2.0 multimodal AI video generation.
Upstream full skill: https://github.com/dexhunter/seedance2-skill
## Reference Syntax
Use explicit reference roles:
- `@Image1 as the first frame`
- `@Image1's character as the subject`
- `scene references @Image2`
- `reference @Video1's camera movement`
- `BGM references @Audio1`
## Prompt Blueprint
```text
[Subject setup] + [Scene/environment] + [Action/motion] + [Camera movement] + [Style/lighting] + [Audio/sound] + [Constraints]
```
Keep reference-heavy prompts precise. Say what each uploaded asset controls and what the model may invent.
Install `dexhunter/seedance2-skill` from the Skills marketplace for the full guide.
@@ -0,0 +1,29 @@
---
name: seedance
description: 中文 Seedance 2.0 / 即梦视频与图片提示词工程,支持 @素材引用、分镜、运镜、首帧图、图生视频、短剧、广告视频和长视频流水线。
---
# Seedance 2.0 中文提示词 Skill
Use this skill when the user asks for 即梦 / Seedance / Dreamina video prompts, image-to-video prompts, storyboard prompts, first-frame image prompts, camera motion, short video ads, short drama clips, or multimodal reference prompts.
Upstream full skill: https://github.com/MapleShaw/seedance2.0-prompt-skill
## Core Rules
1. Write prompts in Chinese unless the user asks otherwise.
2. Use official reference syntax: `@图片1`-`@图片9`, `@视频1`-`@视频3`, `@音频1`-`@音频3`.
3. For uploaded images, explain the role of each reference: first frame, character, scene, product, action, camera, rhythm, or style.
4. For short clips under 15 seconds, focus on one impossible or highly attractive moment with a strong first two seconds.
5. For longer videos, split into character reference, storyboard, key frames, per-shot prompts, and editing instructions.
6. Avoid realistic identifiable human faces in uploaded references unless the platform and user confirm compliance.
## Prompt Shape
```text
[主体/角色] + [场景/光影/质感] + [动作节奏] + [运镜] + [镜头/景别] + [声音/情绪] + [硬约束]
```
For image-to-video, keep the text concise and only add what the image does not already specify.
Install `MapleShaw/seedance2.0-prompt-skill` from the Skills marketplace for the full reference library.
+244 -5
View File
@@ -4,6 +4,73 @@ use boosapi_core::types::*;
use std::path::{Path, PathBuf};
use tauri::State;
struct BuiltinMarketplaceSkill {
name: &'static str,
description: &'static str,
repo: &'static str,
stars: i64,
installs: i64,
keywords: &'static [&'static str],
fallback_skill_name: Option<&'static str>,
}
const BUILTIN_MARKETPLACE_SKILLS: &[BuiltinMarketplaceSkill] = &[
BuiltinMarketplaceSkill {
name: "jianying-editor",
description: "剪映 AI 自动化剪辑 Skill,支持草稿生成、素材导入、字幕、配音、特效和导出。",
repo: "luoluoluo22/jianying-editor-skill",
stars: 0,
installs: 0,
keywords: &["jianying", "剪映", "capcut", "video", "editing", "视频", "剪辑", "boosapi"],
fallback_skill_name: None,
},
BuiltinMarketplaceSkill {
name: "seedance",
description: "中文 Seedance 2.0 / 即梦视频与图片提示词工程,支持 @素材引用、分镜、运镜和长视频流水线。",
repo: "MapleShaw/seedance2.0-prompt-skill",
stars: 0,
installs: 0,
keywords: &["seedance", "即梦", "jimeng", "video", "prompt", "视频", "提示词", "boosapi"],
fallback_skill_name: None,
},
BuiltinMarketplaceSkill {
name: "seedance-prompt-en",
description: "English Seedance 2.0 multimodal AI video prompt writing guide with @Image/@Video/@Audio reference syntax.",
repo: "dexhunter/seedance2-skill",
stars: 0,
installs: 0,
keywords: &["seedance", "jimeng", "dreamina", "video", "prompt", "english", "boosapi"],
fallback_skill_name: None,
},
BuiltinMarketplaceSkill {
name: "dwg-mcp-server",
description: "DWG MCP Server 包装技能:指导 Agent 安装并使用 dwg-mcp 检查 AutoCAD DWG 文件。",
repo: "dimitrovakulenko/dwg-mcp-server",
stars: 0,
installs: 0,
keywords: &["dwg", "cad", "autocad", "mcp", "drawing", "图纸", "建筑", "boosapi"],
fallback_skill_name: Some("dwg-mcp-server"),
},
BuiltinMarketplaceSkill {
name: "cad-ai-agent",
description: "CAD/DXF Agent 包装技能:用于 DXF/DWG/PDF 图纸摘要、合规检查、工程量统计、RFI 和安全编辑流程。",
repo: "jeremylongshore/cad-ai-agent",
stars: 0,
installs: 0,
keywords: &["cad", "dxf", "dwg", "rfi", "takeoff", "compliance", "drawing", "图纸", "boosapi"],
fallback_skill_name: Some("cad-ai-agent"),
},
BuiltinMarketplaceSkill {
name: "gorden-ppt-skill",
description: "中文 PPT 生成与编辑 Skill,内置模板、版式保持、出框检测和 PowerPoint 成品导出流程。",
repo: "GordenSun/GordenPPTSkill",
stars: 0,
installs: 0,
keywords: &["ppt", "powerpoint", "slides", "演示", "幻灯片", "汇报", "boosapi"],
fallback_skill_name: None,
},
];
fn home_dir() -> PathBuf {
dirs::home_dir().expect("Could not determine home directory")
}
@@ -12,8 +79,79 @@ fn skills_dir() -> PathBuf {
boosapi_home().join("skills")
}
struct BundledSkill {
name: &'static str,
source_ref: &'static str,
body: &'static str,
}
const BUNDLED_SKILLS: &[BundledSkill] = &[
BundledSkill {
name: "jianying-editor",
source_ref: "luoluoluo22/jianying-editor-skill",
body: include_str!("../../builtin-skills/jianying-editor/SKILL.md"),
},
BundledSkill {
name: "seedance",
source_ref: "MapleShaw/seedance2.0-prompt-skill",
body: include_str!("../../builtin-skills/seedance/SKILL.md"),
},
BundledSkill {
name: "seedance-prompt-en",
source_ref: "dexhunter/seedance2-skill",
body: include_str!("../../builtin-skills/seedance-prompt-en/SKILL.md"),
},
BundledSkill {
name: "dwg-mcp-server",
source_ref: "dimitrovakulenko/dwg-mcp-server",
body: include_str!("../../builtin-skills/dwg-mcp-server/SKILL.md"),
},
BundledSkill {
name: "cad-ai-agent",
source_ref: "jeremylongshore/cad-ai-agent",
body: include_str!("../../builtin-skills/cad-ai-agent/SKILL.md"),
},
BundledSkill {
name: "gorden-ppt-skill",
source_ref: "GordenSun/GordenPPTSkill",
body: include_str!("../../builtin-skills/gorden-ppt-skill/SKILL.md"),
},
];
pub fn seed_builtin_skills() -> Result<(), String> {
let target_root = skills_dir();
std::fs::create_dir_all(&target_root).map_err(|e| e.to_string())?;
for skill in BUNDLED_SKILLS {
let skill_dir = target_root.join(skill.name);
let skill_file = skill_dir.join("SKILL.md");
if skill_file.exists() {
continue;
}
std::fs::create_dir_all(&skill_dir).map_err(|e| e.to_string())?;
std::fs::write(&skill_file, skill.body).map_err(|e| e.to_string())?;
let manifest = serde_json::json!({
"source_kind": "builtin",
"source_ref": skill.source_ref,
"installed_at": chrono::Utc::now().to_rfc3339(),
"installed_via": "boosapi-desktop"
});
std::fs::write(
skill_dir.join("skill-manifest.json"),
serde_json::to_string_pretty(&manifest).unwrap(),
)
.map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
pub async fn list_skills(state: State<'_, AppState>) -> Result<Vec<SkillInfo>, String> {
seed_builtin_skills()?;
let home = home_dir();
let skills = open_agent_sdk::skills::load_all_global(&home);
let disabled = boosapi_core::repo::skill::get_disabled_skills(&state.sea_db)
@@ -220,6 +358,8 @@ async fn install_from_github(owner: &str, repo: &str, target_dir: &Path) -> Resu
}
copy_dir_recursive(&extracted, &skill_target)?;
let installed_name =
ensure_skill_entrypoint(&skill_target, owner, repo).unwrap_or_else(|| repo.to_string());
let manifest = serde_json::json!({
"source_kind": "github",
@@ -236,7 +376,45 @@ async fn install_from_github(owner: &str, repo: &str, target_dir: &Path) -> Resu
)
.map_err(|e| e.to_string())?;
Ok(repo.to_string())
Ok(installed_name)
}
fn ensure_skill_entrypoint(skill_target: &Path, owner: &str, repo: &str) -> Option<String> {
let existing_skill = skill_target.join("SKILL.md");
if existing_skill.exists() {
return read_skill_name(&existing_skill).or_else(|| Some(repo.to_string()));
}
let source_ref = format!("{}/{}", owner, repo);
let builtin = BUILTIN_MARKETPLACE_SKILLS
.iter()
.find(|skill| skill.repo.eq_ignore_ascii_case(&source_ref))?;
let name = builtin.fallback_skill_name?;
let body = BUNDLED_SKILLS
.iter()
.find(|skill| skill.name == name)
.map(|skill| skill.body)?;
if std::fs::write(&existing_skill, body).is_ok() {
Some(name.to_string())
} else {
None
}
}
fn read_skill_name(skill_file: &Path) -> Option<String> {
let text = std::fs::read_to_string(skill_file).ok()?;
let trimmed = text.trim_start();
if !trimmed.starts_with("---") {
return None;
}
let rest = trimmed[3..].trim_start_matches(['\r', '\n']);
let end_idx = rest.find("\n---")?;
let yaml = &rest[..end_idx];
let value: serde_yaml::Value = serde_yaml::from_str(yaml).ok()?;
value
.get("name")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
}
async fn install_from_local(source: &str, target_dir: &Path) -> Result<String, String> {
@@ -407,12 +585,14 @@ pub async fn search_marketplace(
source: Option<String>,
) -> Result<Vec<MarketplaceSkill>, String> {
let installed_refs = installed_source_refs();
let query = query.trim();
let builtin_results = builtin_marketplace_results(query, &installed_refs);
match source.as_deref().unwrap_or("skills.sh") {
"github" => {
let url = format!(
"https://api.github.com/search/repositories?q={}+topic:agent-skill&sort=stars&per_page=20",
urlencoding::encode(&query)
urlencoding::encode(query)
);
let client = reqwest::Client::new();
@@ -431,7 +611,7 @@ pub async fn search_marketplace(
let body: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
let items = body["items"].as_array().cloned().unwrap_or_default();
let results: Vec<MarketplaceSkill> = items
let mut results: Vec<MarketplaceSkill> = items
.into_iter()
.map(|item| {
let skill_name = item["name"].as_str().unwrap_or("").to_string();
@@ -449,12 +629,13 @@ pub async fn search_marketplace(
})
.collect();
merge_marketplace_results(&mut results, builtin_results);
Ok(results)
}
_ => {
let url = format!(
"https://skills.sh/api/search?q={}",
urlencoding::encode(&query)
urlencoding::encode(query)
);
let client = reqwest::Client::new();
@@ -472,7 +653,7 @@ pub async fn search_marketplace(
let body: serde_json::Value = response.json().await.map_err(|e| e.to_string())?;
let items = body["skills"].as_array().cloned().unwrap_or_default();
let results: Vec<MarketplaceSkill> = items
let mut results: Vec<MarketplaceSkill> = items
.into_iter()
.map(|item| {
let skill_name = item["name"].as_str().unwrap_or("").to_string();
@@ -490,11 +671,69 @@ pub async fn search_marketplace(
})
.collect();
merge_marketplace_results(&mut results, builtin_results);
Ok(results)
}
}
}
fn builtin_marketplace_results(
query: &str,
installed_refs: &std::collections::HashSet<String>,
) -> Vec<MarketplaceSkill> {
let normalized_query = query.to_lowercase();
let tokens: Vec<&str> = normalized_query
.split(|ch: char| ch.is_whitespace() || ch == '/' || ch == '-' || ch == '_' || ch == '.')
.filter(|token| !token.is_empty())
.collect();
BUILTIN_MARKETPLACE_SKILLS
.iter()
.filter(|skill| {
if tokens.is_empty() {
return true;
}
let haystack = format!(
"{} {} {} {}",
skill.name,
skill.description,
skill.repo,
skill.keywords.join(" ")
)
.to_lowercase();
tokens.iter().all(|token| haystack.contains(token))
})
.map(|skill| MarketplaceSkill {
name: skill.name.to_string(),
description: skill.description.to_string(),
repo: skill.repo.to_string(),
stars: skill.stars,
installs: skill.installs,
installed: installed_refs.contains(&skill.repo.to_lowercase()),
})
.collect()
}
fn merge_marketplace_results(
results: &mut Vec<MarketplaceSkill>,
builtin_results: Vec<MarketplaceSkill>,
) {
for builtin in builtin_results.into_iter().rev() {
if let Some(existing) = results
.iter_mut()
.find(|item| item.repo.eq_ignore_ascii_case(&builtin.repo))
{
if existing.description.is_empty() {
existing.description = builtin.description;
}
existing.name = builtin.name;
existing.installed = existing.installed || builtin.installed;
} else {
results.insert(0, builtin);
}
}
}
#[tauri::command]
pub async fn check_skill_updates() -> Result<Vec<SkillUpdateInfo>, String> {
let skills_path = skills_dir();
+2
View File
@@ -520,6 +520,8 @@ pub fn run() {
version = %app.package_info().version,
"BoosApi setup started"
);
commands::skills::seed_builtin_skills().expect("failed to seed built-in skills");
tracing::info!("BoosApi built-in skills seeded");
// Ensure ~/Documents/boosapi/{images,files,backups}/ exist
boosapi_core::storage_paths::ensure_documents_dirs()