雷鸟接收线程优化
authorcheng <chengyf@zhihuishixun.com>
Wed, 17 Sep 2025 14:34:47 +0000 (22:34 +0800)
committercheng <chengyf@zhihuishixun.com>
Wed, 17 Sep 2025 14:34:47 +0000 (22:34 +0800)
code/sdk/zhihui/leiniao/imsl_ln/leiniao_api_at.c
code/sdk/zhihui/leiniao/imsl_ln/ln_api.c
code/sdk/zhihui/leiniao/imsl_ln/ln_api.h
code/sdk/zhihui/leiniao/imsl_ln/ln_api_device_info.c
code/sdk/zhihui/leiniao/imsl_ln/ln_api_speech_chat.c
code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_choose.c
code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_random.c
code/sdk/zhihui/leiniao/leiniao_python_test/device_websocket_client_6.py

index 6b68230..53e385d 100644 (file)
@@ -708,7 +708,8 @@ static uint32_t ln_audio_pause_at(uint8_t *para, uint32_t paraLen, char *strComm
 {
     VAD_UNUSED(para); VAD_UNUSED(paraLen); VAD_UNUSED(strCommonCmd);
     printf("[LN][AUDIO_PAUSE]\n");
-    do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_AUDIO_PAUSE, NULL, 0);
+    // do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_AUDIO_PAUSE, NULL, 0);
+    uapi_ln_audio_download_is_cancel(1);
     return ERRCODE_SUCC;
 }
 
@@ -718,6 +719,7 @@ static uint32_t ln_audio_resume_at(uint8_t *para, uint32_t paraLen, char *strCom
     VAD_UNUSED(para); VAD_UNUSED(paraLen); VAD_UNUSED(strCommonCmd);
     printf("[LN][AUDIO_RESUME]\n");
     do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_AUDIO_RESUME, NULL, 0);
+    uapi_ln_audio_download_is_cancel(0);
     return ERRCODE_SUCC;
 }
 
index 7417323..9b9287f 100644 (file)
@@ -162,6 +162,54 @@ const char* uapi_ln_get_user_id(void)
     return g_ln_device_user_id;
 }
 
+/* 获取设备角色id */
+const char *uapi_ln_get_character_id(void)
+{
+    return g_ln_character_id;
+}
+
+/* 获取设备角色名称 */
+const char *uapi_ln_get_character_name(void)
+{
+    return g_ln_character_name;
+}
+
+/* 获取设备角色眼睛形状 */
+const char *uapi_ln_get_character_eyes_shape(void)
+{
+    return g_ln_avatar_eyes_shape;
+}
+
+/* 获取设备角色头发颜色 */
+const char *uapi_ln_get_character_hair_color(void)
+{
+    return g_ln_avatar_hair_color;
+}
+
+/* 获取设备角色眼睛颜色 */
+const char *uapi_ln_get_character_eyes_color(void)
+{
+    return g_ln_avatar_eyes_color;
+}
+
+/* 获取设备角色头发样式 */
+const char *uapi_ln_get_character_hair_style(void)
+{
+    return g_ln_avatar_hair_style;
+}
+
+/* 获取设备角色屏幕图片 */
+const char *uapi_ln_get_character_screen_image(void)
+{
+    return g_ln_avatar_screen_image;
+}
+
+/* 获取设备角色屏幕图片是否为表情,默认false */
+int uapi_ln_get_character_screen_image_as_emoji(void)
+{
+    return g_ln_avatar_screen_image_as_emoji;
+}
+
 /* 自适应申请HttpClient缓冲(保证header>=2048),失败则逐步扩容重试 */
 HTTPC_RESULT ln_http_prepare_auto(HttpClientData *clientData, int header_size, int resp_size)
 {
@@ -201,18 +249,28 @@ static int ln_calc_file_md5_lower_hex(const char *file_path, char out_hex[33])
         mbedtls_md5_free(&ctx);
         return -1;
     }
-    unsigned char buf[4096];
+    /* 使用堆缓冲,避免在调用栈上占用4KB,降低线程峰值栈使用 */
+    const size_t kBufSize = 4096;
+    unsigned char *buf = (unsigned char *)malloc(kBufSize);
+    if (!buf)
+    {
+        fclose(fp);
+        mbedtls_md5_free(&ctx);
+        return -1;
+    }
     size_t n;
-    while ((n = fread(buf, 1, sizeof(buf), fp)) > 0)
+    while ((n = fread(buf, 1, kBufSize, fp)) > 0)
     {
         if (mbedtls_md5_update(&ctx, buf, n) != 0)
         {
             fclose(fp);
+            free(buf);
             mbedtls_md5_free(&ctx);
             return -1;
         }
     }
     fclose(fp);
+    free(buf);
     if (mbedtls_md5_finish(&ctx, md5_ctx_buf) != 0)
     {
         mbedtls_md5_free(&ctx);
@@ -616,7 +674,7 @@ static uint32_t ln_download_mp3_file_advanced(const char *audio_url, const char
     do
     {
         if (g_ln_audio_download_cancel) {
-            printf("[SSE] download canceled by user\n");
+            printf("download canceled by user\n");
             break;
         }
         HTTPC_RESULT recv_res = HttpClientRecvResponse(&client, &client_data);
@@ -643,6 +701,11 @@ static uint32_t ln_download_mp3_file_advanced(const char *audio_url, const char
                 (void)ln_audio_player_write((const uint8_t*)client_data.responseBuf, (uint32_t)client_data.contentBlockLen);
 
             }
+            else
+            {
+                /* EAGAIN 或 chunk 分界但无数据时,让出CPU避免忙等 */
+                LOS_Msleep(10);
+            }
         }
         else
         {
@@ -879,6 +942,11 @@ uint32_t ln_parse_file_and_process_data(const char *file_path, int interface_typ
     int success_count = 0;
     int error_count = 0;
 
+    if(g_ln_audio_download_cancel)//如果之前中断了播放,新的对话就恢复播放
+    {
+        uapi_ln_audio_download_is_cancel(0);
+    }
+
     while (fgets(line, sizeof(line), fp))
     {
         if (g_ln_audio_download_cancel) {
@@ -956,10 +1024,13 @@ static void ln_http_server_send_task(void const *arg)
 
     while (1)
     {
+        /* 每轮重置 msgsize,兼容底层可能改写该值的实现 */
+        msgsize = sizeof(ln_http_data_mail_t);
         ret = osal_msg_queue_read_copy(g_ln_send_queue, (void *)&mail, &msgsize,
                                        OSAL_MSGQ_WAIT_FOREVER);
         if (ret != OSAL_SUCCESS)
         {
+            LOS_Msleep(100);
             continue;
         }
         if (mail.cmd == LN_HTTP_SERVICE_CMD_GET_DEVICE_TOKEN)
@@ -1124,10 +1195,14 @@ static void ln_http_server_recv_task(void const *arg)
 
     while (1)
     {
+        /* 某些OSAL实现会在read后改写msgsize,循环开始时重置,避免下次读取异常 */
+        msgsize = sizeof(ln_http_data_mail_t);
         ret = osal_msg_queue_read_copy(g_ln_recv_queue, (void *)&mail, &msgsize,
                                        OSAL_MSGQ_WAIT_FOREVER);
         if (ret != OSAL_SUCCESS)
         {
+            printf("[LN][RECV] queue read error=0x%x\n", ret);
+            LOS_Msleep(100);
             continue;
         }
 
@@ -1145,6 +1220,11 @@ static void ln_http_server_recv_task(void const *arg)
             tag = "STORY_FINISH";
         } else if (mail.cmd == LN_HTTP_RECV_DEVICE_CHARACTER) {
             tag = "DEVICE_CHARACTER";
+            //解析角色配置信息
+            if(mail.data)
+            {
+                ln_parse_character_config(mail.data, (int)mail.len);
+            }
         } else if (mail.cmd == LN_HTTP_RECV_DEVICE_USER_ID) {
             tag = "DEVICE_USER_ID";
         } else if (mail.cmd == LN_HTTP_RECV_OTA_TASK) {
@@ -1158,11 +1238,21 @@ static void ln_http_server_recv_task(void const *arg)
             /* 在收到 OTA_DOWNLOAD_MSG 后,解析状态并回调处理(进度/完成/失败) */
             if (mail.data) 
             {    
-                OtaDownloadMsg m = {0};
-                if (parse_ota_download_msg(mail.data, &m) == 0) {
-                    handle_ota_download_msg(&m);
-                } else {
-                    printf("OTA_DOWNLOAD_MSG: parse failed\n");
+                /* 改为堆分配,进一步降低接收线程的峰值栈占用 */
+                OtaDownloadMsg *msg_ptr = (OtaDownloadMsg *)malloc(sizeof(OtaDownloadMsg));
+                if (msg_ptr)
+                {
+                    memset(msg_ptr, 0, sizeof(OtaDownloadMsg));
+                    if (parse_ota_download_msg(mail.data, msg_ptr) == 0) {
+                        handle_ota_download_msg(msg_ptr);
+                    } else {
+                        printf("OTA_DOWNLOAD_MSG: parse failed\n");
+                    }
+                    free(msg_ptr);
+                }
+                else
+                {
+                    printf("OTA_DOWNLOAD_MSG: alloc failed\n");
                 }
             }
         }
index 965f7b8..ace8186 100644 (file)
@@ -166,6 +166,23 @@ void uapi_ln_audio_download_is_cancel(int status);
 uint32_t uapi_ln_is_device_bound(void);
 /* 获取设备绑定用户ID */
 const char* uapi_ln_get_user_id(void);
+/* 获取设备角色id */
+const char *uapi_ln_get_character_id(void);
+/* 获取设备角色名称 */
+const char *uapi_ln_get_character_name(void);
+/* 获取设备角色眼睛形状 */
+const char *uapi_ln_get_character_eyes_shape(void);
+/* 获取设备角色头发颜色 */
+const char *uapi_ln_get_character_hair_color(void);
+/* 获取设备角色眼睛颜色 */
+const char *uapi_ln_get_character_eyes_color(void);
+/* 获取设备角色头发样式 */
+const char *uapi_ln_get_character_hair_style(void);
+/* 获取设备角色屏幕图片 */
+const char *uapi_ln_get_character_screen_image(void);
+/* 获取设备角色屏幕图片是否为表情 */
+int uapi_ln_get_character_screen_image_as_emoji(void);
+
 
 /* ============================== 共享全局状态(跨文件) ============================== */
 extern char *g_device_token;
@@ -175,6 +192,15 @@ extern char g_ln_last_package_url[512];
 extern char g_ln_last_target_version[64];
 extern char g_ln_last_package_md5[65];
 extern int g_ln_last_package_size;
+/* 设备角色信息(由 ln_parse_character_config 填充)*/
+extern char g_ln_character_id[64];              //角色id
+extern char g_ln_character_name[64];            //角色名称
+extern char g_ln_avatar_eyes_shape[64];         //眼睛形状
+extern char g_ln_avatar_hair_color[64];         //头发颜色
+extern char g_ln_avatar_eyes_color[64];         //眼睛颜色
+extern char g_ln_avatar_hair_style[64];         //发型
+extern char g_ln_avatar_screen_image[256];      //屏幕图片
+extern int  g_ln_avatar_screen_image_as_emoji;  //屏幕图片是否为表情
 
 /* ============================== 公共工具函数(供拆分后的模块复用) ============================== */
 
@@ -189,6 +215,8 @@ uint32_t ln_token_is_expired(int *expired_out);
 uint32_t uapi_ln_refresh_device_token(void);
 uint32_t uapi_ln_device_get_user_id(LnDeviceBindStatus *out_status, char *out_user_id, uint32_t out_user_id_len);
 
+//解析设备角色配置
+void ln_parse_character_config(const char *json_buf, int json_len);
 /* ============================== 各接口函数声明 ============================== */
 uint32_t ln_get_device_token_http(const char *device_id,
                                   const char *device_iccid,
index 949d383..05d5a55 100644 (file)
@@ -3,6 +3,114 @@
 #include <string.h>
 #include <cJSON.h>
 
+/* =================== 设备角色信息存储 =================== */
+char g_ln_character_id[64] = {0};
+char g_ln_character_name[64] = {0};
+char g_ln_avatar_hair_color[64] = {0};
+char g_ln_avatar_eyes_color[64] = {0};
+char g_ln_avatar_hair_style[64] = {0};
+char g_ln_avatar_eyes_shape[64] = {0};
+char g_ln_avatar_screen_image[256] = {0};
+int  g_ln_avatar_screen_image_as_emoji = 0;
+
+/* 安全写入帮助函数 */
+static inline void ln_safe_strcpy(char *dst, size_t dst_size, const char *src)
+{
+    if (!dst || dst_size == 0) return;
+    if (src == NULL) {
+        dst[0] = '\0';
+        return;
+    }
+    strncpy(dst, src, dst_size - 1);
+    dst[dst_size - 1] = '\0';
+}
+
+/* 解析设备角色配置 JSON 并保存到全局变量 */
+void ln_parse_character_config(const char *json_buf, int json_len)
+{
+    if (!json_buf || json_len <= 0)
+    {
+        printf("[DEVICE_CHARACTER] invalid json input\n");
+        return;
+    }
+
+    char *text = (char *)malloc((size_t)json_len + 1);
+    if (!text) return;
+    memcpy(text, json_buf, (size_t)json_len);
+    text[json_len] = '\0';
+
+    cJSON *root = cJSON_Parse(text);
+    if (!root)
+    {
+        printf("[DEVICE_CHARACTER] json parse failed\n");
+        free(text);
+        return;
+    }
+
+    cJSON *errorCode = cJSON_GetObjectItem(root, "errorCode");
+    if (errorCode && !cJSON_IsNull(errorCode))
+    {
+        printf("[DEVICE_CHARACTER] server error: %s\n",
+               cJSON_IsString(errorCode) ? errorCode->valuestring : "unknown");
+        cJSON_Delete(root);
+        free(text);
+        return;
+    }
+
+    cJSON *data = cJSON_GetObjectItem(root, "data");
+    if (!data || !cJSON_IsObject(data))
+    {
+        printf("[DEVICE_CHARACTER] no data object\n");
+        cJSON_Delete(root);
+        free(text);
+        return;
+    }
+
+    /* 基础字段 */
+    cJSON *cid = cJSON_GetObjectItem(data, "characterId");
+    cJSON *cname = cJSON_GetObjectItem(data, "characterName");
+    ln_safe_strcpy(g_ln_character_id, sizeof(g_ln_character_id), (cid && cJSON_IsString(cid)) ? cid->valuestring : "");
+    ln_safe_strcpy(g_ln_character_name, sizeof(g_ln_character_name), (cname && cJSON_IsString(cname)) ? cname->valuestring : "");
+
+    /* 头像配置 */
+    cJSON *avatar = cJSON_GetObjectItem(data, "characterAvatar");
+    if (avatar && cJSON_IsObject(avatar))
+    {
+        cJSON *hairColor = cJSON_GetObjectItem(avatar, "hairColor");
+        cJSON *eyesColor = cJSON_GetObjectItem(avatar, "eyesColor");
+        cJSON *hairStyle = cJSON_GetObjectItem(avatar, "hairStyle");
+        cJSON *eyesShape = cJSON_GetObjectItem(avatar, "eyesShape");
+        cJSON *screenImage = cJSON_GetObjectItem(avatar, "screenImage");
+        cJSON *screenImageAsEmoji = cJSON_GetObjectItem(avatar, "screenImageAsEmoji");
+
+        ln_safe_strcpy(g_ln_avatar_hair_color, sizeof(g_ln_avatar_hair_color), (hairColor && cJSON_IsString(hairColor)) ? hairColor->valuestring : "");
+        ln_safe_strcpy(g_ln_avatar_eyes_color, sizeof(g_ln_avatar_eyes_color), (eyesColor && cJSON_IsString(eyesColor)) ? eyesColor->valuestring : "");
+        ln_safe_strcpy(g_ln_avatar_hair_style, sizeof(g_ln_avatar_hair_style), (hairStyle && cJSON_IsString(hairStyle)) ? hairStyle->valuestring : "");
+        ln_safe_strcpy(g_ln_avatar_eyes_shape, sizeof(g_ln_avatar_eyes_shape), (eyesShape && cJSON_IsString(eyesShape)) ? eyesShape->valuestring : "");
+        ln_safe_strcpy(g_ln_avatar_screen_image, sizeof(g_ln_avatar_screen_image), (screenImage && cJSON_IsString(screenImage)) ? screenImage->valuestring : "");
+        g_ln_avatar_screen_image_as_emoji = (screenImageAsEmoji && cJSON_IsBool(screenImageAsEmoji)) ? (cJSON_IsTrue(screenImageAsEmoji) ? 1 : 0) : 0;
+    }
+    else
+    {
+        /* 缺省时清空 */
+        g_ln_avatar_hair_color[0] = '\0';
+        g_ln_avatar_eyes_color[0] = '\0';
+        g_ln_avatar_hair_style[0] = '\0';
+        g_ln_avatar_eyes_shape[0] = '\0';
+        g_ln_avatar_screen_image[0] = '\0';
+        g_ln_avatar_screen_image_as_emoji = 0;
+    }
+
+    printf("[DEVICE_CHARACTER] parsed: id=%s\n name=%s\n hairColor=%s\n eyesColor=%s\n hairStyle=%s\n eyesShape=%s\n screenImage=%s\n asEmoji=%d\n",
+           g_ln_character_id, g_ln_character_name,
+           g_ln_avatar_hair_color, g_ln_avatar_eyes_color,
+           g_ln_avatar_hair_style, g_ln_avatar_eyes_shape,
+           g_ln_avatar_screen_image, g_ln_avatar_screen_image_as_emoji);
+
+    cJSON_Delete(root);
+    free(text);
+}
+
 // 获取设备角色信息
 uint32_t ln_device_get_character_http(const char *device_id,
                                       const char *device_token)
index 03c22e2..2283a03 100644 (file)
@@ -165,7 +165,7 @@ uint32_t ln_speech_chat_stream_http(const char *device_id,
     printf("chatstream header_buf: %s\n", header_buf);
 
     /* 准备缓冲区(自适应) */
-    HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, LN_CLIENT_PREPARE_SIZE);
+    HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, LN_CLIENT_PREPARE_SIZE*2);
     if (prep != HTTP_SUCCESS)
     {
         free(body);
@@ -240,6 +240,11 @@ uint32_t ln_speech_chat_stream_http(const char *device_id,
                 }
                 total_written += written;
             }
+            else
+            {
+                /* 无新数据块但仍需继续时,适当让出CPU降低忙等 */
+                LOS_Msleep(10);
+            }
         }
         else
         {
index 54a0532..1f2ecac 100644 (file)
@@ -237,6 +237,11 @@ uint32_t ln_story_choose_http(const char *device_id,
                 }
                 total_written += written;
             }
+            else
+            {
+                /* 无新数据块但仍需继续时,适当让出CPU降低忙等 */
+                LOS_Msleep(10);
+            }
         }
         else
         {
index 281bbab..2c48113 100644 (file)
@@ -170,6 +170,11 @@ uint32_t ln_story_random_http(const char *device_id,
                 }
                 total_written += written;
             }
+            else
+            {
+                /* 无新数据块但仍需继续时,适当让出CPU降低忙等 */
+                LOS_Msleep(10);
+            }
         }
         else
         {
index 54d9618..7236dbe 100644 (file)
@@ -11,6 +11,8 @@ import hmac
 import random
 import string
 import logging
+import uuid
+import base64
 from enum import Enum
 
 # 配置日志
@@ -23,8 +25,8 @@ logger = logging.getLogger(__name__)
 
 # 常量定义
 class Constants(Enum):
-    # WS_BASE_URL = "wss://ambybox-api.test.leiniao.com/ambybox-device-api/v1/websocket"
-    WS_BASE_URL = "ws://127.0.0.1:8765"  # 临时本地测试,到时候需要替换为正式环境
+    WS_BASE_URL = "wss://ambybox-api.test.leiniao.com/ambybox-device-api/v1/websocket"
+    # WS_BASE_URL = "ws://127.0.0.1:8765"  # 临时本地测试,到时候需要替换为正式环境
     TOKEN_API_URL = "https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/auth/getDeviceToken"
     UPGRADE_TEST_URL = "https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/upgrade/getTask"
     SIGN_SECRET = "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus="
@@ -34,6 +36,7 @@ class Constants(Enum):
     HEARTBEAT_TIMEOUT = 10   # 心跳超时(秒)
     CONNECT_TIMEOUT = 20     # 连接超时(秒)
     MAX_RECONNECT_DELAY = 60 # 最大重连延迟(秒)
+    AUDIO_CHUNK_SIZE = 8 * 1024  # 音频分片大小(8KB)
 
 
 class DeviceWebSocketClient:
@@ -59,7 +62,8 @@ class DeviceWebSocketClient:
             "SleepModeEnd": self.handle_sleep_mode_end,
             "DeviceBind": self.handle_device_bind,
             "DeviceUnbind": self.handle_device_unbind,
-            "ScheduleReminder": self.handle_schedule_reminder
+            "ScheduleReminder": self.handle_schedule_reminder,
+            "VoiceChatResponse": self.handle_voice_chat_response  # 新增语音响应处理器
         }
         
         # 线程相关
@@ -182,7 +186,7 @@ class DeviceWebSocketClient:
         self.reconnect_count = 0
         self._start_heartbeat()
 
-        # 发送上线通知(修正缩进和变量名)
+        # 发送上线通知
         online_msg = json.dumps({"type": "online", "deviceId": self.device_id, "timestamp": int(time.time())})
         self.ws.send(online_msg)
         print(f"📤 已发送上线消息: {online_msg}")
@@ -303,7 +307,92 @@ class DeviceWebSocketClient:
         self.connected = False
         logger.info("WebSocket客户端已关闭")
 
-    # 命令处理器实现
+    # 新增:语音对话分片发送功能
+    def send_voice_chat_request(self, audio_file_path: str) -> bool:
+        """
+        发送语音对话请求(支持分片传输)
+        
+        Args:
+            audio_file_path: 音频文件本地路径
+            
+        Returns:
+            是否成功启动分片发送(不保证所有分片都发送成功)
+        """
+        try:
+            # 1. 读取音频文件二进制数据
+            with open(audio_file_path, 'rb') as f:
+                audio_bytes = f.read()
+            total_size = len(audio_bytes)
+            if total_size == 0:
+                logger.error("音频文件为空,无法发送")
+                return False
+            
+            # 2. 生成对话唯一标识(UUIDv4)
+            chat_id = str(uuid.uuid4())
+            logger.info(f"开始处理语音对话,chatId: {chat_id},文件大小: {total_size}字节")
+            
+            # 3. 分片配置(8KB原始数据/分片)
+            chunk_size = Constants.AUDIO_CHUNK_SIZE.value
+            total_chunks = (total_size + chunk_size - 1) // chunk_size  # 向上取整计算总分片数
+            
+            # 4. 循环发送分片
+            for chunk_idx in range(total_chunks):
+                # 计算当前分片的起止位置
+                start = chunk_idx * chunk_size
+                end = min(start + chunk_size, total_size)
+                current_chunk = audio_bytes[start:end]
+                
+                # 5. 音频分片Base64编码
+                audio_base64 = base64.b64encode(current_chunk).decode('utf-8')
+                
+                # 6. 构造分片消息
+                message = {
+                    "deviceId": self.device_id,
+                    "timestamp": int(time.time()),
+                    "messageId": f"voice_{chat_id}_{chunk_idx}",  # 分片唯一ID
+                    "command": "VoiceChatRequest",
+                    "data": {
+                        "audioBase64": audio_base64,
+                        "chatId": chat_id,
+                        "finished": (chunk_idx == total_chunks - 1)  # 最后一个分片标记为完成
+                    }
+                }
+                
+                # 7. 发送分片(调用现有发送方法)
+                if not self.send_message(json.dumps(message)):
+                    logger.error(f"第{chunk_idx + 1}/{total_chunks}个分片发送失败,终止传输")
+                    return False
+                
+                logger.info(f"已发送分片 {chunk_idx + 1}/{total_chunks} "
+                            f"(大小: {len(current_chunk)}字节,是否最后一片: {message['data']['finished']})")
+                
+                # 可选:添加微小延迟避免服务器压力
+                time.sleep(0.05)
+            
+            logger.info(f"所有分片发送完成,共{total_chunks}个分片,chatId: {chat_id}")
+            return True
+            
+        except FileNotFoundError:
+            logger.error(f"音频文件不存在: {audio_file_path}")
+        except Exception as e:
+            logger.error(f"语音分片发送失败: {str(e)}")
+        return False
+
+    # 新增:语音对话响应处理
+    def handle_voice_chat_response(self, data: Dict[str, Any]) -> None:
+        """处理服务端返回的语音对话响应"""
+        logger.info("收到语音对话响应 (VoiceChatResponse)")
+        response_data = data.get("data", {})
+        chat_id = response_data.get("chatId")
+        finished = response_data.get("finished", False)
+        content_data = response_data.get("data", {})
+        
+        logger.info(f"对话ID: {chat_id}")
+        logger.info(f"是否处理完成: {finished}")
+        logger.info(f"文本响应: {content_data.get('textContent', '无')}")
+        logger.info(f"音频响应URL: {content_data.get('audioUrl', '无')}")
+
+    # 原有命令处理器实现
     def handle_character_change(self, data: Dict[str, Any]) -> None:
         logger.info("收到角色切换命令")
         command_data = data.get("data", {})
@@ -401,6 +490,10 @@ def main() -> NoReturn:
         return
     
     try:
+        # 示例:连接成功后发送测试音频(替换为实际音频文件路径)
+        time.sleep(2)  # 等待连接稳定
+        # ws_client.send_voice_chat_request("test_audio.mp3")  # 取消注释并替换为实际音频路径
+        
         while True:
             time.sleep(1)
     except KeyboardInterrupt: