From 0664aa70b9af070398a5b29d48abe4a3765d2709 Mon Sep 17 00:00:00 2001 From: staminaoo Date: Wed, 20 May 2026 18:24:24 +0800 Subject: [PATCH 1/2] Add Windows support --- README.md | 44 ++++++++++++- install.ps1 | 32 ++++++++++ sub2cli | 37 +++++++++-- sub2cli-inject | 167 ++++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 257 insertions(+), 23 deletions(-) create mode 100644 install.ps1 diff --git a/README.md b/README.md index 78db16b..e42416b 100644 --- a/README.md +++ b/README.md @@ -76,15 +76,54 @@ pip3 install --user requests websocket-client 2 个 binary 装到 `~/.local/bin/` (覆盖位置走 `SUB2CLI_INSTALL_DIR` env). `sub2cli-inject` 零依赖, `sub2cli` 需要 `requests` + `websocket-client`. +**Windows PowerShell:** + +```powershell +git clone https://github.com/r266-tech/sub2cli +cd sub2cli +.\install.ps1 +python -m pip install --user requests websocket-client +``` + +Windows 下会额外生成同名 `.cmd` 包装器。 +如果 PowerShell 执行策略拦截脚本: + +```powershell +powershell -ExecutionPolicy Bypass -File .\install.ps1 +``` + +如果系统默认 `python` 不是目标 Python: + +```powershell +.\install.ps1 -Python "C:\Path\To\python.exe" +``` + +如果安装目录不在 PATH, 按安装脚本提示加入用户 PATH, 或当前 PowerShell 临时加入: + +```powershell +$env:Path += ";$HOME\.local\bin" +``` + 启动: ```bash sub2cli ``` +Windows 直接注入 Codex CLI: + +```powershell +sub2cli-inject add-api https://relay.example.com/v1 sk-... +``` + +Windows 下 `sub2cli-inject` 会写入 `%USERPROFILE%\.codex\auth..json`, +切换渠道时复制当前 slot 到 `%USERPROFILE%\.codex\auth.json`, 并更新 +`%USERPROFILE%\.codex\config.toml`. 不管理 Codex App profile symlink; 如果 +Codex App 正在运行, 切换后手动重新打开。 + ## 依赖 -- macOS (Codex App 路径依赖 `~/Library/Application Support/Codex`) +- macOS 或 Windows - Python 3.10+ - Edge / Chromium with `--remote-debugging-port=9222` (用于读浏览器里 Sub2API 网页的 auth_token) - pip 包: `requests`, `websocket-client` (sub2cli 主体) @@ -128,7 +167,8 @@ sub2cli-inject Codex 渠道写入器 (vendored from r266-tech/codex-provider `sub2cli` 启动时从 Edge CDP (`http://127.0.0.1:9222`) 读 Sub2API 网页的 `localStorage.auth_token`, 调网关 REST API (`/auth/me` / `/keys` / `/groups/available` / `/settings/public` / `/redeem/history` / `/chat/completions` / `/images/generations`). -`sub2cli-inject` 写 `~/.codex/auth..json` + 改 `~/.codex/config.toml` 的 `[model_providers.OpenAI]` + symlink + 重启 Codex App. +`sub2cli-inject` 写 `~/.codex/auth..json` + 改 `~/.codex/config.toml` 的 `[model_providers.OpenAI]`. +macOS 下用 symlink 切换 `auth.json` / Codex App profile 并重启 Codex App;Windows 下复制当前 slot 的 `auth.json` 到 `~/.codex/auth.json`,保留同样的 Codex CLI 配置切换能力,Codex App 需要手动重新打开。 ## Upstream / 致谢 diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..12bc496 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,32 @@ +param( + [string]$InstallDir = $(if ($env:SUB2CLI_INSTALL_DIR) { $env:SUB2CLI_INSTALL_DIR } else { Join-Path $env:USERPROFILE ".local\bin" }), + [string]$Python = $(if ($env:PYTHON) { $env:PYTHON } else { "python" }) +) + +$ErrorActionPreference = "Stop" +$bins = @("sub2cli", "sub2cli-inject") +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null + +foreach ($bin in $bins) { + Copy-Item -LiteralPath (Join-Path $scriptDir $bin) -Destination (Join-Path $InstallDir $bin) -Force + $cmd = @( + "@echo off", + "`"$Python`" `"%~dp0$bin`" %*" + ) + Set-Content -LiteralPath (Join-Path $InstallDir "$bin.cmd") -Encoding ASCII -Value $cmd + Write-Host "Installed: $(Join-Path $InstallDir "$bin.cmd")" +} + +Write-Host "" +Write-Host "Python dependencies:" +Write-Host " python -m pip install --user requests websocket-client" +Write-Host "" +if (($env:PATH -split ';') -notcontains $InstallDir) { + Write-Host "$InstallDir is not in PATH." + Write-Host "Add it for the current user:" + Write-Host " [Environment]::SetEnvironmentVariable('Path', `$env:Path + ';$InstallDir', 'User')" +} +Write-Host "" +Write-Host "Run: sub2cli" diff --git a/sub2cli b/sub2cli index 0777469..2036538 100755 --- a/sub2cli +++ b/sub2cli @@ -23,12 +23,18 @@ import select import shutil import subprocess import sys -import termios import time -import tty import urllib.request from urllib.parse import urlparse +if os.name == "nt": + import msvcrt + termios = None + tty = None +else: + import termios + import tty + try: import readline # noqa: F401 启用 input() 的行编辑 (退格/CJK 宽度/Ctrl-U/方向键) except ImportError: @@ -486,6 +492,8 @@ def read_input(prompt: str) -> str | None: first = input(prompt) except EOFError: return None + if os.name == "nt": + return first chunks = [first] deadline = time.time() + 0.2 while True: @@ -522,7 +530,7 @@ def ask_choice(prompt: str, n: int, default: int | None = None) -> int | None: print(color(f" 请输入 1..{n} 之间的整数", C_YELLOW)) -# ==================== TUI 菜单(基于 termios,参考 codex-provider-macos)==================== +# ==================== TUI 菜单(Unix termios / Windows msvcrt)==================== def clear_screen() -> None: if not sys.stdout.isatty(): @@ -535,6 +543,22 @@ def read_key() -> str: """阻塞读一个键。返回: up / down / enter / quit / back / 或单字符串。""" if not sys.stdin.isatty(): return "quit" + if os.name == "nt": + ch = msvcrt.getwch() + if ch in ("\x00", "\xe0"): + code = msvcrt.getwch() + if code in ("H", "K"): + return "up" + if code in ("P", "M"): + return "down" + return "back" + if ch in ("\r", "\n"): + return "enter" + if ch in ("q", "Q", "\x03"): + return "quit" + if ch in ("b", "B", "\x08"): + return "back" + return "" fd = sys.stdin.fileno() old = termios.tcgetattr(fd) try: @@ -985,9 +1009,9 @@ def cmd_export(token: str, cfg: dict) -> None: print(color("shell 片段:", C_BOLD)) for ln in snippet.splitlines(): print(f" {ln}") - # pbcopy + clipboard_cmd = ["clip.exe"] if os.name == "nt" else ["pbcopy"] try: - subprocess.run(["pbcopy"], input=snippet, text=True, check=True, timeout=3) + subprocess.run(clipboard_cmd, input=snippet, text=True, check=True, timeout=3) print(color(" ✓ 已复制到剪贴板", C_GREEN)) except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired): print(color(" (剪贴板复制失败,自己粘上方文本)", C_DIM)) @@ -998,12 +1022,13 @@ def _resolve_inject_bin() -> str | None: here = os.path.dirname(os.path.abspath(__file__)) candidates = [ os.path.join(here, "sub2cli-inject"), + os.path.join(here, "sub2cli-inject.cmd"), os.path.expanduser("~/.local/bin/sub2cli-inject"), os.path.expanduser("~/.local/bin/codex-provider"), # 兼容已装 codex-provider 的用户 shutil.which("sub2cli-inject"), shutil.which("codex-provider"), ] - return next((p for p in candidates if p and os.path.isfile(p) and os.access(p, os.X_OK)), None) + return next((p for p in candidates if p and os.path.isfile(p) and (os.name == "nt" or os.access(p, os.X_OK))), None) def _run_inject(inject_bin: str, url: str, api_key: str, source_label: str) -> None: diff --git a/sub2cli-inject b/sub2cli-inject index bc26788..c9c8d38 100755 --- a/sub2cli-inject +++ b/sub2cli-inject @@ -17,9 +17,15 @@ import select import sqlite3 import subprocess import sys -import termios import time -import tty + +if os.name == "nt": + import msvcrt + termios = None + tty = None +else: + import termios + import tty try: import readline # noqa: F401 启用 input() 的行编辑 (退格/CJK 宽度/Ctrl-U/方向键) @@ -31,9 +37,14 @@ from urllib import error as urlerror from urllib import parse as urlparse from urllib import request as urlrequest +IS_WINDOWS = os.name == "nt" HOME = Path(os.environ.get("CODEX_PROVIDER_HOME", str(Path.home()))).expanduser() CODEX_HOME = Path(os.environ.get("CODEX_HOME", str(HOME / ".codex"))).expanduser() -APP_SUPPORT = HOME / "Library" / "Application Support" +APP_SUPPORT = ( + Path(os.environ.get("LOCALAPPDATA", str(HOME / "AppData" / "Local"))) / "Codex" + if IS_WINDOWS + else HOME / "Library" / "Application Support" +) APP_PROFILE = APP_SUPPORT / "Codex" AUTH_JSON = CODEX_HOME / "auth.json" CONFIG_TOML = CODEX_HOME / "config.toml" @@ -117,6 +128,49 @@ def atomic_symlink(link_path: Path, target: Path) -> None: os.replace(tmp, link_path) +def copy_path_atomic(src: Path, dst: Path) -> bool: + if not src.exists(): + return False + if dst.exists(): + try: + if src.read_bytes() == dst.read_bytes(): + return False + except OSError: + pass + dst.parent.mkdir(parents=True, exist_ok=True) + tmp = dst.with_name(f"{dst.name}.{os.getpid()}.{time.time_ns()}.tmp") + try: + tmp.write_bytes(src.read_bytes()) + tmp.replace(dst) + except Exception: + if tmp.exists() or tmp.is_symlink(): + try: + tmp.unlink() + except OSError: + pass + raise + return True + + +def install_auth_for_windows_slot(auth_target: Path) -> bool: + if auth_target.exists(): + return copy_path_atomic(auth_target, AUTH_JSON) + if AUTH_JSON.exists() or AUTH_JSON.is_symlink(): + AUTH_JSON.unlink() + return True + return False + + +def save_current_windows_auth(data: dict) -> None: + current = data.get("current") + slots = data.get("slots", {}) + if current not in slots or not AUTH_JSON.exists(): + return + auth_path = Path(slots[current].get("auth_file", "")).expanduser() + if auth_path: + copy_path_atomic(AUTH_JSON, auth_path) + + def symlink_points_to(link_path: Path, target: Path) -> bool: if not link_path.is_symlink(): return False @@ -174,12 +228,28 @@ def save_slots(data: dict) -> None: def codex_running() -> bool: + if IS_WINDOWS: + result = subprocess.run( + ["tasklist", "/FI", "IMAGENAME eq Codex.exe", "/NH"], + capture_output=True, + text=True, + ) + return result.returncode == 0 and "Codex.exe" in result.stdout return subprocess.run(["pgrep", "-x", "Codex"], capture_output=True).returncode == 0 def quit_codex(timeout: float = 8.0) -> bool: if not codex_running(): return False + if IS_WINDOWS: + subprocess.run(["taskkill", "/IM", "Codex.exe"], check=False, capture_output=True) + deadline = time.time() + timeout + while time.time() < deadline: + if not codex_running(): + return True + time.sleep(0.25) + fail("Codex.exe 退出超时。请手动退出 Codex App 后重试。") + return True try: subprocess.run( ["osascript", "-e", 'tell application id "com.openai.codex" to quit'], @@ -200,6 +270,9 @@ def quit_codex(timeout: float = 8.0) -> bool: def launch_codex() -> None: + if IS_WINDOWS: + print(" Windows: 请手动重新打开 Codex App。") + return subprocess.Popen(["open", "-a", "Codex"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) @@ -219,6 +292,33 @@ def decode_auth_email(auth_file: Path) -> str | None: def cmd_init(slot: str = "local", display: str | None = None, *, quiet: bool = False) -> int: slot = validate_slot(slot) data = load_slots(required=False) + if IS_WINDOWS: + if data.get("current") and data.get("slots"): + if not quiet: + print(f"already initialized: current={data['current']}") + print(f"slots file: {SLOTS_FILE}") + return 0 + CODEX_HOME.mkdir(parents=True, exist_ok=True) + auth_slot = CODEX_HOME / f"auth.{slot}.json" + if AUTH_JSON.exists() and not auth_slot.exists(): + copy_path_atomic(AUTH_JSON, auth_slot) + email = decode_auth_email(auth_slot) if auth_slot.exists() else None + display_name = display or (f"Codex - {email}" if email else f"Codex - {slot}") + data["slots"][slot] = { + "display_name": display_name, + "mode": "oauth", + "auth_file": str(auth_slot), + "app_profile_dir": "", + } + data["current"] = slot + data["app_history_slot"] = slot + save_slots(data) + if not quiet: + print(f"initialized: {slot}") + print(f" auth: {auth_slot}") + print(f" slots: {SLOTS_FILE}") + return 0 + if data.get("current") and data.get("slots") and AUTH_JSON.is_symlink() and APP_PROFILE.is_symlink(): if not quiet: print(f"already initialized: current={data['current']}") @@ -654,10 +754,17 @@ def slot_is_clean(data: dict, target_slot: str) -> bool: profile_target = history_profile_target(data, target_slot) if data.get("current") != target_slot: return False - if not symlink_points_to(AUTH_JSON, auth_target): - return False - if not symlink_points_to(APP_PROFILE, profile_target): - return False + if IS_WINDOWS: + try: + if not AUTH_JSON.exists() or AUTH_JSON.read_bytes() != auth_target.read_bytes(): + return False + except OSError: + return False + else: + if not symlink_points_to(AUTH_JSON, auth_target): + return False + if not symlink_points_to(APP_PROFILE, profile_target): + return False if cfg.get("mode") == "relay": api_key = str(cfg.get("api_key") or "") try: @@ -685,13 +792,17 @@ def cmd_switch(name: str, *, no_restart: bool = False) -> int: print(" 无需修改;Codex App 未重启") return 0 + if IS_WINDOWS: + save_current_windows_auth(data) + if not no_restart: quit_codex() auth_target = Path(cfg["auth_file"]).expanduser() profile_target = history_profile_target(data, target_slot) auth_target.parent.mkdir(parents=True, exist_ok=True) - profile_target.mkdir(parents=True, exist_ok=True) + if not IS_WINDOWS: + profile_target.mkdir(parents=True, exist_ok=True) if mode == "relay": api_key = str(cfg.get("api_key") or "").strip() @@ -703,8 +814,11 @@ def cmd_switch(name: str, *, no_restart: bool = False) -> int: else: patch_config(mode="oauth", model=cfg.get("model", DEFAULT_MODEL)) - atomic_symlink(AUTH_JSON, auth_target) - atomic_symlink(APP_PROFILE, profile_target) + if IS_WINDOWS: + install_auth_for_windows_slot(auth_target) + else: + atomic_symlink(AUTH_JSON, auth_target) + atomic_symlink(APP_PROFILE, profile_target) session_stats = normalize_sessions_if_needed(slot_thread_provider(cfg)) previous = data.get("current") @@ -714,8 +828,9 @@ def cmd_switch(name: str, *, no_restart: bool = False) -> int: verb = "已修复当前渠道" if previous == target_slot else "已切换到" print(f"{verb}:{cfg.get('display_name', target_slot)} ({target_slot}) [{mode}]") - print(f" auth.json -> {auth_target}") - print(f" App profile -> {profile_target}(历史保留)") + print(f" auth.json {'<-' if IS_WINDOWS else '->'} {auth_target}") + if not IS_WINDOWS: + print(f" App profile -> {profile_target}(历史保留)") if mode == "relay": print(f" base_url -> {cfg.get('base_url')}") print(f" model -> {cfg.get('model', DEFAULT_MODEL)}") @@ -724,7 +839,8 @@ def cmd_switch(name: str, *, no_restart: bool = False) -> int: print_session_summary(session_stats) if not no_restart: launch_codex() - print(" Codex App 已重新打开") + if not IS_WINDOWS: + print(" Codex App 已重新打开") else: print(" --no-restart:Codex App 未重新打开") return 0 @@ -847,7 +963,8 @@ def cmd_status() -> int: print(f"当前渠道:{cfg.get('display_name', current)} ({current}) [{cfg.get('mode', 'oauth')}]") print(f" 渠道文件: {SLOTS_FILE}") print(f" auth.json: {'-> ' + os.readlink(AUTH_JSON) if AUTH_JSON.is_symlink() else str(AUTH_JSON)}") - print(f" App profile: {'-> ' + os.readlink(APP_PROFILE) if APP_PROFILE.is_symlink() else str(APP_PROFILE)}") + if not IS_WINDOWS: + print(f" App profile: {'-> ' + os.readlink(APP_PROFILE) if APP_PROFILE.is_symlink() else str(APP_PROFILE)}") print(f" App 运行中: {'是' if codex_running() else '否'}") counts = thread_provider_counts() if counts: @@ -875,7 +992,7 @@ def cmd_remove(slot: str, *, hard: bool = False) -> int: if CODEX_HOME.resolve(strict=False) in auth_path.resolve(strict=False).parents: auth_path.unlink() print(f" removed {auth_path}") - if profile_path.exists() and not profile_path.is_symlink(): + if (not IS_WINDOWS) and profile_path.exists() and not profile_path.is_symlink(): if APP_SUPPORT.resolve(strict=False) in profile_path.resolve(strict=False).parents: import shutil @@ -913,6 +1030,26 @@ def pause() -> None: def read_key() -> str: + if os.name == "nt": + ch = msvcrt.getwch() + if ch in ("\x00", "\xe0"): + code = msvcrt.getwch() + if code in ("H", "K"): + return "up" + if code in ("P", "M"): + return "down" + return "back" + if ch in ("\r", "\n"): + return "enter" + if ch in ("q", "Q", "\x03"): + return "quit" + if ch in ("b", "B", "\x08"): + return "back" + if ch in ("v", "V"): + return "version" + if ch in ("m", "M"): + return "more" + return "" fd = sys.stdin.fileno() old = termios.tcgetattr(fd) try: From aa65b2d0d841d1846d936d8c8e73f3235b8d27eb Mon Sep 17 00:00:00 2001 From: staminaoo Date: Thu, 21 May 2026 10:45:38 +0800 Subject: [PATCH 2/2] Address Windows review feedback --- README.md | 2 +- install.ps1 | 2 +- sub2cli | 7 ++++-- sub2cli-inject | 68 ++++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 62 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index e42416b..6fc880e 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ cd sub2cli python -m pip install --user requests websocket-client ``` -Windows 下会额外生成同名 `.cmd` 包装器。 +Windows 下会额外生成同名 `.cmd` 包装器。包装器会嵌入安装时解析到的 Python 路径;如果之后移动或更换 Python, 重新运行 `.\install.ps1`。 如果 PowerShell 执行策略拦截脚本: ```powershell diff --git a/install.ps1 b/install.ps1 index 12bc496..5ef722b 100644 --- a/install.ps1 +++ b/install.ps1 @@ -15,7 +15,7 @@ foreach ($bin in $bins) { "@echo off", "`"$Python`" `"%~dp0$bin`" %*" ) - Set-Content -LiteralPath (Join-Path $InstallDir "$bin.cmd") -Encoding ASCII -Value $cmd + Set-Content -LiteralPath (Join-Path $InstallDir "$bin.cmd") -Encoding UTF8 -Value $cmd Write-Host "Installed: $(Join-Path $InstallDir "$bin.cmd")" } diff --git a/sub2cli b/sub2cli index 2036538..6a44956 100755 --- a/sub2cli +++ b/sub2cli @@ -551,7 +551,7 @@ def read_key() -> str: return "up" if code in ("P", "M"): return "down" - return "back" + return "" if ch in ("\r", "\n"): return "enter" if ch in ("q", "Q", "\x03"): @@ -1039,7 +1039,10 @@ def _run_inject(inject_bin: str, url: str, api_key: str, source_label: str) -> N print(color(f" 调用: {inject_bin} add-api ", C_DIM)) print() try: - rc = subprocess.call([sys.executable, inject_bin, "add-api", url, api_key]) + if os.name == "nt" and inject_bin.lower().endswith(".cmd"): + rc = subprocess.call([inject_bin, "add-api", url, api_key]) + else: + rc = subprocess.call([sys.executable, inject_bin, "add-api", url, api_key]) except OSError as exc: print(color(f" 调用注入器失败: {exc}", C_RED)) return diff --git a/sub2cli-inject b/sub2cli-inject index c9c8d38..078992c 100755 --- a/sub2cli-inject +++ b/sub2cli-inject @@ -152,13 +152,50 @@ def copy_path_atomic(src: Path, dst: Path) -> bool: return True -def install_auth_for_windows_slot(auth_target: Path) -> bool: +def prepare_windows_auth_temp(auth_target: Path) -> tuple[Path, bool]: + tmp = AUTH_JSON.with_name(f"{AUTH_JSON.name}.{os.getpid()}.{time.time_ns()}.tmp") + if tmp.exists() or tmp.is_symlink(): + tmp.unlink() if auth_target.exists(): - return copy_path_atomic(auth_target, AUTH_JSON) - if AUTH_JSON.exists() or AUTH_JSON.is_symlink(): - AUTH_JSON.unlink() - return True - return False + tmp.write_bytes(auth_target.read_bytes()) + return tmp, False + return tmp, True + + +def install_prepared_windows_auth(tmp: Path, remove_auth: bool) -> None: + try: + if remove_auth: + if AUTH_JSON.exists() or AUTH_JSON.is_symlink(): + AUTH_JSON.unlink() + return + tmp.replace(AUTH_JSON) + finally: + if tmp.exists() or tmp.is_symlink(): + try: + tmp.unlink() + except OSError: + pass + + +def windows_auth_matches(auth_target: Path) -> bool: + if not auth_target.exists(): + return not AUTH_JSON.exists() + try: + return AUTH_JSON.exists() and AUTH_JSON.read_bytes() == auth_target.read_bytes() + except OSError: + return False + + +def recover_windows_current_slot(data: dict) -> None: + current = data.get("current") + slots = data.get("slots", {}) + if current not in slots: + return + auth_path = Path(slots[current].get("auth_file", "")).expanduser() + if windows_auth_matches(auth_path): + return + tmp, remove_auth = prepare_windows_auth_temp(auth_path) + install_prepared_windows_auth(tmp, remove_auth) def save_current_windows_auth(data: dict) -> None: @@ -294,6 +331,7 @@ def cmd_init(slot: str = "local", display: str | None = None, *, quiet: bool = F data = load_slots(required=False) if IS_WINDOWS: if data.get("current") and data.get("slots"): + recover_windows_current_slot(data) if not quiet: print(f"already initialized: current={data['current']}") print(f"slots file: {SLOTS_FILE}") @@ -780,6 +818,8 @@ def slot_is_clean(data: dict, target_slot: str) -> bool: def cmd_switch(name: str, *, no_restart: bool = False) -> int: ensure_initialized() data = load_slots() + if IS_WINDOWS: + recover_windows_current_slot(data) slots = data.get("slots", {}) target_slot = resolve_slot(name, slots) cfg = slots[target_slot] @@ -792,12 +832,12 @@ def cmd_switch(name: str, *, no_restart: bool = False) -> int: print(" 无需修改;Codex App 未重启") return 0 - if IS_WINDOWS: - save_current_windows_auth(data) - if not no_restart: quit_codex() + if IS_WINDOWS: + save_current_windows_auth(data) + auth_target = Path(cfg["auth_file"]).expanduser() profile_target = history_profile_target(data, target_slot) auth_target.parent.mkdir(parents=True, exist_ok=True) @@ -815,7 +855,7 @@ def cmd_switch(name: str, *, no_restart: bool = False) -> int: patch_config(mode="oauth", model=cfg.get("model", DEFAULT_MODEL)) if IS_WINDOWS: - install_auth_for_windows_slot(auth_target) + auth_tmp, remove_auth = prepare_windows_auth_temp(auth_target) else: atomic_symlink(AUTH_JSON, auth_target) atomic_symlink(APP_PROFILE, profile_target) @@ -825,6 +865,8 @@ def cmd_switch(name: str, *, no_restart: bool = False) -> int: data["current"] = target_slot data.setdefault("app_history_slot", target_slot) save_slots(data) + if IS_WINDOWS: + install_prepared_windows_auth(auth_tmp, remove_auth) verb = "已修复当前渠道" if previous == target_slot else "已切换到" print(f"{verb}:{cfg.get('display_name', target_slot)} ({target_slot}) [{mode}]") @@ -882,7 +924,7 @@ def cmd_relay( "display_name": display or (existing or {}).get("display_name") or f"Codex - {slot} relay", "mode": "relay", "auth_file": str(CODEX_HOME / f"auth.{slot}.json"), - "app_profile_dir": str(APP_SUPPORT / f"Codex.{slot}"), + "app_profile_dir": "" if IS_WINDOWS else str(APP_SUPPORT / f"Codex.{slot}"), "base_url": base_url, "model": model, "api_key": api_key, @@ -917,7 +959,7 @@ def cmd_oauth( "display_name": display or (existing or {}).get("display_name") or f"Codex - {slot}", "mode": "oauth", "auth_file": str(CODEX_HOME / f"auth.{slot}.json"), - "app_profile_dir": str(APP_SUPPORT / f"Codex.{slot}"), + "app_profile_dir": "" if IS_WINDOWS else str(APP_SUPPORT / f"Codex.{slot}"), } save_slots(data) print(f"{'已更新' if existing else '已添加'}账号渠道:{slot}") @@ -1038,7 +1080,7 @@ def read_key() -> str: return "up" if code in ("P", "M"): return "down" - return "back" + return "" if ch in ("\r", "\n"): return "enter" if ch in ("q", "Q", "\x03"):