修复: loop_state 懒创建继承+入口早退清理(#10), update_full 剥离列封 lost-update(#11), MidStream 保文不回填半截 reasoning, 标题剥标签 sanitize(#9), ANSI 源头抑制+strip 兜底(#8)

This commit is contained in:
lxy
2026-10-06 12:13:25 +08:00
parent 03dc27bc14
commit 70d6e98f82
8 changed files with 251 additions and 23 deletions
+40 -7
View File
@@ -236,34 +236,67 @@ fn clean_title(raw: &str) -> String {
///
/// 开头常出现测试/问候("1"、"你好"等),取最长消息自然跳过这些噪音。
/// 极端情况全部过短时(仅测试消息),用第一条非空 user 兜底。
/// #9 标题泄漏:提取前剥离 HTML/Markdown 标签(<details>/<summary>/代码围栏标记等),
/// 治「content 首行是标签结构体,截断后标题裸露 `<details> <summa…`」。
fn sanitize_title_source(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
let mut chars = raw.chars().peekable();
while let Some(c) = chars.next() {
match c {
'<' => {
// HTML 标签:吞到 '>'(含);残缺(无 '>')吞到行尾
let mut closed = false;
for n in chars.by_ref() {
if n == '>' {
closed = true;
break;
}
if n == '\n' && !closed {
break;
}
}
let _ = closed;
}
'`' => {
// 代码围栏/行内标记:跳过反引号本身(保留代码文本语义)
}
'#' | '*' | '_' | '~' if out.is_empty() || out.ends_with(|ch: char| ch.is_whitespace()) => {
// 行首 Markdown 修饰符(# 标题/* 列表/_ 强调)不进标题;行中出现的保留(可能是正常标点)
}
_ => out.push(c),
}
}
// 压缩空白(标签剥离后可能留下连续空格/空行)
out.split_whitespace().collect::<Vec<_>>().join(" ")
}
pub(crate) fn extract_title(messages: &[ChatMessage]) -> Option<String> {
const MAX_TITLE_CHARS: usize = 60;
const SCAN_LIMIT: usize = 5;
let user_msgs: Vec<&str> = messages.iter()
let user_msgs: Vec<String> = messages.iter()
.filter(|m| matches!(m.role, MessageRole::User))
.take(SCAN_LIMIT)
.map(|m| m.content.trim())
.map(|m| sanitize_title_source(m.content.trim()))
.filter(|s| !s.is_empty())
.collect();
// 取字符数最多的
let best = user_msgs.iter()
.max_by_key(|s| s.chars().count())
.copied();
.max_by_key(|s| s.chars().count());
match best {
Some(text) => {
let mut chars = text.chars();
let t: String = chars.by_ref().take(MAX_TITLE_CHARS).collect();
if chars.next().is_some() { Some(format!("{}...", t)) } else { Some(t) }
}
None => {
} None => {
// 极端情况全空 → 第一条非空 user(兜底)
messages.iter()
.find(|m| matches!(m.role, MessageRole::User) && !m.content.trim().is_empty())
.map(|m| {
let mut chars = m.content.trim().chars();
let sanitized = sanitize_title_source(m.content.trim());
let mut chars = sanitized.chars();
let t: String = chars.by_ref().take(MAX_TITLE_CHARS).collect();
if chars.next().is_some() { format!("{}...", t) } else { t }
})