taskkill /F /IM nginx.exe
This commit is contained in:
@@ -1,15 +1,401 @@
|
||||
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer, Responder, middleware::Logger};
|
||||
use actix_web_actors::ws;
|
||||
use serde::Deserialize;
|
||||
use actix_web::http::Method;
|
||||
use actix_ws::{Message};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use base64;
|
||||
use shared::{TaskRequest, TaskResponse, HealthResponse, utils};
|
||||
use sha1::{Sha1, Digest};
|
||||
use futures::StreamExt;
|
||||
|
||||
|
||||
mod communication;
|
||||
use communication::{WebSocketConnection, ConnectionManager, WebSocketPool, CommunicationConfig, WebSocketClient};
|
||||
use communication::{ConnectionManager, WebSocketPool, CommunicationConfig, WebSocketClient};
|
||||
|
||||
/// 企业微信消息发送结构体
|
||||
#[derive(Serialize)]
|
||||
struct WeChatMessage {
|
||||
touser: String,
|
||||
msgtype: String,
|
||||
agentid: i32,
|
||||
text: WeChatTextContent,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct WeChatTextContent {
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct WeChatAccessTokenResponse {
|
||||
access_token: String,
|
||||
// expires_in: i32, // 未使用,暂时注释
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct WeChatSendMessageResponse {
|
||||
errcode: i32,
|
||||
errmsg: String,
|
||||
}
|
||||
|
||||
/// 获取企业微信访问令牌
|
||||
async fn get_wechat_access_token() -> Result<String, Box<dyn std::error::Error>> {
|
||||
let (_token, corp_id, _encoding_aes_key, corp_secret, _debug, _debug_config, _http) = get_wechat_config();
|
||||
|
||||
let url = format!("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={}&corpsecret={}", corp_id, corp_secret);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client.get(&url).send().await?;
|
||||
let result: WeChatAccessTokenResponse = response.json().await?;
|
||||
|
||||
Ok(result.access_token)
|
||||
}
|
||||
|
||||
/// 发送企业微信消息
|
||||
async fn send_wechat_message(touser: &str, content: &str, debug: bool) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if debug {
|
||||
println!("📤 开始发送企业微信消息");
|
||||
}
|
||||
|
||||
// 获取访问令牌
|
||||
let access_token = get_wechat_access_token().await?;
|
||||
if debug {
|
||||
println!(" 获取到访问令牌: {}", access_token);
|
||||
}
|
||||
|
||||
// 构建消息
|
||||
let message = WeChatMessage {
|
||||
touser: touser.to_string(),
|
||||
msgtype: "text".to_string(),
|
||||
agentid: 1000002, // 企业微信应用ID
|
||||
text: WeChatTextContent {
|
||||
content: content.to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
// 发送消息
|
||||
let url = format!("https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={}", access_token);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client.post(&url)
|
||||
.json(&message)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let result: WeChatSendMessageResponse = response.json().await?;
|
||||
|
||||
if result.errcode == 0 {
|
||||
if debug {
|
||||
println!("✅ 消息发送成功");
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
if debug {
|
||||
println!("❌ 消息发送失败: {} - {}", result.errcode, result.errmsg);
|
||||
}
|
||||
Err(format!("消息发送失败: {} - {}", result.errcode, result.errmsg).into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
struct WeChatConfig {
|
||||
wechat: WeChatSettings,
|
||||
debug: DebugSettings,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
struct WeChatSettings {
|
||||
token: String,
|
||||
corp_id: String,
|
||||
encoding_aes_key: String,
|
||||
corp_secret: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
struct DebugSettings {
|
||||
wechat: bool,
|
||||
config: bool,
|
||||
http: bool,
|
||||
}
|
||||
|
||||
impl Default for WeChatConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
wechat: WeChatSettings {
|
||||
token: "mytoken123456".to_string(),
|
||||
corp_id: "wwa7bb7aec981103b4".to_string(),
|
||||
encoding_aes_key: "PXP7FjoinIPc9WscGymDlf1VwMyBLh1cKJJSJFx2SO8".to_string(),
|
||||
corp_secret: "your_corp_secret_here".to_string(),
|
||||
},
|
||||
debug: DebugSettings {
|
||||
wechat: false,
|
||||
config: false,
|
||||
http: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 企业微信配置(从config.json文件读取,便于配置管理)
|
||||
fn get_wechat_config() -> (String, String, String, String, bool, bool, bool) {
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
// 配置文件路径
|
||||
let config_path = Path::new("./config.json");
|
||||
|
||||
// 尝试读取配置文件
|
||||
if config_path.exists() {
|
||||
// 先创建默认配置,用于获取debug.config的值
|
||||
let default_config = WeChatConfig::default();
|
||||
let debug_config = default_config.debug.config;
|
||||
|
||||
if debug_config {
|
||||
println!("📁 读取配置文件: {:?}", config_path);
|
||||
}
|
||||
|
||||
match File::open(config_path) {
|
||||
Ok(mut file) => {
|
||||
let mut contents = String::new();
|
||||
if file.read_to_string(&mut contents).is_ok() {
|
||||
match serde_json::from_str::<WeChatConfig>(&contents) {
|
||||
Ok(config) => {
|
||||
if config.debug.config {
|
||||
println!("✅ 配置文件读取成功");
|
||||
}
|
||||
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) => {
|
||||
if debug_config {
|
||||
println!("❌ 配置文件解析失败: {}, 使用默认配置", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if debug_config {
|
||||
println!("❌ 配置文件读取失败,使用默认配置");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let default_config = WeChatConfig::default();
|
||||
if default_config.debug.config {
|
||||
println!("❌ 打开配置文件失败: {}, 使用默认配置", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let default_config = WeChatConfig::default();
|
||||
if default_config.debug.config {
|
||||
println!("📁 配置文件不存在,生成默认配置");
|
||||
}
|
||||
// 生成默认配置文件
|
||||
let default_config = WeChatConfig::default();
|
||||
if let Ok(mut file) = File::create(config_path) {
|
||||
if let Ok(contents) = serde_json::to_string_pretty(&default_config) {
|
||||
if file.write_all(contents.as_bytes()).is_ok() {
|
||||
if default_config.debug.config {
|
||||
println!("✅ 默认配置文件生成成功: {:?}", config_path);
|
||||
}
|
||||
} else {
|
||||
if default_config.debug.config {
|
||||
println!("❌ 默认配置文件生成失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用默认值
|
||||
let default_config = WeChatConfig::default();
|
||||
(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)
|
||||
}
|
||||
|
||||
/// 解析企业微信XML消息
|
||||
fn parse_wechat_xml_message(xml_content: &str, debug: bool) -> (Option<String>, Option<String>, Option<String>, Option<String>) {
|
||||
if debug {
|
||||
println!("📄 开始解析企业微信XML消息");
|
||||
}
|
||||
|
||||
// 检查是否为加密消息
|
||||
if xml_content.contains("<Encrypt>") {
|
||||
if debug {
|
||||
println!("🔒 发现加密消息,开始解密");
|
||||
}
|
||||
|
||||
// 提取Encrypt标签内容
|
||||
if let Some(encrypt_content) = extract_xml_tag(xml_content, "Encrypt") {
|
||||
if debug {
|
||||
println!(" 提取到加密内容");
|
||||
}
|
||||
|
||||
// 解密消息
|
||||
match decrypt_wechat_message(&encrypt_content) {
|
||||
Ok(decrypted_xml) => {
|
||||
if debug {
|
||||
println!("✅ 消息解密成功");
|
||||
println!(" 解密后内容: {}", decrypted_xml);
|
||||
}
|
||||
|
||||
// 从解密后的XML中提取发送者、内容、消息类型和事件类型
|
||||
let from_user_name = extract_xml_tag(&decrypted_xml, "FromUserName");
|
||||
let content = extract_xml_tag(&decrypted_xml, "Content");
|
||||
let msg_type = extract_xml_tag(&decrypted_xml, "MsgType");
|
||||
let event = extract_xml_tag(&decrypted_xml, "Event");
|
||||
|
||||
if debug {
|
||||
if let Some(from_user) = &from_user_name {
|
||||
println!(" 发送者: {}", from_user);
|
||||
}
|
||||
if let Some(msg_content) = &content {
|
||||
println!(" 消息内容: {}", msg_content);
|
||||
}
|
||||
if let Some(msg_type_val) = &msg_type {
|
||||
println!(" 消息类型: {}", msg_type_val);
|
||||
}
|
||||
if let Some(event_val) = &event {
|
||||
println!(" 事件类型: {}", event_val);
|
||||
}
|
||||
}
|
||||
|
||||
return (from_user_name, content, msg_type, event);
|
||||
}
|
||||
Err(e) => {
|
||||
if debug {
|
||||
println!("❌ 消息解密失败: {}", e);
|
||||
}
|
||||
return (None, None, None, None);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if debug {
|
||||
println!("❌ 无法提取Encrypt标签");
|
||||
}
|
||||
return (None, None, None, None);
|
||||
}
|
||||
} else {
|
||||
// 非加密消息,直接解析
|
||||
if debug {
|
||||
println!("🔓 非加密消息,直接解析");
|
||||
}
|
||||
let from_user_name = extract_xml_tag(xml_content, "FromUserName");
|
||||
let content = extract_xml_tag(xml_content, "Content");
|
||||
let msg_type = extract_xml_tag(xml_content, "MsgType");
|
||||
let event = extract_xml_tag(xml_content, "Event");
|
||||
|
||||
if debug {
|
||||
if let Some(from_user) = &from_user_name {
|
||||
println!(" 发送者: {}", from_user);
|
||||
}
|
||||
if let Some(msg_content) = &content {
|
||||
println!(" 消息内容: {}", msg_content);
|
||||
}
|
||||
if let Some(msg_type_val) = &msg_type {
|
||||
println!(" 消息类型: {}", msg_type_val);
|
||||
}
|
||||
if let Some(event_val) = &event {
|
||||
println!(" 事件类型: {}", event_val);
|
||||
}
|
||||
}
|
||||
|
||||
return (from_user_name, content, msg_type, event);
|
||||
}
|
||||
}
|
||||
|
||||
/// 解密企业微信消息
|
||||
fn decrypt_wechat_message(encrypted: &str) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// 获取企业微信配置
|
||||
let (_, _corp_id, encoding_aes_key, _corp_secret, _debug, _debug_config, _http) = get_wechat_config();
|
||||
|
||||
// 企业微信官方要求:EncodingAESKey 使用URL_SAFE Base64解码,且需要补全=号
|
||||
let mut aes_key_str = encoding_aes_key.to_string();
|
||||
while aes_key_str.len() % 4 != 0 {
|
||||
aes_key_str.push('=');
|
||||
}
|
||||
let key = base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE, &aes_key_str)?;
|
||||
|
||||
if key.len() != 32 {
|
||||
return Err("Invalid key length".into());
|
||||
}
|
||||
|
||||
// 企业微信官方规定:IV 必须是 EncodingAESKey 解码后的前 16 字节
|
||||
let iv = &key[0..16];
|
||||
let mut iv_array = [0u8; 16];
|
||||
iv_array.copy_from_slice(iv);
|
||||
|
||||
// 企业微信官方要求:加密消息内容(Encrypt)使用STANDARD Base64解码
|
||||
let ciphertext = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &encrypted)?;
|
||||
|
||||
// 使用AES-CBC-256解密
|
||||
use aes::cipher::{KeyIvInit, BlockDecryptMut};
|
||||
use aes::Aes256;
|
||||
use cbc::Decryptor;
|
||||
use cipher::block_padding::NoPadding;
|
||||
|
||||
let decryptor = Decryptor::<Aes256>::new_from_slices(&key, &iv_array)?;
|
||||
let mut buffer = ciphertext.to_vec();
|
||||
let plaintext = decryptor.decrypt_padded_mut::<NoPadding>(&mut buffer)
|
||||
.map_err(|e| format!("Decryption error: {:?}", e))?;
|
||||
|
||||
// 企业微信官方格式:16字节随机串 + 4字节长度(网络序) + 消息内容 + CorpID
|
||||
if plaintext.len() < 20 {
|
||||
return Err("Decrypted data too short".into());
|
||||
}
|
||||
|
||||
// 解析4字节长度(网络序,大端)
|
||||
let msg_len = u32::from_be_bytes([plaintext[16], plaintext[17], plaintext[18], plaintext[19]]);
|
||||
let msg_start = 20;
|
||||
let msg_end = msg_start + msg_len as usize;
|
||||
|
||||
if msg_end > plaintext.len() {
|
||||
return Err("Invalid message length".into());
|
||||
}
|
||||
|
||||
// 截取消息内容
|
||||
let msg = &plaintext[msg_start..msg_end];
|
||||
let result = String::from_utf8_lossy(msg).to_string();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 提取XML标签内容
|
||||
fn extract_xml_tag(xml: &str, tag: &str) -> Option<String> {
|
||||
// 尝试匹配带CDATA的格式
|
||||
let start_tag_cdata = format!("<{}><![CDATA[", tag);
|
||||
let cdata_end = "]]>";
|
||||
let end_tag = format!("</{}", tag);
|
||||
|
||||
if let Some(start_idx) = xml.find(&start_tag_cdata) {
|
||||
let start_idx = start_idx + start_tag_cdata.len();
|
||||
// 先找到CDATA的结束
|
||||
if let Some(cdata_end_idx) = xml[start_idx..].find(cdata_end) {
|
||||
let content = &xml[start_idx..start_idx + cdata_end_idx];
|
||||
return Some(content.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试匹配不带CDATA的格式
|
||||
let start_tag = format!("<{}", tag);
|
||||
|
||||
if let Some(start_idx) = xml.find(&start_tag) {
|
||||
// 找到标签的结束位置
|
||||
if let Some(tag_end_idx) = xml[start_idx..].find(">").map(|i| start_idx + i + 1) {
|
||||
if let Some(end_idx) = xml[tag_end_idx..].find(&end_tag) {
|
||||
let content = &xml[tag_end_idx..tag_end_idx + end_idx];
|
||||
// 去除前后空白
|
||||
let trimmed_content = content.trim();
|
||||
if !trimmed_content.is_empty() {
|
||||
return Some(trimmed_content.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// 任务处理服务
|
||||
struct TaskService {
|
||||
@@ -204,20 +590,31 @@ impl TaskService {
|
||||
}
|
||||
|
||||
/// 验证企业微信签名
|
||||
fn validate_wechat_signature(signature: &str, timestamp: &str, nonce: &str, token: &str) -> bool {
|
||||
println!("🔐 验证企业微信签名:");
|
||||
println!(" signature: {}", signature);
|
||||
println!(" timestamp: {}", timestamp);
|
||||
println!(" nonce: {}", nonce);
|
||||
println!(" token: {}", token);
|
||||
fn validate_wechat_signature(msg_signature: &str, timestamp: &str, nonce: &str, data: &str, debug: bool) -> bool {
|
||||
if debug {
|
||||
println!("🔐 验证企业微信签名:");
|
||||
println!(" msg_signature: {}", msg_signature);
|
||||
println!(" timestamp: {}", timestamp);
|
||||
println!(" nonce: {}", nonce);
|
||||
println!(" data: {}", data);
|
||||
}
|
||||
|
||||
// 企业微信签名验证算法
|
||||
// 1. 将token、timestamp、nonce三个参数进行字典序排序
|
||||
let mut params = vec![token, timestamp, nonce];
|
||||
// 获取企业微信配置
|
||||
let (token, _corp_id, _encoding_aes_key, _corp_secret, _, _, _) = get_wechat_config();
|
||||
if debug {
|
||||
println!(" token: {}", token);
|
||||
}
|
||||
|
||||
// 企业微信签名验证算法(严格按照官方要求)
|
||||
// 1. 将token、timestamp、nonce、data四个参数进行字典序排序
|
||||
let mut params = vec![token.as_str(), timestamp, nonce, data];
|
||||
params.sort();
|
||||
|
||||
// 2. 将三个参数字符串拼接成一个字符串
|
||||
// 2. 将排序后的参数拼接成一个字符串
|
||||
let combined = params.join("");
|
||||
if debug {
|
||||
println!(" 排序后拼接字符串: {}", combined);
|
||||
}
|
||||
|
||||
// 3. 进行sha1加密
|
||||
let mut hasher = Sha1::new();
|
||||
@@ -225,10 +622,12 @@ impl TaskService {
|
||||
let result = hasher.finalize();
|
||||
let computed_signature = hex::encode(result);
|
||||
|
||||
// 4. 与signature对比
|
||||
let is_valid = computed_signature == signature;
|
||||
println!(" 计算签名: {}", computed_signature);
|
||||
println!(" 验证结果: {}", if is_valid { "✅ 通过" } else { "❌ 失败" });
|
||||
// 4. 与msg_signature对比
|
||||
let is_valid = computed_signature == msg_signature;
|
||||
if debug {
|
||||
println!(" 计算签名: {}", computed_signature);
|
||||
println!(" 验证结果: {}", if is_valid { "✅ 通过" } else { "❌ 失败" });
|
||||
}
|
||||
|
||||
is_valid
|
||||
}
|
||||
@@ -263,10 +662,15 @@ impl TaskService {
|
||||
/// WebSocket连接处理器
|
||||
async fn websocket_handler(
|
||||
req: HttpRequest,
|
||||
stream: web::Payload,
|
||||
app_data: web::Data<TaskService>,
|
||||
body: web::Payload,
|
||||
_app_data: web::Data<TaskService>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
println!("🔗 收到WebSocket连接请求: {:?}", req);
|
||||
println!("🔗 收到WebSocket连接请求: ");
|
||||
println!(" HttpRequest {:?} {:?}:{}", req.method(), req.version(), req.path());
|
||||
println!(" headers: ");
|
||||
for (name, value) in req.headers() {
|
||||
println!(" {:?}: {:?}", name, value);
|
||||
}
|
||||
|
||||
// 验证连接来源(可以添加API密钥验证)
|
||||
let api_key = req.headers().get("X-API-Key")
|
||||
@@ -277,30 +681,77 @@ async fn websocket_handler(
|
||||
|
||||
if api_key != expected_key {
|
||||
println!("❌ WebSocket连接认证失败");
|
||||
return Ok(HttpResponse::Unauthorized().json(serde_json::json!({
|
||||
"error": "Invalid API key",
|
||||
"message": "WebSocket连接认证失败"
|
||||
})));
|
||||
return Err(actix_web::error::ErrorUnauthorized("Invalid API key"));
|
||||
}
|
||||
|
||||
println!("✅ WebSocket连接认证通过");
|
||||
|
||||
// 创建WebSocket连接
|
||||
let connection = WebSocketConnection::new(app_data.connection_manager.clone());
|
||||
// 获取请求路径,区分连接类型
|
||||
let path = req.path();
|
||||
let is_control_connection = path == "/api/v1/ws/control";
|
||||
|
||||
// 获取连接信息(用于调试)
|
||||
let connection_info = connection.get_info();
|
||||
println!("ℹ️ WebSocket连接信息: id={}, 连接时间: {:?}", connection_info.id, connection_info.connected_at.elapsed());
|
||||
if is_control_connection {
|
||||
println!("🎯 检测到SmartClaw服务连接 (控制通道)");
|
||||
} else {
|
||||
println!("📱 检测到设备连接 (任务通道)");
|
||||
}
|
||||
|
||||
// 获取响应通道信息(用于调试)
|
||||
let sender_info = connection.get_response_sender();
|
||||
let receiver_info = connection.get_response_receiver();
|
||||
println!("📤 响应发送器: {:?}, 📥 响应接收器: {:?}", sender_info.is_some(), receiver_info.is_some());
|
||||
println!("🔗 开始WebSocket握手...");
|
||||
// 使用actix-ws处理WebSocket连接
|
||||
let (response, mut session, msg_stream) = match actix_ws::handle(&req, body) {
|
||||
Ok(result) => {
|
||||
println!("✅ WebSocket握手成功");
|
||||
result
|
||||
},
|
||||
Err(e) => {
|
||||
println!("❌ WebSocket握手失败: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let resp = ws::start(connection, &req, stream)?;
|
||||
println!("🔄 启动WebSocket消息处理循环...");
|
||||
// 启动WebSocket消息处理循环
|
||||
actix_web::rt::spawn(async move {
|
||||
println!("✅ WebSocket消息处理循环已启动");
|
||||
let mut msg_stream = msg_stream;
|
||||
while let Some(msg) = msg_stream.next().await {
|
||||
match msg {
|
||||
Ok(Message::Text(text)) => {
|
||||
println!("📨 收到消息: {}", text);
|
||||
// 处理消息
|
||||
}
|
||||
Ok(Message::Binary(bin)) => {
|
||||
println!("📨 收到二进制消息: {} bytes", bin.len());
|
||||
}
|
||||
Ok(Message::Ping(msg)) => {
|
||||
println!("📨 收到Ping");
|
||||
let _ = session.pong(&msg).await;
|
||||
}
|
||||
Ok(Message::Pong(_)) => {
|
||||
println!("📨 收到Pong");
|
||||
}
|
||||
Ok(Message::Close(reason)) => {
|
||||
println!("📨 收到关闭消息: {:?}", reason);
|
||||
break;
|
||||
}
|
||||
Ok(Message::Continuation(_)) => {
|
||||
// 处理 continuation 消息
|
||||
}
|
||||
Ok(Message::Nop) => {
|
||||
// 处理 nop 消息
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ WebSocket错误: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("🔚 WebSocket连接已关闭");
|
||||
});
|
||||
|
||||
println!("✅ WebSocket连接已建立");
|
||||
Ok(resp)
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// 健康检查处理器
|
||||
@@ -386,16 +837,26 @@ async fn test_websocket_connection_send(app_data: web::Data<TaskService>) -> imp
|
||||
|
||||
/// 企业微信回调处理器
|
||||
async fn handle_wechat_callback(req: HttpRequest, body: web::Bytes) -> impl Responder {
|
||||
println!("📱 收到企业微信回调");
|
||||
// 获取企业微信配置,包括debug配置
|
||||
let (_, _, _, _, debug_wechat, _debug_config, _http) = get_wechat_config();
|
||||
|
||||
// 获取请求方法
|
||||
let method = req.method();
|
||||
|
||||
// 获取查询参数
|
||||
let query_string = req.query_string();
|
||||
println!(" 查询参数: {}", query_string);
|
||||
|
||||
// 根据debug配置控制日志输出
|
||||
if debug_wechat {
|
||||
println!("📱 收到企业微信回调");
|
||||
println!(" 请求方法: {}", method);
|
||||
println!(" 查询参数: {}", query_string);
|
||||
}
|
||||
|
||||
// 解析查询参数
|
||||
#[derive(Deserialize)]
|
||||
struct WeChatQuery {
|
||||
signature: String,
|
||||
msg_signature: String,
|
||||
timestamp: String,
|
||||
nonce: String,
|
||||
echostr: Option<String>,
|
||||
@@ -404,49 +865,150 @@ async fn handle_wechat_callback(req: HttpRequest, body: web::Bytes) -> impl Resp
|
||||
let query: WeChatQuery = match web::Query::<WeChatQuery>::from_query(query_string) {
|
||||
Ok(q) => q.into_inner(),
|
||||
Err(e) => {
|
||||
println!("❌ 解析查询参数失败: {}", e);
|
||||
return HttpResponse::BadRequest().json(serde_json::json!({
|
||||
"error": "Invalid query parameters",
|
||||
"message": e.to_string()
|
||||
}));
|
||||
if debug_wechat {
|
||||
println!("❌ 解析查询参数失败: {}", e);
|
||||
}
|
||||
return HttpResponse::BadRequest().body("error");
|
||||
}
|
||||
};
|
||||
|
||||
// 获取企业微信配置
|
||||
let token = env::var("WECHAT_TOKEN").unwrap_or_else(|_| "your_token_here".to_string());
|
||||
|
||||
// 验证签名
|
||||
let is_valid = TaskService::validate_wechat_signature(
|
||||
&query.signature,
|
||||
&query.timestamp,
|
||||
&query.nonce,
|
||||
&token
|
||||
);
|
||||
|
||||
if !is_valid {
|
||||
return HttpResponse::Unauthorized().json(serde_json::json!({
|
||||
"error": "Invalid signature",
|
||||
"message": "签名验证失败"
|
||||
}));
|
||||
// 核心判断:GET 和 POST 必须分开处理
|
||||
if method == &Method::GET {
|
||||
// 1. GET请求 = URL 验证
|
||||
if debug_wechat {
|
||||
println!("🔐 开始URL验证流程");
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
let is_valid = TaskService::validate_wechat_signature(
|
||||
&query.msg_signature,
|
||||
&query.timestamp,
|
||||
&query.nonce,
|
||||
&query.echostr.as_ref().unwrap_or(&String::new()),
|
||||
debug_wechat
|
||||
);
|
||||
|
||||
if !is_valid {
|
||||
if debug_wechat {
|
||||
println!("❌ URL验证签名失败");
|
||||
}
|
||||
return HttpResponse::Unauthorized().body("invalid signature");
|
||||
}
|
||||
|
||||
// 验证通过,返回echostr
|
||||
if let Some(echostr) = query.echostr {
|
||||
if debug_wechat {
|
||||
println!("✅ URL验证成功,返回 echostr: {}", echostr);
|
||||
}
|
||||
return HttpResponse::Ok().body(echostr);
|
||||
} else {
|
||||
if debug_wechat {
|
||||
println!("❌ URL验证失败:缺少echostr参数");
|
||||
}
|
||||
return HttpResponse::BadRequest().body("missing echostr");
|
||||
}
|
||||
} else if method == &Method::POST {
|
||||
// 2. POST请求 = 消息推送
|
||||
if debug_wechat {
|
||||
println!("📥 开始消息推送处理流程");
|
||||
|
||||
// 处理实际的消息回调
|
||||
let body_str = String::from_utf8_lossy(&body);
|
||||
println!(" 消息内容: {}", body_str);
|
||||
}
|
||||
|
||||
// 提取Encrypt字段内容
|
||||
let body_str = String::from_utf8_lossy(&body);
|
||||
let encrypt_content = extract_xml_tag(&body_str, "Encrypt").unwrap_or_default();
|
||||
|
||||
// 验证签名(POST请求需要包含Encrypt字段)
|
||||
if debug_wechat {
|
||||
println!("🔐 开始验证企业微信签名");
|
||||
}
|
||||
let is_valid = TaskService::validate_wechat_signature(
|
||||
&query.msg_signature,
|
||||
&query.timestamp,
|
||||
&query.nonce,
|
||||
&encrypt_content,
|
||||
debug_wechat
|
||||
);
|
||||
|
||||
if !is_valid {
|
||||
if debug_wechat {
|
||||
println!("❌ 消息推送签名验证失败");
|
||||
}
|
||||
return HttpResponse::Unauthorized().body("invalid signature");
|
||||
}
|
||||
|
||||
if debug_wechat {
|
||||
println!("✅ 签名验证通过,开始处理消息");
|
||||
}
|
||||
|
||||
// 解析XML消息
|
||||
let (from_user_name, content, msg_type, event) = parse_wechat_xml_message(&body_str, debug_wechat);
|
||||
|
||||
// 尝试发送回复消息
|
||||
if let Some(user_id) = from_user_name {
|
||||
// 判断是否需要回复:只回复文本消息和非位置上报事件
|
||||
let should_reply = match msg_type.as_deref() {
|
||||
Some("text") => true, // 文本消息需要回复
|
||||
Some("event") => {
|
||||
// 事件消息中,除了位置上报事件外都回复
|
||||
event.as_deref() != Some("LOCATION")
|
||||
}
|
||||
_ => false, // 其他类型消息不回复
|
||||
};
|
||||
|
||||
if should_reply {
|
||||
if debug_wechat {
|
||||
println!("📤 准备发送回复消息给用户: {}", user_id);
|
||||
}
|
||||
|
||||
let reply_content = if let Some(msg_content) = content {
|
||||
format!("我收到你的消息啦!你说: {}", msg_content)
|
||||
} else {
|
||||
"我收到你的消息啦!".to_string()
|
||||
};
|
||||
|
||||
// 异步发送消息(不阻塞主线程)
|
||||
let user_id_clone = user_id.clone();
|
||||
let reply_content_clone = reply_content.clone();
|
||||
let debug_wechat_clone = debug_wechat;
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = send_wechat_message(&user_id_clone, &reply_content_clone, debug_wechat_clone).await {
|
||||
if debug_wechat_clone {
|
||||
println!("❌ 发送回复消息失败: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if debug_wechat {
|
||||
println!("✅ 已开始发送回复消息");
|
||||
}
|
||||
} else {
|
||||
if debug_wechat {
|
||||
println!("⚠️ 消息类型不需要回复,跳过回复");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if debug_wechat {
|
||||
println!("⚠️ 无法获取发送者信息,跳过回复");
|
||||
}
|
||||
}
|
||||
|
||||
// 企业微信要求返回纯文本 "success"
|
||||
if debug_wechat {
|
||||
println!("✅ 企业微信消息处理完成,返回 success");
|
||||
}
|
||||
HttpResponse::Ok().body("success")
|
||||
} else {
|
||||
// 其他请求方法
|
||||
if debug_wechat {
|
||||
println!("❌ 不支持的请求方法: {}", method);
|
||||
}
|
||||
HttpResponse::MethodNotAllowed().body("method not allowed")
|
||||
}
|
||||
|
||||
// 如果是验证请求(首次配置时需要)
|
||||
if let Some(echostr) = query.echostr {
|
||||
println!("✅ 企业微信验证请求,返回 echostr: {}", echostr);
|
||||
return HttpResponse::Ok().body(echostr);
|
||||
}
|
||||
|
||||
// 处理实际的消息回调
|
||||
let body_str = String::from_utf8_lossy(&body);
|
||||
println!(" 消息内容: {}", body_str);
|
||||
|
||||
// TODO: 解析XML消息并处理
|
||||
|
||||
HttpResponse::Ok().json(serde_json::json!({
|
||||
"status": "success",
|
||||
"message": "企业微信回调已接收",
|
||||
"timestamp": utils::current_timestamp()
|
||||
}))
|
||||
}
|
||||
|
||||
/// 微信小程序回调处理器
|
||||
@@ -771,8 +1333,15 @@ async fn system_info(app_data: web::Data<TaskService>) -> impl Responder {
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
// 初始化日志
|
||||
// 获取企业微信配置,包括http日志配置
|
||||
let (_token, _corp_id, _encoding_aes_key, _corp_secret, _debug, _debug_config, http_log) = get_wechat_config();
|
||||
|
||||
// 根据配置决定Actix Web的日志级别
|
||||
let actix_web_log_level = if http_log { "info" } else { "warn" };
|
||||
let log_filter = format!("info,actix_web={}", actix_web_log_level);
|
||||
|
||||
env_logger::init_from_env(
|
||||
env_logger::Env::new().default_filter_or("info,actix_web=info")
|
||||
env_logger::Env::new().default_filter_or(&log_filter)
|
||||
);
|
||||
|
||||
// 由于nginx代理,网关服务监听在8000端口
|
||||
@@ -913,6 +1482,9 @@ async fn main() -> std::io::Result<()> {
|
||||
App::new()
|
||||
.app_data(task_service.clone())
|
||||
.wrap(Logger::default())
|
||||
// 企业微信回调 - 直接匹配企业微信配置路径 /wecom
|
||||
.route("/wecom", web::post().to(handle_wechat_callback))
|
||||
// 其他API路由 - 通过 /api/v1 前缀
|
||||
.service(
|
||||
web::scope("/api/v1")
|
||||
// 健康检查
|
||||
@@ -923,8 +1495,7 @@ async fn main() -> std::io::Result<()> {
|
||||
.route("/task", web::post().to(handle_task))
|
||||
.route("/task/{task_id}", web::get().to(get_task_status))
|
||||
.route("/tasks", web::get().to(list_tasks))
|
||||
// 微信集成
|
||||
.route("/wechat/callback", web::post().to(handle_wechat_callback))
|
||||
// 微信小程序集成
|
||||
.route("/wechat/miniprogram/callback", web::post().to(handle_wechat_miniprogram_callback))
|
||||
// WebSocket连接(内网服务器连接)
|
||||
.route("/ws/control", web::get().to(websocket_handler))
|
||||
@@ -944,12 +1515,15 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
println!("✅ 网关服务已启动在 {} (通过nginx代理)", bind_address);
|
||||
println!("🔍 可用接口:");
|
||||
println!(" 🎯 企业微信回调 - 直接匹配企业微信配置");
|
||||
println!(" POST /wecom - 企业微信回调(必须直接匹配企业微信配置)");
|
||||
println!("");
|
||||
println!(" 📋 API接口(通过 /api/v1 前缀):");
|
||||
println!(" GET /api/v1/health - 健康检查");
|
||||
println!(" GET /api/v1/system - 系统信息");
|
||||
println!(" POST /api/v1/task - 处理任务");
|
||||
println!(" GET /api/v1/task/{{task_id}} - 查询任务状态");
|
||||
println!(" GET /api/v1/task/<task_id> - 查询任务状态");
|
||||
println!(" GET /api/v1/tasks - 查询任务列表");
|
||||
println!(" POST /api/v1/wechat/callback - 企业微信回调");
|
||||
println!(" POST /api/v1/wechat/miniprogram/callback - 微信小程序回调");
|
||||
println!(" GET /api/v1/ws/control - WebSocket控制通道");
|
||||
println!(" GET /api/v1/ws/task - WebSocket任务通道");
|
||||
|
||||
Reference in New Issue
Block a user