Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/codex-plus-manager/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,7 @@ const defaultSettings: BackendSettings = {
zedRemoteSyncToZedSettings: false,
codexAppUpstreamWorktreeCreate: true,
codexAppNativeMenuPlacement: true,
codexAppServiceTierControls: false,
codexAppServiceTierControls: true,
codexAppImageOverlayEnabled: false,
codexAppImageOverlayPath: "",
codexAppImageOverlayOpacity: 35,
Expand Down Expand Up @@ -1973,7 +1973,7 @@ function EnhanceScreen({
<FeatureToggle title="强制解锁入口" detail="恢复 1.1.9 的入口解锁方式,强制显示并启用插件入口。" checked={form.codexAppPluginEntryUnlock} disabled={!masterEnabled || !patchMode} onChange={(value) => setEnhanceFlag("codexAppPluginEntryUnlock", value)} />
<FeatureToggle title="特殊插件强制安装" detail="解除 App unavailable / 应用不可用导致的前端安装禁用。" checked={form.codexAppForcePluginInstall} disabled={!masterEnabled || !patchMode} onChange={(value) => setEnhanceFlag("codexAppForcePluginInstall", value)} />
<FeatureToggle title="模型白名单解锁" detail="从环境变量和 config.toml 的 /v1/models 拉取模型并补进模型列表。" checked={form.codexAppModelWhitelistUnlock} disabled={!masterEnabled} onChange={(value) => setEnhanceFlag("codexAppModelWhitelistUnlock", value)} />
<FeatureToggle title="Fast 按钮" detail="显示服务模式切换按钮;Fast 仅支持 gpt-5.4 / gpt-5.5,其他模型按 Standard 发送。" checked={form.codexAppServiceTierControls} disabled={!masterEnabled} onChange={(value) => setEnhanceFlag("codexAppServiceTierControls", value)} />
<FeatureToggle title="系统 Fast 开关" detail="是否开启系统 Fast 开关:已默认开启,API Key 登录复用 Codex 原生速度选项与标识;具体 Fast / Standard 在 Codex 界面选择。" checked={true} disabled onChange={() => {}} />
<FeatureToggle title="会话删除" detail="在会话列表悬停显示删除按钮,并支持撤销。" checked={form.codexAppSessionDelete} disabled={!masterEnabled} onChange={(value) => setEnhanceFlag("codexAppSessionDelete", value)} />
<FeatureToggle title="Markdown 导出" detail="在会话列表显示导出按钮,导出带时间戳的 Markdown。" checked={form.codexAppMarkdownExport} disabled={!masterEnabled} onChange={(value) => setEnhanceFlag("codexAppMarkdownExport", value)} />
<FeatureToggle title="会话项目移动" detail="把会话移动到普通对话或其他本地项目。" checked={form.codexAppProjectMove} disabled={!masterEnabled} onChange={(value) => setEnhanceFlag("codexAppProjectMove", value)} />
Expand Down
13 changes: 6 additions & 7 deletions assets/inject/renderer-inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,7 @@
}

function defaultCodexPlusSettings() {
return { pluginEntryUnlock: true, pluginMarketplaceUnlock: true, forcePluginInstall: true, modelWhitelistUnlock: true, sessionDelete: true, markdownExport: true, projectMove: true, conversationTimeline: true, conversationView: false, conversationViewMaxWidth: conversationViewDefaultWidth, threadScrollRestore: true, zedRemoteOpen: true, upstreamWorktreeCreate: true, nativeMenuPlacement: true, serviceTierControls: false };
return { pluginEntryUnlock: true, pluginMarketplaceUnlock: true, forcePluginInstall: true, modelWhitelistUnlock: true, sessionDelete: true, markdownExport: true, projectMove: true, conversationTimeline: true, threadIdBadge: false, conversationView: false, conversationViewMaxWidth: conversationViewDefaultWidth, threadScrollRestore: true, zedRemoteOpen: true, upstreamWorktreeCreate: true, nativeMenuPlacement: true, serviceTierControls: true };
}

const codexPlusBackendSettingMap = {
Expand Down Expand Up @@ -989,6 +989,7 @@
settings.pluginMarketplaceUnlock = false;
settings.forcePluginInstall = false;
}
settings.serviceTierControls = true;
return settings;
} catch {
const settings = { ...defaultCodexPlusSettings(), ...backendCodexPlusSettings() };
Expand All @@ -997,6 +998,7 @@
settings.pluginMarketplaceUnlock = false;
settings.forcePluginInstall = false;
}
settings.serviceTierControls = true;
return settings;
}
}
Expand Down Expand Up @@ -1736,10 +1738,7 @@
function applyCodexServiceTierRequestOverride(method, params, threadIdHint = "") {
const override = codexServiceTierOverrideForRequest(method, params, threadIdHint);
if (!override) return params;
const nextParams = { ...(params || {}), serviceTier: override.serviceTier };
if (Object.prototype.hasOwnProperty.call(nextParams, "service_tier") || override.fastBlocked) {
nextParams.service_tier = override.serviceTier;
}
const nextParams = { ...(params || {}), serviceTier: override.serviceTier, service_tier: override.serviceTier };
sendCodexPlusDiagnostic("service_tier_request_override_applied", {
method,
threadId: override.threadId || "",
Expand Down Expand Up @@ -2160,8 +2159,8 @@
<button type="button" class="codex-plus-toggle" data-codex-plus-setting="modelWhitelistUnlock"><span></span></button>
</div>
<div class="codex-plus-row">
<div><div class="codex-plus-row-title">Fast 按钮</div><div class="codex-plus-row-description">显示服务模式切换按钮;Fast 仅支持 ${codexServiceTierFastModelListLabel()},其他模型按 Standard 发送。</div></div>
<button type="button" class="codex-plus-toggle" data-codex-plus-setting="serviceTierControls"><span></span></button>
<div><div class="codex-plus-row-title">系统 Fast 开关</div><div class="codex-plus-row-description">是否开启系统 Fast 开关:已默认开启,API Key 登录复用 Codex 原生速度选项与标识;具体 Fast / Standard 在 Codex 界面选择,Fast 仅支持 ${codexServiceTierFastModelListLabel()}。</div></div>
<button type="button" class="codex-plus-toggle" data-codex-plus-setting="serviceTierControls" disabled><span></span></button>
</div>
<div class="codex-plus-row" data-codex-service-tier-controls="true">
<div><div class="codex-plus-row-title">服务模式</div><div class="codex-plus-row-description">继承使用 config.toml 的 service tier;全局模式覆盖全部 thread;自定义允许按 thread 覆盖。</div></div>
Expand Down
46 changes: 36 additions & 10 deletions crates/codex-plus-core/src/launcher.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::collections::HashSet;
use std::env;
use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use std::process::Stdio;
Expand Down Expand Up @@ -143,7 +144,7 @@ pub trait LaunchHooks: Send + Sync {
&self,
app_dir: &Path,
debug_port: u16,
extra_args: &[String],
settings: &BackendSettings,
) -> anyhow::Result<CodexLaunch>;
async fn bridge_context(
&self,
Expand Down Expand Up @@ -266,9 +267,7 @@ where
helper_started = true;
}

let launch = hooks
.launch_codex(&app_dir, debug_port, &settings.codex_extra_args)
.await?;
let launch = hooks.launch_codex(&app_dir, debug_port, &settings).await?;
launched = Some(launch.clone());
keep_launched_on_error = true;
if settings.computer_use_guard_enabled {
Expand Down Expand Up @@ -492,8 +491,9 @@ impl LaunchHooks for DefaultLaunchHooks {
&self,
app_dir: &Path,
debug_port: u16,
extra_args: &[String],
settings: &BackendSettings,
) -> anyhow::Result<CodexLaunch> {
let extra_args = &settings.codex_extra_args;
if cfg!(windows) {
if let Some(activation) = build_packaged_activation(app_dir, debug_port, extra_args) {
let CodexLaunch::PackagedActivation {
Expand Down Expand Up @@ -564,20 +564,46 @@ impl LaunchHooks for DefaultLaunchHooks {
} else {
MacosCleanupPolicy::QuitIfNotPreviouslyRunning
};
let command = build_macos_open_command(app_dir, debug_port, extra_args);
let command = build_codex_command(app_dir, debug_port, extra_args);
let executable = command
.first()
.ok_or_else(|| anyhow::anyhow!("macOS open command is empty"))?;
let child = Command::new(executable)
.ok_or_else(|| anyhow::anyhow!("macOS Codex command is empty"))?;
let mut child_command = Command::new(executable);
child_command
.args(&command[1..])
.stdout(Stdio::null())
.stderr(Stdio::null())
.stderr(Stdio::null());
if settings.enhancements_enabled {
let preload_path = crate::service_tier_preload::ensure_service_tier_preload()
.context("failed to prepare service tier preload")?;
let node_options =
crate::service_tier_preload::node_options_with_service_tier_preload(
env::var("NODE_OPTIONS").ok().as_deref(),
&preload_path.to_string_lossy(),
);
child_command.env("NODE_OPTIONS", node_options.clone());
let _ = crate::diagnostic_log::append_diagnostic_log(
"launcher.service_tier_preload_enabled",
serde_json::json!({
"preload_path": preload_path.to_string_lossy(),
"node_options": node_options,
}),
);
} else {
let _ = crate::diagnostic_log::append_diagnostic_log(
"launcher.service_tier_preload_disabled",
serde_json::json!({
"enhancements_enabled": settings.enhancements_enabled,
}),
);
}
let child = child_command
.spawn()
.context("failed to launch macOS Codex app")?;
*self.child.lock().await = Some(child);
return Ok(CodexLaunch::Process {
command,
wait_strategy: ProcessWaitStrategy::ExternalWaitCommand,
wait_strategy: ProcessWaitStrategy::TrackedChild,
macos_cleanup_policy: Some(cleanup_policy),
});
}
Expand Down
1 change: 1 addition & 0 deletions crates/codex-plus-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub mod relay_config;
pub mod relay_switch;
pub mod routes;
pub mod script_market;
pub mod service_tier_preload;
pub mod settings;
pub mod status;
pub mod update;
Expand Down
235 changes: 235 additions & 0 deletions crates/codex-plus-core/src/service_tier_preload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
use std::fs;
use std::path::PathBuf;

use anyhow::Context;

const PRELOAD_FILE: &str = "service-tier-preload.js";

pub fn ensure_service_tier_preload() -> anyhow::Result<PathBuf> {
let dir = crate::paths::default_app_state_dir().join("preload");
fs::create_dir_all(&dir).with_context(|| {
format!(
"failed to create service tier preload directory {}",
dir.display()
)
})?;
let path = dir.join(PRELOAD_FILE);
fs::write(&path, service_tier_preload_script()).with_context(|| {
format!(
"failed to write service tier preload script {}",
path.display()
)
})?;
Ok(path)
}

pub fn node_options_with_service_tier_preload(
existing: Option<&str>,
preload_path: &str,
) -> String {
let require_arg = format!("--require={preload_path}");
match existing.map(str::trim).filter(|value| !value.is_empty()) {
Some(existing) if existing.contains(&require_arg) => existing.to_string(),
Some(existing) => format!("{require_arg} {existing}"),
None => require_arg,
}
}

pub fn service_tier_preload_script() -> &'static str {
r#""use strict";

const fs = require("fs");
const path = require("path");
const Module = require("module");

const PATCH_MARK = Symbol.for("codex-plus.service-tier-protocol-handle-patched");
const PATCH_VERSION = "protocol-handle-2";
const SERVICE_TIER_SETTINGS_ASSET_RE = /^use-service-tier-settings-.*\.js$/;
const READ_SERVICE_TIER_ASSET_RE = /^read-service-tier-for-request-.*\.js$/;
const LOG_PATH = path.join(process.env.HOME || process.cwd(), ".codex-session-delete", "codex-plus.log");
const SETTINGS_PATH = path.join(process.env.HOME || process.cwd(), ".codex-session-delete", "settings.json");

function log(event, detail) {
try {
fs.mkdirSync(path.dirname(LOG_PATH), { recursive: true });
fs.appendFileSync(LOG_PATH, JSON.stringify({
timestamp_ms: Date.now(),
pid: process.pid,
event,
detail: detail || {},
}) + "\n");
} catch {}
}

function patchServiceTierSettingsAsset(source) {
let patched = source;
patched = replaceOnce(
patched,
"s=o?.authMethod===`chatgpt`",
"s=o?.authMethod===`chatgpt`||o?.authMethod===`apikey`",
"service tier settings auth gate"
);
patched = replaceOnce(
patched,
"s&&!f&&u!=null",
"s&&!f",
"service tier settings API key config requirement"
);
return patched;
}

function patchReadServiceTierAsset(source) {
let patched = source;
patched = replaceOnce(
patched,
"return n===`chatgpt`?(await e.query.fetch(c,{authMethod:n,hostId:t})).requirements?.featureRequirements?.fast_mode!==!1:!1",
"return n===`chatgpt`?(await e.query.fetch(c,{authMethod:n,hostId:t})).requirements?.featureRequirements?.fast_mode!==!1:n===`apikey`",
"read service tier auth gate"
);
patched = replaceOnce(
patched,
"return d.service_tier==null?t(await m(o,c??d.model),d.service_tier,s):t(null,d.service_tier,s)",
"return d.service_tier==null?t(await m(o,c??d.model),d.service_tier,s):t(await m(o,c??d.model),d.service_tier,s)",
"read service tier explicit config model lookup"
);
return patched;
}

function replaceOnce(source, from, to, label) {
if (source.includes(to)) return source;
if (!source.includes(from)) throw new Error(`${label} pattern not found`);
return source.replace(from, to);
}

function appProtocolAssetName(url) {
if (typeof url !== "string") return "";
try {
const parsed = new URL(url);
if (parsed.protocol !== "app:" || parsed.host !== "-") return "";
const segments = decodeURIComponent(parsed.pathname).split("/").filter(Boolean);
return segments.length >= 2 && segments[0] === "assets" ? segments[segments.length - 1] : "";
} catch {
return "";
}
}

function serviceTierControlsEnabled() {
try {
const settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, "utf8"));
return settings && settings.enhancementsEnabled !== false;
} catch {
return true;
}
}

function discoverPatchedAssets() {
if (!serviceTierControlsEnabled()) {
log("service_tier_preload_disabled_by_settings", {});
return new Map();
}
const assetsDir = path.join(process.resourcesPath, "app.asar", "webview", "assets");
const assets = new Map();
for (const name of fs.readdirSync(assetsDir)) {
const filePath = path.join(assetsDir, name);
if (SERVICE_TIER_SETTINGS_ASSET_RE.test(name)) {
assets.set(name, {
kind: "native-service-tier-settings",
patched: patchServiceTierSettingsAsset(fs.readFileSync(filePath, "utf8")),
});
} else if (READ_SERVICE_TIER_ASSET_RE.test(name)) {
assets.set(name, {
kind: "native-read-service-tier",
patched: patchReadServiceTierAsset(fs.readFileSync(filePath, "utf8")),
});
}
}
if (assets.size === 0) throw new Error("target native speed UI assets were not found");
return assets;
}

function installProtocolHandlePatch(electron) {
const protocol = electron && electron.protocol;
if (!protocol || typeof protocol.handle !== "function") {
log("service_tier_preload_protocol_unavailable", {});
return;
}
if (protocol.handle[PATCH_MARK] === PATCH_VERSION) return;

const patchedAssets = discoverPatchedAssets();
if (patchedAssets.size === 0) return;
const originalHandle = protocol.handle;
const wrappedHandle = function codexPlusServiceTierProtocolHandle(scheme, handler) {
if (String(scheme) !== "app" || typeof handler !== "function") {
return originalHandle.apply(this, arguments);
}
const wrappedHandler = async function codexPlusServiceTierAppProtocolHandler(request) {
const asset = patchedAssets.get(appProtocolAssetName(request && request.url));
if (!asset) return handler.call(this, request);
log("service_tier_preload_asset_patched", { kind: asset.kind, url: request && request.url, version: PATCH_VERSION });
return new Response(Buffer.from(asset.patched, "utf8"), {
headers: {
"Content-Length": String(Buffer.byteLength(asset.patched, "utf8")),
"Content-Type": "text/javascript; charset=utf-8",
"X-Codex-Plus-Patch": PATCH_VERSION,
},
});
};
return originalHandle.call(this, scheme, wrappedHandler);
};

Object.defineProperty(wrappedHandle, PATCH_MARK, {
configurable: false,
enumerable: false,
value: PATCH_VERSION,
});
protocol.handle = wrappedHandle;
log("service_tier_preload_protocol_patch_installed", {
assets: Array.from(patchedAssets.keys()),
version: PATCH_VERSION,
});
}

const originalLoad = Module._load;
Module._load = function codexPlusServiceTierModuleLoad(request, parent, isMain) {
const result = originalLoad.apply(this, arguments);
if (request === "electron") {
try {
installProtocolHandlePatch(result);
} catch (error) {
log("service_tier_preload_protocol_patch_failed", { message: String(error) });
}
}
return result;
};

log("service_tier_preload_loaded", { version: PATCH_VERSION });
"#
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn node_options_prepends_preload() {
assert_eq!(
node_options_with_service_tier_preload(Some("--trace-warnings"), "/tmp/preload.js"),
"--require=/tmp/preload.js --trace-warnings"
);
}

#[test]
fn preload_script_wraps_electron_module_load_and_app_protocol() {
let script = service_tier_preload_script();

assert!(script.contains("Module._load"));
assert!(script.contains("protocol.handle"));
assert!(script.contains("serviceTierControlsEnabled"));
assert!(script.contains("settings.enhancementsEnabled !== false"));
assert!(script.contains("service_tier_preload_disabled_by_settings"));
assert!(script.contains("use-service-tier-settings-"));
assert!(script.contains("read-service-tier-for-request-"));
assert!(script.contains("s=o?.authMethod===`chatgpt`||o?.authMethod===`apikey`"));
assert!(script.contains("n===`apikey`"));
}
}
Loading