增量提交

This commit is contained in:
zqm
2026-04-21 13:46:20 +08:00
parent f64209cb04
commit 09eb6fb1bd
44 changed files with 4411 additions and 931 deletions

View File

@@ -0,0 +1,2 @@
set RUSTFLAGS=-C target-feature=+crt-static
cargo build --release

View File

@@ -18,9 +18,7 @@ fn get_current_time() -> String {
/// 带时间前缀的打印宏
macro_rules! log {
($($arg:tt)*) => {
println!("[{}] {}", get_current_time(), format!($($arg)*));
};
($($arg:tt)*) => { println!("[{}] {}", get_current_time(), format!($($arg)*)) }
}
/// WebSocket 客户端连接管理器
@@ -302,14 +300,52 @@ impl WebSocketClient {
}
}
}
Some("wechat_message") => {
// 处理企业微信消息
Some("miniprogram_message") => {
if debug {
log!("📱 收到企业微信消息");
log!("收到小程序消息: {:?}", message);
}
let from_user_name = message.get("data").and_then(|d| d.get("from_user_name").and_then(|v| v.as_str())).unwrap_or("miniprogram_user");
let content = message.get("data").and_then(|d| d.get("content").and_then(|v| v.as_str())).unwrap_or("");
let conn_id = message.get("data").and_then(|d| d.get("conn_id").and_then(|v| v.as_str())).unwrap_or("");
if !content.is_empty() {
if debug { log!("处理小程序消息: {}", content); }
let ai_response = match self.call_lmstudio(content).await {
Ok(r) => r,
Err(e) => {
if debug { log!("LMStudio error: {}", e); }
format!("处理消息时出错: {}", e)
}
};
if debug { log!("AI回复: {}", ai_response); }
let reply_message = serde_json::json!({
"type": "miniprogram_message_response",
"data": {
"conn_id": conn_id,
"from_user_name": from_user_name,
"content": ai_response,
"msg_type": "text",
"timestamp": chrono::Utc::now().timestamp()
}
});
if let Some(sender) = &*self.sender.lock().await {
if let Err(e) = sender.send(reply_message.to_string()).await {
if debug { log!("发送回复失败: {}", e); }
} else {
if debug { log!("AI回复已发送"); }
}
}
}
}
Some("wechat_message") => {
if debug {
log!("收到企业微信消息");
log!(" 消息内容: {:?}", message);
}
// 提取消息数据
let from_user_name = message.get("data").and_then(|d| d.get("from_user_name").and_then(|v| v.as_str())).unwrap_or("");
let content = message.get("data").and_then(|d| d.get("content").and_then(|v| v.as_str())).unwrap_or("");
@@ -384,6 +420,25 @@ impl WebSocketClient {
log!("✅ 收到确认消息");
}
}
Some("wechat_app_sse_request") => {
if debug {
log!("📡 收到 WechatApp SSE 请求");
}
let request_id = message.get("request_id").and_then(|v| v.as_str()).unwrap_or("");
let data = message.get("data");
if !request_id.is_empty() && data.is_some() {
if debug {
log!("📡 处理 SSE 请求request_id: {}", request_id);
}
let client_clone = self.clone();
let request_id_clone = request_id.to_string();
let data_clone = data.cloned();
tokio::spawn(async move {
client_clone.handle_sse_request(request_id_clone, data_clone).await;
});
}
}
Some(msg_type) => {
if debug {
log!("❓ 收到未知消息类型: {}", msg_type);
@@ -524,7 +579,7 @@ impl WebSocketClient {
// 构建LMStudio API请求
let payload = json!({
"model": "qwen-3-panda-agi-v2", // 使用当前可用模型
"model": "qwen2.5-vl-7b-instruct", // 使用当前可用模型,qwen-3-panda-agi-v2
"input": message,
"system_prompt": system_prompt,
"temperature": 0.7,
@@ -595,6 +650,105 @@ impl WebSocketClient {
Ok(ai_response)
}
/// 处理 WechatApp SSE 请求
async fn handle_sse_request(&self, request_id: String, data: Option<serde_json::Value>) {
if !self.lmstudio_enabled {
let error_msg = serde_json::json!({
"type": "sse_error",
"request_id": request_id,
"error": "LMStudio is not enabled"
});
self.send_message(error_msg.to_string()).await.ok();
return;
}
if self.debug {
log!("📡 开始处理 SSE 请求: {}", request_id);
}
// 解析请求数据
let chat_request = match data {
Some(d) => d,
None => {
let error_msg = serde_json::json!({
"type": "sse_error",
"request_id": request_id,
"error": "Invalid request data"
});
self.send_message(error_msg.to_string()).await.ok();
return;
}
};
// 发送请求到 LMStudio SSE 端点
let client = reqwest::Client::new();
let lmstudio_url = format!("{}/v1/chat/completions", self.lmstudio_url);
if self.debug {
log!("🌐 发送 SSE 请求到 LMStudio: {}", lmstudio_url);
log!("📝 请求数据: {}", chat_request);
}
let response = match client.post(&lmstudio_url)
.header("Content-Type", "application/json")
.json(&chat_request)
.send()
.await {
Ok(resp) => resp,
Err(e) => {
if self.debug {
log!("❌ LMStudio SSE 请求失败: {}", e);
}
let error_msg = serde_json::json!({
"type": "sse_error",
"request_id": request_id,
"error": format!("LMStudio request failed: {}", e)
});
self.send_message(error_msg.to_string()).await.ok();
return;
}
};
// 处理 SSE 流
let body = response.bytes().await;
match body {
Ok(bytes) => {
// 将 SSE 片段发送回网关
let sse_chunk = serde_json::json!({
"type": "sse_chunk",
"request_id": request_id,
"chunk": String::from_utf8_lossy(&bytes)
});
if self.debug {
log!("📡 发送 SSE 片段: {}", sse_chunk);
}
self.send_message(sse_chunk.to_string()).await.ok();
}
Err(e) => {
if self.debug {
log!("❌ 读取 SSE 流失败: {}", e);
}
let error_msg = serde_json::json!({
"type": "sse_error",
"request_id": request_id,
"error": format!("Failed to read SSE stream: {}", e)
});
self.send_message(error_msg.to_string()).await.ok();
return;
}
}
// 发送 SSE 完成消息
let done_msg = serde_json::json!({
"type": "sse_done",
"request_id": request_id
});
if self.debug {
log!("📡 发送 SSE 完成消息: {}", done_msg);
}
self.send_message(done_msg.to_string()).await.ok();
}
}
/// WebSocket 客户端管理器

View File

@@ -6,6 +6,7 @@ edition = "2024"
[dependencies]
actix-web = "^4.0"
tokio = { version = "^1.0", features = ["full"] }
tokio-stream = "^0.1"
serde = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0"
reqwest = { version = "^0.11", features = ["json"] }

View File

@@ -0,0 +1,2 @@
set RUSTFLAGS=-C target-feature=+crt-static
cargo build --release

View File

@@ -0,0 +1,17 @@
{
"wechat": {
"token": "mytoken123456",
"corp_id": "wwa7bb7aec981103b4",
"encoding_aes_key": "PXP7FjoinIPc9WscGymDlf1VwMyBLh1cKJJSJFx2SO8",
"corp_secret": "your_corp_secret_here"
},
"wechat_mini_program": {
"app_id": "your_app_id",
"app_secret": "your_app_secret"
},
"debug": {
"wechat": false,
"config": false,
"http": true
}
}

View File

@@ -13,9 +13,7 @@ fn get_current_time() -> String {
/// 带时间前缀的打印宏
macro_rules! log {
($($arg:tt)*) => {
println!("[{}] {}", get_current_time(), format!($($arg)*));
};
($($arg:tt)*) => { println!("[{}] {}", get_current_time(), format!($($arg)*)) }
}
/// 连接信息
@@ -169,6 +167,19 @@ impl WebSocketPool {
}
/// 广播消息到所有控制通道连接SmartClaw
pub async fn send_to_session_by_id(&self, conn_id: &str, message: serde_json::Value) -> Result<(), String> {
let sessions = self.sessions.read().await;
let msg_str = message.to_string();
if let Some(session) = sessions.get(conn_id) {
let mut s = session.write().await;
s.text(msg_str).await.map_err(|e| e.to_string())
} else {
Err(format!("session not found: {}", conn_id))
}
}
pub async fn broadcast_to_control(&self, message: serde_json::Value) -> Result<(), String> {
let manager = self.manager.read().await;
let connections = manager.get_all_connections();
@@ -202,7 +213,8 @@ impl WebSocketPool {
Ok(())
}
/// 广播消息到所有连接(兼容旧接口)
pub async fn broadcast(&self, message: serde_json::Value) -> Result<(), String> {
self.broadcast_to_control(message).await
}

View File

@@ -11,6 +11,16 @@ use shared::{TaskRequest, TaskResponse, HealthResponse, utils};
use sha1::{Sha1, Digest};
use futures::StreamExt;
use crate::communication::ConnectionInfo;
use crate::sse_proxy::chat_completions;
/// 微信小程序手机号解密请求结构体
#[derive(Deserialize)]
struct PhoneDecryptRequest {
#[serde(rename = "encryptedData")]
_encrypted_data: String,
_iv: String,
_code: Option<String>,
}
/// 获取当前本地时间的格式化字符串
fn get_current_time() -> String {
@@ -20,14 +30,15 @@ fn get_current_time() -> String {
/// 带时间前缀的打印宏
macro_rules! log {
($($arg:tt)*) => {
println!("[{}] {}", get_current_time(), format!($($arg)*));
};
($($arg:tt)*) => { println!("[{}] {}", get_current_time(), format!($($arg)*)) };
}
mod communication;
mod sse_manager;
mod sse_proxy;
use communication::{ConnectionManager, WebSocketPool, CommunicationConfig, WebSocketClient};
use sse_manager::SseManager;
/// 企业微信消息发送结构体
#[derive(Serialize)]
@@ -117,6 +128,8 @@ async fn send_wechat_message(touser: &str, content: &str, debug: bool) -> Result
#[derive(Deserialize, Serialize, Debug)]
struct WeChatConfig {
wechat: WeChatSettings,
#[serde(default)]
wechat_app: WechatAppSettings,
debug: DebugSettings,
}
@@ -135,6 +148,28 @@ struct DebugSettings {
http: bool,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
struct WechatAppSettings {
#[serde(default = "WechatAppSettings::default_app_id")]
app_id: String,
#[serde(default = "WechatAppSettings::default_app_secret")]
app_secret: String,
}
impl WechatAppSettings {
fn default_app_id() -> String { "wx07f7e566fb459333".to_string() }
fn default_app_secret() -> String { "506673cfc479c67d9b5588e9977f0baa".to_string() }
}
impl Default for WechatAppSettings {
fn default() -> Self {
Self {
app_id: Self::default_app_id(),
app_secret: Self::default_app_secret(),
}
}
}
impl Default for WeChatConfig {
fn default() -> Self {
Self {
@@ -144,6 +179,7 @@ impl Default for WeChatConfig {
encoding_aes_key: "PXP7FjoinIPc9WscGymDlf1VwMyBLh1cKJJSJFx2SO8".to_string(),
corp_secret: "your_corp_secret_here".to_string(),
},
wechat_app: WechatAppSettings::default(),
debug: DebugSettings {
wechat: false,
config: false,
@@ -181,6 +217,17 @@ fn get_wechat_config() -> (String, String, String, String, bool, bool, bool) {
if config.debug.config {
println!("✅ 配置文件读取成功");
}
// 检查是否缺少 wechat_app 字段,缺少则补写
let json_str = serde_json::to_string_pretty(&config).unwrap_or_default();
if !json_str.contains("wechat_app") {
if let Ok(mut f) = std::fs::File::create(config_path) {
use std::io::Write;
let _ = f.write_all(json_str.as_bytes());
println!("✅ 已补写 wechat_app 默认配置到 config.json");
}
}
return (config.wechat.token, config.wechat.corp_id, config.wechat.encoding_aes_key, config.wechat.corp_secret, config.debug.wechat, config.debug.config, config.debug.http);
}
Err(e) => {
@@ -229,6 +276,105 @@ fn get_wechat_config() -> (String, String, String, String, bool, bool, bool) {
(default_config.wechat.token, default_config.wechat.corp_id, default_config.wechat.encoding_aes_key, default_config.wechat.corp_secret, default_config.debug.wechat, default_config.debug.config, default_config.debug.http)
}
use std::collections::HashMap;
use std::sync::LazyLock;
use std::sync::RwLock as StdRwLock;
/// 全局 session_key 缓存openid -> session_key
static SESSION_KEY_CACHE: LazyLock<StdRwLock<HashMap<String, String>>> =
LazyLock::new(|| StdRwLock::new(HashMap::new()));
use aes::Aes128;
use cbc::cipher::{BlockDecryptMut, KeyIvInit};
type Aes128CbcDec = cbc::Decryptor<Aes128>;
/// 微信小程序手机号解密AES-128-CBC + PKCS7
fn decrypt_wechat_phone(session_key: &str, encrypted_data: &str, iv: &str) -> Result<String, String> {
use base64::{engine::general_purpose::STANDARD as B64, Engine};
let key = B64.decode(session_key).map_err(|e| format!("session_key decode: {}", e))?;
let mut data = B64.decode(encrypted_data).map_err(|e| format!("encryptedData decode: {}", e))?;
let iv_bytes = B64.decode(iv).map_err(|e| format!("iv decode: {}", e))?;
let cipher = Aes128CbcDec::new_from_slices(&key, &iv_bytes)
.map_err(|e| format!("AES init: {}", e))?;
cipher.decrypt_padded_mut::<cbc::cipher::block_padding::Pkcs7>(&mut data)
.map_err(|e| format!("AES decrypt/pad: {}", e))?;
let json_str = String::from_utf8(data).map_err(|e| format!("utf8: {}", e))?;
let obj: serde_json::Value = serde_json::from_str(&json_str)
.map_err(|e| format!("json parse: {}", e))?;
obj.get("phoneNumber")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| format!("phoneNumber not found in: {}", json_str))
}
/// 通过微信 code 换 openid 和 session_key
async fn wechat_code_to_openid(code: &str) -> Result<(String, String), String> {
let (app_id, app_secret) = get_wechat_app_config();
let url = format!(
"https://api.weixin.qq.com/sns/jscode2session?appid={}&secret={}&js_code={}&grant_type=authorization_code",
app_id, app_secret, code
);
let client = reqwest::Client::new();
#[derive(Deserialize)]
struct Resp { openid: Option<String>, session_key: Option<String>, errmsg: Option<String> }
let resp: Resp = client.get(&url).send().await
.map_err(|e| e.to_string())?
.json().await
.map_err(|e| e.to_string())?;
match (resp.openid, resp.session_key) {
(Some(openid), Some(sk)) => Ok((openid, sk)),
_ => Err(resp.errmsg.unwrap_or_else(|| "no openid/session_key".into())),
}
}
/// 微信小程序配置从config.json读取缺少则补写默认值
fn get_wechat_app_config() -> (String, String) {
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
let config_path = Path::new("./config.json");
if config_path.exists() {
if let Ok(mut file) = File::open(config_path) {
let mut contents = String::new();
if file.read_to_string(&mut contents).is_ok() {
// 用完整 WeChatConfig 解析serde 会用 #[serde(default)] 填充缺少的 wechat_app
if let Ok(config) = serde_json::from_str::<WeChatConfig>(&contents) {
let needs_write = !contents.contains("wechat_app");
if needs_write {
if let Ok(updated) = serde_json::to_string_pretty(&config) {
if let Ok(mut f) = File::create(config_path) {
let _ = f.write_all(updated.as_bytes());
println!("✅ 已补写 wechat_app 默认配置到 config.json");
}
}
}
return (config.wechat_app.app_id, config.wechat_app.app_secret);
}
// 回退:直接解析 JSON 取 wechat_app 字段
if let Ok(val) = serde_json::from_str::<serde_json::Value>(&contents) {
if let Some(wa) = val.get("wechat_app") {
let app_id = wa.get("app_id").and_then(|v| v.as_str()).unwrap_or("wx07f7e566fb459333").to_string();
let app_secret = wa.get("app_secret").and_then(|v| v.as_str()).unwrap_or("506673cfc479c67d9b5588e9977f0baa").to_string();
return (app_id, app_secret);
}
}
}
}
}
let s = WechatAppSettings::default();
(s.app_id, s.app_secret)
}
/// 解析企业微信XML消息
fn parse_wechat_xml_message(xml_content: &str, debug: bool) -> (Option<String>, Option<String>, Option<String>, Option<String>) {
if debug {
@@ -417,6 +563,7 @@ struct TaskService {
connection_manager: Arc<RwLock<ConnectionManager>>,
websocket_pool: WebSocketPool,
communication_config: CommunicationConfig,
sse_manager: Arc<SseManager>,
}
impl TaskService {
@@ -425,15 +572,18 @@ impl TaskService {
let connection_manager = Arc::new(RwLock::new(ConnectionManager::new()));
let websocket_pool = WebSocketPool::new(connection_manager.clone());
let communication_config = CommunicationConfig::default();
let sse_manager = Arc::new(SseManager::new());
log!("🚀 初始化任务处理服务");
log!("📋 WebSocket连接池已创建");
log!("⚙️ 通信配置已加载: {:?}", communication_config.websocket_url);
log!("🔄 SSE Manager 已初始化");
Self {
connection_manager,
websocket_pool,
communication_config,
sse_manager,
}
}
@@ -648,7 +798,7 @@ impl TaskService {
}
/// 验证微信小程序签名
fn validate_miniprogram_signature(signature: &str, data: &str, session_key: &str) -> bool {
fn validate_wechat_app_signature(signature: &str, data: &str, session_key: &str) -> bool {
log!("🔐 验证微信小程序签名:");
log!(" signature: {}", signature);
log!(" data: {}", data);
@@ -704,14 +854,18 @@ async fn websocket_handler(
// 获取请求路径,区分连接类型
let path = req.path();
let is_control_connection = path == "/api/v1/ws/control";
let is_miniprogram = path == "/api/v1/ws/miniprogram";
if is_control_connection {
log!("🎯 检测到SmartClaw服务连接 (控制通道)");
} else if is_miniprogram {
log!("📨 检测到小程序连接");
} else {
log!("📱 检测到设备连接 (任务通道)");
}
log!("🔗 开始WebSocket握手...");
// 使用actix-ws处理WebSocket连接
let (response, mut session, msg_stream) = match actix_ws::handle(&req, body) {
Ok(result) => {
@@ -726,6 +880,7 @@ async fn websocket_handler(
// 生成连接ID
let connection_id = uuid::Uuid::new_v4().to_string();
// 添加连接到连接管理器
{{
@@ -734,7 +889,7 @@ async fn websocket_handler(
id: connection_id.clone(),
connected_at: Instant::now(),
last_heartbeat: Instant::now(),
client_info: Some(if is_control_connection { "SmartClaw" } else { "Device" }.to_string()),
client_info: Some(if is_control_connection { "SmartClaw" } else if is_miniprogram { "wechat_app" } else { "Device" }.to_string()),
};
manager.add_connection(connection_info);
// 保存会话到WebSocketPool
@@ -754,6 +909,110 @@ async fn websocket_handler(
// 处理消息
if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&text) {
match parsed.get("type").and_then(|t| t.as_str()) {
Some("miniprogram_message") => {
let conn_id_owned = connection_id.clone();
// 收到小程序消息记录conn_id广播给SmartClaw
let data = parsed.get("data");
let content = data.and_then(|d| d.get("content").and_then(|v| v.as_str())).unwrap_or("");
log!("收到小程序消息: {}", content);
// 将发送者conn_id存入消息让SmartClaw知道回传给谁
let mut forward_msg = parsed.clone();
if let Some(d) = forward_msg.get_mut("data").and_then(|d| d.as_object_mut()) {
d.insert("conn_id".to_string(), serde_json::json!(conn_id_owned));
}
match app_data.websocket_pool.broadcast_to_control(forward_msg).await {
Ok(_) => log!("小程序消息已广播conn_id={}", conn_id_owned),
Err(e) => log!("广播失败: {}", e),
}
}
Some("miniprogram_message_response") => {
// 收到SmartClaw的AI响应提取conn_id直接发送
let target_conn_id = parsed.get("data")
.and_then(|d| d.get("conn_id"))
.and_then(|v| v.as_str())
.unwrap_or("");
if !target_conn_id.is_empty() {
match app_data.websocket_pool.send_to_session_by_id(target_conn_id, parsed.clone()).await {
Ok(_) => log!("AI回复已发送小程序 {}", target_conn_id),
Err(e) => log!("发送AI回复失败: {}", e),
}
} else {
log!("miniprogram_message_response missing conn_id");
}
}
Some("wechat_login") => {
let code = parsed.get("data")
.and_then(|d| d.get("code"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let Some(code) = code else {
log!("wechat_login missing code");
return;
};
let conn_id_owned = connection_id.clone();
let app_data_clone = app_data.clone();
tokio::spawn(async move {
match wechat_code_to_openid(&code).await {
Ok((openid, session_key)) => {
SESSION_KEY_CACHE.write().ok()
.map(|mut c| { c.insert(openid.clone(), session_key); });
let resp = serde_json::json!({
"type": "wechat_login_ret",
"data": { "openid": openid }
});
if let Err(e) = app_data_clone.websocket_pool.send_to_session_by_id(&conn_id_owned, resp).await {
log!("wechat_login_ret 发送失败: {}", e);
} else {
log!("✅ wechat_login 成功 openid={}", openid);
}
}
Err(e) => {
let resp = serde_json::json!({
"type": "wechat_login_ret",
"data": { "error": e }
});
let _ = app_data_clone.websocket_pool.send_to_session_by_id(&conn_id_owned, resp).await;
}
}
});
}
Some("wechat_decrypt_phone") => {
let openid = parsed.get("data").and_then(|d| d.get("openid")).and_then(|v| v.as_str()).map(|s| s.to_string());
let encrypted_data = parsed.get("data").and_then(|d| d.get("encryptedData")).and_then(|v| v.as_str()).map(|s| s.to_string());
let iv = parsed.get("data").and_then(|d| d.get("iv")).and_then(|v| v.as_str()).map(|s| s.to_string());
let conn_id_owned = connection_id.clone();
let app_data_owned = app_data.clone();
let (Some(openid), Some(encrypted_data), Some(iv)) = (openid, encrypted_data, iv) else {
log!("wechat_decrypt_phone 参数缺失");
return;
};
tokio::spawn(async move {
let session_key = SESSION_KEY_CACHE.read().ok()
.and_then(|c| c.get(&openid).cloned());
let resp = match session_key {
Some(sk) => match decrypt_wechat_phone(&sk, &encrypted_data, &iv) {
Ok(phone) => serde_json::json!({"type": "wechat_decrypt_phone_ret", "data": {"phone": phone}}),
Err(e) => serde_json::json!({"type": "wechat_decrypt_phone_ret", "data": {"error": e}}),
},
None => serde_json::json!({"type": "wechat_decrypt_phone_ret", "data": {"error": "session_key not found, please re-login"}}),
};
match app_data_owned.websocket_pool.send_to_session_by_id(&conn_id_owned, resp).await {
Ok(_) => log!("手机号解密响应已发送 conn_id={}", conn_id_owned),
Err(e) => log!("手机号解密响应发送失败: {}", e),
}
});
}
Some("wechat_message_response") => {
// 处理SmartClaw的微信消息回复
log!("📱 收到SmartClaw的微信消息回复");
@@ -784,6 +1043,47 @@ async fn websocket_handler(
let mut manager = app_data.connection_manager.write().await;
manager.update_heartbeat(&connection_id);
}
Some("sse_chunk") => {
// 处理 SSE 片段消息
let request_id = parsed
.get("request_id")
.and_then(|v| v.as_str())
.unwrap_or("");
let chunk = parsed
.get("chunk")
.and_then(|v| v.as_str())
.unwrap_or("");
if let Some(tx) = app_data.sse_manager.get(request_id).await {
let _ = tx.send(chunk.as_bytes().to_vec()).await;
}
}
Some("sse_done") => {
// 处理 SSE 完成消息
let request_id = parsed
.get("request_id")
.and_then(|v| v.as_str())
.unwrap_or("");
app_data.sse_manager.remove(request_id).await;
}
Some("sse_error") => {
// 处理 SSE 错误消息
let request_id = parsed
.get("request_id")
.and_then(|v| v.as_str())
.unwrap_or("");
let error_msg = parsed.get("error")
.and_then(|v| v.as_str())
.unwrap_or("未知错误");
if let Some(tx) = app_data.sse_manager.get(request_id).await {
let _ = tx.send(format!("data: {{\"error\": \"{}\"}}\n\n", error_msg).into_bytes()).await;
app_data.sse_manager.remove(request_id).await;
}
}
Some("connect") => {
// 处理连接消息
log!("🔗 收到连接消息");
@@ -1157,7 +1457,7 @@ async fn handle_wechat_callback(req: HttpRequest, body: web::Bytes, app_data: we
}
/// 微信小程序回调处理器
async fn handle_wechat_miniprogram_callback(req: HttpRequest, body: web::Bytes) -> impl Responder {
async fn handle_wechat_app_callback(req: HttpRequest, body: web::Bytes) -> impl Responder {
println!("📱 收到微信小程序回调");
// 获取查询参数
@@ -1166,13 +1466,13 @@ async fn handle_wechat_miniprogram_callback(req: HttpRequest, body: web::Bytes)
// 解析查询参数
#[derive(Deserialize)]
struct MiniProgramQuery {
struct MiniAppQuery {
signature: String,
openid: Option<String>,
session_key: Option<String>,
}
let query: MiniProgramQuery = match web::Query::<MiniProgramQuery>::from_query(query_string) {
let query: MiniAppQuery = match web::Query::<MiniAppQuery>::from_query(query_string) {
Ok(q) => q.into_inner(),
Err(e) => {
println!("❌ 解析查询参数失败: {}", e);
@@ -1196,7 +1496,7 @@ async fn handle_wechat_miniprogram_callback(req: HttpRequest, body: web::Bytes)
}
// 验证签名
let is_valid = TaskService::validate_miniprogram_signature(
let is_valid = TaskService::validate_wechat_app_signature(
&query.signature,
&body_str,
&session_key
@@ -1432,6 +1732,26 @@ struct TaskListQuery {
per_page: Option<u32>,
}
/// 微信小程序手机号解密处理器
async fn decrypt_phone_number(req: web::Json<PhoneDecryptRequest>) -> impl Responder {
log!("📱 收到微信小程序手机号解密请求");
let _phone_request = req.into_inner();
// 这里应该调用微信官方接口获取session_key然后解密手机号
// 由于是模拟环境,我们使用假数据
// 实际项目中应该:
// 1. 使用code调用微信接口获取session_key
// 2. 使用session_key、iv和encryptedData解密手机号
// 模拟解密结果
let phone_number = "138****8888";
log!("✅ 手机号解密成功: {}", phone_number);
HttpResponse::Ok().json(serde_json::json!({"phoneNumber": phone_number}))
}
/// 系统信息处理器
async fn system_info(app_data: web::Data<TaskService>) -> impl Responder {
let manager = app_data.connection_manager.read().await;
@@ -1461,7 +1781,7 @@ async fn system_info(app_data: web::Data<TaskService>) -> impl Responder {
"health_check",
"task_processing",
"wechat_integration",
"miniprogram_integration",
"wechat_app_integration",
"websocket_support",
"nginx_proxy_integration"
],
@@ -1502,6 +1822,8 @@ async fn main() -> std::io::Result<()> {
// 创建任务处理服务
let task_service = web::Data::new(TaskService::new());
// 创建 SSE Manager
let sse_manager = web::Data::new(SseManager::new());
// 创建WebSocket客户端配置用于测试和演示
let ws_config = CommunicationConfig::production();
@@ -1626,9 +1948,14 @@ async fn main() -> std::io::Result<()> {
let server = HttpServer::new(move || {
App::new()
.app_data(task_service.clone())
.app_data(sse_manager.clone())
.wrap(Logger::default())
// 企业微信回调 - 直接匹配企业微信配置路径 /wecom
.route("/wecom", web::post().to(handle_wechat_callback))
// SSE 端点
.route("/v1/chat/completions", web::post().to(chat_completions))
// 微信小程序手机号解密端点
.route("/v1/wechat/phone", web::post().to(decrypt_phone_number))
// 其他API路由 - 通过 /api/v1 前缀
.service(
web::scope("/api/v1")
@@ -1641,10 +1968,11 @@ async fn main() -> std::io::Result<()> {
.route("/task/{task_id}", web::get().to(get_task_status))
.route("/tasks", web::get().to(list_tasks))
// 微信小程序集成
.route("/wechat/miniprogram/callback", web::post().to(handle_wechat_miniprogram_callback))
.route("/wechat/miniprogram/callback", web::post().to(handle_wechat_app_callback))
// WebSocket连接内网服务器连接
.route("/ws/control", web::get().to(websocket_handler))
.route("/ws/task", web::get().to(websocket_handler))
.route("/ws/miniprogram", web::get().to(websocket_handler))
// 测试接口(用于开发调试)
.route("/test/websocket/send", web::post().to(test_websocket_send))
.route("/test/websocket/send_and_wait", web::post().to(test_websocket_send_and_wait))

View File

@@ -0,0 +1,35 @@
use tokio::sync::mpsc;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
/// 管理所有活跃的 SSE 请求,等待 SmartClaw 的 WebSocket 响应
pub struct SseManager {
pending: Arc<RwLock<HashMap<String, mpsc::Sender<Vec<u8>>>>>,
}
impl SseManager {
pub fn new() -> Self {
Self {
pending: Arc::new(RwLock::new(HashMap::new())),
}
}
/// 注册一个 SSE 请求,关联其响应 channel
pub async fn add(&self, request_id: String, tx: mpsc::Sender<Vec<u8>>) {
let mut pending = self.pending.write().await;
pending.insert(request_id, tx);
}
/// 根据 request_id 获取对应的响应 channel
pub async fn get(&self, request_id: &str) -> Option<mpsc::Sender<Vec<u8>>> {
let pending = self.pending.read().await;
pending.get(request_id).cloned()
}
/// 移除完成的请求
pub async fn remove(&self, request_id: &str) {
let mut pending = self.pending.write().await;
pending.remove(request_id);
}
}

View File

@@ -0,0 +1,75 @@
use actix_web::{web, HttpRequest, HttpResponse};
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use tokio_stream::StreamExt;
use serde::{Deserialize, Serialize};
use serde_json;
use uuid;
use crate::sse_manager::SseManager;
use crate::communication::WebSocketPool;
/// ChatCompletions 请求结构
#[derive(Deserialize, Serialize, Debug)]
pub struct ChatCompletionsRequest {
pub model: String,
pub messages: Vec<Message>,
pub stream: bool,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct Message {
pub role: String,
pub content: String,
}
/// POST /v1/chat/completions
/// 网关将请求透传给 SmartClaw由 SmartClaw 代为请求 LMStudio
pub async fn chat_completions(
_req: HttpRequest,
body: web::Json<ChatCompletionsRequest>,
pool: web::Data<WebSocketPool>,
sse_manager: web::Data<SseManager>,
) -> HttpResponse {
let request_id = uuid::Uuid::new_v4().to_string();
let body = body.into_inner();
// 1. 验证 stream 参数(必须为 true
if !body.stream {
return HttpResponse::BadRequest().json(serde_json::json!({
"error": { "message": "stream must be true", "type": "invalid_request_error" }
}));
}
// 2. 构建转发给 SmartClaw 的 WebSocket 消息
let forward = serde_json::json!(
{
"type": "wechat_app_sse_request",
"request_id": request_id,
"data": body
}
);
// 3. 创建 SSE channel等待 SmartClaw 响应
let (client_tx, server_rx) = mpsc::channel::<Vec<u8>>(1024);
// 4. 注册请求到待响应映射
sse_manager.add(request_id.clone(), client_tx).await;
// 5. 发给 SmartClaw
if let Err(_e) = pool.broadcast_to_control(forward).await {
sse_manager.remove(&request_id).await;
return HttpResponse::BadGateway().json(serde_json::json!({
"error": { "message": "SmartClaw 不可用", "type": "bad_gateway" }
}));
}
// 6. 将 SmartClaw 的 WebSocket 消息转换为 SSE 流返回
let stream = ReceiverStream::new(server_rx)
.map(|chunk| Ok::<_, std::convert::Infallible>(actix_web::web::Bytes::from(chunk)));
HttpResponse::Ok()
.content_type("text/event-stream")
.append_header(("Cache-Control", "no-cache"))
.append_header(("X-Request-Id", request_id.as_str()))
.streaming(stream)
}