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:
+107
-31
@@ -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(¶ms);
|
||||
|
||||
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(), "应有预报");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user