extends Node # ============================================================================ # ApiClient.gd - 后端 JSON API 客户端 # ============================================================================ # 统一处理 WhaleTown 后端 HTTP JSON 请求、认证头和响应信封解析。 # ============================================================================ signal request_failed(endpoint: String, message: String) const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd") const REQUEST_TIMEOUT: float = 12.0 var _activeRequests: Array[HTTPRequest] = [] func _exit_tree() -> void: for request in _activeRequests: if is_instance_valid(request): request.cancel_request() request.queue_free() _activeRequests.clear() func get_json(endpoint: String, callback: Callable, authenticated: bool = true) -> void: request_json(endpoint, {}, callback, HTTPClient.METHOD_GET, authenticated) func post_json(endpoint: String, payload: Dictionary, callback: Callable, authenticated: bool = true) -> void: request_json(endpoint, payload, callback, HTTPClient.METHOD_POST, authenticated) func patch_json(endpoint: String, payload: Dictionary, callback: Callable, authenticated: bool = true) -> void: request_json(endpoint, payload, callback, HTTPClient.METHOD_PATCH, authenticated) func put_json(endpoint: String, payload: Dictionary, callback: Callable, authenticated: bool = true) -> void: request_json(endpoint, payload, callback, HTTPClient.METHOD_PUT, authenticated) func request_json(endpoint: String, payload: Dictionary, callback: Callable, method: int = HTTPClient.METHOD_GET, authenticated: bool = true) -> void: var request := HTTPRequest.new() request.timeout = REQUEST_TIMEOUT add_child(request) _activeRequests.append(request) request.request_completed.connect(func(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void: _activeRequests.erase(request) request.queue_free() _handle_response(endpoint, result, responseCode, body, callback) ) var url := "%s%s" % [NetworkConfig.get_api_base_url(), endpoint] var body := "" if method == HTTPClient.METHOD_GET else JSON.stringify(payload) var err := request.request(url, _headers(authenticated), method, body) if err != OK: _activeRequests.erase(request) request.queue_free() var message := "网络请求发送失败: %s" % error_string(err) request_failed.emit(endpoint, message) callback.call(false, {}, {"message": message}) func _headers(authenticated: bool) -> PackedStringArray: var headers := PackedStringArray(["Content-Type: application/json"]) if authenticated: var authManager := get_node_or_null("/root/AuthManager") var accessToken := str(authManager.call("get_access_token")).strip_edges() if authManager != null and authManager.has_method("get_access_token") else "" if not accessToken.is_empty(): headers.append("Authorization: Bearer %s" % accessToken) return headers func _handle_response(endpoint: String, result: int, responseCode: int, body: PackedByteArray, callback: Callable) -> void: if result != HTTPRequest.RESULT_SUCCESS: var message := "网络请求失败: %s" % _http_result_to_string(result) request_failed.emit(endpoint, message) callback.call(false, {}, {"message": message}) return var bodyText := body.get_string_from_utf8() var json := JSON.new() if json.parse(bodyText) != OK: var parseMessage := "服务器响应解析失败" request_failed.emit(endpoint, parseMessage) callback.call(false, {}, {"message": parseMessage, "response_code": responseCode}) return var payloadVariant: Variant = json.data if not (payloadVariant is Dictionary): var formatMessage := "服务器响应格式错误" request_failed.emit(endpoint, formatMessage) callback.call(false, {}, {"message": formatMessage, "response_code": responseCode}) return var response: Dictionary = payloadVariant var ok := responseCode >= 200 and responseCode < 300 and bool(response.get("success", true)) if ok: callback.call(true, response, {}) return var errorInfo := { "message": str(response.get("message", "请求失败")), "response_code": responseCode, "error_code": str(response.get("error_code", "")) } request_failed.emit(endpoint, str(errorInfo.get("message", "请求失败"))) callback.call(false, response, errorInfo) func _http_result_to_string(result: int) -> String: match result: HTTPRequest.RESULT_CHUNKED_BODY_SIZE_MISMATCH: return "响应分块大小不匹配" HTTPRequest.RESULT_CANT_CONNECT: return "无法连接服务器" HTTPRequest.RESULT_CANT_RESOLVE: return "无法解析服务器地址" HTTPRequest.RESULT_CONNECTION_ERROR: return "连接中断" HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR: return "TLS握手失败" HTTPRequest.RESULT_NO_RESPONSE: return "服务器无响应" HTTPRequest.RESULT_BODY_SIZE_LIMIT_EXCEEDED: return "响应内容过大" HTTPRequest.RESULT_REQUEST_FAILED: return "请求失败" HTTPRequest.RESULT_DOWNLOAD_FILE_CANT_OPEN: return "下载文件无法打开" HTTPRequest.RESULT_DOWNLOAD_FILE_WRITE_ERROR: return "下载文件写入失败" HTTPRequest.RESULT_REDIRECT_LIMIT_REACHED: return "重定向次数过多" HTTPRequest.RESULT_TIMEOUT: return "请求超时" _: return "未知错误 %s" % result