Skip to content
Open
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
5 changes: 5 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ jobs:
cache: 'npm'

- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt

- uses: Swatinem/rust-cache@v2
with:
Expand All @@ -60,6 +62,9 @@ jobs:
- name: Check for hardcoded version literals in HTML
run: npm run version:check

- name: cargo fmt (check)
run: cargo fmt --manifest-path src-tauri/Cargo.toml -- --check

- name: cargo check (lib)
run: cargo check --manifest-path src-tauri/Cargo.toml --lib

Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ tauri-build = { version = "2.5.6", features = [] }
[dependencies]
base64 = "0.22.1"
chrono = { version = "0.4.44", features = ["serde"] }
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls", "socks"] }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
semver = "1.0.28"
Expand Down
26 changes: 26 additions & 0 deletions src-tauri/mac/front/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,32 @@ <h2 id="profiles-heading" class="section-title">Profiles</h2>
<button id="settings-codex-cli-button" class="settings-action-button" type="button">Change</button>
</div>
</div>
<div class="settings-row settings-row--field settings-row--proxy">
<strong data-i18n-key="settingsProxy">Proxy</strong>
<div class="settings-cli-inline">
<input
id="settings-proxy-input"
class="settings-input"
type="text"
autocomplete="off"
spellcheck="false"
placeholder="http://127.0.0.1:7890"
/>
<button
id="settings-proxy-save-button"
class="settings-action-button"
type="button"
data-i18n-key="settingsProxySave"
>Save</button>
<button
id="settings-proxy-clear-button"
class="settings-action-button"
type="button"
data-i18n-key="settingsProxyClear"
>Clear</button>
</div>
<p id="settings-proxy-hint" class="settings-hint" data-i18n-key="settingsProxyHint"></p>
</div>
<div class="settings-row">
<strong data-i18n-key="settingsVersion">Version</strong>
<span id="settings-version-value" class="settings-value settings-value--strong">—</span>
Expand Down
7 changes: 6 additions & 1 deletion src-tauri/mac/runtime/cli_shim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use std::path::{Path, PathBuf};

use crate::errors::{AppError, AppResult};
use crate::shared::paths::{
get_backup_root, INSTALL_STATE_FILENAME, LOGIN_RUNTIME_DIRNAME, REFRESH_RUNTIME_DIRNAME,
get_backup_root, INSTALL_STATE_FILENAME, LOGIN_RUNTIME_DIRNAME, PROXY_STATE_FILENAME,
REFRESH_RUNTIME_DIRNAME,
};

pub const MACOS_RUNTIME_DIRNAME: &str = "macos";
Expand All @@ -16,6 +17,10 @@ pub fn get_runtime_dir(codex_home: &Path) -> PathBuf {
get_backup_root(Some(codex_home)).join(MACOS_RUNTIME_DIRNAME)
}

pub fn get_proxy_state_file(codex_home: &Path) -> PathBuf {
get_runtime_dir(codex_home).join(PROXY_STATE_FILENAME)
}

pub fn get_refresh_runtime_dir(codex_home: &Path) -> PathBuf {
get_runtime_dir(codex_home).join(REFRESH_RUNTIME_DIRNAME)
}
Expand Down
4 changes: 4 additions & 0 deletions src-tauri/mac/runtime/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,10 @@ fn build_login_command(real_codex_path: &Path, runtime_codex_home: &Path) -> Com
command.arg("login");
command.current_dir(runtime_codex_home);
command.env("CODEX_HOME", runtime_codex_home);
// codex login 的 OAuth 浏览器流程本身由浏览器走系统代理,但 codex
// CLI 本地起 callback server / 走 token exchange 时也可能发请求,
// 注入代理 env 与 app-server 路径保持一致。
crate::shared::proxy::apply_proxy_env(&mut command);
command
}

Expand Down
81 changes: 79 additions & 2 deletions src-tauri/shared/commands/actions.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::errors::CommandError;
use crate::models::{
ActionResponse, AddProfilePayload, CodexCliRedetectResult, CodexCliStatus, OpenUrlPayload,
ProfilePayload, RenameProfilePayload, SetCodexCliPathPayload, UpdateCheckPayload,
UpdateCheckResponse, UpdateProfileBaseUrlPayload,
ProfilePayload, ProxyConfig, RenameProfilePayload, SetCodexCliPathPayload,
SetProxyConfigPayload, UpdateCheckPayload, UpdateCheckResponse, UpdateProfileBaseUrlPayload,
};

#[cfg(target_os = "macos")]
Expand Down Expand Up @@ -247,6 +247,83 @@ pub fn cancel_codex_login() -> Result<bool, CommandError> {
Ok(crate::shared::login_cancel::cancel_login_in_progress())
}

/// 返回当前生效的代理配置。仅 macOS 真正读取 `proxy_state.json`;
/// Windows / Linux 永远返回默认(直连)状态。
#[tauri::command]
pub fn get_proxy_config() -> Result<ProxyConfig, CommandError> {
#[cfg(target_os = "macos")]
{
let state = crate::shared::proxy::read_proxy_state_cached();
Ok(ProxyConfig {
proxy_url: state.proxy_url,
})
}
#[cfg(not(target_os = "macos"))]
{
Ok(ProxyConfig::default())
}
}

/// 保存代理配置。空字符串视为清空(直连)。立即丢弃已缓存的
/// reqwest client,使下一次 plan / quota 刷新走新代理。
///
/// URL 校验:用 `reqwest::Proxy::all` 试构建,失败返回
/// `INVALID_PROXY_URL`。支持 `http://` / `https://` / `socks5://`
/// / `socks5h://`。
#[tauri::command]
pub fn set_proxy_config(payload: SetProxyConfigPayload) -> Result<ProxyConfig, CommandError> {
#[cfg(target_os = "macos")]
{
let trimmed = payload.proxy_url.trim();
let next_state = if trimmed.is_empty() {
crate::shared::proxy::ProxyState::default()
} else {
// 用 reqwest 校验 URL 是否可被解析为代理。这一步不发起
// 任何网络请求,只是构造 Proxy 内部结构。
reqwest::Proxy::all(trimmed).map_err(|error| {
CommandError::new(
"INVALID_PROXY_URL",
format!(
"Invalid proxy URL {trimmed:?}: {error}. Expected http://, https://, socks5:// or socks5h://."
),
)
})?;
crate::shared::proxy::ProxyState {
proxy_url: Some(trimmed.to_string()),
}
};
crate::shared::proxy::set_proxy_state(None, next_state.clone());
// 丢弃旧 client,下一次 build_http_client 按新配置重建。
crate::shared::chatgpt_api::invalidate_http_client();
Ok(ProxyConfig {
proxy_url: next_state.proxy_url,
})
}
#[cfg(not(target_os = "macos"))]
{
let _ = payload;
Err(CommandError::new(
"PROXY_CONFIG_UNSUPPORTED",
"Proxy configuration is only supported on macOS in this build.",
))
}
}

/// 清空代理配置(恢复直连),并丢弃缓存的 reqwest client。
#[tauri::command]
pub fn clear_proxy_config() -> Result<ProxyConfig, CommandError> {
#[cfg(target_os = "macos")]
{
crate::shared::proxy::clear_proxy_state(None);
crate::shared::chatgpt_api::invalidate_http_client();
Ok(ProxyConfig::default())
}
#[cfg(not(target_os = "macos"))]
{
Ok(ProxyConfig::default())
}
}

#[tauri::command]
pub fn open_xiaohongshu(app: tauri::AppHandle) -> Result<ActionResponse, CommandError> {
let path = platform_runtime::actions::open_xiaohongshu(&app)?;
Expand Down
71 changes: 70 additions & 1 deletion src-tauri/shared/front/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ import {
checkUpdate,
clearCodexCliPath,
clearProfileAccount,
clearProxyConfig,
deleteProfile,
getCodexCliStatus,
getCurrentLiveQuota,
getProfilesSnapshot,
getProxyConfig,
loginCurrentProfile,
openCodex,
openContact,
Expand All @@ -37,10 +39,11 @@ import {
redetectCodexCliPath,
renameProfile,
setCodexCliPath,
setProxyConfig,
switchProfile,
updateProfileBaseUrl,
} from "@front-shared/tauri";
import type { CodexCliCandidate, CodexCliRedetectResult, CodexCliStatus } from "@front-shared/types";
import type { CodexCliCandidate, CodexCliRedetectResult, CodexCliStatus, ProxyConfig } from "@front-shared/types";
import {
applyLocale,
elements,
Expand Down Expand Up @@ -706,6 +709,51 @@ async function refreshCodexCliSettingsDisplay(): Promise<void> {
}
}

function applyProxySettingsDisplay(config: ProxyConfig): void {
if (!elements.settingsProxyInput) {
return;
}
elements.settingsProxyInput.value = config.proxy_url ?? "";
}

async function refreshProxySettingsDisplay(): Promise<void> {
// win/index.html 不渲染代理 UI,elements 为 null —— 直接 no-op。
if (!elements.settingsProxyInput) {
return;
}
try {
applyProxySettingsDisplay(await getProxyConfig());
} catch {
// Best-effort:保留 input 当前值,让用户仍能尝试设置。
}
}

async function handleSaveProxyConfig(): Promise<void> {
if (!elements.settingsProxyInput) {
return;
}
const url = elements.settingsProxyInput.value.trim();
try {
await setProxyConfig(url);
showToast(url ? t(state.locale, "settingsProxySaved") : t(state.locale, "settingsProxyCleared"));
} catch (error) {
showToast(error instanceof Error ? error.message : t(state.locale, "settingsProxySaveFailed"), true);
}
}

async function handleClearProxyConfig(): Promise<void> {
if (!elements.settingsProxyInput) {
return;
}
try {
await clearProxyConfig();
applyProxySettingsDisplay({ proxy_url: null });
showToast(t(state.locale, "settingsProxyCleared"));
} catch (error) {
showToast(error instanceof Error ? error.message : t(state.locale, "settingsProxySaveFailed"), true);
}
}

function codexCliSourceLabel(source: CodexCliStatus["source"]): string {
switch (source) {
case "user_override":
Expand Down Expand Up @@ -1067,6 +1115,26 @@ export function bootstrap(): void {
elements.settingsCodexCliButton.addEventListener("click", () => {
void openCodexCliDialog();
});
// 代理配置(mac 专属 UI;win 上 elements 为 null,绑定跳过)。
if (elements.settingsProxySaveButton) {
elements.settingsProxySaveButton.addEventListener("click", () => {
void handleSaveProxyConfig();
});
}
if (elements.settingsProxyClearButton) {
elements.settingsProxyClearButton.addEventListener("click", () => {
void handleClearProxyConfig();
});
}
// Enter 提交:与 Update URL 行为一致,省得用户去找 Save 按钮。
if (elements.settingsProxyInput) {
elements.settingsProxyInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
void handleSaveProxyConfig();
}
});
}
elements.localeEnButton.addEventListener("click", () => {
setLocale("en");
});
Expand Down Expand Up @@ -1110,6 +1178,7 @@ export function bootstrap(): void {
scheduleDailyPlanRefresh();

void refreshCodexCliSettingsDisplay();
void refreshProxySettingsDisplay();

state.loading = true;
rerenderDashboard();
Expand Down
23 changes: 23 additions & 0 deletions src-tauri/shared/front/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,29 @@ p {
flex: none;
}

/* Proxy row reuses .settings-cli-inline to lay out an input + Save/Clear
buttons. Let the input take the flexible remainder instead of its
default width:100% squeezing the buttons. */
.settings-cli-inline .settings-input {
flex: 1;
min-width: 0;
}

.settings-hint {
grid-column: 1 / -1;
margin: 6px 0 0;
color: var(--text-muted);
font-size: 0.78rem;
line-height: 1.4;
}

.settings-row--proxy {
min-height: auto;
align-items: center;
gap: 10px;
padding: 14px 0;
}

.settings-value--inline {
flex: 1;
min-width: 0;
Expand Down
16 changes: 16 additions & 0 deletions src-tauri/shared/front/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,14 @@ const enMessages = {
settingsCodexCliEmpty: "Not detected",
settingsCodexCliDetect: "Auto-detect",
settingsCodexCliDetecting: "Detecting…",
settingsProxy: "Proxy",
settingsProxyHint:
"HTTP/HTTPS/SOCKS5. Applied to ChatGPT plan/quota refresh, codex login, and the update check. Empty = direct.",
settingsProxySave: "Save",
settingsProxyClear: "Clear",
settingsProxySaved: "Proxy saved.",
settingsProxyCleared: "Proxy cleared (direct connection).",
settingsProxySaveFailed: "Failed to save proxy.",
codexCliDetectedHeading: "Detected (verified runnable)",
codexCliDetectApplied: "Detected and set: {path}",
codexCliDetectNone: "Couldn't auto-detect codex. Set the path manually below.",
Expand Down Expand Up @@ -515,6 +523,14 @@ const messages: Record<Locale, Messages> = {
settingsCodexCliEmpty: "未检测到",
settingsCodexCliDetect: "自动检测",
settingsCodexCliDetecting: "检测中…",
settingsProxy: "代理",
settingsProxyHint:
"支持 HTTP/HTTPS/SOCKS5。应用于 ChatGPT 套餐/配额刷新、codex 登录及更新检查。留空 = 直连。",
settingsProxySave: "保存",
settingsProxyClear: "清空",
settingsProxySaved: "代理已保存。",
settingsProxyCleared: "已清空代理(恢复直连)。",
settingsProxySaveFailed: "保存代理失败。",
codexCliDetectedHeading: "已检测到(已验证可运行)",
codexCliDetectApplied: "已检测并设置:{path}",
codexCliDetectNone: "未能自动检测到 codex,请在下方手动设置路径。",
Expand Down
Loading
Loading