Skip to content
This repository was archived by the owner on Jul 24, 2026. It is now read-only.

Commit 0664aa7

Browse files
committed
Add Windows support
1 parent c4d419c commit 0664aa7

4 files changed

Lines changed: 257 additions & 23 deletions

File tree

README.md

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,54 @@ pip3 install --user requests websocket-client
7676

7777
2 个 binary 装到 `~/.local/bin/` (覆盖位置走 `SUB2CLI_INSTALL_DIR` env). `sub2cli-inject` 零依赖, `sub2cli` 需要 `requests` + `websocket-client`.
7878

79+
**Windows PowerShell:**
80+
81+
```powershell
82+
git clone https://github.com/r266-tech/sub2cli
83+
cd sub2cli
84+
.\install.ps1
85+
python -m pip install --user requests websocket-client
86+
```
87+
88+
Windows 下会额外生成同名 `.cmd` 包装器。
89+
如果 PowerShell 执行策略拦截脚本:
90+
91+
```powershell
92+
powershell -ExecutionPolicy Bypass -File .\install.ps1
93+
```
94+
95+
如果系统默认 `python` 不是目标 Python:
96+
97+
```powershell
98+
.\install.ps1 -Python "C:\Path\To\python.exe"
99+
```
100+
101+
如果安装目录不在 PATH, 按安装脚本提示加入用户 PATH, 或当前 PowerShell 临时加入:
102+
103+
```powershell
104+
$env:Path += ";$HOME\.local\bin"
105+
```
106+
79107
启动:
80108

81109
```bash
82110
sub2cli
83111
```
84112

113+
Windows 直接注入 Codex CLI:
114+
115+
```powershell
116+
sub2cli-inject add-api https://relay.example.com/v1 sk-...
117+
```
118+
119+
Windows 下 `sub2cli-inject` 会写入 `%USERPROFILE%\.codex\auth.<slot>.json`,
120+
切换渠道时复制当前 slot 到 `%USERPROFILE%\.codex\auth.json`, 并更新
121+
`%USERPROFILE%\.codex\config.toml`. 不管理 Codex App profile symlink; 如果
122+
Codex App 正在运行, 切换后手动重新打开。
123+
85124
## 依赖
86125

87-
- macOS (Codex App 路径依赖 `~/Library/Application Support/Codex`)
126+
- macOS 或 Windows
88127
- Python 3.10+
89128
- Edge / Chromium with `--remote-debugging-port=9222` (用于读浏览器里 Sub2API 网页的 auth_token)
90129
- pip 包: `requests`, `websocket-client` (sub2cli 主体)
@@ -128,7 +167,8 @@ sub2cli-inject Codex 渠道写入器 (vendored from r266-tech/codex-provider
128167

129168
`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`).
130169

131-
`sub2cli-inject``~/.codex/auth.<slot>.json` + 改 `~/.codex/config.toml``[model_providers.OpenAI]` + symlink + 重启 Codex App.
170+
`sub2cli-inject``~/.codex/auth.<slot>.json` + 改 `~/.codex/config.toml``[model_providers.OpenAI]`.
171+
macOS 下用 symlink 切换 `auth.json` / Codex App profile 并重启 Codex App;Windows 下复制当前 slot 的 `auth.json``~/.codex/auth.json`,保留同样的 Codex CLI 配置切换能力,Codex App 需要手动重新打开。
132172

133173
## Upstream / 致谢
134174

install.ps1

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
param(
2+
[string]$InstallDir = $(if ($env:SUB2CLI_INSTALL_DIR) { $env:SUB2CLI_INSTALL_DIR } else { Join-Path $env:USERPROFILE ".local\bin" }),
3+
[string]$Python = $(if ($env:PYTHON) { $env:PYTHON } else { "python" })
4+
)
5+
6+
$ErrorActionPreference = "Stop"
7+
$bins = @("sub2cli", "sub2cli-inject")
8+
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
9+
10+
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
11+
12+
foreach ($bin in $bins) {
13+
Copy-Item -LiteralPath (Join-Path $scriptDir $bin) -Destination (Join-Path $InstallDir $bin) -Force
14+
$cmd = @(
15+
"@echo off",
16+
"`"$Python`" `"%~dp0$bin`" %*"
17+
)
18+
Set-Content -LiteralPath (Join-Path $InstallDir "$bin.cmd") -Encoding ASCII -Value $cmd
19+
Write-Host "Installed: $(Join-Path $InstallDir "$bin.cmd")"
20+
}
21+
22+
Write-Host ""
23+
Write-Host "Python dependencies:"
24+
Write-Host " python -m pip install --user requests websocket-client"
25+
Write-Host ""
26+
if (($env:PATH -split ';') -notcontains $InstallDir) {
27+
Write-Host "$InstallDir is not in PATH."
28+
Write-Host "Add it for the current user:"
29+
Write-Host " [Environment]::SetEnvironmentVariable('Path', `$env:Path + ';$InstallDir', 'User')"
30+
}
31+
Write-Host ""
32+
Write-Host "Run: sub2cli"

sub2cli

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,18 @@ import select
2323
import shutil
2424
import subprocess
2525
import sys
26-
import termios
2726
import time
28-
import tty
2927
import urllib.request
3028
from urllib.parse import urlparse
3129

30+
if os.name == "nt":
31+
import msvcrt
32+
termios = None
33+
tty = None
34+
else:
35+
import termios
36+
import tty
37+
3238
try:
3339
import readline # noqa: F401 启用 input() 的行编辑 (退格/CJK 宽度/Ctrl-U/方向键)
3440
except ImportError:
@@ -486,6 +492,8 @@ def read_input(prompt: str) -> str | None:
486492
first = input(prompt)
487493
except EOFError:
488494
return None
495+
if os.name == "nt":
496+
return first
489497
chunks = [first]
490498
deadline = time.time() + 0.2
491499
while True:
@@ -522,7 +530,7 @@ def ask_choice(prompt: str, n: int, default: int | None = None) -> int | None:
522530
print(color(f" 请输入 1..{n} 之间的整数", C_YELLOW))
523531

524532

525-
# ==================== TUI 菜单(基于 termios,参考 codex-provider-macos)====================
533+
# ==================== TUI 菜单(Unix termios / Windows msvcrt)====================
526534

527535
def clear_screen() -> None:
528536
if not sys.stdout.isatty():
@@ -535,6 +543,22 @@ def read_key() -> str:
535543
"""阻塞读一个键。返回: up / down / enter / quit / back / 或单字符串。"""
536544
if not sys.stdin.isatty():
537545
return "quit"
546+
if os.name == "nt":
547+
ch = msvcrt.getwch()
548+
if ch in ("\x00", "\xe0"):
549+
code = msvcrt.getwch()
550+
if code in ("H", "K"):
551+
return "up"
552+
if code in ("P", "M"):
553+
return "down"
554+
return "back"
555+
if ch in ("\r", "\n"):
556+
return "enter"
557+
if ch in ("q", "Q", "\x03"):
558+
return "quit"
559+
if ch in ("b", "B", "\x08"):
560+
return "back"
561+
return ""
538562
fd = sys.stdin.fileno()
539563
old = termios.tcgetattr(fd)
540564
try:
@@ -985,9 +1009,9 @@ def cmd_export(token: str, cfg: dict) -> None:
9851009
print(color("shell 片段:", C_BOLD))
9861010
for ln in snippet.splitlines():
9871011
print(f" {ln}")
988-
# pbcopy
1012+
clipboard_cmd = ["clip.exe"] if os.name == "nt" else ["pbcopy"]
9891013
try:
990-
subprocess.run(["pbcopy"], input=snippet, text=True, check=True, timeout=3)
1014+
subprocess.run(clipboard_cmd, input=snippet, text=True, check=True, timeout=3)
9911015
print(color(" ✓ 已复制到剪贴板", C_GREEN))
9921016
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
9931017
print(color(" (剪贴板复制失败,自己粘上方文本)", C_DIM))
@@ -998,12 +1022,13 @@ def _resolve_inject_bin() -> str | None:
9981022
here = os.path.dirname(os.path.abspath(__file__))
9991023
candidates = [
10001024
os.path.join(here, "sub2cli-inject"),
1025+
os.path.join(here, "sub2cli-inject.cmd"),
10011026
os.path.expanduser("~/.local/bin/sub2cli-inject"),
10021027
os.path.expanduser("~/.local/bin/codex-provider"), # 兼容已装 codex-provider 的用户
10031028
shutil.which("sub2cli-inject"),
10041029
shutil.which("codex-provider"),
10051030
]
1006-
return next((p for p in candidates if p and os.path.isfile(p) and os.access(p, os.X_OK)), None)
1031+
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)
10071032

10081033

10091034
def _run_inject(inject_bin: str, url: str, api_key: str, source_label: str) -> None:

0 commit comments

Comments
 (0)