add update button
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Claude Code Router
|
||||
|
||||
我正在为该项目寻求资金支持,以更好地维持其发展。如果您有任何想法,请随时与我联系: [m@musiiot.top](mailto:m@musiiot.top)
|
||||
|
||||
> 一款强大的工具,可将 Claude Code 请求路由到不同的模型,并自定义任何请求。
|
||||
|
||||

|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -70,5 +64,43 @@ export const createServer = (config: any): Server => {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -162,3 +162,6 @@ export const initConfig = async () => {
|
||||
|
||||
// 导出日志清理函数
|
||||
export { cleanupLogFiles };
|
||||
|
||||
// 导出更新功能
|
||||
export { checkForUpdates, performUpdate } from "./update";
|
||||
|
||||
80
src/utils/update.ts
Normal file
80
src/utils/update.ts
Normal file
@@ -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;
|
||||
}
|
||||
244
ui/src/App.tsx
244
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
|
||||
@@ -153,6 +121,124 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
// 检查更新函数
|
||||
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) {
|
||||
return (
|
||||
@@ -215,6 +301,26 @@ function App() {
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{/* 更新版本按钮 */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => checkForUpdates(true)}
|
||||
disabled={isCheckingUpdate}
|
||||
className="transition-all-ease hover:scale-110 relative"
|
||||
>
|
||||
<div className="relative">
|
||||
<CircleArrowUp className="h-5 w-5" />
|
||||
{isNewVersionAvailable && !isCheckingUpdate && (
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full border-2 border-white"></div>
|
||||
)}
|
||||
</div>
|
||||
{isCheckingUpdate && (
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"></div>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
<Button onClick={saveConfig} variant="outline" className="transition-all-ease hover:scale-[1.02] active:scale-[0.98]">
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{t('app.save')}
|
||||
@@ -244,6 +350,46 @@ function App() {
|
||||
onOpenChange={setIsJsonEditorOpen}
|
||||
showToast={(message, type) => setToast({ message, type })}
|
||||
/>
|
||||
{/* 版本更新对话框 */}
|
||||
<Dialog open={isUpdateDialogOpen} onOpenChange={setIsUpdateDialogOpen}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t('app.new_version_available')}
|
||||
{newVersionInfo && (
|
||||
<span className="ml-2 text-sm font-normal text-muted-foreground">
|
||||
v{newVersionInfo.version}
|
||||
</span>
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('app.update_description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="max-h-96 overflow-y-auto py-4">
|
||||
{newVersionInfo?.changelog ? (
|
||||
<div className="whitespace-pre-wrap text-sm">
|
||||
{newVersionInfo.changelog}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-muted-foreground">
|
||||
{t('app.no_changelog_available')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsUpdateDialogOpen(false)}
|
||||
>
|
||||
{t('app.later')}
|
||||
</Button>
|
||||
<Button onClick={performUpdate}>
|
||||
{t('app.update_now')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{toast && (
|
||||
<Toast
|
||||
message={toast.message}
|
||||
|
||||
@@ -194,6 +194,16 @@ class ApiClient {
|
||||
async restartService(): Promise<unknown> {
|
||||
return this.post<void>('/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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "登录到您的账户",
|
||||
|
||||
Reference in New Issue
Block a user