From bc08c4ab48416cb793a83221686507c62db74eee Mon Sep 17 00:00:00 2001 From: musistudio Date: Wed, 13 Aug 2025 21:32:57 +0800 Subject: [PATCH] add update button --- README.md | 3 + README_zh.md | 2 + src/middleware/auth.ts | 2 +- src/server.ts | 46 ++++++-- src/utils/index.ts | 3 + src/utils/update.ts | 80 ++++++++++++++ ui/src/App.tsx | 244 ++++++++++++++++++++++++++++++++--------- ui/src/lib/api.ts | 10 ++ ui/src/locales/en.json | 11 +- ui/src/locales/zh.json | 11 +- 10 files changed, 353 insertions(+), 59 deletions(-) create mode 100644 src/utils/update.ts diff --git a/README.md b/README.md index b1d9fc2..dea0a77 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Claude Code Router +I am seeking funding support for this project to better sustain its development. If you have any ideas, feel free to reach out to me: [m@musiiot.top](mailto:m@musiiot.top) + + [中文版](README_zh.md) > A powerful tool to route Claude Code requests to different models and customize any request. diff --git a/README_zh.md b/README_zh.md index 734ad63..a5e010b 100644 --- a/README_zh.md +++ b/README_zh.md @@ -1,5 +1,7 @@ # Claude Code Router +我正在为该项目寻求资金支持,以更好地维持其发展。如果您有任何想法,请随时与我联系: [m@musiiot.top](mailto:m@musiiot.top) + > 一款强大的工具,可将 Claude Code 请求路由到不同的模型,并自定义任何请求。 ![](blog/images/claude-code.png) diff --git a/src/middleware/auth.ts b/src/middleware/auth.ts index 8e9de55..6398457 100644 --- a/src/middleware/auth.ts +++ b/src/middleware/auth.ts @@ -15,7 +15,7 @@ export const apiKeyAuth = `http://127.0.0.1:${config.PORT || 3456}`, `http://localhost:${config.PORT || 3456}`, ]; - if (req.headers.origin && allowedOrigins.includes(req.headers.origin)) { + if (req.headers.origin && !allowedOrigins.includes(req.headers.origin)) { reply.status(403).send("CORS not allowed for this origin"); return; } else { diff --git a/src/server.ts b/src/server.ts index 989bc6c..dad7bba 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,5 +1,6 @@ import Server from "@musistudio/llms"; import { readConfigFile, writeConfigFile, backupConfigFile } from "./utils"; +import { checkForUpdates, performUpdate } from "./utils"; import { join } from "path"; import fastifyStatic from "@fastify/static"; @@ -39,13 +40,6 @@ export const createServer = (config: any): Server => { // Add endpoint to restart the service with access control server.app.post("/api/restart", async (req, reply) => { - // Only allow full access users to restart service - const accessLevel = (req as any).accessLevel || "restricted"; - if (accessLevel !== "full") { - reply.status(403).send("Full access required to restart service"); - return; - } - reply.send({ success: true, message: "Service restart initiated" }); // Restart the service after a short delay to allow response to be sent @@ -69,6 +63,44 @@ export const createServer = (config: any): Server => { server.app.get("/ui", async (_, reply) => { return reply.redirect("/ui/"); }); + + // 版本检查端点 + server.app.get("/api/update/check", async (req, reply) => { + try { + // 获取当前版本 + const currentVersion = require("../package.json").version; + const { hasUpdate, latestVersion, changelog } = await checkForUpdates(currentVersion); + + return { + hasUpdate, + latestVersion: hasUpdate ? latestVersion : undefined, + changelog: hasUpdate ? changelog : undefined + }; + } catch (error) { + console.error("Failed to check for updates:", error); + reply.status(500).send({ error: "Failed to check for updates" }); + } + }); + + // 执行更新端点 + server.app.post("/api/update/perform", async (req, reply) => { + try { + // 只允许完全访问权限的用户执行更新 + const accessLevel = (req as any).accessLevel || "restricted"; + if (accessLevel !== "full") { + reply.status(403).send("Full access required to perform updates"); + return; + } + + // 执行更新逻辑 + const result = await performUpdate(); + + return result; + } catch (error) { + console.error("Failed to perform update:", error); + reply.status(500).send({ error: "Failed to perform update" }); + } + }); return server; }; diff --git a/src/utils/index.ts b/src/utils/index.ts index b7af694..9c775b0 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -162,3 +162,6 @@ export const initConfig = async () => { // 导出日志清理函数 export { cleanupLogFiles }; + +// 导出更新功能 +export { checkForUpdates, performUpdate } from "./update"; diff --git a/src/utils/update.ts b/src/utils/update.ts new file mode 100644 index 0000000..bcafeda --- /dev/null +++ b/src/utils/update.ts @@ -0,0 +1,80 @@ +import { exec } from "child_process"; +import { promisify } from "util"; +import { join } from "path"; +import { readFileSync } from "fs"; + +const execPromise = promisify(exec); + +/** + * 检查是否有新版本可用 + * @param currentVersion 当前版本 + * @returns 包含更新信息的对象 + */ +export async function checkForUpdates(currentVersion: string) { + try { + // 从npm registry获取最新版本信息 + const { stdout } = await execPromise("npm view @musistudio/claude-code-router version"); + const latestVersion = stdout.trim(); + + // 比较版本 + const hasUpdate = compareVersions(latestVersion, currentVersion) > 0; + + // 如果有更新,获取更新日志 + let changelog = ""; + + return { hasUpdate, latestVersion, changelog }; + } catch (error) { + console.error("Error checking for updates:", error); + // 如果检查失败,假设没有更新 + return { hasUpdate: false, latestVersion: currentVersion, changelog: "" }; + } +} + +/** + * 执行更新操作 + * @returns 更新结果 + */ +export async function performUpdate() { + try { + // 执行npm update命令 + const { stdout, stderr } = await execPromise("npm update -g @musistudio/claude-code-router"); + + if (stderr) { + console.error("Update stderr:", stderr); + } + + console.log("Update stdout:", stdout); + + return { + success: true, + message: "Update completed successfully. Please restart the application to apply changes." + }; + } catch (error) { + console.error("Error performing update:", error); + return { + success: false, + message: `Failed to perform update: ${error instanceof Error ? error.message : 'Unknown error'}` + }; + } +} + +/** + * 比较两个版本号 + * @param v1 版本号1 + * @param v2 版本号2 + * @returns 1 if v1 > v2, -1 if v1 < v2, 0 if equal + */ +function compareVersions(v1: string, v2: string): number { + const parts1 = v1.split(".").map(Number); + const parts2 = v2.split(".").map(Number); + + for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { + const num1 = i < parts1.length ? parts1[i] : 0; + const num2 = i < parts2.length ? parts2[i] : 0; + + if (num1 > num2) return 1; + if (num1 < num2) return -1; + } + + return 0; +} \ No newline at end of file diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 9a96f86..837fe88 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useCallback, useRef } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { SettingsDialog } from "@/components/SettingsDialog"; @@ -9,13 +9,21 @@ import { JsonEditor } from "@/components/JsonEditor"; import { Button } from "@/components/ui/button"; import { useConfig } from "@/components/ConfigProvider"; import { api } from "@/lib/api"; -import { Settings, Languages, Save, RefreshCw, FileJson } from "lucide-react"; +import { Settings, Languages, Save, RefreshCw, FileJson, CircleArrowUp } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Toast } from "@/components/ui/toast"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, + DialogDescription, +} from "@/components/ui/dialog"; import "@/styles/animations.css"; function App() { @@ -26,53 +34,13 @@ function App() { const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false); const [isCheckingAuth, setIsCheckingAuth] = useState(true); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null); - - useEffect(() => { - const checkAuth = async () => { - // If we already have a config, we're authenticated - if (config) { - setIsCheckingAuth(false); - return; - } - - // For empty API key, allow access without checking config - const apiKey = localStorage.getItem('apiKey'); - if (!apiKey) { - setIsCheckingAuth(false); - return; - } - - // If we don't have a config, try to fetch it - try { - await api.getConfig(); - // If successful, we don't need to do anything special - // The ConfigProvider will handle setting the config - } catch (err) { - // If it's a 401, the API client will redirect to login - // For other errors, we still show the app to display the error - console.error('Error checking auth:', err); - // Redirect to login on authentication error - if ((err as Error).message === 'Unauthorized') { - navigate('/login'); - } - } finally { - setIsCheckingAuth(false); - } - }; - - checkAuth(); - - // Listen for unauthorized events - const handleUnauthorized = () => { - navigate('/login'); - }; - - window.addEventListener('unauthorized', handleUnauthorized); - - return () => { - window.removeEventListener('unauthorized', handleUnauthorized); - }; - }, [config, navigate]); + // 版本检查状态 + const [isNewVersionAvailable, setIsNewVersionAvailable] = useState(false); + const [isUpdateDialogOpen, setIsUpdateDialogOpen] = useState(false); + const [newVersionInfo, setNewVersionInfo] = useState<{ version: string; changelog: string } | null>(null); + const [isCheckingUpdate, setIsCheckingUpdate] = useState(false); + const [hasCheckedUpdate, setHasCheckedUpdate] = useState(false); + const hasAutoCheckedUpdate = useRef(false); const saveConfig = async () => { // Handle case where config might be null or undefined @@ -152,6 +120,124 @@ function App() { setToast({ message: t('app.config_saved_restart_failed') + ': ' + (error as Error).message, type: 'error' }); } }; + + // 检查更新函数 + const checkForUpdates = useCallback(async (showDialog: boolean = true) => { + // 如果已经检查过且有新版本,根据参数决定是否显示对话框 + if (hasCheckedUpdate && isNewVersionAvailable) { + if (showDialog) { + setIsUpdateDialogOpen(true); + } + return; + } + + setIsCheckingUpdate(true); + try { + const updateInfo = await api.checkForUpdates(); + + if (updateInfo.hasUpdate && updateInfo.latestVersion && updateInfo.changelog) { + setIsNewVersionAvailable(true); + setNewVersionInfo({ + version: updateInfo.latestVersion, + changelog: updateInfo.changelog + }); + // 只有在showDialog为true时才显示对话框 + if (showDialog) { + setIsUpdateDialogOpen(true); + } + } else if (showDialog) { + // 只有在showDialog为true时才显示没有更新的提示 + setToast({ message: t('app.no_updates_available'), type: 'success' }); + } + + setHasCheckedUpdate(true); + } catch (error) { + console.error('Failed to check for updates:', error); + if (showDialog) { + setToast({ message: t('app.update_check_failed') + ': ' + (error as Error).message, type: 'error' }); + } + } finally { + setIsCheckingUpdate(false); + } + }, [hasCheckedUpdate, isNewVersionAvailable, t]); + + useEffect(() => { + const checkAuth = async () => { + // If we already have a config, we're authenticated + if (config) { + setIsCheckingAuth(false); + // 自动检查更新,但不显示对话框 + if (!hasCheckedUpdate && !hasAutoCheckedUpdate.current) { + hasAutoCheckedUpdate.current = true; + checkForUpdates(false); + } + return; + } + + // For empty API key, allow access without checking config + const apiKey = localStorage.getItem('apiKey'); + if (!apiKey) { + setIsCheckingAuth(false); + return; + } + + // If we don't have a config, try to fetch it + try { + await api.getConfig(); + // If successful, we don't need to do anything special + // The ConfigProvider will handle setting the config + } catch (err) { + // If it's a 401, the API client will redirect to login + // For other errors, we still show the app to display the error + console.error('Error checking auth:', err); + // Redirect to login on authentication error + if ((err as Error).message === 'Unauthorized') { + navigate('/login'); + } + } finally { + setIsCheckingAuth(false); + // 在获取配置完成后检查更新,但不显示对话框 + if (!hasCheckedUpdate && !hasAutoCheckedUpdate.current) { + hasAutoCheckedUpdate.current = true; + checkForUpdates(false); + } + } + }; + + checkAuth(); + + // Listen for unauthorized events + const handleUnauthorized = () => { + navigate('/login'); + }; + + window.addEventListener('unauthorized', handleUnauthorized); + + return () => { + window.removeEventListener('unauthorized', handleUnauthorized); + }; + }, [config, navigate, hasCheckedUpdate, checkForUpdates]); + + // 执行更新函数 + const performUpdate = async () => { + if (!newVersionInfo) return; + + try { + const result = await api.performUpdate(); + + if (result.success) { + setToast({ message: t('app.update_successful'), type: 'success' }); + setIsNewVersionAvailable(false); + setIsUpdateDialogOpen(false); + setHasCheckedUpdate(false); // 重置检查状态,以便下次重新检查 + } else { + setToast({ message: t('app.update_failed') + ': ' + result.message, type: 'error' }); + } + } catch (error) { + console.error('Failed to perform update:', error); + setToast({ message: t('app.update_failed') + ': ' + (error as Error).message, type: 'error' }); + } + }; if (isCheckingAuth) { @@ -215,6 +301,26 @@ function App() { + {/* 更新版本按钮 */} + + + + + {toast && ( { return this.post('/restart', {}); } + + // Check for updates + async checkForUpdates(): Promise<{ hasUpdate: boolean; latestVersion?: string; changelog?: string }> { + return this.get<{ hasUpdate: boolean; latestVersion?: string; changelog?: string }>('/update/check'); + } + + // Perform update + async performUpdate(): Promise<{ success: boolean; message: string }> { + return this.post<{ success: boolean; message: string }>('/api/update/perform', {}); + } } // Create a default instance of the API client diff --git a/ui/src/locales/en.json b/ui/src/locales/en.json index c79459d..78484e9 100644 --- a/ui/src/locales/en.json +++ b/ui/src/locales/en.json @@ -12,7 +12,16 @@ "config_saved_success": "Config saved successfully", "config_saved_failed": "Failed to save config", "config_saved_restart_success": "Config saved and service restarted successfully", - "config_saved_restart_failed": "Failed to save config and restart service" + "config_saved_restart_failed": "Failed to save config and restart service", + "new_version_available": "New Version Available", + "update_description": "A new version is available. Please review the changelog and update to get the latest features and improvements.", + "no_changelog_available": "No changelog available", + "later": "Later", + "update_now": "Update Now", + "no_updates_available": "No updates available", + "update_check_failed": "Failed to check for updates", + "update_successful": "Update successful", + "update_failed": "Update failed" }, "login": { "title": "Sign in to your account", diff --git a/ui/src/locales/zh.json b/ui/src/locales/zh.json index fff7ce3..6186df3 100644 --- a/ui/src/locales/zh.json +++ b/ui/src/locales/zh.json @@ -12,7 +12,16 @@ "config_saved_success": "配置保存成功", "config_saved_failed": "配置保存失败", "config_saved_restart_success": "配置保存并服务重启成功", - "config_saved_restart_failed": "配置保存并服务重启失败" + "config_saved_restart_failed": "配置保存并服务重启失败", + "new_version_available": "有新版本可用", + "update_description": "发现新版本。请查看更新日志并更新以获取最新功能和改进。", + "no_changelog_available": "暂无更新日志", + "later": "稍后再说", + "update_now": "立即更新", + "no_updates_available": "当前已是最新版本", + "update_check_failed": "检查更新失败", + "update_successful": "更新成功", + "update_failed": "更新失败" }, "login": { "title": "登录到您的账户",