From 9405d00db655a844455ecd81733ccf068482c632 Mon Sep 17 00:00:00 2001 From: wanxiaoT Date: Mon, 6 Jul 2026 10:50:19 +0800 Subject: [PATCH] fix: stabilize Windows Codex launch --- assets/inject/renderer-inject.js | 258 +++++++++++++++++++-- crates/codex-plus-core/src/launcher.rs | 36 ++- crates/codex-plus-core/src/ports.rs | 13 +- crates/codex-plus-core/tests/cdp_bridge.rs | 45 ++++ crates/codex-plus-core/tests/launcher.rs | 4 +- 5 files changed, 328 insertions(+), 28 deletions(-) diff --git a/assets/inject/renderer-inject.js b/assets/inject/renderer-inject.js index 4bd8a8e5a..6501a7e37 100644 --- a/assets/inject/renderer-inject.js +++ b/assets/inject/renderer-inject.js @@ -1445,6 +1445,110 @@ return await codexServiceTierModulePromises.get(namePart); } + async function loadCodexAppModuleAny(nameParts) { + let lastError = null; + for (const namePart of nameParts) { + try { + return await loadCodexAppModule(namePart); + } catch (error) { + lastError = error; + } + } + throw lastError || new Error(`未找到 Codex App asset: ${nameParts.join(", ")}`); + } + + function codexAppServerReactKeys(element) { + return Object.keys(element || {}).filter((key) => key.startsWith("__reactFiber") || key.startsWith("__reactInternalInstance") || key.startsWith("__reactProps")); + } + + function codexAppScopeCandidates() { + const roots = [ + document.getElementById("root"), + document.body, + ...Array.from(document.querySelectorAll("main, aside, [role='dialog'], [role='menu'], [role='listbox'], [data-radix-popper-content-wrapper]") || []), + ].filter(Boolean); + const queue = []; + for (const node of roots) { + for (const key of codexAppServerReactKeys(node)) { + let fiber = node[key]; + for (let depth = 0; fiber && depth < 80; depth += 1, fiber = fiber.return) { + queue.push(fiber); + let context = fiber.dependencies?.firstContext; + for (let contextDepth = 0; context && contextDepth < 50; contextDepth += 1, context = context.next) { + queue.push(context.memoizedValue); + } + } + } + } + + const scopes = []; + const visited = new WeakSet(); + for (let index = 0; index < queue.length && index < 12000 && scopes.length < 80; index += 1) { + const value = queue[index]; + if (!value || typeof value !== "object" || visited.has(value)) continue; + visited.add(value); + let keys = []; + try { + keys = Object.keys(value); + } catch { + continue; + } + if (keys.includes("chain") && keys.includes("node") && typeof value.get === "function" && typeof value.set === "function" && typeof value.watch === "function") { + scopes.push(value); + } + for (const key of keys.slice(0, 80)) { + if (key === "ownerDocument" || key === "parentElement" || key === "parentNode" || key === "children" || key === "childNodes") continue; + try { + const child = value[key]; + if (child && (typeof child === "object" || typeof child === "function")) queue.push(child); + } catch { + } + } + } + return scopes; + } + + function isCodexAppServerRegistryClass(value) { + return typeof value === "function" + && typeof value.prototype?.getAll === "function" + && typeof value.prototype?.getDefault === "function" + && typeof value.prototype?.getForHostId === "function"; + } + + function pushCodexAppServerCandidate(candidates, visited, value) { + if (!value || typeof value !== "object" || visited.has(value)) return; + visited.add(value); + candidates.push(value); + try { + if (value.requestClient && typeof value.requestClient === "object") pushCodexAppServerCandidate(candidates, visited, value.requestClient); + } catch { + } + } + + function codexAppServerRequestCandidates(module) { + const candidates = []; + const visited = new WeakSet(); + for (const value of Object.values(module || {})) { + if (value && typeof value === "object") pushCodexAppServerCandidate(candidates, visited, value); + } + + const Registry = Object.values(module || {}).find(isCodexAppServerRegistryClass); + if (Registry) { + for (const scope of codexAppScopeCandidates()) { + try { + const registry = new Registry(scope); + const all = typeof registry.getAll === "function" ? registry.getAll() : []; + if (Array.isArray(all)) all.forEach((manager) => pushCodexAppServerCandidate(candidates, visited, manager)); + pushCodexAppServerCandidate(candidates, visited, registry.getDefault?.()); + pushCodexAppServerCandidate(candidates, visited, registry.getForHostId?.("local")); + pushCodexAppServerCandidate(candidates, visited, registry.getImplForHostId?.("local")); + } catch { + } + } + } + return candidates; + } + async function codexSettingStorageModule() { const module = await loadCodexAppModule("setting-storage-"); if (typeof module.n !== "function" || typeof module.s !== "function") { @@ -2074,8 +2178,89 @@ return message; } + function applyCodexServiceTierRequestClientParams(method, params) { + const requestMethod = appServerModelRequestMethod(String(method || ""), params); + const directParams = applyCodexServiceTierRequestOverride(requestMethod, params); + if (directParams !== params) return directParams; + + if (!params || typeof params !== "object" || !params.params || typeof params.params !== "object") { + return params; + } + const nestedMethod = appServerModelRequestMethod(String(params.method || method || ""), params.params); + const nestedParams = applyCodexServiceTierRequestOverride( + nestedMethod, + params.params, + params.conversationId || params.threadId || "" + ); + return nestedParams === params.params ? params : { ...params, params: nestedParams }; + } + + function patchCodexServiceTierRequestClient(client) { + if (!client || typeof client.sendRequest !== "function") return false; + if (client.__codexServiceTierRequestPatch === codexServiceTierRequestOverrideVersion) return true; + const originalSendRequest = client.__codexServiceTierOriginalSendRequest || client.sendRequest.bind(client); + client.__codexServiceTierOriginalSendRequest = originalSendRequest; + client.sendRequest = function codexServiceTierPatchedSendRequest(method, params, options) { + const nextParams = applyCodexServiceTierRequestClientParams(method, params); + return originalSendRequest(method, nextParams, options); + }; + client.__codexServiceTierRequestPatch = codexServiceTierRequestOverrideVersion; + return true; + } + + function installCodexServiceTierAppServerRequestPatch() { + if (window.__codexServiceTierAppServerRequestPatchInstalled === codexServiceTierRequestOverrideVersion) return; + const patch = async () => { + try { + const module = await loadCodexAppModuleAny([ + "app-server-manager-signals-", + "app-server-manager-hooks-", + ]); + const candidates = codexAppServerRequestCandidates(module); + let patchedCount = 0; + for (const candidate of candidates) { + if (patchCodexServiceTierRequestClient(candidate)) patchedCount += 1; + if (typeof candidate.sendRequest !== "function" && typeof candidate.get === "function") { + try { + if (patchCodexServiceTierRequestClient(candidate.get())) patchedCount += 1; + } catch { + } + } + } + if (patchedCount > 0) { + window.__codexServiceTierAppServerRequestPatchInstalled = codexServiceTierRequestOverrideVersion; + window.__codexServiceTierRequestOverrideInstalled = codexServiceTierRequestOverrideVersion; + sendCodexPlusDiagnostic("service_tier_app_server_request_patch_installed", { + candidateCount: candidates.length, + patchedCount, + }); + } else { + sendCodexPlusDiagnosticOnce( + "service-tier-app-server-request-patch-not-found", + "service_tier_app_server_request_patch_not_found", + { + exportCount: Object.keys(module || {}).length, + candidateCount: candidates.length, + } + ); + } + } catch (error) { + sendCodexPlusDiagnosticOnce( + "service-tier-app-server-request-patch-failed", + "service_tier_app_server_request_patch_failed", + { + errorName: error?.name || "", + errorMessage: error?.message || String(error), + } + ); + } + }; + void patch(); + } + function installCodexServiceTierDispatcherPatch() { if (window.__codexServiceTierRequestOverrideInstalled === codexServiceTierRequestOverrideVersion) return; + installCodexServiceTierAppServerRequestPatch(); const patch = async () => { try { const module = await loadCodexAppModule("setting-storage-"); @@ -2096,10 +2281,14 @@ window.__codexServiceTierRequestOverrideInstalled = codexServiceTierRequestOverrideVersion; sendCodexPlusDiagnostic("service_tier_dispatcher_patch_installed", {}); } catch (error) { - sendCodexPlusDiagnostic("service_tier_dispatcher_patch_failed", { - errorName: error?.name || "", - errorMessage: error?.message || String(error), - }); + sendCodexPlusDiagnosticOnce( + "service-tier-dispatcher-patch-failed", + "service_tier_dispatcher_patch_failed", + { + errorName: error?.name || "", + errorMessage: error?.message || String(error), + } + ); } }; void patch(); @@ -3232,6 +3421,10 @@ function runPluginAutoExpand(force = false) { if (!codexPlusSettings().pluginAutoExpand) return; const currentSignature = pluginAutoExpandSignature(); + if (!currentSignature) { + window.__codexPluginAutoExpandLastSignature = ""; + return; + } if (!force && currentSignature && currentSignature === window.__codexPluginAutoExpandLastSignature) return; window.__codexPluginAutoExpandLastSignature = currentSignature; window.__codexPluginAutoExpandRunning = true; @@ -3244,11 +3437,13 @@ const button = pluginAutoExpandButtonCandidates()[0]; if (!button || window.__codexPluginAutoExpandClicks >= codexPluginAutoExpandMaxClicks) { window.__codexPluginAutoExpandRunning = false; - sendCodexPlusDiagnostic("plugin_auto_expand_finished", { - version: codexPluginAutoExpandVersion, - clicks: window.__codexPluginAutoExpandClicks || 0, - exhausted: !!button, - }); + if ((window.__codexPluginAutoExpandClicks || 0) > 0 || !!button) { + sendCodexPlusDiagnostic("plugin_auto_expand_finished", { + version: codexPluginAutoExpandVersion, + clicks: window.__codexPluginAutoExpandClicks || 0, + exhausted: !!button, + }); + } return; } window.__codexPluginAutoExpandClicks = (window.__codexPluginAutoExpandClicks || 0) + 1; @@ -3505,8 +3700,11 @@ if (!codexPlusSettings().pluginMarketplaceUnlock) return; const patch = async () => { try { - const module = await loadCodexAppModule("app-server-manager-signals-"); - const candidates = Object.values(module).filter((value) => value && typeof value === "object"); + const module = await loadCodexAppModuleAny([ + "app-server-manager-signals-", + "app-server-manager-hooks-", + ]); + const candidates = codexAppServerRequestCandidates(module); let patchedCount = 0; for (const candidate of candidates) { if (patchPluginMarketplaceRequestClient(candidate)) patchedCount += 1; @@ -3530,10 +3728,14 @@ }); } } catch (error) { - sendCodexPlusDiagnostic("plugin_marketplace_request_patch_failed", { - errorName: error?.name || "", - errorMessage: error?.message || String(error), - }); + sendCodexPlusDiagnosticOnce( + "plugin-marketplace-request-patch-failed", + "plugin_marketplace_request_patch_failed", + { + errorName: error?.name || "", + errorMessage: error?.message || String(error), + } + ); } }; void patch(); @@ -3757,6 +3959,13 @@ }).catch(() => {}); } + function sendCodexPlusDiagnosticOnce(key, event, detail) { + window.__codexPlusDiagnosticOnce = window.__codexPlusDiagnosticOnce || new Set(); + if (window.__codexPlusDiagnosticOnce.has(key)) return; + window.__codexPlusDiagnosticOnce.add(key); + sendCodexPlusDiagnostic(event, detail); + } + sendCodexPlusDiagnostic("script_loaded", { version: codexPlusVersion, build: codexPlusBuild, @@ -4983,8 +5192,11 @@ if (window.__codexPlusAppServerModelRequestPatchInstalled === codexAppServerModelRequestPatchVersion) return; const patch = async () => { try { - const module = await loadCodexAppModule("app-server-manager-signals-"); - const candidates = Object.values(module).filter((value) => value && typeof value === "object"); + const module = await loadCodexAppModuleAny([ + "app-server-manager-signals-", + "app-server-manager-hooks-", + ]); + const candidates = codexAppServerRequestCandidates(module); let patchedCount = 0; for (const candidate of candidates) { if (patchAppServerModelRequestClient(candidate)) patchedCount += 1; @@ -5008,10 +5220,14 @@ }); } } catch (error) { - sendCodexPlusDiagnostic("model_app_server_request_patch_failed", { - errorName: error?.name || "", - errorMessage: error?.message || String(error), - }); + sendCodexPlusDiagnosticOnce( + "model-app-server-request-patch-failed", + "model_app_server_request_patch_failed", + { + errorName: error?.name || "", + errorMessage: error?.message || String(error), + } + ); } }; void patch(); diff --git a/crates/codex-plus-core/src/launcher.rs b/crates/codex-plus-core/src/launcher.rs index 75a851c2c..6b67657a7 100644 --- a/crates/codex-plus-core/src/launcher.rs +++ b/crates/codex-plus-core/src/launcher.rs @@ -385,8 +385,28 @@ fn relay_protocol_proxy_enabled(settings: &BackendSettings) -> bool { } fn select_native_menu_inspector_port(debug_port: u16) -> u16 { + select_native_menu_inspector_port_with( + debug_port, + cfg!(windows), + crate::ports::select_platform_loopback_port, + ) +} + +fn select_native_menu_inspector_port_with( + debug_port: u16, + is_windows: bool, + select_loopback_port: impl Fn(u16) -> u16, +) -> u16 { let requested = debug_port.saturating_add(100); - crate::ports::select_platform_loopback_port(requested) + if is_windows { + // Windows packaged-app activation reuses an existing Codex instance and + // ignores new command-line arguments. If the existing instance already + // listens on the requested inspector port, choosing a random free port + // only makes the native-menu localizer retry against a dead port. + requested + } else { + select_loopback_port(requested) + } } fn start_native_menu_localizer(inspector_port: u16) { @@ -2272,4 +2292,18 @@ mod tests { &missing_runtime_package )); } + + #[test] + fn native_menu_inspector_port_keeps_requested_port_on_windows() { + let selected = select_native_menu_inspector_port_with(9229, true, |_| 62528); + + assert_eq!(selected, 9329); + } + + #[test] + fn native_menu_inspector_port_uses_loopback_selector_off_windows() { + let selected = select_native_menu_inspector_port_with(9229, false, |_| 62528); + + assert_eq!(selected, 62528); + } } diff --git a/crates/codex-plus-core/src/ports.rs b/crates/codex-plus-core/src/ports.rs index 2263b21fa..0dc394866 100644 --- a/crates/codex-plus-core/src/ports.rs +++ b/crates/codex-plus-core/src/ports.rs @@ -83,11 +83,16 @@ pub fn select_packaged_codex_debug_port(requested: u16) -> u16 { pub fn select_packaged_codex_debug_port_with( requested: u16, - is_windows: bool, - can_bind: impl Fn(u16) -> bool, - find_available: impl Fn() -> u16, + _is_windows: bool, + _can_bind: impl Fn(u16) -> bool, + _find_available: impl Fn() -> u16, ) -> u16 { - select_platform_loopback_port_with(requested, is_windows, can_bind, find_available) + // Windows packaged-app activation reuses an existing Codex instance when one + // is already running. In that case new command-line arguments are ignored, + // so selecting a random free debug port here makes the launcher inject into + // a port that Codex will never open. Keep the requested port stable and let + // CDP probing handle IPv4/IPv6 availability. + requested } pub fn select_platform_loopback_port_with( diff --git a/crates/codex-plus-core/tests/cdp_bridge.rs b/crates/codex-plus-core/tests/cdp_bridge.rs index 96d7e22ae..3dee93395 100644 --- a/crates/codex-plus-core/tests/cdp_bridge.rs +++ b/crates/codex-plus-core/tests/cdp_bridge.rs @@ -268,6 +268,15 @@ fn injection_script_disables_plugin_auto_expand_in_relay_mode() { assert!(script.contains("if (!codexPlusSettings().pluginAutoExpand) return")); } +#[test] +fn injection_script_does_not_log_plugin_auto_expand_noops() { + let script = assets::injection_script(57321); + + assert!(script.contains("if (!currentSignature)")); + assert!(script.contains("window.__codexPluginAutoExpandLastSignature = \"\"")); + assert!(script.contains("if ((window.__codexPluginAutoExpandClicks || 0) > 0 || !!button)")); +} + #[test] fn injection_script_defines_version_gated_plugin_unlock_strategy() { let script = assets::injection_script(57321); @@ -612,6 +621,16 @@ fn injection_script_exposes_fast_service_tier_control() { assert!(script.contains("data-codex-service-tier-controls")); assert!(script.contains("removeCodexServiceTierBadges")); assert!(script.contains("installCodexServiceTierDispatcherPatch")); + assert!(script.contains("installCodexServiceTierAppServerRequestPatch")); + assert!(script.contains("patchCodexServiceTierRequestClient")); + assert!(script.contains("__codexServiceTierAppServerRequestPatchInstalled")); + assert!(script.contains("service_tier_app_server_request_patch_installed")); + assert!(script.contains("applyCodexServiceTierRequestClientParams")); + assert!( + script.contains( + "nestedParams === params.params ? params : { ...params, params: nestedParams }" + ) + ); assert!(script.contains("服务模式")); assert!(script.contains("data-codex-service-tier-status")); assert!(script.contains("data-codex-service-tier-inherit")); @@ -1741,3 +1760,29 @@ fn noop_handler() -> bridge::BridgeHandler { as Pin> + Send>> }) } + +#[test] +fn renderer_script_falls_back_to_app_server_manager_hooks_asset() { + let script = assets::renderer_script().replace("\r\n", "\n"); + + assert!(script.contains("async function loadCodexAppModuleAny(nameParts)")); + assert!(script.contains("function codexAppServerRequestCandidates(module)")); + assert!(script.contains("function isCodexAppServerRegistryClass(value)")); + assert!(script.contains("function codexAppScopeCandidates()")); + assert!(script.contains("\"app-server-manager-signals-\"")); + assert!(script.contains("\"app-server-manager-hooks-\"")); + assert!(script.contains("loadCodexAppModuleAny([\n \"app-server-manager-signals-\",\n \"app-server-manager-hooks-\",\n ])")); + assert!(script.contains( + "const Registry = Object.values(module || {}).find(isCodexAppServerRegistryClass);" + )); + assert!(script.contains("registry.getImplForHostId?.(\"local\")")); +} + +#[test] +fn renderer_script_rate_limits_repeated_app_server_manager_patch_failures() { + let script = assets::renderer_script().replace("\r\n", "\n"); + + assert!(script.contains("function sendCodexPlusDiagnosticOnce(key, event, detail)")); + assert!(script.contains("plugin-marketplace-request-patch-failed")); + assert!(script.contains("model-app-server-request-patch-failed")); +} diff --git a/crates/codex-plus-core/tests/launcher.rs b/crates/codex-plus-core/tests/launcher.rs index c6a0a4a03..1411777a9 100644 --- a/crates/codex-plus-core/tests/launcher.rs +++ b/crates/codex-plus-core/tests/launcher.rs @@ -500,10 +500,10 @@ fn ports_windows_falls_back_to_ephemeral_when_requested_is_busy() { } #[test] -fn ports_windows_packaged_debug_falls_back_to_ephemeral_when_requested_is_busy() { +fn ports_windows_packaged_debug_keeps_requested_when_requested_is_busy() { let selected = select_packaged_codex_debug_port_with(9229, true, |_| false, || 43001); - assert_eq!(selected, 43001); + assert_eq!(selected, 9229); } #[test]