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

@@ -18,32 +18,25 @@ signal profile_update_succeeded(profile: Dictionary)
signal profile_update_failed(message: String)
signal logout_completed()
const NetworkConfig = preload("res://_Core/utils/NetworkConfig.gd")
const SecureSessionStore = preload("res://_Core/security/SecureSessionStore.gd")
const DEFAULT_AUTH_CONFIG_PATH: String = "user://auth.cfg"
const REQUEST_TIMEOUT: float = 12.0
const DEFAULT_AUTH_CONFIG_PATH: String = "user://auth.secure"
const LEGACY_AUTH_CONFIG_PATH: String = "user://auth.cfg"
var _access_token: String = ""
var _refresh_token: String = ""
var _current_user: Dictionary = {}
var _current_profile: Dictionary = {}
var _active_requests: Array[HTTPRequest] = []
var _session_generation: int = 0
var _account_generation: int = 0
var _refresh_in_flight: bool = false
var _auth_config_path: String = DEFAULT_AUTH_CONFIG_PATH
var _show_welcome_after_registration: bool = false
var _secure_session_store: RefCounted = SecureSessionStore.new()
func _ready() -> void:
_load_cached_session()
func _exit_tree() -> void:
for request in _active_requests:
if is_instance_valid(request):
request.cancel_request()
request.queue_free()
_active_requests.clear()
func is_authenticated() -> bool:
return not _access_token.strip_edges().is_empty()
@@ -213,69 +206,30 @@ func refresh_session() -> void:
func logout() -> void:
_clear_session_memory()
if FileAccess.file_exists(_auth_config_path):
DirAccess.remove_absolute(_auth_config_path)
_secure_session_store.call("clear", _auth_config_path)
auth_state_changed.emit(false, {})
_emit_event(EventNames.AUTH_LOGOUT, {})
logout_completed.emit()
call_deferred("_return_to_auth_scene")
func _request_json(endpoint: String, payload: Dictionary, callback: Callable, method: int = HTTPClient.METHOD_POST, authenticated: bool = false, accountBound: bool = false) -> void:
var request := HTTPRequest.new()
request.timeout = REQUEST_TIMEOUT
request.set_meta("account_bound", accountBound)
add_child(request)
_active_requests.append(request)
var requestGeneration := _account_generation
request.request_completed.connect(func(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void:
_active_requests.erase(request)
request.queue_free()
if accountBound and requestGeneration != _account_generation:
return
_handle_json_response(result, response_code, body, callback)
var api_client := get_node_or_null("/root/ApiClient")
if api_client == null or not api_client.has_method("request_json"):
callback.call(false, {}, {"message": "ApiClient 不可用"})
return
var request_generation := _account_generation
api_client.call(
"request_json",
endpoint,
payload,
func(success: bool, response: Dictionary, error_info: Dictionary) -> void:
if accountBound and request_generation != _account_generation:
return
callback.call(success, response, error_info),
method,
authenticated
)
var url := "%s%s" % [NetworkConfig.get_api_base_url(), endpoint]
var headers := PackedStringArray(["Content-Type: application/json"])
if authenticated and not _access_token.strip_edges().is_empty():
headers.append("Authorization: Bearer %s" % _access_token)
var body := "" if method == HTTPClient.METHOD_GET else JSON.stringify(payload)
var err := request.request(url, headers, method, body)
if err != OK:
_active_requests.erase(request)
request.queue_free()
if not accountBound or requestGeneration == _account_generation:
callback.call(false, {}, {"message": "网络请求发送失败: %s" % error_string(err)})
func _handle_json_response(result: int, response_code: int, body: PackedByteArray, callback: Callable) -> void:
var body_text := body.get_string_from_utf8()
if result != HTTPRequest.RESULT_SUCCESS:
callback.call(false, {}, {"message": "网络请求失败: %s" % _http_result_to_string(result)})
return
var json := JSON.new()
if json.parse(body_text) != OK:
callback.call(false, {}, {"message": "服务器响应解析失败"})
return
var payload_variant: Variant = json.data
if not (payload_variant is Dictionary):
callback.call(false, {}, {"message": "服务器响应格式错误"})
return
var response: Dictionary = payload_variant
var success := response_code >= 200 and response_code < 300 and bool(response.get("success", true))
if success:
callback.call(true, response, {})
return
callback.call(false, response, {
"message": str(response.get("message", "请求失败")),
"response_code": response_code,
"error_code": str(response.get("error_code", ""))
})
func _on_login_response(success: bool, data: Dictionary, error_info: Dictionary) -> void:
if not success:
login_failed.emit(str(error_info.get("message", "登录失败")))
@@ -415,35 +369,34 @@ func _refresh_player_snapshot() -> void:
playerStateManager.call_deferred("refresh_snapshot")
func _save_cached_session() -> void:
var config := ConfigFile.new()
config.set_value("auth", "refresh_token", _refresh_token)
config.set_value("auth", "access_token", _access_token)
config.set_value("auth", "saved_at", Time.get_unix_time_from_system())
var err := config.save(_auth_config_path)
if err != OK:
push_warning("AuthManager: 保存本地登录状态失败: %s" % error_string(err))
if _refresh_token.is_empty():
_secure_session_store.call("clear", _auth_config_path)
return
if not bool(_secure_session_store.call("save_refresh_token", _auth_config_path, _refresh_token)):
push_warning("AuthManager: 当前平台不支持安全持久化,将仅保留本次会话")
func _load_cached_session(emit_cached_state: bool = true) -> void:
if not FileAccess.file_exists(_auth_config_path):
return
var config := ConfigFile.new()
if config.load(_auth_config_path) != OK:
return
_refresh_token = str(config.get_value("auth", "refresh_token", "")).strip_edges()
_access_token = str(config.get_value("auth", "access_token", "")).strip_edges()
_migrate_legacy_cached_session()
_refresh_token = str(_secure_session_store.call("load_refresh_token", _auth_config_path)).strip_edges()
_access_token = ""
_current_user.clear()
_current_profile.clear()
if _refresh_token.is_empty():
return
_session_generation += 1
_account_generation += 1
if emit_cached_state:
call_deferred("refresh_session")
if is_authenticated():
_session_generation += 1
_account_generation += 1
if emit_cached_state and is_authenticated():
call_deferred("_emit_cached_auth_state")
func _emit_cached_auth_state() -> void:
auth_state_changed.emit(true, get_current_user())
func _migrate_legacy_cached_session() -> void:
if not FileAccess.file_exists(LEGACY_AUTH_CONFIG_PATH):
return
var config := ConfigFile.new()
if config.load(LEGACY_AUTH_CONFIG_PATH) == OK:
var legacy_refresh_token := str(config.get_value("auth", "refresh_token", "")).strip_edges()
if not legacy_refresh_token.is_empty():
_secure_session_store.call("save_refresh_token", _auth_config_path, legacy_refresh_token)
DirAccess.remove_absolute(ProjectSettings.globalize_path(LEGACY_AUTH_CONFIG_PATH))
func _clear_session_memory() -> void:
_access_token = ""
@@ -456,11 +409,6 @@ func _advance_account_generation() -> void:
_session_generation += 1
_account_generation += 1
_refresh_in_flight = false
for request in _active_requests.duplicate():
if is_instance_valid(request) and bool(request.get_meta("account_bound", false)):
request.cancel_request()
request.queue_free()
_active_requests.erase(request)
func _return_to_auth_scene() -> void:
var tree := get_tree()
@@ -503,20 +451,3 @@ func _password_has_letter_and_number(password: String) -> bool:
if code >= 48 and code <= 57:
has_number = true
return has_letter and has_number
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