修复: DeepSeek 400 全量扫描 + 队列 per-conv 隔离

- openai_compat: 扫描所有 assistant 消息剥离 orphan tool_calls(原仅查末条)
- queue 加 conversationId 字段,按会话精准 drain
- regenerate/editMessage 只清本会话排队消息
- newConversation 保留旧会话排队消息
- AiError 只清出错会话的队列项
This commit is contained in:
lxy
2026-07-20 00:19:50 +08:00
parent 42efb31bbf
commit e9e3578d26
59 changed files with 2875 additions and 1330 deletions
+35 -11
View File
@@ -287,16 +287,23 @@ type SkillsGuard = RwLockReadGuard<'static, Option<Vec<SkillInfo>>>;
///
/// 返 `RwLockReadGuard<Option<Vec<SkillInfo>>>`,调用方解 `*guard` 得 `&Vec<SkillInfo>`。
/// 懒初始化走双检锁:先读锁查 Some(快),None 时释放 → 扫盘 → 写锁填回 → 读锁重取。
fn skills_lock() -> SkillsGuard {
// 快路径:读锁命中
///
/// 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 {
// 快路径:读锁命中(无 fs,纯内存)
{
let g = RwLock::read(&SKILLS).expect("SKILLS poisoned");
if g.is_some() {
return g;
}
}
// 慢路径:扫盘 + 写锁填回
let scanned = scan_skills().skills;
// 慢路径:扫盘(spawn_blocking 隔离同步 fs 递归,防阻塞 tokio runtime)
let scanned = tokio::task::spawn_blocking(scan_skills)
.await
.map(|res| res.skills)
.unwrap_or_default();
{
let mut g = RwLock::write(&SKILLS).expect("SKILLS poisoned");
// 另一线程可能已填,二次检查(双检锁)
@@ -314,8 +321,11 @@ fn skills_lock() -> SkillsGuard {
///
/// 替代原 `OnceLock::get_or_init` 路径:返 owned `Vec<SkillInfo>`clone),
/// 因 RwLock 不能返 `&'static`。调用方(config.rs:30 / read_skill_content_stripped)已同步适配。
pub(crate) fn skills_cached() -> Vec<SkillInfo> {
let g = skills_lock();
///
/// P1-260617-3:改 async,慢路径(首次/重扫)走 `skills_lock_async` → spawn_blocking
/// 隔离同步 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()
}
@@ -332,11 +342,25 @@ pub(crate) fn invalidate_skills() {
///
/// 核心设计6:注入用正文,避免 YAML 头噪声污染 system prompt。
/// 缓存未命中返 None;文件读失败返 None。
pub(crate) fn read_skill_content_stripped(name: &str) -> Option<String> {
let g = skills_lock();
let skills = g.as_ref()?;
let info = skills.iter().find(|s| s.name == name)?;
let md = fs::read_to_string(&info.path).ok()?;
///
/// P1-260617-3:改 async,缓存懒初始化(可能触发扫盘)走 spawn_blocking 防阻塞 runtime。
///
/// 注:guard(std::sync::RwLockReadGuard 非 Send)必须在 spawn_blocking 的 .await 前 drop,
/// 否则非 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 点
let path: String = {
let g = skills_lock_async().await;
let skills = g.as_ref()?;
let info = skills.iter().find(|s| s.name == name)?;
info.path.clone()
};
// 单文件读取仍是同步 fs,但只一次小读;持续阻塞风险远低于扫盘递归。
// 仍包 spawn_blocking 与慢路径一致(对齐 detect_stack:防 Windows fs 调度慢)。
let md = tokio::task::spawn_blocking(move || fs::read_to_string(&path))
.await
.ok()?
.ok()?;
Some(strip_frontmatter(&md))
}