refactor: harden client runtime and exports

This commit is contained in:
ANG-Server
2026-07-23 00:59:00 +08:00
parent 5d2339a29b
commit fc872fe8af
27 changed files with 902 additions and 856 deletions

View File

@@ -25,7 +25,6 @@ const REGISTRATION_CHOICE_SPRITESHEET_GRID_BOX_PATH: String = REGISTRATION_CHOIC
const REGISTRATION_CHOICE_REFERENCE_UPLOAD_BOX_PATH: String = REGISTRATION_CHOICE_ASSET_DIR + "/reference_upload_box.png"
const REGISTRATION_CHOICE_IMAGE_PLACEHOLDER_BOX_PATH: String = REGISTRATION_CHOICE_ASSET_DIR + "/image_placeholder_box.png"
const UI_FONT_PATH: String = "res://assets/fonts/msyh.ttc"
const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd")
const SKIN_GENERATION_CREATE_ENDPOINT: String = "/api/skin-generation/jobs"
const SKIN_GENERATION_POLL_ENDPOINT_TEMPLATE: String = "/api/skin-generation/jobs/%s"
const SKIN_GENERATION_POLL_INTERVAL: float = 2.0
@@ -94,8 +93,6 @@ var _brand_font: SystemFont
var _choice_font: SystemFont
var _resuming_cached_session: bool = false
var _cached_resume_start_generation: int = 0
var _skin_generation_create_request: HTTPRequest
var _skin_generation_poll_request: HTTPRequest
var _skin_generation_active: bool = false
var _skin_generation_job_id: String = ""
var _skin_generation_poll_elapsed: float = 0.0
@@ -686,7 +683,6 @@ func _ready() -> void:
_scene_manager = get_node_or_null("/root/SceneManager")
_appearance_manager = get_node_or_null("/root/AppearanceManager")
_setup_skin_generation_requests()
_connect_signals()
_refresh_appearance_ui()
_show_login()
@@ -727,19 +723,6 @@ func _connect_signals() -> void:
_auth_manager.connect("profile_update_failed", _on_profile_update_failed)
_auth_manager.connect("auth_state_changed", _on_auth_state_changed)
func _setup_skin_generation_requests() -> void:
_skin_generation_create_request = HTTPRequest.new()
_skin_generation_create_request.name = "SkinGenerationCreateRequest"
_skin_generation_create_request.timeout = SKIN_GENERATION_REQUEST_TIMEOUT
_skin_generation_create_request.request_completed.connect(_on_skin_generation_create_completed)
add_child(_skin_generation_create_request)
_skin_generation_poll_request = HTTPRequest.new()
_skin_generation_poll_request.name = "SkinGenerationPollRequest"
_skin_generation_poll_request.timeout = SKIN_GENERATION_REQUEST_TIMEOUT
_skin_generation_poll_request.request_completed.connect(_on_skin_generation_poll_completed)
add_child(_skin_generation_poll_request)
func _process(delta: float) -> void:
if not _skin_generation_active or _skin_generation_job_id.is_empty():
return
@@ -749,10 +732,6 @@ func _process(delta: float) -> void:
_poll_skin_generation_job()
func _exit_tree() -> void:
if is_instance_valid(_skin_generation_create_request):
_skin_generation_create_request.cancel_request()
if is_instance_valid(_skin_generation_poll_request):
_skin_generation_poll_request.cancel_request()
_skin_generation_active = false
_skin_generation_job_id = ""
@@ -1402,23 +1381,27 @@ func _on_generate_skin_pressed() -> void:
"source_image_base64": _image_to_png_base64(sourceImage),
"source_mime_type": "image/png",
}
var err := _skin_generation_create_request.request(
"%s%s" % [NetworkConfig.get_api_base_url(), SKIN_GENERATION_CREATE_ENDPOINT],
_auth_json_headers(),
var api_client := get_node_or_null("/root/ApiClient")
if api_client == null or not api_client.has_method("request_json"):
_skin_generation_active = false
_set_skin_generation_controls_enabled(true)
_set_workshop_generation_status("角色生成服务不可用")
return
api_client.call(
"request_json",
SKIN_GENERATION_CREATE_ENDPOINT,
payload,
_on_skin_generation_create_completed,
HTTPClient.METHOD_POST,
JSON.stringify(payload)
true,
SKIN_GENERATION_REQUEST_TIMEOUT
)
if err != OK:
_skin_generation_active = false
_set_skin_generation_controls_enabled(true)
_set_workshop_generation_status("角色生成请求发送失败:%s" % error_string(err))
func _on_skin_generation_create_completed(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
var parsed := _parse_skin_generation_response(result, responseCode, body)
if not bool(parsed.get("ok", false)):
func _on_skin_generation_create_completed(success: bool, response: Dictionary, error_info: Dictionary) -> void:
if not success:
_skin_generation_active = false
_set_skin_generation_controls_enabled(true)
var errorMessage := str(parsed.get("message", "角色生成任务创建失败"))
var errorMessage := str(error_info.get("message", "角色生成任务创建失败"))
if errorMessage.contains("已经使用过注册角色生成机会") or errorMessage.contains("没有可用的注册角色生成机会"):
_awaiting_registration_skin_generation = false
_set_workshop_generation_status("该账号已完成注册角色生成,正在进入小镇...")
@@ -1427,7 +1410,8 @@ func _on_skin_generation_create_completed(result: int, responseCode: int, _heade
_set_workshop_generation_status(errorMessage)
return
var data: Dictionary = parsed.get("data", {})
var data_variant: Variant = response.get("data", {})
var data: Dictionary = data_variant as Dictionary if data_variant is Dictionary else {}
_skin_generation_job_id = str(data.get("job_id", "")).strip_edges()
if _skin_generation_job_id.is_empty():
_skin_generation_active = false
@@ -1443,26 +1427,33 @@ func _poll_skin_generation_job() -> void:
_skin_generation_poll_in_flight = true
var endpoint := SKIN_GENERATION_POLL_ENDPOINT_TEMPLATE % _skin_generation_job_id
var err := _skin_generation_poll_request.request(
"%s%s" % [NetworkConfig.get_api_base_url(), endpoint],
_auth_json_headers(),
HTTPClient.METHOD_GET
)
if err != OK:
var api_client := get_node_or_null("/root/ApiClient")
if api_client == null or not api_client.has_method("request_json"):
_skin_generation_poll_in_flight = false
_set_workshop_generation_status("查询生成状态失败:%s" % error_string(err))
_set_workshop_generation_status("角色生成服务不可用")
return
api_client.call(
"request_json",
endpoint,
{},
_on_skin_generation_poll_completed,
HTTPClient.METHOD_GET,
true,
SKIN_GENERATION_REQUEST_TIMEOUT
)
func _on_skin_generation_poll_completed(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
func _on_skin_generation_poll_completed(success: bool, response: Dictionary, error_info: Dictionary) -> void:
_skin_generation_poll_in_flight = false
var parsed := _parse_skin_generation_response(result, responseCode, body)
if not bool(parsed.get("ok", false)):
if responseCode == 401 or responseCode == 403 or responseCode == 404:
if not success:
var response_code := int(error_info.get("response_code", 0))
if response_code == 401 or response_code == 403 or response_code == 404:
_skin_generation_active = false
_set_skin_generation_controls_enabled(true)
_set_workshop_generation_status(str(parsed.get("message", "查询生成状态失败")))
_set_workshop_generation_status(str(error_info.get("message", "查询生成状态失败")))
return
var data: Dictionary = parsed.get("data", {})
var data_variant: Variant = response.get("data", {})
var data: Dictionary = data_variant as Dictionary if data_variant is Dictionary else {}
var status := str(data.get("status", "")).strip_edges()
var message := str(data.get("message", "")).strip_edges()
if not message.is_empty():
@@ -1548,73 +1539,6 @@ func _set_skin_generation_controls_enabled(enabled: bool) -> void:
if is_instance_valid(workshop_generate_button):
workshop_generate_button.disabled = not enabled
func _json_headers() -> PackedStringArray:
return PackedStringArray([
"Content-Type: application/json",
"Accept: application/json",
])
func _auth_json_headers() -> PackedStringArray:
var headers := _json_headers()
if _auth_manager != null and _auth_manager.has_method("get_access_token"):
var token := str(_auth_manager.call("get_access_token")).strip_edges()
if not token.is_empty():
headers.append("Authorization: Bearer %s" % token)
return headers
func _parse_skin_generation_response(result: int, responseCode: int, body: PackedByteArray) -> Dictionary:
if result != HTTPRequest.RESULT_SUCCESS:
return {
"ok": false,
"message": "网络请求失败:%s" % _http_result_to_string(result),
}
var bodyText := body.get_string_from_utf8()
var json := JSON.new()
var error := json.parse(bodyText)
if error != OK or not (json.data is Dictionary):
return {
"ok": false,
"message": "服务器响应解析失败",
}
var response: Dictionary = json.data as Dictionary
var success := responseCode >= 200 and responseCode < 300 and bool(response.get("success", true))
if not success:
return {
"ok": false,
"message": str(response.get("message", "请求失败")),
"response_code": responseCode,
}
var dataVariant: Variant = response.get("data", response)
if not (dataVariant is Dictionary):
return {
"ok": false,
"message": "服务器响应格式错误",
}
return {
"ok": true,
"data": dataVariant as Dictionary,
}
func _http_result_to_string(result: int) -> String:
match result:
HTTPRequest.RESULT_SUCCESS:
return "SUCCESS"
HTTPRequest.RESULT_TIMEOUT:
return "TIMEOUT"
HTTPRequest.RESULT_CANT_CONNECT:
return "CANT_CONNECT"
HTTPRequest.RESULT_CANT_RESOLVE:
return "CANT_RESOLVE"
HTTPRequest.RESULT_CONNECTION_ERROR:
return "CONNECTION_ERROR"
HTTPRequest.RESULT_TLS_HANDSHAKE_ERROR:
return "TLS_HANDSHAKE_ERROR"
_:
return "UNKNOWN_%d" % result
func _hide_skin_workshop() -> void:
if _awaiting_registration_skin_generation:

View File

@@ -8,8 +8,8 @@ extends Control
# ============================================================================
const MOVEMENT_ACTIONS: Array[String] = ["move_left", "move_right", "move_up", "move_down"]
const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd")
const API_PATH: String = "/course-resources/datawhale"
const REQUEST_TIMEOUT: float = 18.0
const PANEL_SIZE: Vector2 = Vector2(1500, 1020)
const MIN_MARGIN: Vector2 = Vector2(24, 24)
const CARD_SIZE: Vector2 = Vector2(320, 348)
@@ -24,7 +24,6 @@ var _overlay: ColorRect
var _panel: Control
var _grid: GridContainer
var _statusLabel: Label
var _request: HTTPRequest
var _isOpen: bool = false
var _transitionTween: Tween
@@ -124,10 +123,6 @@ func _buildUi() -> void:
root.add_child(_buildGridPanel())
root.add_child(_buildFooter())
_request = HTTPRequest.new()
_request.timeout = 18.0
_request.request_completed.connect(_onRequestCompleted)
add_child(_request)
_updatePanelAlpha(0.0, 0.0)
func _buildHeader() -> Control:
@@ -222,23 +217,15 @@ func _buildFooter() -> Control:
func _fetchCourses() -> void:
_statusLabel.text = "正在同步 Datawhale 课程..."
_clearChildren(_grid)
var err := _request.request("%s%s" % [NetworkConfig.get_api_base_url(), API_PATH])
if err != OK:
_statusLabel.text = "课程加载失败:%s" % error_string(err)
func _onRequestCompleted(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
if result != HTTPRequest.RESULT_SUCCESS or responseCode < 200 or responseCode >= 300:
_statusLabel.text = "课程加载失败,请稍后再试"
var api_client := get_node_or_null("/root/ApiClient")
if api_client == null or not api_client.has_method("get_json"):
_statusLabel.text = "课程服务不可用"
return
api_client.call("get_json", API_PATH, _onRequestCompleted, false, REQUEST_TIMEOUT)
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
if not parsed is Dictionary:
_statusLabel.text = "课程数据解析失败"
return
var payload: Dictionary = parsed as Dictionary
if not bool(payload.get("success", true)):
_statusLabel.text = str(payload.get("message", "课程加载失败"))
func _onRequestCompleted(success: bool, payload: Dictionary, error_info: Dictionary) -> void:
if not success:
_statusLabel.text = str(error_info.get("message", "课程加载失败,请稍后再试"))
return
var data: Variant = payload.get("data", {})

View File

@@ -1,7 +1,6 @@
extends CanvasLayer
class_name DatawhaleHonorRankingPanel
const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd")
const TEX_PANEL_FRAME = preload("res://assets/ui/datawhale_honor/honor_panel_wood_frame.png")
const TEX_WINDOW_SHELL = preload("res://assets/ui/datawhale_honor/honor_window_shell.png")
const TEX_PARCHMENT_PANEL = preload("res://assets/ui/datawhale_honor/honor_parchment_panel.png")
@@ -37,6 +36,7 @@ const TEX_AVATAR_FALLBACK_3 = preload("res://assets/ui/auth/generated/auth_chara
const API_PATH: String = "/rankings/datawhale-honor"
const PANEL_SIZE: Vector2 = Vector2(1500, 850)
const REQUEST_LIMIT: int = 9
const REQUEST_TIMEOUT: float = 18.0
const LOWER_RANK_LIMIT: int = 6
const LOWER_RANK_SLOT_HEIGHT: float = 45.0
const LOWER_RANK_AVATAR_SIZE: float = 30.0
@@ -85,8 +85,8 @@ var _podiumRow: Control
var _rankList: VBoxContainer
var _footerLabel: Label
var _statusLabel: Label
var _request: HTTPRequest
var _avatarRequests: Array[HTTPRequest] = []
var _rankingRequestGeneration: int = 0
var _chatUi: Control
var _chatUiPrevMouseFilter: Control.MouseFilter = Control.MOUSE_FILTER_STOP
var _chatUiMouseDisabled: bool = false
@@ -186,10 +186,6 @@ func _buildUi() -> void:
_panel.add_child(_buildRuleOverlay())
_panel.add_child(_buildCloseButton())
_request = HTTPRequest.new()
_request.timeout = 18.0
_request.request_completed.connect(_onRequestCompleted)
add_child(_request)
_setLoadingState()
func _buildBoardBody() -> Control:
@@ -252,38 +248,28 @@ func _positionPanel() -> void:
_panel.position = (viewportSize - PANEL_SIZE * scaleFactor) * 0.5
func _fetchRanking(categoryId: String) -> void:
if _request == null:
return
if _isLoading:
_request.cancel_request()
_activeCategory = categoryId
_isLoading = true
_rankingRequestGeneration += 1
_setLoadingState()
var endpoint := "%s%s?category=%s&limit=%d&refresh=true" % [
NetworkConfig.get_api_base_url(),
var endpoint := "%s?category=%s&limit=%d&refresh=true" % [
API_PATH,
_urlEncode(_activeCategory),
REQUEST_LIMIT,
]
var err := _request.request(endpoint, PackedStringArray(), HTTPClient.METHOD_GET, "")
if err != OK:
var api_client := get_node_or_null("/root/ApiClient")
if api_client == null or not api_client.has_method("get_json"):
_isLoading = false
_setErrorState("荣誉榜请求失败:%s" % error_string(err))
_setErrorState("荣誉榜服务不可用")
return
api_client.call("get_json", endpoint, Callable(self, "_onRequestCompleted").bind(_rankingRequestGeneration), false, REQUEST_TIMEOUT)
func _onRequestCompleted(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
func _onRequestCompleted(success: bool, payload: Dictionary, error_info: Dictionary, request_generation: int) -> void:
if request_generation != _rankingRequestGeneration:
return
_isLoading = false
if result != HTTPRequest.RESULT_SUCCESS or responseCode < 200 or responseCode >= 300:
_setErrorState("荣誉榜加载失败,请稍后再试")
return
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
if not parsed is Dictionary:
_setErrorState("荣誉榜数据解析失败")
return
var payload := parsed as Dictionary
if not bool(payload.get("success", true)):
_setErrorState(str(payload.get("message", "荣誉榜加载失败")))
if not success:
_setErrorState(str(error_info.get("message", "荣誉榜加载失败,请稍后再试")))
return
var dataVariant: Variant = payload.get("data", {})

View File

@@ -20,7 +20,6 @@ const MALL_BADGE_TEXTURE: Texture2D = preload("res://assets/ui/mall/branding/bra
const COIN_TEXTURE: Texture2D = preload("res://assets/ui/mall/branding/branding_whale_coin.png")
const CATALOG_SCRIPT: Script = preload("res://Config/MallCatalog.gd")
const ITEM_CARD_SCRIPT: Script = preload("res://scenes/ui/mall/MallItemCard.gd")
const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd")
var _overlay: ColorRect
var _panel: Control
@@ -39,8 +38,6 @@ var _toastLabel: Label
var _confirmDialog: Control
var _confirmLabel: Label
var _currencyLabel: Label
var _purchaseRequest: HTTPRequest
var _catalogRequest: HTTPRequest
var _categoryButtons: Dictionary = {}
var _cards: Array[Button] = []
@@ -175,8 +172,6 @@ func _build_ui() -> void:
_build_toast()
_build_confirm_dialog()
_build_purchase_request()
_build_catalog_request()
_update_panel_alpha(0.0, 0.0)
func _build_header() -> Control:
@@ -859,20 +854,6 @@ func _format_item_price(item: Dictionary) -> String:
func _is_skin_item(item: Dictionary) -> bool:
return not str(item.get("skinId", "")).strip_edges().is_empty()
func _build_purchase_request() -> void:
_purchaseRequest = HTTPRequest.new()
_purchaseRequest.name = "mallPurchaseRequest"
_purchaseRequest.timeout = 12.0
_purchaseRequest.request_completed.connect(_on_purchase_request_completed)
add_child(_purchaseRequest)
func _build_catalog_request() -> void:
_catalogRequest = HTTPRequest.new()
_catalogRequest.name = "mallCatalogRequest"
_catalogRequest.timeout = 12.0
_catalogRequest.request_completed.connect(_on_catalog_request_completed)
add_child(_catalogRequest)
func _connect_auth_state() -> void:
var authManager := get_node_or_null("/root/AuthManager")
if authManager == null or not authManager.has_signal("auth_state_changed"):
@@ -907,10 +888,6 @@ func _current_account_generation() -> int:
return -1
func _cancel_account_bound_requests() -> void:
if is_instance_valid(_catalogRequest):
_catalogRequest.cancel_request()
if is_instance_valid(_purchaseRequest):
_purchaseRequest.cancel_request()
_catalogRequestAccountGeneration = -1
_purchaseRequestAccountGeneration = -1
@@ -926,14 +903,6 @@ func _reset_account_bound_state() -> void:
_selectedItem = {}
_hide_confirm_dialog()
func _auth_headers() -> PackedStringArray:
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 ""
return PackedStringArray([
"Content-Type: application/json",
"Authorization: Bearer %s" % accessToken,
])
func _submit_backend_purchase(item: Dictionary) -> void:
var authManager := get_node_or_null("/root/AuthManager")
var accessToken := str(authManager.call("get_access_token")).strip_edges() if authManager != null else ""
@@ -949,43 +918,24 @@ func _submit_backend_purchase(item: Dictionary) -> void:
"item_id": str(item.get("id", "")),
}
_purchaseRequestAccountGeneration = _current_account_generation()
var err := _purchaseRequest.request("%s/shop/purchases" % NetworkConfig.get_api_base_url(), _auth_headers(), HTTPClient.METHOD_POST, JSON.stringify(payload))
if err != OK:
var api_client := get_node_or_null("/root/ApiClient")
if api_client == null or not api_client.has_method("post_json"):
_purchaseRequestAccountGeneration = -1
_hide_confirm_dialog()
_emit_event(EventNames.MALL_PURCHASE_FAILED, {"item": item, "reason": "request_failed"})
_show_toast("购买请求发送失败")
_show_toast("购买服务不可用")
_update_purchase_button(_selectedItem)
return
api_client.call("post_json", "/shop/purchases", payload, _on_purchase_request_completed, true)
func _on_purchase_request_completed(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
func _on_purchase_request_completed(success: bool, response: Dictionary, error_info: Dictionary) -> void:
if _purchaseRequestAccountGeneration != _current_account_generation():
return
_purchaseRequestAccountGeneration = -1
if result != HTTPRequest.RESULT_SUCCESS:
_hide_confirm_dialog()
_emit_event(EventNames.MALL_PURCHASE_FAILED, {"item": _selectedItem, "reason": "network_failed"})
_show_toast("购买失败,请稍后再试")
_update_purchase_button(_selectedItem)
return
var json := JSON.new()
if json.parse(body.get_string_from_utf8()) != OK:
_hide_confirm_dialog()
_emit_event(EventNames.MALL_PURCHASE_FAILED, {"item": _selectedItem, "reason": "invalid_response"})
_show_toast("购买响应解析失败")
_update_purchase_button(_selectedItem)
return
var responseVariant: Variant = json.data
if not (responseVariant is Dictionary):
_hide_confirm_dialog()
_emit_event(EventNames.MALL_PURCHASE_FAILED, {"item": _selectedItem, "reason": "invalid_response"})
_show_toast("购买响应格式错误")
_update_purchase_button(_selectedItem)
return
var response: Dictionary = responseVariant
if responseCode < 200 or responseCode >= 300 or not bool(response.get("success", true)):
if not success:
_hide_confirm_dialog()
_emit_event(EventNames.MALL_PURCHASE_FAILED, {"item": _selectedItem, "reason": "backend_rejected"})
_show_toast(str(response.get("message", "购买失败")))
_show_toast(str(error_info.get("message", "购买失败")))
_update_purchase_button(_selectedItem)
return
var dataVariant: Variant = response.get("data", {})
@@ -1030,37 +980,19 @@ func _fetch_catalog() -> void:
_walletError = ""
_update_currency()
_catalogRequestAccountGeneration = _current_account_generation()
var err := _catalogRequest.request("%s/shop/catalog" % NetworkConfig.get_api_base_url(), _auth_headers(), HTTPClient.METHOD_GET, "")
if err != OK:
var api_client := get_node_or_null("/root/ApiClient")
if api_client == null or not api_client.has_method("get_json"):
_catalogRequestAccountGeneration = -1
_catalogLoading = false
_catalogLoaded = false
_catalogError = "商城数据请求发送失败"
_walletLoading = false
_walletLoaded = false
_walletError = _catalogError
_update_currency()
_render_grid([])
push_warning("MallPanel: 商城数据请求发送失败: %s" % error_string(err))
_apply_catalog_error("商城服务不可用")
return
api_client.call("get_json", "/shop/catalog", _on_catalog_request_completed, true)
func _on_catalog_request_completed(result: int, responseCode: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
func _on_catalog_request_completed(success: bool, response: Dictionary, error_info: Dictionary) -> void:
if _catalogRequestAccountGeneration != _current_account_generation():
return
_catalogRequestAccountGeneration = -1
if result != HTTPRequest.RESULT_SUCCESS or responseCode < 200 or responseCode >= 300:
_apply_catalog_error("商城数据读取失败")
return
var json := JSON.new()
if json.parse(body.get_string_from_utf8()) != OK:
_apply_catalog_error("商城响应解析失败")
return
var responseVariant: Variant = json.data
if not (responseVariant is Dictionary):
_apply_catalog_error("商城响应格式错误")
return
var response: Dictionary = responseVariant
if not bool(response.get("success", true)):
_apply_catalog_error(str(response.get("message", "商城数据读取失败")))
if not success:
_apply_catalog_error(str(error_info.get("message", "商城数据读取失败")))
return
var dataVariant: Variant = response.get("data", {})
if not (dataVariant is Dictionary):