From a03800f196621727413a4d3a3d12c83c966a866e Mon Sep 17 00:00:00 2001 From: engigu Date: Wed, 18 Mar 2026 10:53:41 +0800 Subject: [PATCH] feat: add mirror settings #29 --- internal/controllers/mise_controller.go | 45 +++ internal/router/api_routes.go | 3 + internal/services/mise_service.go | 47 ++++ web/src/api/index.ts | 5 +- web/src/views/languages/Languages.vue | 359 +++++++++++++++++------- 5 files changed, 359 insertions(+), 100 deletions(-) diff --git a/internal/controllers/mise_controller.go b/internal/controllers/mise_controller.go index 5ec3cd8..ae4b147 100644 --- a/internal/controllers/mise_controller.go +++ b/internal/controllers/mise_controller.go @@ -117,3 +117,48 @@ func (c *MiseController) UnsetGlobal(ctx *gin.Context) { } utils.Success(ctx, nil) } + +// Envs 获取全局环境变量 +func (c *MiseController) Envs(ctx *gin.Context) { + envs, err := c.service.Envs() + if err != nil { + utils.ServerError(ctx, "获取全局环境变量失败: "+err.Error()) + return + } + utils.Success(ctx, envs) +} + +// SetEnv 设置全局环境变量 +func (c *MiseController) SetEnv(ctx *gin.Context) { + var req struct { + Key string `json:"key"` + Value string `json:"value"` + } + if err := ctx.ShouldBindJSON(&req); err != nil { + utils.BadRequest(ctx, "参数错误: "+err.Error()) + return + } + if req.Key == "" { + utils.BadRequest(ctx, "参数 key 不能为空") + return + } + if err := c.service.SetEnv(req.Key, req.Value); err != nil { + utils.ServerError(ctx, "设置环境变量失败: "+err.Error()) + return + } + utils.Success(ctx, nil) +} + +// UnsetEnv 取消全局环境变量 +func (c *MiseController) UnsetEnv(ctx *gin.Context) { + key := ctx.Query("key") + if key == "" { + utils.BadRequest(ctx, "参数 key 不能为空") + return + } + if err := c.service.UnsetEnv(key); err != nil { + utils.ServerError(ctx, "取消环境变量失败: "+err.Error()) + return + } + utils.Success(ctx, nil) +} diff --git a/internal/router/api_routes.go b/internal/router/api_routes.go index 4fa56b9..a6c31a4 100644 --- a/internal/router/api_routes.go +++ b/internal/router/api_routes.go @@ -212,6 +212,9 @@ func registerMiseRoutes(g *gin.RouterGroup, c *Controllers) { mise.GET("/verify-cmd", c.Mise.VerifyCommand) mise.POST("/use-global", c.Mise.UseGlobal) mise.POST("/unset-global", c.Mise.UnsetGlobal) + mise.GET("/envs", c.Mise.Envs) + mise.POST("/envs", c.Mise.SetEnv) + mise.DELETE("/envs", c.Mise.UnsetEnv) } } diff --git a/internal/services/mise_service.go b/internal/services/mise_service.go index 5c1f6bd..da59614 100644 --- a/internal/services/mise_service.go +++ b/internal/services/mise_service.go @@ -359,3 +359,50 @@ func (s *MiseService) UnsetGlobal(plugin, version string) error { } return nil } +// Envs 获取全局环境变量 +func (s *MiseService) Envs() (map[string]string, error) { + cmd := exec.Command("mise", "set") + cmd.Env = os.Environ() + output, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("mise set failed: %v, output: %s", err, string(output)) + } + + envs := make(map[string]string) + lines := strings.Split(string(output), "\n") + for _, line := range lines { + fields := strings.Fields(line) + // 期望格式: KEY VALUE SOURCE + // 跳过标题行或不合规行 + if len(fields) < 3 || strings.ToLower(fields[0]) == "key" { + continue + } + // 仅显示来自全局配置文件的环境变量 + if strings.Contains(strings.ToLower(fields[len(fields)-1]), "config.toml") { + envs[fields[0]] = fields[1] + } + } + return envs, nil +} + +// SetEnv 设置全局环境变量 +func (s *MiseService) SetEnv(key, value string) error { + cmd := exec.Command("mise", "set", "-g", fmt.Sprintf("%s=%s", key, value)) + cmd.Env = os.Environ() + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("mise set -g failed: %v, output: %s", err, string(output)) + } + return nil +} + +// UnsetEnv 取消全局环境变量 +func (s *MiseService) UnsetEnv(key string) error { + cmd := exec.Command("mise", "unset", "-g", key) + cmd.Env = os.Environ() + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("mise unset -g failed: %v, output: %s", err, string(output)) + } + return nil +} diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 5728b53..ca9e926 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -269,7 +269,10 @@ export const api = { versions: (plugin: string) => request(`/mise/versions?plugin=${plugin}`), verifyCommand: (plugin: string, version: string) => request<{ command: string }>(`/mise/verify-cmd?plugin=${plugin}&version=${version}`), useGlobal: (plugin: string, version: string) => request('/mise/use-global', { method: 'POST', body: JSON.stringify({ plugin, version }) }), - unsetGlobal: (plugin: string, version: string) => request('/mise/unset-global', { method: 'POST', body: JSON.stringify({ plugin, version }) }) + unsetGlobal: (plugin: string, version: string) => request('/mise/unset-global', { method: 'POST', body: JSON.stringify({ plugin, version }) }), + getEnvs: () => request>('/mise/envs'), + setEnv: (key: string, value: string) => request('/mise/envs', { method: 'POST', body: JSON.stringify({ key, value }) }), + unsetEnv: (key: string) => request(`/mise/envs?key=${key}`, { method: 'DELETE' }) }, terminal: { cmds: () => request<{ name: string, description: string }[]>('/terminal/cmds') diff --git a/web/src/views/languages/Languages.vue b/web/src/views/languages/Languages.vue index ed5810d..3a93bbc 100644 --- a/web/src/views/languages/Languages.vue +++ b/web/src/views/languages/Languages.vue @@ -10,8 +10,9 @@ import { ScrollArea } from '@/components/ui/scroll-area' import { Plus, Globe, Search, RefreshCw, Loader2, Trash2, Terminal as TerminalIcon, X, AlertCircle, - Check, ChevronsUpDown + Check, ChevronsUpDown, ArrowRight } from 'lucide-vue-next' +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' import { api, type MiseLanguage } from '@/api' import { toast } from 'vue-sonner' import XTerminal from '@/components/XTerminal.vue' @@ -56,12 +57,67 @@ const filteredPlugins = computed(() => { return availablePlugins.value.filter(p => p.toLowerCase().includes(s)) }) -const filteredVersions = computed(() => { - const list = availableVersions.value - if (!versionSearch.value) return list - const s = versionSearch.value.toLowerCase() - return list.filter(v => v.toLowerCase().includes(s)) -}) +const activeTab = ref('runtimes') + +// 全局变量相关 +const globalEnvs = ref>({}) +const loadingEnvs = ref(false) +const showAddEnvDialog = ref(false) +const newEnvKey = ref('') +const newEnvValue = ref('') + +const ENV_PRESETS = [ + { label: 'npm (淘宝镜像)', key: 'NPM_CONFIG_REGISTRY', value: 'https://registry.npmmirror.com' }, + { label: 'Python (清华镜像)', key: 'PIP_INDEX_URL', value: 'https://pypi.tuna.tsinghua.edu.cn/simple' }, + { label: 'Go (goproxy.cn)', key: 'GOPROXY', value: 'https://goproxy.cn,direct' }, + { label: 'Rust (Sparse protocol)', key: 'CARGO_REGISTRY_CRATES_IO_PROTOCOL', value: 'sparse' }, + { label: 'Rust (中科大镜像)', key: 'CARGO_REGISTRIES_CRATES_IO_INDEX', value: 'https://mirrors.ustc.edu.cn/crates.io-index' }, +] + +async function loadGlobalEnvs() { + loadingEnvs.value = true + try { + globalEnvs.value = await api.mise.getEnvs() + } catch (e) { + toast.error('获取全局环境变量失败: ' + e) + } finally { + loadingEnvs.value = false + } +} + +async function handleSetEnv(key: string, value: string) { + try { + await api.mise.setEnv(key, value) + toast.success(`环境变量 ${key} 设置成功`) + loadGlobalEnvs() + } catch (e) { + toast.error('设置失败: ' + e) + } +} + +async function handleUnsetEnv(key: string) { + try { + await api.mise.unsetEnv(key) + toast.success(`环境变量 ${key} 已移除`) + loadGlobalEnvs() + } catch (e) { + toast.error('移除失败: ' + e) + } +} + +function applyPreset(preset: { key: string, value: string }) { + newEnvKey.value = preset.key + newEnvValue.value = preset.value + showAddEnvDialog.value = true +} + +function startAddEnv() { + if (!newEnvKey.value.trim()) return + handleSetEnv(newEnvKey.value.trim(), newEnvValue.value.trim()) + showAddEnvDialog.value = false + newEnvKey.value = '' + newEnvValue.value = '' +} const showTerminalDialog = ref(false) const terminalCommand = ref('') @@ -73,6 +129,13 @@ const filteredLanguages = computed(() => { return languages.value.filter(l => l.plugin.toLowerCase().includes(q) || l.version.toLowerCase().includes(q)) }) +const filteredVersions = computed(() => { + const list = availableVersions.value + if (!versionSearch.value) return list + const s = versionSearch.value.toLowerCase() + return list.filter(v => v.toLowerCase().includes(s)) +}) + async function loadLanguages() { loading.value = true errorMsg.value = '' @@ -260,7 +323,10 @@ async function toggleDefault(lang: MiseLanguage) { } } -onMounted(loadLanguages) +onMounted(() => { + loadLanguages() + loadGlobalEnvs() +})