Upload 10 files
Browse files
.cnb.yml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
"(main)":
|
| 2 |
push:
|
| 3 |
- runner:
|
| 4 |
cpus: 2
|
|
|
|
| 1 |
+
"(main|dev)":
|
| 2 |
push:
|
| 3 |
- runner:
|
| 4 |
cpus: 2
|
README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
| 1 |
+
## 部署到 claw cloud
|
| 2 |
+
|
| 3 |
+
image: docker.cnb.cool/0_0/learn/julep
|
| 4 |
+
端口: 8080
|
| 5 |
+
cpu: 0.2
|
| 6 |
+
内存: 64M
|
| 7 |
+
环境变量: JULEP_API_BASE_URL=https://api.julep.ai/api
|
| 8 |
+
|
| 9 |
+
如果跟 new-api 部署在同一个区域,可以让 new-api 直接用内网地址连接到 julep。无需暴露到公网。
|
| 10 |
+
|
| 11 |
+
## 致谢
|
| 12 |
+
|
| 13 |
+
https://linux.do/t/topic/577836/27
|
handlers.go
CHANGED
|
@@ -19,7 +19,7 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
|
|
| 19 |
// ... (Keep Method Check, Authentication, Decode Body, Validation as before) ...
|
| 20 |
// 1. Check Method
|
| 21 |
if r.Method != http.MethodPost {
|
| 22 |
-
writeJSONError(w, reqLogger, http.StatusMethodNotAllowed, fmt.Sprintf("
|
| 23 |
return
|
| 24 |
}
|
| 25 |
|
|
@@ -30,7 +30,7 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
|
|
| 30 |
if authHeader != "" {
|
| 31 |
invalidAuthType = "invalid_authorization_type"
|
| 32 |
}
|
| 33 |
-
writeJSONError(w, reqLogger, http.StatusUnauthorized, "Authorization
|
| 34 |
return
|
| 35 |
}
|
| 36 |
reqLogger.Debug("Authorization header present") // Changed to Debug
|
|
@@ -42,15 +42,15 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
|
|
| 42 |
if err != nil {
|
| 43 |
var syntaxError *json.SyntaxError
|
| 44 |
var unmarshalTypeError *json.UnmarshalTypeError
|
| 45 |
-
errMsg := "
|
| 46 |
errCode := "invalid_json"
|
| 47 |
if errors.As(err, &syntaxError) { // Use errors.As
|
| 48 |
-
errMsg = fmt.Sprintf("
|
| 49 |
} else if errors.As(err, &unmarshalTypeError) { // Use errors.As
|
| 50 |
-
errMsg = fmt.Sprintf("
|
| 51 |
errCode = "invalid_field_type"
|
| 52 |
} else if err.Error() == "http: request body too large" {
|
| 53 |
-
errMsg = "
|
| 54 |
errCode = "request_too_large"
|
| 55 |
writeJSONError(w, reqLogger, http.StatusRequestEntityTooLarge, errMsg, stringPtr("invalid_request_error"), &errCode, nil)
|
| 56 |
return
|
|
@@ -66,14 +66,14 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
|
|
| 66 |
reqLogger.Warn("Validation failed: 'messages' field is empty")
|
| 67 |
param := "messages"
|
| 68 |
code := "missing_field"
|
| 69 |
-
writeJSONError(w, reqLogger, http.StatusBadRequest, "'messages'
|
| 70 |
return
|
| 71 |
}
|
| 72 |
if openaiRequest.Model == "" {
|
| 73 |
reqLogger.Warn("Validation failed: 'model' field is empty")
|
| 74 |
param := "model"
|
| 75 |
code := "missing_field"
|
| 76 |
-
writeJSONError(w, reqLogger, http.StatusBadRequest, "'model'
|
| 77 |
return
|
| 78 |
}
|
| 79 |
reqLogger.Info("Request decoded and validated", "model", openaiRequest.Model, "stream_requested", openaiRequest.Stream)
|
|
@@ -86,12 +86,14 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
|
|
| 86 |
reqLogger.Error("Upstream Julep call failed", "error", err, "status_code", upstreamStatusCode)
|
| 87 |
// Use the status code returned by callJulepChat for the client response
|
| 88 |
errType := "upstream_error"
|
|
|
|
| 89 |
if upstreamStatusCode == http.StatusGatewayTimeout {
|
| 90 |
errType = "gateway_timeout"
|
|
|
|
| 91 |
} else if upstreamStatusCode >= 400 && upstreamStatusCode < 500 {
|
| 92 |
errType = "invalid_request_error" // Or map specific Julep 4xx codes
|
| 93 |
}
|
| 94 |
-
writeJSONError(w, reqLogger, upstreamStatusCode,
|
| 95 |
return
|
| 96 |
}
|
| 97 |
|
|
@@ -101,7 +103,8 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
|
|
| 101 |
if !isStreaming {
|
| 102 |
// 6.a. Send Non-Streaming Response
|
| 103 |
reqLogger.Info("Sending non-streaming response")
|
| 104 |
-
|
|
|
|
| 105 |
w.WriteHeader(http.StatusOK) // Status OK as the overall operation succeeded
|
| 106 |
if err := json.NewEncoder(w).Encode(finalOpenAIResponse); err != nil {
|
| 107 |
reqLogger.Error("Failed to encode non-streaming response", "error", err)
|
|
@@ -110,7 +113,8 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
|
|
| 110 |
// 6.b. Send Simulated Streaming Response from the complete Julep data
|
| 111 |
reqLogger.Info("Sending simulated streaming response based on Julep result")
|
| 112 |
|
| 113 |
-
|
|
|
|
| 114 |
w.Header().Set("Cache-Control", "no-cache")
|
| 115 |
w.Header().Set("Connection", "keep-alive")
|
| 116 |
// Optional: w.Header().Set("X-Accel-Buffering", "no")
|
|
@@ -119,11 +123,12 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
|
|
| 119 |
if !ok {
|
| 120 |
reqLogger.Error("Streaming unsupported: ResponseWriter does not implement http.Flusher")
|
| 121 |
errType := "internal_server_error"
|
|
|
|
| 122 |
// It's likely too late to send a proper JSON error here if headers were already flushed implicitly.
|
| 123 |
// Best effort: log and potentially send plain text error before trying to write stream headers.
|
| 124 |
-
http.Error(w,
|
| 125 |
// Attempt to write JSON error anyway, might fail.
|
| 126 |
-
writeJSONError(w, reqLogger, http.StatusInternalServerError, "
|
| 127 |
return
|
| 128 |
}
|
| 129 |
|
|
@@ -144,13 +149,14 @@ func chatCompletionsHandler(logger *slog.Logger, w http.ResponseWriter, r *http.
|
|
| 144 |
|
| 145 |
// streamFullResponseAsChunks takes a complete OpenAIResponse and sends it
|
| 146 |
// to the client as a series of SSE chunks, simulating a real stream.
|
|
|
|
| 147 |
func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, logger *slog.Logger, fullResp *OpenAIResponse) error {
|
| 148 |
if len(fullResp.Choices) == 0 {
|
| 149 |
logger.Warn("Full response has no choices to stream")
|
| 150 |
// Send DONE immediately if no choices
|
| 151 |
_, err := fmt.Fprintf(w, "data: [DONE]\n\n")
|
| 152 |
if err != nil {
|
| 153 |
-
return fmt.Errorf("
|
| 154 |
}
|
| 155 |
flusher.Flush()
|
| 156 |
return nil
|
|
@@ -175,7 +181,7 @@ func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, log
|
|
| 175 |
},
|
| 176 |
}
|
| 177 |
if err := sendChunk(w, flusher, logger, roleChunk); err != nil {
|
| 178 |
-
return fmt.Errorf("
|
| 179 |
}
|
| 180 |
time.Sleep(10 * time.Millisecond) // Small delay
|
| 181 |
}
|
|
@@ -185,12 +191,21 @@ func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, log
|
|
| 185 |
if content != "" {
|
| 186 |
// Simulate streaming by breaking content into smaller parts
|
| 187 |
const chunkSize = 5 // Small chunk size for demonstration
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
}
|
| 193 |
-
contentPiece :=
|
| 194 |
|
| 195 |
contentChunk := OpenAIChunk{
|
| 196 |
ID: fullResp.ID,
|
|
@@ -206,7 +221,7 @@ func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, log
|
|
| 206 |
},
|
| 207 |
}
|
| 208 |
if err := sendChunk(w, flusher, logger, contentChunk); err != nil {
|
| 209 |
-
return fmt.Errorf("
|
| 210 |
}
|
| 211 |
time.Sleep(30 * time.Millisecond) // Simulate generation time between chunks
|
| 212 |
}
|
|
@@ -228,7 +243,7 @@ func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, log
|
|
| 228 |
},
|
| 229 |
}
|
| 230 |
if err := sendChunk(w, flusher, logger, toolChunk); err != nil {
|
| 231 |
-
return fmt.Errorf("
|
| 232 |
}
|
| 233 |
time.Sleep(10 * time.Millisecond) // Small delay
|
| 234 |
}
|
|
@@ -248,7 +263,7 @@ func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, log
|
|
| 248 |
},
|
| 249 |
}
|
| 250 |
if err := sendChunk(w, flusher, logger, finalChunk); err != nil {
|
| 251 |
-
return fmt.Errorf("
|
| 252 |
}
|
| 253 |
|
| 254 |
// --- Send DONE message ---
|
|
@@ -257,7 +272,7 @@ func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, log
|
|
| 257 |
if err != nil {
|
| 258 |
// Log error, but might not reach client if connection closed
|
| 259 |
logger.Error("Failed to write [DONE] message", "error", err)
|
| 260 |
-
return fmt.Errorf("
|
| 261 |
}
|
| 262 |
flusher.Flush() // Ensure DONE is sent
|
| 263 |
|
|
@@ -265,19 +280,22 @@ func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, log
|
|
| 265 |
}
|
| 266 |
|
| 267 |
// sendChunk encodes the chunk to JSON and writes it in SSE format.
|
|
|
|
| 268 |
func sendChunk(w http.ResponseWriter, flusher http.Flusher, logger *slog.Logger, chunk OpenAIChunk) error {
|
|
|
|
| 269 |
chunkBytes, err := json.Marshal(chunk)
|
| 270 |
if err != nil {
|
| 271 |
logger.Error("Failed to marshal stream chunk", "error", err, "chunk_id", chunk.ID)
|
| 272 |
-
return fmt.Errorf("
|
| 273 |
}
|
| 274 |
|
| 275 |
// Write in Server-Sent Event format: data: <json>\n\n
|
|
|
|
| 276 |
_, err = fmt.Fprintf(w, "data: %s\n\n", string(chunkBytes))
|
| 277 |
if err != nil {
|
| 278 |
// Log error, connection might be closed by client
|
| 279 |
logger.Error("Failed to write chunk to response writer", "error", err, "chunk_id", chunk.ID)
|
| 280 |
-
return fmt.Errorf("
|
| 281 |
}
|
| 282 |
|
| 283 |
// Flush the buffer to send the chunk immediately
|
|
|
|
| 19 |
// ... (Keep Method Check, Authentication, Decode Body, Validation as before) ...
|
| 20 |
// 1. Check Method
|
| 21 |
if r.Method != http.MethodPost {
|
| 22 |
+
writeJSONError(w, reqLogger, http.StatusMethodNotAllowed, fmt.Sprintf("方法 %s 不允许", r.Method), stringPtr("invalid_request_error"), nil, nil) // Example Chinese error
|
| 23 |
return
|
| 24 |
}
|
| 25 |
|
|
|
|
| 30 |
if authHeader != "" {
|
| 31 |
invalidAuthType = "invalid_authorization_type"
|
| 32 |
}
|
| 33 |
+
writeJSONError(w, reqLogger, http.StatusUnauthorized, "需要 Authorization 标头 (例如, 'Bearer YOUR_API_KEY')", &invalidAuthType, nil, nil) // Example Chinese error
|
| 34 |
return
|
| 35 |
}
|
| 36 |
reqLogger.Debug("Authorization header present") // Changed to Debug
|
|
|
|
| 42 |
if err != nil {
|
| 43 |
var syntaxError *json.SyntaxError
|
| 44 |
var unmarshalTypeError *json.UnmarshalTypeError
|
| 45 |
+
errMsg := "无效的 JSON 请求体" // Example Chinese error
|
| 46 |
errCode := "invalid_json"
|
| 47 |
if errors.As(err, &syntaxError) { // Use errors.As
|
| 48 |
+
errMsg = fmt.Sprintf("偏移量 %d 处 JSON 语法无效", syntaxError.Offset) // Example Chinese error
|
| 49 |
} else if errors.As(err, &unmarshalTypeError) { // Use errors.As
|
| 50 |
+
errMsg = fmt.Sprintf("字段 '%s' 类型无效,应为 %s", unmarshalTypeError.Field, unmarshalTypeError.Type) // Example Chinese error
|
| 51 |
errCode = "invalid_field_type"
|
| 52 |
} else if err.Error() == "http: request body too large" {
|
| 53 |
+
errMsg = "请求体超过限制 (1MB)" // Example Chinese error
|
| 54 |
errCode = "request_too_large"
|
| 55 |
writeJSONError(w, reqLogger, http.StatusRequestEntityTooLarge, errMsg, stringPtr("invalid_request_error"), &errCode, nil)
|
| 56 |
return
|
|
|
|
| 66 |
reqLogger.Warn("Validation failed: 'messages' field is empty")
|
| 67 |
param := "messages"
|
| 68 |
code := "missing_field"
|
| 69 |
+
writeJSONError(w, reqLogger, http.StatusBadRequest, "'messages' 是必填字段且必须是非空数组", stringPtr("invalid_request_error"), &code, ¶m) // Example Chinese error
|
| 70 |
return
|
| 71 |
}
|
| 72 |
if openaiRequest.Model == "" {
|
| 73 |
reqLogger.Warn("Validation failed: 'model' field is empty")
|
| 74 |
param := "model"
|
| 75 |
code := "missing_field"
|
| 76 |
+
writeJSONError(w, reqLogger, http.StatusBadRequest, "'model' 是必填字段", stringPtr("invalid_request_error"), &code, ¶m) // Example Chinese error
|
| 77 |
return
|
| 78 |
}
|
| 79 |
reqLogger.Info("Request decoded and validated", "model", openaiRequest.Model, "stream_requested", openaiRequest.Stream)
|
|
|
|
| 86 |
reqLogger.Error("Upstream Julep call failed", "error", err, "status_code", upstreamStatusCode)
|
| 87 |
// Use the status code returned by callJulepChat for the client response
|
| 88 |
errType := "upstream_error"
|
| 89 |
+
errMsg := fmt.Sprintf("上游 API 错误: %s", err.Error()) // Example Chinese error
|
| 90 |
if upstreamStatusCode == http.StatusGatewayTimeout {
|
| 91 |
errType = "gateway_timeout"
|
| 92 |
+
errMsg = fmt.Sprintf("上游 API 超时: %s", err.Error()) // Example Chinese error
|
| 93 |
} else if upstreamStatusCode >= 400 && upstreamStatusCode < 500 {
|
| 94 |
errType = "invalid_request_error" // Or map specific Julep 4xx codes
|
| 95 |
}
|
| 96 |
+
writeJSONError(w, reqLogger, upstreamStatusCode, errMsg, &errType, nil, nil)
|
| 97 |
return
|
| 98 |
}
|
| 99 |
|
|
|
|
| 103 |
if !isStreaming {
|
| 104 |
// 6.a. Send Non-Streaming Response
|
| 105 |
reqLogger.Info("Sending non-streaming response")
|
| 106 |
+
// CORRECTED: Explicitly add charset=utf-8
|
| 107 |
+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
| 108 |
w.WriteHeader(http.StatusOK) // Status OK as the overall operation succeeded
|
| 109 |
if err := json.NewEncoder(w).Encode(finalOpenAIResponse); err != nil {
|
| 110 |
reqLogger.Error("Failed to encode non-streaming response", "error", err)
|
|
|
|
| 113 |
// 6.b. Send Simulated Streaming Response from the complete Julep data
|
| 114 |
reqLogger.Info("Sending simulated streaming response based on Julep result")
|
| 115 |
|
| 116 |
+
// CORRECTED: Explicitly add charset=utf-8
|
| 117 |
+
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
|
| 118 |
w.Header().Set("Cache-Control", "no-cache")
|
| 119 |
w.Header().Set("Connection", "keep-alive")
|
| 120 |
// Optional: w.Header().Set("X-Accel-Buffering", "no")
|
|
|
|
| 123 |
if !ok {
|
| 124 |
reqLogger.Error("Streaming unsupported: ResponseWriter does not implement http.Flusher")
|
| 125 |
errType := "internal_server_error"
|
| 126 |
+
errMsg := "内部服务器错误:流式传输不受支持" // Example Chinese error
|
| 127 |
// It's likely too late to send a proper JSON error here if headers were already flushed implicitly.
|
| 128 |
// Best effort: log and potentially send plain text error before trying to write stream headers.
|
| 129 |
+
http.Error(w, errMsg, http.StatusInternalServerError)
|
| 130 |
// Attempt to write JSON error anyway, might fail.
|
| 131 |
+
writeJSONError(w, reqLogger, http.StatusInternalServerError, "服务器配置不支持流式传输", &errType, nil, nil) // Example Chinese error
|
| 132 |
return
|
| 133 |
}
|
| 134 |
|
|
|
|
| 149 |
|
| 150 |
// streamFullResponseAsChunks takes a complete OpenAIResponse and sends it
|
| 151 |
// to the client as a series of SSE chunks, simulating a real stream.
|
| 152 |
+
// (No changes needed inside this function itself, as it relies on the Content-Type set before calling it)
|
| 153 |
func streamFullResponseAsChunks(w http.ResponseWriter, flusher http.Flusher, logger *slog.Logger, fullResp *OpenAIResponse) error {
|
| 154 |
if len(fullResp.Choices) == 0 {
|
| 155 |
logger.Warn("Full response has no choices to stream")
|
| 156 |
// Send DONE immediately if no choices
|
| 157 |
_, err := fmt.Fprintf(w, "data: [DONE]\n\n")
|
| 158 |
if err != nil {
|
| 159 |
+
return fmt.Errorf("写入 [DONE] 消息失败: %w", err) // Example Chinese error
|
| 160 |
}
|
| 161 |
flusher.Flush()
|
| 162 |
return nil
|
|
|
|
| 181 |
},
|
| 182 |
}
|
| 183 |
if err := sendChunk(w, flusher, logger, roleChunk); err != nil {
|
| 184 |
+
return fmt.Errorf("发送角色块失败: %w", err) // Example Chinese error
|
| 185 |
}
|
| 186 |
time.Sleep(10 * time.Millisecond) // Small delay
|
| 187 |
}
|
|
|
|
| 191 |
if content != "" {
|
| 192 |
// Simulate streaming by breaking content into smaller parts
|
| 193 |
const chunkSize = 5 // Small chunk size for demonstration
|
| 194 |
+
// IMPORTANT: This simple byte-wise chunking can break multi-byte UTF-8 characters.
|
| 195 |
+
// For proper UTF-8 streaming, chunking should happen at rune boundaries.
|
| 196 |
+
// However, since we are simulating from a *complete* response here, and
|
| 197 |
+
// sendChunk uses json.Marshal which handles UTF-8 correctly, this simulation
|
| 198 |
+
// *should* still produce valid JSON payloads per chunk. The potential issue
|
| 199 |
+
// is less severe than if we were reading a raw byte stream.
|
| 200 |
+
// A more robust simulation would iterate over runes.
|
| 201 |
+
runeContent := []rune(content)
|
| 202 |
+
const runeChunkSize = 3 // Chunk runes instead of bytes
|
| 203 |
+
for i := 0; i < len(runeContent); i += runeChunkSize {
|
| 204 |
+
end := i + runeChunkSize
|
| 205 |
+
if end > len(runeContent) {
|
| 206 |
+
end = len(runeContent)
|
| 207 |
}
|
| 208 |
+
contentPiece := string(runeContent[i:end]) // Convert rune slice back to string
|
| 209 |
|
| 210 |
contentChunk := OpenAIChunk{
|
| 211 |
ID: fullResp.ID,
|
|
|
|
| 221 |
},
|
| 222 |
}
|
| 223 |
if err := sendChunk(w, flusher, logger, contentChunk); err != nil {
|
| 224 |
+
return fmt.Errorf("发送内容块失败: %w", err) // Example Chinese error
|
| 225 |
}
|
| 226 |
time.Sleep(30 * time.Millisecond) // Simulate generation time between chunks
|
| 227 |
}
|
|
|
|
| 243 |
},
|
| 244 |
}
|
| 245 |
if err := sendChunk(w, flusher, logger, toolChunk); err != nil {
|
| 246 |
+
return fmt.Errorf("发送 tool_calls 块失败: %w", err) // Example Chinese error
|
| 247 |
}
|
| 248 |
time.Sleep(10 * time.Millisecond) // Small delay
|
| 249 |
}
|
|
|
|
| 263 |
},
|
| 264 |
}
|
| 265 |
if err := sendChunk(w, flusher, logger, finalChunk); err != nil {
|
| 266 |
+
return fmt.Errorf("发送最终块失败: %w", err) // Example Chinese error
|
| 267 |
}
|
| 268 |
|
| 269 |
// --- Send DONE message ---
|
|
|
|
| 272 |
if err != nil {
|
| 273 |
// Log error, but might not reach client if connection closed
|
| 274 |
logger.Error("Failed to write [DONE] message", "error", err)
|
| 275 |
+
return fmt.Errorf("写入 [DONE] 消息失败: %w", err) // Example Chinese error
|
| 276 |
}
|
| 277 |
flusher.Flush() // Ensure DONE is sent
|
| 278 |
|
|
|
|
| 280 |
}
|
| 281 |
|
| 282 |
// sendChunk encodes the chunk to JSON and writes it in SSE format.
|
| 283 |
+
// (No changes needed here as json.Marshal produces UTF-8 and Fprintf respects it)
|
| 284 |
func sendChunk(w http.ResponseWriter, flusher http.Flusher, logger *slog.Logger, chunk OpenAIChunk) error {
|
| 285 |
+
// json.Marshal correctly handles Go strings (UTF-8) into JSON strings (UTF-8)
|
| 286 |
chunkBytes, err := json.Marshal(chunk)
|
| 287 |
if err != nil {
|
| 288 |
logger.Error("Failed to marshal stream chunk", "error", err, "chunk_id", chunk.ID)
|
| 289 |
+
return fmt.Errorf("块编码失败: %w", err) // Example Chinese error
|
| 290 |
}
|
| 291 |
|
| 292 |
// Write in Server-Sent Event format: data: <json>\n\n
|
| 293 |
+
// fmt.Fprintf handles UTF-8 correctly when writing strings/byte slices
|
| 294 |
_, err = fmt.Fprintf(w, "data: %s\n\n", string(chunkBytes))
|
| 295 |
if err != nil {
|
| 296 |
// Log error, connection might be closed by client
|
| 297 |
logger.Error("Failed to write chunk to response writer", "error", err, "chunk_id", chunk.ID)
|
| 298 |
+
return fmt.Errorf("块写入失败: %w", err) // Example Chinese error
|
| 299 |
}
|
| 300 |
|
| 301 |
// Flush the buffer to send the chunk immediately
|
utils.go
CHANGED
|
@@ -10,7 +10,8 @@ import (
|
|
| 10 |
|
| 11 |
// writeJSONError sends a standard JSON error response.
|
| 12 |
func writeJSONError(w http.ResponseWriter, logger *slog.Logger, statusCode int, message string, errType *string, errCode *string, param *string) {
|
| 13 |
-
|
|
|
|
| 14 |
w.WriteHeader(statusCode)
|
| 15 |
resp := ErrorResponse{
|
| 16 |
Error: APIError{
|
|
|
|
| 10 |
|
| 11 |
// writeJSONError sends a standard JSON error response.
|
| 12 |
func writeJSONError(w http.ResponseWriter, logger *slog.Logger, statusCode int, message string, errType *string, errCode *string, param *string) {
|
| 13 |
+
// CORRECTED: Explicitly add charset=utf-8
|
| 14 |
+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
| 15 |
w.WriteHeader(statusCode)
|
| 16 |
resp := ErrorResponse{
|
| 17 |
Error: APIError{
|