From 1c037db0c23c6f9120d0be248054e9c94ce1348b Mon Sep 17 00:00:00 2001 From: pulsareonbot Date: Sat, 4 Jul 2026 17:18:58 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=A7=A3=E6=9E=90=20llama-cli=20?= =?UTF-8?q?=E8=BE=93=E5=87=BA,=E5=8F=AA=E6=8F=90=E5=8F=96=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=9B=9E=E5=A4=8D=20(=E5=8E=BB=E9=99=A4=20banner/?= =?UTF-8?q?=E8=BF=9B=E5=BA=A6=E5=8A=A8=E7=94=BB/=E7=BB=9F=E8=AE=A1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plugins/ai/backend.rs | 98 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 3 deletions(-) diff --git a/src/plugins/ai/backend.rs b/src/plugins/ai/backend.rs index e7feffd..e8085fd 100644 --- a/src/plugins/ai/backend.rs +++ b/src/plugins/ai/backend.rs @@ -194,9 +194,18 @@ impl AiBackend for LocalCliBackend { ); } - // llama-cli 输出到 stdout,取生成部分 - let reply = String::from_utf8_lossy(&output.stdout); - Ok(reply.trim().to_string()) + // llama-cli --single-turn 输出格式: + // [banner/logo] + // > ← 用户输入回显 + // [进度动画 |/-\] + // <生成的回复> ← 我们要提取的部分 + // [空行] + // [ Prompt: ... | Generation: ... ] ← 性能统计 + // + // 解析策略:找到 "> " 开头的行,跳过进度动画行,取实际回复行 + let raw = String::from_utf8_lossy(&output.stdout); + let reply = parse_llama_output(&raw); + Ok(reply) } fn tts(&self, text: &str, model_path: &Path, output_path: &Path) -> Result<()> { @@ -275,3 +284,86 @@ impl AiBackend for CloudBackend { "cloud" } } + +/// 解析 llama-cli --single-turn 的 stdout 输出,提取生成的回复文本 +/// +/// 输出格式: +/// ```text +/// [banner/logo ASCII art] +/// build : ... +/// model : ... +/// ... +/// > +/// +/// [进度动画 |/-\] +/// <生成的回复> +/// +/// [ Prompt: ... | Generation: ... ] +/// +/// Exiting... +/// ``` +fn parse_llama_output(raw: &str) -> String { + let lines: Vec<&str> = raw.lines().collect(); + let mut reply_lines = Vec::new(); + let mut after_user_input = false; + + for line in &lines { + let trimmed = line.trim(); + + // 跳过空行(但在回复区内保留) + if trimmed.is_empty() && !after_user_input { + continue; + } + + // 找到用户输入回显行 "> xxx" + if trimmed.starts_with('>') { + after_user_input = true; + continue; + } + + if !after_user_input { + continue; + } + + // 跳过进度动画行(只含 |/-\ 和空格) + let cleaned: String = trimmed.chars().filter(|c| !"|\\-/ ".contains(*c)).collect(); + if cleaned.is_empty() { + continue; + } + + // 遇到性能统计行 [ Prompt: ... ] 停止 + if trimmed.starts_with('[') && trimmed.contains("Generation") { + break; + } + // 遇到 "Exiting..." 停止 + if trimmed == "Exiting..." { + break; + } + + reply_lines.push(trimmed); + } + + reply_lines.join("\n").trim().to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_llama_output() { + let raw = " █ █ build : b1-fdb1db8\n\ +model : model_store/llm/qwen2.5-0.5b-q4_k_m.gguf\n\ +\n\ +> hello\n\ +\n\ +|/-\\|/-\\ 汪!\n\ +\n\ +[ Prompt: 31.1 t/s | Generation: 26.3 t/s ]\n\ +\n\ +Exiting...\n"; + + let reply = parse_llama_output(raw); + assert_eq!(reply, "汪!"); + } +}