Преглед изворни кода

添加网页端流式对话支持

Rbb666 пре 2 месеци
родитељ
комит
2c5ca8abb4
5 измењених фајлова са 561 додато и 65 уклоњено
  1. 13 9
      llm.c
  2. 8 2
      llm.h
  3. 141 27
      ports/chat_port.c
  4. 139 1
      ports/llm_webnet.c
  5. 260 26
      resource/index.html

+ 13 - 9
llm.c

@@ -300,19 +300,19 @@ static void llm_run(void *p)
         {
         {
 #ifdef PKG_LLMCHAT_HISTORY_PAYLOAD
 #ifdef PKG_LLMCHAT_HISTORY_PAYLOAD
             add_message2messages(input_buffer, "user", &handle);
             add_message2messages(input_buffer, "user", &handle);
-
-            char *result = handle.get_answer(handle.messages);
-
-            add_message2messages(result, "assistant", &handle);
+            {
+                char *result = handle.get_answer(&handle, handle.messages);
+                add_message2messages(result, "assistant", &handle);
+            }
 
 
 #else
 #else
 
 
             add_message2messages(input_buffer, "user", &handle);
             add_message2messages(input_buffer, "user", &handle);
-
-            char *result = handle.get_answer(handle.messages);
-            
-            rt_free(result);
-            clear_messages(&handle);
+            {
+                char *result = handle.get_answer(&handle, handle.messages);
+                rt_free(result);
+                clear_messages(&handle);
+            }
 
 
 #endif
 #endif
         }
         }
@@ -347,12 +347,16 @@ static int llm2rtt(int argc, char **argv)
         handle.line_curpos = 0;
         handle.line_curpos = 0;
         handle.device = RT_NULL;
         handle.device = RT_NULL;
         handle.rx_indicate = RT_NULL;
         handle.rx_indicate = RT_NULL;
+        handle.stream_cb = RT_NULL;
+        handle.stream_user_data = RT_NULL;
     }
     }
 
 
     rt_sem_init(&(handle.rx_sem), "llm_rxsem", 0, RT_IPC_FLAG_FIFO);
     rt_sem_init(&(handle.rx_sem), "llm_rxsem", 0, RT_IPC_FLAG_FIFO);
 
 
     handle.argc = argc;
     handle.argc = argc;
     handle.get_answer = get_llm_answer;
     handle.get_answer = get_llm_answer;
+    handle.stream_cb = RT_NULL;
+    handle.stream_user_data = RT_NULL;
     if (!cJSON_IsArray(handle.messages))
     if (!cJSON_IsArray(handle.messages))
     {
     {
         handle.messages = cJSON_CreateArray();
         handle.messages = cJSON_CreateArray();

+ 8 - 2
llm.h

@@ -41,6 +41,8 @@ enum llm_input_stat
     LLM_WAIT_FUNC_KEY,
     LLM_WAIT_FUNC_KEY,
 };
 };
 
 
+typedef void (*llm_stream_callback_t)(const char *chunk, void *user_data);
+
 struct llm_obj
 struct llm_obj
 {
 {
     /*  thread  */
     /*  thread  */
@@ -66,15 +68,19 @@ struct llm_obj
 
 
     /*  messages */
     /*  messages */
     cJSON *messages;
     cJSON *messages;
-    char *(*get_answer)(cJSON *messages);
+    char *(*get_answer)(struct llm_obj *handle, cJSON *messages);
     void *(*send_llm_mb)(char*  input_buffer);
     void *(*send_llm_mb)(char*  input_buffer);
     rt_mailbox_t inputbuff_mb;
     rt_mailbox_t inputbuff_mb;
 
 
     rt_mailbox_t outputbuff_mb;
     rt_mailbox_t outputbuff_mb;
+
+    /* optional streaming callback */
+    llm_stream_callback_t stream_cb;
+    void *stream_user_data;
 };
 };
 typedef struct llm_obj *llm_t;
 typedef struct llm_obj *llm_t;
 
 
-char *get_llm_answer(cJSON *messages);
+char *get_llm_answer(llm_t handle, cJSON *messages);
 void add_message2messages(const char *input_buffer, char *role,llm_t handle);
 void add_message2messages(const char *input_buffer, char *role,llm_t handle);
 cJSON *create_message(const char *input_buffer, char *role);
 cJSON *create_message(const char *input_buffer, char *role);
 void clear_messages(llm_t handle);
 void clear_messages(llm_t handle);

+ 141 - 27
ports/chat_port.c

@@ -50,7 +50,67 @@ static const char *get_dynamic_api_url(void)
 static char authHeader[128] = {0};
 static char authHeader[128] = {0};
 static char responseBuffer[WEB_SOCKET_BUF_SIZE] = {0};
 static char responseBuffer[WEB_SOCKET_BUF_SIZE] = {0};
 static char contentBuffer[WEB_SOCKET_BUF_SIZE] = {0};
 static char contentBuffer[WEB_SOCKET_BUF_SIZE] = {0};
-static char allContent[WEB_SOCKET_BUF_SIZE] = {0};
+
+static rt_bool_t append_chunk_to_buffer(char **buffer,
+                                        size_t *length,
+                                        size_t *capacity,
+                                        const char *chunk)
+{
+    size_t chunk_len;
+
+    if (buffer == RT_NULL || length == RT_NULL || capacity == RT_NULL || chunk == RT_NULL)
+    {
+        return RT_FALSE;
+    }
+
+    chunk_len = rt_strlen(chunk);
+    if (chunk_len == 0)
+    {
+        return RT_TRUE;
+    }
+
+    if (*capacity < (*length + chunk_len + 1))
+    {
+        size_t new_capacity = (*capacity == 0) ? WEB_SOCKET_BUF_SIZE : *capacity;
+
+        while (new_capacity < (*length + chunk_len + 1))
+        {
+            size_t proposed = new_capacity << 1;
+            if (proposed <= new_capacity)
+            {
+                new_capacity = (*length + chunk_len + 1);
+                break;
+            }
+            new_capacity = proposed;
+        }
+
+        if (*buffer == RT_NULL)
+        {
+            *buffer = (char *)rt_malloc(new_capacity);
+            if (*buffer == RT_NULL)
+            {
+                return RT_FALSE;
+            }
+            (*buffer)[0] = '\0';
+        }
+        else
+        {
+            char *new_buf = (char *)rt_realloc(*buffer, new_capacity);
+            if (new_buf == RT_NULL)
+            {
+                return RT_FALSE;
+            }
+            *buffer = new_buf;
+        }
+
+        *capacity = new_capacity;
+    }
+
+    rt_memcpy(*buffer + *length, chunk, chunk_len);
+    *length += chunk_len;
+    (*buffer)[*length] = '\0';
+    return RT_TRUE;
+}
 
 
 /**
 /**
  * @brief: create char for request payload.
  * @brief: create char for request payload.
@@ -158,6 +218,14 @@ void clear_messages(llm_t handle)
 llm_t create_llm_t()
 llm_t create_llm_t()
 {
 {
     llm_t handle = (llm_t)rt_malloc(sizeof(struct llm_obj));
     llm_t handle = (llm_t)rt_malloc(sizeof(struct llm_obj));
+    if (handle == RT_NULL)
+    {
+        rt_kprintf("Failed to allocate llm handle.\n");
+        return RT_NULL;
+    }
+
+    rt_memset(handle, 0x00, sizeof(struct llm_obj));
+
     rt_err_t result = init_llm(handle);
     rt_err_t result = init_llm(handle);
 
 
     if (result != RT_EOK)
     if (result != RT_EOK)
@@ -217,7 +285,7 @@ void display_llm_messages(llm_t handle)
  #ifdef PKG_LLMCHAT_HISTORY_PAYLOAD
  #ifdef PKG_LLMCHAT_HISTORY_PAYLOAD
             add_message2messages(input_buffer, "user", &handle);
             add_message2messages(input_buffer, "user", &handle);
 
 
-            char *result = handle.get_answer(handle.messages);
+            char *result = handle.get_answer(&handle, handle.messages);
 
 
             add_message2messages(result, "assistant", &handle);
             add_message2messages(result, "assistant", &handle);
 
 
@@ -225,20 +293,33 @@ void display_llm_messages(llm_t handle)
 
 
             add_message2messages(input_buffer, "user", &handle);
             add_message2messages(input_buffer, "user", &handle);
 
 
-            char *result = handle.get_answer(handle.messages);
+            char *result = handle.get_answer(&handle, handle.messages);
 
 
             rt_free(result);
             rt_free(result);
             clear_messages(&handle);
             clear_messages(&handle);
  *
  *
  **/
  **/
-char *get_llm_answer(cJSON *messages)
+char *get_llm_answer(llm_t handle, cJSON *messages)
 {
 {
     struct webclient_session *webSession = RT_NULL;
     struct webclient_session *webSession = RT_NULL;
     char *payload = RT_NULL;
     char *payload = RT_NULL;
     char *result = RT_NULL;
     char *result = RT_NULL;
+    char *assembled = RT_NULL;
+    size_t assembled_len = 0;
+    size_t assembled_cap = 0;
     int bytesRead, responseStatus;
     int bytesRead, responseStatus;
+    int inContent = 0;
+    size_t current_chunk_len = 0;
+    llm_stream_callback_t stream_cb = RT_NULL;
+    void *stream_context = RT_NULL;
+
+    if (handle == RT_NULL)
+    {
+        rt_kprintf("Error: llm handle is NULL.\n");
+        return RT_NULL;
+    }
 
 
-    allContent[0] = '\0';
+    contentBuffer[0] = '\0';
 
 
     /* Check if messages is an array */
     /* Check if messages is an array */
     if (!cJSON_IsArray(messages))
     if (!cJSON_IsArray(messages))
@@ -247,6 +328,9 @@ char *get_llm_answer(cJSON *messages)
         goto cleanup;
         goto cleanup;
     }
     }
 
 
+    stream_cb = handle->stream_cb;
+    stream_context = handle->stream_user_data;
+
     /* Create web session */
     /* Create web session */
     webSession = webclient_session_create(WEB_SOCKET_BUF_SIZE);
     webSession = webclient_session_create(WEB_SOCKET_BUF_SIZE);
     if (webSession == NULL)
     if (webSession == NULL)
@@ -298,45 +382,61 @@ char *get_llm_answer(cJSON *messages)
     /* Read and process response */
     /* Read and process response */
     while ((bytesRead = webclient_read(webSession, responseBuffer, WEB_SOCKET_BUF_SIZE)) > 0)
     while ((bytesRead = webclient_read(webSession, responseBuffer, WEB_SOCKET_BUF_SIZE)) > 0)
     {
     {
-        int inContent = 0;
         for (int i = 0; i < bytesRead; i++)
         for (int i = 0; i < bytesRead; i++)
         {
         {
+            char ch = responseBuffer[i];
+
             if (inContent)
             if (inContent)
             {
             {
-                if (responseBuffer[i] == '"')
+                if (ch == '"')
                 {
                 {
                     inContent = 0;
                     inContent = 0;
 
 
-                    /* Append content to allContent */
-                    size_t newLen = rt_strlen(contentBuffer);
-
-                    /* Print content */
-                    for (size_t i = 0; i < newLen; i++)
+                    if (current_chunk_len > 0)
                     {
                     {
-                        rt_kprintf("%c", contentBuffer[i]);
+                        for (size_t j = 0; j < current_chunk_len; j++)
+                        {
+                            rt_kprintf("%c", contentBuffer[j]);
+                        }
+
+                        if (!append_chunk_to_buffer(&assembled, &assembled_len, &assembled_cap, contentBuffer))
+                        {
+                            rt_kprintf("Error: insufficient memory for LLM response.\n");
+                            goto cleanup;
+                        }
+
+                        if (stream_cb)
+                        {
+                            stream_cb(contentBuffer, stream_context);
+                        }
                     }
                     }
 
 
-                    /* Append content to allContent */
-
-                    strcat(allContent, contentBuffer);
-
-                    /* Reset content buffer */
+                    current_chunk_len = 0;
                     contentBuffer[0] = '\0';
                     contentBuffer[0] = '\0';
                 }
                 }
                 else
                 else
                 {
                 {
-                    strncat(contentBuffer, &responseBuffer[i], 1);
+                    if (current_chunk_len < (sizeof(contentBuffer) - 1))
+                    {
+                        contentBuffer[current_chunk_len++] = ch;
+                        contentBuffer[current_chunk_len] = '\0';
+                    }
                 }
                 }
             }
             }
-            else if (responseBuffer[i] == '"' && i > 8 &&
+            else if (ch == '"' && i >= 10 &&
                      rt_strncmp(&responseBuffer[i - 10], "\"content\":\"", 10) == 0)
                      rt_strncmp(&responseBuffer[i - 10], "\"content\":\"", 10) == 0)
             {
             {
                 inContent = 1;
                 inContent = 1;
+                current_chunk_len = 0;
+                contentBuffer[0] = '\0';
             }
             }
         }
         }
     }
     }
 
 
-    rt_kprintf("\n");
+    if (assembled_len > 0)
+    {
+        rt_kprintf("\n");
+    }
 
 
 cleanup:
 cleanup:
     /* Cleanup resources */
     /* Cleanup resources */
@@ -349,9 +449,15 @@ cleanup:
         cJSON_free(payload);
         cJSON_free(payload);
     }
     }
 
 
-    if (allContent[0] != '\0')
+    if (assembled_len > 0 && assembled != RT_NULL)
+    {
+        result = assembled;
+        assembled = RT_NULL;
+    }
+
+    if (assembled != RT_NULL)
     {
     {
-        result = rt_strdup(allContent);
+        rt_free(assembled);
     }
     }
     return result;
     return result;
 }
 }
@@ -373,17 +479,18 @@ static void recv_inputBuff_mb(void *handle)
         }
         }
         rt_kprintf("\n");
         rt_kprintf("\n");
 
 
+        char *result = RT_NULL;
 #ifdef PKG_LLMCHAT_HISTORY_PAYLOAD
 #ifdef PKG_LLMCHAT_HISTORY_PAYLOAD
-        add_message2messages(input_buffer, "user", &llm);
+        add_message2messages(input_buffer, "user", llm);
 
 
-        char *result = llm.get_answer(llm.messages);
+        result = llm->get_answer(llm, llm->messages);
 
 
-        add_message2messages(result, "assistant", &llm);
+        add_message2messages(result, "assistant", llm);
 
 
 #else
 #else
         add_message2messages(input_buffer, "user", llm);
         add_message2messages(input_buffer, "user", llm);
 
 
-        char *result = llm->get_answer(llm->messages);
+        result = llm->get_answer(llm, llm->messages);
 #if defined(LLM_DBG)
 #if defined(LLM_DBG)
         display_llm_messages(llm);
         display_llm_messages(llm);
 #endif
 #endif
@@ -426,8 +533,15 @@ void send_llm_mb(llm_t handle, char *inputBuffer)
  **/
  **/
 rt_err_t init_llm(llm_t handle)
 rt_err_t init_llm(llm_t handle)
 {
 {
+    if (handle == RT_NULL)
+    {
+        return RT_ERROR;
+    }
+
     handle->get_answer = get_llm_answer;
     handle->get_answer = get_llm_answer;
     handle->messages = cJSON_CreateArray();
     handle->messages = cJSON_CreateArray();
+    handle->stream_cb = RT_NULL;
+    handle->stream_user_data = RT_NULL;
 
 
     handle->inputbuff_mb = rt_mb_create("llm_inputbuff_mb", sizeof(char *) * LLM_CHAT_NUM, RT_IPC_FLAG_FIFO);
     handle->inputbuff_mb = rt_mb_create("llm_inputbuff_mb", sizeof(char *) * LLM_CHAT_NUM, RT_IPC_FLAG_FIFO);
     if (handle->inputbuff_mb == RT_NULL)
     if (handle->inputbuff_mb == RT_NULL)

+ 139 - 1
ports/llm_webnet.c

@@ -14,6 +14,7 @@
 #include <wn_module.h>
 #include <wn_module.h>
 
 
 #include "llm_config.h"
 #include "llm_config.h"
+#include <string.h>
 
 
 #define DBG_TAG              "llm_chat"
 #define DBG_TAG              "llm_chat"
 #ifdef PKG_LLMCHAT_DBG
 #ifdef PKG_LLMCHAT_DBG
@@ -29,6 +30,70 @@ static int llm_webnet_init(void);
 
 
 static llm_t llm_handle = RT_NULL;
 static llm_t llm_handle = RT_NULL;
 
 
+struct llm_stream_context
+{
+    struct webnet_session *session;
+    rt_bool_t has_delta;
+};
+
+static void llm_webnet_stream_send_event(struct webnet_session *session,
+        const char *event,
+        const char *data)
+{
+    if (session == RT_NULL)
+    {
+        return;
+    }
+
+    if (event && event[0])
+    {
+        webnet_session_write(session, (const rt_uint8_t *)"event: ", 7);
+        webnet_session_write(session, (const rt_uint8_t *)event, strlen(event));
+        webnet_session_write(session, (const rt_uint8_t *)"\n", 1);
+    }
+
+    if (data == RT_NULL)
+    {
+        webnet_session_write(session, (const rt_uint8_t *)"data:\n\n", 7);
+        return;
+    }
+
+    const char *cursor = data;
+    while (cursor)
+    {
+        const char *newline = strchr(cursor, '\n');
+        size_t len = newline ? (size_t)(newline - cursor) : strlen(cursor);
+
+        webnet_session_write(session, (const rt_uint8_t *)"data: ", 6);
+        if (len > 0)
+        {
+            webnet_session_write(session, (const rt_uint8_t *)cursor, len);
+        }
+        webnet_session_write(session, (const rt_uint8_t *)"\n", 1);
+
+        if (newline == RT_NULL)
+        {
+            break;
+        }
+        cursor = newline + 1;
+    }
+
+    webnet_session_write(session, (const rt_uint8_t *)"\n", 1);
+}
+
+static void llm_webnet_stream_on_chunk(const char *chunk, void *user_data)
+{
+    struct llm_stream_context *context = (struct llm_stream_context *)user_data;
+
+    if (context == RT_NULL || context->session == RT_NULL || chunk == RT_NULL || chunk[0] == '\0')
+    {
+        return;
+    }
+
+    llm_webnet_stream_send_event(context->session, "delta", chunk);
+    context->has_delta = RT_TRUE;
+}
+
 /** Save configuration to memory - simplified version
 /** Save configuration to memory - simplified version
  *  Configuration is already in memory, no additional operations needed
  *  Configuration is already in memory, no additional operations needed
  */
  */
@@ -478,6 +543,8 @@ static void cgi_chat_handler(struct webnet_session *session)
     cJSON *res_json = RT_NULL;
     cJSON *res_json = RT_NULL;
     char *res_str = RT_NULL;
     char *res_str = RT_NULL;
     rt_size_t post_len = 0;
     rt_size_t post_len = 0;
+    rt_bool_t stream_mode = RT_FALSE;
+    struct llm_stream_context stream_ctx = { RT_NULL, RT_FALSE };
 
 
     LOG_D("=== CGI chat called: method=%d, content_len=%d ===", session->request->method, session->request->content_length);
     LOG_D("=== CGI chat called: method=%d, content_len=%d ===", session->request->method, session->request->content_length);
 
 
@@ -516,6 +583,28 @@ static void cgi_chat_handler(struct webnet_session *session)
         goto error;
         goto error;
     }
     }
 
 
+    msg_item = cJSON_GetObjectItem(req_json, "stream");
+    if (msg_item)
+    {
+        if (cJSON_IsBool(msg_item))
+        {
+            stream_mode = cJSON_IsTrue(msg_item);
+        }
+        else if (cJSON_IsNumber(msg_item))
+        {
+            stream_mode = (msg_item->valuedouble != 0);
+        }
+        else if (cJSON_IsString(msg_item))
+        {
+            const char *stream_val = cJSON_GetStringValue(msg_item);
+            if (stream_val &&
+                    (strcmp(stream_val, "true") == 0 || strcmp(stream_val, "1") == 0 || strcmp(stream_val, "TRUE") == 0))
+            {
+                stream_mode = RT_TRUE;
+            }
+        }
+    }
+
     /* Check whether this is a reset request */
     /* Check whether this is a reset request */
     msg_item = cJSON_GetObjectItem(req_json, "reset");
     msg_item = cJSON_GetObjectItem(req_json, "reset");
     if (msg_item && cJSON_IsTrue(msg_item))
     if (msg_item && cJSON_IsTrue(msg_item))
@@ -564,10 +653,59 @@ static void cgi_chat_handler(struct webnet_session *session)
         goto error;
         goto error;
     }
     }
 
 
+    if (llm_handle == RT_NULL)
+    {
+        LOG_E("Error: LLM handle is NULL");
+        goto error;
+    }
+
     add_message2messages(user_msg, "user", llm_handle);
     add_message2messages(user_msg, "user", llm_handle);
 
 
+    if (stream_mode)
+    {
+        LOG_D("Streaming mode enabled for this request");
+
+        stream_ctx.session = session;
+
+        session->request->result_code = 200;
+        webnet_session_set_header(session, "text/event-stream", 200, "OK", -1);
+        webnet_session_write(session, (const rt_uint8_t *)":rt-thread-stream\n\n", strlen(":rt-thread-stream\n\n"));
+
+        llm_handle->stream_cb = llm_webnet_stream_on_chunk;
+        llm_handle->stream_user_data = &stream_ctx;
+
+        LOG_D("Calling get_llm_answer (streaming)...");
+        ai_reply = llm_handle->get_answer(llm_handle, llm_handle->messages);
+
+        llm_handle->stream_cb = RT_NULL;
+        llm_handle->stream_user_data = RT_NULL;
+
+        if (ai_reply == RT_NULL)
+        {
+            LOG_E("Error: streaming get_llm_answer returned NULL (check API/net)");
+            ai_reply = rt_strdup("Mock reply: Success! WebNet CGI + LLM integrated. (Local llm works; if real API fails, check key/net.)");
+        }
+
+        if (ai_reply != RT_NULL)
+        {
+            if (stream_ctx.has_delta == RT_FALSE)
+            {
+                llm_webnet_stream_send_event(session, "delta", ai_reply);
+            }
+            add_message2messages(ai_reply, "assistant", llm_handle);
+            llm_webnet_stream_send_event(session, "final", ai_reply);
+        }
+        else
+        {
+            llm_webnet_stream_send_event(session, "error", "LLM response failed");
+        }
+
+        llm_webnet_stream_send_event(session, "done", "[DONE]");
+        goto cleanup;
+    }
+
     LOG_D("Calling get_llm_answer...");
     LOG_D("Calling get_llm_answer...");
-    ai_reply = get_llm_answer(llm_handle->messages);
+    ai_reply = llm_handle->get_answer(llm_handle, llm_handle->messages);
     if (ai_reply == RT_NULL)
     if (ai_reply == RT_NULL)
     {
     {
         LOG_E("Error: get_llm_answer returned NULL (check API/net in CGI context)");
         LOG_E("Error: get_llm_answer returned NULL (check API/net in CGI context)");

+ 260 - 26
resource/index.html

@@ -2229,44 +2229,58 @@
                         return;
                         return;
                     }
                     }
                     const chatUrl = new URL('/cgi-bin/chat', location.origin).toString();
                     const chatUrl = new URL('/cgi-bin/chat', location.origin).toString();
-                    // 首次尝试:JSON 请求
-                    let resp = await fetch(chatUrl, {
+                    const payload = { message: message };
+                    const supportsStream = true;
+                    let useStream = supportsStream;
+                    let resp;
+                    let data;
+
+                    if (useStream) {
+                        payload.stream = true;
+                    }
+
+                    resp = await fetch(chatUrl, {
                         method: 'POST',
                         method: 'POST',
                         headers: { 'Content-Type': 'application/json' },
                         headers: { 'Content-Type': 'application/json' },
-                        body: JSON.stringify({ message: message })
+                        body: JSON.stringify(payload)
                     });
                     });
 
 
-                    // 优先以 JSON 解析;失败则以文本解析
-                    let text = await resp.text();
-                    let data;
-                    try { data = JSON.parse(text); } catch (_) { data = null; }
-
-                    // 如果非 OK 或解析不到 JSON,尝试 x-www-form-urlencoded 兜底
-                    if (!resp.ok || !data) {
-                        try {
-                            resp = await fetch(chatUrl, {
-                                method: 'POST',
-                                headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
-                                body: 'message=' + encodeURIComponent(message)
-                            });
-                            text = await resp.text();
-                            try { data = JSON.parse(text); } catch (_) {
-                                data = { success: false, error: text || '服务器返回非JSON' };
-                            }
-                        } catch (e2) {
-                            data = { success: false, error: '网络错误,请检查服务器连接' };
+                    if (useStream && resp.ok) {
+                        const streamResult = await handleStreamedResponse(resp);
+                        if (streamResult && streamResult.success) {
+                            data = streamResult;
+                        }
+                        else {
+                            data = streamResult || { success: false, error: 'LLM响应失败' };
                         }
                         }
                     }
                     }
+                    else {
+                        removeTypingIndicator();
+                        data = await handleLegacyResponse(resp, message, chatUrl);
+                    }
 
 
-                    removeTypingIndicator();
-                    if (data && data.success) {
-                        addMessage(data.response, false);
+                    if (useStream && data && data.success) {
                         // 将最后一条AI消息标记上一条用户输入,便于重新生成
                         // 将最后一条AI消息标记上一条用户输入,便于重新生成
                         const lastMsg = chatContainer.lastElementChild;
                         const lastMsg = chatContainer.lastElementChild;
                         if (lastMsg && lastMsg.classList && lastMsg.classList.contains('message')) {
                         if (lastMsg && lastMsg.classList && lastMsg.classList.contains('message')) {
                             lastMsg.setAttribute('data-prev-user', window.__lastUserMessage || '');
                             lastMsg.setAttribute('data-prev-user', window.__lastUserMessage || '');
                         }
                         }
-                    } else {
+                    }
+                    else if (!useStream) {
+                        if (data && data.success) {
+                            addMessage(data.response, false);
+                            const lastMsg = chatContainer.lastElementChild;
+                            if (lastMsg && lastMsg.classList && lastMsg.classList.contains('message')) {
+                                lastMsg.setAttribute('data-prev-user', window.__lastUserMessage || '');
+                            }
+                        } else {
+                            addMessage(`错误: ${data && data.error ? data.error : '未知错误'}`, false, true);
+                        }
+                    }
+
+                    if (data && data.success) {
+                        // 已经在各自的处理流程中完成渲染
+                    } else if (useStream) {
                         addMessage(`错误: ${data && data.error ? data.error : '未知错误'}`, false, true);
                         addMessage(`错误: ${data && data.error ? data.error : '未知错误'}`, false, true);
                     }
                     }
                 } catch (error) {
                 } catch (error) {
@@ -2454,6 +2468,226 @@
                 chatContainer.scrollTop = chatContainer.scrollHeight;
                 chatContainer.scrollTop = chatContainer.scrollHeight;
             }
             }
 
 
+            function createStreamingMessageShell() {
+                const messageDiv = document.createElement('div');
+                messageDiv.className = 'message ai-message streaming';
+
+                const avatarDiv = document.createElement('div');
+                avatarDiv.className = 'message-avatar ai-avatar';
+                avatarDiv.innerHTML = '<i class="fas fa-robot"></i>';
+
+                const contentDiv = document.createElement('div');
+                contentDiv.className = 'message-content';
+                contentDiv.textContent = '';
+
+                messageDiv.appendChild(avatarDiv);
+                messageDiv.appendChild(contentDiv);
+                chatContainer.appendChild(messageDiv);
+                chatContainer.scrollTop = chatContainer.scrollHeight;
+
+                return { messageDiv, contentDiv, rawText: '' };
+            }
+
+            function updateStreamingMessageShell(state, text) {
+                if (!state || !state.contentDiv) return;
+                state.rawText = text;
+                state.contentDiv.textContent = text;
+                chatContainer.scrollTop = chatContainer.scrollHeight;
+            }
+
+            function finalizeStreamingMessageShell(state, finalText, isError = false, errorTip = '') {
+                if (!state || !state.messageDiv) return;
+                const messageDiv = state.messageDiv;
+                const contentDiv = state.contentDiv;
+
+                if (isError) {
+                    const errorMessage = errorTip || 'LLM响应失败';
+                    contentDiv.innerHTML = `<div class="error-message">${errorMessage}</div>`;
+                    conversationManager.addMessageToCurrent(errorMessage, false, true);
+                    renderConversationList();
+                    messageDiv.setAttribute('data-prev-user', window.__lastUserMessage || '');
+                    chatContainer.scrollTop = chatContainer.scrollHeight;
+                    return;
+                }
+
+                const text = (finalText || state.rawText || '').toString();
+                state.rawText = text;
+
+                contentDiv.innerHTML = parseMarkdown(text);
+                try {
+                    if (window.hljs) {
+                        contentDiv.querySelectorAll('pre code').forEach(function(block){
+                            hljs.highlightElement(block);
+                        });
+                    }
+                } catch (e) {}
+                enhanceCodeBlocks(contentDiv);
+
+                const actionsDiv = document.createElement('div');
+                actionsDiv.className = 'message-actions';
+
+                const copyBtn = document.createElement('button');
+                copyBtn.className = 'icon-btn';
+                copyBtn.innerHTML = '<i class="fas fa-copy"></i>';
+                copyBtn.title = '复制';
+                copyBtn.addEventListener('click', async function() {
+                    const htmlEl = contentDiv.querySelector('.markdown-body');
+                    let textToCopy = text;
+                    if (htmlEl) textToCopy = htmlEl.innerText || htmlEl.textContent || text;
+                    const ok = await copyText(textToCopy);
+                    copyBtn.innerHTML = ok ? '<i class="fas fa-check"></i>' : '<i class="fas fa-times"></i>';
+                    copyBtn.style.color = ok ? '#10a37f' : '#ef4444';
+                    setTimeout(function(){
+                        copyBtn.innerHTML = '<i class="fas fa-copy"></i>';
+                        copyBtn.style.color = '';
+                    }, 1500);
+                });
+
+                const regenBtn = document.createElement('button');
+                regenBtn.className = 'icon-btn';
+                regenBtn.innerHTML = '<i class="fas fa-rotate-right"></i>';
+                regenBtn.title = '重新生成';
+                regenBtn.addEventListener('click', function() {
+                    const prevUser = messageDiv.getAttribute('data-prev-user');
+                    const prompt = (prevUser && prevUser.trim()) ? prevUser.trim() : (window.__lastUserMessage || '');
+                    if (!prompt) return;
+                    messageInput.value = prompt;
+                    messageInput.dispatchEvent(new Event('input'));
+                    sendMessage();
+                });
+
+                actionsDiv.appendChild(copyBtn);
+                actionsDiv.appendChild(regenBtn);
+                messageDiv.appendChild(actionsDiv);
+
+                conversationManager.addMessageToCurrent(text, false, false);
+                renderConversationList();
+
+                messageDiv.classList.remove('streaming');
+                messageDiv.setAttribute('data-prev-user', window.__lastUserMessage || '');
+                chatContainer.scrollTop = chatContainer.scrollHeight;
+            }
+
+            async function handleStreamedResponse(response) {
+                removeTypingIndicator();
+
+                if (!response.body || typeof response.body.getReader !== 'function') {
+                    finalizeStreamingMessageShell(createStreamingMessageShell(), '', true, '浏览器不支持流式响应');
+                    return { success: false, error: '浏览器不支持流式响应' };
+                }
+
+                const state = createStreamingMessageShell();
+                const reader = response.body.getReader();
+                const decoder = new TextDecoder('utf-8');
+                let buffer = '';
+                let aggregatedText = '';
+                let finalText = '';
+
+                try {
+                    while (true) {
+                        const { value, done } = await reader.read();
+                        if (done) break;
+                        buffer += decoder.decode(value, { stream: true });
+                        const parts = buffer.split('\n\n');
+                        buffer = parts.pop() || '';
+
+                        let shouldStop = false;
+
+                        for (const part of parts) {
+                            if (!part.trim() || part.trim().startsWith(':')) {
+                                continue;
+                            }
+
+                            const lines = part.split('\n');
+                            let eventType = 'message';
+                            const dataLines = [];
+
+                            lines.forEach(function(line) {
+                                if (line.startsWith('event:')) {
+                                    eventType = line.slice(6).trim();
+                                }
+                                else if (line.startsWith('data:')) {
+                                    dataLines.push(line.slice(5));
+                                }
+                            });
+
+                            const payload = dataLines.join('\n');
+
+                            if (eventType === 'delta') {
+                                aggregatedText += payload;
+                                updateStreamingMessageShell(state, aggregatedText);
+                            }
+                            else if (eventType === 'final') {
+                                finalText = payload || aggregatedText;
+                            }
+                            else if (eventType === 'error') {
+                                finalizeStreamingMessageShell(state, '', true, payload || 'LLM响应失败');
+                                return { success: false, error: payload || 'LLM响应失败' };
+                            }
+                            else if (eventType === 'done') {
+                                shouldStop = true;
+                                break;
+                            }
+                        }
+
+                        if (shouldStop) {
+                            break;
+                        }
+                    }
+                }
+                catch (err) {
+                    finalizeStreamingMessageShell(state, '', true, err && err.message ? err.message : '网络错误');
+                    return { success: false, error: err && err.message ? err.message : '网络错误' };
+                }
+                finally {
+                    try { reader.releaseLock(); } catch (e) {}
+                }
+
+                if (!finalText) {
+                    finalText = aggregatedText;
+                }
+
+                finalizeStreamingMessageShell(state, finalText || '');
+                return { success: true, response: finalText || aggregatedText };
+            }
+
+            async function handleLegacyResponse(initialResponse, message, chatUrl) {
+                removeTypingIndicator();
+                let resp = initialResponse;
+                let text = await resp.text();
+                let data = null;
+
+                try {
+                    data = JSON.parse(text);
+                } catch (e) {
+                    data = null;
+                }
+
+                if (!resp.ok || !data) {
+                    try {
+                        resp = await fetch(chatUrl, {
+                            method: 'POST',
+                            headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
+                            body: 'message=' + encodeURIComponent(message)
+                        });
+                        text = await resp.text();
+                        try {
+                            data = JSON.parse(text);
+                        } catch (parseErr) {
+                            data = { success: false, error: text || '服务器返回非JSON' };
+                        }
+                    } catch (netErr) {
+                        data = { success: false, error: '网络错误,请检查服务器连接' };
+                    }
+                }
+
+                if (!data) {
+                    data = { success: false, error: '未知响应' };
+                }
+
+                return data;
+            }
+
             // 添加消息到聊天界面(新消息发送时使用)
             // 添加消息到聊天界面(新消息发送时使用)
             function addMessage(content, isUser, isError = false) {
             function addMessage(content, isUser, isError = false) {
                 // 保存消息到对话管理器
                 // 保存消息到对话管理器