添加故事相关接口at测试,还有问题发送选择故事(SSE)或者"随机故事(SSE)的at指令一直没有返回结果后续再看 2200/1 2200/head 2201/target/1 2202/target/1
authorluozheng <1732036298@qq.com>
Wed, 3 Sep 2025 05:57:31 +0000 (13:57 +0800)
committerluozheng <1732036298@qq.com>
Wed, 3 Sep 2025 05:57:31 +0000 (13:57 +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/leiniao_python_test/story_apis_test_3.py

index 5837a1c..09bfcec 100644 (file)
@@ -545,6 +545,47 @@ static uint32_t ln_speech_chat_stream_at(uint8_t *para, uint32_t paraLen, char *
     return ERRCODE_SUCC;
 }
 
+/* 故事-选择故事:AT^LN_STORY_CHOOSE[=audioPath] */
+static uint32_t ln_story_choose_at(uint8_t *para, uint32_t paraLen, char *strCommonCmd)
+{
+    VAD_UNUSED(strCommonCmd);
+    uint8_t argv[LN_MAX_ARGC][LN_MAX_ARGV_LEN] = {0}; uint32_t argc = 0; (void)ConvertCmdToParam(para, paraLen, argv, &argc);
+    const char *audio_path = (argc >= 1) ? (const char *)argv[0] : "/user/story_request.mp3";
+    ln_story_choose_req_t req = {0}; strncpy(req.audio_path, audio_path, sizeof(req.audio_path) - 1);
+    printf("[LN][STORY_CHOOSE] audio=%s\n", req.audio_path);
+    do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_STORY_CHOOSE, (char *)&req, sizeof(req));
+    return ERRCODE_SUCC;
+}
+
+/* 故事-随机故事:AT^LN_STORY_RANDOM[=storyTheme] */
+static uint32_t ln_story_random_at(uint8_t *para, uint32_t paraLen, char *strCommonCmd)
+{
+    VAD_UNUSED(strCommonCmd);
+    uint8_t argv[LN_MAX_ARGC][LN_MAX_ARGV_LEN] = {0}; uint32_t argc = 0; (void)ConvertCmdToParam(para, paraLen, argv, &argc);
+    ln_story_random_req_t req = {0}; if (argc >= 1) { strncpy(req.story_theme, (const char *)argv[0], sizeof(req.story_theme) - 1); }
+    printf("[LN][STORY_RANDOM] theme=%s\n", req.story_theme[0] ? req.story_theme : "<random>");
+    do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_STORY_RANDOM, (char *)&req, sizeof(req));
+    return ERRCODE_SUCC;
+}
+
+/* 故事-获取退出哄睡询问:AT^LN_STORY_EXIT */
+static uint32_t ln_story_exit_inquiry_at(uint8_t *para, uint32_t paraLen, char *strCommonCmd)
+{
+    VAD_UNUSED(para); VAD_UNUSED(paraLen); VAD_UNUSED(strCommonCmd);
+    printf("[LN][STORY_EXIT_INQUIRY]\n");
+    do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_STORY_EXIT_INQUIRY, NULL, 0);
+    return ERRCODE_SUCC;
+}
+
+/* 故事-确认结束哄睡模式:AT^LN_STORY_FINISH */
+static uint32_t ln_story_finish_at(uint8_t *para, uint32_t paraLen, char *strCommonCmd)
+{
+    VAD_UNUSED(para); VAD_UNUSED(paraLen); VAD_UNUSED(strCommonCmd);
+    printf("[LN][STORY_FINISH]\n");
+    do_ln_http_server_send_data(LN_HTTP_SERVICE_CMD_STORY_FINISH, NULL, 0);
+    return ERRCODE_SUCC;
+}
+
 
 static const at_cmd_table_t g_ln_api_cmd[] = {
     /* ----------------------------------- VAD ---------------------------------- */
@@ -559,6 +600,11 @@ static const at_cmd_table_t g_ln_api_cmd[] = {
     /* ---------------------------------- 雷鸟API --------------------------------- */
     {(char *)"AT^LN_GET_TOKEN", ln_get_device_token_at, (char *)"获取雷鸟设备Token: AT^LN_GET_TOKEN[=deviceId,deviceIccid,deviceModel,deviceVersion]"},
     {(char *)"AT^LN_SPEECH_CHAT", ln_speech_chat_stream_at, (char *)"语音对话流: AT^LN_SPEECH_CHAT"},
+    /* 故事相关 AT 指令 */
+    {(char *)"AT^LN_STORY_CHOOSE", ln_story_choose_at, (char *)"选择故事(SSE): AT^LN_STORY_CHOOSE=user/story_request.mp3"},
+    {(char *)"AT^LN_STORY_RANDOM", ln_story_random_at, (char *)"随机故事(SSE): AT^LN_STORY_RANDOM=童话故事"},
+    {(char *)"AT^LN_STORY_EXIT", ln_story_exit_inquiry_at, (char *)"获取退出哄睡询问(JSON): AT^LN_STORY_EXIT"},
+    {(char *)"AT^LN_STORY_FINISH", ln_story_finish_at, (char *)"确认结束哄睡(JSON): AT^LN_STORY_FINISH"},
 };
 
 at_cmd_table_t *get_leiniao_api_cmd_table(uint32_t *cmd_count)
index db3693c..48e1d7a 100644 (file)
@@ -12,7 +12,7 @@
 
 static app_task_definition_t ln_send_taskcfg = {0};
 static app_task_definition_t ln_recv_taskcfg = {0};
-#define LN_HTTP_BOARD_TASK_SIZE 0x2000
+#define LN_HTTP_BOARD_TASK_SIZE 0x3000
 
 static unsigned long g_ln_send_queue = 0xFFFFFFFF;
 static unsigned long g_ln_recv_queue = 0xFFFFFFFF;
@@ -66,6 +66,15 @@ char *uapi_ln_get_device_token(void)
 #define LN_FULL_URL LN_BASE_URL LN_INTERFACE_PATH
 #define LN_CHAT_PATH "/v1/speechChat/chatStream"
 #define LN_CHAT_URL LN_BASE_URL LN_CHAT_PATH
+/* 故事接口路径 */
+#define LN_STORY_CHOOSE_PATH "/v1/story/chooseStory"
+#define LN_STORY_RANDOM_PATH "/v1/story/randomStory"
+#define LN_STORY_EXIT_INQUIRY_PATH "/v1/story/getExitSleepInquiry"
+#define LN_STORY_FINISH_PATH "/v1/story/finishSleepMode"
+#define LN_STORY_CHOOSE_URL LN_BASE_URL LN_STORY_CHOOSE_PATH
+#define LN_STORY_RANDOM_URL LN_BASE_URL LN_STORY_RANDOM_PATH
+#define LN_STORY_EXIT_INQUIRY_URL LN_BASE_URL LN_STORY_EXIT_INQUIRY_PATH
+#define LN_STORY_FINISH_URL LN_BASE_URL LN_STORY_FINISH_PATH
 #define LN_SIGN_SECRET "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus="
 
 /* 生成[a-zA-Z0-9]随机串 */
@@ -278,7 +287,6 @@ static uint32_t ln_get_device_token_http(const char *device_id,
             g_device_token = strdup("<null>");
         }
         /* -------------------------------------------------------------------------- */
-        printf("LN Device Token: %s,len=%d\n", g_device_token, (int)strlen(g_device_token));
         // 打印完整的json字符串
         printf("\njson=%s\n",json_text);
         if (expire && cJSON_IsNumber(expire))
@@ -322,6 +330,24 @@ static uint8_t *ln_read_file_all(const char *path, int *out_len)
     return buf;
 }
 
+/* 自适应申请HttpClient缓冲(保证header>=2048),失败则逐步扩容重试 */
+static HTTPC_RESULT ln_http_prepare_auto(HttpClientData *clientData, int header_size, int resp_size)
+{
+    if (header_size < 2048) header_size = 2048;
+    if (resp_size < 2048) resp_size = 2048;
+    HTTPC_RESULT ret = HttpClientPrepare(clientData, header_size, resp_size);
+    if (ret == HTTP_SUCCESS) return ret;
+    int tries = 0;
+    while (ret != HTTP_SUCCESS && tries < 3) {
+        header_size = header_size < 4096 ? 4096 : header_size * 2;
+        resp_size = resp_size < (16 * 1024) ? (16 * 1024) : resp_size * 2;
+        if (resp_size > (256 * 1024)) resp_size = (256 * 1024);
+        ret = HttpClientPrepare(clientData, header_size, resp_size);
+        tries++;
+    }
+    return ret;
+}
+
 /* 简单 multipart/form-data 组包(单文件字段 file) */
 static int ln_build_multipart_form(const char *boundary,
                                    const char *filename,
@@ -397,14 +423,13 @@ static uint32_t ln_speech_chat_stream_http(const char *device_id,
         "deviceId: %s\r\n"
         "deviceToken: %s\r\n"
         "Accept: text/event-stream\r\n"
-        "Cache-Control: no-cache\r\n"
-        "Connection: keep-alive\r\n",
+        "Cache-Control: no-cache\r\n",
         device_id, device_token);
     if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) { free(body); return ERRCODE_FAIL; }
     HttpClientSetCustomHeader(&client, header_buf);
 
-    /* 准备缓冲区 */
-    HTTPC_RESULT prep = HttpClientPrepare(&clientData, 2048, 64 * 1024);
+    /* 准备缓冲区(自适应) */
+    HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 64 * 1024);
     if (prep != HTTP_SUCCESS) { free(body); return ERRCODE_FAIL; }
 
     /* 设置POST数据与Content-Type(放在Prepare之后,避免被重置) */
@@ -459,6 +484,519 @@ static uint32_t ln_speech_chat_stream_http(const char *device_id,
 
 /* ============================ 雷鸟语音对话流 End ============================ */
 
+/* ============================ 故事相关接口 Start ============================ */
+
+// 累积并解析SSE数据(补充实现)
+static void ln_sse_accumulate_and_dispatch(int cmd, const char *data, int data_len,
+                                          char *acc, int *acc_len, int acc_size) {
+    if (!data || data_len <= 0 || !acc || !acc_len || *acc_len >= acc_size) return;
+
+    // 累积数据
+    int copy_len = (acc_size - *acc_len - 1) < data_len ? (acc_size - *acc_len - 1) : data_len;
+    memcpy(acc + *acc_len, data, copy_len);
+    *acc_len += copy_len;
+    acc[*acc_len] = '\0';
+
+    // 分割SSE事件(按"\n\n"分割)
+    char *ptr = acc;
+    while ((ptr = strstr(ptr, "\n\n")) != NULL) {
+        int event_len = ptr - acc;
+        if (event_len > 0) {
+            // 提取data字段(去掉"date: "前缀)
+            char *data_start = strstr(acc, "data: ");
+            if (data_start) {
+                data_start += 6;  // 跳过"data: "
+                int json_len = (acc + event_len) - data_start;
+                char *json = (char *)malloc(json_len + 1);
+                if (json) {
+                    memcpy(json, data_start, json_len);
+                    json[json_len] = '\0';
+                    // 投递解析后的JSON数据
+                    do_ln_http_server_recv_data(cmd, json, json_len);
+                    free(json);
+                }
+            }
+        }
+        // 移动指针处理剩余数据
+        *acc_len -= (ptr - acc + 2);
+        memmove(acc, ptr + 2, *acc_len);
+        acc[*acc_len] = '\0';
+        ptr = acc;
+    }
+}
+
+/* 通用:设置SSE头(deviceId/deviceToken/Accept) */
+static int ln_set_sse_header(HttpClient *client, const char *device_id, const char *device_token)
+{
+    char header_buf[512];
+    int hdr = snprintf(header_buf, sizeof(header_buf),
+        "deviceId: %s\r\n"
+        "deviceToken: %s\r\n"
+        "Accept: text/event-stream\r\n"
+        "Cache-Control: no-cache\r\n",
+        device_id, device_token);
+    if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) return -1;
+    HttpClientSetCustomHeader(client, header_buf);
+    return 0;
+}
+
+/* 故事-选择故事(SSE,multipart)*/
+static uint32_t ln_story_choose_http(const char *device_id,
+                                     const char *device_token,
+                                     const char *audio_path)
+{
+    if (!device_id || !device_token || !audio_path) {
+        printf("story choose: invalid parameters\n");
+        return ERRCODE_FAIL;
+    }
+
+    /* 读取音频文件 */
+    int audio_len = 0;
+    uint8_t *audio_buf = ln_read_file_all(audio_path, &audio_len);
+    if (!audio_buf || audio_len <= 0) {
+        printf("story choose: read audio file failed (path: %s)\n", audio_path);
+        return ERRCODE_FAIL;
+    }
+
+    /* 生成 boundary 并构建 multipart 表单数据 */
+    char boundary[64] = {0};
+    int boundary_len = snprintf(boundary, sizeof(boundary), "----LNBOUNDARY-%u", 
+                               (unsigned)uapi_tcxo_get_ms());
+    if (boundary_len <= 0 || boundary_len >= (int)sizeof(boundary)) {
+        printf("story choose: create boundary failed\n");
+        free(audio_buf);
+        return ERRCODE_FAIL;
+    }
+
+    char *body = NULL;
+    int body_len = 0;
+    char content_type[96] = {0};
+    if (ln_build_multipart_form(boundary, "story_request.mp3", audio_buf, audio_len,
+                               &body, &body_len, content_type, sizeof(content_type)) != 0) {
+        printf("story choose: build multipart form failed\n");
+        free(audio_buf);
+        return ERRCODE_FAIL;
+    }
+    free(audio_buf); // 音频数据已复制到 body,释放原始缓冲区
+
+    /* CAT1 网络保活处理 */
+    cat1_api_add_cat1_vote();cat1_api_wakeup_cat1(1);cat1_api_keep_wakeup(1);cat1_api_disable_cat1_cmd(0);
+    LOS_Msleep(300);
+
+    HttpClient client = {0};
+    HttpClientData clientData = {0};
+    client.isHttp = false; // 使用 HTTPS
+    client.remotePort = 443;
+
+    /* 设置请求头(包含Content-Type,保持与Python一致) */
+    {
+        char header_buf[1024];
+        int hdr = snprintf(header_buf, sizeof(header_buf),
+            "deviceId: %s\r\n"
+            "deviceToken: %s\r\n"
+            "Accept: text/event-stream\r\n"
+            "Cache-Control: no-cache\r\n"
+            "Content-Type: %s\r\n",
+            device_id, device_token, content_type);
+        if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) {
+            printf("story choose: build header failed\n");
+            free(body);
+            return ERRCODE_FAIL;
+        }
+        HttpClientSetCustomHeader(&client, header_buf);
+    }
+
+    /* 自适应准备 HTTP 缓冲区 */
+    HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024);
+    if (prep != HTTP_SUCCESS) {
+        printf("story choose: prepare HTTP buffer failed (code: %d)\n", prep);
+        free(body);
+        return ERRCODE_FAIL;
+    }
+
+    /* 设置 POST 数据 */
+    clientData.postBuf = body;
+    clientData.postBufLen = body_len;
+    clientData.postContentType = content_type;
+
+    /* 发送 POST 请求 */
+    HTTPC_RESULT res = HttpClientPostRequest(&client, LN_STORY_CHOOSE_URL, &clientData);
+    if (res != HTTP_SUCCESS) {
+        if (res == HTTP_EAGAIN) {
+            /* 驱动状态机直到成功或失败 */
+            while (1) {
+                HTTPC_RESULT rrp = HttpClientRecvResponse(&client, &clientData);
+                if (rrp == HTTP_SUCCESS) { break; }
+                if (rrp == HTTP_EAGAIN) { continue; }
+                printf("story choose post failed %d\n", rrp);
+                free(body);
+                HttpClientUnprepare(&clientData);
+                HttpClientClse(&client);
+                return ERRCODE_FAIL;
+            }
+        } else {
+            printf("story choose: send POST request failed (code: %d)\n", res);
+            free(body);
+            HttpClientUnprepare(&clientData);
+            HttpClientClse(&client);
+            return ERRCODE_FAIL;
+        }
+    }
+
+    /* 检查 HTTP 响应码 */
+    int http_code = HttpClientGetResponseCode(&client);
+    if (http_code != 200) {
+        printf("story choose: invalid HTTP response code (%d)\n", http_code);
+        // 新增:打印响应体内容
+        if (clientData.responseBuf && clientData.responseBufLen > 0) {
+            char *resp = (char *)malloc(clientData.responseBufLen + 1);
+            if (resp) {
+                memcpy(resp, clientData.responseBuf, clientData.responseBufLen);
+                resp[clientData.responseBufLen] = '\0';
+                printf("story choose: server response: %s\n", resp);
+                free(resp);
+            }
+        }
+        free(body);
+        HttpClientUnprepare(&clientData);
+        HttpClientClse(&client);
+        return ERRCODE_FAIL;
+    }
+
+    /* 循环接收 SSE 分块数据(带累积处理)*/
+    int loop_guard = 0;
+    char sse_acc[4096] = {0}; // SSE 数据累积缓冲区
+    int sse_acc_len = 0;
+    const int MAX_LOOP = 2000; // 约 40 秒超时(20ms * 2000)
+    while (loop_guard++ < MAX_LOOP) {
+        HTTPC_RESULT recv_res = HttpClientRecvResponse(&client, &clientData);
+        if (recv_res == HTTP_SUCCESS || recv_res == HTTP_EAGAIN) {
+            if (clientData.responseBuf && clientData.responseBufLen > 0) {
+                // 累积并分发 SSE 数据
+                ln_sse_accumulate_and_dispatch(LN_HTTP_RECV_STORY_CHOOSE,
+                                              clientData.responseBuf,
+                                              clientData.responseBufLen,
+                                              sse_acc, &sse_acc_len, sizeof(sse_acc));
+                HttpClientReset(&clientData); // 重置缓冲区继续接收
+            } else {
+                LOS_Msleep(20); // 无数据时短暂休眠
+            }
+            continue;
+        } else {
+            printf("story choose: SSE receive ended (code: %d)\n", recv_res);
+            break;
+        }
+    }
+
+    /* 释放资源 */
+    free(body);
+    HttpClientUnprepare(&clientData);
+    HttpClientClse(&client);
+    return ERRCODE_SUCC;
+}
+
+/* 故事-随机故事(SSE,JSON)*/
+static uint32_t ln_story_random_http(const char *device_id,
+                                     const char *device_token,
+                                     const char *story_theme /* 可为NULL */)
+{
+    if (!device_id || !device_token) {
+        printf("story random: invalid parameters\n");
+        return ERRCODE_FAIL;
+    }
+
+    /* CAT1 网络保活处理 */
+    cat1_api_add_cat1_vote();cat1_api_wakeup_cat1(1);cat1_api_keep_wakeup(1);cat1_api_disable_cat1_cmd(0);
+    LOS_Msleep(200);
+
+    HttpClient client = {0};
+    HttpClientData clientData = {0};
+    client.isHttp = false; // 使用 HTTPS
+    client.remotePort = 443;
+
+    /* 设置请求头(包含Content-Type,保持与Python一致) */
+    {
+        char header_buf[1024];
+        int hdr = snprintf(header_buf, sizeof(header_buf),
+            "deviceId: %s\r\n"
+            "deviceToken: %s\r\n"
+            "Accept: text/event-stream\r\n"
+            "Cache-Control: no-cache\r\n"
+            "Content-Type: application/json\r\n",
+            device_id, device_token);
+        if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) {
+            printf("story random: build header failed\n");
+            return ERRCODE_FAIL;
+        }
+        HttpClientSetCustomHeader(&client, header_buf);
+    }
+
+    /* 自适应准备 HTTP 缓冲区 */
+    HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024);
+    if (prep != HTTP_SUCCESS) {
+        printf("story random: prepare HTTP buffer failed (code: %d)\n", prep);
+        return ERRCODE_FAIL;
+    }
+
+    /* 构建 JSON 请求体(增加转义处理,避免格式错误)*/
+    char json_body[256] = {0};
+    char escaped_theme[256] = {0};
+    int json_len = 0;
+
+    // 对故事主题进行JSON转义(处理双引号和反斜杠)
+    if (story_theme && story_theme[0]) {
+        int i = 0, j = 0;
+        while (story_theme[i] && j < sizeof(escaped_theme) - 1) {
+            if (story_theme[i] == '"') {
+                escaped_theme[j++] = '\\';
+                escaped_theme[j++] = '"';
+            } else if (story_theme[i] == '\\') {
+                escaped_theme[j++] = '\\';
+                escaped_theme[j++] = '\\';
+            } else {
+                escaped_theme[j++] = story_theme[i];
+            }
+            i++;
+        }
+        escaped_theme[j] = '\0';
+        json_len = snprintf(json_body, sizeof(json_body), 
+                          "{\"storyTheme\":\"%s\"}", escaped_theme);
+    } else {
+        json_len = snprintf(json_body, sizeof(json_body), "{}");
+    }
+    if (json_len <= 0 || json_len >= (int)sizeof(json_body)) {
+        printf("story random: build JSON body failed\n");
+        HttpClientUnprepare(&clientData);
+        return ERRCODE_FAIL;
+    }
+
+    /* 设置 POST 数据 */
+    clientData.postBuf = json_body;
+    clientData.postBufLen = json_len;
+    clientData.postContentType = (char *)"application/json";
+
+
+    /* 打印发送给服务端的请求数据(关键调试信息)*/
+    printf("story random: request details:\n");
+    printf("  URL:%s\n", LN_STORY_RANDOM_URL); // 从Python的RANDOM_STORY_URL提取
+    printf("  Headers:\n");
+    printf("    deviceId: %s\n", device_id);
+    printf("    deviceToken: %s\n", device_token);
+    printf("    Accept: text/event-stream\n");
+    printf("    Cache-Control: no-cache\n");
+    printf("    Content-Type: application/json\n");
+    printf("  Body: %s (length: %d)\n", json_body, json_len);
+
+
+    /* 发送 POST 请求 */
+    HTTPC_RESULT res = HttpClientPostRequest(&client, LN_STORY_RANDOM_URL, &clientData);
+    if (res != HTTP_SUCCESS) {
+        if (res == HTTP_EAGAIN) {
+            /* 驱动状态机直到成功或失败 */
+            while (1) {
+                HTTPC_RESULT rrp = HttpClientRecvResponse(&client, &clientData);
+                if (rrp == HTTP_SUCCESS) { break; }
+                if (rrp == HTTP_EAGAIN) { continue; }
+                printf("story random post failed %d\n", rrp);
+                HttpClientUnprepare(&clientData);
+                HttpClientClse(&client);
+                return ERRCODE_FAIL;
+            }
+        } else {
+            printf("story random: send POST request failed (code: %d)\n", res);
+            HttpClientUnprepare(&clientData);
+            HttpClientClse(&client);
+            return ERRCODE_FAIL;
+        }
+    }
+
+    /* 检查 HTTP 响应码 */
+    int http_code = HttpClientGetResponseCode(&client);
+    if (http_code != 200) {
+        printf("story random: invalid HTTP response code (%d)\n", http_code);
+        // 新增:打印响应体内容
+        if (clientData.responseBuf && clientData.responseBufLen > 0) {
+            char *resp = (char *)malloc(clientData.responseBufLen + 1);
+            if (resp) {
+                memcpy(resp, clientData.responseBuf, clientData.responseBufLen);
+                resp[clientData.responseBufLen] = '\0';
+                printf("story random: server response: %s\n", resp);
+                free(resp);
+            }
+        }
+        HttpClientUnprepare(&clientData);
+        HttpClientClse(&client);
+        return ERRCODE_FAIL;
+    }
+
+    /* 循环接收 SSE 分块数据(带累积处理)*/
+    int loop_guard = 0;
+    char sse_acc[4096] = {0}; // SSE 数据累积缓冲区
+    int sse_acc_len = 0;
+    const int MAX_LOOP = 2000; // 约 40 秒超时(20ms * 2000)
+    while (loop_guard++ < MAX_LOOP) {
+        HTTPC_RESULT recv_res = HttpClientRecvResponse(&client, &clientData);
+        if (recv_res == HTTP_SUCCESS || recv_res == HTTP_EAGAIN) {
+            if (clientData.responseBuf && clientData.responseBufLen > 0) {
+                // 累积并分发 SSE 数据
+                ln_sse_accumulate_and_dispatch(LN_HTTP_RECV_STORY_RANDOM,
+                                              clientData.responseBuf,
+                                              clientData.responseBufLen,
+                                              sse_acc, &sse_acc_len, sizeof(sse_acc));
+                HttpClientReset(&clientData); // 重置缓冲区继续接收
+            } else {
+                LOS_Msleep(20); // 无数据时短暂休眠
+            }
+            continue;
+        } else {
+            printf("story random: SSE receive ended (code: %d)\n", recv_res);
+            break;
+        }
+    }
+
+    /* 释放资源 */
+    HttpClientUnprepare(&clientData);
+    HttpClientClse(&client);
+    return ERRCODE_SUCC;
+}
+
+/* 故事-获取退出哄睡询问(JSON)*/
+static uint32_t ln_story_exit_inquiry_http(const char *device_id,
+                                           const char *device_token)
+{
+    /* CAT1 保活 */
+    cat1_api_add_cat1_vote(); cat1_api_wakeup_cat1(1); cat1_api_keep_wakeup(1); cat1_api_disable_cat1_cmd(0);
+
+    HttpClient client = (HttpClient){0}; HttpClientData clientData = (HttpClientData){0};
+    client.isHttp = false; client.remotePort = 443;
+
+    /* 普通JSON头 */
+    char header_buf[512];
+    int hdr = snprintf(header_buf, sizeof(header_buf),
+        "deviceId: %s\r\ndeviceToken: %s\r\nContent-Type: application/json\r\n",
+        device_id, device_token);
+    printf("ln_story_finish_http header_buf: %s\n", header_buf);
+    if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) return ERRCODE_FAIL;
+    HttpClientSetCustomHeader(&client, header_buf);
+
+    HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024);
+    if (prep != HTTP_SUCCESS) return ERRCODE_FAIL;
+
+    clientData.postBuf = (char *)"{}";
+    clientData.postBufLen = 2;
+    clientData.postContentType = (char *)"application/json";
+
+    HTTPC_RESULT res = HttpClientPostRequest(&client, LN_STORY_EXIT_INQUIRY_URL, &clientData);
+    if (res != HTTP_SUCCESS) { printf("story exit inquiry post failed %d\n", res); goto end; }
+    if (HttpClientGetResponseCode(&client) != 200) { printf("story exit inquiry HTTP %d\n", HttpClientGetResponseCode(&client)); goto end; }
+
+    if (clientData.responseBuf && clientData.responseBufLen > 0)
+    {
+        // 解析响应JSON,提取audioUrl等字段
+        char *json_text = (char *)malloc(clientData.responseBufLen + 1);
+        if (json_text)
+        {
+            memcpy(json_text, clientData.responseBuf, clientData.responseBufLen);
+            json_text[clientData.responseBufLen] = '\0';
+            cJSON *root = cJSON_Parse(json_text);
+            if (root)
+            {
+                cJSON *errorCode = cJSON_GetObjectItem(root, "errorCode");
+                if (errorCode && !cJSON_IsNull(errorCode))
+                {
+                    // 处理认证失败等错误
+                    printf("[EXIT_INQUIRY] Error: %s\n", cJSON_Print(errorCode));
+                }
+                else
+                {
+                    cJSON *data = cJSON_GetObjectItem(root, "data");
+                    if (data && cJSON_IsObject(data))
+                    {
+                        cJSON *audioUrl = cJSON_GetObjectItem(data, "audioUrl");
+                        if (audioUrl && cJSON_IsString(audioUrl))
+                        {
+                            printf("[EXIT_INQUIRY] Play audio: %s\n", audioUrl->valuestring);
+                            printf("[EXIT_INQUIRY] 触发音频播放\n");
+                            // 触发音频播放(调用播放接口)
+                            // ...
+                        }
+                    }
+                }
+                cJSON_Delete(root);
+            }
+            free(json_text);
+        }
+        do_ln_http_server_recv_data(LN_HTTP_RECV_STORY_EXIT,
+                                   clientData.responseBuf,
+                                   (uint32_t)clientData.responseBufLen);
+    }
+
+end:
+    HttpClientUnprepare(&clientData); HttpClientClse(&client); return ERRCODE_SUCC;
+}
+
+/* 故事-确认结束哄睡模式(JSON)*/
+static uint32_t ln_story_finish_http(const char *device_id,
+                                     const char *device_token)
+{
+    /* CAT1 保活 */
+    cat1_api_add_cat1_vote(); cat1_api_wakeup_cat1(1); cat1_api_keep_wakeup(1); cat1_api_disable_cat1_cmd(0);
+
+    HttpClient client = (HttpClient){0}; HttpClientData clientData = (HttpClientData){0};
+    client.isHttp = false; client.remotePort = 443;
+
+    char header_buf[512]; int hdr = snprintf(header_buf, sizeof(header_buf),
+        "deviceId: %s\r\ndeviceToken: %s\r\nContent-Type: application/json\r\n",
+        device_id, device_token);
+    if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) return ERRCODE_FAIL;
+    HttpClientSetCustomHeader(&client, header_buf);
+
+    HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024);
+    if (prep != HTTP_SUCCESS) return ERRCODE_FAIL;
+
+    clientData.postBuf = (char *)"{}"; clientData.postBufLen = 2; clientData.postContentType = (char *)"application/json";
+
+    HTTPC_RESULT res = HttpClientPostRequest(&client, LN_STORY_FINISH_URL, &clientData);
+    if (res != HTTP_SUCCESS) { printf("story finish post failed %d\n", res); goto end; }
+    if (HttpClientGetResponseCode(&client) != 200)
+    {
+        printf("story finish HTTP %d\n", HttpClientGetResponseCode(&client));
+        // 解析非200响应中的错误信息
+        if (clientData.responseBuf && clientData.responseBufLen > 0)
+        {
+            char *json_text = (char *)malloc(clientData.responseBufLen + 1);
+            if (json_text)
+            {
+                memcpy(json_text, clientData.responseBuf, clientData.responseBufLen);
+                json_text[clientData.responseBufLen] = '\0';
+                cJSON *root = cJSON_Parse(json_text);
+                if (root)
+                {
+                    cJSON *errorCode = cJSON_GetObjectItem(root, "errorCode");
+                    if (errorCode && cJSON_IsString(errorCode))
+                    {
+                        printf("[STORY_FINISH] Error: %s\n", errorCode->valuestring);
+                    }
+                    cJSON_Delete(root);
+                }
+                free(json_text);
+            }
+        }
+        goto end;
+    }
+
+    if (clientData.responseBuf && clientData.responseBufLen > 0) {
+        do_ln_http_server_recv_data(LN_HTTP_RECV_STORY_FINISH,
+                                   clientData.responseBuf,
+                                   (uint32_t)clientData.responseBufLen);
+    }
+
+end:
+    HttpClientUnprepare(&clientData); HttpClientClse(&client); return ERRCODE_SUCC;
+}
+
+/* ============================ 故事相关接口 End ============================ */
+
 
 
 
@@ -498,6 +1036,28 @@ static void ln_http_server_send_task(void const *arg)
                 ln_speech_chat_stream_http(req.device_id, req.device_token, req.audio_path);
             }
         }
+        else if (mail.cmd == LN_HTTP_SERVICE_CMD_STORY_CHOOSE)
+        {
+            if (mail.data && mail.len == sizeof(ln_story_choose_req_t)) {
+                ln_story_choose_req_t req; memcpy(&req, mail.data, sizeof(req));
+                ln_story_choose_http(uapi_ln_get_device_id(), uapi_ln_get_device_token(), req.audio_path);
+            }
+        }
+        else if (mail.cmd == LN_HTTP_SERVICE_CMD_STORY_RANDOM)
+        {
+            if (mail.data && mail.len == sizeof(ln_story_random_req_t)) {
+                ln_story_random_req_t req; memcpy(&req, mail.data, sizeof(req));
+                ln_story_random_http(uapi_ln_get_device_id(), uapi_ln_get_device_token(), req.story_theme[0] ? req.story_theme : NULL);
+            }
+        }
+        else if (mail.cmd == LN_HTTP_SERVICE_CMD_STORY_EXIT_INQUIRY)
+        {
+            ln_story_exit_inquiry_http(uapi_ln_get_device_id(), uapi_ln_get_device_token());
+        }
+        else if (mail.cmd == LN_HTTP_SERVICE_CMD_STORY_FINISH)
+        {
+            ln_story_finish_http(uapi_ln_get_device_id(), uapi_ln_get_device_token());
+        }
         if (mail.data != NULL)
         {
             printf("free(mail.data)\n");
@@ -522,18 +1082,27 @@ static void ln_http_server_recv_task(void const *arg)
         {
             continue;
         }
-        if (mail.cmd == LN_HTTP_SERVICE_CMD_SSE_RECV)
+        if (mail.cmd == LN_HTTP_SERVICE_CMD_SSE_RECV
+            || mail.cmd == LN_HTTP_RECV_STORY_CHOOSE
+            || mail.cmd == LN_HTTP_RECV_STORY_RANDOM
+            || mail.cmd == LN_HTTP_RECV_STORY_EXIT
+            || mail.cmd == LN_HTTP_RECV_STORY_FINISH)
         {
             if (mail.data && mail.len)
             {
-                /* 直接打印分片数据(可能包含多行 data:)*/
-                printf("SSE raw len=%u\n", mail.len);
+                /* 分类打印,便于区分不同来源 */
+                const char *tag = (mail.cmd == LN_HTTP_SERVICE_CMD_SSE_RECV) ? "SPEECH_CHAT" :
+                                  (mail.cmd == LN_HTTP_RECV_STORY_CHOOSE) ? "STORY_CHOOSE" :
+                                  (mail.cmd == LN_HTTP_RECV_STORY_RANDOM) ? "STORY_RANDOM" :
+                                  (mail.cmd == LN_HTTP_RECV_STORY_EXIT)   ? "STORY_EXIT"   :
+                                  (mail.cmd == LN_HTTP_RECV_STORY_FINISH) ? "STORY_FINISH" : "UNKNOWN";
+                printf("[%s] SSE raw len=%u\n", tag, mail.len);
                 /* 确保可打印:追加\0 */
                 char *buf = (char *)malloc(mail.len + 1);
                 if (buf) {
                     memcpy(buf, mail.data, mail.len);
                     buf[mail.len] = '\0';
-                    printf("SSE raw data=%s\n", buf);
+                    printf("[%s] SSE raw data=%s\n", tag, buf);
                     free(buf);
                 }
             }
index 1cb30d5..53b7c5c 100644 (file)
@@ -19,6 +19,16 @@ typedef struct
 #define LN_HTTP_SERVICE_CMD_GET_DEVICE_TOKEN 0x1 /* 获取设备 token */
 #define LN_HTTP_SERVICE_CMD_SPEECH_CHAT_STREAM 0x2 /* 语音对话流 */
 #define LN_HTTP_SERVICE_CMD_SSE_RECV 0x3 /* 语音对话流分片接收 */
+/* 故事相关命令 */
+#define LN_HTTP_SERVICE_CMD_STORY_CHOOSE 0x10 /* 智能选择故事(SSE,multipart)*/
+#define LN_HTTP_SERVICE_CMD_STORY_RANDOM 0x11 /* 随机故事(SSE,JSON)*/
+#define LN_HTTP_SERVICE_CMD_STORY_EXIT_INQUIRY 0x12 /* 获取退出哄睡询问(JSON)*/
+#define LN_HTTP_SERVICE_CMD_STORY_FINISH 0x13 /* 确认结束哄睡模式(JSON)*/
+/* 故事相关接收命令(用于区分来源,便于分类打印/处理)*/
+#define LN_HTTP_RECV_STORY_CHOOSE 0x20 /* 故事-选择故事 接收 */
+#define LN_HTTP_RECV_STORY_RANDOM 0x21 /* 故事-随机故事 接收 */
+#define LN_HTTP_RECV_STORY_EXIT   0x22 /* 故事-退出哄睡询问 接收 */
+#define LN_HTTP_RECV_STORY_FINISH 0x23 /* 故事-确认结束哄睡模式 接收 */
 
 typedef struct
 {
@@ -35,6 +45,18 @@ typedef struct
     char audio_path[256];
 } ln_speech_chat_req_t;
 
+/* 故事-选择故事 请求体 */
+typedef struct
+{
+    char audio_path[256];
+} ln_story_choose_req_t;
+
+/* 故事-随机故事 请求体(可选主题)*/
+typedef struct
+{
+    char story_theme[64]; /* 为空则随机 */
+} ln_story_random_req_t;
+
 /* 初始化 LN HTTP 服务 */
 errcode_t uapi_ln_http_server_init(void); 
 /* 通用发送接口:向 LN HTTP 发送队列投递消息 */
index e0c0e8c..615281a 100644 (file)
@@ -98,6 +98,32 @@ def create_sse_headers(token: str) -> Dict[str, str]:
     }
 
 
+def print_request_details(url: str, method: str, headers: Dict, data: Optional[Dict] = None, files: Optional[Dict] = None):
+    """打印请求详细参数"""
+    logger.info("\n" + "=" * 50)
+    logger.info(f"📝 请求详情: {method} {url}")
+    logger.info("-" * 50)
+    
+    logger.info("请求头:")
+    for k, v in headers.items():
+        # 敏感信息部分隐藏
+        if k.lower() in ["devicetoken", "signature"]:
+            logger.info(f"  {k}: {v[:6]}***{v[-6:]}")  # 只显示部分字符
+        else:
+            logger.info(f"  {k}: {v}")
+    
+    if data:
+        logger.info("\n请求体:")
+        logger.info(json.dumps(data, ensure_ascii=False, indent=2))
+    
+    if files:
+        logger.info("\n文件信息:")
+        for k, (filename, _, content_type) in files.items():
+            logger.info(f"  {k}: {filename} ({content_type})")
+    
+    logger.info("=" * 50 + "\n")
+
+
 def process_sse_stream(response: requests.Response) -> None:
     """处理SSE流式响应的通用函数"""
     if response.status_code != 200:
@@ -123,7 +149,7 @@ def process_sse_stream(response: requests.Response) -> None:
                     logger.info(f"主题: {story_data.get('storyTheme', '无')}")
                     logger.info(f"内容: {story_data.get('storyContent', '无')}")
                     logger.info(f"音频URL: {story_data.get('storyAudioUrl', '无')}")
-                    logger.info(f"时长(秒):{story_data.get('storyDuration', '未知')}")  # 新增行
+                    logger.info(f"时长(秒):{story_data.get('storyDuration', '未知')}")
                     logger.info("-" * 50)
                 except json.JSONDecodeError:
                     logger.error(f"❌ 解析JSON失败: {json_data}")
@@ -157,6 +183,9 @@ def get_device_token() -> Optional[str]:
                 "Content-Type": "application/json"
             }
             
+            # 打印请求参数
+            print_request_details(Config.TOKEN_URL, "GET", headers)
+            
             response = requests.get(
                 Config.TOKEN_URL,
                 headers=headers,
@@ -206,6 +235,9 @@ def test_choose_story(token: str):
                 'file': (Config.AUDIO_FILE_PATH.name, audio_file, 'audio/mpeg')
             }
             
+            # 打印请求参数
+            print_request_details(Config.CHOOSE_STORY_URL, "POST", headers, files=files)
+            
             logger.info("📤 发送智能选择故事请求(SSE)...")
             with requests.post(
                 Config.CHOOSE_STORY_URL,
@@ -237,6 +269,9 @@ def test_random_story(token: str, story_theme: Optional[str] = None):
         headers = create_sse_headers(token)
         payload = {"storyTheme": story_theme} if story_theme else {}
         
+        # 打印请求参数
+        print_request_details(Config.RANDOM_STORY_URL, "POST", headers, data=payload)
+        
         logger.info("📤 发送随机故事请求(SSE)...")
         with requests.post(
             Config.RANDOM_STORY_URL,
@@ -259,6 +294,10 @@ def test_exit_sleep_inquiry(token: str):
         return
         
     headers = create_auth_headers(token)
+    
+    # 打印请求参数
+    print_request_details(Config.EXIT_INQUIRY_URL, "POST", headers)
+    
     logger.info("📤 发送获取退出哄睡询问请求...")
     
     response = requests.post(
@@ -294,6 +333,10 @@ def test_finish_sleep_mode(token: str):
         return
         
     headers = create_auth_headers(token)
+    
+    # 打印请求参数
+    print_request_details(Config.FINISH_SLEEP_URL, "POST", headers)
+    
     logger.info("📤 发送确认结束哄睡模式请求...")
     
     response = requests.post(