diff --git a/apps/ai-game-creator-shell/src-tauri/src/platform_session.rs b/apps/ai-game-creator-shell/src-tauri/src/platform_session.rs index c76a3deb3..359ad50f5 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/platform_session.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/platform_session.rs @@ -126,13 +126,12 @@ fn normalize_platform_api_base_url(value: &str) -> Result { let host = parsed .host_str() .ok_or_else(|| "陶泥儿服务地址缺少 host".to_string())?; - let production = matches!(host, "www.genarrative.world" | "dev.genarrative.world"); let loopback = host == "localhost" || host == "127.0.0.1" || host .parse::() .is_ok_and(|ip| ip.is_loopback()); - if !production && !(cfg!(debug_assertions) && loopback && parsed.scheme() == "http") { + if parsed.scheme() == "http" && !(cfg!(debug_assertions) && loopback) { return Err("陶泥儿服务地址不在受信任白名单内".to_string()); } Ok(value.to_string()) @@ -342,6 +341,20 @@ mod tests { ); } + #[test] + fn custom_server_selection_accepts_https_origins_and_rejects_plain_http() { + assert_eq!( + normalize_platform_api_base_url("https://staging.example.com/"), + Ok("https://staging.example.com".to_string()) + ); + assert_eq!( + normalize_platform_api_base_url("http://127.0.0.1:8080/"), + Ok("http://127.0.0.1:8080".to_string()) + ); + assert!(normalize_platform_api_base_url("http://staging.example.com").is_err()); + assert!(normalize_platform_api_base_url("https://staging.example.com/api").is_err()); + } + #[test] fn frozen_platform_session_rejects_logout_account_switch_and_token_rotation() { let expected = PlatformSessionSnapshot { diff --git a/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx b/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx index 80cfec5b6..d7efba85a 100644 --- a/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx +++ b/apps/ai-game-creator-shell/src/app/AuthenticatedClient.tsx @@ -20,6 +20,15 @@ import { normalizeAuthPhoneInput, sendClientPhoneLoginCode, } from '../services/clientAuth'; +import { + AGC_DEVELOPMENT_API_BASE_URL, + AGC_RELEASE_API_BASE_URL, + type ClientServerPreset, + type ClientServerSelection, + getClientServerSelection, + normalizeClientServerBaseUrl, + setClientServerSelection, +} from '../services/clientHttp'; import { beginPlatformSessionTransition, clearCommittedPlatformSession, @@ -95,6 +104,46 @@ export function AuthenticatedClient({ const [loginBusy, setLoginBusy] = useState(false); const [codeBusy, setCodeBusy] = useState(false); const [codeCooldownSeconds, setCodeCooldownSeconds] = useState(0); + const initialServerSelection = getClientServerSelection(); + const [serverSelection, setServerSelection] = useState( + initialServerSelection, + ); + const [customServerUrl, setCustomServerUrl] = useState( + initialServerSelection.customBaseUrl, + ); + const selectedServerAddress = + serverSelection.preset === 'release' + ? AGC_RELEASE_API_BASE_URL + : serverSelection.preset === 'dev' + ? AGC_DEVELOPMENT_API_BASE_URL + : customServerUrl || '请输入自定义服务器地址'; + + function applyServerSelection() { + try { + const next = setClientServerSelection({ + preset: serverSelection.preset, + customBaseUrl: customServerUrl, + }); + setServerSelection(next); + return true; + } catch (error) { + setLoginStatus(error instanceof Error ? error.message : String(error)); + return false; + } + } + + function handleServerPresetChange(preset: ClientServerPreset) { + if (preset === 'custom') { + setServerSelection((current) => ({ ...current, preset })); + return; + } + const next = setClientServerSelection({ + preset, + customBaseUrl: customServerUrl, + }); + setServerSelection(next); + setLoginStatus(`已选择 ${preset} 服务器`); + } useEffect(() => { let disposed = false; @@ -229,6 +278,9 @@ export function AuthenticatedClient({ if (codeBusy || codeCooldownSeconds > 0) { return; } + if (!applyServerSelection()) { + return; + } const normalizedPhone = normalizeAuthPhoneInput(phone); if (!normalizedPhone) { setLoginStatus('请输入手机号'); @@ -265,6 +317,9 @@ export function AuthenticatedClient({ setLoginStatus('请输入密码'); return; } + if (!applyServerSelection()) { + return; + } setLoginBusy(true); setLoginStatus('正在登录'); const loginGeneration = beginPlatformSessionTransition(); @@ -324,6 +379,51 @@ export function AuthenticatedClient({

登录陶泥儿 GameAgent

登录后进入首页和本地项目工作区

+ + {serverSelection.preset === 'custom' ? ( + + ) : null}