feat: chat stats, regenerate with version history, edit & resubmit, image/file attachments

This commit is contained in:
Xianren Studio
2026-08-13 18:18:51 +08:00
parent 4718e41433
commit cd77f75066
12 changed files with 1156 additions and 199 deletions
+1 -1
View File
@@ -6,4 +6,4 @@ pub mod types;
pub use error::{EngineError, Result};
pub use manager::EngineManager;
pub use remote::{stream_chat_remote, RemoteConfig};
pub use types::{ChatMessage, ChatRequest, EngineConfig, EngineStatus};
pub use types::{ChatMessage, ChatRequest, ChatStreamEvent, EngineConfig, EngineStatus};
+18 -4
View File
@@ -1,5 +1,5 @@
use crate::error::{EngineError, Result};
use crate::types::{ChatRequest, EngineConfig, EngineStatus};
use crate::types::{ChatRequest, ChatStreamEvent, EngineConfig, EngineStatus};
use futures::Stream;
use std::process::Stdio;
use std::sync::Arc;
@@ -145,7 +145,7 @@ impl EngineManager {
pub async fn stream_chat(
&self,
req: ChatRequest,
) -> Result<futures::stream::BoxStream<'static, Result<String>>> {
) -> Result<futures::stream::BoxStream<'static, Result<ChatStreamEvent>>> {
let guard = self.inner.lock().await;
let handle = guard.as_ref().ok_or(EngineError::NotRunning)?;
let url = format!("{}/v1/chat/completions", handle.base_url);
@@ -211,7 +211,7 @@ pub(crate) fn sse_text_stream(
+ Unpin
+ Send
+ 'static,
) -> futures::stream::BoxStream<'static, Result<String>> {
) -> futures::stream::BoxStream<'static, Result<ChatStreamEvent>> {
use futures::StreamExt;
Box::pin(async_stream::stream! {
@@ -251,12 +251,26 @@ pub(crate) fn sse_text_stream(
closed = true;
break;
}
if let Some(usage) = value.get("usage") {
let prompt_tokens = usage
.get("prompt_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0) as u32;
let completion_tokens = usage
.get("completion_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0) as u32;
yield Ok(ChatStreamEvent::Usage {
prompt_tokens,
completion_tokens,
});
}
let content = value
.pointer("/choices/0/delta/content")
.and_then(|v| v.as_str())
.or_else(|| value.get("content").and_then(|v| v.as_str()));
if let Some(text) = content {
yield Ok(text.to_string());
yield Ok(ChatStreamEvent::Text(text.to_string()));
}
}
Err(e) => {
+2 -3
View File
@@ -1,6 +1,6 @@
use crate::error::{EngineError, Result};
use crate::manager::sse_text_stream;
use crate::types::ChatRequest;
use crate::types::{ChatRequest, ChatStreamEvent};
/// OpenAI 兼容的远程模型端点配置。
#[derive(Debug, Clone)]
@@ -14,7 +14,7 @@ pub struct RemoteConfig {
pub async fn stream_chat_remote(
cfg: &RemoteConfig,
req: ChatRequest,
) -> Result<futures::stream::BoxStream<'static, Result<String>>> {
) -> Result<futures::stream::BoxStream<'static, Result<ChatStreamEvent>>> {
let base = normalize_base(&cfg.base_url);
let url = format!("{base}/chat/completions");
let client = reqwest::Client::new();
@@ -41,4 +41,3 @@ fn normalize_base(base: &str) -> String {
}
s
}
+55 -2
View File
@@ -1,10 +1,64 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
use serde::ser::SerializeMap;
#[derive(Debug, Clone, Deserialize)]
pub struct ChatMessage {
pub role: String,
pub content: String,
/// 图片(data URL),非空时序列化为多模态 content 数组
#[serde(default)]
pub images: Vec<String>,
}
impl ChatMessage {
pub fn new(role: impl Into<String>, content: impl Into<String>) -> Self {
Self {
role: role.into(),
content: content.into(),
images: Vec::new(),
}
}
}
impl Serialize for ChatMessage {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("role", &self.role)?;
if self.images.is_empty() {
map.serialize_entry("content", &self.content)?;
} else {
let mut parts = Vec::new();
if !self.content.trim().is_empty() {
parts.push(serde_json::json!({
"type": "text",
"text": self.content,
}));
}
for image in &self.images {
parts.push(serde_json::json!({
"type": "image_url",
"image_url": { "url": image },
}));
}
map.serialize_entry("content", &parts)?;
}
map.end()
}
}
/// 流式聊天事件:增量文本 或 用量统计(部分引擎在最后一条 SSE 里给出)。
#[derive(Debug, Clone)]
pub enum ChatStreamEvent {
Text(String),
Usage {
prompt_tokens: u32,
completion_tokens: u32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -57,4 +111,3 @@ pub struct EngineStatus {
pub ngl: Option<i32>,
pub uptime_secs: Option<u64>,
}