@@ -189,10 +189,72 @@ fn quit_command(platform: &str, force: bool) -> Vec<String> {
189189/// `extra_args`: 附加给 Codex Desktop 本身的参数(如 `--remote-debugging-port=9222`)。
190190/// macOS 通过 `open` 的 `--args` 传递;Linux 直接追加到命令;Windows Store
191191/// 应用暂不支持命令行参数(忽略)。
192+ /// [MOC-323] Chat 接入自定义模型:守卫补丁脚本(经 `NODE_OPTIONS=--require` 注入 app 主进程,
193+ /// hook `isDesktopAuthAllowedUrl` 白名单;详见 `resources/chat_guard_patch.js`)。
194+ const CHAT_GUARD_PATCH_JS : & str = include_str ! ( "../../../../resources/chat_guard_patch.js" ) ;
195+
196+ /// [MOC-323] 启动 Codex app 时为「Chat 接入自定义模型」注入的环境变量(仅 macOS)。
197+ /// 设置项 `chatCustomModelEnabled` **默认开**;把守卫补丁写到 `~/.codex-app-transfer/`,令
198+ /// app 的 Chat 对话经 `CODEX_API_BASE_URL` 流进本地 proxy。**host 必须用 `localhost`**:守卫
199+ /// 判 `new URL(base).host`,补丁加的是 `localhost:<port>`,base 用 `127.0.0.1` 则 host 不匹配。
200+ fn chat_launch_env ( platform : & str ) -> Vec < ( String , String ) > {
201+ if platform != "macos" {
202+ return Vec :: new ( ) ;
203+ }
204+ let cfg = crate :: admin:: registry_io:: load ( ) . ok ( ) ;
205+ let enabled = cfg
206+ . as_ref ( )
207+ . and_then ( |c| c. get ( "settings" ) )
208+ . and_then ( |s| s. get ( "chatCustomModelEnabled" ) )
209+ . and_then ( |v| v. as_bool ( ) )
210+ . unwrap_or ( true ) ; // 默认开
211+ if !enabled {
212+ return Vec :: new ( ) ;
213+ }
214+ // [code-review H1] 无可解析 proxy 目标(全新安装 / activeProvider null / provider 不走
215+ // proxy)时**不注入** base_url。否则 Quick Chat 被 CODEX_API_BASE_URL 导向死的本地
216+ // `/backend-api`,连真 ChatGPT 都用不了 —— 新装用户直接丢失 Chat。此时回落真 ChatGPT。
217+ if !crate :: admin:: services:: desktop:: snapshot:: active_provider_supports_relay ( ) {
218+ return Vec :: new ( ) ;
219+ }
220+ let port = cfg
221+ . as_ref ( )
222+ . map ( crate :: admin:: handlers:: proxy:: read_proxy_port)
223+ . unwrap_or ( 18080 ) ;
224+ let Some ( home) = std:: env:: var_os ( "HOME" ) else {
225+ tracing:: warn!( "[Chat] HOME 未设置,Chat 自定义模型本次启动不生效" ) ;
226+ return Vec :: new ( ) ;
227+ } ;
228+ let dir = PathBuf :: from ( home) . join ( ".codex-app-transfer" ) ;
229+ let patch_path = dir. join ( "chat_guard_patch.js" ) ;
230+ // [code-review H4] I/O 失败不能静默吞成空 env(开关显示开、实际走真 ChatGPT);记 warn 便于诊断。
231+ if let Err ( e) = fs:: create_dir_all ( & dir) {
232+ tracing:: warn!( error = %e, dir = %dir. display( ) , "[Chat] 建目录失败,Chat 自定义模型本次不生效" ) ;
233+ return Vec :: new ( ) ;
234+ }
235+ if let Err ( e) = fs:: write ( & patch_path, CHAT_GUARD_PATCH_JS ) {
236+ tracing:: warn!( error = %e, path = %patch_path. display( ) , "[Chat] 写守卫补丁失败,Chat 自定义模型本次不生效" ) ;
237+ return Vec :: new ( ) ;
238+ }
239+ let host = format ! ( "localhost:{port}" ) ;
240+ vec ! [
241+ (
242+ "NODE_OPTIONS" . into( ) ,
243+ format!( "--require {}" , patch_path. to_string_lossy( ) ) ,
244+ ) ,
245+ (
246+ "CODEX_API_BASE_URL" . into( ) ,
247+ format!( "http://{host}/backend-api" ) ,
248+ ) ,
249+ ( "CAS_CHAT_GUARD_HOST" . into( ) , host) ,
250+ ]
251+ }
252+
192253fn open_command (
193254 platform : & str ,
194255 resolved_macos_app : Option < & str > ,
195256 extra_args : & [ String ] ,
257+ extra_env : & [ ( String , String ) ] ,
196258) -> Vec < String > {
197259 match platform {
198260 // [MOC-100 E] 去掉 `-n`(原来强制开新实例以绕过「刚杀完进程、launchd 还没
@@ -203,6 +265,12 @@ fn open_command(
203265 // 走到这里 + POST_QUIT_LAUNCHD_GRACE,那条 race 已不存在 → 用 `open -a` 启**单**实例。
204266 "macos" => {
205267 let mut cmd = vec ! [ "open" . into( ) ] ;
268+ // [MOC-323] `open --env K=V` 把 env 传给被启动的 GUI app(NODE_OPTIONS 守卫补丁 +
269+ // CODEX_API_BASE_URL 路由 Chat 进本地 proxy)。必须在 `-a`/`--args` 之前。
270+ for ( k, v) in extra_env {
271+ cmd. push ( "--env" . into ( ) ) ;
272+ cmd. push ( format ! ( "{k}={v}" ) ) ;
273+ }
206274 match resolved_macos_app {
207275 Some ( path) => {
208276 cmd. push ( "-a" . into ( ) ) ;
@@ -703,7 +771,27 @@ fn should_attach_debug_port() -> Vec<String> {
703771 . and_then ( |s| s. get ( "codexStashEnabled" ) )
704772 . and_then ( |v| v. as_bool ( ) )
705773 . unwrap_or ( false ) ;
706- if !plugin_unlock_needs_port && !theme_enabled && !quota_enabled && !stash_enabled {
774+ // [MOC-323 / code-review C1] Chat 模型 relabel daemon 也走 CDP。chatCustomModelEnabled
775+ // 默认开、其它 CDP 功能默认关 → 不含它则「只开 chat」时 CDP_PORT=0、daemon 静默跳过、
776+ // picker 永远 GPT 名。只在**确有 proxy 路由的活动 provider**(全新安装/无 provider → false,
777+ // 与 chat_launch_env 的 H1 守卫一致,避免无谓开端口)时才为 chat 要端口。
778+ let chat_enabled = cfg
779+ . as_ref ( )
780+ . and_then ( |c| c. get ( "settings" ) )
781+ . and_then ( |s| s. get ( "chatCustomModelEnabled" ) )
782+ . and_then ( |v| v. as_bool ( ) )
783+ . unwrap_or ( true ) ;
784+ // 仅 macOS 实现 Chat(chat_launch_env 只对 macos 注入);非 macos 开了也不工作,
785+ // 故不为它带 CDP 端口(否则白暴露 --remote-debugging-port/--remote-allow-origins=* — code-review [3])。
786+ let chat_needs_port = chat_enabled
787+ && cfg ! ( target_os = "macos" )
788+ && crate :: admin:: services:: desktop:: snapshot:: active_provider_supports_relay ( ) ;
789+ if !plugin_unlock_needs_port
790+ && !theme_enabled
791+ && !quota_enabled
792+ && !stash_enabled
793+ && !chat_needs_port
794+ {
707795 return vec ! [ ] ;
708796 }
709797
@@ -899,7 +987,8 @@ fn open_codex_app(platform: &str) -> Result<(), String> {
899987 None
900988 } ;
901989 let extra_args = should_attach_debug_port ( ) ;
902- let cmd = open_command ( platform, resolved. as_deref ( ) , & extra_args) ;
990+ let chat_env = chat_launch_env ( platform) ;
991+ let cmd = open_command ( platform, resolved. as_deref ( ) , & extra_args, & chat_env) ;
903992 let Some ( ( program, args) ) = cmd. split_first ( ) else {
904993 return Err ( "open command is empty" . to_owned ( ) ) ;
905994 } ;
@@ -1266,17 +1355,17 @@ mod tests {
12661355 fn open_command_uses_resolved_path_when_available ( ) {
12671356 // [MOC-100 E] 去掉 `-n`,改单实例 `open -a`
12681357 assert_eq ! (
1269- open_command( "macos" , Some ( "/Applications/Codex.app" ) , & [ ] ) ,
1358+ open_command( "macos" , Some ( "/Applications/Codex.app" ) , & [ ] , & [ ] ) ,
12701359 vec![ "open" , "-a" , "/Applications/Codex.app" ]
12711360 ) ;
12721361 // [改名适配] 路径落空按 bundle id 兜底(新旧两代同 id;`-a Codex` 在
12731362 // 26.707-only 安装上启不动,`-a ChatGPT` 可能解析到消费级客户端)
12741363 assert_eq ! (
1275- open_command( "macos" , None , & [ ] ) ,
1364+ open_command( "macos" , None , & [ ] , & [ ] ) ,
12761365 vec![ "open" , "-b" , "com.openai.codex" ]
12771366 ) ;
12781367 assert_eq ! (
1279- open_command( "macos" , None , & [ "--remote-debugging-port=9222" . into( ) ] ) ,
1368+ open_command( "macos" , None , & [ "--remote-debugging-port=9222" . into( ) ] , & [ ] ) ,
12801369 vec![
12811370 "open" ,
12821371 "-b" ,
@@ -1285,13 +1374,45 @@ mod tests {
12851374 "--remote-debugging-port=9222"
12861375 ]
12871376 ) ;
1288- let windows = open_command ( "windows" , None , & [ ] ) ;
1377+ let windows = open_command ( "windows" , None , & [ ] , & [ ] ) ;
12891378 assert_eq ! ( windows[ 0 ] , "explorer.exe" ) ;
12901379 assert ! ( windows[ 1 ] . starts_with( "shell:AppsFolder\\ " ) ) ;
12911380 assert ! ( windows[ 1 ] . contains( "OpenAI.Codex" ) ) ;
1292- let linux = open_command ( "linux" , None , & [ ] ) ;
1381+ let linux = open_command ( "linux" , None , & [ ] , & [ ] ) ;
12931382 assert_eq ! ( linux[ 0 ] , "sh" ) ;
12941383 assert_eq ! ( linux[ 1 ] , "-c" ) ;
12951384 assert ! ( linux[ 2 ] . contains( "codex" ) ) ;
12961385 }
1386+
1387+ // [MOC-323] Chat env 经 `open --env` 注入,排在 `-a`/`--args` 之前
1388+ #[ test]
1389+ fn open_command_injects_chat_env_before_app ( ) {
1390+ let env = vec ! [
1391+ ( "NODE_OPTIONS" . to_string( ) , "--require /x/p.js" . to_string( ) ) ,
1392+ (
1393+ "CODEX_API_BASE_URL" . to_string( ) ,
1394+ "http://localhost:18080/backend-api" . to_string( ) ,
1395+ ) ,
1396+ ] ;
1397+ let cmd = open_command (
1398+ "macos" ,
1399+ Some ( "/Applications/ChatGPT.app" ) ,
1400+ & [ "--remote-debugging-port=0" . into ( ) ] ,
1401+ & env,
1402+ ) ;
1403+ assert_eq ! (
1404+ cmd,
1405+ vec![
1406+ "open" ,
1407+ "--env" ,
1408+ "NODE_OPTIONS=--require /x/p.js" ,
1409+ "--env" ,
1410+ "CODEX_API_BASE_URL=http://localhost:18080/backend-api" ,
1411+ "-a" ,
1412+ "/Applications/ChatGPT.app" ,
1413+ "--args" ,
1414+ "--remote-debugging-port=0" ,
1415+ ]
1416+ ) ;
1417+ }
12971418}
0 commit comments