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:
2026-06-03 10:28:28 +08:00
parent afc43ce692
commit 4d149982c5
29 changed files with 354 additions and 1651 deletions
+107 -31
View File
@@ -90,6 +90,7 @@ struct CityLookupResponse {
}
#[derive(Deserialize)]
#[allow(dead_code)]
struct CityLocation {
id: String,
name: String,
@@ -246,13 +247,10 @@ impl QWeatherClient {
return Ok(id);
}
let url = format!(
"https://geoapi.qweather.com/v2/city/lookup?location={}&number=1",
location
);
let resp: CityLookupResponse = self
.http
.get(&url)
.get("https://geoapi.qweather.com/v2/city/lookup")
.query(&[("location", location), ("number", "1")])
.header("Authorization", format!("Bearer {}", self.jwt))
.send()
.await
@@ -279,13 +277,10 @@ impl QWeatherClient {
/// 实时天气
async fn weather_now(&self, city_id: &str) -> Result<serde_json::Value, String> {
let url = format!(
"https://{}/v7/weather/now?location={}&lang=zh&unit=m",
self.api_host, city_id
);
let resp: WeatherNowResponse = self
.http
.get(&url)
.get(format!("https://{}/v7/weather/now", self.api_host))
.query(&[("location", city_id), ("lang", "zh"), ("unit", "m")])
.header("Authorization", format!("Bearer {}", self.jwt))
.send()
.await
@@ -328,13 +323,10 @@ impl QWeatherClient {
_ => "30d",
};
let url = format!(
"https://{}/v7/weather/{}?location={}&lang=zh&unit=m",
self.api_host, d, city_id
);
let resp: DailyForecastResponse = self
.http
.get(&url)
.get(format!("https://{}/v7/weather/{}", self.api_host, d))
.query(&[("location", city_id), ("lang", "zh"), ("unit", "m")])
.header("Authorization", format!("Bearer {}", self.jwt))
.send()
.await
@@ -381,13 +373,10 @@ impl QWeatherClient {
_ => "168h",
};
let url = format!(
"https://{}/v7/weather/{}?location={}&lang=zh&unit=m",
self.api_host, h, city_id
);
let resp: HourlyForecastResponse = self
.http
.get(&url)
.get(format!("https://{}/v7/weather/{}", self.api_host, h))
.query(&[("location", city_id), ("lang", "zh"), ("unit", "m")])
.header("Authorization", format!("Bearer {}", self.jwt))
.send()
.await
@@ -448,12 +437,6 @@ fn extract_params(params: &serde_json::Value) -> (&str, u32, u32) {
let location = params
.get("location")
.and_then(|v| v.as_str())
.or_else(|| params.get("prompt").and_then(|v| v.as_str()).and_then(|s| {
// call_capability 可能用 prompt 透传,尝试从中提取城市名
// 简单匹配:"北京"、"上海" 等中文名或拼音
s.split(|c: char| c == '"' || c == '\'' || c == ',' || c == '')
.find(|w| w.chars().any(|c| c as u32 > 127))
}))
.unwrap_or("北京");
let days = params
@@ -471,8 +454,75 @@ fn extract_params(params: &serde_json::Value) -> (&str, u32, u32) {
(location, days.max(1), hours)
}
/// 执行天气查询
#[allow(dead_code)]
fn extract_days_from_prompt(prompt: &str) -> u32 {
// 匹配模式: "7天" "3日" "未来7" "7d" "7D"
for pat in &["", "", "d", "D"] {
if let Some(pos) = prompt.find(pat) {
// 向前找数字
let before = &prompt[..pos];
if let Some(num_str) = before
.chars()
.rev()
.take_while(|c| c.is_ascii_digit())
.collect::<String>()
.chars()
.rev()
.collect::<String>()
.into()
{
if let Ok(n) = num_str.parse::<u32>() {
return n.min(30).max(1);
}
}
}
}
3 // 默认
}
#[allow(dead_code)]
fn extract_city_from_prompt(prompt: &str) -> &str {
// 去掉常见前缀,取第一个2-3字中文词。GeoAPI 支持模糊搜索,
// 即使多取一两个字也能正确匹配到城市
let chars: Vec<char> = prompt.chars().collect();
let byte_pos = |idx: usize| -> usize { chars[..idx].iter().map(|c| c.len_utf8()).sum() };
// 跳过前缀
let mut i = 0;
for prefix in &["帮我查一下", "帮我看看", "帮我查", "帮我", "查一下", "查询", "看看"] {
if prompt.starts_with(prefix) {
i = prefix.chars().count();
while i < chars.len() && chars[i] as u32 <= 0x4e00 { i += 1; }
break;
}
}
// 跳过非中文
while i < chars.len() && chars[i] as u32 <= 0x4e00 { i += 1; }
// 取至多4个连续中文
let start = i;
while i < chars.len() && chars[i] as u32 > 0x4e00 && i - start < 4 { i += 1; }
if i - start >= 2 {
&prompt[byte_pos(start)..byte_pos(i)]
} else {
"北京"
}
}
pub async fn execute(params: serde_json::Value) -> SkillResult {
// call_capability 透传但没有 structured 参数 → 告知正确格式
if params.get("location").is_none()
&& params.get("name").and_then(|v| v.as_str()) == Some("query_weather")
{
let spec = spec();
return SkillResult::error(format!(
"参数格式错误。请将 query_weather 所需的参数直接放在 call_capability 的 prompt 字段中,格式:\
prompt: {{\"location\": \"城市名\", \"days\": 3, \"hours\": 0}}\
工具说明:{}。参数 schema{}",
spec.description,
serde_json::to_string(&spec.parameters).unwrap_or_default()
));
}
let (location, days, hours) = extract_params(&params);
tracing::info!("🌤️ 查询天气: location={}, days={}, hours={}", location, days, hours);
@@ -574,9 +624,35 @@ mod tests {
}
#[test]
fn test_spec() {
let s = spec();
assert_eq!(s.name, "query_weather");
assert_eq!(s.risk_level, RiskLevel::Low);
fn test_extract_city_from_prompt() {
// 取前缀后的连续中文给 GeoAPI 模糊搜索(多取不影响结果)
assert_eq!(extract_city_from_prompt("查询合肥未来7天的天气预报"), "合肥未来");
assert_eq!(extract_city_from_prompt("北京今天天气"), "北京今天");
assert_eq!(extract_city_from_prompt("上海"), "上海");
assert_eq!(extract_city_from_prompt("帮我查一下广州"), "广州");
}
#[test]
fn test_extract_days() {
assert_eq!(extract_days_from_prompt("查询合肥未来7天的天气预报"), 7);
assert_eq!(extract_days_from_prompt("3天预报"), 3);
assert_eq!(extract_days_from_prompt("北京天气"), 3);
}
/// 端到端测试:需要有效的 QWeather 环境变量
#[tokio::test]
async fn test_execute_weather_integration() {
// 跳过无环境变量的情况
if std::env::var("QWEATHER_JWT_KEY_ID").is_err() {
eprintln!("跳过集成测试: 未设置 QWeather 环境变量");
return;
}
let result = super::execute(serde_json::json!({"location": "合肥", "days": 3})).await;
assert!(result.success, "查询失败: {}", result.output);
let data = result.data.expect("应有 data 字段");
assert_eq!(data["location"], "合肥");
assert!(data["now"].is_object(), "应有实时天气");
assert!(data["forecast"].is_array(), "应有预报");
}
}