拆分ln_api.c文件中api函数到多个文件中
authorluozheng <1732036298@qq.com>
Mon, 8 Sep 2025 05:55:15 +0000 (13:55 +0800)
committerluozheng <1732036298@qq.com>
Mon, 8 Sep 2025 05:55:15 +0000 (13:55 +0800)
13 files changed:
code/sdk/zhihui/leiniao/imsl_ln/CMakeLists.txt
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 [new file with mode: 0644]
code/sdk/zhihui/leiniao/imsl_ln/ln_api_ota.c [new file with mode: 0644]
code/sdk/zhihui/leiniao/imsl_ln/ln_api_speech_chat.c [new file with mode: 0644]
code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_choose.c [new file with mode: 0644]
code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_exit.c [new file with mode: 0644]
code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_finish.c [new file with mode: 0644]
code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_random.c [new file with mode: 0644]
code/sdk/zhihui/leiniao/imsl_ln/ln_api_token.c [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/story_apis_test_3.py
code/sdk/zhihui/leiniao/设备API接口文档.md [new file with mode: 0644]

index c165ad4..cb1e08d 100644 (file)
@@ -9,6 +9,14 @@ set(SOURCES
     ${CMAKE_CURRENT_SOURCE_DIR}/imsl_system_adapter.c
     ${CMAKE_CURRENT_SOURCE_DIR}/leiniao_api_at.c
     ${CMAKE_CURRENT_SOURCE_DIR}/ln_api.c
+    ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_token.c
+    ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_speech_chat.c
+    ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_story_choose.c
+    ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_story_random.c
+    ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_story_exit.c
+    ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_story_finish.c
+    ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_device_info.c
+    ${CMAKE_CURRENT_SOURCE_DIR}/ln_api_ota.c
 )
 
 # 公共头文件路径(供其他模块引用)
index 5f5223b..8f94b4d 100644 (file)
@@ -20,18 +20,18 @@ static unsigned long g_ln_send_queue = 0xFFFFFFFF;
 static unsigned long g_ln_recv_queue = 0xFFFFFFFF;
 
 static char *g_device_id = NULL;
-static char *g_device_token = NULL;//设备token,服务器获取
+char *g_device_token = NULL;//设备token,服务器获取
 static char *g_device_iccid = NULL;
 static char *g_device_model = NULL;
 static char *g_device_version = NULL;
 
 /* 记录最近一次 OTA 查询的升级类型与保存路径 */
-static int g_ln_last_update_type = -1;          // -1:未知 0:可选 1:强制
-static char g_ln_last_saved_path[256] = {0};    // 最近一次保存的 OTA 包路径
-static char g_ln_last_package_url[512] = {0};   // 最近一次查询到的 OTA 包 URL
-static char g_ln_last_target_version[64] = {0}; // 最近一次查询到的 OTA 包目标版本
-static char g_ln_last_package_md5[65] = {0};    // 最近一次查询到的 OTA 包 MD5
-static int g_ln_last_package_size = 0;          // 最近一次查询到的 OTA 包大小(字节)
+int g_ln_last_update_type = -1;          // -1:未知 0:可选 1:强制
+char g_ln_last_saved_path[256] = {0};    // 最近一次保存的 OTA 包路径
+char g_ln_last_package_url[512] = {0};   // 最近一次查询到的 OTA 包 URL
+char g_ln_last_target_version[64] = {0}; // 最近一次查询到的 OTA 包目标版本
+char g_ln_last_package_md5[65] = {0};    // 最近一次查询到的 OTA 包 MD5
+int g_ln_last_package_size = 0;          // 最近一次查询到的 OTA 包大小(字节)
 
 /* 获取设备id: 设备唯一标识 */
 char *uapi_ln_get_device_id(void)
@@ -88,291 +88,8 @@ const char *uapi_ln_ota_get_saved_path(void)
     return (g_ln_last_saved_path[0] != '\0') ? g_ln_last_saved_path : NULL;
 }
 
-/* ============================== 雷鸟设备Token获取 ============================== */
-
-/* 接口基础配置 - 如需切换环境/密钥,在此调整 */
-#define LN_BASE_URL "https://ambybox-api.test.leiniao.com/ambybox-device-api"
-#define LN_INTERFACE_PATH "/v1/auth/getDeviceToken"
-#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
-/* 提取基础域名用于 Host 头(SNI/Host 一致性)*/
-#define LN_BASE_HOST "ambybox-api.test.leiniao.com"
-/* 故事接口路径 */
-#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_DEVICE_GET_CHARACTER_PATH "/v1/device/getCharacter"
-#define LN_DEVICE_GET_USER_ID_PATH "/v1/device/getUserId"
-#define LN_OTA_GET_TASK_PATH "/v1/upgrade/getTask"
-#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_DEVICE_GET_CHARACTER_URL LN_BASE_URL LN_DEVICE_GET_CHARACTER_PATH
-#define LN_DEVICE_GET_USER_ID_URL LN_BASE_URL LN_DEVICE_GET_USER_ID_PATH
-#define LN_OTA_GET_TASK_URL LN_BASE_URL LN_OTA_GET_TASK_PATH
-#define LN_SIGN_SECRET "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus="
-/* OTA 包保存目录(最终文件仍将放在 /user/ 下)*/
-#define LN_OTA_SAVE_DIR "/user"
-
-
-/* 生成[a-zA-Z0-9]随机串 */
-static void ln_gen_nonce(char *out, size_t len)
-{
-    static const char table[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
-    if (out == NULL || len == 0)
-    {
-        return;
-    }
-    /* 使用时间作为随机种子,避免强依赖标准rand种子 */
-    uint32_t ts_s = 0;
-    uapi_get_timestampe_ss(&ts_s);
-    srand((unsigned int)(ts_s ^ (unsigned int)uapi_tcxo_get_ms()));
-    for (size_t i = 0; i + 1 < len; i++)
-    {
-        out[i] = table[rand() % (sizeof(table) - 1)];
-    }
-    out[len - 1] = '\0';
-}
-
-static void ln_to_hex_lower(const uint8_t *in, size_t in_len, char *out)
-{
-    static const char hex[] = "0123456789abcdef";
-    for (size_t i = 0; i < in_len; i++)
-    {
-        out[i * 2] = hex[(in[i] >> 4) & 0x0F];
-        out[i * 2 + 1] = hex[in[i] & 0x0F];
-    }
-    out[in_len * 2] = '\0';
-}
-
-/* 计算 HMAC-SHA256 并输出小写hex字符串 */
-static int ln_hmac_sha256_hex(const char *key, const char *msg, char *out_hex, size_t out_hex_size)
-{
-    if (out_hex_size < 65)
-    { /* 32 bytes -> 64 hex chars + NUL */
-        return -1;
-    }
-    const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
-    if (md_info == NULL)
-    {
-        return -1;
-    }
-    uint8_t digest[32];
-    int ret = mbedtls_md_hmac(md_info, (const unsigned char *)key, strlen(key),
-                              (const unsigned char *)msg, strlen(msg), digest);
-    if (ret != 0)
-    {
-        return -1;
-    }
-    ln_to_hex_lower(digest, sizeof(digest), out_hex);
-    return 0;
-}
-
-/* 发送HTTPS GET请求,携带定制Header,解析返回token */
-static uint32_t ln_get_device_token_http(const char *device_id,
-                                         const char *device_iccid,
-                                         const char *device_model,
-                                         const char *device_version)
-{
-    if (device_id == NULL || device_iccid == NULL)
-    {
-        printf("LN token: invalid parameters\n");
-        return ERRCODE_FAIL;
-    }
-
-    /* 1. 生成时间戳(ms)与nonce */
-    uint32_t ts_s = 0; /* epoch seconds */
-    uapi_get_timestampe_ss(&ts_s);
-    char timestamp[32] = {0};
-    (void)snprintf(timestamp, sizeof(timestamp), "%u000", ts_s); /* 转毫秒字符串 */
-
-    char nonce[17] = {0}; /* 12~16位,取固定16以简化 */
-    ln_gen_nonce(nonce, sizeof(nonce));
-
-    /* 2. 计算签名: deviceId:deviceIccid:timestamp:nonce */
-    char sign_data[256] = {0};
-    (void)snprintf(sign_data, sizeof(sign_data), "%s:%s:%s:%s", device_id, device_iccid, timestamp, nonce);
-    char signature[65] = {0};
-    if (ln_hmac_sha256_hex(LN_SIGN_SECRET, sign_data, signature, sizeof(signature)) != 0)
-    {
-        printf("LN token: hmac calc failed\n");
-        return ERRCODE_FAIL;
-    }
-
-    /* 3. 组织自定义请求头 */
-    char header_buf[1024] = {0};
-    int hdr_len = snprintf(header_buf, sizeof(header_buf),
-                           "deviceId: %s\r\n"
-                           "deviceIccid: %s\r\n"
-                           "timestamp: %s\r\n"
-                           "nonce: %s\r\n"
-                           "signature: %s\r\n"
-                           "deviceModel: %s\r\n"
-                           "deviceVersion: %s\r\n"
-                           "Content-Type: application/json\r\n",
-                           device_id, device_iccid, timestamp, nonce, signature,
-                           (device_model && device_model[0]) ? device_model : "abmybox_1",
-                           (device_version && device_version[0]) ? device_version : "1.0.0");
-    if (hdr_len <= 0 || hdr_len >= (int)sizeof(header_buf))
-    {
-        printf("LN token: header build failed\n");
-        return ERRCODE_FAIL;
-    }
-
-    /* 4. 保活CAT1,准备网络 */
-    cat1_api_add_cat1_vote();
-    cat1_api_wakeup_cat1(1);
-    cat1_api_keep_wakeup(1);
-    cat1_api_disable_cat1_cmd(1);
-    LOS_Msleep(500);
-
-    /* 5. 发起HTTPS GET */
-    HttpClient client = (HttpClient){0};
-    HttpClientData clientData = (HttpClientData){0};
-    client.isHttp = false; /* https */
-    client.remotePort = 443;
-    HttpClientSetCustomHeader(&client, header_buf);
-
-    HTTPC_RESULT prep = HttpClientPrepare(&clientData, 2048, 4096);
-    if (prep != HTTP_SUCCESS)
-    {
-        printf("LN token: HttpClientPrepare failed %d\n", prep);
-        return ERRCODE_FAIL;
-    }
-
-    HTTPC_RESULT res = HttpClientGetRequest(&client, LN_FULL_URL, &clientData);
-    if (res != HTTP_SUCCESS)
-    {
-        printf("LN token: request failed %d\n", res);
-        HttpClientUnprepare(&clientData);
-        HttpClientClse(&client);
-        return ERRCODE_FAIL;
-    }
-
-    int code = HttpClientGetResponseCode(&client);
-    if (code != 200)
-    {
-        printf("LN token: HTTP %d\n", code);
-        HttpClientUnprepare(&clientData);
-        HttpClientClse(&client);
-        return ERRCODE_FAIL;
-    }
-
-    /* 6. 解析响应JSON */
-    if (clientData.responseBuf == NULL || clientData.responseBufLen <= 0)
-    {
-        printf("LN token: empty body\n");
-        HttpClientUnprepare(&clientData);
-        HttpClientClse(&client);
-        return ERRCODE_FAIL;
-    }
-
-    int body_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
-    if (body_len <= 0)
-        body_len = (int)strlen(clientData.responseBuf);
-
-    char *json_text = (char *)malloc((size_t)body_len + 1);
-    if (json_text == NULL)
-    {
-        HttpClientUnprepare(&clientData);
-        HttpClientClse(&client);
-        return ERRCODE_FAIL;
-    }
-    memcpy(json_text, clientData.responseBuf, (size_t)body_len);
-    json_text[body_len] = '\0';
-
-    cJSON *root = cJSON_Parse(json_text);
-    if (!root)
-    {
-        printf("LN token: JSON parse failed\n");
-        free(json_text);
-        HttpClientUnprepare(&clientData);
-        HttpClientClse(&client);
-        return ERRCODE_FAIL;
-    }
-
-    cJSON *errorCode = cJSON_GetObjectItem(root, "errorCode");
-    if (errorCode != NULL && !cJSON_IsNull(errorCode))
-    {
-        cJSON *msg = cJSON_GetObjectItem(root, "message");
-        printf("LN token: failed, err=%s\n", msg && cJSON_IsString(msg) ? msg->valuestring : "unknown");
-        cJSON_Delete(root);
-        free(json_text);
-        HttpClientUnprepare(&clientData);
-        HttpClientClse(&client);
-        return ERRCODE_FAIL;
-    }
-
-    cJSON *data = cJSON_GetObjectItem(root, "data");
-    if (data && cJSON_IsObject(data))
-    {
-        cJSON *token = cJSON_GetObjectItem(data, "token");
-        cJSON *expire = cJSON_GetObjectItem(data, "expireTime");
-        /* --------------------------------- 保存token -------------------------------- */
-        if (g_device_token) 
-        { 
-            free(g_device_token); 
-            g_device_token = NULL; 
-        }
-        if (token && cJSON_IsString(token)) {
-            size_t tlen = strlen(token->valuestring);
-            g_device_token = (char *)malloc(tlen + 1);
-            if (g_device_token) {
-                memcpy(g_device_token, token->valuestring, tlen + 1);
-            }
-        }
-        if (g_device_token == NULL) {
-            g_device_token = strdup("<null>");
-        }
-        /* -------------------------------------------------------------------------- */
-        // 打印完整的json字符串
-        printf("\njson=%s\n",json_text);
-        if (expire && cJSON_IsNumber(expire))
-        {
-            printf("Token expireTime: %lld\n", (long long)expire->valuedouble);
-        }
-    }
-    else
-    {
-        printf("LN token: no data field\n");
-    }
-
-    cJSON_Delete(root);
-    free(json_text);
-    HttpClientUnprepare(&clientData);
-    HttpClientClse(&client);
-    return ERRCODE_SUCC;
-}
-
-/* ============================ 雷鸟设备Token获取 End ============================ */
-
-/* ============================ 雷鸟语音对话流 Start ============================ */
-
-/* 读取整个文件到内存,返回缓冲区和长度;需调用者 free */
-static uint8_t *ln_read_file_all(const char *path, int *out_len)
-{
-    if (out_len) *out_len = 0;
-    if (path == NULL) return NULL;
-    FILE *fp = fopen(path, "rb");
-    if (!fp) return NULL;
-    fseek(fp, 0, SEEK_END);
-    long sz = ftell(fp);
-    fseek(fp, 0, SEEK_SET);
-    if (sz <= 0 || sz > (10 * 1024 * 1024)) { fclose(fp); return NULL; }
-    uint8_t *buf = (uint8_t *)malloc((size_t)sz);
-    if (!buf) { fclose(fp); return NULL; }
-    size_t n = fread(buf, 1, (size_t)sz, fp);
-    fclose(fp);
-    if ((long)n != sz) { free(buf); return NULL; }
-    if (out_len) *out_len = (int)sz;
-    return buf;
-}
-
 /* 自适应申请HttpClient缓冲(保证header>=2048),失败则逐步扩容重试 */
-static HTTPC_RESULT ln_http_prepare_auto(HttpClientData *clientData, int header_size, int resp_size)
+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;
@@ -389,189 +106,8 @@ static HTTPC_RESULT ln_http_prepare_auto(HttpClientData *clientData, int header_
     return ret;
 }
 
-/* 简单 multipart/form-data 组包(单文件字段 file) */
-static int ln_build_multipart_form(const char *boundary,
-                                   const char *filename,
-                                   const uint8_t *file_data,
-                                   int file_len,
-                                   char **out_body,
-                                   int *out_len,
-                                   char *out_content_type,
-                                   int ctype_len)
-{
-    if (!boundary || !filename || !file_data || file_len <= 0 || !out_body || !out_len) return -1;
-    char head[512];
-    int head_len = snprintf(head, sizeof(head),
-        "--%s\r\n"
-        "Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n"
-        "Content-Type: audio/mpeg\r\n\r\n",
-        boundary, filename);
-    char tail[128];
-    int tail_len = snprintf(tail, sizeof(tail), "\r\n--%s--\r\n", boundary);
-    int total = head_len + file_len + tail_len;
-    char *body = (char *)malloc((size_t)total);
-    if (!body) return -1;
-    memcpy(body, head, head_len);
-    memcpy(body + head_len, file_data, (size_t)file_len);
-    memcpy(body + head_len + file_len, tail, tail_len);
-    *out_body = body;
-    *out_len = total;
-    if (out_content_type && ctype_len > 0) {
-        snprintf(out_content_type, ctype_len, "multipart/form-data; boundary=%s", boundary);
-    }
-    return 0;
-}
-
-/* 发送语音流对话(非分块:一次POST完整multipart;Accept: text/event-stream) */
-static uint32_t ln_speech_chat_stream_http(const char *device_id,
-                                           const char *device_token,
-                                           const char *audio_path)
-{
-    if (!device_id || !device_token || !audio_path) return ERRCODE_FAIL;
-
-    /* 读取音频 */
-    int audio_len = 0;
-    uint8_t *audio_buf = ln_read_file_all(audio_path, &audio_len);
-    if (!audio_buf) { printf("chatstream: read audio failed\n"); return ERRCODE_FAIL; }
-    printf("chatstream: audio length = %d\n", audio_len);
-
-    /* boundary */
-    char boundary[64];
-    snprintf(boundary, sizeof(boundary), "----LNBOUNDARY-%u", (unsigned)uapi_tcxo_get_ms());
-
-    /* 组包 */
-    char *body = NULL; int body_len = 0; char content_type[96];
-    const char *fname = audio_path; const char *slash = strrchr(audio_path, '/');
-    if (slash && *(slash + 1)) { fname = slash + 1; }
-    if (ln_build_multipart_form(boundary, fname && *fname ? fname : "audio.mp3", audio_buf, audio_len,
-                                &body, &body_len, content_type, sizeof(content_type)) != 0) {
-        free(audio_buf); return ERRCODE_FAIL;
-    }
-    free(audio_buf);
-
-    /* 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 = (HttpClient){0};
-    HttpClientData clientData = (HttpClientData){0};
-    client.isHttp = false; /* https */
-    client.remotePort = 443;
-
-    /* 自定义头:仅保留自定义业务头,Content-Type 交由 postContentType 指定 */
-    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)) { free(body); return ERRCODE_FAIL; }
-    HttpClientSetCustomHeader(&client, header_buf);
-    
-    /* 关键调试:打印请求头,便于对比Python行为 */
-    printf("chatstream header_buf: %s\n", header_buf);
-
-    /* 准备缓冲区(自适应) */
-    HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 64 * 1024);
-    if (prep != HTTP_SUCCESS) { free(body); return ERRCODE_FAIL; }
-
-    /* 设置POST数据与Content-Type(放在Prepare之后,避免被重置) */
-    clientData.postBuf = body;
-    clientData.postBufLen = body_len;
-    clientData.postContentType = content_type;
-    /* SSE 场景:响应通常为 Transfer-Encoding: chunked,打开分块标志以避免长度校验问题 */
-    clientData.isChunked = true;
-
-    /* 发送请求:对易抖动错误进行有限次重试 */
-    HTTPC_RESULT res = HTTP_EUNKOWN;
-    for (int attempt = 1; attempt <= 3; attempt++) {
-        res = HttpClientPostRequest(&client, LN_CHAT_URL, &clientData);
-        printf("chatstream: post attempt %d result %d\n", attempt, res);
-        if (res == HTTP_SUCCESS) {
-            break;
-        }
-        if (res == HTTP_ECONN || res == HTTP_ECLSD || res == HTTP_ERECV || res == HTTP_ETIMEOUT) {
-            /* 连接/收发超时类错误,短暂休眠后重建会话重试 */
-            HttpClientUnprepare(&clientData);
-            HttpClientClse(&client);
-            LOS_Msleep(200);
-            client = (HttpClient){0};
-            clientData = (HttpClientData){0};
-            client.isHttp = false;
-            client.remotePort = 443;
-            HttpClientSetCustomHeader(&client, header_buf);
-            HTTPC_RESULT prep2 = ln_http_prepare_auto(&clientData, 2048, 64 * 1024);
-            if (prep2 != HTTP_SUCCESS) {
-                printf("chatstream: prepare failed on retry %d, code %d\n", attempt, prep2);
-                break;
-            }
-            clientData.postBuf = body;
-            clientData.postBufLen = body_len;
-            clientData.postContentType = content_type;
-            clientData.isChunked = true;
-            continue;
-        }
-        /* 其他错误不再重试 */
-        break;
-    }
-    if (res != HTTP_SUCCESS) {
-        printf("chatstream: post failed %d\n", res);
-        HttpClientUnprepare(&clientData);
-        HttpClientClse(&client);
-        free(body);
-        return ERRCODE_FAIL;
-    }
-
-    int code = HttpClientGetResponseCode(&client);
-    if (code != 200) {
-        printf("chatstream: HTTP %d\n", code);
-        if (clientData.responseBuf && clientData.responseBufLen > 0) {
-            int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
-            printf("chatstream: resp body: %.*s\n", real_len, clientData.responseBuf);
-        }
-        HttpClientUnprepare(&clientData);
-        HttpClientClse(&client);
-        free(body);
-        return ERRCODE_FAIL;
-    }
-
-    /* 循环接收SSE分片并转投递到接收队列 */
-    while (1) {
-        HTTPC_RESULT rr = HttpClientRecvResponse(&client, &clientData);
-        if (rr == HTTP_SUCCESS || rr == HTTP_EAGAIN) {
-            if (clientData.responseBuf && clientData.responseBufLen > 0) {
-                /* 复制分片数据并投递 */
-                do_ln_http_server_recv_data(LN_HTTP_SERVICE_CMD_SSE_RECV,
-                                           clientData.responseBuf,
-                                           (uint32_t)clientData.responseBufLen);
-                /* 重置缓冲继续收 */
-                HttpClientReset(&clientData);
-            } else {
-                LOS_Msleep(20);
-            }
-            continue;
-        } else {
-            printf("SSE recv end: %d\n", rr);
-            break;
-        }
-    }
-
-    HttpClientUnprepare(&clientData);
-    HttpClientClse(&client);
-    free(body);
-    return ERRCODE_SUCC;
-}
-
-/* ============================ 雷鸟语音对话流 End ============================ */
-
-/* ============================ 故事相关接口 Start ============================ */
-
 // 累积并解析SSE数据(补充实现)
-static void ln_sse_accumulate_and_dispatch(int cmd, const char *data, int data_len,
+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;
 
@@ -609,712 +145,6 @@ static void ln_sse_accumulate_and_dispatch(int cmd, const char *data, int data_l
     }
 }
 
-/* 故事-选择故事(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;
-
-    /* 设置请求头:精简头部,避免重复(Host/Connection 交由库设置) */
-    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",
-        device_id, device_token);
-    if (hdr <= 0 || hdr >= (int)sizeof(header_buf)) {
-        printf("story choose: build header failed\n");
-        free(body);
-        return ERRCODE_FAIL;
-    }
-    HttpClientSetCustomHeader(&client, header_buf);
-    printf("story choose header_buf: %s\n", 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;
-    clientData.isChunked = true;
-
-    /* 发送请求:对易抖动错误进行有限次重试 */
-    HTTPC_RESULT res = HTTP_EUNKOWN;
-    for (int attempt = 1; attempt <= 3; attempt++) {
-        res = HttpClientPostRequest(&client, LN_STORY_CHOOSE_URL, &clientData);
-        printf("story choose: post attempt %d result %d\n", attempt, res);
-        if (res == HTTP_SUCCESS) {
-            break;
-        }
-        if (res == HTTP_ECONN || res == HTTP_ECLSD || res == HTTP_ERECV || res == HTTP_ETIMEOUT) {
-            /* 连接/收发超时类错误,短暂休眠后重建会话重试 */
-            HttpClientUnprepare(&clientData);
-            HttpClientClse(&client);
-            LOS_Msleep(200);
-            client = (HttpClient){0};
-            clientData = (HttpClientData){0};
-            client.isHttp = false;
-            client.remotePort = 443;
-            HttpClientSetCustomHeader(&client, header_buf);
-            HTTPC_RESULT prep2 = ln_http_prepare_auto(&clientData, 2048, 64 * 1024);
-            if (prep2 != HTTP_SUCCESS) {
-                printf("story choose: prepare failed on retry %d, code %d\n", attempt, prep2);
-                break;
-            }
-            clientData.postBuf = body;
-            clientData.postBufLen = body_len;
-            clientData.postContentType = content_type;
-            clientData.isChunked = true;
-            continue;
-        }
-        /* 其他错误不再重试 */
-        break;
-    }
-    printf("story choose: send POST request result: %d\n", res);
-    if (res != HTTP_SUCCESS) {
-        printf("story choose: send POST request failed (code: %d)\n", res);
-        free(body);
-        HttpClientUnprepare(&clientData);
-        HttpClientClse(&client);
-        return ERRCODE_FAIL;
-    }
-    else
-    {
-        if (client.socket == -1) 
-        {
-            printf("[ln_story_choose_http] 错误:POST请求返回成功,但socket无效\n");
-        }
-    }
-
-    /* 检查 HTTP 响应码 */
-    int http_code = HttpClientGetResponseCode(&client);
-    printf("story choose: send POST request result: %d\n", res);
-    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;
-    }
-
-    printf("story choose: send POST request success\n");
-    if (clientData.responseBuf && clientData.responseBufLen > 0) {
-        int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
-        do_ln_http_server_recv_data(LN_HTTP_RECV_STORY_CHOOSE, clientData.responseBuf, real_len);
-    }
-
-    /* 循环接收 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";
-
-    /* 发送 POST 请求 */
-    HTTPC_RESULT res = HttpClientPostRequest(&client, LN_STORY_RANDOM_URL, &clientData);
-    printf("story random: send POST request result: %d\n", res);
-    if (res != HTTP_SUCCESS) 
-    {
-        printf("story random: send POST request failed (code: %d)\n", res);
-        HttpClientUnprepare(&clientData);
-        HttpClientClse(&client);
-        return ERRCODE_FAIL;
-    }
-    else
-    {
-        if (client.socket == -1) 
-        {
-            printf("[ln_speech_chat_stream_http] 错误:POST请求返回成功,但socket无效\n");
-        }
-    }
-
-    printf("story random: send POST request success\n");
-
-    /* 检查 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;
-    }
-
-    printf("story random: HTTP response code (%d)\n", http_code);
-    if (clientData.responseBuf && clientData.responseBufLen > 0) {
-        int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
-        do_ln_http_server_recv_data(LN_HTTP_RECV_STORY_RANDOM, clientData.responseBuf, real_len);
-    }
-
-    /* 循环接收 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);
-        printf("story random: recv response result: %d\n", recv_res);
-        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)
-{
-    if (!device_id || !device_token) 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);
-
-    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);
-        }
-        // clientData.responseBufLen 表示的是缓冲区的总大小(提前分配的内存大小),而非实际有效的响应数据长度。
-        // 实际有效的响应数据长度应使用 clientData.responseContentLen(HTTP 响应体的真实长度)。
-        // 当 responseBufLen 大于 responseContentLen 时,responseBuf 中从 responseContentLen 到 responseBufLen 的部分是未使用的内存(可能残留之前的垃圾数据),
-        // 这些数据被当作有效内容传递并打印,导致乱码。
-        int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
-        do_ln_http_server_recv_data(LN_HTTP_RECV_STORY_EXIT,clientData.responseBuf,(uint32_t)real_len);
-    }
-
-end:
-    HttpClientUnprepare(&clientData); HttpClientClse(&client); return ERRCODE_SUCC;
-}
-
-/* 故事-确认结束哄睡模式(JSON)*/
-static uint32_t ln_story_finish_http(const char *device_id,
-                                     const char *device_token)
-{
-    if (!device_id || !device_token) 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);
-
-    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) 
-    {
-        int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
-        do_ln_http_server_recv_data(LN_HTTP_RECV_STORY_FINISH,clientData.responseBuf,(uint32_t)real_len);
-    }
-
-end:
-    HttpClientUnprepare(&clientData); HttpClientClse(&client); return ERRCODE_SUCC;
-}
-
-/* ============================ 故事相关接口 End ============================ */
-
-
-/* ============================ 设备信息接口 start ============================ */
-/* 设备-获取角色信息(GET, JSON)*/
-static uint32_t ln_device_get_character_http(const char *device_id,
-                                             const char *device_token)
-{
-    if (!device_id || !device_token) 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);
-
-    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\n""deviceToken: %s\r\n""Content-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;
-
-    HTTPC_RESULT res = HttpClientGetRequest(&client, LN_DEVICE_GET_CHARACTER_URL, &clientData);
-    if (res != HTTP_SUCCESS) { printf("device getCharacter get failed %d\n", res); goto end; }
-    if (HttpClientGetResponseCode(&client) != 200) 
-    {
-        printf("device getCharacter 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) {
-        int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
-        do_ln_http_server_recv_data(LN_HTTP_RECV_DEVICE_CHARACTER, clientData.responseBuf, (uint32_t)real_len);
-    }
-
-end:
-    HttpClientUnprepare(&clientData); HttpClientClse(&client); return ERRCODE_SUCC;
-}
-
-/* 设备-获取绑定用户ID(GET, JSON)*/
-static uint32_t ln_device_get_user_id_http(const char *device_id,
-                                          const char *device_token)
-{
-    if (!device_id || !device_token) 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);
-
-    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\n""deviceToken: %s\r\n""Content-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;
-
-    HTTPC_RESULT res = HttpClientGetRequest(&client, LN_DEVICE_GET_USER_ID_URL, &clientData);
-    if (res != HTTP_SUCCESS) { printf("device getUserId get failed %d\n", res); goto end; }
-    if (HttpClientGetResponseCode(&client) != 200) {
-        printf("device getUserId 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) {
-        int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
-        do_ln_http_server_recv_data(LN_HTTP_RECV_DEVICE_USER_ID, clientData.responseBuf, (uint32_t)real_len);
-    }
-
-end:
-    HttpClientUnprepare(&clientData); HttpClientClse(&client); return ERRCODE_SUCC;
-}
-/* ============================ 设备信息接口 End ============================ */
-
-
-
-/* ============================ OTA 升级任务接口 Start ============================ */
-/* 设备-获取OTA升级任务(GET, JSON,带查询参数 deviceId, sourceVersion)*/
-static uint32_t ln_ota_get_task_http(const char *device_id,
-                                     const char *device_token,
-                                     const char *source_version)
-{
-    if (!device_id || !device_token || !source_version) 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);
-
-    HttpClient client = (HttpClient){0}; HttpClientData clientData = (HttpClientData){0};
-    client.isHttp = false; client.remotePort = 443;
-
-    /* 头:与 device info 一致,额外带 model/version 可辅助服务端日志定位 */
-    char header_buf[512];
-    int hdr = snprintf(header_buf, sizeof(header_buf),
-        "deviceId: %s\r\n"
-        "deviceToken: %s\r\n"
-        "deviceModel: %s\r\n"
-        "deviceVersion: %s\r\n"
-        "Content-Type: application/json\r\n",
-        device_id, device_token,
-        (uapi_ln_get_device_model() ? uapi_ln_get_device_model() : "abmybox_1"),
-        (uapi_ln_get_device_version() ? uapi_ln_get_device_version() : "1.0.0"));
-    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;
-
-    /* 拼 GET URL 参数 */
-    char url_with_params[512];
-    int ulen = snprintf(url_with_params, sizeof(url_with_params),
-                        "%s?deviceId=%s&sourceVersion=%s",
-                        LN_OTA_GET_TASK_URL, device_id, source_version);
-    if (ulen <= 0 || ulen >= (int)sizeof(url_with_params)) { HttpClientUnprepare(&clientData); return ERRCODE_FAIL; }
-
-    printf("[OTA] GET %s\n", url_with_params);
-
-    HTTPC_RESULT res = HttpClientGetRequest(&client, url_with_params, &clientData);
-    if (res != HTTP_SUCCESS) { printf("[OTA] getTask failed %d\n", res); goto end; }
-
-    int code = HttpClientGetResponseCode(&client);
-    printf("[OTA] HTTP %d\n", code);
-    if (code != 200) {
-        if (clientData.responseBuf && clientData.responseBufLen > 0) {
-            int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
-            do_ln_http_server_recv_data(LN_HTTP_RECV_OTA_TASK, clientData.responseBuf, (uint32_t)real_len);
-        }
-        goto end;
-    }
-
-    if (clientData.responseBuf && clientData.responseBufLen > 0) {
-        /* 关键打印:返回的响应数据 */
-        printf("[OTA] getTask response: %.*s\n",
-               clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen,
-               clientData.responseBuf);
-        int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
-        do_ln_http_server_recv_data(LN_HTTP_RECV_OTA_TASK, clientData.responseBuf, (uint32_t)real_len);
-    }
-
-end:
-    HttpClientUnprepare(&clientData); HttpClientClse(&client); return ERRCODE_SUCC;
-}
-/* ============================ OTA 升级任务接口 End ============================ */
-
-
-
-/* ============================ OTA 包下载实现 Start ============================ */
-/* 将响应缓冲写入文件 */
-static int ln_write_buffer_to_file(const char *save_path, const char *buf, int content_len, int buf_len)
-{
-    if (save_path == NULL || buf == NULL)
-    {
-        return -1;
-    }
-    printf("[OTA] write file: path=%s, content_len=%d, buf_len=%d\n", save_path, content_len, buf_len);
-    int actual_size = content_len > 0 ? content_len : buf_len;
-    if (actual_size <= 0)
-    {
-        printf("[OTA] write file failed: invalid size\n");
-        return -1;
-    }
-    FILE *fp = fopen(save_path, "wb");
-    if (fp == NULL)
-    {
-        printf("[OTA] write file failed: open error\n");
-        return -1;
-    }
-    size_t written = fwrite(buf, 1, (size_t)actual_size, fp);
-    fclose(fp);
-    printf("[OTA] write file done: written=%zu\n", written);
-    return (written == (size_t)actual_size) ? 0 : -1;
-}
-
 /* 计算文件MD5(小写hex)*/
 static int ln_calc_file_md5_lower_hex(const char *file_path, char out_hex[33])
 {
@@ -1364,160 +194,6 @@ static int ln_calc_file_md5_lower_hex(const char *file_path, char out_hex[33])
     return 0;
 }
 
-/* 分片下载:使用 Range 头按固定分片大小请求,边下边写并上报进度 */
-static int ln_download_file_to_path_range(const char *full_url, const char *save_path, int total_size)
-{
-    if (!full_url || !save_path || total_size <= 0)
-    {
-        return -1;
-    }
-    printf("[OTA] range download: url=%s -> %s, total=%d\n", full_url, save_path, total_size);
-
-    /* CAT1 网络保活处理 */
-    cat1_api_add_cat1_vote(); cat1_api_wakeup_cat1(1); cat1_api_keep_wakeup(1); cat1_api_disable_cat1_cmd(0);
-
-    FILE *fp = fopen(save_path, "wb");
-    if (!fp)
-    {
-        printf("[OTA] open file failed: %s\n", save_path);
-        return -1;
-    }
-
-    const int chunk_size = 256 * 1024; /* 256KB 分片,兼顾内存与吞吐 */
-    int64_t downloaded = 0;
-
-    while (downloaded < total_size)
-    {
-        int start = (int)downloaded;
-        int remaining = total_size - start;
-        int this_chunk = remaining > chunk_size ? chunk_size : remaining;
-        int end = start + this_chunk - 1;
-
-        HttpClient client = (HttpClient){0};
-        HttpClientData clientData = (HttpClientData){0};
-
-        /* 判断协议 */
-        bool is_https = (strstr(full_url, "https://") == full_url);
-        client.isHttp = is_https ? false : true;
-        client.remotePort = is_https ? 443 : 80;
-
-        /* 自定义 Range 头 */
-        char header_buf[256];
-        int hdr = snprintf(header_buf, sizeof(header_buf), "Range: bytes=%d-%d\r\n", start, end);
-        if (hdr <= 0 || hdr >= (int)sizeof(header_buf))
-        {
-            fclose(fp);
-            return -1;
-        }
-        HttpClientSetCustomHeader(&client, header_buf);
-
-        /* 预留足够的响应缓冲区(分片大小+余量)*/
-        HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, this_chunk + 16 * 1024);
-        if (prep != HTTP_SUCCESS)
-        {
-            fclose(fp);
-            return -1;
-        }
-
-        HTTPC_RESULT res = HttpClientGetRequest(&client, full_url, &clientData);
-        if (res != HTTP_SUCCESS)
-        {
-            printf("[OTA] range request failed: %d\n", res);
-            HttpClientUnprepare(&clientData);
-            HttpClientClse(&client);
-            fclose(fp);
-            return -1;
-        }
-
-        int code = HttpClientGetResponseCode(&client);
-        if (!(code == 206 || code == 200))
-        {
-            printf("[OTA] range HTTP code=%d\n", code);
-            HttpClientUnprepare(&clientData);
-            HttpClientClse(&client);
-            fclose(fp);
-            return -1;
-        }
-
-        if (clientData.responseBuf == NULL || clientData.responseBufLen <= 0)
-        {
-            printf("[OTA] range empty body\n");
-            HttpClientUnprepare(&clientData);
-            HttpClientClse(&client);
-            fclose(fp);
-            return -1;
-        }
-
-        int buf_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
-        size_t written = fwrite(clientData.responseBuf, 1, (size_t)buf_len, fp);
-        if ((int)written != buf_len)
-        {
-            printf("[OTA] range fwrite failed\n");
-            HttpClientUnprepare(&clientData);
-            HttpClientClse(&client);
-            fclose(fp);
-            return -1;
-        }
-
-        downloaded += buf_len;
-        printf("[OTA] range downloaded: %d/%d\n", downloaded, total_size);
-
-        /* 进度上报 */
-        int percent = (int)((downloaded * 100) / total_size);
-        if (percent > 100) percent = 100;
-        char prog[128];
-        int pn = snprintf(prog, sizeof(prog), "{\"status\":%d,\"progress\":%d}", OTA_DOWNLOAD_STATUS_PROGRESS, percent);
-        if (pn > 0) do_ln_http_server_recv_data(LN_HTTP_RECV_OTA_DOWNLOAD_MSG, prog, (uint32_t)pn);
-
-        HttpClientUnprepare(&clientData);
-        HttpClientClse(&client);
-    }
-
-    fclose(fp);
-    return 0;
-}
-
-/* 仅下载:根据最近一次 getTask 的记录字段执行下载,不再主动请求任务 */
-static uint32_t ln_ota_download_flow_http(const char *device_id,
-                                          const char *device_token,
-                                          const char *source_version)
-{
-    (void)device_id;
-    (void)device_token;
-    (void)source_version;
-    if (g_ln_last_target_version[0] == '\0' || g_ln_last_package_url[0] == '\0')
-    {
-        printf("[OTA] no cached task to download, please GET_TASK first\n");
-        return ERRCODE_FAIL;
-    }
-    /* 保存路径:LN_OTA_SAVE_DIR/upgrade_<ver>.bin */
-    char save_path[256];
-    snprintf(save_path, sizeof(save_path), LN_OTA_SAVE_DIR "/upgrade_%s.bin", g_ln_last_target_version);
-    memset(g_ln_last_saved_path, 0, sizeof(g_ln_last_saved_path));
-    strncpy(g_ln_last_saved_path, save_path, sizeof(g_ln_last_saved_path) - 1);
-
-    printf("[OTA] download only: version=%s, url=%s -> %s\n", g_ln_last_target_version, g_ln_last_package_url, save_path);
-    int dres;
-    if (g_ln_last_package_size > 0)
-    {
-        dres = ln_download_file_to_path_range(g_ln_last_package_url, save_path, g_ln_last_package_size);
-    }
-    if (dres != 0)
-    {
-        char msg[256];
-        int n = snprintf(msg, sizeof(msg), "{\"status\":%d,\"error\":\"download failed\"}", 2);
-        if (n > 0) do_ln_http_server_recv_data(LN_HTTP_RECV_OTA_DOWNLOAD_MSG, msg, (uint32_t)n);
-        return ERRCODE_FAIL;
-    }
-
-    /* 发送完成消息(不在此处做MD5,保持AT^LN_OTA_DOWNLOAD单一职责:下载)*/
-    char done[512];
-    int m = snprintf(done, sizeof(done), "{\"status\":%d,\"filePath\":\"%s\",\"md5\":\"%s\"}", 1, save_path, g_ln_last_package_md5);
-    if (m > 0) do_ln_http_server_recv_data(LN_HTTP_RECV_OTA_DOWNLOAD_MSG, done, (uint32_t)m);
-    return ERRCODE_SUCC;
-}
-
-
 /* 在接收线程内使用:解析 OTA 任务 JSON,决定是否触发下载 */
 static void ln_handle_ota_task_response(const char *json_buf, int json_len)
 {
index 734aea0..4394554 100644 (file)
@@ -8,6 +8,8 @@ extern "C"
 
 #include "stdint.h"
 #include "errcode.h"
+/* HTTP types for helper function prototypes */
+#include "netstack/http/include/httpclient.h"
 
 typedef struct
 {
@@ -16,6 +18,32 @@ typedef struct
     uint32_t len;
 } ln_http_data_mail_t;
 
+/* ============================== 公共宏与URL路径 ============================== */
+/* 接口基础配置 - 如需切换环境/密钥,在此调整 */
+#define LN_BASE_URL "https://ambybox-api.test.leiniao.com/ambybox-device-api"
+#define LN_INTERFACE_PATH "/v1/auth/getDeviceToken"
+#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_DEVICE_GET_CHARACTER_PATH "/v1/device/getCharacter"
+#define LN_DEVICE_GET_USER_ID_PATH "/v1/device/getUserId"
+#define LN_OTA_GET_TASK_PATH "/v1/upgrade/getTask"
+#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_DEVICE_GET_CHARACTER_URL LN_BASE_URL LN_DEVICE_GET_CHARACTER_PATH
+#define LN_DEVICE_GET_USER_ID_URL LN_BASE_URL LN_DEVICE_GET_USER_ID_PATH
+#define LN_OTA_GET_TASK_URL LN_BASE_URL LN_OTA_GET_TASK_PATH
+#define LN_SIGN_SECRET "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus="
+/* OTA 包保存目录(最终文件仍将放在 /user/ 下)*/
+#define LN_OTA_SAVE_DIR "/user"
+
 #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 /* 语音对话流分片接收 */
@@ -110,6 +138,57 @@ void uapi_ln_clear_device_token(void);
 int uapi_ln_ota_get_update_type(void);
 /* OTA 下载保存的最后一个文件路径(无则返回NULL)*/
 const char *uapi_ln_ota_get_saved_path(void);
+
+/* ============================== 共享全局状态(跨文件) ============================== */
+extern char *g_device_token;
+extern int g_ln_last_update_type;
+extern char g_ln_last_saved_path[256];
+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;
+
+/* ============================== 公共工具函数(供拆分后的模块复用) ============================== */
+HTTPC_RESULT ln_http_prepare_auto(HttpClientData *clientData, int header_size, int resp_size);
+void ln_sse_accumulate_and_dispatch(int cmd, const char *data, int data_len,
+                                    char *acc, int *acc_len, int acc_size);
+/* ============================== 各接口函数声明 ============================== */
+uint32_t ln_get_device_token_http(const char *device_id,
+                                  const char *device_iccid,
+                                  const char *device_model,
+                                  const char *device_version);
+
+uint32_t ln_speech_chat_stream_http(const char *device_id,
+                                    const char *device_token,
+                                    const char *audio_path);
+
+uint32_t ln_story_choose_http(const char *device_id,
+                              const char *device_token,
+                              const char *audio_path);
+
+uint32_t ln_story_random_http(const char *device_id,
+                              const char *device_token,
+                              const char *story_theme);
+
+uint32_t ln_story_exit_inquiry_http(const char *device_id,
+                                    const char *device_token);
+
+uint32_t ln_story_finish_http(const char *device_id,
+                              const char *device_token);
+
+uint32_t ln_device_get_character_http(const char *device_id,
+                                      const char *device_token);
+
+uint32_t ln_device_get_user_id_http(const char *device_id,
+                                    const char *device_token);
+
+uint32_t ln_ota_get_task_http(const char *device_id,
+                               const char *device_token,
+                               const char *source_version);
+
+uint32_t ln_ota_download_flow_http(const char *device_id,
+                                   const char *device_token,
+                                   const char *source_version);
 #ifdef __cplusplus
 }
 #endif
diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_device_info.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_device_info.c
new file mode 100644 (file)
index 0000000..e89a969
--- /dev/null
@@ -0,0 +1,136 @@
+#include "ln_api.h"
+#include <stdio.h>
+
+// 获取设备角色信息
+uint32_t ln_device_get_character_http(const char *device_id,
+                                      const char *device_token)
+{
+    if (!device_id || !device_token)
+        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);
+
+    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\n"
+                       "deviceToken: %s\r\n"
+                       "Content-Type: application/json\r\n",
+                       device_id, device_token);
+    if (hdr <= 0 || hdr >= (int)sizeof(header_buf))
+        return ERRCODE_FAIL;
+    HttpClientSetCustomHeader(&client, header_buf);
+
+    printf("device getCharacter header %s\n", header_buf);
+
+    HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024);
+    if (prep != HTTP_SUCCESS)
+    {
+        printf("device getCharacter prepare failed %d\n", prep);
+        return ERRCODE_FAIL;
+    }
+
+    HTTPC_RESULT res = HttpClientGetRequest(&client, LN_DEVICE_GET_CHARACTER_URL, &clientData);
+    if (res != HTTP_SUCCESS)
+    {
+        printf("device getCharacter get failed %d\n", res);
+        goto end;
+    }
+    int code = HttpClientGetResponseCode(&client);
+    if (code != 200)
+    {
+        printf("device getCharacter: HTTP %d\n", code);
+        if (clientData.responseBuf && clientData.responseBufLen > 0)
+        {
+            int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
+            do_ln_http_server_recv_data(LN_HTTP_RECV_DEVICE_CHARACTER, clientData.responseBuf, (uint32_t)real_len);
+        }
+        goto end;
+    }
+
+    if (clientData.responseBuf && clientData.responseBufLen > 0)
+    {
+        int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
+        do_ln_http_server_recv_data(LN_HTTP_RECV_DEVICE_CHARACTER, clientData.responseBuf, (uint32_t)real_len);
+    }
+
+end:
+    HttpClientUnprepare(&clientData);
+    HttpClientClse(&client);
+    return ERRCODE_SUCC;
+}
+
+// 获取设备绑定用户ID
+uint32_t ln_device_get_user_id_http(const char *device_id,
+                                    const char *device_token)
+{
+    if (!device_id || !device_token)
+        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);
+
+    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\n"
+                       "deviceToken: %s\r\n"
+                       "Content-Type: application/json\r\n",
+                       device_id, device_token);
+    if (hdr <= 0 || hdr >= (int)sizeof(header_buf))
+        return ERRCODE_FAIL;
+    HttpClientSetCustomHeader(&client, header_buf);
+
+    printf("device getUserId header %s\n", header_buf);
+
+    HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 8 * 1024);
+    if (prep != HTTP_SUCCESS)
+    {
+        printf("device getUserId prepare failed %d\n", prep);
+        return ERRCODE_FAIL;
+    }
+
+    HTTPC_RESULT res = HttpClientGetRequest(&client, LN_DEVICE_GET_USER_ID_URL, &clientData);
+    if (res != HTTP_SUCCESS)
+    {
+        printf("device getUserId get failed %d\n", res);
+        goto end;
+    }
+    int code = HttpClientGetResponseCode(&client);
+    if (code != 200)
+    {
+        printf("device getUserId: HTTP %d\n", code);
+        if (clientData.responseBuf && clientData.responseBufLen > 0)
+        {
+            int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
+            do_ln_http_server_recv_data(LN_HTTP_RECV_DEVICE_USER_ID, clientData.responseBuf, (uint32_t)real_len);
+        }
+        goto end;
+    }
+
+    if (clientData.responseBuf && clientData.responseBufLen > 0)
+    {
+        int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
+        do_ln_http_server_recv_data(LN_HTTP_RECV_DEVICE_USER_ID, clientData.responseBuf, (uint32_t)real_len);
+    }
+
+end:
+    HttpClientUnprepare(&clientData);
+    HttpClientClse(&client);
+    return ERRCODE_SUCC;
+}
diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_ota.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_ota.c
new file mode 100644 (file)
index 0000000..9dec066
--- /dev/null
@@ -0,0 +1,246 @@
+#include "ln_api.h"
+#include <stdio.h>
+
+uint32_t ln_ota_get_task_http(const char *device_id,
+                              const char *device_token,
+                              const char *source_version)
+{
+    if (!device_id || !device_token || !source_version)
+        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);
+
+    HttpClient client = (HttpClient){0};
+    HttpClientData clientData = (HttpClientData){0};
+    client.isHttp = false;
+    client.remotePort = 443;
+
+    /* 头:与 device info 一致,额外带 model/version 可辅助服务端日志定位 */
+    char header_buf[512];
+    int hdr = snprintf(header_buf, sizeof(header_buf),
+                       "deviceId: %s\r\n"
+                       "deviceToken: %s\r\n"
+                       "deviceModel: %s\r\n"
+                       "deviceVersion: %s\r\n"
+                       "Content-Type: application/json\r\n",
+                       device_id, device_token,
+                       (uapi_ln_get_device_model() ? uapi_ln_get_device_model() : "abmybox_1"),
+                       (uapi_ln_get_device_version() ? uapi_ln_get_device_version() : "1.0.0"));
+    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)
+    {
+        printf("[OTA] getTask prepare failed %d\n", prep);
+        return ERRCODE_FAIL;
+    }
+
+    /* 拼 GET URL 参数 */
+    char url_with_params[512] = {0};
+    int ulen = snprintf(url_with_params, sizeof(url_with_params),
+                        "%s?deviceId=%s&sourceVersion=%s",
+                        LN_OTA_GET_TASK_URL, device_id, source_version);
+    if (ulen <= 0 || ulen >= (int)sizeof(url_with_params))
+    {
+        HttpClientUnprepare(&clientData);
+        return ERRCODE_FAIL;
+    }
+
+    printf("[OTA] GET %s\n", url_with_params);
+
+    HTTPC_RESULT res = HttpClientGetRequest(&client, url_with_params, &clientData);
+    if (res != HTTP_SUCCESS)
+    {
+        printf("[OTA] getTask failed %d\n", res);
+        goto end;
+    }
+
+    int code = HttpClientGetResponseCode(&client);
+    if (code != 200)
+    {
+        printf("[OTA] getTask failed %d\n", code);
+        if (clientData.responseBuf && clientData.responseBufLen > 0)
+        {
+            int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
+            do_ln_http_server_recv_data(LN_HTTP_RECV_OTA_TASK, clientData.responseBuf, (uint32_t)real_len);
+        }
+        goto end;
+    }
+
+    if (clientData.responseBuf && clientData.responseBufLen > 0)
+    {
+        int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
+        do_ln_http_server_recv_data(LN_HTTP_RECV_OTA_TASK, clientData.responseBuf, (uint32_t)real_len);
+    }
+
+end:
+    HttpClientUnprepare(&clientData);
+    HttpClientClse(&client);
+    return ERRCODE_SUCC;
+}
+
+/* 分片下载:使用 Range 头按固定分片大小请求,边下边写并上报进度 */
+static int ln_download_file_to_path_range(const char *full_url, const char *save_path, int total_size)
+{
+    if (!full_url || !save_path || total_size <= 0)
+    {
+        printf("[OTA] range download: url=%s -> %s, total=%d\n", full_url, save_path, total_size);
+        return -1;
+    }
+    printf("[OTA] range download: url=%s -> %s, total=%d\n", full_url, save_path, total_size);
+
+    /* CAT1 网络保活处理 */
+    cat1_api_add_cat1_vote();
+    cat1_api_wakeup_cat1(1);
+    cat1_api_keep_wakeup(1);
+    cat1_api_disable_cat1_cmd(0);
+
+    FILE *fp = fopen(save_path, "wb");
+    if (!fp)
+    {
+        printf("[OTA] open file failed: %s\n", save_path);
+        return -1;
+    }
+
+    const int chunk_size = 256 * 1024; /* 256KB 分片,兼顾内存与吞吐 */
+    int64_t downloaded = 0;
+
+    while (downloaded < total_size)
+    {
+        int start = (int)downloaded;
+        int remaining = total_size - start;
+        int this_chunk = remaining > chunk_size ? chunk_size : remaining;
+        int end = start + this_chunk - 1;
+
+        HttpClient client = (HttpClient){0};
+        HttpClientData clientData = (HttpClientData){0};
+
+        /* 判断协议 */
+        bool is_https = (strstr(full_url, "https://") == full_url);
+        client.isHttp = is_https ? false : true;
+        client.remotePort = is_https ? 443 : 80;
+
+        /* 自定义 Range 头 */
+        char header_buf[256];
+        int hdr = snprintf(header_buf, sizeof(header_buf), "Range: bytes=%d-%d\r\n", start, end);
+        if (hdr <= 0 || hdr >= (int)sizeof(header_buf))
+        {
+            printf("[OTA] range header failed: %d\n", hdr);
+            fclose(fp);
+            return -1;
+        }
+        HttpClientSetCustomHeader(&client, header_buf);
+
+        /* 预留足够的响应缓冲区(分片大小+余量)*/
+        HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, this_chunk + 16 * 1024);
+        if (prep != HTTP_SUCCESS)
+        {
+            printf("[OTA] range prepare failed: %d\n", prep);
+            fclose(fp);
+            return -1;
+        }
+
+        HTTPC_RESULT res = HttpClientGetRequest(&client, full_url, &clientData);
+        if (res != HTTP_SUCCESS)
+        {
+            printf("[OTA] range request failed: %d\n", res);
+            HttpClientUnprepare(&clientData);
+            HttpClientClse(&client);
+            fclose(fp);
+            return -1;
+        }
+
+        int code = HttpClientGetResponseCode(&client);
+        if (!(code == 206 || code == 200))
+        {
+            printf("[OTA] range HTTP code=%d\n", code);
+            HttpClientUnprepare(&clientData);
+            HttpClientClse(&client);
+            fclose(fp);
+            return -1;
+        }
+
+        if (clientData.responseBuf == NULL || clientData.responseBufLen <= 0)
+        {
+            printf("[OTA] range empty body\n");
+            HttpClientUnprepare(&clientData);
+            HttpClientClse(&client);
+            fclose(fp);
+            return -1;
+        }
+
+        int buf_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
+        size_t written = fwrite(clientData.responseBuf, 1, (size_t)buf_len, fp);
+        if ((int)written != buf_len)
+        {
+            printf("[OTA] range fwrite failed\n");
+            HttpClientUnprepare(&clientData);
+            HttpClientClse(&client);
+            fclose(fp);
+            return -1;
+        }
+
+        downloaded += buf_len;
+        printf("[OTA] range downloaded: %d/%d\n", downloaded, total_size);
+
+        /* 进度上报 */
+        int percent = (int)((downloaded * 100) / total_size);
+        if (percent > 100)
+            percent = 100;
+        char prog[128];
+        int pn = snprintf(prog, sizeof(prog), "{\"status\":%d,\"progress\":%d}", OTA_DOWNLOAD_STATUS_PROGRESS, percent);
+        if (pn > 0)
+            do_ln_http_server_recv_data(LN_HTTP_RECV_OTA_DOWNLOAD_MSG, prog, (uint32_t)pn);
+
+        HttpClientUnprepare(&clientData);
+        HttpClientClse(&client);
+    }
+
+    fclose(fp);
+    return 0;
+}
+
+uint32_t ln_ota_download_flow_http(const char *device_id,
+                                   const char *device_token,
+                                   const char *source_version)
+{
+    (void)device_id;
+    (void)device_token;
+    (void)source_version;
+    if (g_ln_last_target_version[0] == '\0' || g_ln_last_package_url[0] == '\0')
+    {
+        printf("[OTA] no cached task to download, please GET_TASK first\n");
+        return ERRCODE_FAIL;
+    }
+    char save_path[256];
+    snprintf(save_path, sizeof(save_path), LN_OTA_SAVE_DIR "/upgrade_%s.bin", g_ln_last_target_version);
+    memset(g_ln_last_saved_path, 0, sizeof(g_ln_last_saved_path));
+    strncpy(g_ln_last_saved_path, save_path, sizeof(g_ln_last_saved_path) - 1);
+
+    printf("[OTA] download only: version=%s, url=%s -> %s\n", g_ln_last_target_version, g_ln_last_package_url, save_path);
+    int dres;
+    if (g_ln_last_package_size > 0)
+    {
+        dres = ln_download_file_to_path_range(g_ln_last_package_url, save_path, g_ln_last_package_size);
+    }
+    if (dres != 0)
+    {
+        char msg[256];
+        int n = snprintf(msg, sizeof(msg), "{\"status\":%d,\"error\":\"download failed\"}", 2);
+        if (n > 0)
+            do_ln_http_server_recv_data(LN_HTTP_RECV_OTA_DOWNLOAD_MSG, msg, (uint32_t)n);
+        return ERRCODE_FAIL;
+    }
+
+    char done[512];
+    int m = snprintf(done, sizeof(done), "{\"status\":%d,\"filePath\":\"%s\",\"md5\":\"%s\"}", 1, save_path, g_ln_last_package_md5);
+    if (m > 0)
+        do_ln_http_server_recv_data(LN_HTTP_RECV_OTA_DOWNLOAD_MSG, done, (uint32_t)m);
+    return ERRCODE_SUCC;
+}
diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_speech_chat.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_speech_chat.c
new file mode 100644 (file)
index 0000000..e9c7983
--- /dev/null
@@ -0,0 +1,246 @@
+#include "ln_api.h"
+#include <stdio.h>
+
+/* 读取整个文件到内存,返回缓冲区和长度;需调用者 free */
+static uint8_t *ln_read_file_all_speech_chat(const char *path, int *out_len)
+{
+    if (out_len)
+        *out_len = 0;
+    if (path == NULL)
+        return NULL;
+    FILE *fp = fopen(path, "rb");
+    if (!fp)
+        return NULL;
+    fseek(fp, 0, SEEK_END);
+    long sz = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+    if (sz <= 0 || sz > (10 * 1024 * 1024))
+    {
+        fclose(fp);
+        return NULL;
+    }
+    uint8_t *buf = (uint8_t *)malloc((size_t)sz);
+    if (!buf)
+    {
+        fclose(fp);
+        return NULL;
+    }
+    size_t n = fread(buf, 1, (size_t)sz, fp);
+    fclose(fp);
+    if ((long)n != sz)
+    {
+        free(buf);
+        return NULL;
+    }
+    if (out_len)
+        *out_len = (int)sz;
+    return buf;
+}
+
+/* 简单 multipart/form-data 组包(单文件字段 file) */
+static int ln_build_multipart_form_speech_chat(const char *boundary,
+                                               const char *filename,
+                                               const uint8_t *file_data,
+                                               int file_len,
+                                               char **out_body,
+                                               int *out_len,
+                                               char *out_content_type,
+                                               int ctype_len)
+{
+    if (!boundary || !filename || !file_data || file_len <= 0 || !out_body || !out_len)
+        return -1;
+    char head[512];
+    int head_len = snprintf(head, sizeof(head),
+                            "--%s\r\n"
+                            "Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n"
+                            "Content-Type: audio/mpeg\r\n\r\n",
+                            boundary, filename);
+    char tail[128];
+    int tail_len = snprintf(tail, sizeof(tail), "\r\n--%s--\r\n", boundary);
+    int total = head_len + file_len + tail_len;
+    char *body = (char *)malloc((size_t)total);
+    if (!body)
+        return -1;
+    memcpy(body, head, head_len);
+    memcpy(body + head_len, file_data, (size_t)file_len);
+    memcpy(body + head_len + file_len, tail, tail_len);
+    *out_body = body;
+    *out_len = total;
+    if (out_content_type && ctype_len > 0)
+    {
+        snprintf(out_content_type, ctype_len, "multipart/form-data; boundary=%s", boundary);
+    }
+    return 0;
+}
+
+uint32_t ln_speech_chat_stream_http(const char *device_id,
+                                    const char *device_token,
+                                    const char *audio_path)
+{
+    if (!device_id || !device_token || !audio_path)
+        return ERRCODE_FAIL;
+
+    /* 读取音频 */
+    int audio_len = 0;
+    uint8_t *audio_buf = ln_read_file_all_speech_chat(audio_path, &audio_len);
+    if (!audio_buf)
+    {
+        printf("chatstream: read audio failed\n");
+        return ERRCODE_FAIL;
+    }
+    printf("chatstream: audio length = %d\n", audio_len);
+
+    /* boundary */
+    char boundary[64];
+    snprintf(boundary, sizeof(boundary), "----LNBOUNDARY-%u", (unsigned)uapi_tcxo_get_ms());
+
+    /* 组包 */
+    char *body = NULL;
+    int body_len = 0;
+    char content_type[96];
+    const char *fname = audio_path;
+    const char *slash = strrchr(audio_path, '/');
+    if (slash && *(slash + 1))
+    {
+        fname = slash + 1;
+    }
+    if (ln_build_multipart_form_speech_chat(boundary, fname && *fname ? fname : "audio.mp3", audio_buf, audio_len,
+                                            &body, &body_len, content_type, sizeof(content_type)) != 0)
+    {
+        free(audio_buf);
+        return ERRCODE_FAIL;
+    }
+    free(audio_buf);
+
+    /* 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 = (HttpClient){0};
+    HttpClientData clientData = (HttpClientData){0};
+    client.isHttp = false; /* https */
+    client.remotePort = 443;
+
+    /* 自定义头:仅保留自定义业务头,Content-Type 交由 postContentType 指定 */
+    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))
+    {
+        free(body);
+        return ERRCODE_FAIL;
+    }
+    HttpClientSetCustomHeader(&client, header_buf);
+    printf("chatstream header_buf: %s\n", header_buf);
+
+    /* 准备缓冲区(自适应) */
+    HTTPC_RESULT prep = ln_http_prepare_auto(&clientData, 2048, 64 * 1024);
+    if (prep != HTTP_SUCCESS)
+    {
+        free(body);
+        return ERRCODE_FAIL;
+    }
+
+    /* 设置POST数据与Content-Type(放在Prepare之后,避免被重置) */
+    clientData.postBuf = body;
+    clientData.postBufLen = body_len;
+    clientData.postContentType = content_type;
+    /* SSE 场景:响应通常为 Transfer-Encoding: chunked,打开分块标志以避免长度校验问题 */
+    clientData.isChunked = true;
+
+    /* 发送请求:对易抖动错误进行有限次重试 */
+    HTTPC_RESULT res = HTTP_EUNKOWN;
+    for (int attempt = 1; attempt <= 3; attempt++)
+    {
+        res = HttpClientPostRequest(&client, LN_CHAT_URL, &clientData);
+        printf("chatstream: post attempt %d result %d\n", attempt, res);
+        if (res == HTTP_SUCCESS)
+        {
+            break;
+        }
+        if (res == HTTP_ECONN || res == HTTP_ECLSD || res == HTTP_ERECV || res == HTTP_ETIMEOUT)
+        {
+            /* 连接/收发超时类错误,短暂休眠后重建会话重试 */
+            HttpClientUnprepare(&clientData);
+            HttpClientClse(&client);
+            LOS_Msleep(200);
+            client = (HttpClient){0};
+            clientData = (HttpClientData){0};
+            client.isHttp = false;
+            client.remotePort = 443;
+            HttpClientSetCustomHeader(&client, header_buf);
+            HTTPC_RESULT prep2 = ln_http_prepare_auto(&clientData, 2048, 64 * 1024);
+            if (prep2 != HTTP_SUCCESS)
+            {
+                printf("chatstream: prepare failed on retry %d, code %d\n", attempt, prep2);
+                break;
+            }
+            clientData.postBuf = body;
+            clientData.postBufLen = body_len;
+            clientData.postContentType = content_type;
+            clientData.isChunked = true;
+            continue;
+        }
+        /* 其他错误不再重试 */
+        break;
+    }
+    if (res != HTTP_SUCCESS)
+    {
+        printf("chatstream: post failed %d\n", res);
+        HttpClientUnprepare(&clientData);
+        HttpClientClse(&client);
+        free(body);
+        return ERRCODE_FAIL;
+    }
+
+    int code = HttpClientGetResponseCode(&client);
+    if (code != 200)
+    {
+        printf("chatstream: HTTP %d\n", code);
+        if (clientData.responseBuf && clientData.responseBufLen > 0)
+        {
+            int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
+            do_ln_http_server_recv_data(LN_HTTP_SERVICE_CMD_SSE_RECV, clientData.responseBuf, (uint32_t)real_len);
+        }
+        HttpClientUnprepare(&clientData);
+        HttpClientClse(&client);
+        free(body);
+        return ERRCODE_FAIL;
+    }
+
+    /* 循环接收SSE分片并转投递到接收队列 */
+    while (1)
+    {
+        HTTPC_RESULT rr = HttpClientRecvResponse(&client, &clientData);
+        if (rr == HTTP_SUCCESS || rr == HTTP_EAGAIN)
+        {
+            if (clientData.responseBuf && clientData.responseBufLen > 0)
+            {
+                do_ln_http_server_recv_data(LN_HTTP_SERVICE_CMD_SSE_RECV, clientData.responseBuf, (uint32_t)clientData.responseBufLen);
+                HttpClientReset(&clientData);
+            }
+            else
+            {
+                LOS_Msleep(20);
+            }
+            continue;
+        }
+        else
+        {
+            printf("SSE recv end: %d\n", rr);
+            break;
+        }
+    }
+
+    HttpClientUnprepare(&clientData);
+    HttpClientClse(&client);
+    free(body);
+    return ERRCODE_SUCC;
+}
diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_choose.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_choose.c
new file mode 100644 (file)
index 0000000..8dbcdb3
--- /dev/null
@@ -0,0 +1,265 @@
+#include "ln_api.h"
+#include <stdio.h>
+
+/* 读取整个文件到内存,返回缓冲区和长度;需调用者 free */
+static uint8_t *ln_read_file_all_story(const char *path, int *out_len)
+{
+    if (out_len)
+        *out_len = 0;
+    if (path == NULL)
+        return NULL;
+    FILE *fp = fopen(path, "rb");
+    if (!fp)
+        return NULL;
+    fseek(fp, 0, SEEK_END);
+    long sz = ftell(fp);
+    fseek(fp, 0, SEEK_SET);
+    if (sz <= 0 || sz > (10 * 1024 * 1024))
+    {
+        fclose(fp);
+        return NULL;
+    }
+    uint8_t *buf = (uint8_t *)malloc((size_t)sz);
+    if (!buf)
+    {
+        fclose(fp);
+        return NULL;
+    }
+    size_t n = fread(buf, 1, (size_t)sz, fp);
+    fclose(fp);
+    if ((long)n != sz)
+    {
+        free(buf);
+        return NULL;
+    }
+    if (out_len)
+        *out_len = (int)sz;
+    return buf;
+}
+
+/* 简单 multipart/form-data 组包(单文件字段 file) */
+static int ln_build_multipart_form_story(const char *boundary,
+                                         const char *filename,
+                                         const uint8_t *file_data,
+                                         int file_len,
+                                         char **out_body,
+                                         int *out_len,
+                                         char *out_content_type,
+                                         int ctype_len)
+{
+    if (!boundary || !filename || !file_data || file_len <= 0 || !out_body || !out_len)
+        return -1;
+    char head[512];
+    int head_len = snprintf(head, sizeof(head),
+                            "--%s\r\n"
+                            "Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n"
+                            "Content-Type: audio/mpeg\r\n\r\n",
+                            boundary, filename);
+    char tail[128];
+    int tail_len = snprintf(tail, sizeof(tail), "\r\n--%s--\r\n", boundary);
+    int total = head_len + file_len + tail_len;
+    char *body = (char *)malloc((size_t)total);
+    if (!body)
+        return -1;
+    memcpy(body, head, head_len);
+    memcpy(body + head_len, file_data, (size_t)file_len);
+    memcpy(body + head_len + file_len, tail, tail_len);
+    *out_body = body;
+    *out_len = total;
+    if (out_content_type && ctype_len > 0)
+    {
+        snprintf(out_content_type, ctype_len, "multipart/form-data; boundary=%s", boundary);
+    }
+    return 0;
+}
+
+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_story(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_story(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);
+
+    /* 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;
+
+    /* 设置请求头:精简头部,避免重复(Host/Connection 交由库设置) */
+    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",
+                       device_id, device_token);
+    if (hdr <= 0 || hdr >= (int)sizeof(header_buf))
+    {
+        printf("story choose: build header failed\n");
+        free(body);
+        return ERRCODE_FAIL;
+    }
+    HttpClientSetCustomHeader(&client, header_buf);
+    printf("story choose header_buf: %s\n", 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;
+    clientData.isChunked = true;
+
+    /* 发送请求:对易抖动错误进行有限次重试 */
+    HTTPC_RESULT res = HTTP_EUNKOWN;
+    for (int attempt = 1; attempt <= 3; attempt++)
+    {
+        res = HttpClientPostRequest(&client, LN_STORY_CHOOSE_URL, &clientData);
+        printf("story choose: post attempt %d result %d\n", attempt, res);
+        if (res == HTTP_SUCCESS)
+        {
+            break;
+        }
+        if (res == HTTP_ECONN || res == HTTP_ECLSD || res == HTTP_ERECV || res == HTTP_ETIMEOUT)
+        {
+            HttpClientUnprepare(&clientData);
+            HttpClientClse(&client);
+            LOS_Msleep(200);
+            client = (HttpClient){0};
+            clientData = (HttpClientData){0};
+            client.isHttp = false;
+            client.remotePort = 443;
+            HttpClientSetCustomHeader(&client, header_buf);
+            HTTPC_RESULT prep2 = ln_http_prepare_auto(&clientData, 2048, 64 * 1024);
+            if (prep2 != HTTP_SUCCESS)
+            {
+                printf("story choose: prepare failed on retry %d, code %d\n", attempt, prep2);
+                break;
+            }
+            clientData.postBuf = body;
+            clientData.postBufLen = body_len;
+            clientData.postContentType = content_type;
+            clientData.isChunked = true;
+            continue;
+        }
+        break;
+    }
+    printf("story choose: send POST request result: %d\n", res);
+    if (res != HTTP_SUCCESS)
+    {
+        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);
+    printf("story choose: send POST request result: %d\n", res);
+    if (http_code != 200)
+    {
+        printf("story choose: invalid HTTP response code (%d)\n", http_code);
+        if (clientData.responseBuf && clientData.responseBufLen > 0)
+        {
+            int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
+            do_ln_http_server_recv_data(LN_HTTP_RECV_STORY_CHOOSE, clientData.responseBuf, real_len);
+        }
+        free(body);
+        HttpClientUnprepare(&clientData);
+        HttpClientClse(&client);
+        return ERRCODE_FAIL;
+    }
+
+    printf("story choose: send POST request success\n");
+    if (clientData.responseBuf && clientData.responseBufLen > 0)
+    {
+        int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
+        do_ln_http_server_recv_data(LN_HTTP_RECV_STORY_CHOOSE, clientData.responseBuf, real_len);
+    }
+
+    /* 循环接收 SSE 分块数据(带累积处理)*/
+    int loop_guard = 0;
+    char sse_acc[4096] = {0};
+    int sse_acc_len = 0;
+    const int MAX_LOOP = 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)
+            {
+                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;
+}
diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_exit.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_exit.c
new file mode 100644 (file)
index 0000000..9e3193f
--- /dev/null
@@ -0,0 +1,65 @@
+#include "ln_api.h"
+#include <stdio.h>
+
+uint32_t ln_story_exit_inquiry_http(const char *device_id,
+                                    const char *device_token)
+{
+    if (!device_id || !device_token)
+        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);
+
+    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));
+        if (clientData.responseBuf && clientData.responseBufLen > 0)
+        {
+            int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
+            do_ln_http_server_recv_data(LN_HTTP_RECV_STORY_EXIT, clientData.responseBuf, real_len);
+        }
+        goto end;
+    }
+
+    if (clientData.responseBuf && clientData.responseBufLen > 0)
+    {
+        int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
+        do_ln_http_server_recv_data(LN_HTTP_RECV_STORY_EXIT, clientData.responseBuf, (uint32_t)real_len);
+    }
+
+end:
+    HttpClientUnprepare(&clientData);
+    HttpClientClse(&client);
+    return ERRCODE_SUCC;
+}
diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_finish.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_finish.c
new file mode 100644 (file)
index 0000000..f0006fd
--- /dev/null
@@ -0,0 +1,64 @@
+#include "ln_api.h"
+#include <stdio.h>
+
+uint32_t ln_story_finish_http(const char *device_id,
+                              const char *device_token)
+{
+    if (!device_id || !device_token)
+        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);
+
+    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;
+    }
+    int code = HttpClientGetResponseCode(&client);
+    if (code != 200)
+    {
+        printf("device getUserId: HTTP %d\n", code);
+        if (clientData.responseBuf && clientData.responseBufLen > 0)
+        {
+            int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
+            do_ln_http_server_recv_data(LN_HTTP_RECV_STORY_FINISH, clientData.responseBuf, (uint32_t)real_len);
+        }
+        goto end;
+    }
+
+    if (clientData.responseBuf && clientData.responseBufLen > 0)
+    {
+        int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
+        do_ln_http_server_recv_data(LN_HTTP_RECV_STORY_FINISH, clientData.responseBuf, (uint32_t)real_len);
+    }
+
+end:
+    HttpClientUnprepare(&clientData);
+    HttpClientClse(&client);
+    return ERRCODE_SUCC;
+}
diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_random.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_story_random.c
new file mode 100644 (file)
index 0000000..ebf74cc
--- /dev/null
@@ -0,0 +1,171 @@
+#include "ln_api.h"
+#include <stdio.h>
+
+uint32_t ln_story_random_http(const char *device_id,
+                              const char *device_token,
+                              const char *story_theme)
+{
+    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;
+
+    if (story_theme && story_theme[0])
+    {
+        int i = 0, j = 0;
+        while (story_theme[i] && j < (int)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";
+
+    /* 发送 POST 请求 */
+    HTTPC_RESULT res = HttpClientPostRequest(&client, LN_STORY_RANDOM_URL, &clientData);
+    printf("story random: send POST request result: %d\n", res);
+    if (res != HTTP_SUCCESS)
+    {
+        printf("story random: send POST request failed (code: %d)\n", res);
+        HttpClientUnprepare(&clientData);
+        HttpClientClse(&client);
+        return ERRCODE_FAIL;
+    }
+
+    printf("story random: send POST request success\n");
+
+    /* 检查 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;
+    }
+
+    printf("story random: HTTP response code (%d)\n", http_code);
+    if (clientData.responseBuf && clientData.responseBufLen > 0)
+    {
+        int real_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
+        do_ln_http_server_recv_data(LN_HTTP_RECV_STORY_RANDOM, clientData.responseBuf, real_len);
+    }
+
+    /* 循环接收 SSE 分块数据(带累积处理)*/
+    int loop_guard = 0;
+    char sse_acc[4096] = {0};
+    int sse_acc_len = 0;
+    const int MAX_LOOP = 2000;
+    while (loop_guard++ < MAX_LOOP)
+    {
+        HTTPC_RESULT recv_res = HttpClientRecvResponse(&client, &clientData);
+        printf("story random: recv response result: %d\n", recv_res);
+        if (recv_res == HTTP_SUCCESS || recv_res == HTTP_EAGAIN)
+        {
+            if (clientData.responseBuf && clientData.responseBufLen > 0)
+            {
+                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;
+}
diff --git a/code/sdk/zhihui/leiniao/imsl_ln/ln_api_token.c b/code/sdk/zhihui/leiniao/imsl_ln/ln_api_token.c
new file mode 100644 (file)
index 0000000..60259ad
--- /dev/null
@@ -0,0 +1,237 @@
+#include "ln_api.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "cJSON.h"
+/* for HMAC-SHA256 */
+#include "mbedtls/md.h"
+/* for MD5 file verify */
+#include "mbedtls/md5.h"
+
+static void ln_to_hex_lower(const uint8_t *in, size_t in_len, char *out)
+{
+    static const char hex[] = "0123456789abcdef";
+    for (size_t i = 0; i < in_len; i++)
+    {
+        out[i * 2] = hex[(in[i] >> 4) & 0x0F];
+        out[i * 2 + 1] = hex[in[i] & 0x0F];
+    }
+    out[in_len * 2] = '\0';
+}
+
+/* 计算 HMAC-SHA256 并输出小写hex字符串 */
+static int ln_hmac_sha256_hex(const char *key, const char *msg, char *out_hex, size_t out_hex_size)
+{
+    if (out_hex_size < 65)
+    { /* 32 bytes -> 64 hex chars + NUL */
+        return -1;
+    }
+    const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
+    if (md_info == NULL)
+    {
+        return -1;
+    }
+    uint8_t digest[32];
+    int ret = mbedtls_md_hmac(md_info, (const unsigned char *)key, strlen(key),
+                              (const unsigned char *)msg, strlen(msg), digest);
+    if (ret != 0)
+    {
+        return -1;
+    }
+    ln_to_hex_lower(digest, sizeof(digest), out_hex);
+    return 0;
+}
+
+/* 生成[a-zA-Z0-9]随机串 */
+static void ln_gen_nonce(char *out, size_t len)
+{
+    static const char table[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+    if (out == NULL || len == 0)
+    {
+        return;
+    }
+    /* 使用时间作为随机种子,避免强依赖标准rand种子 */
+    uint32_t ts_s = 0;
+    uapi_get_timestampe_ss(&ts_s);
+    srand((unsigned int)(ts_s ^ (unsigned int)uapi_tcxo_get_ms()));
+    for (size_t i = 0; i + 1 < len; i++)
+    {
+        out[i] = table[rand() % (sizeof(table) - 1)];
+    }
+    out[len - 1] = '\0';
+}
+
+uint32_t ln_get_device_token_http(const char *device_id,
+                                  const char *device_iccid,
+                                  const char *device_model,
+                                  const char *device_version)
+{
+    if (device_id == NULL || device_iccid == NULL)
+    {
+        printf("LN token: invalid parameters\n");
+        return ERRCODE_FAIL;
+    }
+
+    /* 1. 生成时间戳(ms)与nonce */
+    uint32_t ts_s = 0; /* epoch seconds */
+    uapi_get_timestampe_ss(&ts_s);
+    char timestamp[32] = {0};
+    (void)snprintf(timestamp, sizeof(timestamp), "%u000", ts_s); /* 转毫秒字符串 */
+
+    char nonce[17] = {0}; /* 12~16位,取固定16以简化 */
+    ln_gen_nonce(nonce, sizeof(nonce));
+
+    /* 2. 计算签名: deviceId:deviceIccid:timestamp:nonce */
+    char sign_data[256] = {0};
+    (void)snprintf(sign_data, sizeof(sign_data), "%s:%s:%s:%s", device_id, device_iccid, timestamp, nonce);
+    char signature[65] = {0};
+    if (ln_hmac_sha256_hex(LN_SIGN_SECRET, sign_data, signature, sizeof(signature)) != 0)
+    {
+        printf("LN token: hmac calc failed\n");
+        return ERRCODE_FAIL;
+    }
+
+    /* 3. 组织自定义请求头 */
+    char header_buf[1024] = {0};
+    int hdr_len = snprintf(header_buf, sizeof(header_buf),
+                           "deviceId: %s\r\n"
+                           "deviceIccid: %s\r\n"
+                           "timestamp: %s\r\n"
+                           "nonce: %s\r\n"
+                           "signature: %s\r\n"
+                           "deviceModel: %s\r\n"
+                           "deviceVersion: %s\r\n"
+                           "Content-Type: application/json\r\n",
+                           device_id, device_iccid, timestamp, nonce, signature,
+                           (device_model && device_model[0]) ? device_model : "abmybox_1",
+                           (device_version && device_version[0]) ? device_version : "1.0.0");
+    if (hdr_len <= 0 || hdr_len >= (int)sizeof(header_buf))
+    {
+        printf("LN token: header build failed\n");
+        return ERRCODE_FAIL;
+    }
+
+    /* 4. 保活CAT1,准备网络 */
+    cat1_api_add_cat1_vote();
+    cat1_api_wakeup_cat1(1);
+    cat1_api_keep_wakeup(1);
+    cat1_api_disable_cat1_cmd(1);
+    LOS_Msleep(500);
+
+    /* 5. 发起HTTPS GET */
+    HttpClient client = (HttpClient){0};
+    HttpClientData clientData = (HttpClientData){0};
+    client.isHttp = false; /* https */
+    client.remotePort = 443;
+    HttpClientSetCustomHeader(&client, header_buf);
+
+    HTTPC_RESULT prep = HttpClientPrepare(&clientData, 2048, 4096);
+    if (prep != HTTP_SUCCESS)
+    {
+        printf("LN token: HttpClientPrepare failed %d\n", prep);
+        return ERRCODE_FAIL;
+    }
+
+    HTTPC_RESULT res = HttpClientGetRequest(&client, LN_FULL_URL, &clientData);
+    if (res != HTTP_SUCCESS)
+    {
+        printf("LN token: request failed %d\n", res);
+        HttpClientUnprepare(&clientData);
+        HttpClientClse(&client);
+        return ERRCODE_FAIL;
+    }
+
+    int code = HttpClientGetResponseCode(&client);
+    if (code != 200)
+    {
+        printf("LN token: HTTP %d\n", code);
+        HttpClientUnprepare(&clientData);
+        HttpClientClse(&client);
+        return ERRCODE_FAIL;
+    }
+
+    /* 6. 解析响应JSON */
+    if (clientData.responseBuf == NULL || clientData.responseBufLen <= 0)
+    {
+        printf("LN token: empty body\n");
+        HttpClientUnprepare(&clientData);
+        HttpClientClse(&client);
+        return ERRCODE_FAIL;
+    }
+
+    int body_len = clientData.responseContentLen > 0 ? clientData.responseContentLen : clientData.responseBufLen;
+    if (body_len <= 0)
+        body_len = (int)strlen(clientData.responseBuf);
+
+    char *json_text = (char *)malloc((size_t)body_len + 1);
+    if (json_text == NULL)
+    {
+        HttpClientUnprepare(&clientData);
+        HttpClientClse(&client);
+        return ERRCODE_FAIL;
+    }
+    memcpy(json_text, clientData.responseBuf, (size_t)body_len);
+    json_text[body_len] = '\0';
+
+    cJSON *root = cJSON_Parse(json_text);
+    if (!root)
+    {
+        printf("LN token: JSON parse failed\n");
+        free(json_text);
+        HttpClientUnprepare(&clientData);
+        HttpClientClse(&client);
+        return ERRCODE_FAIL;
+    }
+
+    cJSON *errorCode = cJSON_GetObjectItem(root, "errorCode");
+    if (errorCode != NULL && !cJSON_IsNull(errorCode))
+    {
+        cJSON *msg = cJSON_GetObjectItem(root, "message");
+        printf("LN token: failed, err=%s\n", msg && cJSON_IsString(msg) ? msg->valuestring : "unknown");
+        cJSON_Delete(root);
+        free(json_text);
+        HttpClientUnprepare(&clientData);
+        HttpClientClse(&client);
+        return ERRCODE_FAIL;
+    }
+
+    cJSON *data = cJSON_GetObjectItem(root, "data");
+    if (data && cJSON_IsObject(data))
+    {
+        cJSON *token = cJSON_GetObjectItem(data, "token");
+        cJSON *expire = cJSON_GetObjectItem(data, "expireTime");
+        if (g_device_token)
+        {
+            free(g_device_token);
+            g_device_token = NULL;
+        }
+        if (token && cJSON_IsString(token))
+        {
+            size_t tlen = strlen(token->valuestring);
+            g_device_token = (char *)malloc(tlen + 1);
+            if (g_device_token)
+            {
+                memcpy(g_device_token, token->valuestring, tlen + 1);
+            }
+        }
+        if (g_device_token == NULL)
+        {
+            g_device_token = strdup("<null>");
+        }
+        printf("\njson=%s\n", json_text);
+        if (expire && cJSON_IsNumber(expire))
+        {
+            printf("Token expireTime: %lld\n", (long long)expire->valuedouble);
+        }
+    }
+    else
+    {
+        printf("LN token: no data field\n");
+    }
+
+    cJSON_Delete(root);
+    free(json_text);
+    HttpClientUnprepare(&clientData);
+    HttpClientClse(&client);
+    return ERRCODE_SUCC;
+}
index 615281a..4869b28 100644 (file)
@@ -125,6 +125,10 @@ def print_request_details(url: str, method: str, headers: Dict, data: Optional[D
 
 
 def process_sse_stream(response: requests.Response) -> None:
+    # 新增:打印完整请求头(包括自动生成的Content-Type)
+    logger.info("完整请求头(含自动生成部分):")
+    for k, v in response.request.headers.items():
+        logger.info(f"  {k}: {v}")
     """处理SSE流式响应的通用函数"""
     if response.status_code != 200:
         logger.error(f"❌ 请求失败,状态码: {response.status_code}")
diff --git a/code/sdk/zhihui/leiniao/设备API接口文档.md b/code/sdk/zhihui/leiniao/设备API接口文档.md
new file mode 100644 (file)
index 0000000..b3a7ed1
--- /dev/null
@@ -0,0 +1,1554 @@
+# 设备API接口文档\r
+\r
+测试服务BaseUrl:https://ambybox-api.test.leiniao.com/ambybox-device-api\r
+\r
+## 1. 获取设备Token接口\r
+\r
+### 时序图\r
+\r
+```mermaid\r
+sequenceDiagram\r
+    participant Device as 设备端\r
+    participant Server as 服务端\r
+    \r
+    Note over Device, Server: 设备认证流程\r
+    \r
+    Device->>Device: 生成时间戳和随机数\r
+    Device->>Device: 构建签名数据<br/>deviceId:deviceIccid:timestamp:nonce\r
+    Device->>Device: 使用HMAC-SHA256生成签名\r
+    \r
+    Device->>Server: GET /v1/auth/getDeviceToken<br/>Headers: deviceId, deviceIccid, timestamp, nonce, signature\r
+    \r
+    alt 签名验证成功\r
+        Server->>Server: 验证时间戳(5分钟内有效)\r
+        Server->>Server: 验证签名正确性\r
+        Server->>Server: 生成JWT Token\r
+        Server->>Device: 返回设备Token<br/>{"deviceId", "token", "expireTime"}\r
+        \r
+        Note over Device: Token获取成功,缓存Token\r
+        \r
+    else 签名验证失败\r
+        Server->>Device: 返回错误<br/>{"errorCode": "INVALID_SIGNATURE"}\r
+        Note over Device: 认证失败,重新尝试\r
+    end\r
+```\r
+\r
+### 接口描述\r
+设备认证接口,用于获取设备访问令牌。该接口无需认证,但需要进行签名校验。\r
+\r
+### 请求信息\r
+- **URL**: `/v1/auth/getDeviceToken`\r
+- **方法**: `GET`\r
+- **Content-Type**: `application/json`\r
+\r
+### 请求头参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| deviceIccid | String | 是 | 设备物联网卡号 | "12345678901234567890" |\r
+| timestamp | String | 是 | 毫秒时间戳 | "1640995200000" |\r
+| nonce | String | 是 | 随机数,用于防重放攻击 | "abc123" |\r
+| signature | String | 是 | 签名,用于验证请求合法性 | "a1b2c3d4..." |\r
+| deviceModel | String | 否 | 设备型号,默认值: "abmybox_1" | "abmybox_1" |\r
+| deviceVersion | String | 否 | 设备固件版本,默认值: "1.0.0" | "1.0.0" |\r
+\r
+### 签名算法\r
+签名使用 HMAC-SHA256 算法生成,具体步骤如下:\r
+\r
+1. **构建签名数据**:将参数按以下格式拼接\r
+   ```\r
+   signData = deviceId + ":" + deviceIccid + ":" + timestamp + ":" + nonce\r
+   ```\r
+\r
+2. **生成签名**:使用预共享密钥对签名数据进行 HMAC-SHA256 加密\r
+   ```\r
+   signature = HMAC-SHA256(signSecret, signData)\r
+   ```\r
+\r
+3. **转换格式**:将签名结果转换为十六进制字符串(小写)\r
+\r
+### 签名验证规则\r
+- 时间戳有效性:请求时间戳与服务器时间差不能超过5分钟\r
+- 签名正确性:使用相同算法生成的签名必须与请求中的签名一致\r
+- 防重放攻击:建议客户端每次请求使用不同的随机数\r
+- 测试签名密钥:vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus=\r
+\r
+\r
+### 响应数据\r
+**成功响应**:\r
+```json\r
+{\r
+  "errorCode": null,\r
+  "message": null,\r
+  "data": {\r
+    "deviceId": "DEVICE_001",\r
+    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",\r
+    "expireTime": 2592000\r
+  }\r
+}\r
+```\r
+\r
+**失败响应**:\r
+```json\r
+{\r
+  "errorCode": "INVALID_SIGNATURE",\r
+  "message": "签名验证失败",\r
+  "data": null\r
+}\r
+```\r
+\r
+### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| deviceId | String | 设备ID |\r
+| token | String | 设备访问令牌(JWT格式) |\r
+| expireTime | Long | 令牌过期时间(秒,默认30天) |\r
+\r
+### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| INVALID_SIGNATURE | 签名验证失败 |\r
+| SERVER_ERROR | 服务器内部错误 |\r
+\r
+---\r
+\r
+## 2. 语音对话接口\r
+\r
+### 时序图\r
+\r
+```mermaid\r
+sequenceDiagram\r
+    participant Device as 设备端\r
+    participant Server as 服务端\r
+    \r
+    Note over Device, Server: 流式语音对话流程\r
+    \r
+    Device->>Device: 录制用户语音\r
+    Device->>Device: 转换为MP3格式\r
+    \r
+    Device->>Server: POST /v1/speechChat/chatStream<br/>Headers: deviceId, deviceToken, Accept: text/event-stream<br/>Form: audio file\r
+    \r
+    alt Token验证成功\r
+        Server->>Server: 验证设备Token\r
+        Server->>Server: 建立SSE连接\r
+        Server->>Server: 语音识别(ASR)\r
+        Server->>Server: 意图识别\r
+        \r
+        loop 流式响应生成\r
+            Server->>Server: 生成回复片段\r
+            Server->>Server: 文本转语音(TTS)\r
+            Server->>Device: SSE事件: 回复片段<br/>{"audioUrl", "textContent", "intention"}\r
+            Device->>Device: 播放回复音频片段\r
+            Device->>Device: 显示文本片段\r
+        end\r
+        \r
+        Server->>Device: SSE连接关闭(对话完成)\r
+        \r
+        alt 识别到哄睡意图\r
+            Note over Device: intention = "StartSleepMode"\r
+            Device->>Device: 准备进入哄睡模式\r
+        end\r
+        \r
+    else Token验证失败\r
+        Server->>Device: 返回认证错误<br/>{"errorCode": "AUTH_FAILURE"}\r
+        Device->>Device: 重新获取Token\r
+    end\r
+```\r
+\r
+### 接口描述\r
+流式语音对话接口,接收音频文件并通过SSE(Server-Sent Events)流式返回对话结果。\r
+\r
+### 请求信息\r
+- **URL**: `/v1/speechChat/chatStream`\r
+- **方法**: `POST`\r
+- **Content-Type**: `multipart/form-data`\r
+- **响应类型**: `text/event-stream` (SSE流式响应)\r
+\r
+### 认证要求\r
+该接口需要设备Token认证,有以下两种方式传递Token:\r
+\r
+1. **请求头方式**:\r
+   ```\r
+   deviceToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\r
+   ```\r
+\r
+2. **查询参数方式**:\r
+   ```\r
+   /v1/speechChat/chatStream?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\r
+   ```\r
+\r
+### 请求头参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |\r
+| Accept | String | 是 | 必须设置为text/event-stream | "text/event-stream" |\r
+| Cache-Control | String | 建议 | 建议设置为no-cache | "no-cache" |\r
+\r
+### 请求参数\r
+| 参数名 | 类型 | 必填 | 描述 | 限制 |\r
+|--------|------|------|------|------|\r
+| file | MultipartFile | 是 | 音频文件 | MP3格式,压缩前应为采样率16k或8k单声道的PCM |\r
+\r
+### Token认证规则\r
+- Token必须是有效的JWT格式\r
+- Token必须包含设备ID、设备型号、设备版本等信息\r
+- Token不能过期(默认有效期30天)\r
+- 请求头中的设备ID不能为空\r
+- 设备ID必须与Token中的设备ID一致\r
+\r
+### SSE流式响应数据\r
+**回复片段事件**:\r
+```\r
+data: {"errorCode":null,"message":null,"data":{"audioUrl":"https://example.com/audio/chunk_001.mp3","textContent":"好的,","intention":null}}\r
+\r
+data: {"errorCode":null,"message":null,"data":{"audioUrl":"https://example.com/audio/chunk_002.mp3","textContent":"我来为你","intention":null}}\r
+\r
+data: {"errorCode":null,"message":null,"data":{"audioUrl":"https://example.com/audio/chunk_003.mp3","textContent":"讲一个故事","intention":"StartSleepMode"}}\r
+```\r
+\r
+**失败响应(认证失败)**:\r
+```json\r
+{\r
+  "errorCode": "AUTH_FAILURE",\r
+  "message": "设备Token无效",\r
+  "data": null\r
+}\r
+```\r
+\r
+**失败响应(服务异常)**:\r
+```json\r
+{\r
+  "errorCode": "SERVER_ERROR",\r
+  "message": "服务器内部错误",\r
+  "data": null\r
+}\r
+```\r
+\r
+### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| audioUrl | String | 回复音频片段文件URL |\r
+| textContent | String | 回复的文本片段内容 |\r
+| intention | String | 用户意图识别结果,通常在第一个片段中返回,可能的值:StartSleepMode(哄我睡觉)等 |\r
+\r
+### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) |\r
+| SERVER_ERROR | 服务器内部错误 |\r
+\r
+### SSE协议说明\r
+- 接口使用SSE(Server-Sent Events)协议进行流式响应\r
+- 每个回复片段作为一个独立的SSE事件发送\r
+- 客户端需要实现SSE客户端来接收流式数据\r
+- 连接会在对话生成完成后自动关闭\r
+- 客户端应当设置适当的超时时间(建议60秒)\r
+\r
+### 音频数据处理说明\r
+- 返回的 `audioUrl` 字段是音频片段文件URL\r
+- 音频格式通常为MP3格式\r
+- 客户端需要将音频文件URL下载后播放\r
+- 建议客户端实现音频片段的缓存和顺序播放机制\r
+\r
+### 用户意图说明\r
+流式语音对话接口会识别用户的意图,目前支持的意图包括:\r
+- **StartSleepMode**:用户表达想要听故事或哄睡的意图\r
+- 更多意图类型会在后续版本中添加\r
+\r
+用户意图通常在最后一个或几个SSE事件中返回,客户端应当:\r
+- 监听所有SSE事件中的intention字段\r
+- 当收到非null的intention时,根据意图类型进行相应处理\r
+- 收到StartSleepMode意图时,可以调用故事相关的API接口\r
+\r
+### 使用说明\r
+- 该接口适用于需要实时响应的语音对话场景\r
+- 客户端可以边接收边播放,实现流式播放体验\r
+- 相比普通语音对话接口,流式接口能提供更自然的交互体验\r
+- 建议在网络状况良好的环境下使用,以确保音频片段的连续播放\r
+- 对话生成完成后,SSE连接会自动关闭\r
+\r
+\r
+\r
+## 3. 故事相关接口\r
+\r
+### 时序图\r
+\r
+```mermaid\r
+sequenceDiagram\r
+    participant Device as 设备端\r
+    participant Server as 服务端\r
+    \r
+    Note over Device, Server: 哄睡故事完整流程\r
+    \r
+    %% 意图触发\r
+    Device->>Server: 语音对话识别到哄睡意图<br/>intention = "StartSleepMode"\r
+    \r
+    %% 故事选择分支\r
+    alt 用户指定故事类型\r
+        Device->>Device: 录制用户偏好语音\r
+        Device->>Server: POST /v1/story/chooseStory<br/>Headers: Accept: text/event-stream<br/>Form: audio file\r
+    else 随机故事\r
+        Device->>Server: POST /v1/story/randomStory<br/>Headers: Accept: text/event-stream<br/>Body: {"storyTheme": "可选主题"}\r
+    end\r
+    \r
+    %% SSE流式响应\r
+    Server->>Server: 建立SSE连接\r
+    Server->>Server: 分析语音/生成故事主题\r
+    Server->>Server: 开始生成故事片段\r
+    \r
+    loop 故事片段流式推送\r
+        Server->>Server: 生成故事片段文本\r
+        Server->>Server: 文本转语音\r
+        Server->>Device: SSE事件: 故事片段<br/>{"storyContent", "storyAudioUrl", "storyTheme"}\r
+        Device->>Device: 播放音频片段\r
+        Device->>Device: 显示文本内容\r
+    end\r
+    \r
+    Server->>Device: SSE连接关闭(故事完成)\r
+    \r
+    %% 退出确认流程\r
+    Device->>Server: POST /v1/story/getExitSleepInquiry<br/>获取退出询问\r
+    Server->>Device: 返回退出询问<br/>{"audioContent", "textContent"}\r
+    Device->>Device: 播放退出询问音频\r
+    \r
+    Device->>Device: 等待用户确认\r
+    alt 用户确认退出\r
+        Device->>Server: POST /v1/story/finishSleepMode<br/>确认结束哄睡\r
+        Server->>Device: 返回成功响应\r
+        Device->>Device: 退出哄睡模式\r
+    else 用户继续\r
+        Note over Device: 继续哄睡模式或重新开始故事\r
+    end\r
+```\r
+\r
+### 3.1 智能选择故事\r
+\r
+#### 接口描述\r
+智能选择故事接口。用户通过语音表达故事类型偏好,系统会识别故事主题并流式返回相应的故事内容。该接口使用SSE(Server-Sent Events)协议进行流式响应,需要设备Token认证。\r
+\r
+#### 请求信息\r
+- **URL**: `/v1/story/chooseStory`\r
+- **方法**: `POST`\r
+- **Content-Type**: `multipart/form-data`\r
+- **响应类型**: `text/event-stream` (SSE流式响应)\r
+\r
+#### 认证要求\r
+该接口需要设备Token认证,Token通过请求头或查询参数传递。\r
+\r
+#### 请求头参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |\r
+| Accept | String | 是 | 必须设置为text/event-stream | "text/event-stream" |\r
+| Cache-Control | String | 建议 | 建议设置为no-cache | "no-cache" |\r
+\r
+#### 请求表单参数\r
+| 参数名 | 类型 | 必填 | 描述 | 限制 |\r
+|--------|------|------|------|------|\r
+| file | MultipartFile | 是 | 语音文件 | MP3格式,压缩前应为采样率16k或8k单声道的PCM |\r
+\r
+\r
+#### SSE流式响应数据\r
+**故事片段事件**:\r
+```\r
+data: {"errorCode":null,"message":null,"data":{"storyTheme":"童话故事","storyContent":"从前有一个可爱的小女孩","storyAudioUrl":"https://example.com/story/audio/chunk_001.mp3","storyDuration":null}}\r
+\r
+data: {"errorCode":null,"message":null,"data":{"storyTheme":"童话故事","storyContent":"她住在森林深处的小木屋里","storyAudioUrl":"https://example.com/story/audio/chunk_002.mp3","storyDuration":null}}\r
+\r
+data: {"errorCode":null,"message":null,"data":{"storyTheme":"童话故事","storyContent":"每天都会去采集美丽的花朵","storyAudioUrl":"https://example.com/story/audio/chunk_003.mp3","storyDuration":null}}\r
+```\r
+\r
+**错误响应(认证失败)**:\r
+```json\r
+{\r
+  "errorCode": "AUTH_FAILURE",\r
+  "message": "设备Token无效",\r
+  "data": null\r
+}\r
+```\r
+\r
+**错误响应(故事生成失败)**:\r
+```json\r
+{\r
+  "errorCode": "SERVER_ERROR",\r
+  "message": "故事生成失败",\r
+  "data": null\r
+}\r
+```\r
+\r
+#### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| storyTheme | String | 故事主题 |\r
+| storyContent | String | 故事片段文本内容 |\r
+| storyAudioUrl | String | 故事片段音频文件URL |\r
+| storyDuration | Integer | 故事片段时长(秒),流式响应中通常为null |\r
+\r
+#### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) |\r
+| SERVER_ERROR | 服务器内部错误 |\r
+\r
+#### SSE协议说明\r
+- 接口使用SSE(Server-Sent Events)协议进行流式响应\r
+- 每个故事片段作为一个独立的SSE事件发送\r
+- 客户端需要实现SSE客户端来接收流式数据\r
+- 连接会在故事生成完成后自动关闭\r
+- 客户端应当设置适当的超时时间(建议120秒)\r
+\r
+#### 使用说明\r
+- 用户可以通过语音表达故事偏好,如"我想听童话故事"、"讲个冒险故事吧"等\r
+- 系统会分析语音内容,识别故事主题,并实时生成故事片段\r
+- 每个故事片段包含文本内容和对应的音频URL\r
+- 客户端可以边接收边播放,实现流式播放体验\r
+- 故事生成完成后,SSE连接会自动关闭\r
+\r
+### 3.2 随机故事\r
+\r
+#### 接口描述\r
+随机故事接口。系统会随机生成一个故事并流式返回给用户。该接口使用SSE(Server-Sent Events)协议进行流式响应,需要设备Token认证。\r
+\r
+#### 请求信息\r
+- **URL**: `/v1/story/randomStory`\r
+- **方法**: `POST`\r
+- **Content-Type**: `application/json`\r
+- **响应类型**: `text/event-stream` (SSE流式响应)\r
+\r
+#### 认证要求\r
+该接口需要设备Token认证,Token通过请求头或查询参数传递。\r
+\r
+#### 请求头参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |\r
+| Accept | String | 是 | 必须设置为text/event-stream | "text/event-stream" |\r
+| Cache-Control | String | 建议 | 建议设置为no-cache | "no-cache" |\r
+\r
+\r
+#### 请求体参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| storyTheme | String | 否 | 故事主题 | "爱情故事" |\r
+\r
+\r
+示例:\r
+```json\r
+{\r
+  "storyTheme": "爱情故事"\r
+}\r
+```\r
+\r
+\r
+#### SSE流式响应数据\r
+**故事片段事件**:\r
+```\r
+data: {"errorCode":null,"message":null,"data":{"storyTheme":"冒险故事","storyContent":"在一个阳光明媚的早晨","storyAudioUrl":"https://example.com/story/audio/chunk_001.mp3","storyDuration":null}}\r
+\r
+data: {"errorCode":null,"message":null,"data":{"storyTheme":"冒险故事","storyContent":"小兔子决定去探险","storyAudioUrl":"https://example.com/story/audio/chunk_002.mp3","storyDuration":null}}\r
+\r
+data: {"errorCode":null,"message":null,"data":{"storyTheme":"冒险故事","storyContent":"它背上小书包,踏上了未知的旅程","storyAudioUrl":"https://example.com/story/audio/chunk_003.mp3","storyDuration":null}}\r
+```\r
+\r
+**错误响应(认证失败)**:\r
+```json\r
+{\r
+  "errorCode": "AUTH_FAILURE",\r
+  "message": "设备Token无效",\r
+  "data": null\r
+}\r
+```\r
+\r
+**错误响应(故事生成失败)**:\r
+```json\r
+{\r
+  "errorCode": "SERVER_ERROR",\r
+  "message": "故事生成失败",\r
+  "data": null\r
+}\r
+```\r
+\r
+#### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| storyTheme | String | 故事主题 |\r
+| storyContent | String | 故事片段文本内容 |\r
+| storyAudioUrl | String | 故事片段音频文件URL |\r
+| storyDuration | Integer | 故事片段时长(秒),流式响应中通常为null |\r
+\r
+#### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) |\r
+| SERVER_ERROR | 服务器内部错误 |\r
+\r
+#### SSE协议说明\r
+- 接口使用SSE(Server-Sent Events)协议进行流式响应\r
+- 每个故事片段作为一个独立的SSE事件发送\r
+- 客户端需要实现SSE客户端来接收流式数据\r
+- 连接会在故事生成完成后自动关闭\r
+- 客户端应当设置适当的超时时间(建议120秒)\r
+\r
+#### 使用说明\r
+- 该接口不需要任何参数,系统会自动生成随机主题的故事\r
+- 适用于用户没有特定偏好,想要听任意故事的场景\r
+- 可以作为默认的故事推荐方式\r
+- 客户端可以边接收边播放,实现流式播放体验\r
+- 故事生成完成后,SSE连接会自动关闭\r
+\r
+### 3.3 获取退出哄睡询问\r
+\r
+#### 接口描述\r
+获取退出哄睡询问接口。在用户要结束哄睡前,返回询问内容用于确认用户是否真的要退出哄睡模式。该接口需要设备Token认证。\r
+\r
+#### 请求信息\r
+- **URL**: `/v1/story/getExitSleepInquiry`\r
+- **方法**: `POST`\r
+- **Content-Type**: `application/json`\r
+\r
+#### 认证要求\r
+该接口需要设备Token认证,Token通过请求头或查询参数传递。\r
+\r
+#### 请求头参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |\r
+\r
+#### 请求参数\r
+无请求参数\r
+\r
+#### 响应数据\r
+**成功响应**:\r
+```json\r
+{\r
+  "errorCode": null,\r
+  "message": null,\r
+  "data": {\r
+    "audioUrl": "https://example.com/audio/response.mp3",\r
+    "textContent": "你确定要退出哄睡模式吗?如果确定要退出,请说确定",\r
+    "intention": "EXIT_SLEEP_MODE"\r
+  }\r
+}\r
+```\r
+\r
+**失败响应(认证失败)**:\r
+```json\r
+{\r
+  "errorCode": "AUTH_FAILURE",\r
+  "message": "设备Token无效",\r
+  "data": null\r
+}\r
+```\r
+\r
+#### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| audioUrl | String | 询问音频文件URL |\r
+| textContent | String | 询问的文本内容 |\r
+| intention | String | 用户意图类型,此接口固定返回"EXIT_SLEEP_MODE" |\r
+\r
+#### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) |\r
+| SERVER_ERROR | 服务器内部错误 |\r
+\r
+#### 使用说明\r
+- 该接口用于获取退出哄睡模式的确认询问\r
+- 返回的音频和文本内容用于向用户确认是否真的要退出哄睡模式\r
+- 设备播放询问内容后,应等待用户的确认回复\r
+- 如果用户确认退出,再调用`finishSleepMode`接口正式结束哄睡模式\r
+\r
+### 3.4 确认结束哄睡模式\r
+\r
+#### 接口描述\r
+结束哄睡模式接口。当用户完成哄睡故事播放或主动结束哄睡模式时,调用此接口通知服务器。该接口需要设备Token认证。\r
+\r
+#### 请求信息\r
+- **URL**: `/v1/story/finishSleepMode`\r
+- **方法**: `POST`\r
+- **Content-Type**: `application/json`\r
+\r
+#### 认证要求\r
+该接口需要设备Token认证,Token通过请求头或查询参数传递。\r
+\r
+#### 请求头参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |\r
+\r
+#### 请求参数\r
+无请求参数\r
+\r
+#### 响应数据\r
+**成功响应**:\r
+```json\r
+{\r
+  "errorCode": null,\r
+  "message": null,\r
+  "data": ""\r
+}\r
+```\r
+\r
+**失败响应(认证失败)**:\r
+```json\r
+{\r
+  "errorCode": "AUTH_FAILURE",\r
+  "message": "设备Token无效",\r
+  "data": null\r
+}\r
+```\r
+\r
+#### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| data | String |  |\r
+\r
+#### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) |\r
+| SERVER_ERROR | 服务器内部错误 |\r
+\r
+#### 使用说明\r
+- 该接口用于通知服务器哄睡模式已结束\r
+- 服务器可以记录哄睡会话的结束时间和统计信息\r
+- 调用此接口后,服务器可能会停止推送哄睡相关的消息\r
+- 建议在故事播放完成或用户主动停止时调用此接口\r
+\r
+### 故事接口使用流程\r
+1. **意图识别**:用户通过语音对话接口表达哄我睡觉的意图,系统返回`StartSleepMode`意图\r
+2. **故事选择**:\r
+   - 如果用户有特定偏好,调用`chooseStory`接口,传入用户的语音描述,获取对应主题的故事流\r
+   - 如果用户没有特定偏好,调用`randomStory`接口获取随机故事流\r
+3. **SSE连接建立**:客户端建立SSE连接,准备接收流式故事数据\r
+4. **流式故事播放**:\r
+   - 接收到每个故事片段事件后,立即播放对应的音频文件\r
+   - 可以同时显示文本内容,实现边听边看的体验\r
+   - 继续接收后续故事片段,直到故事完整播放完毕\r
+5. **连接关闭**:故事生成完成后,SSE连接自动关闭\r
+6. **退出询问**:故事播放完成或用户主动结束时,调用`getExitSleepInquiry`获取二次确认询问\r
+7. **播放询问**:设备播放退出询问的音频内容,等待用户确认\r
+8. **确认结束**:用户确认后,调用`finishSleepMode`接口通知服务器正式结束哄睡模式\r
+\r
+### SSE客户端实现要点\r
+- **请求头设置**:必须设置`Accept: text/event-stream`和`Cache-Control: no-cache`\r
+- **连接超时**:建议设置120秒超时时间,因为故事生成可能需要较长时间\r
+- **事件解析**:正确解析SSE事件格式,提取`data:`字段中的JSON数据\r
+- **错误处理**:处理连接失败、超时、认证失败等异常情况\r
+- **资源管理**:连接完成后正确释放资源,避免内存泄漏\r
+\r
+\r
+---\r
+\r
+## 4. 设备信息接口\r
+\r
+### 时序图\r
+\r
+```mermaid\r
+sequenceDiagram\r
+    participant Device as 设备端\r
+    participant Server as 服务端\r
+    \r
+    Note over Device, Server: 设备信息管理流程\r
+    \r
+    %% 获取设备角色信息\r
+    Device->>Server: GET /v1/device/getCharacter<br/>Headers: deviceId, deviceToken\r
+    \r
+    alt 首次开机\r
+        Server->>Device: 返回默认角色信息<br/>{"characterId", "characterName", "characterAvatar", "emotionEmojis"}\r
+        Device->>Device: 应用默认角色配置\r
+    else 已有角色配置\r
+        Server->>Device: 返回用户选择的角色信息\r
+        Device->>Device: 应用角色配置\r
+    end\r
+    \r
+    %% 检查用户绑定状态\r
+    Device->>Server: GET /v1/device/getUserId<br/>获取绑定用户ID\r
+    \r
+    alt 设备已绑定用户\r
+        Server->>Device: 返回用户ID<br/>{"data": "user_001"}\r
+        Device->>Device: 正常运行模式\r
+    else 设备未绑定\r
+        Server->>Device: 返回空值<br/>{"data": null}\r
+        Device->>Device: 显示绑定二维码\r
+        \r
+        %% 生成绑定二维码\r
+        Device->>Server: POST /v1/qrcode/generate/image<br/>{"content": "绑定链接", "width": 300, "height": 300}\r
+        Server->>Device: 返回二维码图片(PNG)\r
+        Device->>Device: 显示二维码等待绑定\r
+    end\r
+```\r
+\r
+### 4.1 获取设备角色信息\r
+\r
+#### 接口描述\r
+获取设备角色信息接口。首次开机时返回默认角色,否则返回小程序选择的角色信息。该接口需要设备Token认证。\r
+\r
+#### 请求信息\r
+- **URL**: `/v1/device/getCharacter`\r
+- **方法**: `GET`\r
+- **Content-Type**: `application/json`\r
+\r
+#### 认证要求\r
+该接口需要设备Token认证,Token通过请求头传递。\r
+\r
+#### 请求头参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |\r
+\r
+#### 响应数据\r
+**成功响应**:\r
+```json\r
+{\r
+  "errorCode": null,\r
+  "message": null,\r
+  "data": {\r
+    "characterId": "char_001",\r
+    "characterName": "小艾",\r
+    "characterAvatar": {\r
+      "hairColor": "orange",\r
+      "eyesColor": "blue",\r
+      "hairStyle": "long-hair",\r
+      "screenImage": "https://example.com/avatar.jpg",\r
+      "screenImageAsEmoji": false\r
+    },\r
+    "emotionEmojis": [\r
+      {\r
+        "emotion": "happy",\r
+        "emojiUrl": "https://example.com/emoji/happy.png"\r
+      },\r
+      {\r
+        "emotion": "sad",\r
+        "emojiUrl": "https://example.com/emoji/sad.png"\r
+      }\r
+    ]\r
+  }\r
+}\r
+```\r
+\r
+**失败响应**:\r
+```json\r
+{\r
+  "errorCode": "AUTH_FAILURE",\r
+  "message": "设备Token无效",\r
+  "data": null\r
+}\r
+```\r
+\r
+#### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| characterId | String | 角色ID |\r
+| characterName | String | 角色名称 |\r
+| characterAvatar | Object | 角色形象配置 |\r
+| emotionEmojis | Array | 角色表情数组 |\r
+\r
+**characterAvatar字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 | 可选值 |\r
+|--------|------|------|--------|\r
+| hairColor | String | 发色 | orange, red, purple, green, blue, black |\r
+| eyesColor | String | 瞳色 | orange, red, purple, green, blue, yellow |\r
+| hairStyle | String | 发型 | side-parted, slicked-back, long-hair |\r
+| screenImage | String | 屏保图片URL | |\r
+| screenImageAsEmoji | Boolean | 屏保图片是否作为对话表情 | 默认false |\r
+\r
+**emotionEmoji字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| emotion | String | 心情名称 |\r
+| emojiUrl | String | 表情URL |\r
+\r
+#### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) |\r
+\r
+### 4.2 获取设备绑定用户ID\r
+\r
+#### 接口描述\r
+获取设备绑定的用户ID接口。用于检查设备是否已绑定用户,如果未绑定则需要弹出二维码供用户绑定。该接口需要设备Token认证。\r
+\r
+#### 请求信息\r
+- **URL**: `/v1/device/getUserId`\r
+- **方法**: `GET`\r
+- **Content-Type**: `application/json`\r
+\r
+#### 认证要求\r
+该接口需要设备Token认证,Token通过请求头传递。\r
+\r
+#### 请求头参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |\r
+\r
+#### 响应数据\r
+**成功响应(已绑定用户)**:\r
+```json\r
+{\r
+  "errorCode": null,\r
+  "message": null,\r
+  "data": "user_001"\r
+}\r
+```\r
+\r
+**成功响应(未绑定用户)**:\r
+```json\r
+{\r
+  "errorCode": null,\r
+  "message": null,\r
+  "data": null\r
+}\r
+```\r
+\r
+**失败响应**:\r
+```json\r
+{\r
+  "errorCode": "AUTH_FAILURE",\r
+  "message": "设备Token无效",\r
+  "data": null\r
+}\r
+```\r
+\r
+#### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| data | String | 用户ID,如果设备未绑定用户则返回null |\r
+\r
+#### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| AUTH_FAILURE | 认证失败(设备ID为空或Token无效) |\r
+\r
+---\r
+\r
+## 5. OTA升级任务接口\r
+\r
+### 时序图\r
+\r
+```mermaid\r
+sequenceDiagram\r
+    participant Device as 设备端\r
+    participant Server as 服务端\r
+    \r
+    Note over Device, Server: OTA升级流程\r
+    \r
+    %% 设备启动检查升级\r
+    Device->>Device: 设备启动\r
+    Device->>Device: 获取当前固件版本\r
+    \r
+    Device->>Server: GET /v1/upgrade/getTask<br/>?deviceId=xxx&sourceVersion=1.0.0\r
+    \r
+    alt 有可用升级\r
+        Server->>Server: 检查升级策略\r
+        Server->>Server: 查找可用升级包\r
+        Server->>Device: 返回升级任务<br/>{"updateId", "targetVersion", "packageUrl", "packageMd5", "updateType"}\r
+        \r
+        Device->>Device: 检查升级类型\r
+        \r
+        alt 强制升级 (updateType=1)\r
+            Device->>Device: 立即开始升级\r
+        else 可选升级 (updateType=0)\r
+            Device->>Device: 提示用户选择\r
+            alt 用户同意升级\r
+                Device->>Device: 开始升级\r
+            else 用户拒绝\r
+                Device->>Device: 继续正常运行\r
+            end\r
+        end\r
+        \r
+        %% 执行升级\r
+        opt 开始升级流程\r
+            Device->>Device: 下载升级包\r
+            Device->>Server: 下载 packageUrl\r
+            Server->>Device: 返回升级包文件\r
+            \r
+            Device->>Device: 校验MD5\r
+            alt MD5校验成功\r
+                Device->>Device: 执行固件升级\r
+                Device->>Device: 重启设备\r
+                Device->>Device: 升级完成,版本更新\r
+                Device->>Server: 重新获取设备Token<br/>(新版本)\r
+            else MD5校验失败\r
+                Device->>Device: 升级失败,重新下载\r
+            end\r
+        end\r
+        \r
+    else 无可用升级\r
+        Server->>Device: 返回无升级<br/>{"errorCode": "UPGRADE_NOT_FOUND"}\r
+        Device->>Device: 继续正常运行\r
+    end\r
+```\r
+\r
+### 接口描述\r
+获取OTA升级任务接口。设备可通过此接口查询是否有可用的固件升级任务,如果有则返回升级包的详细信息。\r
+\r
+### 请求信息\r
+- **URL**: `/v1/upgrade/getTask`\r
+- **方法**: `GET`\r
+- **Content-Type**: `application/json`\r
+\r
+### 请求参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符 | "DEVICE_001" |\r
+| sourceVersion | String | 是 | 设备当前固件版本 | "1.0.0" |\r
+\r
+### 响应数据\r
+**成功响应(有升级任务)**:\r
+```json\r
+{\r
+  "errorCode": null,\r
+  "message": null,\r
+  "data": {\r
+    "updateId": 123,\r
+    "sourceVersion": "1.0.0",\r
+    "targetVersion": "1.1.0",\r
+    "updateType": 1,\r
+    "packageId": 456,\r
+    "packageUrl": "https://example.com/firmware/update_1.1.0.bin",\r
+    "packageMd5": "a1b2c3d4e5f6789012345678901234567890",\r
+    "packageSize": 1048576,\r
+    "description": "升级描述"\r
+  }\r
+}\r
+```\r
+\r
+**成功响应(无升级任务)**:\r
+```json\r
+{\r
+  "errorCode": "UPGRADE_NOT_FOUND",\r
+  "message": "已是最新版本",\r
+  "data": null\r
+}\r
+```\r
+\r
+**失败响应**:\r
+```json\r
+{\r
+  "errorCode": "SERVER_ERROR",\r
+  "message": "服务器异常",\r
+  "data": null\r
+}\r
+```\r
+\r
+### 响应字段说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| updateId | Integer | 升级任务ID |\r
+| sourceVersion | String | 源版本号 |\r
+| targetVersion | String | 目标版本号 |\r
+| updateType | Integer | 升级类型,1:强制升级,0:可选升级 |\r
+| packageId | Integer | 升级包ID |\r
+| packageUrl | String | 升级包下载URL |\r
+| packageMd5 | String | 升级包MD5校验值 |\r
+| packageSize | Long | 升级包大小(字节) |\r
+| description | String | 升级描述 |\r
+\r
+### 错误码\r
+| 错误码 | 描述 |\r
+|--------|------|\r
+| UPGRADE_NOT_FOUND | 已是最新版本 |\r
+| SERVER_ERROR | 服务器异常 |\r
+\r
+### 升级流程说明\r
+1. 设备启动时,调用此接口检查是否有可用的升级任务\r
+2. 如果有升级任务,获取升级包URL和校验信息\r
+3. 设备下载升级包,使用MD5校验文件完整性\r
+4. 校验通过后,设备执行固件升级操作\r
+5. 升级完成后,设备重启并重新获取设备token\r
+\r
+---\r
+\r
+## 6. WebSocket连接端点\r
+\r
+### 时序图\r
+\r
+```mermaid\r
+sequenceDiagram\r
+    participant Device as 设备端\r
+    participant Server as 服务端\r
+    participant MiniProgram as 小程序端\r
+    \r
+    Note over Device,MiniProgram: WebSocket通信流程\r
+    \r
+    %% 建立连接\r
+    Device->>Server: WebSocket连接请求<br/>Headers: deviceId, deviceToken\r
+    \r
+    alt Token验证成功\r
+        Server->>Device: 连接成功(101)\r
+        Server->>Server: 记录设备会话\r
+        \r
+        %% 心跳保持\r
+        loop 心跳循环\r
+            Device->>Server: ping\r
+            Server->>Device: pong\r
+        end\r
+        \r
+        %% 业务消息推送示例\r
+        par 角色切换\r
+            MiniProgram->>Server: 用户在小程序切换角色\r
+            Server->>Device: WebSocket推送<br/>{"command": "CharacterChange", "data": {...}}\r
+            Device->>Device: 应用新角色配置\r
+        and 哄睡模式控制\r
+            MiniProgram->>Server: 用户启动哄睡模式\r
+            Server->>Device: WebSocket推送<br/>{"command": "SleepModeStart", "data": {"storyTheme": "..."}}\r
+            Device->>Device: 启动哄睡模式\r
+        and 设备绑定通知\r
+            MiniProgram->>Server: 用户绑定设备\r
+            Server->>Device: WebSocket推送<br/>{"command": "DeviceBind", "data": {"userId": "...", "userName": "..."}}\r
+            Device->>Device: 更新绑定状态\r
+        and 日程提醒\r
+            Server->>Device: WebSocket推送<br/>{"command": "ScheduleReminder", "data": {"scheduleName": "...", "reminderContent": "..."}}\r
+            Device->>Device: 播放提醒内容\r
+        end\r
+        \r
+    else Token验证失败\r
+        Server->>Device: 连接拒绝(401)\r
+        Device->>Device: 重新获取Token后重连\r
+    end\r
+    \r
+    %% 连接断开\r
+    alt 正常断开\r
+        Device->>Server: 关闭连接\r
+        Server->>Server: 清理设备会话\r
+    else 异常断开\r
+        Server->>Server: 检测到连接断开\r
+        Server->>Server: 清理设备会话\r
+        Device->>Device: 自动重连机制\r
+    end\r
+```\r
+\r
+### 接口描述\r
+WebSocket连接端点,用于设备与服务器建立实时双向通信连接。该连接需要设备Token认证。\r
+\r
+### 连接信息\r
+- **URL**: `wss://ambybox-api.test.leiniao.com/ambybox-device-api/v1/websocket`\r
+- **协议**: `WebSocket`\r
+- **认证**: 设备Token认证\r
+\r
+### 连接认证\r
+WebSocket连接需要设备Token认证,有以下两种方式传递Token:\r
+\r
+1. **请求头方式**:\r
+   ```\r
+   deviceId: DEVICE_001\r
+   deviceToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\r
+   reconnect: 0\r
+   ```\r
+\r
+2. **查询参数方式**:\r
+   ```\r
+   wss://ambybox-api.test.leiniao.com/ambybox-device-api/v1/websocket?deviceToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\r
+   ```\r
+\r
+### 连接参数\r
+| 参数名 | 类型 | 必填 | 描述 | 示例 |\r
+|--------|------|------|------|------|\r
+| deviceId | String | 是 | 设备唯一标识符(请求头) | "DEVICE_001" |\r
+| deviceToken | String | 是 | 设备访问令牌 | "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." |\r
+| reconnect | Integer | 否 | 重连次数,设备启动时设置为0,断线重连递增 | 0 |\r
+\r
+### 连接状态码\r
+| 状态码 | 描述 |\r
+|--------|------|\r
+| 400 | 设备ID为空 |\r
+| 401 | 设备Token无效或过期 |\r
+| 101 | 正常连接 |\r
+\r
+\r
+### 连接流程\r
+1. 客户端携带设备Token发起WebSocket连接\r
+2. 服务器验证Token有效性和设备ID匹配性\r
+3. 连接建立成功,设备会话被管理器记录\r
+4. 客户端可发送心跳消息保持连接活跃\r
+5. 连接断开时,设备会话从管理器中移除\r
+\r
+### 错误处理\r
+- **认证失败**:Token无效或设备ID不匹配,连接被拒绝\r
+- **传输错误**:连接出现异常时,服务器会主动关闭连接\r
+- **心跳超时**:长时间无消息交互时,连接可能被服务器关闭\r
+\r
+\r
+\r
+### 客户端心跳消息\r
+**客户端发送心跳**:\r
+```\r
+ping\r
+```\r
+\r
+**服务器响应心跳**:\r
+```\r
+pong\r
+```\r
+\r
+### 服务端业务消息\r
+\r
+服务端通过WebSocket连接推送业务消息给设备端,消息采用JSON格式,消息结构如下:\r
+\r
+```json\r
+{\r
+  "deviceId": "设备ID",\r
+  "timestamp": 1640995200,\r
+  "messageId": "消息ID,用于设备去重防重放",\r
+  "command": "命令名称",\r
+  "data": {\r
+    // 具体的消息数据,根据不同命令名称而变化\r
+  }\r
+}\r
+```\r
+\r
+#### 消息结构说明\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| deviceId | String | 设备唯一标识符 |\r
+| timestamp | Long | 时间戳,单位:秒 |\r
+| messageId | String | 消息ID,用于设备去重防重放 |\r
+| command | String | 命令名称,见下方支持的命令列表 |\r
+| data | Object | 消息体,不同命令对应不同的数据结构 |\r
+\r
+支持的命令名称如下:\r
+\r
+#### 1. 角色切换 (CharacterChange)\r
+**命令描述**:通知设备切换到指定角色\r
+**消息示例**:\r
+```json\r
+{\r
+  "deviceId": "DEVICE_001",\r
+  "timestamp": 1640995200,\r
+  "messageId": "msg_001",\r
+  "command": "CharacterChange",\r
+  "data": {\r
+    "characterId": "char_001",\r
+    "characterName": "小艾",\r
+    "characterAvatar": {\r
+      "hairColor": "orange",\r
+      "eyesColor": "blue",\r
+      "hairStyle": "long-hair",\r
+      "screenImage": "https://example.com/avatar.jpg",\r
+      "screenImageAsEmoji": false\r
+    },\r
+    "emotionEmojis": [\r
+      {\r
+        "emotion": "happy",\r
+        "emojiUrl": "https://example.com/emoji/happy.png"\r
+      },\r
+      {\r
+        "emotion": "sad",\r
+        "emojiUrl": "https://example.com/emoji/sad.png"\r
+      }\r
+    ]\r
+  }\r
+}\r
+```\r
+\r
+**data字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| characterId | String | 角色ID |\r
+| characterName | String | 角色名称 |\r
+| characterAvatar | Object | 角色形象配置 |\r
+| emotionEmojis | Array | 角色表情数组 |\r
+\r
+**characterAvatar字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 | 可选值 |\r
+|--------|------|------|--------|\r
+| hairColor | String | 发色 | orange, red, purple, green, blue, black |\r
+| eyesColor | String | 瞳色 | orange, red, purple, green, blue, yellow |\r
+| hairStyle | String | 发型 | side-parted, slicked-back, long-hair |\r
+| screenImage | String | 屏保图片URL | |\r
+| screenImageAsEmoji | Boolean | 屏保图片是否作为对话表情 | 默认false |\r
+\r
+**emotionEmoji字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| emotion | String | 心情名称 |\r
+| emojiUrl | String | 表情图片URL |\r
+\r
+#### 2. 角色配置更改 (CharacterConfigChange)\r
+**命令描述**:更新当前角色的配置信息\r
+**消息示例**:\r
+```json\r
+{\r
+  "deviceId": "DEVICE_001",\r
+  "timestamp": 1640995200,\r
+  "messageId": "msg_002",\r
+  "command": "CharacterConfigChange",\r
+  "data": {\r
+    "characterId": "char_001",\r
+    "characterName": "小艾",\r
+    "characterAvatar": {\r
+      "hairColor": "blue",\r
+      "eyesColor": "green",\r
+      "hairStyle": "side-parted",\r
+      "screenImage": "https://example.com/new_avatar.jpg",\r
+      "screenImageAsEmoji": true\r
+    },\r
+    "emotionEmojis": [\r
+      {\r
+        "emotion": "excited",\r
+        "emojiUrl": "https://example.com/emoji/excited.png"\r
+      }\r
+    ]\r
+  }\r
+}\r
+```\r
+\r
+**data字段说明**:与角色切换命令相同\r
+\r
+#### 3. 哄睡模式开始 (SleepModeStart)\r
+**命令描述**:通知设备启动哄睡模式\r
+**消息示例**:\r
+```json\r
+{\r
+  "deviceId": "DEVICE_001",\r
+  "timestamp": 1640995200,\r
+  "messageId": "msg_003",\r
+  "command": "SleepModeStart",\r
+  "data": {\r
+    "storyTheme": "爱情故事"\r
+  }\r
+}\r
+```\r
+\r
+**data字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| storyTheme | String | 故事主题 |\r
+\r
+设备收到此命令后,使用推送的主题调用randomStory接口,播放故事。\r
+\r
+\r
+#### 4. 哄睡模式结束 (SleepModeEnd)\r
+**命令描述**:通知设备结束哄睡模式\r
+**消息示例**:\r
+```json\r
+{\r
+  "deviceId": "DEVICE_001",\r
+  "timestamp": 1640995200,\r
+  "messageId": "msg_004",\r
+  "command": "SleepModeEnd",\r
+  "data": {}\r
+}\r
+```\r
+\r
+**data字段说明**:无特殊字段\r
+\r
+设备收到改命令后,需要停止播放故事。\r
+\r
+\r
+#### 6. 哄睡模式故事内容\r
+\r
+设备收到`SleepModeStart` 命令后,需要调用`/v1/story/randomStory`接口获取故事内容。\r
+\r
+\r
+\r
+#### 7. 设备绑定 (DeviceBind)\r
+**命令描述**:通知设备已被用户绑定\r
+**消息示例**:\r
+```json\r
+{\r
+  "deviceId": "DEVICE_001",\r
+  "timestamp": 1640995200,\r
+  "messageId": "msg_007",\r
+  "command": "DeviceBind",\r
+  "data": {\r
+    "userId": "user_001",\r
+    "userName": "张三",\r
+    "bindTime": 1640995200\r
+  }\r
+}\r
+```\r
+\r
+**data字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| userId | String | 用户ID |\r
+| userName | String | 用户名称 |\r
+| bindTime | Long | 绑定时间戳,单位:秒 |\r
+\r
+#### 8. 设备解绑 (DeviceUnbind)\r
+**命令描述**:通知设备已被用户解绑\r
+**消息示例**:\r
+```json\r
+{\r
+  "deviceId": "DEVICE_001",\r
+  "timestamp": 1640995200,\r
+  "messageId": "msg_008",\r
+  "command": "DeviceUnbind",\r
+  "data": {\r
+    "userId": "user_001",\r
+    "reason": "用户主动解绑",\r
+    "unbindTime": 1640995200\r
+  }\r
+}\r
+```\r
+\r
+**data字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| userId | String | 用户ID |\r
+| reason | String | 解绑原因 |\r
+| unbindTime | Long | 解绑时间戳,单位:秒 |\r
+\r
+#### 9. 日程提醒 (ScheduleReminder)\r
+**命令描述**:推送日程提醒信息\r
+**消息示例**:\r
+```json\r
+{\r
+  "deviceId": "DEVICE_001",\r
+  "timestamp": 1640995200,\r
+  "messageId": "msg_009",\r
+  "command": "ScheduleReminder",\r
+  "data": {\r
+    "scheduleId": 123,\r
+    "deviceId": "DEVICE_001",\r
+    "scheduleName": "开会提醒",\r
+    "reminderContent": "您有一个会议即将开始,请准备参加",\r
+    "reminderAudioUrl": "https://example.com/reminder/reminder_001.mp3",\r
+    "enableVoice": 1,\r
+    "enableVibration": 1\r
+  }\r
+}\r
+```\r
+\r
+**data字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| scheduleId | Integer | 日程ID |\r
+| deviceId | String | 设备ID |\r
+| scheduleName | String | 日程名称 |\r
+| reminderContent | String | 提醒内容 |\r
+| reminderAudioUrl | String | 提醒音频文件URL |\r
+| enableVoice | Integer | 是否启用语音,1:启用,0:未启用 |\r
+| enableVibration | Integer | 是否启用震动,1:启用,0:未启用 |\r
+\r
+#### 10. 日程提醒配置 (ScheduleReminderConfig)\r
+**命令描述**:配置日程提醒相关设置\r
+**消息示例**:\r
+```json\r
+{\r
+  "deviceId": "DEVICE_001",\r
+  "timestamp": 1640995200,\r
+  "messageId": "msg_010",\r
+  "command": "ScheduleReminderConfig",\r
+  "data": {\r
+      "scheduleName": "七点起床"\r
+  }\r
+}\r
+```\r
+\r
+**data字段说明**:\r
+\r
+| 字段名 | 类型 | 描述 |\r
+|--------|------|------|\r
+| scheduleName | String | 日程名称 |\r
+\r
+\r
+\r
+## 7. 二维码生成接口\r
+\r
+### 接口描述\r
+生成二维码图片接口,根据提供的内容生成PNG格式的二维码图片并直接返回图片二进制数据。该接口无需认证。\r
+\r
+### 请求信息\r
+- **URL**: `/v1/qrcode/generate/image`\r
+- **方法**: `POST`\r
+- **Content-Type**: `application/json`\r
+\r
+### 请求参数\r
+| 参数名 | 类型 | 必填 | 描述 | 限制 | 默认值 |\r
+|--------|------|------|------|------|--------|\r
+| content | String | 是 | 二维码内容 | 不能为空 | - |\r
+| width | Integer | 否 | 图片宽度 | 100-1000像素 | 300 |\r
+| height | Integer | 否 | 图片高度 | 100-1000像素 | 300 |\r
+| withLogo | Boolean | 否 | 是否包含Logo | - | false |\r
+\r
+### 请求示例\r
+```json\r
+{\r
+  "content": "https://example.com/device/bind?deviceId=DEVICE_001",\r
+  "width": 300,\r
+  "height": 300,\r
+  "withLogo": false\r
+}\r
+```\r
+\r
+### 响应数据\r
+**成功响应**:\r
+- **Content-Type**: `image/png`\r
+- **Content-Disposition**: `inline; filename=qrcode.png`\r
+- **响应体**: PNG格式的二维码图片二进制数据\r
+\r
+**失败响应**:\r
+- **400 Bad Request**: 请求参数无效(如content为空)\r
+- **500 Internal Server Error**: 服务器内部错误\r
+\r
+### 响应头说明\r
+| 头字段名 | 值 | 描述 |\r
+|---------|-----|------|\r
+| Content-Type | image/png | 响应内容类型为PNG图片 |\r
+| Content-Length | 图片字节数 | 图片文件大小 |\r
+| Content-Disposition | inline; filename=qrcode.png | 内联显示,文件名为qrcode.png |\r
+\r
+### 参数验证规则\r
+- **content**: 不能为空或空字符串\r
+- **width**: 如果提供,必须在100-1000像素范围内,否则使用默认值300\r
+- **height**: 如果提供,必须在100-1000像素范围内,否则使用默认值300\r
+- **withLogo**: 可选参数,默认为false\r
+\r
+### 使用场景\r
+此接口主要用于:\r
+1. 设备绑定:生成包含设备ID的绑定二维码\r
+2. 设备信息展示:生成设备相关信息的二维码\r
+3. 通用二维码生成:根据任意文本内容生成二维码\r
+\r
+### 客户端处理说明\r
+- 响应为二进制图片数据,可直接显示或保存\r
+- 建议客户端设置合理的超时时间\r
+- 可通过Content-Length头获取图片大小信息\r
+\r
+---\r
+\r
+## 8. 系统整体时序图\r
+\r
+### 设备完整生命周期流程\r
+\r
+```mermaid\r
+sequenceDiagram\r
+    participant Device as 设备端\r
+    participant Server as 服务端\r
+    participant User as 用户\r
+    participant MiniProgram as 小程序端\r
+    \r
+    Note over Device,MiniProgram: 设备完整生命周期流程\r
+    \r
+    %% 设备启动和认证\r
+    rect rgb(240, 248, 255)\r
+        Note over Device, Server: 1. 设备启动和认证阶段\r
+        Device->>Device: 设备开机启动\r
+        Device->>Server: 获取设备Token (签名认证)\r
+        Server->>Device: 返回JWT Token\r
+        Device->>Server: 建立WebSocket连接\r
+        Server->>Device: 连接成功\r
+    end\r
+    \r
+    %% 设备初始化\r
+    rect rgb(245, 255, 250)\r
+        Note over Device, Server: 2. 设备初始化阶段\r
+        Device->>Server: 检查OTA升级\r
+        alt 有升级\r
+            Server->>Device: 返回升级包信息\r
+            Device->>Device: 执行升级\r
+        else 无升级\r
+            Server->>Device: 无升级任务\r
+        end\r
+        \r
+        Device->>Server: 获取设备角色信息\r
+        Server->>Device: 返回角色配置\r
+        Device->>Device: 应用角色设置\r
+        \r
+        Device->>Server: 检查用户绑定状态\r
+        alt 未绑定\r
+            Server->>Device: 返回未绑定\r
+            Device->>Server: 生成绑定二维码\r
+            Server->>Device: 返回二维码图片\r
+            Device->>Device: 显示二维码等待绑定\r
+        else 已绑定\r
+            Server->>Device: 返回用户ID\r
+            Device->>Device: 进入正常运行模式\r
+        end\r
+    end\r
+    \r
+    %% 用户交互阶段\r
+    rect rgb(255, 250, 240)\r
+        Note over Device, MiniProgram: 3. 用户交互阶段\r
+        \r
+        par 语音对话\r
+            User->>Device: 语音输入\r
+            Device->>Server: 语音对话接口\r
+            Server->>Device: 返回回复和意图识别\r
+            Device->>User: 播放回复音频\r
+            \r
+            alt 识别到哄睡意图\r
+                Device->>Server: 请求故事接口(SSE)\r
+                loop 故事流式推送\r
+                    Server->>Device: 推送故事片段\r
+                    Device->>User: 播放故事音频\r
+                end\r
+                Device->>Server: 结束哄睡模式\r
+            end\r
+            \r
+        and 小程序控制\r
+            MiniProgram->>Server: 角色切换/哄睡控制\r
+            Server->>Device: WebSocket推送命令\r
+            Device->>Device: 执行相应操作\r
+            \r
+        and 系统提醒\r
+            Server->>Device: WebSocket推送日程提醒\r
+            Device->>User: 播放提醒内容\r
+        end\r
+    end\r
+    \r
+    %% 运行维护\r
+    rect rgb(248, 248, 255)\r
+        Note over Device, Server: 4. 运行维护阶段\r
+        loop 持续运行\r
+            Device->>Server: 心跳保持\r
+            Server->>Device: 心跳响应\r
+        end\r
+        \r
+        opt 定期检查升级\r
+            Device->>Server: 检查OTA升级\r
+            Server->>Device: 升级任务响应\r
+        end\r
+    end\r
+```\r
+\r
+## 9. 通用说明\r
+\r
+### 设备认证流程\r
+1. 设备首次启动时,使用签名算法调用获取设备Token接口\r
+2. 获取到Token后,后续所有API调用都需要携带该Token\r
+3. Token过期后,需要重新获取新的Token\r
+\r
+### 安全注意事项\r
+- 签名密钥和Token密钥必须安全保存,不能泄露\r
+- 时间戳验证可以防止重放攻击\r
+- 使用HTTPS协议传输,确保数据安全\r
+- WebSocket连接建议使用WSS协议确保通信安全\r
+\r
+### 开发调试\r
+- 签名算法实现时注意字符串拼接的顺序和分隔符\r
+- 时间戳必须是毫秒级别\r
+- WebSocket连接测试时注意Token的有效性\r
+- 建议实现心跳机制保持连接活跃\r
+\r
+### 时序图说明\r
+本文档包含以下时序图:\r
+1. **设备认证流程**:展示设备如何获取访问令牌\r
+2. **语音对话流程**:展示语音交互的完整过程\r
+3. **哄睡故事流程**:展示从意图识别到故事播放的完整流程\r
+4. **设备信息管理**:展示设备角色配置和用户绑定流程\r
+5. **WebSocket通信**:展示实时通信和命令推送流程\r
+6. **OTA升级流程**:展示固件升级的完整过程\r
+7. **系统整体流程**:展示设备完整生命周期的综合时序图\r
+\r
+每个时序图都详细描述了参与方之间的交互序列,有助于开发者理解系统架构和实现细节。\r