From 6fa08fbd510b5ddcf467cb01c4d1fbee220d8aac Mon Sep 17 00:00:00 2001 From: luozheng <1732036298@qq.com> Date: Wed, 3 Sep 2025 13:57:31 +0800 Subject: [PATCH] =?utf8?q?=E6=B7=BB=E5=8A=A0=E6=95=85=E4=BA=8B=E7=9B=B8?= =?utf8?q?=E5=85=B3=E6=8E=A5=E5=8F=A3at=E6=B5=8B=E8=AF=95=EF=BC=8C?= =?utf8?q?=E8=BF=98=E6=9C=89=E9=97=AE=E9=A2=98=E5=8F=91=E9=80=81=E9=80=89?= =?utf8?q?=E6=8B=A9=E6=95=85=E4=BA=8B(SSE)=E6=88=96=E8=80=85"=E9=9A=8F?= =?utf8?q?=E6=9C=BA=E6=95=85=E4=BA=8B(SSE)=E7=9A=84at=E6=8C=87=E4=BB=A4?= =?utf8?q?=E4=B8=80=E7=9B=B4=E6=B2=A1=E6=9C=89=E8=BF=94=E5=9B=9E=E7=BB=93?= =?utf8?q?=E6=9E=9C=E5=90=8E=E7=BB=AD=E5=86=8D=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- .../zhihui/leiniao/imsl_ln/leiniao_api_at.c | 46 ++ code/sdk/zhihui/leiniao/imsl_ln/ln_api.c | 589 +++++++++++++++++- code/sdk/zhihui/leiniao/imsl_ln/ln_api.h | 22 + .../leiniao_python_test/story_apis_test_3.py | 45 +- 4 files changed, 691 insertions(+), 11 deletions(-) diff --git a/code/sdk/zhihui/leiniao/imsl_ln/leiniao_api_at.c b/code/sdk/zhihui/leiniao/imsl_ln/leiniao_api_at.c index 5837a1cf9..09bfcec9e 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/leiniao_api_at.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/leiniao_api_at.c @@ -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 : ""); + 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) diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c index db3693ca3..48e1d7ad9 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.c @@ -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(""); } /* -------------------------------------------------------------------------- */ - 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); } } diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.h b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.h index 1cb30d579..53b7c5c68 100644 --- a/code/sdk/zhihui/leiniao/imsl_ln/ln_api.h +++ b/code/sdk/zhihui/leiniao/imsl_ln/ln_api.h @@ -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 发送队列投递消息 */ diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/story_apis_test_3.py b/code/sdk/zhihui/leiniao/leiniao_python_test/story_apis_test_3.py index e0c0e8cc1..615281a8f 100644 --- a/code/sdk/zhihui/leiniao/leiniao_python_test/story_apis_test_3.py +++ b/code/sdk/zhihui/leiniao/leiniao_python_test/story_apis_test_3.py @@ -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( -- 2.17.1