From a549bd3b2f2f6f0da2ea268ed73ace982df6ed2f Mon Sep 17 00:00:00 2001 From: wunianxiao Date: Tue, 2 Jun 2026 13:51:46 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20manage=5Fscheduled=5Ftask=20+=20search?= =?UTF-8?q?=5Fweb=20=E6=B3=A8=E5=85=A5=20+=20=E5=88=A0=E9=99=A4=20LM=20Stu?= =?UTF-8?q?dio?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - manage_scheduled_task: INSERT 加 user_id='', 检查 psql 退出码 - search_web: 用 python json.dumps 构造请求体(防止注入) - 删除 lmstudio.rs / LmStudioConfig,仅保留 DeepSeek - config.json 移除 lmstudio 段 --- config.json | 5 - .../manage_scheduled_task/scripts/manage.sh | 25 ++- resources/skills/search_web/scripts/search.sh | 13 +- src/config.rs | 35 ---- src/llm/lmstudio.rs | 157 ------------------ src/llm/mod.rs | 1 - src/llm/provider.rs | 12 +- src/main.rs | 7 +- 8 files changed, 29 insertions(+), 226 deletions(-) delete mode 100644 src/llm/lmstudio.rs diff --git a/config.json b/config.json index 1056b9f..85c1cee 100644 --- a/config.json +++ b/config.json @@ -9,11 +9,6 @@ "api_key": "${DEEPSEEK_API_KEY}", "model": "${DEEPSEEK_MODEL}", "base_url": "${DEEPSEEK_BASE_URL}" - }, - "lmstudio": { - "base_url": "${LM_STUDIO_BASE_URL}", - "model": "${LM_STUDIO_MODEL}", - "api_key": "${LM_STUDIO_API_KEY}" } } } diff --git a/resources/skills/manage_scheduled_task/scripts/manage.sh b/resources/skills/manage_scheduled_task/scripts/manage.sh index d0e86af..12f1aba 100755 --- a/resources/skills/manage_scheduled_task/scripts/manage.sh +++ b/resources/skills/manage_scheduled_task/scripts/manage.sh @@ -15,23 +15,30 @@ case "$ACTION" in add) [ -z "$CMD" ] && echo '{"error":"add 需要 command 参数"}' && exit 0 psql "$DB_URL" -v name="$NAME" -v desc="$DESC" -v cmd="$CMD" -v interval="$INTERVAL" << 'SQL' - INSERT INTO scheduled_tasks (id, name, description, command, interval_seconds, enabled, next_run_at) - VALUES (gen_random_uuid(), :'name', :'desc', :'cmd', :interval, true, NOW() + interval ':interval seconds') + INSERT INTO scheduled_tasks (id, name, description, command, interval_seconds, enabled, user_id, next_run_at) + VALUES (gen_random_uuid(), :'name', :'desc', :'cmd', :interval, true, '', NOW() + interval ':interval seconds') ON CONFLICT (name) DO UPDATE SET command=EXCLUDED.command, interval_seconds=EXCLUDED.interval_seconds, enabled=true, description=EXCLUDED.description SQL - echo '{"ok":true,"action":"add","name":"'"$NAME"'"}' + if [ $? -eq 0 ]; then + echo '{"ok":true,"action":"add","name":"'"$NAME"'"}' + else + echo '{"error":"添加失败"}' + fi ;; delete) - psql "$DB_URL" -v name="$NAME" -c "DELETE FROM scheduled_tasks WHERE name = :'name'" 2>/dev/null - echo '{"ok":true,"action":"delete","name":"'"$NAME"'"}' + psql "$DB_URL" -v name="$NAME" -c "DELETE FROM scheduled_tasks WHERE name = :'name'" 2>/dev/null && \ + echo '{"ok":true,"action":"delete","name":"'"$NAME"'"}' || \ + echo '{"error":"删除失败"}' ;; enable) - psql "$DB_URL" -v name="$NAME" -c "UPDATE scheduled_tasks SET enabled = true WHERE name = :'name'" 2>/dev/null - echo '{"ok":true,"action":"enable","name":"'"$NAME"'"}' + psql "$DB_URL" -v name="$NAME" -c "UPDATE scheduled_tasks SET enabled = true WHERE name = :'name'" 2>/dev/null && \ + echo '{"ok":true,"action":"enable","name":"'"$NAME"'"}' || \ + echo '{"error":"启用失败"}' ;; disable) - psql "$DB_URL" -v name="$NAME" -c "UPDATE scheduled_tasks SET enabled = false WHERE name = :'name'" 2>/dev/null - echo '{"ok":true,"action":"disable","name":"'"$NAME"'"}' + psql "$DB_URL" -v name="$NAME" -c "UPDATE scheduled_tasks SET enabled = false WHERE name = :'name'" 2>/dev/null && \ + echo '{"ok":true,"action":"disable","name":"'"$NAME"'"}' || \ + echo '{"error":"禁用失败"}' ;; *) echo '{"error":"未知操作: '"$ACTION"'(支持: add, delete, enable, disable)"}' diff --git a/resources/skills/search_web/scripts/search.sh b/resources/skills/search_web/scripts/search.sh index e1dea13..1a72909 100755 --- a/resources/skills/search_web/scripts/search.sh +++ b/resources/skills/search_web/scripts/search.sh @@ -9,9 +9,16 @@ COUNT=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin). API_KEY="${TAVILY_API_KEY}" [ -z "$API_KEY" ] && echo '{"error":"请设置 TAVILY_API_KEY"}' && exit 0 -RESP=$(curl -s -X POST "https://api.tavily.com/search" \ - -H "Content-Type: application/json" \ - -d "{\"api_key\":\"$API_KEY\",\"query\":\"$QUERY\",\"max_results\":$COUNT,\"include_answer\":true}") +export SEARCH_QUERY="$QUERY" SEARCH_COUNT="$COUNT" +RESP=$(python3 -c " +import urllib.request, json, os +key = os.environ['TAVILY_API_KEY'] +query = os.environ['SEARCH_QUERY'] +count = int(os.environ.get('SEARCH_COUNT', 5)) +body = json.dumps({'api_key': key, 'query': query, 'max_results': count, 'include_answer': True}).encode() +req = urllib.request.Request('https://api.tavily.com/search', data=body, headers={'Content-Type': 'application/json'}) +print(urllib.request.urlopen(req).read().decode()) +" 2>/dev/null) export RESP QUERY COUNT python3 -c " diff --git a/src/config.rs b/src/config.rs index c42484a..7579b8e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,17 +15,11 @@ pub struct AppConfig { #[derive(Debug, Clone, Deserialize)] pub struct LlmConfig { - /// LLM 提供商:deepseek | lmstudio #[serde(default = "default_llm_provider")] pub provider: String, - /// DeepSeek 配置 #[serde(default)] pub deepseek: DeepSeekConfig, - - /// LM Studio 配置 - #[serde(default)] - pub lmstudio: LmStudioConfig, } #[derive(Debug, Clone, Deserialize)] @@ -38,16 +32,6 @@ pub struct DeepSeekConfig { pub base_url: String, } -#[derive(Debug, Clone, Deserialize)] -pub struct LmStudioConfig { - #[serde(default = "default_lmstudio_base_url")] - pub base_url: String, - #[serde(default = "default_lmstudio_model")] - pub model: String, - #[serde(default)] - pub api_key: String, -} - #[derive(Debug, Clone, Deserialize)] pub struct StorageConfig { /// 状态文件目录 @@ -76,14 +60,6 @@ fn default_deepseek_base_url() -> String { env_or_default("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1") } -fn default_lmstudio_base_url() -> String { - env_or_default("LM_STUDIO_BASE_URL", "http://localhost:1234/v1") -} - -fn default_lmstudio_model() -> String { - env_or_default("LM_STUDIO_MODEL", "local-model") -} - fn default_state_dir() -> String { ".data/weixin-ilink".to_string() } @@ -97,7 +73,6 @@ impl Default for LlmConfig { Self { provider: default_llm_provider(), deepseek: DeepSeekConfig::default(), - lmstudio: LmStudioConfig::default(), } } } @@ -112,16 +87,6 @@ impl Default for DeepSeekConfig { } } -impl Default for LmStudioConfig { - fn default() -> Self { - Self { - base_url: default_lmstudio_base_url(), - model: default_lmstudio_model(), - api_key: String::new(), - } - } -} - impl Default for StorageConfig { fn default() -> Self { Self { diff --git a/src/llm/lmstudio.rs b/src/llm/lmstudio.rs deleted file mode 100644 index c1862a8..0000000 --- a/src/llm/lmstudio.rs +++ /dev/null @@ -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 { - 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 { - 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::(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) - } -} diff --git a/src/llm/mod.rs b/src/llm/mod.rs index 914c173..f892753 100644 --- a/src/llm/mod.rs +++ b/src/llm/mod.rs @@ -1,6 +1,5 @@ pub mod conversation; pub mod deepseek; -pub mod lmstudio; pub mod provider; pub mod types; diff --git a/src/llm/provider.rs b/src/llm/provider.rs index c685d39..b088b01 100644 --- a/src/llm/provider.rs +++ b/src/llm/provider.rs @@ -27,16 +27,8 @@ pub trait LlmProvider: Send + Sync { pub type BoxedProvider = Arc; /// 从配置创建恰当的提供商 -pub fn create_provider(config: &ConversationConfig) -> Result { - 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 { + Ok(Arc::new(super::deepseek::DeepSeekProvider::new()?)) } // ─── 内部:SSE 解析工具 ─── diff --git a/src/main.rs b/src/main.rs index 4ece05a..37a986e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -230,12 +230,7 @@ async fn cmd_listen( let conversation = if enable_llm { let sys_prompt = std::env::var("WEIXIN_LLM_SYSTEM_PROMPT") .unwrap_or_else(|_| DEFAULT_SYSTEM_PROMPT.to_string()); - let provider = std::env::var("LLM_PROVIDER").unwrap_or_else(|_| "deepseek".to_string()); - let model = if provider == "lmstudio" { - config.llm.lmstudio.model.clone() - } else { - config.llm.deepseek.model.clone() - }; + let model = config.llm.deepseek.model.clone(); let mut cfg = ConversationConfig { system_prompt: sys_prompt,