diff --git a/Claw/.trae/documents/fix_date_response_plan.md b/Claw/.trae/documents/fix_date_response_plan.md
deleted file mode 100644
index 442bbd1..0000000
--- a/Claw/.trae/documents/fix_date_response_plan.md
+++ /dev/null
@@ -1,61 +0,0 @@
-# SmartClaw 日期回复问题修复计划
-
-## [/] 任务 1: 分析日期回复不正确的原因
-- **优先级**: P0
-- **依赖**: 无
-- **描述**:
- - 分析为什么SmartClaw回复的日期不正确(显示2024年10月30日,而实际是2026年3月23日)
- - 检查LMStudio API调用的参数和系统提示
-- **成功标准**:
- - 确定日期回复不正确的根本原因
-- **测试要求**:
- - `programmatic` TR-1.1: 检查LMStudio API调用的请求参数
- - `human-judgement` TR-1.2: 分析系统提示是否包含日期信息
-
-## [ ] 任务 2: 修改system prompt,添加日期信息
-- **优先级**: P0
-- **依赖**: 任务 1
-- **描述**:
- - 修改system prompt,在其中添加当前日期信息
- - 确保每次调用时都使用最新的日期
-- **成功标准**:
- - system prompt中包含当前日期信息
-- **测试要求**:
- - `programmatic` TR-2.1: 检查system prompt是否包含当前日期
- - `human-judgement` TR-2.2: 验证日期格式是否正确
-
-## [ ] 任务 3: 修改LMStudio API调用,传递日期信息
-- **优先级**: P0
-- **依赖**: 任务 1
-- **描述**:
- - 在调用LMStudio API时,在input参数中添加当前日期信息
- - 确保模型能够获取到最新的日期
-- **成功标准**:
- - LMStudio API调用中包含当前日期信息
-- **测试要求**:
- - `programmatic` TR-3.1: 检查API请求参数是否包含日期信息
- - `human-judgement` TR-3.2: 验证日期信息是否正确传递
-
-## [ ] 任务 4: 测试日期回复功能
-- **优先级**: P0
-- **依赖**: 任务 2, 任务 3
-- **描述**:
- - 测试SmartClaw对日期相关问题的回复
- - 验证回复的日期是否正确
-- **成功标准**:
- - SmartClaw能够正确回复当前日期
-- **测试要求**:
- - `programmatic` TR-4.1: 发送"今天是几月几日?"的问题,检查回复是否包含正确的日期
- - `human-judgement` TR-4.2: 验证回复的日期格式是否合理
-
-## [ ] 任务 5: 优化日期处理逻辑
-- **优先级**: P1
-- **依赖**: 任务 4
-- **描述**:
- - 优化日期处理逻辑,确保在不同场景下都能正确处理日期相关问题
- - 考虑时区和格式问题
-- **成功标准**:
- - 日期处理逻辑能够适应不同的日期相关问题
-- **测试要求**:
- - `programmatic` TR-5.1: 测试不同的日期相关问题,如"明天是几号?"、"昨天是几号?"
- - `human-judgement` TR-5.2: 验证回复是否合理且准确
diff --git a/Claw/Server/SmartClaw/build.bat b/Claw/Server/SmartClaw/build.bat
new file mode 100644
index 0000000..de8cad7
--- /dev/null
+++ b/Claw/Server/SmartClaw/build.bat
@@ -0,0 +1,2 @@
+set RUSTFLAGS=-C target-feature=+crt-static
+cargo build --release
\ No newline at end of file
diff --git a/Claw/Server/SmartClaw/src/websocket_client.rs b/Claw/Server/SmartClaw/src/websocket_client.rs
index 89f8b5c..aa91a85 100644
--- a/Claw/Server/SmartClaw/src/websocket_client.rs
+++ b/Claw/Server/SmartClaw/src/websocket_client.rs
@@ -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) {
+ 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 客户端管理器
diff --git a/Claw/Server/gateway/Cargo.toml b/Claw/Server/gateway/Cargo.toml
index 4dbf17d..fe0609a 100644
--- a/Claw/Server/gateway/Cargo.toml
+++ b/Claw/Server/gateway/Cargo.toml
@@ -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"] }
diff --git a/Claw/Server/gateway/build.bat b/Claw/Server/gateway/build.bat
new file mode 100644
index 0000000..de8cad7
--- /dev/null
+++ b/Claw/Server/gateway/build.bat
@@ -0,0 +1,2 @@
+set RUSTFLAGS=-C target-feature=+crt-static
+cargo build --release
\ No newline at end of file
diff --git a/Claw/Server/gateway/config.json b/Claw/Server/gateway/config.json
new file mode 100644
index 0000000..773e827
--- /dev/null
+++ b/Claw/Server/gateway/config.json
@@ -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
+ }
+}
diff --git a/Claw/Server/gateway/src/communication.rs b/Claw/Server/gateway/src/communication.rs
index 9e30d4e..b8d268e 100644
--- a/Claw/Server/gateway/src/communication.rs
+++ b/Claw/Server/gateway/src/communication.rs
@@ -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
}
diff --git a/Claw/Server/gateway/src/main.rs b/Claw/Server/gateway/src/main.rs
index 37950bd..eafd64d 100644
--- a/Claw/Server/gateway/src/main.rs
+++ b/Claw/Server/gateway/src/main.rs
@@ -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,
+}
/// 获取当前本地时间的格式化字符串
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>> =
+ LazyLock::new(|| StdRwLock::new(HashMap::new()));
+
+use aes::Aes128;
+use cbc::cipher::{BlockDecryptMut, KeyIvInit};
+type Aes128CbcDec = cbc::Decryptor;
+
+/// 微信小程序手机号解密(AES-128-CBC + PKCS7)
+fn decrypt_wechat_phone(session_key: &str, encrypted_data: &str, iv: &str) -> Result {
+ 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::(&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, session_key: Option, errmsg: Option }
+ 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::(&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::(&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, Option, Option, Option) {
if debug {
@@ -417,6 +563,7 @@ struct TaskService {
connection_manager: Arc>,
websocket_pool: WebSocketPool,
communication_config: CommunicationConfig,
+ sse_manager: Arc,
}
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::(&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,
session_key: Option,
}
- let query: MiniProgramQuery = match web::Query::::from_query(query_string) {
+ let query: MiniAppQuery = match web::Query::::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,
}
+/// 微信小程序手机号解密处理器
+async fn decrypt_phone_number(req: web::Json) -> 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) -> impl Responder {
let manager = app_data.connection_manager.read().await;
@@ -1461,7 +1781,7 @@ async fn system_info(app_data: web::Data) -> 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))
diff --git a/Claw/Server/gateway/src/sse_manager.rs b/Claw/Server/gateway/src/sse_manager.rs
new file mode 100644
index 0000000..2c6e264
--- /dev/null
+++ b/Claw/Server/gateway/src/sse_manager.rs
@@ -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>>>>,
+}
+
+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>) {
+ let mut pending = self.pending.write().await;
+ pending.insert(request_id, tx);
+ }
+
+ /// 根据 request_id 获取对应的响应 channel
+ pub async fn get(&self, request_id: &str) -> Option>> {
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/Claw/Server/gateway/src/sse_proxy.rs b/Claw/Server/gateway/src/sse_proxy.rs
new file mode 100644
index 0000000..64def78
--- /dev/null
+++ b/Claw/Server/gateway/src/sse_proxy.rs
@@ -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,
+ 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,
+ pool: web::Data,
+ sse_manager: web::Data,
+) -> 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::>(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)
+}
diff --git a/Claw/client/wechat_app/app.js b/Claw/client/wechat_app/app.js
index 14ee631..9509865 100644
--- a/Claw/client/wechat_app/app.js
+++ b/Claw/client/wechat_app/app.js
@@ -11,6 +11,9 @@ App({
// 初始化网络监听
this.initNetworkListener()
+
+ // 初始化用户信息
+ this.initUserInfo()
},
onShow: function () {
@@ -25,6 +28,207 @@ App({
console.error('小程序错误:', msg)
},
+ // 初始化用户信息
+ initUserInfo: function() {
+ // 从本地存储获取用户信息
+ const storedUserInfo = wx.getStorageSync('userInfo')
+ if (storedUserInfo) {
+ this.globalData.userInfo = storedUserInfo
+ console.log('从本地存储加载用户信息:', storedUserInfo)
+ }
+
+ // 检查授权状态
+ this.checkAuthStatus()
+ },
+
+ // 检查授权状态
+ checkAuthStatus: function() {
+ wx.getSetting({
+ success: (res) => {
+ console.log('授权状态:', res.authSetting)
+ // 检查用户信息授权
+ if (res.authSetting['scope.userInfo']) {
+ console.log('已授权用户信息')
+ } else {
+ console.log('未授权用户信息')
+ }
+
+ // 检查地理位置授权
+ if (res.authSetting['scope.userLocation']) {
+ console.log('已授权地理位置')
+ } else {
+ console.log('未授权地理位置')
+ }
+ }
+ })
+ },
+
+ // 获取用户信息
+ getUserInfo: function(cb) {
+ const that = this
+
+ if (this.globalData.userInfo) {
+ typeof cb == "function" && cb(this.globalData.userInfo)
+ } else {
+ // 检查本地存储是否有用户信息
+ const storedUserInfo = wx.getStorageSync('userInfo')
+ if (storedUserInfo) {
+ that.globalData.userInfo = storedUserInfo
+ typeof cb == "function" && cb(storedUserInfo)
+ } else {
+ // 未授权,需要用户点击授权
+ typeof cb == "function" && cb(null)
+ }
+ }
+ },
+
+ // 触发用户授权
+ triggerUserAuth: function(cb) {
+ const that = this
+
+ wx.getUserProfile({
+ desc: '用于完善个人资料,提供个性化服务',
+ success: (res) => {
+ that.globalData.userInfo = res.userInfo
+ // 存储到本地
+ wx.setStorageSync('userInfo', res.userInfo)
+ // 获取全局唯一ID
+ that.getGlobalUserId((userId) => {
+ typeof cb == "function" && cb(res.userInfo)
+ })
+ },
+ fail: (err) => {
+ console.error('获取用户信息失败:', err)
+ typeof cb == "function" && cb(null)
+ }
+ })
+ },
+
+ // 获取全局唯一ID(UnionID或OpenID)
+ getGlobalUserId: function(cb) {
+ const that = this
+
+ // 尝试从本地存储获取
+ let userId = wx.getStorageSync('globalUserId')
+
+ if (userId) {
+ typeof cb == "function" && cb(userId)
+ return
+ }
+
+ // 调用微信登录获取code
+ wx.login({
+ success: (res) => {
+ if (res.code) {
+ // 这里应该调用后端接口获取UnionID
+ // 由于是模拟环境,我们使用code生成一个假的UnionID
+ // 实际项目中应该发送code到后端,后端调用微信接口获取UnionID
+ const mockUnionId = `union_${res.code}_${Date.now()}`
+ userId = mockUnionId
+
+ // 存储到本地
+ wx.setStorageSync('globalUserId', userId)
+ that.globalData.globalUserId = userId
+
+ typeof cb == "function" && cb(userId)
+ } else {
+ console.error('获取微信登录凭证失败:', res.errMsg)
+ // 失败时生成一个基于设备的ID作为备选
+ const deviceInfo = that.globalData.systemInfo
+ const fallbackId = `fallback_${deviceInfo.model.replace(/\s+/g, '_')}_${Date.now()}`
+ wx.setStorageSync('globalUserId', fallbackId)
+ that.globalData.globalUserId = fallbackId
+ typeof cb == "function" && cb(fallbackId)
+ }
+ },
+ fail: (err) => {
+ console.error('微信登录失败:', err)
+ // 失败时生成一个基于设备的ID作为备选
+ const deviceInfo = that.globalData.systemInfo
+ const fallbackId = `fallback_${deviceInfo.model.replace(/\s+/g, '_')}_${Date.now()}`
+ wx.setStorageSync('globalUserId', fallbackId)
+ that.globalData.globalUserId = fallbackId
+ typeof cb == "function" && cb(fallbackId)
+ }
+ })
+ },
+
+ // 获取手机号
+ getPhoneNumber: function(e, cb) {
+ const that = this
+
+ if (e.detail.encryptedData && e.detail.iv) {
+ // 调用网关接口解密手机号
+ wx.request({
+ url: `${that.globalData.gatewayUrl}/v1/wechat/phone`,
+ method: 'POST',
+ data: {
+ encryptedData: e.detail.encryptedData,
+ iv: e.detail.iv,
+ code: e.detail.code // 有些版本会返回code
+ },
+ header: {
+ 'Content-Type': 'application/json'
+ },
+ success: (res) => {
+ if (res.data && res.data.phoneNumber) {
+ const phoneNumber = res.data.phoneNumber
+ // 存储到本地
+ wx.setStorageSync('phoneNumber', phoneNumber)
+ that.globalData.phoneNumber = phoneNumber
+ typeof cb == "function" && cb(phoneNumber)
+ } else {
+ console.error('解密手机号失败:', res.data)
+ typeof cb == "function" && cb(null)
+ }
+ },
+ fail: (err) => {
+ console.error('请求网关失败:', err)
+ typeof cb == "function" && cb(null)
+ }
+ })
+ } else {
+ console.error('用户拒绝授权手机号或获取加密数据失败')
+ typeof cb == "function" && cb(null)
+ }
+ },
+
+ // 获取地理位置信息
+ getLocation: function(cb) {
+ wx.getLocation({
+ type: 'wgs84',
+ success: (res) => {
+ const locationInfo = {
+ latitude: res.latitude,
+ longitude: res.longitude,
+ speed: res.speed,
+ accuracy: res.accuracy
+ }
+ // 存储到本地
+ wx.setStorageSync('locationInfo', locationInfo)
+ typeof cb == "function" && cb(locationInfo)
+ },
+ fail: (err) => {
+ console.error('获取地理位置失败:', err)
+ typeof cb == "function" && cb(null)
+ }
+ })
+ },
+
+ // 获取设备信息
+ getDeviceInfo: function() {
+ return this.globalData.systemInfo
+ },
+
+ // 获取网络状态
+ getNetworkType: function(cb) {
+ wx.getNetworkType({
+ success: (res) => {
+ typeof cb == "function" && cb(res.networkType)
+ }
+ })
+ },
+
// 检查更新
checkForUpdate: function() {
if (wx.canIUse('getUpdateManager')) {
@@ -72,9 +276,12 @@ App({
// 全局数据
globalData: {
userInfo: null,
+ globalUserId: null,
+ phoneNumber: null,
systemInfo: null,
apiBase: 'https://pactgo.cn/api/v1',
websocketUrl: 'wss://pactgo.cn/ws/task',
+ gatewayUrl: 'http://localhost:8000',
version: '1.0.0'
}
})
\ No newline at end of file
diff --git a/Claw/client/wechat_app/app.json b/Claw/client/wechat_app/app.json
index 35d4220..ad990ea 100644
--- a/Claw/client/wechat_app/app.json
+++ b/Claw/client/wechat_app/app.json
@@ -1,60 +1 @@
-{
- "desc": "智控未来 - 企业微信智能控制系统",
- "pages": [
- "pages/index/index",
- "pages/chat/chat",
- "pages/task/task",
- "pages/user/user"
- ],
- "window": {
- "backgroundTextStyle": "light",
- "navigationBarBackgroundColor": "#fff",
- "navigationBarTitleText": "智控未来",
- "navigationBarTextStyle": "black"
- },
- "tabBar": {
- "color": "#7A7E83",
- "selectedColor": "#3cc51f",
- "borderStyle": "black",
- "backgroundColor": "#ffffff",
- "list": [
- {
- "pagePath": "pages/index/index",
- "iconPath": "assets/icons/home.png",
- "selectedIconPath": "assets/icons/home-active.png",
- "text": "首页"
- },
- {
- "pagePath": "pages/chat/chat",
- "iconPath": "assets/icons/chat.png",
- "selectedIconPath": "assets/icons/chat-active.png",
- "text": "聊天"
- },
- {
- "pagePath": "pages/task/task",
- "iconPath": "assets/icons/task.png",
- "selectedIconPath": "assets/icons/task-active.png",
- "text": "任务"
- },
- {
- "pagePath": "pages/user/user",
- "iconPath": "assets/icons/user.png",
- "selectedIconPath": "assets/icons/user-active.png",
- "text": "我的"
- }
- ]
- },
- "networkTimeout": {
- "request": 10000,
- "downloadFile": 10000,
- "uploadFile": 10000,
- "websocket": 10000
- },
- "permission": {
- "scope.userLocation": {
- "desc": "你的位置信息将用于小程序位置接口的效果展示"
- }
- },
- "requiredBackgroundModes": ["audio"],
- "requiredPrivateInfos": ["getLocation"]
-}
\ No newline at end of file
+{"desc": "智控未来 - 企业微信智能控制系统", "lazyCodeLoading": "requiredComponents", "pages": ["pages/index/index", "pages/chat/chat", "pages/task/task", "pages/user/user"], "window": {"backgroundTextStyle": "light", "navigationBarBackgroundColor": "#1a1a1a", "navigationBarTitleText": "智控未来", "navigationBarTextStyle": "white"}, "tabBar": {"custom": true, "color": "#999999", "selectedColor": "#333333", "borderStyle": "black", "backgroundColor": "#ffffff", "list": [{"pagePath": "pages/index/index", "iconPath": "assets/icons/home.png", "selectedIconPath": "assets/icons/home-active.png", "text": "首页"}, {"pagePath": "pages/chat/chat", "iconPath": "assets/icons/chat.png", "selectedIconPath": "assets/icons/chat-active.png", "text": "设备"}, {"pagePath": "pages/task/task", "iconPath": "assets/icons/task.png", "selectedIconPath": "assets/icons/task-active.png", "text": "任务"}]}, "networkTimeout": {"request": 10000, "downloadFile": 10000, "uploadFile": 10000, "websocket": 10000}, "permission": {"scope.userLocation": {"desc": "你的位置信息将用于小程序位置接口的效果展示"}}, "requiredBackgroundModes": ["audio"], "requiredPrivateInfos": ["getLocation"]}
\ No newline at end of file
diff --git a/Claw/client/wechat_app/app.wxss b/Claw/client/wechat_app/app.wxss
index 0e50f7d..0097829 100644
--- a/Claw/client/wechat_app/app.wxss
+++ b/Claw/client/wechat_app/app.wxss
@@ -1,108 +1,109 @@
-/* 全局样式 */
+/* 全局深色主题样式 */
page {
- background-color: #f5f5f5;
+ background-color: #0d0d0d;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimSun, sans-serif;
+ color: #ffffff;
}
-/* 通用容器 */
+/* 通用容器 - 深色主题 */
.container {
padding: 20rpx;
- background-color: #fff;
+ background-color: #1a1a1a;
margin: 20rpx;
- border-radius: 10rpx;
- box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
+ border-radius: 16rpx;
}
/* 按钮样式 */
.btn {
- background-color: #07c160;
+ background: linear-gradient(135deg, #1677FF 0%, #0958d9 100%);
color: white;
border: none;
- border-radius: 5rpx;
+ border-radius: 12rpx;
padding: 20rpx 40rpx;
font-size: 28rpx;
text-align: center;
- transition: background-color 0.3s;
+ transition: all 0.3s ease;
}
.btn:hover {
- background-color: #06a050;
+ opacity: 0.9;
}
.btn:active {
- background-color: #059040;
+ opacity: 0.85;
+ transform: scale(0.98);
}
.btn:disabled {
- background-color: #ccc;
- color: #999;
+ background-color: #333333;
+ color: #666666;
}
-/* 输入框样式 */
+/* 输入框样式 - 深色 */
.input {
- border: 1rpx solid #ddd;
- border-radius: 5rpx;
+ border: 2rpx solid #2a2a2a;
+ border-radius: 12rpx;
padding: 20rpx;
font-size: 28rpx;
- background-color: #fff;
+ background-color: #1a1a1a;
+ color: #ffffff;
}
.input:focus {
- border-color: #07c160;
+ border-color: #1677FF;
outline: none;
}
-/* 卡片样式 */
+/* 卡片样式 - 深色 */
.card {
- background-color: #fff;
- border-radius: 10rpx;
+ background-color: #1a1a1a;
+ border-radius: 16rpx;
padding: 30rpx;
margin: 20rpx 0;
- box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
/* 标题样式 */
.title {
font-size: 32rpx;
font-weight: bold;
- color: #333;
+ color: #ffffff;
margin-bottom: 20rpx;
}
/* 副标题样式 */
.subtitle {
font-size: 24rpx;
- color: #666;
+ color: #999999;
margin-bottom: 10rpx;
}
/* 文本样式 */
.text {
font-size: 28rpx;
- color: #333;
- line-height: 1.5;
+ color: #e5e5e5;
+ line-height: 1.6;
}
.text-small {
font-size: 24rpx;
- color: #999;
+ color: #888888;
}
-/* 状态颜色 */
+/* 状态颜色 - 深色主题适配 */
.status-success {
- color: #07c160;
+ color: #52c41a;
}
.status-warning {
- color: #f0ad4e;
+ color: #faad14;
}
.status-error {
- color: #dd524d;
+ color: #ff4d4f;
}
.status-info {
- color: #10aeff;
+ color: #1677FF;
}
/* 加载动画 */
@@ -116,8 +117,8 @@ page {
.loading-spinner {
width: 40rpx;
height: 40rpx;
- border: 4rpx solid #f3f3f3;
- border-top: 4rpx solid #07c160;
+ border: 4rpx solid #2a2a2a;
+ border-top: 4rpx solid #1677FF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@@ -127,62 +128,49 @@ page {
100% { transform: rotate(360deg); }
}
-/* 消息样式 */
+/* 消息样式 - 深色主题 */
.message {
padding: 20rpx;
margin: 10rpx 0;
- border-radius: 10rpx;
- background-color: #f8f8f8;
+ border-radius: 12rpx;
+ background-color: #1a1a1a;
}
.message.user {
- background-color: #95ec69;
+ background-color: #1677FF;
text-align: right;
}
.message.system {
- background-color: #e8f5e8;
+ background-color: #222222;
text-align: left;
}
-/* 任务状态 */
+/* 任务状态标签 */
.task-status {
display: inline-block;
- padding: 5rpx 15rpx;
+ padding: 6rpx 18rpx;
border-radius: 20rpx;
- font-size: 24rpx;
+ font-size: 22rpx;
font-weight: bold;
}
.task-status.pending {
- background-color: #f0ad4e;
- color: white;
+ background-color: rgba(250, 173, 20, 0.15);
+ color: #faad14;
}
.task-status.processing {
- background-color: #10aeff;
- color: white;
+ background-color: rgba(22, 119, 255, 0.15);
+ color: #1677FF;
}
.task-status.completed {
- background-color: #07c160;
- color: white;
+ background-color: rgba(82, 196, 26, 0.15);
+ color: #52c41a;
}
.task-status.failed {
- background-color: #dd524d;
- color: white;
+ background-color: rgba(255, 77, 79, 0.15);
+ color: #ff4d4f;
}
-
-/* 响应式布局 */
-@media (max-width: 750rpx) {
- .container {
- margin: 10rpx;
- padding: 15rpx;
- }
-
- .card {
- padding: 20rpx;
- margin: 15rpx 0;
- }
-}
\ No newline at end of file
diff --git a/Claw/client/wechat_app/assets/icons/send.png b/Claw/client/wechat_app/assets/icons/send.png
new file mode 100644
index 0000000..d9410bf
Binary files /dev/null and b/Claw/client/wechat_app/assets/icons/send.png differ
diff --git a/Claw/client/wechat_app/assets/icons/send@2x.png b/Claw/client/wechat_app/assets/icons/send@2x.png
new file mode 100644
index 0000000..e3bd34b
Binary files /dev/null and b/Claw/client/wechat_app/assets/icons/send@2x.png differ
diff --git a/Claw/client/wechat_app/assets/icons/send@3x.png b/Claw/client/wechat_app/assets/icons/send@3x.png
new file mode 100644
index 0000000..c99c1af
Binary files /dev/null and b/Claw/client/wechat_app/assets/icons/send@3x.png differ
diff --git a/Claw/client/wechat_app/components/auth-prompt/auth-prompt.js b/Claw/client/wechat_app/components/auth-prompt/auth-prompt.js
new file mode 100644
index 0000000..b50ac5d
--- /dev/null
+++ b/Claw/client/wechat_app/components/auth-prompt/auth-prompt.js
@@ -0,0 +1,139 @@
+// 授权提示组件
+Component({
+ properties: {
+ show: {
+ type: Boolean,
+ value: false
+ },
+ authType: {
+ type: String,
+ value: 'userInfo' // userInfo, location, camera, record
+ }
+ },
+
+ data: {
+ authConfig: {
+ userInfo: {
+ title: '授权用户信息',
+ desc: '为了提供更好的服务,需要获取您的用户信息',
+ buttonText: '授权',
+ icon: '/assets/images/user-avatar.png'
+ },
+ location: {
+ title: '授权地理位置',
+ desc: '为了提供基于位置的服务,需要获取您的地理位置',
+ buttonText: '授权',
+ icon: '/assets/images/location-icon.png'
+ },
+ camera: {
+ title: '授权相机',
+ desc: '为了拍摄照片或视频,需要获取相机权限',
+ buttonText: '授权',
+ icon: '/assets/images/camera-icon.png'
+ },
+ record: {
+ title: '授权录音',
+ desc: '为了录制语音,需要获取录音权限',
+ buttonText: '授权',
+ icon: '/assets/images/mic-icon.png'
+ }
+ }
+ },
+
+ methods: {
+ // 授权按钮点击
+ handleAuth() {
+ const { authType } = this.properties
+
+ switch (authType) {
+ case 'userInfo':
+ this.authUserInfo()
+ break
+ case 'location':
+ this.authLocation()
+ break
+ case 'camera':
+ this.authCamera()
+ break
+ case 'record':
+ this.authRecord()
+ break
+ }
+ },
+
+ // 授权用户信息
+ authUserInfo() {
+ const app = getApp()
+ app.triggerUserAuth((userInfo) => {
+ if (userInfo) {
+ this.triggerEvent('authSuccess', { authType: 'userInfo', data: userInfo })
+ this.setData({ show: false })
+ } else {
+ console.error('用户信息授权失败')
+ this.triggerEvent('authFail', { authType: 'userInfo', error: '授权失败' })
+ }
+ })
+ },
+
+ // 授权地理位置
+ authLocation() {
+ wx.getLocation({
+ type: 'wgs84',
+ success: (res) => {
+ this.triggerEvent('authSuccess', { authType: 'location', data: res })
+ this.setData({ show: false })
+ },
+ fail: (err) => {
+ console.error('地理位置授权失败:', err)
+ this.triggerEvent('authFail', { authType: 'location', error: err })
+ }
+ })
+ },
+
+ // 授权相机
+ authCamera() {
+ wx.authorize({
+ scope: 'scope.camera',
+ success: () => {
+ this.triggerEvent('authSuccess', { authType: 'camera' })
+ this.setData({ show: false })
+ },
+ fail: (err) => {
+ console.error('相机授权失败:', err)
+ this.triggerEvent('authFail', { authType: 'camera', error: err })
+ }
+ })
+ },
+
+ // 授权录音
+ authRecord() {
+ wx.authorize({
+ scope: 'scope.record',
+ success: () => {
+ this.triggerEvent('authSuccess', { authType: 'record' })
+ this.setData({ show: false })
+ },
+ fail: (err) => {
+ console.error('录音授权失败:', err)
+ this.triggerEvent('authFail', { authType: 'record', error: err })
+ }
+ })
+ },
+
+ // 取消授权
+ handleCancel() {
+ this.triggerEvent('authCancel', { authType: this.properties.authType })
+ this.setData({ show: false })
+ },
+
+ // 打开设置
+ openSettings() {
+ wx.openSetting({
+ success: (res) => {
+ console.log('设置结果:', res.authSetting)
+ this.triggerEvent('settingsOpen', { authType: this.properties.authType, settings: res.authSetting })
+ }
+ })
+ }
+ }
+})
\ No newline at end of file
diff --git a/Claw/client/wechat_app/components/auth-prompt/auth-prompt.json b/Claw/client/wechat_app/components/auth-prompt/auth-prompt.json
new file mode 100644
index 0000000..3c698e1
--- /dev/null
+++ b/Claw/client/wechat_app/components/auth-prompt/auth-prompt.json
@@ -0,0 +1,5 @@
+{
+ "component": true,
+ "usingComponents": {},
+ "description": "授权提示组件"
+}
\ No newline at end of file
diff --git a/Claw/client/wechat_app/components/auth-prompt/auth-prompt.wxml b/Claw/client/wechat_app/components/auth-prompt/auth-prompt.wxml
new file mode 100644
index 0000000..81223b0
--- /dev/null
+++ b/Claw/client/wechat_app/components/auth-prompt/auth-prompt.wxml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ {{authConfig[authType].title}}
+ {{authConfig[authType].desc}}
+
+
+
+
+
+ 如果授权失败,请在
+ 设置
+ 中开启权限
+
+
+
\ No newline at end of file
diff --git a/Claw/client/wechat_app/components/auth-prompt/auth-prompt.wxss b/Claw/client/wechat_app/components/auth-prompt/auth-prompt.wxss
new file mode 100644
index 0000000..c4e9a08
--- /dev/null
+++ b/Claw/client/wechat_app/components/auth-prompt/auth-prompt.wxss
@@ -0,0 +1,106 @@
+.auth-prompt {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 9999;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.auth-prompt-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+.auth-prompt-content {
+ position: relative;
+ width: 80%;
+ max-width: 400rpx;
+ background-color: #fff;
+ border-radius: 16rpx;
+ padding: 40rpx;
+ box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.auth-prompt-icon {
+ width: 120rpx;
+ height: 120rpx;
+ margin-bottom: 32rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.auth-prompt-icon image {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+}
+
+.auth-prompt-title {
+ font-size: 32rpx;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 16rpx;
+}
+
+.auth-prompt-desc {
+ font-size: 24rpx;
+ color: #666;
+ text-align: center;
+ line-height: 36rpx;
+ margin-bottom: 40rpx;
+}
+
+.auth-prompt-buttons {
+ width: 100%;
+ display: flex;
+ margin-bottom: 24rpx;
+}
+
+.auth-prompt-cancel {
+ flex: 1;
+ height: 80rpx;
+ line-height: 80rpx;
+ font-size: 28rpx;
+ color: #666;
+ background-color: #f5f5f5;
+ border-radius: 8rpx 0 0 8rpx;
+ border: none;
+}
+
+.auth-prompt-confirm {
+ flex: 1;
+ height: 80rpx;
+ line-height: 80rpx;
+ font-size: 28rpx;
+ color: #fff;
+ background-color: #07c160;
+ border-radius: 0 8rpx 8rpx 0;
+ border: none;
+}
+
+.auth-prompt-tip {
+ font-size: 20rpx;
+ color: #999;
+ text-align: center;
+}
+
+.auth-prompt-settings {
+ color: #07c160;
+ margin: 0 4rpx;
+}
+
+.auth-prompt-settings:active {
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/Claw/client/wechat_app/components/task-card/task-card.wxss b/Claw/client/wechat_app/components/task-card/task-card.wxss
index 626d29d..6b2b2ff 100644
--- a/Claw/client/wechat_app/components/task-card/task-card.wxss
+++ b/Claw/client/wechat_app/components/task-card/task-card.wxss
@@ -1,27 +1,26 @@
-/* 任务卡片组件样式 */
+/* 任务卡片组件样式 - 深色主题 */
.task-card {
- background: white;
+ background: linear-gradient(180deg, #1a1a1a 0%, #161616 100%);
border-radius: 20rpx;
padding: 30rpx;
margin: 20rpx 0;
- box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
- border-left: 8rpx solid #e0e0e0;
+ border-left: 8rpx solid #2a2a2a;
}
.task-card.pending {
- border-left-color: #f0ad4e;
+ border-left-color: #faad14;
}
.task-card.processing {
- border-left-color: #10aeff;
+ border-left-color: #1677FF;
}
.task-card.completed {
- border-left-color: #07c160;
+ border-left-color: #52c41a;
}
.task-card.failed {
- border-left-color: #dd524d;
+ border-left-color: #ff4d4f;
}
.task-header {
@@ -30,13 +29,13 @@
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
- border-bottom: 1rpx solid #f0f0f0;
+ border-bottom: 2rpx solid rgba(255,255,255,0.06);
}
.task-title {
font-size: 32rpx;
font-weight: bold;
- color: #333;
+ color: #ffffff;
flex: 1;
margin-right: 20rpx;
}
@@ -50,23 +49,23 @@
}
.task-status.pending {
- background-color: #fff3cd;
- color: #856404;
+ background-color: rgba(250,173,20,0.15);
+ color: #faad14;
}
.task-status.processing {
- background-color: #cce5ff;
- color: #004085;
+ background-color: rgba(22,119,255,0.15);
+ color: #1677FF;
}
.task-status.completed {
- background-color: #d4edda;
- color: #155724;
+ background-color: rgba(82,196,26,0.15);
+ color: #52c41a;
}
.task-status.failed {
- background-color: #f8d7da;
- color: #721c24;
+ background-color: rgba(255,77,79,0.15);
+ color: #ff4d4f;
}
.task-content {
@@ -75,7 +74,7 @@
.task-description {
font-size: 28rpx;
- color: #666;
+ color: #aaaaaa;
line-height: 1.5;
margin-bottom: 20rpx;
}
@@ -90,10 +89,11 @@
.task-type,
.task-priority {
font-size: 24rpx;
- color: #999;
- background: #f8f8f8;
+ color: #888888;
+ background: #222222;
padding: 8rpx 16rpx;
border-radius: 10rpx;
+ border: 2rpx solid rgba(255,255,255,0.04);
}
.task-timeline {
@@ -104,7 +104,7 @@
.task-time {
font-size: 24rpx;
- color: #999;
+ color: #666666;
}
.task-actions {
@@ -113,7 +113,7 @@
gap: 15rpx;
margin-top: 20rpx;
padding-top: 20rpx;
- border-top: 1rpx solid #f0f0f0;
+ border-top: 2rpx solid rgba(255,255,255,0.06);
}
.action-btn {
@@ -127,27 +127,27 @@
}
.action-btn.primary {
- background: linear-gradient(135deg, #07c160 0%, #06a050 100%);
+ background: linear-gradient(135deg, #1677FF 0%, #0958d9 100%);
color: white;
}
.action-btn.success {
- background: linear-gradient(135deg, #28a745 0%, #218838 100%);
+ background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
color: white;
}
.action-btn.warning {
- background: linear-gradient(135deg, #ffc107 0%, #e0a800 100%);
- color: #212529;
+ background: linear-gradient(135deg, #faad14 0%, #d48806 100%);
+ color: #1a1a1a;
}
.action-btn.info {
- background: linear-gradient(135deg, #17a2b8 0%, #138496 100%);
+ background: linear-gradient(135deg, #13c2c2 0%, #08979c 100%);
color: white;
}
.action-btn.danger {
- background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
+ background: linear-gradient(135deg, #ff4d4f 0%, #cf1322 100%);
color: white;
}
@@ -164,21 +164,22 @@
.progress-text {
font-size: 24rpx;
- color: #666;
+ color: #888888;
min-width: 60rpx;
text-align: right;
}
.task-result {
- background: #f8f9fa;
+ background: #141414;
border-radius: 10rpx;
padding: 20rpx;
margin-top: 20rpx;
+ border: 2rpx solid rgba(255,255,255,0.06);
}
.result-label {
font-size: 24rpx;
- color: #666;
+ color: #888888;
font-weight: bold;
margin-bottom: 10rpx;
display: block;
@@ -186,10 +187,10 @@
.result-content {
font-size: 26rpx;
- color: #333;
+ color: #e5e5e5;
line-height: 1.5;
- background: white;
+ background: #0d0d0d;
padding: 15rpx;
border-radius: 8rpx;
- border: 1rpx solid #e9ecef;
-}
\ No newline at end of file
+ border: 2rpx solid #252525;
+}
diff --git a/Claw/client/wechat_app/components/user-avatar/user-avatar.wxss b/Claw/client/wechat_app/components/user-avatar/user-avatar.wxss
index 075e391..68c2576 100644
--- a/Claw/client/wechat_app/components/user-avatar/user-avatar.wxss
+++ b/Claw/client/wechat_app/components/user-avatar/user-avatar.wxss
@@ -1,4 +1,4 @@
-/* 用户头像组件样式 */
+/* 用户头像组件样式 - 深色适配 */
.user-avatar {
position: relative;
display: inline-block;
@@ -29,10 +29,11 @@
/* 形状样式 */
.user-avatar.circle {
border-radius: 50%;
+ border: 3rpx solid rgba(22,119,255,0.35);
}
.user-avatar.rounded {
- border-radius: 10rpx;
+ border-radius: 12rpx;
}
.user-avatar.square {
@@ -54,24 +55,27 @@
width: 20rpx;
height: 20rpx;
border-radius: 50%;
- border: 4rpx solid white;
+ border: 3rpx solid #1a1a1a;
z-index: 1;
}
.status-indicator.online {
- background-color: #07c160;
+ background-color: #52c41a;
+ box-shadow: 0 0 6rpx rgba(82,196,26,0.5);
}
.status-indicator.offline {
- background-color: #999;
+ background-color: #555555;
}
.status-indicator.busy {
- background-color: #f0ad4e;
+ background-color: #faad14;
+ box-shadow: 0 0 6rpx rgba(250,173,20,0.5);
}
.status-indicator.away {
- background-color: #10aeff;
+ background-color: #1677FF;
+ box-shadow: 0 0 6rpx rgba(22,119,255,0.5);
}
/* 徽章 */
@@ -79,7 +83,7 @@
position: absolute;
top: -10rpx;
right: -10rpx;
- background-color: #dd524d;
+ background: linear-gradient(135deg, #ff4d4f, #cf1322);
color: white;
border-radius: 50%;
min-width: 32rpx;
@@ -91,6 +95,7 @@
font-weight: bold;
padding: 0 8rpx;
z-index: 2;
+ box-shadow: 0 4rpx 12rpx rgba(255,77,79,0.4);
}
.badge-text {
@@ -98,14 +103,14 @@
line-height: 1;
}
-/* 加载遮罩 */
+/* 加载遮罩 - 深色 */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
- background-color: rgba(255, 255, 255, 0.8);
+ background-color: rgba(26,26,26,0.85);
display: flex;
align-items: center;
justify-content: center;
@@ -115,8 +120,8 @@
.loading-spinner {
width: 40rpx;
height: 40rpx;
- border: 4rpx solid #f3f3f3;
- border-top: 4rpx solid #07c160;
+ border: 4rpx solid #2a2a2a;
+ border-top: 4rpx solid #1677FF;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@@ -134,10 +139,10 @@
left: 0;
right: 0;
bottom: 0;
- background: linear-gradient(135deg, #07c160 0%, #06a050 100%);
+ background: linear-gradient(135deg, #1677FF 0%, #0958d9 100%);
z-index: -1;
}
.user-avatar.error::before {
- background: linear-gradient(135deg, #dd524d 0%, #c82333 100%);
-}
\ No newline at end of file
+ background: linear-gradient(135deg, #ff4d4f 0%, #cf1322 100%);
+}
diff --git a/Claw/client/wechat_app/custom-tab-bar/index.js b/Claw/client/wechat_app/custom-tab-bar/index.js
new file mode 100644
index 0000000..c73300f
--- /dev/null
+++ b/Claw/client/wechat_app/custom-tab-bar/index.js
@@ -0,0 +1,377 @@
+/**
+ * 自定义底部导航栏组件 - 公众号客服交互模式
+ *
+ * 功能清单:
+ * 1. 3个Tab切换(首页/设备/任务)
+ * 2. 右侧键盘图标 → 跳转到聊天页
+ * 3. 聊天输入模式 - 文字模式(默认)
+ * 4. 左侧🔊小喇叭 → 切换到语音录制模式
+ * 5. 语音模式下左侧⌨键盘 → 切回文字模式
+ * 6. 按住说话 → 松开发送
+ * 7. 语音转文字(模拟)
+ */
+
+Component({
+ data: {
+ // ---- TabBar 相关 ----
+ currentTab: 0,
+ tabList: [
+ { pagePath: '/pages/index/index', icon: '/assets/icons/home.png', selectedIcon: '/assets/icons/home-active.png', text: '首页' },
+ { pagePath: '/pages/chat/chat', icon: '/assets/icons/chat.png', selectedIcon: '/assets/icons/chat-active.png', text: '设备' },
+ { pagePath: '/pages/task/task', icon: '/assets/icons/task.png', selectedIcon: '/assets/icons/task-active.png', text: '任务' }
+ ],
+
+ // ---- 聊天面板相关 ----
+ showChatPanel: false,
+
+ // ---- 输入模式 ----
+ voiceMode: false,
+ inputText: '',
+ hasInput: false,
+ inputFocus: true,
+
+ // ---- 语音录制相关 ----
+ isRecording: false,
+ recordStartTime: 0,
+ recorderManager: null,
+ tempFilePath: '',
+
+ // ---- 语音转文字 ----
+ isVoiceToText: false,
+
+ // ---- 软键盘适配 ----
+ keyboardHeight: 0,
+ tabBarBottom: 0,
+ },
+
+ lifetimes: {
+ attached() {
+ this._initRecorder()
+ this._initAudioPlayer()
+ this._calcKeyboardStyle()
+ }
+ },
+
+ pageLifetimes: {
+ show() {
+ const pages = getCurrentPages()
+ const currentPage = pages[pages.length - 1]
+ const isChatPage = currentPage && currentPage.route === 'pages/chat/chat'
+ console.log('[TabBar] 页面显示:', currentPage ? currentPage.route : '未知', '→', isChatPage ? '聊天模式' : '导航模式')
+ this.setData({ showChatPanel: isChatPage })
+ if (!isChatPage) {
+ this.setData({ keyboardHeight: 0, inputText: '', hasInput: false })
+ this._calcKeyboardStyle()
+ }
+ }
+ },
+
+ methods: {
+
+ /* ==========================================================
+ 一、TabBar 切换
+ ========================================================== */
+
+ onSwitchTab(e) {
+ const { index, path } = e.currentTarget.dataset
+ if (index === this.data.currentTab) return
+ console.log('[TabBar] 切换到:', this.data.tabList[index].text)
+ this.setData({ currentTab: index })
+ wx.switchTab({
+ url: path,
+ fail: (err) => {
+ console.error('[TabBar] 页面跳转失败:', err)
+ this.setData({ currentTab: index === 0 ? 1 : 0 })
+ }
+ })
+ },
+
+ /* ==========================================================
+ 二、聊天页面跳转
+ ========================================================== */
+
+ goToChat() {
+ this._stopRecordingIfNeeded()
+ wx.switchTab({ url: '/pages/chat/chat' })
+ },
+
+ goHome() {
+ this._stopRecordingIfNeeded()
+ this.setData({
+ keyboardHeight: 0,
+ inputText: '',
+ hasInput: false,
+ voiceMode: false,
+ isRecording: false,
+ isVoiceToText: false,
+ inputFocus: false
+ })
+ this._calcKeyboardStyle()
+ wx.switchTab({ url: '/pages/index/index' })
+ },
+
+ /* ==========================================================
+ 三、输入模式切换(文字 ↔ 语音)
+ ========================================================== */
+
+ switchToVoiceMode() {
+ console.log('[输入] 切换到语音模式')
+ wx.hideKeyboard()
+ this.setData({ voiceMode: true, inputFocus: false })
+ wx.showToast({ title: '已切换为语音模式', icon: 'none', duration: 1000 })
+ },
+
+ switchToTextMode() {
+ console.log('[输入] 切换到文字模式')
+ this._stopRecordingIfNeeded()
+ this.setData({ voiceMode: false, isRecording: false, inputFocus: true })
+ wx.showToast({ title: '已切换为文字模式', icon: 'none', duration: 1000 })
+ },
+
+ /* ==========================================================
+ 四、文字消息收发
+ ========================================================== */
+
+ onInputChange(e) {
+ const val = e.detail.value
+ this.setData({ inputText: val, hasInput: !!val.trim() })
+ },
+
+ _calcKeyboardStyle() {
+ const sys = wx.getSystemInfoSync()
+ const rpxToPx = sys.windowWidth / 750
+ const tabH = Math.round(100 * rpxToPx)
+ const kbH = this.data.keyboardHeight
+ this.setData({ tabBarBottom: kbH })
+ },
+
+ onInputFocus(e) {
+ const h = e.detail.height || 0
+ console.log('[键盘] focus 高度:', h, 'px')
+ if (h > 0) {
+ this.setData({ keyboardHeight: h })
+ this._calcKeyboardStyle()
+ }
+ },
+
+ onKeyboardHeightChange(e) {
+ const h = e.detail.height || 0
+ console.log('[键盘] 实时高度:', h, 'px')
+ this.setData({ keyboardHeight: h })
+ this._calcKeyboardStyle()
+ },
+
+ onInputBlur() {
+ this.setData({ keyboardHeight: 0 })
+ this._calcKeyboardStyle()
+ },
+
+ sendTextMessage() {
+ const text = this.data.inputText.trim()
+ if (!text) {
+ wx.showToast({ title: '请输入消息', icon: 'none' })
+ return
+ }
+ this.setData({ inputText: '', hasInput: false })
+ const pages = getCurrentPages()
+ const chatPage = pages.find(p => p.route === 'pages/chat/chat')
+ if (chatPage && chatPage.sendMessage) {
+ chatPage.sendMessage(text)
+ }
+ },
+
+ /* ==========================================================
+ 五、语音录制功能
+ ========================================================== */
+
+ _initRecorder() {
+ try {
+ const manager = wx.getRecorderManager()
+ manager.onStart(() => {
+ console.log('[录音] 开始录制')
+ this.setData({ isRecording: true, recordStartTime: Date.now() })
+ })
+ manager.onStop((res) => {
+ console.log('[录音] 录制结束:', res.tempFilePath, res.duration + 'ms')
+ const duration = res.duration || (Date.now() - this.data.recordStartTime)
+ this.setData({ isRecording: false, tempFilePath: res.tempFilePath || '' })
+ if (duration < 500) {
+ console.log('[录音] 录制时间过短,忽略')
+ return
+ }
+ this._sendVoiceMessage(res.tempFilePath || this.data.tempFilePath, duration)
+ })
+ manager.onError((err) => {
+ console.error('[录音] 错误:', err)
+ this.setData({ isRecording: false })
+ wx.showToast({ title: '录音失败: ' + (err.errMsg || '未知错误'), icon: 'none' })
+ })
+ this.setData({ recorderManager: manager })
+ } catch (e) {
+ console.warn('[录音] 初始化失败,可能不支持录音:', e)
+ this.setData({ recorderManager: null })
+ }
+ },
+
+ startRecordVoice() {
+ const mgr = this.data.recorderManager
+ if (!mgr) {
+ wx.showToast({ title: '录音功能不可用', icon: 'none' })
+ return
+ }
+ console.log('[录音] 手指按下,开始录音...')
+ wx.vibrateShort({ type: 'medium' })
+ mgr.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 48000, format: 'mp3' })
+ },
+
+ stopRecordVoice() {
+ const mgr = this.data.recorderManager
+ if (!mgr || !this.data.isRecording) return
+ console.log('[录音] 手指松开,停止录音')
+ wx.vibrateShort({ type: 'light' })
+ mgr.stop()
+ },
+
+ cancelRecordVoice() {
+ const mgr = this.data.recorderManager
+ if (!mgr || !this.data.isRecording) return
+ console.log('[录音] 取消录音')
+ wx.vibrateShort({ type: 'light' })
+ mgr.stop()
+ this.setData({ isRecording: false, tempFilePath: '' })
+ wx.showToast({ title: '已取消发送', icon: 'none' })
+ },
+
+ _stopRecordingIfNeeded() {
+ if (this.data.isRecording && this.data.recorderManager) {
+ try { this.data.recorderManager.stop() } catch (e) { /* ignore */ }
+ this.setData({ isRecording: false })
+ }
+ },
+
+ _sendVoiceMessage(tempFilePath, durationMs) {
+ const durationSec = Math.ceil(durationMs / 1000)
+ const pages = getCurrentPages()
+ const chatPage = pages.find(p => p.route === 'pages/chat/chat')
+ if (!chatPage || !chatPage.sendMessage) return
+ chatPage.sendMessage(`[语音消息 ${durationSec}秒]`)
+ setTimeout(() => {
+ chatPage.sendMessage('🎤 我已经收到您的语音消息了。目前语音识别功能还在完善中,您也可以直接打字告诉我需要什么帮助哦~')
+ }, 1200)
+ },
+
+ /* ==========================================================
+ 六、语音转文字(点击🎤按钮触发)
+ ========================================================== */
+
+ toggleVoiceToText() {
+ if (this.data.isVoiceToText) {
+ this.stopVoiceToText()
+ } else {
+ this.startVoiceToText()
+ }
+ },
+
+ startVoiceToText() {
+ console.log('[语音转文字] 🎤 开始识别')
+ const mgr = this.data.recorderManager
+ if (!mgr) {
+ wx.showToast({ title: '语音功能不可用', icon: 'none' })
+ return
+ }
+ this.setData({ isVoiceToText: true, inputFocus: false, inputText: '' })
+ wx.vibrateShort({ type: 'medium' })
+ wx.showToast({ title: '请说话...', icon: 'none', duration: 1500 })
+ mgr.start({ duration: 30000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 48000, format: 'mp3' })
+ },
+
+ stopVoiceToText() {
+ console.log('[语音转文字] ⏹ 停止识别')
+ wx.vibrateShort({ type: 'light' })
+ wx.showToast({ title: '正在识别...', icon: 'loading', duration: 2000 })
+ const mgr = this.data.recorderManager
+ if (mgr && this.data.tempFilePath) {
+ try { mgr.stop() } catch (e) { /* ignore */ }
+ }
+ this.setData({ isVoiceToText: false })
+ this._recognizeVoice(this.data.tempFilePath)
+ },
+
+ _recognizeVoice(audioPath) {
+ if (!audioPath) {
+ console.warn('[语音转文字] 没有音频文件,跳过识别')
+ return
+ }
+ /*
+ // 方案一:调用微信同声传译插件(需在 app.json 声明 plugin)
+ const plugin = requirePlugin('WechatSI')
+ const manager = plugin.getRecordRecognitionManager()
+ manager.onStop((res) => {
+ const text = res.result || ''
+ if (text.trim()) {
+ this.setData({ inputText: text, inputFocus: true })
+ }
+ })
+ manager.onError((err) => {
+ wx.showToast({ title: '识别失败', icon: 'none' })
+ })
+ */
+
+ // 方案二:模拟返回(开发调试用)
+ setTimeout(() => {
+ const mockResults = ['帮我查看一下设备状态', '打开客厅灯', '今天的任务有哪些', '你好']
+ const recognizedText = mockResults[Math.floor(Math.random() * mockResults.length)]
+ this.setData({ inputText: recognizedText, hasInput: !!recognizedText.trim(), inputFocus: true })
+ wx.hideToast()
+ console.log(`[语音转文字] ✅ 模拟识别结果已写入: "${recognizedText}"`)
+ }, 1500)
+ },
+
+ /* ==========================================================
+ 七、语音播放
+ ========================================================== */
+
+ _initAudioPlayer() {
+ try {
+ const ctx = wx.createInnerAudioContext()
+ ctx.onPlay(() => { this.setData({ isPlayingVoice: true }) })
+ ctx.onEnded(() => { this.setData({ isPlayingVoice: false }) })
+ ctx.onError((err) => {
+ console.error('[播放] 错误:', err)
+ this.setData({ isPlayingVoice: false })
+ })
+ this.setData({ innerAudioContext: ctx })
+ } catch (e) {
+ this.setData({ innerAudioContext: null })
+ }
+ },
+
+ /* ==========================================================
+ 八、工具按钮
+ ========================================================== */
+
+ onTapEmoji() {
+ console.log('[输入] 表情按钮点击')
+ wx.showToast({ title: '表情功能开发中', icon: 'none' })
+ },
+
+ onRightBtnTap() {
+ if (this.data.hasInput) {
+ this.sendTextMessage()
+ } else {
+ this.onTapMore()
+ }
+ },
+
+ onTapMore() {
+ wx.showActionSheet({
+ itemList: ['发送图片', '拍摄照片', '发送文件', '位置信息'],
+ success: (res) => {
+ const actions = ['image', 'camera', 'file', 'location']
+ console.log('[更多] 选择了:', actions[res.tapIndex])
+ }
+ })
+ }
+
+ }
+})
diff --git a/Claw/client/wechat_app/custom-tab-bar/index.json b/Claw/client/wechat_app/custom-tab-bar/index.json
new file mode 100644
index 0000000..2696194
--- /dev/null
+++ b/Claw/client/wechat_app/custom-tab-bar/index.json
@@ -0,0 +1 @@
+{"component": true, "usingComponents": {}}
\ No newline at end of file
diff --git a/Claw/client/wechat_app/custom-tab-bar/index.wxml b/Claw/client/wechat_app/custom-tab-bar/index.wxml
new file mode 100644
index 0000000..b3b2741
--- /dev/null
+++ b/Claw/client/wechat_app/custom-tab-bar/index.wxml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+ {{item.text}}
+
+
+
+
+
+ ⌨
+
+
+
+
+
+
+
+
+ ☰
+
+
+
+
+ 🔊
+
+
+ ⌨
+
+
+
+
+
+
+
+ {{isRecording ? '松开发送' : '按住 说话'}}
+
+
+
+
+
+
+
+
+
+ 🎤
+
+
+ 🎤
+
+
+
+
+ 😀
+
+
+
+
+
+ ↗
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ 正在播放...
+
diff --git a/Claw/client/wechat_app/custom-tab-bar/index.wxss b/Claw/client/wechat_app/custom-tab-bar/index.wxss
new file mode 100644
index 0000000..fe1330d
--- /dev/null
+++ b/Claw/client/wechat_app/custom-tab-bar/index.wxss
@@ -0,0 +1,472 @@
+/**
+ * 自定义底部导航栏 - 公众号客服交互模式
+ *
+ * 两种状态:
+ * 状态1(普通):[首页] [设备] [任务] [⌨]
+ * 状态2(聊天):[☰] [🔊/⌨] [输入框........] [🎤] [😀] [+]
+ *
+ * 统一配色方案:
+ * 主题色:#1677FF(蓝),用于高亮、选中态、交互反馈
+ * 背景:#FFFFFF(白)/ #1A1A1A(深黑)
+ * 边框:#E8E8E8(浅灰)/ #2E2E2E(深灰)
+ * 文字:#333333(深灰)/ #999999(灰)/ #E8E8E8(浅白)
+ */
+
+/* ============================================================
+ 一、设计令牌(统一颜色变量)
+ ============================================================ */
+
+/* 状态1(普通模式)*/
+.tab-bar {
+ /* 背景与边框 */
+ --bg-primary: #ffffff;
+ --border-primary: #e8e8e8;
+ /* Tab 文字 */
+ --tab-inactive: #999999;
+ --tab-active: #333333;
+ /* 键盘按钮 */
+ --kb-border: #bbbbbb;
+ --kb-icon: #666666;
+ --kb-border-left: #f0f0f0;
+ /* 聊天模式工具按钮(浅色背景用深色图标) */
+ --tool-bg: #f5f5f5;
+ --tool-icon: #555555;
+}
+
+/* 聊天模式下:导航栏保持白色不变,工具按钮区用浅灰底色区分 */
+.tab-bar.chat-mode {
+ /* 背景与边框 — 导航栏保持白色,与整体布局统一 */
+ --bg-primary: #ffffff;
+ --border-primary: #e8e8e8;
+ /* Tab 文字 */
+ --tab-inactive: #999999;
+ --tab-active: #333333;
+ /* 键盘按钮 */
+ --kb-border: #bbbbbb;
+ --kb-icon: #666666;
+ --kb-border-left: #f0f0f0;
+ /* 聊天工具按钮 — 浅灰底色,与白色导航栏融合但不突兀 */
+ --tool-bg: #f0f0f0;
+ --tool-icon: #555555;
+}
+
+/* 全局统一色值 */
+page {
+ /* 主题蓝(唯一品牌色) */
+ --brand-blue: #1677FF;
+ --brand-blue-dark: #0958D9;
+ --brand-blue-light: #73ADFF;
+ --brand-blue-glow: rgba(22, 119, 255, 0.8);
+ /* 深色面板 */
+ --bg-dark: #0d0d0d;
+ --bg-dark-2: #1e1e1e;
+ --bg-dark-3: #2e2e2e;
+ /* 文字(深色模式) */
+ --text-dark: #e8e8e8;
+ --text-dark-secondary: #cccccc;
+ --text-dark-muted: #888888;
+}
+
+/* ============================================================
+ 二、底部导航栏容器(固定在底部)
+ ============================================================ */
+.tab-bar {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ height: 100rpx;
+ background-color: var(--bg-primary);
+ border-top: 1rpx solid var(--border-primary);
+ display: flex;
+ align-items: center;
+ padding-bottom: env(safe-area-inset-bottom);
+ z-index: 99999;
+}
+
+/* 聊天模式下切换为深色背景 */
+.tab-bar.chat-mode {
+ background-color: var(--bg-primary);
+ border-top: 1rpx solid var(--border-primary);
+}
+
+
+/* ============================================================
+ 二、状态1:普通导航模式
+ ============================================================ */
+
+/* ---- Tab菜单项(3个平分剩余空间)---- */
+.tab-item {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+}
+
+.tab-item:active {
+ opacity: 0.6;
+}
+
+.tab-text {
+ font-size: 28rpx;
+ color: var(--tab-inactive);
+ line-height: 1.2;
+}
+
+.tab-item.active .tab-text {
+ color: var(--tab-active);
+ font-weight: 500;
+}
+
+/* ---- 右侧键盘按钮(固定窄宽度)---- */
+.keyboard-btn {
+ width: 100rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ border-left: 1rpx solid var(--kb-border-left);
+ cursor: pointer;
+ position: relative;
+}
+
+.keyboard-btn:active {
+ opacity: 0.5;
+ background-color: var(--border-primary);
+}
+
+.kb-circle {
+ width: 52rpx;
+ height: 52rpx;
+ border-radius: 50%;
+ border: 2rpx solid var(--kb-border);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.kb-icon {
+ font-size: 26rpx;
+ color: var(--kb-icon);
+}
+
+
+/* ============================================================
+ 三、状态2:聊天输入模式
+ ============================================================ */
+
+/* ---- 工具按钮(通用:菜单/语音/表情/更多等)---- */
+.chat-tool-btn {
+ flex-shrink: 0;
+ width: 80rpx;
+ height: 80rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ /* 浅灰底色,融入白色导航栏 */
+ border-radius: 12rpx;
+}
+
+.chat-tool-btn:active {
+ opacity: 0.6;
+ background-color: rgba(0,0,0,0.08) !important;
+}
+
+.tool-icon {
+ font-size: 40rpx;
+ line-height: 1;
+ color: var(--tool-icon);
+}
+
+.send-area .tool-icon {
+ font-size: 36rpx;
+ color: var(--tool-icon);
+}
+
+.send-area.disabled {
+ opacity: 0.25;
+}
+
+/* ---- 🎤 语音转文字激活状态(高亮)---- */
+.send-icon {
+ transition: all 0.2s ease;
+ display: inline-block;
+}
+
+.send-icon-active {
+ animation: voice-pulse 1.5s ease-in-out infinite;
+ filter: drop-shadow(0 0 8px var(--brand-blue-glow));
+}
+
+@keyframes voice-pulse {
+ 0%, 100% {
+ transform: scale(1);
+ filter: drop-shadow(0 0 6px var(--brand-blue-glow));
+ }
+ 50% {
+ transform: scale(1.15);
+ filter: drop-shadow(0 0 14px var(--brand-blue));
+ }
+}
+
+/* 🎤激活时按钮高亮(浅色背景下的蓝色边框) */
+.voice-active {
+ background: rgba(22, 119, 255, 0.1) !important;
+ border: 1.5px solid rgba(22, 119, 255, 0.6) !important;
+ border-radius: 12rpx !important;
+ box-shadow: 0 0 10px rgba(22, 119, 255, 0.25) !important;
+}
+
+/* ---- 发送按钮(替换+号) ---- */
+.send-action-btn {
+ background: #1677FF !important;
+ border-radius: 16rpx !important;
+ width: 96rpx !important;
+ box-shadow: 0 4rpx 14rpx rgba(22, 119, 255, 0.3);
+}
+
+.send-action-btn:active {
+ opacity: 0.85;
+ transform: scale(0.95);
+}
+
+.send-icon-img {
+ width: 42rpx;
+ height: 42rpx;
+ display: block;
+}
+
+.send-arrow-icon {
+ font-size: 38rpx;
+ color: #ffffff;
+ font-weight: bold;
+ line-height: 1;
+}
+
+.send-label {
+ font-size: 26rpx;
+ font-weight: 600;
+ color: #ffffff;
+ letter-spacing: 2rpx;
+}
+
+/* ---- 输入框 ---- */
+.chat-input {
+ flex: 1;
+ height: 68rpx;
+ min-width: 0;
+ background-color: #f0f0f0;
+ border-radius: 12rpx;
+ padding: 0 20rpx;
+ font-size: 28rpx;
+ color: #333333;
+ margin: 0 4rpx;
+}
+
+.placeholder-style {
+ color: #999999;
+ font-size: 28rpx;
+}
+
+/* ---- 语音录制按钮 ---- */
+.voice-btn {
+ flex: 1;
+ height: 68rpx;
+ min-width: 0;
+ border-radius: 12rpx;
+ background-color: #f0f0f0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ overflow: hidden;
+ margin: 0 4rpx;
+ transition: all 0.15s ease;
+}
+
+.voice-btn:active,
+.voice-btn.recording {
+ background-color: var(--brand-blue);
+ box-shadow: 0 2rpx 16rpx rgba(22, 119, 255, 0.35);
+}
+
+.voice-text {
+ font-size: 28rpx;
+ color: #666666;
+ letter-spacing: 2rpx;
+}
+
+.voice-btn.recording .voice-text {
+ color: #ffffff;
+}
+
+/* 录音波纹动画 */
+.record-wave {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ pointer-events: none;
+}
+
+.wave {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 120rpx;
+ height: 120rpx;
+ border-radius: 50%;
+ border: 3rpx solid rgba(22, 119, 255, 0.3);
+ transform: translate(-50%, -50%);
+ animation: waveExpand 1.5s ease-out infinite;
+}
+
+.wave2 { animation-delay: 0.5s; }
+.wave3 { animation-delay: 1s; }
+
+@keyframes waveExpand {
+ from { width: 60rpx; height: 60rpx; opacity: 1; border-width: 4rpx; }
+ to { width: 200rpx; height: 200rpx; opacity: 0; border-width: 1rpx; }
+}
+
+
+/* ============================================================
+ 四、聊天消息面板(覆盖在页面内容上方)
+ ============================================================ */
+
+.chat-overlay {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: calc(100rpx + env(safe-area-inset-bottom));
+ top: 0;
+ z-index: 99998;
+ display: flex;
+ flex-direction: column;
+}
+
+/* 消息列表 */
+.chat-msg-list {
+ flex: 1;
+ background-color: var(--bg-dark);
+ padding: 24rpx;
+ overflow-y: auto;
+}
+
+.msg-bottom-pad {
+ height: 20rpx;
+ flex-shrink: 0;
+}
+
+/* ---- 单行消息 ---- */
+.msg-row {
+ display: flex;
+ margin-bottom: 24rpx;
+ align-items: flex-start;
+}
+
+.msg-user {
+ flex-direction: row-reverse;
+}
+
+/* 头像 */
+.msg-avatar {
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 22rpx;
+ font-weight: bold;
+ flex-shrink: 0;
+ margin: 0 10rpx;
+}
+
+.bot-avatar {
+ background: linear-gradient(135deg, var(--brand-blue), var(--brand-blue-dark));
+ color: #fff;
+}
+
+.user-avatar {
+ background: linear-gradient(135deg, var(--brand-blue-light), var(--brand-blue));
+ color: #fff;
+}
+
+/* 消息气泡 */
+.msg-bubble {
+ max-width: 65%;
+ padding: 18rpx 22rpx;
+ border-radius: 18rpx;
+ word-break: break-all;
+}
+
+.bot-bubble {
+ background-color: var(--bg-dark-2);
+ border: 2rpx solid rgba(255,255,255,0.06);
+ border-radius: 4rpx 18rpx 18rpx 18rpx;
+}
+
+.user-bubble {
+ background: linear-gradient(135deg, var(--brand-blue), var(--brand-blue-dark));
+ border-radius: 18rpx 4rpx 18rpx 18rpx;
+ box-shadow: 0 4rpx 14rpx rgba(22, 119, 255, 0.22);
+}
+
+.msg-text {
+ font-size: 27rpx;
+ line-height: 1.5;
+ word-break: break-all;
+}
+
+.bot-bubble .msg-text { color: var(--text-dark); }
+.user-bubble .msg-text { color: #ffffff; }
+
+
+/* ============================================================
+ 五、语音播放中提示
+ ============================================================ */
+
+.voice-playing-toast {
+ position: fixed;
+ top: 160rpx;
+ left: 50%;
+ transform: translateX(-50%);
+ background-color: rgba(0,0,0,0.85);
+ border-radius: 20rpx;
+ padding: 22rpx 36rpx;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8rpx;
+ z-index: 20000;
+ backdrop-filter: blur(10rpx);
+}
+
+.playing-wave {
+ display: flex;
+ align-items: center;
+ gap: 4rpx;
+ height: 38rpx;
+}
+
+.p-bar {
+ width: 6rpx;
+ background-color: var(--brand-blue);
+ border-radius: 3rpx;
+ animation: barBounce 0.6s ease-in-out infinite alternate;
+}
+.p1{height:16rpx;animation-delay:0s} .p2{height:28rpx;animation-delay:.1s}
+.p3{height:38rpx;animation-delay:.2s} .p4{height:26rpx;animation-delay:.3s}
+.p5{height:18rpx;animation-delay:.4s}
+
+@keyframes barBounce {
+ from{transform:scaleY(.4)} to{transform:scaleY(1)}
+}
+
+.playing-text {
+ font-size: 22rpx;
+ color: var(--text-dark-secondary);
+}
diff --git a/Claw/client/wechat_app/pages/chat/chat.js b/Claw/client/wechat_app/pages/chat/chat.js
index 1c15232..4ed982d 100644
--- a/Claw/client/wechat_app/pages/chat/chat.js
+++ b/Claw/client/wechat_app/pages/chat/chat.js
@@ -1,4 +1,4 @@
-// 聊天页面逻辑
+// 聊天页面编辑
const { API, WebSocketManager, util, constants } = require('../../utils/api.js')
const { formatRelativeTime } = util
const { MESSAGE_TYPE, WS_MESSAGE_TYPE } = constants
@@ -13,18 +13,157 @@ Page({
websocketManager: null,
lastMessageId: '',
isConnected: false,
- currentPage: 1
+ currentPage: 1,
+ userInfo: null,
+ showUserInfoCard: false,
+ locationInfo: null,
+ deviceInfo: null,
+ networkType: '',
+ showAuthPrompt: false,
+ authType: 'userInfo'
},
onLoad() {
- this.initWebSocket()
+ this._initWebSocket()
this.loadChatHistory()
this.addWelcomeMessage()
+ this.checkUserAuth()
+ // 通知 tab-bar 进入聊天模式
+ if (typeof this.getTabBar === 'function' && this.getTabBar()) {
+ this.getTabBar().setData({
+ currentTab: 1,
+ showChatPanel: true
+ })
+ }
+ },
+
+ // 检查用户授权状态
+ checkUserAuth() {
+ const app = getApp()
+ const userInfo = app.globalData.userInfo
+
+ if (userInfo) {
+ this.setData({ userInfo })
+ this.showUserInfoCard()
+ this.getUserAdditionalInfo()
+ } else {
+ this.requestUserAuth()
+ }
+ },
+
+ // 请求用户授权
+ requestUserAuth() {
+ const app = getApp()
+ app.getUserInfo((userInfo) => {
+ if (userInfo) {
+ this.setData({ userInfo })
+ this.showUserInfoCard()
+ this.getUserAdditionalInfo()
+ } else {
+ // 未授权,显示授权提示组件
+ this.setData({
+ showAuthPrompt: true,
+ authType: 'userInfo'
+ })
+ }
+ })
+ },
+
+ // 获取用户附加信息
+ getUserAdditionalInfo() {
+ const app = getApp()
+
+ // 获取地理位置信息
+ app.getLocation((locationInfo) => {
+ if (locationInfo) {
+ this.setData({ locationInfo })
+ }
+ })
+
+ // 获取设备信息
+ const deviceInfo = app.getDeviceInfo()
+ if (deviceInfo) {
+ this.setData({ deviceInfo })
+ }
+
+ // 获取网络状态
+ app.getNetworkType((networkType) => {
+ if (networkType) {
+ this.setData({ networkType })
+ }
+ })
+ },
+
+ // 显示用户信息卡片
+ showUserInfoCard() {
+ const { userInfo, locationInfo, deviceInfo, networkType } = this.data
+
+ // 获取UnionID
+ let userId = this.getUnionId()
+
+ // 获取手机号
+ const phoneNumber = wx.getStorageSync('phoneNumber')
+
+ let userInfoContent = `你好,${userInfo.nickName}!\n`
+ userInfoContent += `UnionID:${userId}\n`
+
+ if (phoneNumber) {
+ userInfoContent += `手机号:${phoneNumber}\n`
+ }
+
+ if (userInfo.gender === 1) {
+ userInfoContent += '性别:男\n'
+ } else if (userInfo.gender === 2) {
+ userInfoContent += '性别:女\n'
+ }
+
+ if (userInfo.city) {
+ userInfoContent += `地区:${userInfo.country} ${userInfo.province} ${userInfo.city}\n`
+ }
+
+ if (locationInfo) {
+ userInfoContent += `位置:${locationInfo.latitude.toFixed(2)}, ${locationInfo.longitude.toFixed(2)}\n`
+ }
+
+ if (deviceInfo) {
+ userInfoContent += `设备:${deviceInfo.model}\n`
+ userInfoContent += `系统:${deviceInfo.system}\n`
+ }
+
+ if (networkType) {
+ userInfoContent += `网络:${networkType}`
+ }
+
+ const userInfoMessage = {
+ id: util.generateUniqueId(),
+ content: userInfoContent,
+ type: MESSAGE_TYPE.TEXT,
+ isMe: false,
+ nickname: '系统',
+ avatar: '/assets/images/system-avatar.png',
+ time: formatRelativeTime(new Date())
+ }
+
+ this.addMessage(userInfoMessage)
+ this.setData({ showUserInfoCard: true })
+ },
+
+ onShow() {
+ // 更新自定义 TabBar 选中态 & 强制聊天模式
+ if (typeof this.getTabBar === 'function' && this.getTabBar()) {
+ this.getTabBar().setData({
+ currentTab: 1,
+ showChatPanel: true
+ })
+ }
+ // 每次显示页面时检查授权状态
+ this.checkUserAuth()
},
onUnload() {
- if (this.data.websocketManager) {
- this.data.websocketManager.disconnect()
+ if (this.wsManager) {
+ this.wsManager.disconnect()
+ this.wsManager = null
}
},
@@ -32,7 +171,7 @@ Page({
addWelcomeMessage() {
const welcomeMessage = {
id: util.generateUniqueId(),
- content: '你好!我是智控未来的AI助手,有什么可以帮你的吗?',
+ content: '你好,我是智控未来的AI助手,有什么可以帮助你的吗?',
type: MESSAGE_TYPE.TEXT,
isMe: false,
nickname: '智控未来',
@@ -42,25 +181,14 @@ Page({
this.addMessage(welcomeMessage)
},
- // 初始化WebSocket连接
- initWebSocket() {
- const manager = new WebSocketManager()
+ // 初始化 WebSocket 连接(小程序 WebSocket)
+ _initWebSocket() {
+ if (this.wsManager && this.wsManager.isConnected) return
const app = getApp()
-
- manager.onMessage(WS_MESSAGE_TYPE.MESSAGE, (data) => {
- this.handleNewMessage(data)
- })
-
- manager.onMessage(WS_MESSAGE_TYPE.SYSTEM, (data) => {
- this.handleSystemMessage(data)
- })
-
- manager.connect(app.globalData.websocketUrl || 'wss://pactgo.cn/api/v1/ws/control')
-
- this.setData({
- websocketManager: manager,
- isConnected: true
- })
+ const wsUrl = app.globalData.websocketUrl || 'wss://pactgo.cn/api/v1/ws/miniprogram'
+ this.wsManager = new WebSocketManager()
+ this.wsManager.connect(wsUrl)
+ this.setData({ isConnected: true })
},
// 加载聊天记录
@@ -82,7 +210,7 @@ Page({
// 加载更多历史消息
loadMoreHistory() {
- if (this.data.currentPage < 3) { // 模拟加载3页历史
+ if (this.data.currentPage < 3) {
this.setData({ currentPage: this.data.currentPage + 1 })
const moreHistory = this.getMockHistoryMessages(this.data.currentPage)
if (moreHistory.length > 0) {
@@ -98,7 +226,7 @@ Page({
if (page === 1) {
messages.push({
id: 'history-1',
- content: '你好,我是智控未来的AI助手',
+ content: '你好,我是智控未来的AI助手,有什么可以帮助你的吗?',
type: MESSAGE_TYPE.TEXT,
isMe: false,
nickname: '智控未来',
@@ -116,7 +244,7 @@ Page({
})
messages.push({
id: 'history-3',
- content: '我可以帮你解答问题、提供信息、生成内容等。请问有什么具体需求吗?',
+ content: '我可以帮你解答问题、提供信息、生成内容等。请问你有什么具体需求吗?',
type: MESSAGE_TYPE.TEXT,
isMe: false,
nickname: '智控未来',
@@ -135,7 +263,7 @@ Page({
})
messages.push({
id: 'history-5',
- content: '学习编程的步骤:
- 选择一门编程语言,如Python、JavaScript等
- 学习基础语法和概念
- 实践项目,积累经验
- 参与社区,学习他人的代码
',
+ content: '学习编程的步骤:
- 选择一门编程语言,如Python、JavaScript等
- 学习基础语法和概念
- 动手项目,积累经验
- 参与社区,学习他人的代码
',
type: MESSAGE_TYPE.richText,
isMe: false,
nickname: '智控未来',
@@ -149,7 +277,7 @@ Page({
// 处理新消息
handleNewMessage(data) {
this.setData({ isTyping: false })
-
+
// 模拟不同类型的消息
if (data.content.includes('代码')) {
const codeMessage = {
@@ -181,7 +309,7 @@ Page({
} else if (data.content.includes('富文本')) {
const richTextMessage = {
id: data.id || util.generateUniqueId(),
- content: '标题
这是一段加粗的文本,包含
',
+ content: '标题
这是一段加粗的文字,包含
',
type: 'richText',
isMe: false,
nickname: '智控未来',
@@ -239,26 +367,20 @@ Page({
}, 100)
},
- // 输入变化
+ // 输入框变化
onInputChange(e) {
this.setData({
inputValue: e.detail.value
})
},
- // 发送消息
- async sendMessage() {
- const content = this.data.inputValue.trim()
- if (!content || this.data.sending) {
- return
- }
-
- this.setData({
- sending: true,
- isTyping: true
- })
-
- // 添加用户消息到界面
+ // 发送消息(走 SSE → Gateway → SmartClaw → LMStudio)
+ async sendMessage(text) {
+ const content = text ? text.trim() : this.data.inputValue.trim()
+ if (!content || this.data.sending) return
+
+ this.setData({ sending: true, isTyping: true })
+
const userMessage = {
id: util.generateUniqueId(),
content: content,
@@ -268,58 +390,144 @@ Page({
avatar: '/assets/images/user-avatar.png',
time: formatRelativeTime(new Date())
}
-
this.addMessage(userMessage)
-
- try {
- // 模拟AI回复
- setTimeout(() => {
- const aiResponse = {
- id: util.generateUniqueId(),
- content: this.getMockAIResponse(content),
- type: MESSAGE_TYPE.TEXT,
- isMe: false,
- nickname: '智控未来',
- avatar: '/assets/images/ai-avatar.png',
- time: formatRelativeTime(new Date())
- }
- this.handleNewMessage(aiResponse)
- }, 1500)
-
- this.setData({
- inputValue: '',
- sending: false
- })
- } catch (error) {
- console.error('发送消息失败:', error)
- util.showError('发送失败,请重试')
- this.setData({
- sending: false,
- isTyping: false
- })
+ this.setData({ inputValue: '' })
+
+ // 创建 AI 占位消息(打字机效果)
+ const aiMsgId = util.generateUniqueId()
+ const aiMsg = {
+ id: aiMsgId, content: '', type: MESSAGE_TYPE.TEXT,
+ isMe: false, nickname: '智控未来',
+ avatar: '/assets/images/ai-avatar.png',
+ time: formatRelativeTime(new Date()), isStreaming: true
}
+ this.addMessage(aiMsg)
+
+ try {
+ const app = getApp()
+ const gatewayUrl = app.globalData.gatewayUrl || 'http://localhost:8000'
+
+ // 构建 SSE 请求数据
+ const requestData = {
+ model: "qwen2.5-vl-7b-instruct",
+ messages: [
+ {
+ role: "user",
+ content: content
+ }
+ ],
+ stream: true,
+ temperature: 0.7,
+ max_tokens: 500
+ }
+
+ // 发送 SSE 请求
+ const self = this
+ let fullResponse = ''
+
+ // 使用 wx.request 发送请求并处理 SSE 流
+ wx.request({
+ url: `${gatewayUrl}/v1/chat/completions`,
+ method: 'POST',
+ header: {
+ 'Content-Type': 'application/json'
+ },
+ data: requestData,
+ responseType: 'text',
+ success: function(res) {
+ // 处理 SSE 响应
+ const lines = res.data.split('\n')
+ for (const line of lines) {
+ if (line.startsWith('data: ')) {
+ const data = line.substring(6)
+ if (data === '[DONE]') {
+ // SSE 流结束
+ self._typewriterAppend(aiMsgId, fullResponse, true)
+ break
+ }
+ try {
+ const json = JSON.parse(data)
+ if (json.choices && json.choices[0] && json.choices[0].delta && json.choices[0].delta.content) {
+ const chunk = json.choices[0].delta.content
+ fullResponse += chunk
+ self._typewriterAppend(aiMsgId, fullResponse)
+ }
+ } catch (e) {
+ console.error('解析 SSE 数据失败:', e)
+ }
+ }
+ }
+ },
+ fail: function(err) {
+ console.error('SSE 请求失败:', err)
+ self._typewriterAppend(aiMsgId, self.getMockAIResponse(content), true)
+ }
+ })
+
+ // 8 秒超时:降级为模拟回复
+ setTimeout(() => {
+ const msgs = self.data.messages
+ const aiM = msgs.find(m => m.id === aiMsgId)
+ if (aiM && aiM.isStreaming) {
+ aiM.isStreaming = false
+ self._typewriterAppend(aiMsgId, self.getMockAIResponse(content), true)
+ }
+ }, 8000)
+
+ } catch (err) {
+ console.error('发送失败:', err)
+ const aiResponse = {
+ id: aiMsgId,
+ content: this.getMockAIResponse(content),
+ type: MESSAGE_TYPE.TEXT,
+ isMe: false,
+ nickname: '智控未来',
+ avatar: '/assets/images/ai-avatar.png',
+ time: formatRelativeTime(new Date())
+ }
+ this.handleNewMessage(aiResponse)
+ }
+
+ this.setData({ sending: false })
+ },
+
+ // 打字机效果
+ _typewriterAppend(msgId, fullContent, isFinal = false) {
+ const msgs = this.data.messages
+ const idx = msgs.findIndex(m => m.id === msgId)
+ if (idx === -1) return
+ const cur = msgs[idx].content || ''
+ if (isFinal || cur.length >= fullContent.length) {
+ msgs[idx] = { ...msgs[idx], content: fullContent, isStreaming: false }
+ this.setData({ messages: [...msgs], isTyping: false })
+ return
+ }
+ const next = fullContent[cur.length]
+ msgs[idx] = { ...msgs[idx], content: cur + next, isStreaming: true }
+ this.setData({ messages: [...msgs] })
+ setTimeout(() => { this._typewriterAppend(msgId, fullContent) }, 30)
},
// 模拟AI回复
getMockAIResponse(content) {
const responses = {
- '你好': '你好!很高兴为你服务。',
+ '你好': '你好,很高兴为你服务。',
'今天天气怎么样': '今天天气晴朗,适合户外活动。',
'如何学习编程': '学习编程需要持之以恒,建议从基础语法开始,多动手实践。',
'代码': 'def hello():\n print("Hello, World!")',
- '卡片': '这是一个卡片消息',
+ '卡片': '这是一个卡片消息。',
'富文本': '这是一段富文本消息,包含加粗和下划线。',
- '今天是几月几日': '今天是' + new Date().toLocaleDateString('zh-CN'),
+ '今天是几月几号': '今天是 ' + new Date().toLocaleDateString('zh-CN'),
'你是谁': '我是智控未来的AI助手,由LMStudio提供支持。'
}
- return responses[content] || '感谢你的提问,我会为你提供准确的回答。'
+ return responses[content] || '感谢你的提问,我会为你提供准确的答案。'
},
// 长按消息
onMessageLongPress(e) {
const messageId = e.currentTarget.dataset.messageId
const message = this.data.messages.find(msg => msg.id === messageId)
-
+
if (message && message.type === MESSAGE_TYPE.TEXT) {
wx.showActionSheet({
itemList: ['复制消息', '删除消息'],
@@ -386,7 +594,7 @@ Page({
onCardButtonTap(e) {
const action = e.currentTarget.dataset.action
wx.showToast({
- title: `你点击了${action}`,
+ title: '你点击了' + action,
icon: 'none'
})
},
@@ -404,10 +612,7 @@ Page({
messages: [],
lastMessageId: ''
})
-
- // 清空本地存储
wx.removeStorageSync('chatHistory')
-
wx.showToast({
title: '已清空',
icon: 'success'
@@ -415,5 +620,89 @@ Page({
}
}
})
+ },
+
+ // 授权成功回调
+ onAuthSuccess(e) {
+ const { authType, data } = e.detail
+ console.log('授权成功:', authType, data)
+
+ const app = getApp()
+
+ switch (authType) {
+ case 'userInfo':
+ app.globalData.userInfo = data
+ wx.setStorageSync('userInfo', data)
+ this.setData({ userInfo: data })
+ this.showUserInfoCard()
+ this.getUserAdditionalInfo()
+ break
+ case 'location':
+ const locationInfo = {
+ latitude: data.latitude,
+ longitude: data.longitude,
+ speed: data.speed,
+ accuracy: data.accuracy
+ }
+ wx.setStorageSync('locationInfo', locationInfo)
+ this.setData({ locationInfo })
+ break
+ }
+ },
+
+ // 授权失败回调
+ onAuthFail(e) {
+ const { authType, error } = e.detail
+ console.error('授权失败:', authType, error)
+
+ wx.showToast({
+ title: '授权失败,请重试',
+ icon: 'none'
+ })
+ },
+
+ // 取消授权回调
+ onAuthCancel(e) {
+ const { authType } = e.detail
+ console.log('取消授权:', authType)
+
+ // 如果是用户信息授权,使用默认信息
+ if (authType === 'userInfo') {
+ this.setData({
+ userInfo: {
+ nickName: '游客',
+ avatarUrl: '/assets/images/user-avatar.png',
+ gender: 0,
+ city: '',
+ province: '',
+ country: ''
+ }
+ })
+ this.showUserInfoCard()
+ }
+ },
+
+ // 打开设置回调
+ onSettingsOpen(e) {
+ const { authType, settings } = e.detail
+ console.log('打开设置:', authType, settings)
+ },
+
+ // 获取UnionID
+ getUnionId() {
+ // 从本地存储获取
+ let unionId = wx.getStorageSync('globalUserId')
+
+ if (!unionId) {
+ const app = getApp()
+ // 如果本地没有,调用app方法获取
+ app.getGlobalUserId((id) => {
+ unionId = id
+ })
+ // 同时返回一个临时ID,确保页面正常显示
+ unionId = '获取中...'
+ }
+
+ return unionId
}
-})
\ No newline at end of file
+})
diff --git a/Claw/client/wechat_app/pages/chat/chat.json b/Claw/client/wechat_app/pages/chat/chat.json
index 29a78c2..9dcd819 100644
--- a/Claw/client/wechat_app/pages/chat/chat.json
+++ b/Claw/client/wechat_app/pages/chat/chat.json
@@ -1,7 +1 @@
-{
- "navigationBarTitleText": "智控未来",
- "navigationBarBackgroundColor": "#ffffff",
- "navigationBarTextStyle": "black",
- "enablePullDownRefresh": false,
- "backgroundTextStyle": "light"
-}
\ No newline at end of file
+{"navigationBarTitleText": "设备", "navigationBarBackgroundColor": "#1a1a1a", "navigationBarTextStyle": "white", "enablePullDownRefresh": false, "backgroundTextStyle": "light", "usingComponents": {"message": "../../components/message/message", "auth-prompt": "../../components/auth-prompt/auth-prompt"}}
\ No newline at end of file
diff --git a/Claw/client/wechat_app/pages/chat/chat.wxml b/Claw/client/wechat_app/pages/chat/chat.wxml
index 0f4cccb..0cdae4e 100644
--- a/Claw/client/wechat_app/pages/chat/chat.wxml
+++ b/Claw/client/wechat_app/pages/chat/chat.wxml
@@ -100,8 +100,8 @@
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/Claw/client/wechat_app/pages/chat/chat.wxss b/Claw/client/wechat_app/pages/chat/chat.wxss
index 6f09f3d..1f07b92 100644
--- a/Claw/client/wechat_app/pages/chat/chat.wxss
+++ b/Claw/client/wechat_app/pages/chat/chat.wxss
@@ -1,11 +1,11 @@
-/* 聊天页面样式 */
+/* 聊天页面样式 - 深色主题 */
@import '../../utils/constant.wxss';
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
- background-color: #f7f7f7;
+ background-color: #0d0d0d;
}
.message-list {
@@ -20,12 +20,12 @@
justify-content: center;
align-items: center;
height: 400rpx;
- color: #999;
+ color: #555555;
}
.empty-text {
font-size: 28rpx;
- color: #999;
+ color: #555555;
}
.history-tip {
@@ -37,8 +37,8 @@
.history-tip text {
font-size: 24rpx;
- color: #999;
- background-color: #f0f0f0;
+ color: #666666;
+ background-color: #1a1a1a;
padding: 5rpx 20rpx;
border-radius: 20rpx;
}
@@ -85,7 +85,7 @@
.message-nickname {
font-size: 24rpx;
- color: #999;
+ color: #666666;
font-weight: 500;
}
@@ -96,15 +96,15 @@
}
.message-left .message-body {
- background-color: white;
+ background-color: #1e1e1e;
border-radius: 16rpx 16rpx 16rpx 4rpx;
- box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.05);
+ border: 2rpx solid rgba(255,255,255,0.06);
}
.message-right .message-body {
background-color: #1677FF;
border-radius: 16rpx 16rpx 4rpx 16rpx;
- box-shadow: 0 1rpx 2rpx rgba(22, 119, 255, 0.1);
+ box-shadow: 0 4rpx 16rpx rgba(22,119,255,0.25);
}
.message-text-container {
@@ -117,7 +117,7 @@
}
.message-left .message-text {
- color: #333;
+ color: #e8e8e8;
}
.message-right .message-text {
@@ -132,7 +132,7 @@
.message-time {
font-size: 20rpx;
- color: #ccc;
+ color: #444444;
margin-right: 16rpx;
}
@@ -143,12 +143,13 @@
margin: 4rpx 0;
}
-/* 代码块样式 */
+/* 代码块样式 - 深色 */
.message-code {
- background-color: #f5f5f5;
+ background-color: #141414;
border-radius: 12rpx;
overflow: hidden;
margin: 8rpx 0;
+ border: 2rpx solid #252525;
}
.code-header {
@@ -156,8 +157,8 @@
justify-content: space-between;
align-items: center;
padding: 12rpx 16rpx;
- background-color: #e8e8e8;
- border-bottom: 1rpx solid #ddd;
+ background-color: #1a1a1a;
+ border-bottom: 2rpx solid #252525;
}
.code-language {
@@ -188,30 +189,30 @@
padding: 16rpx;
max-height: 400rpx;
overflow-y: auto;
- background-color: #f5f5f5;
+ background-color: #0d0d0d;
}
.code-text {
font-size: 24rpx;
line-height: 1.4;
- color: #333;
+ color: #a8dadc;
font-family: 'Courier New', Courier, monospace;
white-space: pre-wrap;
}
-/* 按钮卡片样式 */
+/* 按钮卡片样式 - 深色 */
.message-button-card {
- background-color: white;
+ background-color: #1e1e1e;
border-radius: 12rpx;
padding: 20rpx;
- box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.05);
margin: 8rpx 0;
+ border: 2rpx solid rgba(255,255,255,0.06);
}
.card-title {
font-size: 26rpx;
font-weight: 500;
- color: #333;
+ color: #ffffff;
margin-bottom: 16rpx;
}
@@ -222,8 +223,8 @@
}
.card-button {
- background-color: #f8f8f8;
- border: 1rpx solid #e0e0e0;
+ background-color: #141414;
+ border: 2rpx solid #252525;
border-radius: 8rpx;
padding: 16rpx;
font-size: 26rpx;
@@ -266,7 +267,7 @@
.typing-text {
font-size: 24rpx;
- color: #999;
+ color: #666666;
}
@keyframes typing-bounce {
@@ -278,17 +279,16 @@
}
}
-/* 输入区域 */
+/* 输入区域 - 深色 */
.input-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
- background: white;
- border-top: 1rpx solid #e0e0e0;
+ background: linear-gradient(180deg, #1a1a1a 0%, #141414 100%);
+ border-top: 2rpx solid rgba(255,255,255,0.06);
padding: 20rpx;
z-index: 1000;
- box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.input-wrapper {
@@ -299,11 +299,12 @@
.message-input {
flex: 1;
- border: 1rpx solid #e0e0e0;
+ border: 2rpx solid #252525;
border-radius: 28rpx;
padding: 20rpx 28rpx;
font-size: 28rpx;
- background-color: #f8f8f8;
+ background-color: #0d0d0d;
+ color: #ffffff;
min-height: 80rpx;
max-height: 200rpx;
overflow-y: auto;
@@ -343,7 +344,7 @@
transform: none;
}
-/* 富文本样式 */
+/* 富文本样式 - 深色主题 */
rich-text {
line-height: 1.5;
}
@@ -361,17 +362,17 @@ rich-text :deep(h3) {
rich-text :deep(h1) {
font-size: 32rpx;
- color: #333;
+ color: #ffffff;
}
rich-text :deep(h2) {
font-size: 28rpx;
- color: #444;
+ color: #e5e5e5;
}
rich-text :deep(h3) {
font-size: 26rpx;
- color: #555;
+ color: #cccccc;
}
rich-text :deep(ul),
@@ -383,6 +384,7 @@ rich-text :deep(ol) {
rich-text :deep(li) {
margin: 8rpx 0;
line-height: 1.5;
+ color: #d0d0d0;
}
rich-text :deep(strong) {
diff --git a/Claw/client/wechat_app/pages/index/index.js b/Claw/client/wechat_app/pages/index/index.js
index 8613273..65ef2bc 100644
--- a/Claw/client/wechat_app/pages/index/index.js
+++ b/Claw/client/wechat_app/pages/index/index.js
@@ -1,224 +1,322 @@
-// 首页逻辑
+// 首页逻辑 - 深色主题信息流
const app = getApp()
Page({
data: {
- userInfo: null,
+ // 用户信息
hasUserInfo: false,
- canIUse: wx.canIUse('button.open-type.getUserInfo'),
+ openId: '',
+ phoneNumber: '',
+ unionId: '',
+ locationInfo: null,
+ deviceInfo: null,
+ systemInfo: null,
+ networkType: '',
+ wxVersion: '',
+ accountInfo: null,
+ clipboardData: null,
+
+ // 时间显示
+ currentTime: '',
+ yesterdayTime: '',
+
+ // 轮播Banner数据 - 蓝色主题
+ banners: [
+ { id: 1, tag: '健康科普', title: '在多才和多艺之间\n我选择了多肉~', desc: '医生,我喝水都要胖,咋个整?', image: '' },
+ { id: 2, tag: '专家讲座', title: '春季养生指南:\n中医教你调理身体', desc: '', image: '' }
+ ],
+
+ // 轮播Banner数据 - 红色喜报主题
+ redBanners: [
+ { id: 1, title: '四川省人民医院高质量发展跃上新台阶,成功迈入全国公立医院第一方阵!', image: '' }
+ ],
+
websocketConnected: false,
- version: app.globalData.version
+ version: app.globalData.version || '1.0.0',
+ socketTask: null,
},
onLoad() {
- if (app.globalData.userInfo) {
- this.setData({
- userInfo: app.globalData.userInfo,
- hasUserInfo: true
- })
- } else if (this.data.canIUse) {
- // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
- // 所以此处加入 callback 以防止这种情况
- app.userInfoReadyCallback = res => {
- this.setData({
- userInfo: res.userInfo,
- hasUserInfo: true
- })
- }
- } else {
- // 在没有 open-type=getUserInfo 版本的兼容处理
- wx.getUserInfo({
- success: res => {
- app.globalData.userInfo = res.userInfo
- this.setData({
- userInfo: res.userInfo,
- hasUserInfo: true
- })
- }
- })
+ this.updateTimeDisplay()
+ this.loadStoredInfo()
+ this.initWebSocketSafe()
+ },
+
+ onShow() {
+ this.updateTimeDisplay()
+ this.setTabBarActive()
+ if (this.data.hasUserInfo) this.loadStoredInfo()
+ },
+
+ // ==============================================
+ // 加载本地存储的信息
+ // ==============================================
+ loadStoredInfo() {
+ const openId = wx.getStorageSync('openId') || ''
+ const phoneNumber = wx.getStorageSync('phoneNumber') || ''
+ const deviceInfo = app.getDeviceInfo?.() || null
+
+ this.setData({ openId, phoneNumber, deviceInfo })
+
+ if (openId) {
+ this.setData({ hasUserInfo: true })
+ }
+
+ // 获取网络状态
+ app.getNetworkType?.((networkType) => {
+ if (networkType) this.setData({ networkType })
+ })
+
+ // 获取位置(需要授权)
+ app.getLocation?.((locationInfo) => {
+ if (locationInfo) this.setData({ locationInfo })
+ })
+
+ // 获取小程序信息
+ try {
+ const accountInfo = wx.getAccountInfoSync()
+ this.setData({ accountInfo })
+ } catch (error) {
+ console.error('获取小程序信息失败:', error)
+ this.setData({ accountInfo: null })
+ }
+
+ // 获取剪贴板信息(需要授权)
+ wx.getClipboardData({
+ success: (res) => {
+ this.setData({ clipboardData: res.data })
+ },
+ fail: (error) => {
+ console.error('获取剪贴板信息失败:', error)
+ this.setData({ clipboardData: null })
+ }
+ })
+
+ // 获取微信版本信息
+ try {
+ const systemInfo = wx.getSystemInfoSync()
+ this.setData({ wxVersion: systemInfo.version, systemInfo })
+ } catch (error) {
+ console.error('获取微信版本信息失败:', error)
+ this.setData({ wxVersion: null, systemInfo: null })
+ }
+
+ // 获取UnionID(个人小程序无法获取)
+ this.setData({ unionId: '无法获取(需企业资质)' })
+
+ // 获取手机号(个人小程序无法获取)
+ if (!phoneNumber) {
+ this.setData({ phoneNumber: '无法获取(需企业资质)' })
}
-
- // 初始化WebSocket连接
- this.initWebSocket()
},
- getUserProfile(e) {
- // 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认
- wx.getUserProfile({
- desc: '展示用户信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
+ // ==============================================
+ // 登录:通过 WebSocket 发 wechat_login,收到 openid 后存本地
+ // ==============================================
+ onLogin() {
+ // 确保 WebSocket 已连接
+ if (!this.data.socketTask || !this.data.websocketConnected) {
+ // 先重连
+ this.initWebSocketSafe()
+ setTimeout(() => this._doLogin(), 1500)
+ return
+ }
+ this._doLogin()
+ },
+
+ _doLogin() {
+ if (!this.data.websocketConnected) {
+ wx.showToast({ title: '连接中...', icon: 'none' })
+ return
+ }
+ wx.login({
success: (res) => {
- app.globalData.userInfo = res.userInfo
- this.setData({
- userInfo: res.userInfo,
- hasUserInfo: true
+ if (!res.code) {
+ wx.showToast({ title: '微信登录失败', icon: 'none' })
+ return
+ }
+ // 记录待处理登录(收到 wechat_login_ret 时判断)
+ this._pendingLogin = true
+ this.data.socketTask.send({
+ data: JSON.stringify({
+ type: 'wechat_login',
+ data: { code: res.code }
+ }),
+ fail: () => {
+ this._pendingLogin = false
+ wx.showToast({ title: '发送失败', icon: 'none' })
+ }
})
-
- // 发送用户信息到服务器
- this.sendUserInfoToServer(res.userInfo)
+ },
+ fail:
+
+ // ==============================================
+ // 获取手机号(button open-type="getPhoneNumber")
+ // ==============================================
+ onGetPhoneNumber(e) {
+ // 检查是否有加密数据(旧版方式,个人小程序可用)
+ const { encryptedData, iv, code } = e.detail
+ if (!encryptedData || !iv) {
+ wx.showToast({ title: '请允许授权', icon: 'none' })
+ return
+ }
+
+ // 必须先登录有 openid
+ const openid = this.data.openId || wx.getStorageSync('openId')
+ if (!openid) {
+ wx.showToast({ title: '请先登录', icon: 'none' })
+ return
+ }
+
+ // 确保 WebSocket 连接
+ if (!this.data.websocketConnected) {
+ wx.showToast({ title: '连接中...', icon: 'none' })
+ this.initWebSocketSafe()
+ setTimeout(() => {
+ this._sendPhoneDecrypt(openid, encryptedData, iv)
+ }, 1500)
+ return
+ }
+
+ this._sendPhoneDecrypt(openid, encryptedData, iv)
+ },
+
+ _sendPhoneDecrypt(openid, encryptedData, iv) {
+ if (!this.data.websocketConnected) {
+ wx.showToast({ title: 'WebSocket 未连接', icon: 'none' })
+ return
+ }
+ this._pendingPhone = true
+ this.data.socketTask.send({
+ data: JSON.stringify({
+ type: 'wechat_decrypt_phone',
+ data: { openid, encryptedData, iv }
+ }),
+ fail: () => {
+ this._pendingPhone = false
+ wx.showToast({ title: '发送失败', icon: 'none' })
}
})
},
- getUserInfo(e) {
- // 不推荐使用getUserInfo获取用户信息,预计自2021年4月13日起,getUserInfo将不再弹出弹窗,并直接返回匿名的用户个人信息
- app.globalData.userInfo = e.detail.userInfo
+ // ==============================================
+ // 时间显示
+ // ==============================================
+ updateTimeDisplay() {
+ const now = new Date()
+ const m = now.getMonth() + 1
+ const d = now.getDate()
+ const h = now.getHours()
+ const mm = String(now.getMinutes()).padStart(2, '0')
+ const period = h >= 12 ? '下午' : '上午'
+ const displayH = h > 12 ? h - 12 : h
+
this.setData({
- userInfo: e.detail.userInfo,
- hasUserInfo: true
- })
-
- // 发送用户信息到服务器
- this.sendUserInfoToServer(e.detail.userInfo)
- },
-
- // 发送用户信息到服务器
- sendUserInfoToServer(userInfo) {
- wx.request({
- url: `${app.globalData.apiBase}/user/info`,
- method: 'POST',
- data: {
- userInfo: userInfo,
- deviceId: app.globalData.systemInfo.model
- },
- header: {
- 'content-type': 'application/json'
- },
- success: (res) => {
- console.log('用户信息上传成功', res.data)
- },
- fail: (err) => {
- console.error('用户信息上传失败', err)
- }
+ currentTime: `${m}月${d}日 ${period}${displayH}:${mm}`,
+ yesterdayTime: `昨天 晚上8:36`
})
},
- // 初始化WebSocket连接
- initWebSocket() {
- const socket = wx.connectSocket({
- url: app.globalData.websocketUrl,
- header: {
- 'content-type': 'application/json'
- }
- })
+ // ==============================================
+ // TabBar 选中
+ // ==============================================
+ setTabBarActive() {
+ if (this.getTabBar) this.getTabBar()?.setData({ currentTab: 0 })
+ },
+
+ // ==============================================
+ // 新闻点击
+ // ==============================================
+ onNewsTap(e) {
+ const id = e.currentTarget.dataset.id
+ console.log('点击新闻:', id)
+ wx.showToast({ title: '加载中...', icon: 'none' })
+ },
+
+ // ==============================================
+ // WebSocket 安全版(防重复连接、防崩溃)
+ // ==============================================
+ initWebSocketSafe() {
+ if (this.data.socketTask) return
+
+ const socket = wx.connectSocket({ url: app.globalData.websocketUrl })
+ this.setData({ socketTask: socket })
socket.onOpen(() => {
- console.log('WebSocket连接已打开')
- this.setData({
- websocketConnected: true
- })
-
- // 发送认证信息
+ this.setData({ websocketConnected: true })
socket.send({
data: JSON.stringify({
type: 'auth',
- userId: app.globalData.userInfo ? app.globalData.userInfo.nickName : 'anonymous',
- deviceId: app.globalData.systemInfo.model,
+ userId: app.globalData.userInfo?.nickName || 'anonymous',
+ deviceId: app.globalData.systemInfo?.model || '',
timestamp: Date.now()
})
})
})
- socket.onMessage((res) => {
- console.log('收到WebSocket消息', res.data)
+ socket.onMessage(msg => {
try {
- const data = JSON.parse(res.data)
+ const data = JSON.parse(msg.data)
this.handleWebSocketMessage(data)
- } catch (e) {
- console.error('解析WebSocket消息失败', e)
- }
+ } catch (e) {}
})
socket.onClose(() => {
- console.log('WebSocket连接已关闭')
- this.setData({
- websocketConnected: false
- })
-
- // 3秒后尝试重连
- setTimeout(() => {
- this.initWebSocket()
- }, 3000)
+ this.setData({ websocketConnected: false, socketTask: null })
+ setTimeout(() => this.initWebSocketSafe(), 3000)
})
- socket.onError((err) => {
- console.error('WebSocket连接错误', err)
- this.setData({
- websocketConnected: false
- })
+ socket.onError(() => {
+ this.setData({ websocketConnected: false, socketTask: null })
})
},
- // 处理WebSocket消息
handleWebSocketMessage(data) {
switch (data.type) {
- case 'task_status':
- // 处理任务状态更新
- this.handleTaskStatusUpdate(data)
+ case 'wechat_login_ret':
+ // 收到登录响应(可能比 send 回调更早或更晚到达)
+ if (data.data?.openid) {
+ wx.setStorageSync('openId', data.data.openid)
+ this.setData({ openId: data.data.openid, hasUserInfo: true })
+ wx.showToast({ title: '登录成功', icon: 'success' })
+ } else {
+ wx.showToast({ title: '登录失败:' + (data.data?.error || '未知'), icon: 'none' })
+ }
+ this._pendingLogin = false
break
- case 'message':
- // 处理聊天消息
- this.handleChatMessage(data)
+ case 'ai_message':
+ // AI 回复(聊天页使用)
+ if (this.handleAiMessage) this.handleAiMessage(data)
+ break
+ case 'wechat_decrypt_phone_ret':
+ // 手机号解密响应
+ if (data.data?.phone) {
+ wx.setStorageSync('phoneNumber', data.data.phone)
+ this.setData({ phoneNumber: data.data.phone })
+ wx.showToast({ title: '绑定成功', icon: 'success' })
+ } else {
+ wx.showToast({ title: '获取失败:' + (data.data?.error || '未知'), icon: 'none' })
+ }
+ this._pendingPhone = false
+ break
+ case 'pong':
break
default:
- console.log('未知消息类型', data.type)
+ break
}
},
- // 处理任务状态更新
- handleTaskStatusUpdate(data) {
- // 可以在这里更新任务列表或显示通知
- if (data.status === 'completed') {
- wx.showToast({
- title: '任务完成',
- icon: 'success'
- })
- } else if (data.status === 'failed') {
- wx.showToast({
- title: '任务失败',
- icon: 'error'
- })
- }
- },
-
- // 处理聊天消息
- handleChatMessage(data) {
- // 可以在这里显示新消息通知
- if (data.message) {
- wx.showToast({
- title: '新消息',
- icon: 'none'
- })
- }
- },
-
- // 跳转到聊天页面
+ // ==============================================
+ // 跳转聊天页
+ // ==============================================
goToChat() {
- wx.navigateTo({
- url: '/pages/chat/chat'
- })
+ wx.navigateTo({ url: '/pages/chat/chat' })
},
- // 跳转到任务页面
+ // ==============================================
+ // 跳转任务页
+ // ==============================================
goToTask() {
- wx.navigateTo({
- url: '/pages/task/task'
- })
- },
-
- // 显示设备信息
- showDeviceInfo() {
- const systemInfo = app.globalData.systemInfo
- wx.showModal({
- title: '设备信息',
- content: `设备型号:${systemInfo.model}\n系统版本:${systemInfo.system}\n微信版本:${systemInfo.version}\n屏幕尺寸:${systemInfo.screenWidth}x${systemInfo.screenHeight}`,
- showCancel: false
- })
- },
-
- onShareAppMessage() {
- return {
- title: '智控未来 - 企业微信智能控制系统',
- path: '/pages/index/index'
- }
+ wx.navigateTo({ url: '/pages/task/task' })
}
-})
\ No newline at end of file
+})
diff --git a/Claw/client/wechat_app/pages/index/index.json b/Claw/client/wechat_app/pages/index/index.json
index c746902..163babb 100644
--- a/Claw/client/wechat_app/pages/index/index.json
+++ b/Claw/client/wechat_app/pages/index/index.json
@@ -1,3 +1 @@
-{
- "navigationBarTitleText": "智控未来"
-}
\ No newline at end of file
+{"navigationBarTitleText": "智控未来"}
\ No newline at end of file
diff --git a/Claw/client/wechat_app/pages/index/index.wxml b/Claw/client/wechat_app/pages/index/index.wxml
index d9602a6..28d9b92 100644
--- a/Claw/client/wechat_app/pages/index/index.wxml
+++ b/Claw/client/wechat_app/pages/index/index.wxml
@@ -1,50 +1,283 @@
-
-
-
-
-
- {{userInfo.nickName}}
-
-
-
-
-
- 请先授权登录以使用完整功能
-
-
-
-