fix: manage_scheduled_task + search_web 注入 + 删除 LM Studio
- manage_scheduled_task: INSERT 加 user_id='', 检查 psql 退出码 - search_web: 用 python json.dumps 构造请求体(防止注入) - 删除 lmstudio.rs / LmStudioConfig,仅保留 DeepSeek - config.json 移除 lmstudio 段
This commit is contained in:
@@ -1,157 +0,0 @@
|
||||
use crate::llm::provider::{parse_chat_chunk, LlmProvider, ParsedChunk, StreamReceiver, StreamSender};
|
||||
use crate::llm::types::{ConversationConfig, Message, StreamChunk};
|
||||
use async_trait::async_trait;
|
||||
use reqwest::Client as HttpClient;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// LM Studio 提供商(OpenAI 兼容接口)
|
||||
pub struct LmStudioProvider {
|
||||
http: HttpClient,
|
||||
base_url: String,
|
||||
api_key: String,
|
||||
}
|
||||
|
||||
impl LmStudioProvider {
|
||||
pub fn new() -> Result<Self, String> {
|
||||
let base_url = std::env::var("LM_STUDIO_BASE_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:1234/v1".to_string());
|
||||
// 标准化 base_url: 确保有协议和 /v1 路径
|
||||
let base_url = normalize_base_url(&base_url);
|
||||
let api_key = std::env::var("LM_STUDIO_API_KEY").unwrap_or_default();
|
||||
|
||||
Ok(Self {
|
||||
http: HttpClient::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.build()
|
||||
.map_err(|e| format!("创建 HTTP client 失败: {}", e))?,
|
||||
base_url,
|
||||
api_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl LlmProvider for LmStudioProvider {
|
||||
fn name(&self) -> &str {
|
||||
"lmstudio"
|
||||
}
|
||||
|
||||
async fn chat_stream(
|
||||
&self,
|
||||
config: &ConversationConfig,
|
||||
messages: &[Message],
|
||||
) -> Result<StreamReceiver, String> {
|
||||
let url = format!("{}/chat/completions", self.base_url);
|
||||
|
||||
let body = serde_json::json!({
|
||||
"model": config.model,
|
||||
"messages": messages,
|
||||
"temperature": config.temperature,
|
||||
"max_tokens": config.max_tokens,
|
||||
"stream": true,
|
||||
});
|
||||
|
||||
let (tx, rx) = mpsc::channel::<StreamChunk>(64);
|
||||
let http = self.http.clone();
|
||||
let api_key = self.api_key.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = stream_openai(http, &url, &api_key, body, tx.clone()).await {
|
||||
let _ = tx.send(StreamChunk::Error(e)).await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(rx)
|
||||
}
|
||||
}
|
||||
|
||||
/// 与 DeepSeek 公用相同的 SSE 流式处理,但去除 thinking 相关字段
|
||||
async fn stream_openai(
|
||||
http: HttpClient,
|
||||
url: &str,
|
||||
api_key: &str,
|
||||
body: serde_json::Value,
|
||||
tx: StreamSender,
|
||||
) -> Result<(), String> {
|
||||
let mut req = http.post(url).header("Content-Type", "application/json");
|
||||
|
||||
if !api_key.is_empty() {
|
||||
req = req.header("Authorization", format!("Bearer {}", api_key));
|
||||
}
|
||||
|
||||
let response = req
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("请求失败: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
let _ = tx.send(StreamChunk::Error(format!("HTTP {}: {}", status, text))).await;
|
||||
return Err(format!("HTTP {}: {}", status, text));
|
||||
}
|
||||
|
||||
let mut full_text = String::new();
|
||||
let mut buf = String::new();
|
||||
let mut stream = response.bytes_stream();
|
||||
|
||||
use futures_util::StreamExt;
|
||||
while let Some(chunk_result) = stream.next().await {
|
||||
let chunk = chunk_result.map_err(|e| format!("读取流失败: {}", e))?;
|
||||
let text = String::from_utf8_lossy(&chunk);
|
||||
buf.push_str(&text);
|
||||
|
||||
while let Some(line_end) = buf.find('\n') {
|
||||
let line = buf[..line_end].to_string();
|
||||
buf = buf[line_end + 1..].to_string();
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// LM Studio 使用标准 OpenAI SSE 格式
|
||||
if let Some(parsed) = parse_chat_chunk(trimmed) {
|
||||
match parsed {
|
||||
ParsedChunk::Text(t) => {
|
||||
full_text.push_str(&t);
|
||||
let _ = tx.send(StreamChunk::Text(t)).await;
|
||||
}
|
||||
ParsedChunk::Reasoning(r) => {
|
||||
let _ = tx.send(StreamChunk::Reasoning(r)).await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = tx
|
||||
.send(StreamChunk::Done {
|
||||
text: full_text,
|
||||
reasoning: None,
|
||||
tool_calls: None,
|
||||
usage: None,
|
||||
})
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 确保 base_url 有协议前缀和 /v1 路径
|
||||
fn normalize_base_url(raw: &str) -> String {
|
||||
let raw = raw.trim();
|
||||
let with_protocol = if raw.starts_with("http://") || raw.starts_with("https://") {
|
||||
raw.to_string()
|
||||
} else {
|
||||
format!("http://{}", raw)
|
||||
};
|
||||
|
||||
// 去掉末尾 /
|
||||
let trimmed = with_protocol.trim_end_matches('/');
|
||||
// 确保路径以 /v1 结尾
|
||||
if trimmed.ends_with("/v1") {
|
||||
trimmed.to_string()
|
||||
} else {
|
||||
format!("{}/v1", trimmed)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
pub mod conversation;
|
||||
pub mod deepseek;
|
||||
pub mod lmstudio;
|
||||
pub mod provider;
|
||||
pub mod types;
|
||||
|
||||
|
||||
+2
-10
@@ -27,16 +27,8 @@ pub trait LlmProvider: Send + Sync {
|
||||
pub type BoxedProvider = Arc<dyn LlmProvider>;
|
||||
|
||||
/// 从配置创建恰当的提供商
|
||||
pub fn create_provider(config: &ConversationConfig) -> Result<BoxedProvider, String> {
|
||||
let provider = std::env::var("LLM_PROVIDER")
|
||||
.unwrap_or_else(|_| "deepseek".to_string())
|
||||
.to_lowercase();
|
||||
|
||||
match provider.as_str() {
|
||||
"deepseek" => Ok(Arc::new(super::deepseek::DeepSeekProvider::new()?)),
|
||||
"lmstudio" => Ok(Arc::new(super::lmstudio::LmStudioProvider::new()?)),
|
||||
_ => Err(format!("不支持的 LLM 提供商: {}", provider)),
|
||||
}
|
||||
pub fn create_provider(_config: &ConversationConfig) -> Result<BoxedProvider, String> {
|
||||
Ok(Arc::new(super::deepseek::DeepSeekProvider::new()?))
|
||||
}
|
||||
|
||||
// ─── 内部:SSE 解析工具 ───
|
||||
|
||||
Reference in New Issue
Block a user