增量提交
This commit is contained in:
2
Claw/Server/SmartClaw/build.bat
Normal file
2
Claw/Server/SmartClaw/build.bat
Normal file
@@ -0,0 +1,2 @@
|
||||
set RUSTFLAGS=-C target-feature=+crt-static
|
||||
cargo build --release
|
||||
@@ -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 客户端管理器
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
2
Claw/Server/gateway/build.bat
Normal file
2
Claw/Server/gateway/build.bat
Normal file
@@ -0,0 +1,2 @@
|
||||
set RUSTFLAGS=-C target-feature=+crt-static
|
||||
cargo build --release
|
||||
17
Claw/Server/gateway/config.json
Normal file
17
Claw/Server/gateway/config.json
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
35
Claw/Server/gateway/src/sse_manager.rs
Normal file
35
Claw/Server/gateway/src/sse_manager.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
75
Claw/Server/gateway/src/sse_proxy.rs
Normal file
75
Claw/Server/gateway/src/sse_proxy.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user