修复: unwrap 吞错 + lock 中毒 panic 高危项(数据吞 warn 跳过 / lock 降级返 Err)
This commit is contained in:
@@ -45,7 +45,20 @@ pub async fn restore_pending_approvals(state: &AppState) {
|
||||
// 故对每条 pending 都调一次 conv() 是幂等的(后续命中 entry().or_insert_with 跳过新建)。
|
||||
let mut convs_restored: HashSet<String> = HashSet::new();
|
||||
for rec in pending {
|
||||
let args: serde_json::Value = serde_json::from_str(&rec.arguments).unwrap_or_default();
|
||||
// 损坏 arguments 不还原(语义保留:数据异常不恢复)——
|
||||
// unwrap_or_default() 会吞错误置空 {},污染 pending 还原成无参数 tool_call(语义错);
|
||||
// 改为解析失败 warn! + continue 跳过该条,与下方 risk_level 解析失败的损坏过滤语义对齐。
|
||||
let args: serde_json::Value = match serde_json::from_str(&rec.arguments) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
"跳过 pending 审批恢复(tool_call_id={}):arguments 解析失败(不还原以免污染无参数语义): {}",
|
||||
rec.tool_call_id,
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// 过滤 risk_level 解析失败的损坏记录(语义保留:数据异常不恢复)·不再绑定 risk(PendingApproval.risk_level 已删)
|
||||
if risk_from_str(&rec.risk_level).is_none() {
|
||||
continue;
|
||||
|
||||
@@ -585,8 +585,20 @@ async fn extract_knowledge_from_conversation(
|
||||
.unwrap_or_else(|| "未命名对话".to_string());
|
||||
|
||||
// 消息读取:优先 ai_messages 表(消息拆分存储真相源),表空时 fallback 旧 messages JSON 列(老库兼容)
|
||||
// 注意:DB 查询 Err 不能吞成空 Vec(否则 records.is_empty() 误为真 → 走旧 messages JSON 回退,
|
||||
// 语义错:本应报 DB 故障)。这里显式 match:Ok 正常流程,Err 记 warn 后跳过本轮知识抽取。
|
||||
let msg_repo = AiMessageRepo::new(db);
|
||||
let records = msg_repo.list_by_conversation(conv_id).await.unwrap_or_default();
|
||||
let records = match msg_repo.list_by_conversation(conv_id).await {
|
||||
Ok(records) => records,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
error = %e,
|
||||
conv_id,
|
||||
"[KNOWLEDGE-EXTRACT] list_by_conversation 失败,跳过本轮知识抽取"
|
||||
);
|
||||
return Ok(0); // DB 故障,不当空数据回退(0 条,不置去重标志,允许下次重试)
|
||||
}
|
||||
};
|
||||
let messages: Vec<ChatMessage> = if !records.is_empty() {
|
||||
records.iter().map(crate::commands::ai::commands::record_to_message).collect()
|
||||
} else {
|
||||
@@ -598,7 +610,19 @@ async fn extract_knowledge_from_conversation(
|
||||
"[KNOWLEDGE-EXTRACT] ai_messages 表为空,回退旧 messages JSON 列(老库兼容)"
|
||||
);
|
||||
}
|
||||
serde_json::from_str(&conv.messages).unwrap_or_default()
|
||||
// 损坏 → match Err 分流:勿 unwrap_or_default 吞成空 Vec(空 Vec 会让 knowledge 抽取基于空上下文,
|
||||
// 误产空知识)。坏数据 warn 后跳过本轮(0 条,不置去重标志,允许下次重试),与 DB 故障同语义。
|
||||
match serde_json::from_str::<Vec<ChatMessage>>(&conv.messages) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
error = %e,
|
||||
conv_id,
|
||||
"[KNOWLEDGE-EXTRACT] 旧 messages JSON 列解析失败,跳过本轮知识抽取(勿基于空上下文抽取)"
|
||||
);
|
||||
return Ok(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
// 过滤 user/assistant,取最后 6 条
|
||||
let recent: Vec<&ChatMessage> = messages
|
||||
|
||||
@@ -446,7 +446,17 @@ async fn route_load_messages(state: &State<'_, AppState>, conversation_id: Strin
|
||||
conv_id = %conversation_id,
|
||||
"[remote_bridge] load_messages ai_messages 表空,回退旧 messages JSON 列(老库未迁移)"
|
||||
);
|
||||
serde_json::from_str(&rec.messages).unwrap_or_default()
|
||||
match serde_json::from_str::<Vec<ChatMessage>>(&rec.messages) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
error = %e,
|
||||
conv_id = %conversation_id,
|
||||
"[remote_bridge] load_messages 老 messages JSON 解析失败,跳过(推送空列表)"
|
||||
);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
|
||||
@@ -291,12 +291,24 @@ type SkillsGuard = RwLockReadGuard<'static, Option<Vec<SkillInfo>>>;
|
||||
/// P1-260617-3:`scan_skills` 同步递归 `fs::read_dir` + `read_to_string`(plugins/marketplaces
|
||||
/// 多层嵌套,Windows 文件多时同步阻塞 tokio runtime)。本函数改 async,慢路径扫盘包
|
||||
/// `spawn_blocking` 隔离(对齐 commands/project.rs detect_stack 模式)。快路径(读锁命中)仍同步无 fs。
|
||||
async fn skills_lock_async() -> SkillsGuard {
|
||||
///
|
||||
/// 锁中毒(P1-260617-3 加固):读写锁 expect 中毒会 panic,技能加载热路径 panic 不可接受。
|
||||
/// 中毒 → tracing::error! 记录 + 返 None(调用方 `skills_cached` 得空 Vec / `read_skill_content_stripped`
|
||||
/// 返 None),不再 panic。中毒通常因持锁 panicking 线程(早期改 *g 时 unwrap)残留,缓存本身可重建,
|
||||
/// 返空后下次 `invalidate_skills` 或进程重启自愈。
|
||||
async fn skills_lock_async() -> Option<SkillsGuard> {
|
||||
// 快路径:读锁命中(无 fs,纯内存)
|
||||
{
|
||||
let g = RwLock::read(&SKILLS).expect("SKILLS poisoned");
|
||||
// 锁中毒不 panic:PoisonError 携 guard 仍可恢复数据,但缓存一致性不保 → 记录后返 None 降级空。
|
||||
let g = match RwLock::read(&SKILLS) {
|
||||
Ok(g) => g,
|
||||
Err(_) => {
|
||||
tracing::error!("SKILLS 读锁中毒,返空技能列表");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
if g.is_some() {
|
||||
return g;
|
||||
return Some(g);
|
||||
}
|
||||
}
|
||||
// 慢路径:扫盘(spawn_blocking 隔离同步 fs 递归,防阻塞 tokio runtime)
|
||||
@@ -305,16 +317,28 @@ async fn skills_lock_async() -> SkillsGuard {
|
||||
.map(|res| res.skills)
|
||||
.unwrap_or_default();
|
||||
{
|
||||
let mut g = RwLock::write(&SKILLS).expect("SKILLS poisoned");
|
||||
let mut g = match RwLock::write(&SKILLS) {
|
||||
Ok(g) => g,
|
||||
Err(_) => {
|
||||
tracing::error!("SKILLS 写锁中毒,返空技能列表");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// 另一线程可能已填,二次检查(双检锁)
|
||||
if g.is_none() {
|
||||
*g = Some(scanned);
|
||||
}
|
||||
}
|
||||
// 再取读锁返回(此时必 Some)
|
||||
let g = RwLock::read(&SKILLS).expect("SKILLS poisoned");
|
||||
let g = match RwLock::read(&SKILLS) {
|
||||
Ok(g) => g,
|
||||
Err(_) => {
|
||||
tracing::error!("SKILLS 读锁(慢路径后)中毒,返空技能列表");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
debug_assert!(g.is_some(), "skills_lock 慢路径后必 Some");
|
||||
g
|
||||
Some(g)
|
||||
}
|
||||
|
||||
/// 技能扫描结果缓存(进程内;命中即 clone,不重复扫盘)。
|
||||
@@ -326,7 +350,8 @@ async fn skills_lock_async() -> SkillsGuard {
|
||||
/// 隔离同步 fs 防阻塞 tokio runtime(Tauri 单线程 runtime)。快路径(读锁命中)无 fs。
|
||||
pub(crate) async fn skills_cached() -> Vec<SkillInfo> {
|
||||
let g = skills_lock_async().await;
|
||||
g.clone().unwrap_or_default()
|
||||
// 锁中毒 → None → unwrap_or_default() 得空 Vec(对齐 P1-260617-3 中毒降级)。
|
||||
g.and_then(|g| g.clone()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 置缓存为 None,下次 `skills_cached()` 触发重扫。
|
||||
@@ -334,7 +359,15 @@ pub(crate) async fn skills_cached() -> Vec<SkillInfo> {
|
||||
/// `ai_reload_skills` IPC 调用:写锁置 None → 紧接 `skills_cached()` 重扫,
|
||||
/// 实现"改技能不重启即生效"。
|
||||
pub(crate) fn invalidate_skills() {
|
||||
let mut g = RwLock::write(&SKILLS).expect("SKILLS poisoned");
|
||||
// 锁中毒:本就是想置 None 清缓存重建,但中毒时持锁线程已 panic,此处无法恢复一致状态。
|
||||
// 记录错误并提前返回(下次加载由 skills_lock_async 中毒降级返空,进程重启自愈)。
|
||||
let mut g = match RwLock::write(&SKILLS) {
|
||||
Ok(g) => g,
|
||||
Err(_) => {
|
||||
tracing::error!("SKILLS 写锁中毒(invalidate_skills),跳过置 None");
|
||||
return;
|
||||
}
|
||||
};
|
||||
*g = None;
|
||||
}
|
||||
|
||||
@@ -349,8 +382,9 @@ pub(crate) fn invalidate_skills() {
|
||||
/// 否则非 Send 跨 await 点致 future 不 Send(MentionResolver 要求 Send)。guard 用 { } 限作用域。
|
||||
pub(crate) async fn read_skill_content_stripped(name: String) -> Option<String> {
|
||||
// 在作用域内取 path 后立即 drop guard,避免非 Send guard 跨 spawn_blocking await 点
|
||||
// 锁中毒 → skills_lock_async 返 None → ? 早返 None(对齐 P1-260617-3 中毒降级返空 desc)。
|
||||
let path: String = {
|
||||
let g = skills_lock_async().await;
|
||||
let g = skills_lock_async().await?;
|
||||
let skills = g.as_ref()?;
|
||||
let info = skills.iter().find(|s| s.name == name)?;
|
||||
info.path.clone()
|
||||
|
||||
Reference in New Issue
Block a user