清理废弃代码,重构长期记忆系统
核心变更: - 删除 worker.rs + ipc.rs(废弃进程架构) - 删除 ias listen 路径(cmd_listen / listen_loop / ToolExecutor) - 合并 ias service → ias daemon --log-file - 修复 main.rs call_capability 不处理上下文工具的 bug MemoryStore 重构: - write_for 空内容防御 + 精确去重 - 缓存结构 Vec<String> → Vec<(i32, String)>(用数据库 id 标识) - 新增 delete_for / update_for 方法 + LLM 工具定义 - load_if_needed 避免每次消息查 DB - 缓存 TTL(1h) + 每用户 200 条上限 - read_for limit 参数(默认 30 条) daemon.rs: - execute_tool 统一为递归派发,消除 call_capability 重复路由 净减少约 1400 行代码,0 warning,20 测试通过
This commit is contained in:
+142
-209
@@ -42,23 +42,20 @@ fn base64url(data: &[u8]) -> String {
|
||||
}
|
||||
|
||||
fn generate_jwt() -> Result<String, String> {
|
||||
let key_file = std::env::var("QWEATHER_JWT_PRIVATE_KEY_FILE")
|
||||
.unwrap_or_else(|_| "qweather/ed25519-private.pem".into());
|
||||
let key_file = std::env::var("QWEATHER_JWT_PRIVATE_KEY_FILE").unwrap_or_else(|_| {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".ias")
|
||||
.join("qweather")
|
||||
.join("ed25519-private.pem")
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
});
|
||||
|
||||
let pem = std::fs::read_to_string(&key_file)
|
||||
.map_err(|e| format!("读取密钥文件 {} 失败: {}", key_file, e))?;
|
||||
|
||||
// 手动解析 PEM: 去掉头尾,base64 解码得到 DER,再用 from_pkcs8_der
|
||||
let der = pem
|
||||
.lines()
|
||||
.filter(|l| !l.starts_with("-----"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
let der_bytes = general_purpose::STANDARD
|
||||
.decode(&der)
|
||||
.map_err(|e| format!("Base64 解码密钥失败: {e}"))?;
|
||||
|
||||
let signing_key = SigningKey::from_pkcs8_der(&der_bytes)
|
||||
let signing_key = SigningKey::from_pkcs8_pem(&pem)
|
||||
.map_err(|e| format!("解析 Ed25519 密钥失败: {e}"))?;
|
||||
|
||||
let key_id = std::env::var("QWEATHER_JWT_KEY_ID").unwrap_or_default();
|
||||
@@ -426,200 +423,153 @@ impl QWeatherClient {
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 公开接口
|
||||
// 公开接口 — 3 个独立工具
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
pub fn spec() -> SkillSpec {
|
||||
/// 1. weather_now — 实时天气
|
||||
pub fn now_spec() -> SkillSpec {
|
||||
SkillSpec {
|
||||
name: "query_weather".into(),
|
||||
description:
|
||||
"查询指定城市的天气信息,包括实时天气、未来几天预报、逐小时预报。参数 location 为城市名"
|
||||
.into(),
|
||||
name: "weather_now".into(),
|
||||
description: "查询指定城市的实时天气(温度、体感温度、天气状况、风向风力、湿度、降水量、气压、能见度)。参数 location 为城市名".into(),
|
||||
risk_level: RiskLevel::Low,
|
||||
parameters: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string", "description": "城市名,如 北京、上海"},
|
||||
"days": {"type": "integer", "description": "预报天数,默认 3,最大 30", "default": 3},
|
||||
"hours": {"type": "integer", "description": "逐时预报小时数,默认 0(不查询),最大 168", "default": 0}
|
||||
"location": {"type": "string", "description": "城市名,如 北京、上海"}
|
||||
},
|
||||
"required": ["location"]
|
||||
}),
|
||||
timeout_secs: 15,
|
||||
timeout_secs: 10,
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_params(params: &serde_json::Value) -> (&str, u32, u32) {
|
||||
pub async fn now_execute(params: serde_json::Value) -> SkillResult {
|
||||
let location = params
|
||||
.get("location")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("合肥");
|
||||
|
||||
let days = params
|
||||
.get("days")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(3)
|
||||
.min(30) as u32;
|
||||
|
||||
let hours = params
|
||||
.get("hours")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0)
|
||||
.min(168) as 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()
|
||||
&& let Ok(n) = num_str.parse::<u32>() {
|
||||
return n.min(30).max(1);
|
||||
}
|
||||
}
|
||||
.unwrap_or("");
|
||||
if location.is_empty() {
|
||||
return SkillResult::error("请提供 location(城市名)");
|
||||
}
|
||||
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 {
|
||||
let (location, days, hours) = extract_params(¶ms);
|
||||
|
||||
tracing::info!(
|
||||
"🌤️ 查询天气: location={}, days={}, hours={}",
|
||||
location,
|
||||
days,
|
||||
hours
|
||||
);
|
||||
|
||||
let client = match QWeatherClient::new().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return SkillResult::error(format!("初始化天气客户端失败: {}", e)),
|
||||
};
|
||||
|
||||
// 先查城市 ID
|
||||
let city_id = match client.lookup_city(location).await {
|
||||
Ok(id) => id,
|
||||
Err(e) => return SkillResult::error(e),
|
||||
};
|
||||
|
||||
// 并发查询
|
||||
let now_fut = client.weather_now(&city_id);
|
||||
let daily_fut = client.daily_forecast(&city_id, days);
|
||||
|
||||
// hours == 0 时跳过逐时预报,节省 API 调用
|
||||
let (now_result, daily_result, hourly_result) = if hours > 0 {
|
||||
let hourly_fut = client.hourly_forecast(&city_id, hours);
|
||||
let (n, d, h) = tokio::join!(now_fut, daily_fut, hourly_fut);
|
||||
(n, d, h)
|
||||
} else {
|
||||
let (n, d) = tokio::join!(now_fut, daily_fut);
|
||||
(n, d, Err("hours=0, skipped".into()))
|
||||
};
|
||||
|
||||
let now = match now_result {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
tracing::warn!("实时天气查询失败: {}", e);
|
||||
serde_json::Value::Null
|
||||
}
|
||||
};
|
||||
|
||||
let daily = match daily_result {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
tracing::warn!("每日预报查询失败: {}", e);
|
||||
serde_json::Value::Null
|
||||
}
|
||||
};
|
||||
|
||||
let hourly = match hourly_result {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
tracing::warn!("逐时预报查询失败: {}", e);
|
||||
serde_json::Value::Null
|
||||
}
|
||||
};
|
||||
|
||||
let output = serde_json::json!({
|
||||
"location": location,
|
||||
"now": now,
|
||||
"forecast": daily,
|
||||
"hourly": hourly,
|
||||
});
|
||||
|
||||
// 过滤 null 字段
|
||||
let mut output = output;
|
||||
if let Some(obj) = output.as_object_mut() {
|
||||
obj.retain(|_, v| !v.is_null());
|
||||
// 过滤 forecast/hourly 中每个元素内的空值
|
||||
for key in &["forecast", "hourly"] {
|
||||
if let Some(arr) = obj.get_mut(*key).and_then(|v| v.as_array_mut()) {
|
||||
for item in arr.iter_mut() {
|
||||
if let Some(m) = item.as_object_mut() {
|
||||
m.retain(|_, v| !v.is_null() && v.as_str().is_none_or(|s| !s.is_empty()));
|
||||
}
|
||||
}
|
||||
}
|
||||
match client.weather_now(&city_id).await {
|
||||
Ok(data) => {
|
||||
let output = serde_json::json!({"location": location, "now": data});
|
||||
SkillResult::ok_with_data(serde_json::to_string(&output).unwrap_or_default(), output)
|
||||
}
|
||||
Err(e) => SkillResult::error(e),
|
||||
}
|
||||
}
|
||||
|
||||
let text = serde_json::to_string(&output).unwrap_or_default();
|
||||
SkillResult::ok_with_data(text, output)
|
||||
/// 2. weather_forecast — 每日预报
|
||||
pub fn forecast_spec() -> SkillSpec {
|
||||
SkillSpec {
|
||||
name: "weather_forecast".into(),
|
||||
description: "查询指定城市未来几天的天气预报(最高/最低温度、白天/夜间天气状况、风向风力、湿度、紫外线指数)。参数 location 为城市名,days 为预报天数(默认3,最大30)".into(),
|
||||
risk_level: RiskLevel::Low,
|
||||
parameters: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string", "description": "城市名,如 北京、上海"},
|
||||
"days": {"type": "integer", "description": "预报天数,默认3,最大30"}
|
||||
},
|
||||
"required": ["location"]
|
||||
}),
|
||||
timeout_secs: 10,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn forecast_execute(params: serde_json::Value) -> SkillResult {
|
||||
let location = params
|
||||
.get("location")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
if location.is_empty() {
|
||||
return SkillResult::error("请提供 location(城市名)");
|
||||
}
|
||||
let days = params
|
||||
.get("days")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(3)
|
||||
.min(30)
|
||||
.max(1) as u32;
|
||||
|
||||
let client = match QWeatherClient::new().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return SkillResult::error(format!("初始化天气客户端失败: {}", e)),
|
||||
};
|
||||
let city_id = match client.lookup_city(location).await {
|
||||
Ok(id) => id,
|
||||
Err(e) => return SkillResult::error(e),
|
||||
};
|
||||
|
||||
match client.daily_forecast(&city_id, days).await {
|
||||
Ok(data) => {
|
||||
let output = serde_json::json!({"location": location, "forecast": data});
|
||||
SkillResult::ok_with_data(serde_json::to_string(&output).unwrap_or_default(), output)
|
||||
}
|
||||
Err(e) => SkillResult::error(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// 3. weather_hourly — 逐时预报
|
||||
pub fn hourly_spec() -> SkillSpec {
|
||||
SkillSpec {
|
||||
name: "weather_hourly".into(),
|
||||
description: "查询指定城市未来逐小时的天气预报(温度、天气状况、风向风力、湿度、降水概率、降水量)。参数 location 为城市名,hours 为预报小时数(默认24,最大168)".into(),
|
||||
risk_level: RiskLevel::Low,
|
||||
parameters: serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"location": {"type": "string", "description": "城市名,如 北京、上海"},
|
||||
"hours": {"type": "integer", "description": "预报小时数,默认24,最大168"}
|
||||
},
|
||||
"required": ["location"]
|
||||
}),
|
||||
timeout_secs: 10,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn hourly_execute(params: serde_json::Value) -> SkillResult {
|
||||
let location = params
|
||||
.get("location")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
if location.is_empty() {
|
||||
return SkillResult::error("请提供 location(城市名)");
|
||||
}
|
||||
let hours = params
|
||||
.get("hours")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(24)
|
||||
.min(168)
|
||||
.max(1) as u32;
|
||||
|
||||
let client = match QWeatherClient::new().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return SkillResult::error(format!("初始化天气客户端失败: {}", e)),
|
||||
};
|
||||
let city_id = match client.lookup_city(location).await {
|
||||
Ok(id) => id,
|
||||
Err(e) => return SkillResult::error(e),
|
||||
};
|
||||
|
||||
match client.hourly_forecast(&city_id, hours).await {
|
||||
Ok(data) => {
|
||||
let output = serde_json::json!({"location": location, "hourly": data});
|
||||
SkillResult::ok_with_data(serde_json::to_string(&output).unwrap_or_default(), output)
|
||||
}
|
||||
Err(e) => SkillResult::error(e),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -640,48 +590,31 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_params() {
|
||||
let params = serde_json::json!({"location": "上海", "days": 5});
|
||||
let (loc, days, hours) = extract_params(¶ms);
|
||||
assert_eq!(loc, "上海");
|
||||
assert_eq!(days, 5);
|
||||
assert_eq!(hours, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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() {
|
||||
// 跳过无环境变量的情况
|
||||
async fn test_now_execute_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;
|
||||
let result = super::now_execute(serde_json::json!({"location": "合肥"})).await;
|
||||
assert!(result.success, "查询失败: {}", result.output);
|
||||
let data = result.data.expect("应有 data 字段");
|
||||
assert_eq!(data["location"], "合肥");
|
||||
assert!(data["now"].is_object(), "应有实时天气");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_forecast_execute_integration() {
|
||||
if std::env::var("QWEATHER_JWT_KEY_ID").is_err() {
|
||||
eprintln!("跳过集成测试: 未设置 QWeather 环境变量");
|
||||
return;
|
||||
}
|
||||
let result =
|
||||
super::forecast_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["forecast"].is_array(), "应有预报");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user