Spaces:
Paused
Paused
icebear0828 Claude Opus 4.6 commited on
Commit ·
06a5304
1
Parent(s): c52388f
feat: GitHub Star badge + check-for-updates in dashboard header
Browse files- Header: ⭐ Star badge linking to GitHub repo (amber pill style)
- Header: 🔄 Check for Updates button with status display
- Checks both proxy self-update (git fetch/pull/build) and Codex fingerprint (appcast)
- CLI: auto git pull + npm install + build, prompt restart
- Docker: fingerprint auto-update, proxy shows manual rebuild hint
- Electron: version display only, managed by desktop app
- New endpoints: GET /admin/update-status, POST /admin/check-update
- Footer: version info (proxy commit + Codex Desktop version)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CHANGELOG.md +9 -0
- shared/hooks/use-update-status.ts +79 -0
- shared/i18n/translations.ts +26 -0
- src/routes/web.ts +87 -1
- src/self-update.ts +150 -0
- src/update-checker.ts +5 -0
- web/src/App.tsx +49 -2
- web/src/components/Footer.tsx +24 -4
- web/src/components/Header.tsx +36 -1
- web/vite.config.ts +1 -0
CHANGELOG.md
CHANGED
|
@@ -8,6 +8,15 @@
|
|
| 8 |
|
| 9 |
### Added
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
- GPT-5.4 + Codex Spark 模型支持:新增 `gpt-5.4`(4 种 effort: minimal/low/medium/high)和 `gpt-5.3-codex-spark`(minimal/low),`codex` 别名更新为 `gpt-5.4`
|
| 12 |
- 扩展推理等级:支持 `minimal`、`xhigh` 等新 effort 值,客户端发送的任意 `reasoning_effort` 均透传到后端
|
| 13 |
- 模型家族矩阵选择器:Dashboard 模型选择从平面下拉改为家族列表 + 推理等级按钮组,通过 `/v1/models/catalog` 端点获取完整目录
|
|
|
|
| 8 |
|
| 9 |
### Added
|
| 10 |
|
| 11 |
+
- Dashboard GitHub Star 徽章:Header 新增醒目的 ⭐ Star 按钮(amber 药丸样式),点击跳转 GitHub 仓库页面,方便用户收藏和获取更新
|
| 12 |
+
- Dashboard 检查更新功能:Footer 显示 Proxy 版本+commit 和 Codex Desktop 指纹版本,提供"检查更新"按钮同时检查两种更新
|
| 13 |
+
- Proxy 自更新(CLI 模式):通过 `git fetch` 检查新提交,自动执行 `git pull + npm install + npm run build`,完成后提示重启
|
| 14 |
+
- Codex 指纹更新:手动触发现有 appcast 检查,自动应用指纹/模型配置变更
|
| 15 |
+
- Docker 兼容:指纹可自动更新,代理代码提示手动 `docker compose up -d --build`
|
| 16 |
+
- Electron 兼容:显示版本信息,更新由桌面应用管理
|
| 17 |
+
- `GET /admin/update-status` 端点:返回 proxy 和 codex 两种更新的当前状态
|
| 18 |
+
- `POST /admin/check-update` 端点:同时触发 proxy 自检 + codex 指纹检查,自动应用可用更新
|
| 19 |
+
- `src/self-update.ts`:Proxy 自更新模块(git 子进程实现,支持检查/拉取/构建)
|
| 20 |
- GPT-5.4 + Codex Spark 模型支持:新增 `gpt-5.4`(4 种 effort: minimal/low/medium/high)和 `gpt-5.3-codex-spark`(minimal/low),`codex` 别名更新为 `gpt-5.4`
|
| 21 |
- 扩展推理等级:支持 `minimal`、`xhigh` 等新 effort 值,客户端发送的任意 `reasoning_effort` 均透传到后端
|
| 22 |
- 模型家族矩阵选择器:Dashboard 模型选择从平面下拉改为家族列表 + 推理等级按钮组,通过 `/v1/models/catalog` 端点获取完整目录
|
shared/hooks/use-update-status.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect, useCallback } from "preact/hooks";
|
| 2 |
+
|
| 3 |
+
export interface UpdateStatus {
|
| 4 |
+
proxy: {
|
| 5 |
+
version: string;
|
| 6 |
+
commit: string | null;
|
| 7 |
+
can_self_update: boolean;
|
| 8 |
+
commits_behind: number | null;
|
| 9 |
+
update_in_progress: boolean;
|
| 10 |
+
};
|
| 11 |
+
codex: {
|
| 12 |
+
current_version: string | null;
|
| 13 |
+
current_build: string | null;
|
| 14 |
+
latest_version: string | null;
|
| 15 |
+
latest_build: string | null;
|
| 16 |
+
update_available: boolean;
|
| 17 |
+
update_in_progress: boolean;
|
| 18 |
+
last_check: string | null;
|
| 19 |
+
};
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export interface CheckResult {
|
| 23 |
+
proxy?: {
|
| 24 |
+
commits_behind: number;
|
| 25 |
+
current_commit: string | null;
|
| 26 |
+
latest_commit: string | null;
|
| 27 |
+
update_applied?: boolean;
|
| 28 |
+
error?: string;
|
| 29 |
+
};
|
| 30 |
+
codex?: {
|
| 31 |
+
update_available: boolean;
|
| 32 |
+
current_version: string;
|
| 33 |
+
latest_version: string | null;
|
| 34 |
+
error?: string;
|
| 35 |
+
};
|
| 36 |
+
proxy_update_in_progress: boolean;
|
| 37 |
+
codex_update_in_progress: boolean;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export function useUpdateStatus() {
|
| 41 |
+
const [status, setStatus] = useState<UpdateStatus | null>(null);
|
| 42 |
+
const [checking, setChecking] = useState(false);
|
| 43 |
+
const [result, setResult] = useState<CheckResult | null>(null);
|
| 44 |
+
const [error, setError] = useState<string | null>(null);
|
| 45 |
+
|
| 46 |
+
const load = useCallback(async () => {
|
| 47 |
+
try {
|
| 48 |
+
const resp = await fetch("/admin/update-status");
|
| 49 |
+
if (resp.ok) {
|
| 50 |
+
setStatus(await resp.json());
|
| 51 |
+
}
|
| 52 |
+
} catch { /* ignore */ }
|
| 53 |
+
}, []);
|
| 54 |
+
|
| 55 |
+
useEffect(() => { load(); }, [load]);
|
| 56 |
+
|
| 57 |
+
const checkForUpdate = useCallback(async () => {
|
| 58 |
+
setChecking(true);
|
| 59 |
+
setError(null);
|
| 60 |
+
setResult(null);
|
| 61 |
+
try {
|
| 62 |
+
const resp = await fetch("/admin/check-update", { method: "POST" });
|
| 63 |
+
const data: CheckResult = await resp.json();
|
| 64 |
+
if (!resp.ok) {
|
| 65 |
+
setError("Check failed");
|
| 66 |
+
} else {
|
| 67 |
+
setResult(data);
|
| 68 |
+
// Refresh status after check
|
| 69 |
+
await load();
|
| 70 |
+
}
|
| 71 |
+
} catch (err) {
|
| 72 |
+
setError(err instanceof Error ? err.message : "Network error");
|
| 73 |
+
} finally {
|
| 74 |
+
setChecking(false);
|
| 75 |
+
}
|
| 76 |
+
}, [load]);
|
| 77 |
+
|
| 78 |
+
return { status, checking, result, error, checkForUpdate };
|
| 79 |
+
}
|
shared/i18n/translations.ts
CHANGED
|
@@ -59,6 +59,19 @@ export const translations = {
|
|
| 59 |
refresh: "Refresh",
|
| 60 |
updatedAt: "Updated at",
|
| 61 |
footer: "\u00a9 2025 Codex Proxy. All rights reserved.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
},
|
| 63 |
zh: {
|
| 64 |
serverOnline: "\u670d\u52a1\u8fd0\u884c\u4e2d",
|
|
@@ -123,6 +136,19 @@ export const translations = {
|
|
| 123 |
updatedAt: "\u66f4\u65b0\u4e8e",
|
| 124 |
footer:
|
| 125 |
"\u00a9 2025 Codex Proxy\u3002\u4fdd\u7559\u6240\u6709\u6743\u5229\u3002",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
},
|
| 127 |
} as const;
|
| 128 |
|
|
|
|
| 59 |
refresh: "Refresh",
|
| 60 |
updatedAt: "Updated at",
|
| 61 |
footer: "\u00a9 2025 Codex Proxy. All rights reserved.",
|
| 62 |
+
starOnGithub: "Star",
|
| 63 |
+
checkForUpdates: "Check for Updates",
|
| 64 |
+
checkingUpdates: "Checking...",
|
| 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",
|
| 71 |
+
managedByDesktop: "Managed by desktop app",
|
| 72 |
+
lastChecked: "Last checked",
|
| 73 |
+
never: "never",
|
| 74 |
+
commits: "commits",
|
| 75 |
},
|
| 76 |
zh: {
|
| 77 |
serverOnline: "\u670d\u52a1\u8fd0\u884c\u4e2d",
|
|
|
|
| 136 |
updatedAt: "\u66f4\u65b0\u4e8e",
|
| 137 |
footer:
|
| 138 |
"\u00a9 2025 Codex Proxy\u3002\u4fdd\u7559\u6240\u6709\u6743\u5229\u3002",
|
| 139 |
+
starOnGithub: "\u6536\u85cf",
|
| 140 |
+
checkForUpdates: "\u68c0\u67e5\u66f4\u65b0",
|
| 141 |
+
checkingUpdates: "\u68c0\u67e5\u4e2d...",
|
| 142 |
+
upToDate: "\u5df2\u662f\u6700\u65b0",
|
| 143 |
+
updateApplied: "\u66f4\u65b0\u5b8c\u6210\uff0c\u8bf7\u91cd\u542f\u670d\u52a1",
|
| 144 |
+
fingerprintUpdated: "\u6307\u7eb9\u5df2\u66f4\u65b0",
|
| 145 |
+
proxyUpdating: "\u6b63\u5728\u66f4\u65b0\u4ee3\u7406...",
|
| 146 |
+
proxyBehind: "\u843d\u540e",
|
| 147 |
+
dockerUpdateHint: "\u8bf7\u6267\u884c: docker compose up -d --build",
|
| 148 |
+
managedByDesktop: "\u7531\u684c\u9762\u5e94\u7528\u7ba1\u7406",
|
| 149 |
+
lastChecked: "\u4e0a\u6b21\u68c0\u67e5",
|
| 150 |
+
never: "\u4ece\u672a",
|
| 151 |
+
commits: "\u4e2a\u63d0\u4ea4",
|
| 152 |
},
|
| 153 |
} as const;
|
| 154 |
|
src/routes/web.ts
CHANGED
|
@@ -4,7 +4,9 @@ import { readFileSync, existsSync } from "fs";
|
|
| 4 |
import { resolve } from "path";
|
| 5 |
import type { AccountPool } from "../auth/account-pool.js";
|
| 6 |
import { getConfig, getFingerprint } from "../config.js";
|
| 7 |
-
import { getPublicDir, getDesktopPublicDir, getConfigDir, getDataDir } from "../paths.js";
|
|
|
|
|
|
|
| 8 |
|
| 9 |
export function createWebRoutes(accountPool: AccountPool): Hono {
|
| 10 |
const app = new Hono();
|
|
@@ -120,5 +122,89 @@ export function createWebRoutes(accountPool: AccountPool): Hono {
|
|
| 120 |
});
|
| 121 |
});
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
return app;
|
| 124 |
}
|
|
|
|
| 4 |
import { resolve } from "path";
|
| 5 |
import type { AccountPool } from "../auth/account-pool.js";
|
| 6 |
import { getConfig, getFingerprint } from "../config.js";
|
| 7 |
+
import { getPublicDir, getDesktopPublicDir, getConfigDir, getDataDir, isEmbedded } from "../paths.js";
|
| 8 |
+
import { getUpdateState, checkForUpdate, isUpdateInProgress } from "../update-checker.js";
|
| 9 |
+
import { getProxyInfo, canSelfUpdate, checkProxySelfUpdate, applyProxySelfUpdate, isProxyUpdateInProgress } from "../self-update.js";
|
| 10 |
|
| 11 |
export function createWebRoutes(accountPool: AccountPool): Hono {
|
| 12 |
const app = new Hono();
|
|
|
|
| 122 |
});
|
| 123 |
});
|
| 124 |
|
| 125 |
+
// --- Update management endpoints ---
|
| 126 |
+
|
| 127 |
+
app.get("/admin/update-status", (c) => {
|
| 128 |
+
const proxyInfo = getProxyInfo();
|
| 129 |
+
const codexState = getUpdateState();
|
| 130 |
+
|
| 131 |
+
return c.json({
|
| 132 |
+
proxy: {
|
| 133 |
+
version: proxyInfo.version,
|
| 134 |
+
commit: proxyInfo.commit,
|
| 135 |
+
can_self_update: canSelfUpdate(),
|
| 136 |
+
commits_behind: null as number | null,
|
| 137 |
+
update_in_progress: isProxyUpdateInProgress(),
|
| 138 |
+
},
|
| 139 |
+
codex: {
|
| 140 |
+
current_version: codexState?.current_version ?? null,
|
| 141 |
+
current_build: codexState?.current_build ?? null,
|
| 142 |
+
latest_version: codexState?.latest_version ?? null,
|
| 143 |
+
latest_build: codexState?.latest_build ?? null,
|
| 144 |
+
update_available: codexState?.update_available ?? false,
|
| 145 |
+
update_in_progress: isUpdateInProgress(),
|
| 146 |
+
last_check: codexState?.last_check ?? null,
|
| 147 |
+
},
|
| 148 |
+
});
|
| 149 |
+
});
|
| 150 |
+
|
| 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
|
| 158 |
+
if (canSelfUpdate()) {
|
| 159 |
+
try {
|
| 160 |
+
const proxyResult = await checkProxySelfUpdate();
|
| 161 |
+
results.proxy = {
|
| 162 |
+
commits_behind: proxyResult.commitsBehind,
|
| 163 |
+
current_commit: proxyResult.currentCommit,
|
| 164 |
+
latest_commit: proxyResult.latestCommit,
|
| 165 |
+
};
|
| 166 |
+
|
| 167 |
+
// Auto-apply if behind
|
| 168 |
+
if (proxyResult.commitsBehind > 0 && !isProxyUpdateInProgress()) {
|
| 169 |
+
const applyResult = await applyProxySelfUpdate();
|
| 170 |
+
results.proxy.update_applied = applyResult.started;
|
| 171 |
+
if (applyResult.error) results.proxy.error = applyResult.error;
|
| 172 |
+
}
|
| 173 |
+
} catch (err) {
|
| 174 |
+
results.proxy = {
|
| 175 |
+
commits_behind: 0,
|
| 176 |
+
current_commit: null,
|
| 177 |
+
latest_commit: null,
|
| 178 |
+
error: err instanceof Error ? err.message : String(err),
|
| 179 |
+
};
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
|
| 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 = {
|
| 194 |
+
update_available: false,
|
| 195 |
+
current_version: "unknown",
|
| 196 |
+
latest_version: null,
|
| 197 |
+
error: err instanceof Error ? err.message : String(err),
|
| 198 |
+
};
|
| 199 |
+
}
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
return c.json({
|
| 203 |
+
...results,
|
| 204 |
+
proxy_update_in_progress: isProxyUpdateInProgress(),
|
| 205 |
+
codex_update_in_progress: isUpdateInProgress(),
|
| 206 |
+
});
|
| 207 |
+
});
|
| 208 |
+
|
| 209 |
return app;
|
| 210 |
}
|
src/self-update.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Proxy self-update — checks for new commits on GitHub and applies via git pull.
|
| 3 |
+
* Only works in CLI mode (where .git exists). Docker/Electron show manual instructions.
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import { execFile, execFileSync } from "child_process";
|
| 7 |
+
import { existsSync, readFileSync } from "fs";
|
| 8 |
+
import { resolve } from "path";
|
| 9 |
+
import { promisify } from "util";
|
| 10 |
+
import { isEmbedded } from "./paths.js";
|
| 11 |
+
|
| 12 |
+
const execFileAsync = promisify(execFile);
|
| 13 |
+
|
| 14 |
+
export interface ProxyInfo {
|
| 15 |
+
version: string;
|
| 16 |
+
commit: string | null;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export interface ProxySelfUpdateResult {
|
| 20 |
+
commitsBehind: number;
|
| 21 |
+
currentCommit: string | null;
|
| 22 |
+
latestCommit: string | null;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
let _proxyUpdateInProgress = false;
|
| 26 |
+
let _gitAvailable: boolean | null = null;
|
| 27 |
+
|
| 28 |
+
/** Read proxy version from package.json + current git commit hash. */
|
| 29 |
+
export function getProxyInfo(): ProxyInfo {
|
| 30 |
+
let version = "unknown";
|
| 31 |
+
try {
|
| 32 |
+
const pkg = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf-8"));
|
| 33 |
+
version = pkg.version ?? "unknown";
|
| 34 |
+
} catch { /* ignore */ }
|
| 35 |
+
|
| 36 |
+
let commit: string | null = null;
|
| 37 |
+
if (canSelfUpdate()) {
|
| 38 |
+
try {
|
| 39 |
+
const out = execFileSync("git", ["rev-parse", "--short", "HEAD"], {
|
| 40 |
+
cwd: process.cwd(),
|
| 41 |
+
encoding: "utf-8",
|
| 42 |
+
timeout: 5000,
|
| 43 |
+
});
|
| 44 |
+
commit = out.trim() || null;
|
| 45 |
+
} catch { /* ignore */ }
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
return { version, commit };
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/** Whether this environment supports git-based self-update. */
|
| 52 |
+
export function canSelfUpdate(): boolean {
|
| 53 |
+
if (isEmbedded()) return false;
|
| 54 |
+
if (_gitAvailable !== null) return _gitAvailable;
|
| 55 |
+
|
| 56 |
+
// Check .git directory exists
|
| 57 |
+
if (!existsSync(resolve(process.cwd(), ".git"))) {
|
| 58 |
+
_gitAvailable = false;
|
| 59 |
+
return false;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
// Check git command is available
|
| 63 |
+
try {
|
| 64 |
+
execFileSync("git", ["--version"], {
|
| 65 |
+
cwd: process.cwd(),
|
| 66 |
+
timeout: 5000,
|
| 67 |
+
stdio: "ignore",
|
| 68 |
+
});
|
| 69 |
+
_gitAvailable = true;
|
| 70 |
+
} catch {
|
| 71 |
+
_gitAvailable = false;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
return _gitAvailable;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/** Whether a proxy self-update is currently in progress. */
|
| 78 |
+
export function isProxyUpdateInProgress(): boolean {
|
| 79 |
+
return _proxyUpdateInProgress;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/** Fetch latest from origin and check how many commits behind. */
|
| 83 |
+
export async function checkProxySelfUpdate(): Promise<ProxySelfUpdateResult> {
|
| 84 |
+
const cwd = process.cwd();
|
| 85 |
+
|
| 86 |
+
// Get current commit
|
| 87 |
+
let currentCommit: string | null = null;
|
| 88 |
+
try {
|
| 89 |
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--short", "HEAD"], { cwd, timeout: 5000 });
|
| 90 |
+
currentCommit = stdout.trim() || null;
|
| 91 |
+
} catch { /* ignore */ }
|
| 92 |
+
|
| 93 |
+
// Fetch latest
|
| 94 |
+
try {
|
| 95 |
+
await execFileAsync("git", ["fetch", "origin", "master", "--quiet"], { cwd, timeout: 30000 });
|
| 96 |
+
} catch (err) {
|
| 97 |
+
console.warn("[SelfUpdate] git fetch failed:", err instanceof Error ? err.message : err);
|
| 98 |
+
return { commitsBehind: 0, currentCommit, latestCommit: currentCommit };
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// Count commits behind
|
| 102 |
+
let commitsBehind = 0;
|
| 103 |
+
let latestCommit: string | null = null;
|
| 104 |
+
try {
|
| 105 |
+
const { stdout: countOut } = await execFileAsync(
|
| 106 |
+
"git", ["rev-list", "HEAD..origin/master", "--count"], { cwd, timeout: 5000 },
|
| 107 |
+
);
|
| 108 |
+
commitsBehind = parseInt(countOut.trim(), 10) || 0;
|
| 109 |
+
|
| 110 |
+
const { stdout: latestOut } = await execFileAsync(
|
| 111 |
+
"git", ["rev-parse", "--short", "origin/master"], { cwd, timeout: 5000 },
|
| 112 |
+
);
|
| 113 |
+
latestCommit = latestOut.trim() || null;
|
| 114 |
+
} catch { /* ignore */ }
|
| 115 |
+
|
| 116 |
+
return { commitsBehind, currentCommit, latestCommit };
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
/**
|
| 120 |
+
* Apply proxy self-update: git pull + npm install + npm run build.
|
| 121 |
+
* Runs in background. Returns immediately; check isProxyUpdateInProgress() for status.
|
| 122 |
+
*/
|
| 123 |
+
export async function applyProxySelfUpdate(): Promise<{ started: boolean; error?: string }> {
|
| 124 |
+
if (_proxyUpdateInProgress) {
|
| 125 |
+
return { started: false, error: "Update already in progress" };
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
_proxyUpdateInProgress = true;
|
| 129 |
+
const cwd = process.cwd();
|
| 130 |
+
|
| 131 |
+
try {
|
| 132 |
+
console.log("[SelfUpdate] Pulling latest code...");
|
| 133 |
+
await execFileAsync("git", ["pull", "origin", "master"], { cwd, timeout: 60000 });
|
| 134 |
+
|
| 135 |
+
console.log("[SelfUpdate] Installing dependencies...");
|
| 136 |
+
await execFileAsync("npm", ["install"], { cwd, timeout: 120000, shell: true });
|
| 137 |
+
|
| 138 |
+
console.log("[SelfUpdate] Building...");
|
| 139 |
+
await execFileAsync("npm", ["run", "build"], { cwd, timeout: 120000, shell: true });
|
| 140 |
+
|
| 141 |
+
console.log("[SelfUpdate] Update complete. Server restart required.");
|
| 142 |
+
_proxyUpdateInProgress = false;
|
| 143 |
+
return { started: true };
|
| 144 |
+
} catch (err) {
|
| 145 |
+
_proxyUpdateInProgress = false;
|
| 146 |
+
const msg = err instanceof Error ? err.message : String(err);
|
| 147 |
+
console.error("[SelfUpdate] Update failed:", msg);
|
| 148 |
+
return { started: false, error: msg };
|
| 149 |
+
}
|
| 150 |
+
}
|
src/update-checker.ts
CHANGED
|
@@ -206,6 +206,11 @@ export function getUpdateState(): UpdateState | null {
|
|
| 206 |
return _currentState;
|
| 207 |
}
|
| 208 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
function scheduleNextPoll(): void {
|
| 210 |
_pollTimer = setTimeout(() => {
|
| 211 |
checkForUpdate().catch((err) => {
|
|
|
|
| 206 |
return _currentState;
|
| 207 |
}
|
| 208 |
|
| 209 |
+
/** Whether a full-update pipeline is currently running. */
|
| 210 |
+
export function isUpdateInProgress(): boolean {
|
| 211 |
+
return _updateInProgress;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
function scheduleNextPoll(): void {
|
| 215 |
_pollTimer = setTimeout(() => {
|
| 216 |
checkForUpdate().catch((err) => {
|
web/src/App.tsx
CHANGED
|
@@ -9,14 +9,61 @@ import { CodeExamples } from "./components/CodeExamples";
|
|
| 9 |
import { Footer } from "./components/Footer";
|
| 10 |
import { useAccounts } from "../../shared/hooks/use-accounts";
|
| 11 |
import { useStatus } from "../../shared/hooks/use-status";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
function Dashboard() {
|
| 14 |
const accounts = useAccounts();
|
| 15 |
const status = useStatus(accounts.list.length);
|
|
|
|
| 16 |
|
| 17 |
return (
|
| 18 |
<>
|
| 19 |
-
<Header
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
<main class="flex-grow px-4 md:px-8 lg:px-40 py-8 flex justify-center">
|
| 21 |
<div class="flex flex-col w-full max-w-[960px] gap-6">
|
| 22 |
<AddAccount
|
|
@@ -55,7 +102,7 @@ function Dashboard() {
|
|
| 55 |
/>
|
| 56 |
</div>
|
| 57 |
</main>
|
| 58 |
-
<Footer />
|
| 59 |
</>
|
| 60 |
);
|
| 61 |
}
|
|
|
|
| 9 |
import { Footer } from "./components/Footer";
|
| 10 |
import { useAccounts } from "../../shared/hooks/use-accounts";
|
| 11 |
import { useStatus } from "../../shared/hooks/use-status";
|
| 12 |
+
import { useUpdateStatus } from "../../shared/hooks/use-update-status";
|
| 13 |
+
import { useI18n } from "../../shared/i18n/context";
|
| 14 |
+
|
| 15 |
+
function useUpdateMessage() {
|
| 16 |
+
const { t } = useI18n();
|
| 17 |
+
const update = useUpdateStatus();
|
| 18 |
+
|
| 19 |
+
let msg: string | null = null;
|
| 20 |
+
let color = "text-primary";
|
| 21 |
+
|
| 22 |
+
if (!update.checking && update.result) {
|
| 23 |
+
const parts: string[] = [];
|
| 24 |
+
const r = update.result;
|
| 25 |
+
|
| 26 |
+
if (r.proxy?.error) {
|
| 27 |
+
parts.push(`Proxy: ${r.proxy.error}`);
|
| 28 |
+
color = "text-red-500";
|
| 29 |
+
} else if (r.proxy?.update_applied) {
|
| 30 |
+
parts.push(t("updateApplied"));
|
| 31 |
+
color = "text-blue-500";
|
| 32 |
+
} else if (r.proxy && r.proxy.commits_behind > 0) {
|
| 33 |
+
parts.push(`Proxy: ${r.proxy.commits_behind} ${t("commits")} ${t("proxyBehind")}`);
|
| 34 |
+
color = "text-amber-500";
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
if (r.codex?.error) {
|
| 38 |
+
parts.push(`Codex: ${r.codex.error}`);
|
| 39 |
+
color = "text-red-500";
|
| 40 |
+
} else if (r.codex_update_in_progress) {
|
| 41 |
+
parts.push(t("fingerprintUpdated"));
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
msg = parts.length > 0 ? parts.join(" · ") : t("upToDate");
|
| 45 |
+
} else if (!update.checking && update.error) {
|
| 46 |
+
msg = update.error;
|
| 47 |
+
color = "text-red-500";
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
return { ...update, msg, color };
|
| 51 |
+
}
|
| 52 |
|
| 53 |
function Dashboard() {
|
| 54 |
const accounts = useAccounts();
|
| 55 |
const status = useStatus(accounts.list.length);
|
| 56 |
+
const update = useUpdateMessage();
|
| 57 |
|
| 58 |
return (
|
| 59 |
<>
|
| 60 |
+
<Header
|
| 61 |
+
onAddAccount={accounts.startAdd}
|
| 62 |
+
onCheckUpdate={update.checkForUpdate}
|
| 63 |
+
checking={update.checking}
|
| 64 |
+
updateStatusMsg={update.msg}
|
| 65 |
+
updateStatusColor={update.color}
|
| 66 |
+
/>
|
| 67 |
<main class="flex-grow px-4 md:px-8 lg:px-40 py-8 flex justify-center">
|
| 68 |
<div class="flex flex-col w-full max-w-[960px] gap-6">
|
| 69 |
<AddAccount
|
|
|
|
| 102 |
/>
|
| 103 |
</div>
|
| 104 |
</main>
|
| 105 |
+
<Footer updateStatus={update.status} />
|
| 106 |
</>
|
| 107 |
);
|
| 108 |
}
|
web/src/components/Footer.tsx
CHANGED
|
@@ -1,12 +1,32 @@
|
|
| 1 |
import { useT } from "../../../shared/i18n/context";
|
|
|
|
| 2 |
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
const t = useT();
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
return (
|
| 7 |
-
<footer class="mt-auto border-t border-gray-200 dark:border-border-dark bg-white dark:bg-card-dark py-
|
| 8 |
-
<div class="container mx-auto px-4
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
</div>
|
| 11 |
</footer>
|
| 12 |
);
|
|
|
|
| 1 |
import { useT } from "../../../shared/i18n/context";
|
| 2 |
+
import type { UpdateStatus } from "../../../shared/hooks/use-update-status";
|
| 3 |
|
| 4 |
+
interface FooterProps {
|
| 5 |
+
updateStatus: UpdateStatus | null;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export function Footer({ updateStatus }: FooterProps) {
|
| 9 |
const t = useT();
|
| 10 |
|
| 11 |
+
const proxyVersion = updateStatus?.proxy.version ?? "...";
|
| 12 |
+
const proxyCommit = updateStatus?.proxy.commit;
|
| 13 |
+
const codexVersion = updateStatus?.codex.current_version;
|
| 14 |
+
|
| 15 |
return (
|
| 16 |
+
<footer class="mt-auto border-t border-gray-200 dark:border-border-dark bg-white dark:bg-card-dark py-5 transition-colors">
|
| 17 |
+
<div class="container mx-auto px-4 flex flex-col items-center gap-2">
|
| 18 |
+
{/* Version info */}
|
| 19 |
+
<div class="flex flex-wrap items-center justify-center gap-x-3 gap-y-1 text-[0.75rem] text-slate-400 dark:text-text-dim font-mono">
|
| 20 |
+
<span>Proxy v{proxyVersion}{proxyCommit ? ` (${proxyCommit})` : ""}</span>
|
| 21 |
+
{codexVersion && (
|
| 22 |
+
<>
|
| 23 |
+
<span class="text-slate-300 dark:text-border-dark">·</span>
|
| 24 |
+
<span>Codex Desktop v{codexVersion}</span>
|
| 25 |
+
</>
|
| 26 |
+
)}
|
| 27 |
+
</div>
|
| 28 |
+
{/* Copyright */}
|
| 29 |
+
<p class="text-[0.75rem] text-slate-400 dark:text-text-dim">{t("footer")}</p>
|
| 30 |
</div>
|
| 31 |
</footer>
|
| 32 |
);
|
web/src/components/Header.tsx
CHANGED
|
@@ -15,9 +15,13 @@ const SVG_SUN = (
|
|
| 15 |
|
| 16 |
interface HeaderProps {
|
| 17 |
onAddAccount: () => void;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
}
|
| 19 |
|
| 20 |
-
export function Header({ onAddAccount }: HeaderProps) {
|
| 21 |
const { lang, toggleLang, t } = useI18n();
|
| 22 |
const { isDark, toggle: toggleTheme } = useTheme();
|
| 23 |
|
|
@@ -46,6 +50,37 @@ export function Header({ onAddAccount }: HeaderProps) {
|
|
| 46 |
<span class="col-start-1 row-start-1">{t("serverOnline")}</span>
|
| 47 |
</span>
|
| 48 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
{/* Language Toggle */}
|
| 50 |
<button
|
| 51 |
onClick={toggleLang}
|
|
|
|
| 15 |
|
| 16 |
interface HeaderProps {
|
| 17 |
onAddAccount: () => void;
|
| 18 |
+
onCheckUpdate: () => void;
|
| 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 |
|
|
|
|
| 50 |
<span class="col-start-1 row-start-1">{t("serverOnline")}</span>
|
| 51 |
</span>
|
| 52 |
</div>
|
| 53 |
+
{/* Star on GitHub */}
|
| 54 |
+
<a
|
| 55 |
+
href="https://github.com/icebear0828/codex-proxy"
|
| 56 |
+
target="_blank"
|
| 57 |
+
rel="noopener noreferrer"
|
| 58 |
+
class="hidden sm:flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-amber-50 border border-amber-200 text-amber-700 dark:bg-amber-900/20 dark:border-amber-700/30 dark:text-amber-400 hover:bg-amber-100 dark:hover:bg-amber-900/40 transition-colors"
|
| 59 |
+
>
|
| 60 |
+
<svg class="size-3.5" viewBox="0 0 24 24" fill="currentColor">
|
| 61 |
+
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
| 62 |
+
</svg>
|
| 63 |
+
<span class="text-xs font-semibold">{t("starOnGithub")}</span>
|
| 64 |
+
</a>
|
| 65 |
+
{/* Check for Updates */}
|
| 66 |
+
<button
|
| 67 |
+
onClick={onCheckUpdate}
|
| 68 |
+
disabled={checking}
|
| 69 |
+
class="hidden sm:flex items-center gap-1.5 px-3 py-1.5 rounded-full border border-gray-200 dark:border-border-dark text-slate-600 dark:text-text-dim hover:bg-slate-50 dark:hover:bg-border-dark disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
| 70 |
+
>
|
| 71 |
+
<svg class={`size-3.5 ${checking ? "animate-spin" : ""}`} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 72 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.992 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182M20.985 4.356v4.992" />
|
| 73 |
+
</svg>
|
| 74 |
+
<span class="text-xs font-semibold">
|
| 75 |
+
{checking ? t("checkingUpdates") : t("checkForUpdates")}
|
| 76 |
+
</span>
|
| 77 |
+
</button>
|
| 78 |
+
{/* Update status message */}
|
| 79 |
+
{updateStatusMsg && !checking && (
|
| 80 |
+
<span class={`hidden sm:inline text-xs font-medium ${updateStatusColor}`}>
|
| 81 |
+
{updateStatusMsg}
|
| 82 |
+
</span>
|
| 83 |
+
)}
|
| 84 |
{/* Language Toggle */}
|
| 85 |
<button
|
| 86 |
onClick={toggleLang}
|
web/vite.config.ts
CHANGED
|
@@ -22,6 +22,7 @@ export default defineConfig({
|
|
| 22 |
"/auth": "http://localhost:8080",
|
| 23 |
"/health": "http://localhost:8080",
|
| 24 |
"/debug": "http://localhost:8080",
|
|
|
|
| 25 |
},
|
| 26 |
},
|
| 27 |
});
|
|
|
|
| 22 |
"/auth": "http://localhost:8080",
|
| 23 |
"/health": "http://localhost:8080",
|
| 24 |
"/debug": "http://localhost:8080",
|
| 25 |
+
"/admin": "http://localhost:8080",
|
| 26 |
},
|
| 27 |
},
|
| 28 |
});
|