Spaces:
Paused
Paused
icebear0828 commited on
Commit ·
3cf831b
1
Parent(s): af3e19d
fix: update checker shows correct status + version display + dark mode fixes
Browse files- Fix 'Up to date' shown incorrectly when Codex version changed (add version_changed field)
- Change update status text from <span> to clickable <button>
- Add proxy version number next to 'Server Online' badge
- Fix dark mode for ProxyPool form inputs and AccountCard proxy selector
- Add fingerprintUpdating translation key (zh/en)
- README.md +18 -0
- README_EN.md +18 -0
- shared/hooks/use-update-status.ts +1 -0
- shared/i18n/translations.ts +2 -0
- src/routes/web.ts +3 -1
- web/src/App.tsx +5 -1
- web/src/components/AccountCard.tsx +1 -1
- web/src/components/Header.tsx +10 -3
- web/src/components/ProxyPool.tsx +1 -1
README.md
CHANGED
|
@@ -116,6 +116,13 @@ curl http://localhost:8080/v1/chat/completions \
|
|
| 116 |
- **稳定连接** — 自动对齐 Codex Desktop 请求特征,Cookie 持久化减少重复验证
|
| 117 |
- **Web 控制面板** — 账号管理、用量监控、状态总览,中英双语
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
## 🏗️ 技术架构 (Architecture)
|
| 120 |
|
| 121 |
```
|
|
@@ -309,6 +316,17 @@ server:
|
|
| 309 |
| `/auth/accounts` | GET | 账号列表(`?quota=true` 含配额) |
|
| 310 |
| `/auth/accounts/login` | GET | OAuth 登录入口 |
|
| 311 |
| `/debug/fingerprint` | GET | 调试:查看当前伪装头信息 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
|
| 313 |
## 🔧 命令 (Commands)
|
| 314 |
|
|
|
|
| 116 |
- **稳定连接** — 自动对齐 Codex Desktop 请求特征,Cookie 持久化减少重复验证
|
| 117 |
- **Web 控制面板** — 账号管理、用量监控、状态总览,中英双语
|
| 118 |
|
| 119 |
+
### 3. 🌐 代理池 (Proxy Pool)
|
| 120 |
+
- **Per-Account 代理路由** — 为不同账号配置不同的上游代理,实现 IP 多样化和风险隔离
|
| 121 |
+
- **四种分配模式** — Global Default(全局代理)、Direct(直连)、Auto(Round-Robin 轮转)、指定代理
|
| 122 |
+
- **健康检查** — 定时(默认 5 分钟)+ 手动,通过 ipify API 获取出口 IP 和延迟
|
| 123 |
+
- **不可达自动标记** — 代理不可达时自动标记为 unreachable,不参与自动轮转
|
| 124 |
+
- **Dashboard 管理面板** — 添加/删除/检查/启用/禁用代理,每个账号可选择代理或模式
|
| 125 |
+
|
| 126 |
## 🏗️ 技术架构 (Architecture)
|
| 127 |
|
| 128 |
```
|
|
|
|
| 316 |
| `/auth/accounts` | GET | 账号列表(`?quota=true` 含配额) |
|
| 317 |
| `/auth/accounts/login` | GET | OAuth 登录入口 |
|
| 318 |
| `/debug/fingerprint` | GET | 调试:查看当前伪装头信息 |
|
| 319 |
+
| `/api/proxies` | GET | 代理池列表(含分配信息) |
|
| 320 |
+
| `/api/proxies` | POST | 添加代理(HTTP/HTTPS/SOCKS5) |
|
| 321 |
+
| `/api/proxies/:id` | PUT | 更新代理配置 |
|
| 322 |
+
| `/api/proxies/:id` | DELETE | 删除代理 |
|
| 323 |
+
| `/api/proxies/:id/check` | POST | 单个代理健康检查 |
|
| 324 |
+
| `/api/proxies/:id/enable` | POST | 启用代理 |
|
| 325 |
+
| `/api/proxies/:id/disable` | POST | 禁用代理 |
|
| 326 |
+
| `/api/proxies/check-all` | POST | 全部代理健康检查 |
|
| 327 |
+
| `/api/proxies/assign` | POST | 为账号分配代理 |
|
| 328 |
+
| `/api/proxies/assign/:accountId` | DELETE | 取消账号代理分配 |
|
| 329 |
+
| `/api/proxies/settings` | PUT | 更新代理池全局设置 |
|
| 330 |
|
| 331 |
## 🔧 命令 (Commands)
|
| 332 |
|
README_EN.md
CHANGED
|
@@ -109,6 +109,13 @@ curl http://localhost:8080/v1/chat/completions \
|
|
| 109 |
- **Auto token refresh** — JWT renewed automatically before expiry
|
| 110 |
- **Real-time quota monitoring** — dashboard shows remaining usage per account
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
### 3. 🛡️ Anti-Detection & Protocol Impersonation
|
| 113 |
- **Chrome TLS fingerprint** — curl-impersonate replicates the full Chrome 136 TLS handshake
|
| 114 |
- **Desktop header replication** — `originator`, `User-Agent`, `sec-ch-*` headers in exact Codex Desktop order
|
|
@@ -292,6 +299,17 @@ All configuration is in `config/default.yaml`:
|
|
| 292 |
| `/auth/accounts` | GET | Account list and quota |
|
| 293 |
| `/auth/login` | GET | OAuth login entry |
|
| 294 |
| `/debug/fingerprint` | GET | Debug: view current impersonation headers |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
## 🔧 Commands
|
| 297 |
|
|
|
|
| 109 |
- **Auto token refresh** — JWT renewed automatically before expiry
|
| 110 |
- **Real-time quota monitoring** — dashboard shows remaining usage per account
|
| 111 |
|
| 112 |
+
### 3. 🌐 Proxy Pool
|
| 113 |
+
- **Per-account proxy routing** — assign different upstream proxies to different accounts for IP diversity and risk isolation
|
| 114 |
+
- **Four assignment modes** — Global Default, Direct (no proxy), Auto (round-robin rotation), or a specific proxy
|
| 115 |
+
- **Health checks** — scheduled (default every 5 min) + manual, reports exit IP and latency via ipify API
|
| 116 |
+
- **Auto-mark unreachable** — unreachable proxies are automatically flagged and excluded from auto-rotation
|
| 117 |
+
- **Dashboard management** — add/remove/check/enable/disable proxies, per-account proxy selector
|
| 118 |
+
|
| 119 |
### 3. 🛡️ Anti-Detection & Protocol Impersonation
|
| 120 |
- **Chrome TLS fingerprint** — curl-impersonate replicates the full Chrome 136 TLS handshake
|
| 121 |
- **Desktop header replication** — `originator`, `User-Agent`, `sec-ch-*` headers in exact Codex Desktop order
|
|
|
|
| 299 |
| `/auth/accounts` | GET | Account list and quota |
|
| 300 |
| `/auth/login` | GET | OAuth login entry |
|
| 301 |
| `/debug/fingerprint` | GET | Debug: view current impersonation headers |
|
| 302 |
+
| `/api/proxies` | GET | Proxy pool list (with assignments) |
|
| 303 |
+
| `/api/proxies` | POST | Add proxy (HTTP/HTTPS/SOCKS5) |
|
| 304 |
+
| `/api/proxies/:id` | PUT | Update proxy config |
|
| 305 |
+
| `/api/proxies/:id` | DELETE | Remove proxy |
|
| 306 |
+
| `/api/proxies/:id/check` | POST | Health check single proxy |
|
| 307 |
+
| `/api/proxies/:id/enable` | POST | Enable proxy |
|
| 308 |
+
| `/api/proxies/:id/disable` | POST | Disable proxy |
|
| 309 |
+
| `/api/proxies/check-all` | POST | Health check all proxies |
|
| 310 |
+
| `/api/proxies/assign` | POST | Assign proxy to account |
|
| 311 |
+
| `/api/proxies/assign/:accountId` | DELETE | Unassign proxy from account |
|
| 312 |
+
| `/api/proxies/settings` | PUT | Update proxy pool settings |
|
| 313 |
|
| 314 |
## 🔧 Commands
|
| 315 |
|
shared/hooks/use-update-status.ts
CHANGED
|
@@ -31,6 +31,7 @@ export interface CheckResult {
|
|
| 31 |
update_available: boolean;
|
| 32 |
current_version: string;
|
| 33 |
latest_version: string | null;
|
|
|
|
| 34 |
error?: string;
|
| 35 |
};
|
| 36 |
proxy_update_in_progress: boolean;
|
|
|
|
| 31 |
update_available: boolean;
|
| 32 |
current_version: string;
|
| 33 |
latest_version: string | null;
|
| 34 |
+
version_changed?: boolean;
|
| 35 |
error?: string;
|
| 36 |
};
|
| 37 |
proxy_update_in_progress: boolean;
|
shared/i18n/translations.ts
CHANGED
|
@@ -65,6 +65,7 @@ export const translations = {
|
|
| 65 |
upToDate: "Up to date",
|
| 66 |
updateApplied: "Update complete, please restart server",
|
| 67 |
fingerprintUpdated: "Fingerprint updated",
|
|
|
|
| 68 |
proxyUpdating: "Updating proxy...",
|
| 69 |
proxyBehind: "behind",
|
| 70 |
dockerUpdateHint: "Run: docker compose up -d --build",
|
|
@@ -171,6 +172,7 @@ export const translations = {
|
|
| 171 |
upToDate: "\u5df2\u662f\u6700\u65b0",
|
| 172 |
updateApplied: "\u66f4\u65b0\u5b8c\u6210\uff0c\u8bf7\u91cd\u542f\u670d\u52a1",
|
| 173 |
fingerprintUpdated: "\u6307\u7eb9\u5df2\u66f4\u65b0",
|
|
|
|
| 174 |
proxyUpdating: "\u6b63\u5728\u66f4\u65b0\u4ee3\u7406...",
|
| 175 |
proxyBehind: "\u843d\u540e",
|
| 176 |
dockerUpdateHint: "\u8bf7\u6267\u884c: docker compose up -d --build",
|
|
|
|
| 65 |
upToDate: "Up to date",
|
| 66 |
updateApplied: "Update complete, please restart server",
|
| 67 |
fingerprintUpdated: "Fingerprint updated",
|
| 68 |
+
fingerprintUpdating: "Updating fingerprint...",
|
| 69 |
proxyUpdating: "Updating proxy...",
|
| 70 |
proxyBehind: "behind",
|
| 71 |
dockerUpdateHint: "Run: docker compose up -d --build",
|
|
|
|
| 172 |
upToDate: "\u5df2\u662f\u6700\u65b0",
|
| 173 |
updateApplied: "\u66f4\u65b0\u5b8c\u6210\uff0c\u8bf7\u91cd\u542f\u670d\u52a1",
|
| 174 |
fingerprintUpdated: "\u6307\u7eb9\u5df2\u66f4\u65b0",
|
| 175 |
+
fingerprintUpdating: "\u6307\u7eb9\u66f4\u65b0\u4e2d...",
|
| 176 |
proxyUpdating: "\u6b63\u5728\u66f4\u65b0\u4ee3\u7406...",
|
| 177 |
proxyBehind: "\u843d\u540e",
|
| 178 |
dockerUpdateHint: "\u8bf7\u6267\u884c: docker compose up -d --build",
|
src/routes/web.ts
CHANGED
|
@@ -151,7 +151,7 @@ export function createWebRoutes(accountPool: AccountPool): Hono {
|
|
| 151 |
app.post("/admin/check-update", async (c) => {
|
| 152 |
const results: {
|
| 153 |
proxy?: { commits_behind: number; current_commit: string | null; latest_commit: string | null; update_applied?: boolean; error?: string };
|
| 154 |
-
codex?: { update_available: boolean; current_version: string; latest_version: string | null; error?: string };
|
| 155 |
} = {};
|
| 156 |
|
| 157 |
// 1. Proxy self-update check
|
|
@@ -183,11 +183,13 @@ export function createWebRoutes(accountPool: AccountPool): Hono {
|
|
| 183 |
// 2. Codex fingerprint check
|
| 184 |
if (!isEmbedded()) {
|
| 185 |
try {
|
|
|
|
| 186 |
const codexState = await checkForUpdate();
|
| 187 |
results.codex = {
|
| 188 |
update_available: codexState.update_available,
|
| 189 |
current_version: codexState.current_version,
|
| 190 |
latest_version: codexState.latest_version,
|
|
|
|
| 191 |
};
|
| 192 |
} catch (err) {
|
| 193 |
results.codex = {
|
|
|
|
| 151 |
app.post("/admin/check-update", async (c) => {
|
| 152 |
const results: {
|
| 153 |
proxy?: { commits_behind: number; current_commit: string | null; latest_commit: string | null; update_applied?: boolean; error?: string };
|
| 154 |
+
codex?: { update_available: boolean; current_version: string; latest_version: string | null; version_changed?: boolean; error?: string };
|
| 155 |
} = {};
|
| 156 |
|
| 157 |
// 1. Proxy self-update check
|
|
|
|
| 183 |
// 2. Codex fingerprint check
|
| 184 |
if (!isEmbedded()) {
|
| 185 |
try {
|
| 186 |
+
const prevVersion = getUpdateState()?.current_version ?? null;
|
| 187 |
const codexState = await checkForUpdate();
|
| 188 |
results.codex = {
|
| 189 |
update_available: codexState.update_available,
|
| 190 |
current_version: codexState.current_version,
|
| 191 |
latest_version: codexState.latest_version,
|
| 192 |
+
version_changed: prevVersion !== null && codexState.current_version !== prevVersion,
|
| 193 |
};
|
| 194 |
} catch (err) {
|
| 195 |
results.codex = {
|
web/src/App.tsx
CHANGED
|
@@ -40,7 +40,10 @@ function useUpdateMessage() {
|
|
| 40 |
parts.push(`Codex: ${r.codex.error}`);
|
| 41 |
color = "text-red-500";
|
| 42 |
} else if (r.codex_update_in_progress) {
|
| 43 |
-
parts.push(t("
|
|
|
|
|
|
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
msg = parts.length > 0 ? parts.join(" · ") : t("upToDate");
|
|
@@ -71,6 +74,7 @@ function Dashboard() {
|
|
| 71 |
checking={update.checking}
|
| 72 |
updateStatusMsg={update.msg}
|
| 73 |
updateStatusColor={update.color}
|
|
|
|
| 74 |
/>
|
| 75 |
<main class="flex-grow px-4 md:px-8 lg:px-40 py-8 flex justify-center">
|
| 76 |
<div class="flex flex-col w-full max-w-[960px] gap-6">
|
|
|
|
| 40 |
parts.push(`Codex: ${r.codex.error}`);
|
| 41 |
color = "text-red-500";
|
| 42 |
} else if (r.codex_update_in_progress) {
|
| 43 |
+
parts.push(t("fingerprintUpdating"));
|
| 44 |
+
} else if (r.codex?.version_changed) {
|
| 45 |
+
parts.push(`Codex: v${r.codex.current_version}`);
|
| 46 |
+
color = "text-blue-500";
|
| 47 |
}
|
| 48 |
|
| 49 |
msg = parts.length > 0 ? parts.join(" · ") : t("upToDate");
|
|
|
|
| 74 |
checking={update.checking}
|
| 75 |
updateStatusMsg={update.msg}
|
| 76 |
updateStatusColor={update.color}
|
| 77 |
+
version={update.status?.proxy.version ?? null}
|
| 78 |
/>
|
| 79 |
<main class="flex-grow px-4 md:px-8 lg:px-40 py-8 flex justify-center">
|
| 80 |
<div class="flex flex-col w-full max-w-[960px] gap-6">
|
web/src/components/AccountCard.tsx
CHANGED
|
@@ -143,7 +143,7 @@ export function AccountCard({ account, index, onDelete, proxies, onProxyChange }
|
|
| 143 |
onChange={(e) =>
|
| 144 |
onProxyChange(account.id, (e.target as HTMLSelectElement).value)
|
| 145 |
}
|
| 146 |
-
class="text-xs px-2 py-1 rounded-md border border-gray-200 dark:border-border-dark bg-
|
| 147 |
>
|
| 148 |
<option value="global">{t("globalDefault")}</option>
|
| 149 |
<option value="direct">{t("directNoProxy")}</option>
|
|
|
|
| 143 |
onChange={(e) =>
|
| 144 |
onProxyChange(account.id, (e.target as HTMLSelectElement).value)
|
| 145 |
}
|
| 146 |
+
class="text-xs px-2 py-1 rounded-md border border-gray-200 dark:border-border-dark bg-white dark:bg-bg-dark text-slate-700 dark:text-text-main focus:outline-none focus:ring-1 focus:ring-primary cursor-pointer"
|
| 147 |
>
|
| 148 |
<option value="global">{t("globalDefault")}</option>
|
| 149 |
<option value="direct">{t("directNoProxy")}</option>
|
web/src/components/Header.tsx
CHANGED
|
@@ -19,9 +19,10 @@ interface HeaderProps {
|
|
| 19 |
checking: boolean;
|
| 20 |
updateStatusMsg: string | null;
|
| 21 |
updateStatusColor: string;
|
|
|
|
| 22 |
}
|
| 23 |
|
| 24 |
-
export function Header({ onAddAccount, onCheckUpdate, checking, updateStatusMsg, updateStatusColor }: HeaderProps) {
|
| 25 |
const { lang, toggleLang, t } = useI18n();
|
| 26 |
const { isDark, toggle: toggleTheme } = useTheme();
|
| 27 |
|
|
@@ -49,6 +50,9 @@ export function Header({ onAddAccount, onCheckUpdate, checking, updateStatusMsg,
|
|
| 49 |
<span class="invisible col-start-1 row-start-1">Server Online</span>
|
| 50 |
<span class="col-start-1 row-start-1">{t("serverOnline")}</span>
|
| 51 |
</span>
|
|
|
|
|
|
|
|
|
|
| 52 |
</div>
|
| 53 |
{/* Star on GitHub */}
|
| 54 |
<a
|
|
@@ -77,9 +81,12 @@ export function Header({ onAddAccount, onCheckUpdate, checking, updateStatusMsg,
|
|
| 77 |
</button>
|
| 78 |
{/* Update status message */}
|
| 79 |
{updateStatusMsg && !checking && (
|
| 80 |
-
<
|
|
|
|
|
|
|
|
|
|
| 81 |
{updateStatusMsg}
|
| 82 |
-
</
|
| 83 |
)}
|
| 84 |
{/* Language Toggle */}
|
| 85 |
<button
|
|
|
|
| 19 |
checking: boolean;
|
| 20 |
updateStatusMsg: string | null;
|
| 21 |
updateStatusColor: string;
|
| 22 |
+
version: string | null;
|
| 23 |
}
|
| 24 |
|
| 25 |
+
export function Header({ onAddAccount, onCheckUpdate, checking, updateStatusMsg, updateStatusColor, version }: HeaderProps) {
|
| 26 |
const { lang, toggleLang, t } = useI18n();
|
| 27 |
const { isDark, toggle: toggleTheme } = useTheme();
|
| 28 |
|
|
|
|
| 50 |
<span class="invisible col-start-1 row-start-1">Server Online</span>
|
| 51 |
<span class="col-start-1 row-start-1">{t("serverOnline")}</span>
|
| 52 |
</span>
|
| 53 |
+
{version && (
|
| 54 |
+
<span class="text-[0.65rem] font-mono text-primary/70">v{version}</span>
|
| 55 |
+
)}
|
| 56 |
</div>
|
| 57 |
{/* Star on GitHub */}
|
| 58 |
<a
|
|
|
|
| 81 |
</button>
|
| 82 |
{/* Update status message */}
|
| 83 |
{updateStatusMsg && !checking && (
|
| 84 |
+
<button
|
| 85 |
+
onClick={onCheckUpdate}
|
| 86 |
+
class={`hidden sm:inline text-xs font-medium ${updateStatusColor} hover:underline`}
|
| 87 |
+
>
|
| 88 |
{updateStatusMsg}
|
| 89 |
+
</button>
|
| 90 |
)}
|
| 91 |
{/* Language Toggle */}
|
| 92 |
<button
|
web/src/components/ProxyPool.tsx
CHANGED
|
@@ -20,7 +20,7 @@ const statusStyles: Record<string, [string, TranslationKey]> = {
|
|
| 20 |
],
|
| 21 |
};
|
| 22 |
|
| 23 |
-
const inputCls = "px-3 py-2 text-sm border border-gray-200 dark:border-border-dark rounded-lg bg-
|
| 24 |
|
| 25 |
interface ProxyPoolProps {
|
| 26 |
proxies: ProxiesState;
|
|
|
|
| 20 |
],
|
| 21 |
};
|
| 22 |
|
| 23 |
+
const inputCls = "px-3 py-2 text-sm border border-gray-200 dark:border-border-dark rounded-lg bg-white dark:bg-bg-dark text-slate-700 dark:text-text-main focus:outline-none focus:ring-1 focus:ring-primary";
|
| 24 |
|
| 25 |
interface ProxyPoolProps {
|
| 26 |
proxies: ProxiesState;
|