From 569e9caa08cbb5d3d6471afc13e6e777fedfd6d0 Mon Sep 17 00:00:00 2001 From: ant-black <1016930479@qq.com> Date: Sat, 27 Jun 2026 13:45:11 +0800 Subject: [PATCH] chore: finalize browser backup and license cleanup --- backend/app_backup_config.go | 2 - backend/app_browser_config_api.go | 9 + backend/app_dashboard_api.go | 1 - backend/app_license.go | 165 ----- backend/app_profile_package_api.go | 8 - backend/bootstrap.go | 18 +- backend/internal/browser/dashboard.go | 9 +- .../internal/browser/download_core_task.go | 153 ++++- backend/internal/browser/profile_copy.go | 5 - backend/internal/browser/profile_create.go | 9 - backend/internal/config/config.go | 43 +- backend/internal/config/config_defaults.go | 11 - backend/license_state.go | 174 ------ config.yaml | 3 - frontend/src/modules/browser/api.ts | 1 + frontend/src/modules/browser/api/backup.ts | 36 ++ frontend/src/modules/browser/api/cores.ts | 9 + .../browser/components/BrowserBackupModal.tsx | 102 ++++ .../browser/components/BrowserListLayout.tsx | 73 +-- .../browser/components/BrowserListWidgets.tsx | 7 +- .../modules/browser/pages/BrowserListPage.tsx | 75 ++- .../browser/pages/CoreManagementPage.tsx | 31 +- .../pages/browserList/BrowserListDialogs.tsx | 53 +- .../pages/browserList/useBrowserListData.ts | 4 +- .../browserList/useBrowserListSettings.ts | 40 -- .../browser/pages/coreManagement.types.ts | 2 + .../coreManagement/CoreDownloadModal.tsx | 18 +- .../src/modules/dashboard/DashboardPage.tsx | 56 +- frontend/src/modules/dashboard/api.ts | 40 +- frontend/src/modules/dashboard/types.ts | 1 - .../src/modules/profile/AdminKeygenPage.tsx | 140 ----- frontend/src/modules/profile/ProfilePage.tsx | 16 +- frontend/src/modules/profile/index.ts | 1 - frontend/src/routes/AppRoutes.tsx | 5 - frontend/src/wailsjs/go/main/App.d.ts | 16 +- frontend/src/wailsjs/go/main/App.js | 20 +- frontend/src/wailsjs/go/models.ts | 40 +- go.mod | 3 +- go.sum | 572 ------------------ publish/config.init.linux.yaml | 2 - publish/config.init.mac.yaml | 2 - publish/config.init.yaml | 2 - publish/installer.nsi | 11 +- 43 files changed, 486 insertions(+), 1502 deletions(-) delete mode 100644 backend/app_license.go delete mode 100644 backend/license_state.go create mode 100644 frontend/src/modules/browser/api/backup.ts create mode 100644 frontend/src/modules/browser/components/BrowserBackupModal.tsx delete mode 100644 frontend/src/modules/profile/AdminKeygenPage.tsx diff --git a/backend/app_backup_config.go b/backend/app_backup_config.go index 8ef25e67..c8f1f869 100644 --- a/backend/app_backup_config.go +++ b/backend/app_backup_config.go @@ -66,8 +66,6 @@ func (a *App) backupApplyIncomingConfig(incoming *config.Config, resetFirst bool target = backupMergeConfig(current, incoming) } target.Database = current.Database - target.App.MaxProfileLimit = current.App.MaxProfileLimit - target.App.UsedCDKeys = append([]string{}, current.App.UsedCDKeys...) if err := target.Save(a.resolveAppPath("config.yaml")); err != nil { return fmt.Errorf("保存导入配置失败: %w", err) diff --git a/backend/app_browser_config_api.go b/backend/app_browser_config_api.go index 9b2f75ca..04a11c19 100644 --- a/backend/app_browser_config_api.go +++ b/backend/app_browser_config_api.go @@ -90,3 +90,12 @@ func (a *App) BrowserCoreDownload(coreName, url, proxyConfig string) error { go a.browserMgr.DownloadAndExtractCore(a.ctx, coreName, url, proxyConfig) return nil } + +// BrowserCoreRedownload 重新下载并替换指定内核目录 +func (a *App) BrowserCoreRedownload(coreId, url, proxyConfig string) error { + if a.ctx == nil { + return fmt.Errorf("app context is nil") + } + go a.browserMgr.RedownloadCore(a.ctx, coreId, url, proxyConfig) + return nil +} diff --git a/backend/app_dashboard_api.go b/backend/app_dashboard_api.go index 1964b195..5067473d 100644 --- a/backend/app_dashboard_api.go +++ b/backend/app_dashboard_api.go @@ -23,7 +23,6 @@ func (a *App) GetDashboardStats() map[string]interface{} { "proxyCount": stats.ProxyCount, "coreCount": stats.CoreCount, "memUsedMB": int(memUsedMB), - "maxProfileLimit": stats.MaxProfileLimit, "appVersion": a.appVersion(), } } diff --git a/backend/app_license.go b/backend/app_license.go deleted file mode 100644 index 42d749f9..00000000 --- a/backend/app_license.go +++ /dev/null @@ -1,165 +0,0 @@ -package backend - -import ( - appconfig "ant-chrome/backend/internal/config" - "crypto/sha256" - "encoding/hex" - "fmt" - "math/rand" - "strings" - "time" -) - -// LicenseStatus 授权状态 -type LicenseStatus struct { - MaxLimit int `json:"maxLimit"` - UsedCount int `json:"usedCount"` - UsedKeys []string `json:"usedKeys"` -} - -// GetLicenseStatus 获取当前授权状态(给前端使用) -func (a *App) GetLicenseStatus() LicenseStatus { - profilesCount := 0 - if a.browserMgr != nil { - profilesCount = len(a.browserMgr.List()) - } - usedKeys := a.config.App.UsedCDKeys - if usedKeys == nil { - usedKeys = []string{} - } - - return LicenseStatus{ - MaxLimit: a.config.App.MaxProfileLimit, - UsedCount: profilesCount, - UsedKeys: usedKeys, - } -} - -// RedeemCDKey 验证并核销兑换码,成功返回新配置 -func (a *App) RedeemCDKey(cdkey string) error { - if a.config == nil { - a.config = DefaultConfig() - } - cdkey = strings.ToUpper(cdkey) - // 去除所有可能的不小心复制进去的空格、制表符、换行符 - cdkey = strings.ReplaceAll(cdkey, " ", "") - cdkey = strings.ReplaceAll(cdkey, "\t", "") - cdkey = strings.ReplaceAll(cdkey, "\n", "") - cdkey = strings.ReplaceAll(cdkey, "\r", "") - - if cdkey == "" { - return fmt.Errorf("兑换码不能为空") - } - - // 1. 基本校验机制(非常简单:比如前缀必须是 ANT-,并且后面加上一个特定的哈希位能匹配) - // 生成规则我们在 keygen 里实现。校验规则: - // 假设 cdkey 长这样: ANT-XXXX-XXXX-XXXX-XXXX-CHECKSUM - // 为了最简单的极简方案,我们这就不搞太复杂的非对称,纯用带盐的 SHA256 截断作为校验和。 - if !strings.HasPrefix(cdkey, "ANT-") { - return fmt.Errorf("无效的兑换码格式") - } - - parts := strings.Split(cdkey, "-") - if len(parts) < 3 { - return fmt.Errorf("无效的兑换码长度") - } - - // 验证校验和 - checksumIndex := len(parts) - 1 - payload := strings.Join(parts[:checksumIndex], "-") // "ANT-XXXX-XXXX..." - expectedChecksum := generateChecksum(payload) - actualChecksum := parts[checksumIndex] - - if actualChecksum != expectedChecksum { - return fmt.Errorf("无效的兑换码 (Checksum Error)") - } - - // 2. 防重放校验 - for _, usedKey := range a.config.App.UsedCDKeys { - if usedKey == cdkey { - return fmt.Errorf("该兑换码已被使用过") - } - } - - // 3. 兑现与本地保存 - a.config.App.MaxProfileLimit += appconfig.StandardCDKeyProfileBonus - a.config.App.UsedCDKeys = append(a.config.App.UsedCDKeys, cdkey) - - configPath := a.resolveAppPath("config.yaml") - if _, _, err := reconcileConfigWithLocalLicense(configPath, a.config); err != nil { - return fmt.Errorf("保存本机额度状态失败: %v", err) - } - if err := a.config.Save(configPath); err != nil { - return fmt.Errorf("保存配置失败: %v", err) - } - - return nil -} - -// generateChecksum 生成简易校验和 -func generateChecksum(payload string) string { - salt := "ANT-LITE-KEY-SALT-VER-1" - hash := sha256.Sum256([]byte(payload + salt)) - return strings.ToUpper(hex.EncodeToString(hash[:])[0:8]) // 取前8位作为校验 -} - -// RedeemGithubStar 给予用户一个 github star 的一次性奖励 -func (a *App) RedeemGithubStar() error { - if a.config == nil { - a.config = DefaultConfig() - } - cdkey := appconfig.GithubStarRewardKey - // 防重复领取 - for _, usedKey := range a.config.App.UsedCDKeys { - if usedKey == cdkey { - return fmt.Errorf("您已经领取过 GitHub Star 的赠送额度啦!") - } - } - - a.config.App.UsedCDKeys = append(a.config.App.UsedCDKeys, cdkey) - a.config.App.MaxProfileLimit += appconfig.GithubStarProfileBonus - if minLimit := appconfig.MinimumProfileLimitForUsedKeys(a.config.App.UsedCDKeys); a.config.App.MaxProfileLimit < minLimit { - a.config.App.MaxProfileLimit = minLimit - } - - configPath := a.resolveAppPath("config.yaml") - if _, _, err := reconcileConfigWithLocalLicense(configPath, a.config); err != nil { - return fmt.Errorf("保存本机额度状态失败: %v", err) - } - if err := a.config.Save(configPath); err != nil { - return fmt.Errorf("保存配置失败: %v", err) - } - - return nil -} - -// GenerateCDKeys 供内部隐藏管理员页面使用的发卡器接口 -func (a *App) GenerateCDKeys(count int) ([]string, error) { - if count <= 0 || count > 1000 { - return nil, fmt.Errorf("生成数量无效 (1-1000)") - } - - rand.Seed(time.Now().UnixNano()) - var keys []string - - for i := 0; i < count; i++ { - // A basic random 16-char string ABCDEFGH-IJKLMNOP... - charset := "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - b := make([]byte, 16) - for j := range b { - b[j] = charset[rand.Intn(len(charset))] - } - - part1 := string(b[0:4]) - part2 := string(b[4:8]) - part3 := string(b[8:12]) - part4 := string(b[12:16]) - - payload := fmt.Sprintf("ANT-%s-%s-%s-%s", part1, part2, part3, part4) - checksum := generateChecksum(payload) - - keys = append(keys, fmt.Sprintf("%s-%s", payload, checksum)) - } - - return keys, nil -} diff --git a/backend/app_profile_package_api.go b/backend/app_profile_package_api.go index eabeabcd..a7d1de31 100644 --- a/backend/app_profile_package_api.go +++ b/backend/app_profile_package_api.go @@ -240,14 +240,6 @@ func (a *App) importProfilePackageFromPath(zipPath string) (ProfilePackageImport } a.browserMgr.InitData() - if a.config.App.MaxProfileLimit > 0 { - a.browserMgr.Mutex.Lock() - currentCount := len(a.browserMgr.Profiles) - a.browserMgr.Mutex.Unlock() - if currentCount+len(profiles) > a.config.App.MaxProfileLimit { - return ProfilePackageImportResult{}, fmt.Errorf("实例数量已达上限 (%d个),无法导入 %d 个实例", a.config.App.MaxProfileLimit, len(profiles)) - } - } now := time.Now().Format(time.RFC3339) mappings := make(map[string]string, len(profiles)) prepared := make([]browser.Profile, 0, len(profiles)) diff --git a/backend/bootstrap.go b/backend/bootstrap.go index 317dc294..807c3f39 100644 --- a/backend/bootstrap.go +++ b/backend/bootstrap.go @@ -14,19 +14,8 @@ type TrayCallbacks = apptray.Callbacks func LoadConfig(path string) (*Config, error) { cfg, err := appconfig.Load(path) - configChanged := false repairedConfig := false if err == nil { - if changed, _, syncErr := reconcileConfigWithLocalLicense(path, cfg); syncErr != nil { - return cfg, syncErr - } else { - configChanged = changed - } - if configChanged { - if saveErr := cfg.Save(path); saveErr != nil { - return cfg, fmt.Errorf("写回配置失败: %w", saveErr) - } - } return cfg, nil } @@ -40,12 +29,7 @@ func LoadConfig(path string) (*Config, error) { defaultCfg := appconfig.DefaultConfig() repairedConfig = true - if changed, _, syncErr := reconcileConfigWithLocalLicense(path, defaultCfg); syncErr != nil { - return defaultCfg, syncErr - } else { - configChanged = changed - } - if repairedConfig || configChanged { + if repairedConfig { if saveErr := os.MkdirAll(filepath.Dir(path), 0755); saveErr != nil { return defaultCfg, fmt.Errorf("加载配置失败: %w;创建配置目录失败: %v", err, saveErr) } diff --git a/backend/internal/browser/dashboard.go b/backend/internal/browser/dashboard.go index 6ef9b33e..0ab87a87 100644 --- a/backend/internal/browser/dashboard.go +++ b/backend/internal/browser/dashboard.go @@ -2,20 +2,16 @@ package browser import "ant-chrome/backend/internal/config" -const DefaultMaxProfileLimit = 20 - type DashboardStats struct { TotalInstances int RunningInstances int ProxyCount int CoreCount int - MaxProfileLimit int } func BuildDashboardStats(profiles []Profile, cfg *config.Config) DashboardStats { stats := DashboardStats{ - TotalInstances: len(profiles), - MaxProfileLimit: DefaultMaxProfileLimit, + TotalInstances: len(profiles), } for _, profile := range profiles { if profile.Running { @@ -25,9 +21,6 @@ func BuildDashboardStats(profiles []Profile, cfg *config.Config) DashboardStats if cfg != nil { stats.ProxyCount = len(cfg.Browser.Proxies) stats.CoreCount = len(cfg.Browser.Cores) - if cfg.App.MaxProfileLimit > 0 { - stats.MaxProfileLimit = cfg.App.MaxProfileLimit - } } return stats } diff --git a/backend/internal/browser/download_core_task.go b/backend/internal/browser/download_core_task.go index 0404b400..9e4a7005 100644 --- a/backend/internal/browser/download_core_task.go +++ b/backend/internal/browser/download_core_task.go @@ -17,6 +17,37 @@ import ( // DownloadAndExtractCore 执行异步下载解压并在过程中发送事件 func (m *Manager) DownloadAndExtractCore(ctx context.Context, coreName string, targetUrl string, proxyConfig string) { + coreName = strings.TrimSpace(coreName) + for _, core := range m.ListCores() { + if strings.EqualFold(core.CoreName, coreName) || filepath.Base(core.CorePath) == coreName { + m.downloadAndExtractCore(ctx, CoreInput{ + CoreId: core.CoreId, + CoreName: core.CoreName, + CorePath: core.CorePath, + IsDefault: core.IsDefault, + }, targetUrl, proxyConfig, false) + return + } + } + m.downloadAndExtractCore(ctx, CoreInput{CoreName: coreName}, targetUrl, proxyConfig, false) +} + +// RedownloadCore 重新下载指定内核,验证成功后替换原目录并保留原配置。 +func (m *Manager) RedownloadCore(ctx context.Context, coreId string, targetUrl string, proxyConfig string) { + core, ok := m.GetCore(coreId) + if !ok { + runtime.EventsEmit(ctx, "download:progress", DownloadProgress{Phase: "error", Progress: 0, Message: "内核不存在"}) + return + } + m.downloadAndExtractCore(ctx, CoreInput{ + CoreId: core.CoreId, + CoreName: core.CoreName, + CorePath: core.CorePath, + IsDefault: core.IsDefault, + }, targetUrl, proxyConfig, true) +} + +func (m *Manager) downloadAndExtractCore(ctx context.Context, coreInput CoreInput, targetUrl string, proxyConfig string, replaceExisting bool) { log := logger.New("Browser") t := time.Now() @@ -30,27 +61,41 @@ func (m *Manager) DownloadAndExtractCore(ctx context.Context, coreName string, t sendEvent("downloading", 0, "开始解析地址并创建下载请求: "+targetUrl) - // 1. 检查名称重复 - coreName = strings.TrimSpace(coreName) + coreName := strings.TrimSpace(coreInput.CoreName) + if coreName == "" { + sendEvent("error", 0, "内核名称不能为空") + return + } + for _, c := range m.ListCores() { + if replaceExisting && strings.EqualFold(c.CoreId, strings.TrimSpace(coreInput.CoreId)) { + continue + } if strings.EqualFold(c.CoreName, coreName) || filepath.Base(c.CorePath) == coreName { sendEvent("error", 0, "名称已存在,请换一个名称") return } } - // 确保外层 chrome/ 目录存在 - chromeDir := m.ResolveRelativePath("chrome") - if err := os.MkdirAll(chromeDir, 0755); err != nil { - sendEvent("error", 0, "创建 chrome 目录失败") + targetCorePath := strings.TrimSpace(coreInput.CorePath) + if targetCorePath == "" { + targetCorePath = filepath.Join("chrome", coreName) + } + targetDir := m.ResolveRelativePath(targetCorePath) + parentDir := filepath.Dir(targetDir) + if err := os.MkdirAll(parentDir, 0755); err != nil { + sendEvent("error", 0, "创建内核目录失败") return } - targetDir := filepath.Join(chromeDir, coreName) - if _, err := os.Stat(targetDir); !os.IsNotExist(err) { - sendEvent("error", 0, "同名文件夹已存在: "+coreName) + if _, err := os.Stat(targetDir); err == nil && !replaceExisting { + sendEvent("error", 0, "同名内核目录已存在,请改名下载;如需覆盖,请在内核列表使用重新下载") + return + } else if err != nil && !os.IsNotExist(err) { + sendEvent("error", 0, "检查内核目录失败: "+err.Error()) return } + // 2. 准备 HttpClient(优先从 Windows 注册表读取真实系统代理,而非仅靠环境变量) transport := &http.Transport{} if proxyConfig == "__system__" { @@ -83,7 +128,7 @@ func (m *Manager) DownloadAndExtractCore(ctx context.Context, coreName string, t Transport: transport, } - tempFile, err := os.CreateTemp(chromeDir, "download_*.zip") + tempFile, err := os.CreateTemp(parentDir, "download_*.zip") if err != nil { sendEvent("error", 0, "创建临时文件失败: "+err.Error()) return @@ -106,32 +151,84 @@ func (m *Manager) DownloadAndExtractCore(ctx context.Context, coreName string, t sendEvent("extracting", 0, "下载完成,正在准备解压文件...") log.Info("内核下载完成", logger.F("url", targetUrl), logger.F("temp", tempFilePath), logger.F("cost", time.Since(t).String())) + tempExtractDir, err := os.MkdirTemp(parentDir, coreName+"_extract_*") + if err != nil { + sendEvent("error", 0, "创建临时解压目录失败: "+err.Error()) + return + } + cleanupTempExtract := true + defer func() { + if cleanupTempExtract { + os.RemoveAll(tempExtractDir) + } + }() + // 3. 执行解压,并剥离顶层文件夹 - if err := extractZipAndStripRoot(tempFilePath, targetDir, func(p int, msg string) { + if err := extractZipAndStripRoot(tempFilePath, tempExtractDir, func(p int, msg string) { sendEvent("extracting", p, msg) }); err != nil { - os.RemoveAll(targetDir) // 删除不完整的解压文件 sendEvent("error", 0, "解压失败: "+err.Error()) return } - // 4. 将新内核配置入库 - corePath := filepath.Join("chrome", coreName) - if m.ValidateCorePath(corePath).Valid { - newCore := CoreInput{ - CoreId: uuid.NewString(), // 使用固定的 UUID 或生成新的 - CoreName: coreName, - CorePath: corePath, - IsDefault: len(m.ListCores()) == 0, // 如果没有其他内核,这设为默认 - } - if err := m.SaveCore(newCore); err != nil { - sendEvent("error", 0, "保存配置入库失败: "+err.Error()) - return - } + if !m.ValidateCorePath(tempExtractDir).Valid { + sendEvent("error", 0, fmt.Sprintf("解压后未找到浏览器可执行文件(候选:%s),请检查压缩包内容!", strings.Join(CoreExecutableCandidates(), ", "))) + return + } + + if err := replaceCoreDirectory(targetDir, tempExtractDir, replaceExisting); err != nil { + sendEvent("error", 0, "替换内核目录失败: "+err.Error()) + return + } + cleanupTempExtract = false + + coreToSave := CoreInput{ + CoreId: strings.TrimSpace(coreInput.CoreId), + CoreName: coreName, + CorePath: targetCorePath, + IsDefault: coreInput.IsDefault, + } + if coreToSave.CoreId == "" { + coreToSave.CoreId = uuid.NewString() + coreToSave.IsDefault = len(m.ListCores()) == 0 + } + if err := m.SaveCore(coreToSave); err != nil { + sendEvent("error", 0, "保存配置入库失败: "+err.Error()) + return + } + + if replaceExisting && strings.TrimSpace(coreInput.CoreId) != "" { + sendEvent("done", 100, "内核重新下载成功!") + log.Info("内核重新下载成功", logger.F("core_id", coreToSave.CoreId), logger.F("core_name", coreName)) + } else { sendEvent("done", 100, "内核下载与配置成功!") log.Info("内核下载配置入库成功", logger.F("core_name", coreName)) - } else { - os.RemoveAll(targetDir) // 删除不正确的解压内容 - sendEvent("error", 0, fmt.Sprintf("解压后未找到浏览器可执行文件(候选:%s),请检查压缩包内容!", strings.Join(CoreExecutableCandidates(), ", "))) } } + +func replaceCoreDirectory(targetDir string, tempExtractDir string, replaceExisting bool) error { + if !replaceExisting { + return os.Rename(tempExtractDir, targetDir) + } + + backupDir := targetDir + ".backup_" + time.Now().Format("20060102150405") + if _, err := os.Stat(targetDir); err == nil { + if err := os.Rename(targetDir, backupDir); err != nil { + return err + } + } else if err != nil && !os.IsNotExist(err) { + return err + } + + if err := os.Rename(tempExtractDir, targetDir); err != nil { + if backupDir != "" { + _ = os.Rename(backupDir, targetDir) + } + return err + } + + if backupDir != "" { + _ = os.RemoveAll(backupDir) + } + return nil +} diff --git a/backend/internal/browser/profile_copy.go b/backend/internal/browser/profile_copy.go index 6256244f..5d6cda8c 100644 --- a/backend/internal/browser/profile_copy.go +++ b/backend/internal/browser/profile_copy.go @@ -64,11 +64,6 @@ func (m *Manager) copyProfile(profileId string, newName string, fingerprintResol m.Mutex.Lock() defer m.Mutex.Unlock() - if m.Config.App.MaxProfileLimit > 0 && len(m.Profiles) >= m.Config.App.MaxProfileLimit { - log.Error("复制实例失败: 达到数量上限", logger.F("limit", m.Config.App.MaxProfileLimit)) - return nil, newProfileLimitExceededError(m.Config.App.MaxProfileLimit, "复制实例") - } - src, exists := m.Profiles[profileId] if !exists { log.Error("源实例不存在", logger.F("profile_id", profileId)) diff --git a/backend/internal/browser/profile_create.go b/backend/internal/browser/profile_create.go index 21343718..470f480e 100644 --- a/backend/internal/browser/profile_create.go +++ b/backend/internal/browser/profile_create.go @@ -2,7 +2,6 @@ package browser import ( "ant-chrome/backend/internal/logger" - "fmt" "strings" "time" @@ -16,10 +15,6 @@ func (m *Manager) Create(input ProfileInput) (*Profile, error) { m.Mutex.Lock() defer m.Mutex.Unlock() - if m.Config.App.MaxProfileLimit > 0 && len(m.Profiles) >= m.Config.App.MaxProfileLimit { - return nil, fmt.Errorf("实例数量已达上限 (%d个),无法创建新的实例。请兑换额度后重试!", m.Config.App.MaxProfileLimit) - } - now := time.Now().Format(time.RFC3339) profileId := uuid.NewString() userDataDir := strings.TrimSpace(input.UserDataDir) @@ -85,10 +80,6 @@ func (m *Manager) ensureProfileLaunchCode(profile *Profile) { } } -func newProfileLimitExceededError(limit int, action string) error { - return fmt.Errorf("实例数量已达上限 (%d个),无法%s。请兑换额度后重试!", limit, action) -} - func buildProfileGroupID(value string) string { return strings.TrimSpace(value) } diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 35fcf120..50c718e3 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -1,13 +1,6 @@ package config -import "strings" - const ( - DefaultMaxProfileLimit = 20 - StandardCDKeyProfileBonus = 10 - GithubStarRewardKey = "GITHUB_STAR_REWARD" - GithubStarProfileBonus = 50 - GithubStarProfileTotal = DefaultMaxProfileLimit + GithubStarProfileBonus DefaultLaunchServerPort = 19876 DefaultLaunchServerAPIKeyHeader = "X-Ant-Api-Key" DefaultAutomationInstallPolicy = "on_demand" @@ -22,36 +15,6 @@ const ( AutomationNodeSourceBundled = "bundled" ) -// RewardForUsedKey 返回指定兑换记录对应的永久额度奖励。 -func RewardForUsedKey(key string) int { - normalized := strings.ToUpper(strings.TrimSpace(key)) - if normalized == "" { - return 0 - } - if normalized == GithubStarRewardKey { - return GithubStarProfileBonus - } - return StandardCDKeyProfileBonus -} - -// MinimumProfileLimitForUsedKeys 根据兑换记录计算最低应得实例额度。 -func MinimumProfileLimitForUsedKeys(keys []string) int { - limit := DefaultMaxProfileLimit - seen := make(map[string]struct{}, len(keys)) - for _, key := range keys { - normalized := strings.ToUpper(strings.TrimSpace(key)) - if normalized == "" { - continue - } - if _, exists := seen[normalized]; exists { - continue - } - seen[normalized] = struct{}{} - limit += RewardForUsedKey(normalized) - } - return limit -} - // LaunchServerConfig Launch HTTP 服务配置 type LaunchServerConfig struct { Port int `yaml:"port"` @@ -117,10 +80,8 @@ type SQLiteConfig struct { } type AppConfig struct { - Name string `yaml:"name"` - Window WindowConfig `yaml:"window"` - MaxProfileLimit int `yaml:"max_profile_limit"` - UsedCDKeys []string `yaml:"used_cd_keys"` + Name string `yaml:"name"` + Window WindowConfig `yaml:"window"` } type WindowConfig struct { diff --git a/backend/internal/config/config_defaults.go b/backend/internal/config/config_defaults.go index 49b2abd3..0e368333 100644 --- a/backend/internal/config/config_defaults.go +++ b/backend/internal/config/config_defaults.go @@ -65,15 +65,6 @@ func normalizeConfig(config *Config) { if config.App.Window.MinHeight <= 0 { config.App.Window.MinHeight = defaultConfig.App.Window.MinHeight } - if config.App.UsedCDKeys == nil { - config.App.UsedCDKeys = []string{} - } - - expectedLimit := MinimumProfileLimitForUsedKeys(config.App.UsedCDKeys) - if config.App.MaxProfileLimit < expectedLimit { - config.App.MaxProfileLimit = expectedLimit - } - if config.Runtime.MaxMemoryMB <= 0 { config.Runtime.MaxMemoryMB = defaultConfig.Runtime.MaxMemoryMB } @@ -257,8 +248,6 @@ func DefaultConfig() *Config { MinWidth: 1200, MinHeight: 700, }, - MaxProfileLimit: DefaultMaxProfileLimit, - UsedCDKeys: []string{}, }, Runtime: RuntimeConfig{ MaxMemoryMB: 0, diff --git a/backend/license_state.go b/backend/license_state.go deleted file mode 100644 index c16828e5..00000000 --- a/backend/license_state.go +++ /dev/null @@ -1,174 +0,0 @@ -package backend - -import ( - appconfig "ant-chrome/backend/internal/config" - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" -) - -const localLicenseStateFilename = ".ant-license.json" - -type localLicenseState struct { - MaxProfileLimit int `json:"maxProfileLimit"` - UsedCDKeys []string `json:"usedCdKeys,omitempty"` -} - -func localLicenseStatePath(configPath string) string { - configPath = strings.TrimSpace(configPath) - if configPath == "" { - return localLicenseStateFilename - } - dir := filepath.Dir(configPath) - if dir == "." || dir == "" { - if cwd, err := os.Getwd(); err == nil { - dir = cwd - } - } - return filepath.Join(dir, localLicenseStateFilename) -} - -func loadLocalLicenseState(configPath string) (*localLicenseState, bool, error) { - statePath := localLicenseStatePath(configPath) - data, err := os.ReadFile(statePath) - if err != nil { - if os.IsNotExist(err) { - return &localLicenseState{}, false, nil - } - return nil, false, fmt.Errorf("读取本机额度状态失败: %w", err) - } - - var state localLicenseState - if err := json.Unmarshal(data, &state); err != nil { - // 状态文件损坏时回退到当前配置并在后续自动重建,避免阻断启动。 - return &localLicenseState{}, false, nil - } - normalizeLocalLicenseState(&state) - return &state, true, nil -} - -func saveLocalLicenseState(configPath string, state *localLicenseState) error { - if state == nil { - state = &localLicenseState{} - } - cloned := *state - normalizeLocalLicenseState(&cloned) - - data, err := json.MarshalIndent(cloned, "", " ") - if err != nil { - return fmt.Errorf("序列化本机额度状态失败: %w", err) - } - if err := os.WriteFile(localLicenseStatePath(configPath), data, 0644); err != nil { - return fmt.Errorf("写入本机额度状态失败: %w", err) - } - return nil -} - -func reconcileConfigWithLocalLicense(configPath string, cfg *Config) (bool, bool, error) { - if cfg == nil { - return false, false, nil - } - - state, stateExists, err := loadLocalLicenseState(configPath) - if err != nil { - return false, false, err - } - - originalKeys := normalizeUsedCDKeys(cfg.App.UsedCDKeys) - originalMax := cfg.App.MaxProfileLimit - - mergedKeys := unionUsedCDKeys(originalKeys, state.UsedCDKeys) - effectiveMax := maxInt(originalMax, state.MaxProfileLimit) - minLimit := appconfig.MinimumProfileLimitForUsedKeys(mergedKeys) - if effectiveMax < minLimit { - effectiveMax = minLimit - } - - cfg.App.UsedCDKeys = mergedKeys - cfg.App.MaxProfileLimit = effectiveMax - - configChanged := originalMax != effectiveMax || !sameStringSlice(originalKeys, mergedKeys) - - desiredState := &localLicenseState{ - MaxProfileLimit: effectiveMax, - UsedCDKeys: mergedKeys, - } - normalizeLocalLicenseState(desiredState) - - stateChanged := state.MaxProfileLimit != desiredState.MaxProfileLimit || !sameStringSlice(state.UsedCDKeys, desiredState.UsedCDKeys) - shouldPersist := stateExists || desiredState.MaxProfileLimit > DefaultConfig().App.MaxProfileLimit || len(desiredState.UsedCDKeys) > 0 - if shouldPersist && stateChanged { - if err := saveLocalLicenseState(configPath, desiredState); err != nil { - return configChanged, false, err - } - return configChanged, true, nil - } - - return configChanged, false, nil -} - -func normalizeLocalLicenseState(state *localLicenseState) { - if state == nil { - return - } - state.UsedCDKeys = normalizeUsedCDKeys(state.UsedCDKeys) - minLimit := appconfig.MinimumProfileLimitForUsedKeys(state.UsedCDKeys) - if state.MaxProfileLimit < minLimit { - state.MaxProfileLimit = minLimit - } -} - -func normalizeUsedCDKeys(keys []string) []string { - result := make([]string, 0, len(keys)) - seen := make(map[string]struct{}, len(keys)) - for _, key := range keys { - normalized := strings.ToUpper(strings.TrimSpace(key)) - if normalized == "" { - continue - } - if _, ok := seen[normalized]; ok { - continue - } - seen[normalized] = struct{}{} - result = append(result, normalized) - } - return result -} - -func unionUsedCDKeys(primary, secondary []string) []string { - result := make([]string, 0, len(primary)+len(secondary)) - seen := make(map[string]struct{}, len(primary)+len(secondary)) - appendKeys := func(list []string) { - for _, key := range normalizeUsedCDKeys(list) { - if _, ok := seen[key]; ok { - continue - } - seen[key] = struct{}{} - result = append(result, key) - } - } - appendKeys(primary) - appendKeys(secondary) - return result -} - -func sameStringSlice(a, b []string) bool { - if len(a) != len(b) { - return false - } - for i := range a { - if a[i] != b[i] { - return false - } - } - return true -} - -func maxInt(a, b int) int { - if a > b { - return a - } - return b -} diff --git a/config.yaml b/config.yaml index 3870733f..6fa3dd36 100644 --- a/config.yaml +++ b/config.yaml @@ -9,9 +9,6 @@ app: height: 1000 min_width: 1200 min_height: 700 - max_profile_limit: 70 - used_cd_keys: - - GITHUB_STAR_REWARD runtime: max_memory_mb: 0 gc_percent: 100 diff --git a/frontend/src/modules/browser/api.ts b/frontend/src/modules/browser/api.ts index 4489847d..a1467505 100644 --- a/frontend/src/modules/browser/api.ts +++ b/frontend/src/modules/browser/api.ts @@ -11,3 +11,4 @@ export * from './api/groups' export * from './api/launch' export * from './api/automationDemo' export * from './api/filesystem' +export * from './api/backup' diff --git a/frontend/src/modules/browser/api/backup.ts b/frontend/src/modules/browser/api/backup.ts new file mode 100644 index 00000000..0e32be1b --- /dev/null +++ b/frontend/src/modules/browser/api/backup.ts @@ -0,0 +1,36 @@ +import { getBindings } from './runtime' + +export interface BrowserBackupActionResult { + cancelled?: boolean + message?: string + zipPath?: string + resetFirst?: boolean + imported?: number + skipped?: number + conflicts?: number + partial?: boolean + componentTotal?: number + componentSuccess?: number + componentFailed?: number + failedComponents?: Array<{ + componentId?: string + componentName?: string + error?: string + }> +} + +export async function exportFullBrowserBackup(): Promise { + const bindings: any = await getBindings() + if (!bindings?.BackupExportPackage) { + return { cancelled: false, message: '当前环境不支持全量备份' } + } + return (await bindings.BackupExportPackage()) || {} +} + +export async function importFullBrowserBackup(resetFirst: boolean): Promise { + const bindings: any = await getBindings() + if (!bindings?.BackupImportPackage) { + return { cancelled: false, message: '当前环境不支持导入备份' } + } + return (await bindings.BackupImportPackage(resetFirst)) || {} +} diff --git a/frontend/src/modules/browser/api/cores.ts b/frontend/src/modules/browser/api/cores.ts index fffa4553..f1750b13 100644 --- a/frontend/src/modules/browser/api/cores.ts +++ b/frontend/src/modules/browser/api/cores.ts @@ -80,6 +80,15 @@ export async function BrowserCoreDownload(coreName: string, url: string, proxyCo return true } +export async function redownloadBrowserCore(coreId: string, url: string, proxyConfig?: string): Promise { + const bindings: any = await getBindings() + if (bindings?.BrowserCoreRedownload) { + await bindings.BrowserCoreRedownload(coreId, url, proxyConfig || '') + return true + } + return true +} + export async function openCorePath(corePath: string): Promise { const bindings: any = await getBindings() if (bindings?.OpenCorePath) { diff --git a/frontend/src/modules/browser/components/BrowserBackupModal.tsx b/frontend/src/modules/browser/components/BrowserBackupModal.tsx new file mode 100644 index 00000000..46556372 --- /dev/null +++ b/frontend/src/modules/browser/components/BrowserBackupModal.tsx @@ -0,0 +1,102 @@ +import { AlertTriangle, DatabaseBackup, Download, Upload } from 'lucide-react' + +import { Button, Modal } from '../../../shared/components' + +type BackupMode = 'export' | 'import-merge' | 'import-reset' | 'none' + +interface BrowserBackupModalProps { + open: boolean + runningCount: number + selectedCount: number + selectedExporting: boolean + loadingMode: BackupMode + onClose: () => void + onExportSelected: () => void + onExportFull: () => void + onImportMerge: () => void + onImportReset: () => void +} + +export function BrowserBackupModal({ + open, + runningCount, + selectedCount, + selectedExporting, + loadingMode, + onClose, + onExportSelected, + onExportFull, + onImportMerge, + onImportReset, +}: BrowserBackupModalProps) { + const busy = loadingMode !== 'none' || selectedExporting + + return ( + { + if (!busy) onClose() + }} + title="备份与导入" + width="560px" + closable={!busy} + footer={( + <> + + + + + + + )} + > +
+
+
+ + 全量备份范围 +
+
+ 实例名称 / 分组 / 标签 + 代理池 / 订阅 / 测速结果 + 内核配置 / 内核文件 + 应用书签 / 插件配置 + 实例浏览器数据目录 + Cookie / LocalStorage / IndexedDB +
+
+ +
+

备份选中:导出当前选中的 {selectedCount} 个实例及浏览器用户数据,适合迁移少量实例。

+

完整灾备请用全量备份,它会额外包含完整代理池、内核、数据库和应用级配置。

+
+ + {runningCount > 0 && ( +
+ + 当前有 {runningCount} 个实例运行中。建议先停止实例再备份,否则 Cookie、数据库和缓存文件可能未完整落盘。 +
+ )} + +
+

登录态会随浏览器用户数据一起打包,但 Windows 加密信息可能绑定当前系统用户。

+

同机同用户恢复成功率最高;跨机器、重装系统或换 Windows 用户时,Cookie 和密码可能无法解密。

+
+ +
+

全量备份:导出配置、数据库、代理、内核、实例浏览器数据。

+

合并导入:保留当前数据,按 ID、路径、URL 等规则跳过重复项。

+

清空恢复:先初始化当前数据,再从备份包完整恢复。

+
+
+
+ ) +} diff --git a/frontend/src/modules/browser/components/BrowserListLayout.tsx b/frontend/src/modules/browser/components/BrowserListLayout.tsx index ed7f7bbc..51c90e54 100644 --- a/frontend/src/modules/browser/components/BrowserListLayout.tsx +++ b/frontend/src/modules/browser/components/BrowserListLayout.tsx @@ -1,7 +1,7 @@ import { Link } from 'react-router-dom' -import { Activity, CheckCircle, ChevronRight, ChevronUp, Edit2, FileText, Gift, LayoutGrid, List, Play, Plus, RefreshCw, Sliders, Square, Star, Trash2, Upload, XCircle } from 'lucide-react' +import { Archive, CheckCircle, ChevronRight, ChevronUp, Edit2, LayoutGrid, List, Play, Plus, RefreshCw, Sliders, Star, Trash2, Upload, XCircle } from 'lucide-react' -import { Button, Card, FormItem, Input, Modal, StatCard, Switch, Table, Textarea } from '../../../shared/components' +import { Button, Card, FormItem, Input, Modal, Switch, Table, Textarea } from '../../../shared/components' import type { TableColumn } from '../../../shared/components/Table' import type { BrowserCore, BrowserCoreInput, BrowserGroupWithCount, BrowserProxy, BrowserSettings } from '../types' @@ -25,9 +25,9 @@ interface BrowserListHeaderProps { onToggleHeaderCollapsed: () => void onRefresh: () => void onOpenSettings: () => void - onOpenExpandModal: () => void onOpenTrash: () => void onImportProfiles: () => void + onOpenBackup: () => void importingProfiles?: boolean onViewModeChange: (next: BrowserViewMode) => void } @@ -47,25 +47,42 @@ export function BrowserListHeader({ onToggleHeaderCollapsed, onRefresh, onOpenSettings, - onOpenExpandModal, onOpenTrash, onImportProfiles, + onOpenBackup, importingProfiles = false, onViewModeChange, }: BrowserListHeaderProps) { + const statItems = [ + { label: '总数', value: profileCount }, + { label: '运行', value: runningCount }, + { label: '停止', value: Math.max(0, profileCount - runningCount) }, + ] + return ( <> -
-
+
+

实例列表

-

- 当前配置总数 {profileCount} +

+ {statItems.map((item) => ( +
+ {item.label} + {item.value} +
+ ))} {filteredProfileCount !== profileCount && ( - (已筛选 {filteredProfileCount}) +
+ 筛选 + {filteredProfileCount} +
)} -

+
-
+
-
- {!headerCollapsed && ( - <> -
- } /> - } /> - } /> -
- - - + )} ) diff --git a/frontend/src/modules/browser/components/BrowserListWidgets.tsx b/frontend/src/modules/browser/components/BrowserListWidgets.tsx index be74ca6b..deda5b22 100644 --- a/frontend/src/modules/browser/components/BrowserListWidgets.tsx +++ b/frontend/src/modules/browser/components/BrowserListWidgets.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react' -import { ChevronDown, ChevronUp, Copy, Download, Pencil, Play, RefreshCw, Square, Trash2 } from 'lucide-react' +import { Archive, ChevronDown, ChevronUp, Copy, Download, Pencil, Play, RefreshCw, Square, Trash2 } from 'lucide-react' import { Button, toast } from '../../../shared/components' import { regenerateBrowserProfileCode, setBrowserProfileCode } from '../api' @@ -12,6 +12,7 @@ interface BatchToolbarProps { onBatchStart: () => void onBatchStop: () => void onBatchExport: () => void + onOpenBackup: () => void onBatchDelete: () => void batchLoading: boolean exporting?: boolean @@ -25,6 +26,7 @@ export function BatchToolbar({ onBatchStart, onBatchStop, onBatchExport, + onOpenBackup, onBatchDelete, batchLoading, exporting = false, @@ -46,6 +48,9 @@ export function BatchToolbar({ + + {!record.isDefault && (
- +
diff --git a/frontend/src/modules/browser/pages/browserList/BrowserListDialogs.tsx b/frontend/src/modules/browser/pages/browserList/BrowserListDialogs.tsx index 7ae2e34c..64dcf50b 100644 --- a/frontend/src/modules/browser/pages/browserList/BrowserListDialogs.tsx +++ b/frontend/src/modules/browser/pages/browserList/BrowserListDialogs.tsx @@ -1,5 +1,5 @@ import { Link } from 'react-router-dom' -import { ExternalLink, XCircle } from 'lucide-react' +import { XCircle } from 'lucide-react' import { Button, Modal } from '../../../../shared/components' import { BrowserProfileCopyForm } from '../../components/BrowserProfileCopyForm' import { KeywordsModal } from '../../components/KeywordsModal' @@ -15,12 +15,6 @@ interface BrowserListDialogsProps { kwModal: { open: boolean; profile: BrowserProfile | null } onCloseKeywords: () => void onKeywordsSaved: (keywords: string[]) => void - expandModalOpen: boolean - onCloseExpand: () => void - profilesCount: number - maxProfileLimit: number - redeeming: boolean - onOpenGithubStarGift: () => void copyModal: { open: boolean; profile: BrowserProfile | null } copyName: string copyOptions: BrowserProfileCopyOptions @@ -59,12 +53,6 @@ export function BrowserListDialogs({ kwModal, onCloseKeywords, onKeywordsSaved, - expandModalOpen, - onCloseExpand, - profilesCount, - maxProfileLimit, - redeeming, - onOpenGithubStarGift, copyModal, copyName, copyOptions, @@ -149,45 +137,6 @@ export function BrowserListDialogs({ /> )} - 关闭} - > -
-
-
-

当前容量

-

每个配置消耗 1 个实例额度

-
-
- = maxProfileLimit ? 'text-red-500' : 'text-[var(--color-accent)]'}`}> - {profilesCount} - - / {maxProfileLimit} -
-
- -
-
-

点亮 GitHub Star 后领取 50 个永久额度

- -
-
-
-
- void loadCores: () => void } -export function useBrowserListData({ loadQuota, loadCores }: UseBrowserListDataOptions) { +export function useBrowserListData({ loadCores }: UseBrowserListDataOptions) { const [profiles, setProfiles] = useState([]) const [loading, setLoading] = useState(true) const [proxies, setProxies] = useState([]) @@ -103,7 +102,6 @@ export function useBrowserListData({ loadQuota, loadCores }: UseBrowserListDataO useEffect(() => { void loadProfiles() loadGroups() - loadQuota() fetchBrowserProxies().then(setProxies) loadCores() diff --git a/frontend/src/modules/browser/pages/browserList/useBrowserListSettings.ts b/frontend/src/modules/browser/pages/browserList/useBrowserListSettings.ts index 73a4e546..f2469f65 100644 --- a/frontend/src/modules/browser/pages/browserList/useBrowserListSettings.ts +++ b/frontend/src/modules/browser/pages/browserList/useBrowserListSettings.ts @@ -1,8 +1,5 @@ import { useState } from 'react' import { toast } from '../../../../shared/components' -import { PROJECT_GITHUB_URL } from '../../../../config/links' -import { BrowserOpenURL } from '../../../../wailsjs/runtime/runtime' -import { fetchDashboardStats, redeemGithubStar, reloadConfig } from '../../../dashboard/api' import type { BrowserCore, BrowserCoreInput, BrowserSettings } from '../../types' import { deleteBrowserCore, @@ -40,10 +37,6 @@ export function useBrowserListSettings() { const [coreValidation, setCoreValidation] = useState<{ valid: boolean; message: string } | null>(null) const [savingCore, setSavingCore] = useState(false) - const [expandModalOpen, setExpandModalOpen] = useState(false) - const [redeeming, setRedeeming] = useState(false) - const [maxProfileLimit, setMaxProfileLimit] = useState(20) - const loadSettings = async () => { const data = await fetchBrowserSettings() setSettings(data) @@ -56,16 +49,6 @@ export function useBrowserListSettings() { setCores(await fetchBrowserCores()) } - const loadQuota = async () => { - try { - await reloadConfig() - const stats = await fetchDashboardStats() - setMaxProfileLimit(stats.maxProfileLimit || 20) - } catch { - // ignore - } - } - const handleOpenSettings = async () => { await Promise.all([loadSettings(), loadCores()]) setSettingsModalOpen(true) @@ -142,23 +125,6 @@ export function useBrowserListSettings() { loadCores() } - const handleClaimStarGift = async () => { - setRedeeming(true) - const starRes = await redeemGithubStar() - setRedeeming(false) - if (starRes.success) { - toast.success('感谢您的支持!已额外赠送 50 个永久额度!') - loadQuota() - } else { - toast.error(starRes.message || '领取失败') - } - } - - const handleOpenGithubStarGift = async () => { - BrowserOpenURL(PROJECT_GITHUB_URL) - await handleClaimStarGift() - } - return { settingsModalOpen, setSettingsModalOpen, @@ -179,12 +145,7 @@ export function useBrowserListSettings() { coreValidation, setCoreValidation, savingCore, - expandModalOpen, - setExpandModalOpen, - redeeming, - maxProfileLimit, loadCores, - loadQuota, handleOpenSettings, handleSaveSettings, handleOpenCoreModal, @@ -192,6 +153,5 @@ export function useBrowserListSettings() { handleSaveCore, handleDeleteCore, handleSetDefaultCore, - handleOpenGithubStarGift, } } diff --git a/frontend/src/modules/browser/pages/coreManagement.types.ts b/frontend/src/modules/browser/pages/coreManagement.types.ts index d4bb3b40..77d17025 100644 --- a/frontend/src/modules/browser/pages/coreManagement.types.ts +++ b/frontend/src/modules/browser/pages/coreManagement.types.ts @@ -26,10 +26,12 @@ export interface CoreEditForm { } export interface CoreDownloadForm { + coreId?: string name: string url: string proxyMode: string proxyId: string + mode?: 'download' | 'redownload' } export interface CoreDownloadProgress { diff --git a/frontend/src/modules/browser/pages/coreManagement/CoreDownloadModal.tsx b/frontend/src/modules/browser/pages/coreManagement/CoreDownloadModal.tsx index 06ed3ee7..cb2055a8 100644 --- a/frontend/src/modules/browser/pages/coreManagement/CoreDownloadModal.tsx +++ b/frontend/src/modules/browser/pages/coreManagement/CoreDownloadModal.tsx @@ -26,6 +26,7 @@ export function CoreDownloadModal({ onStart, }: CoreDownloadModalProps) { const downloading = progress !== null && progress.phase !== 'error' + const isRedownload = form.mode === 'redownload' const handleClose = () => { if (progress && progress.phase !== 'done' && progress.phase !== 'error') { @@ -40,12 +41,12 @@ export function CoreDownloadModal({ - + } > @@ -55,10 +56,19 @@ export function CoreDownloadModal({ value={form.name} onChange={e => setForm(prev => ({ ...prev, name: e.target.value }))} placeholder="例如: chrome-139" - disabled={progress !== null} + disabled={progress !== null || isRedownload} /> -

该名称将同时作为数据存放的子文件夹名。

+ {!isRedownload && ( +

该名称将同时作为数据存放的子文件夹名。

+ )} + + {isRedownload && ( +
+ 重新下载会在校验新压缩包可用后替换当前内核目录;替换失败会自动恢复旧目录。正在使用该内核的实例请先停止。 +
+ )} + { @@ -83,26 +79,7 @@ export function DashboardPage() { } } - const handleClaimStarGift = async () => { - setRedeeming(true) - const starRes = await redeemGithubStar() - setRedeeming(false) - if (starRes.success) { - toast.success('感谢您的支持!已额外赠送 50 个永久额度!') - load({ reloadFirst: true }) - } else { - toast.error(starRes.message || '领取失败') - } - } - - const handleOpenGithubStarGift = async () => { - BrowserOpenURL(PROJECT_GITHUB_URL) - await handleClaimStarGift() - } - const v = (n: number) => loading ? '-' : n.toString() - const capacityText = loading ? '-' : `${stats.totalInstances} / ${stats.maxProfileLimit}` - const capacityFull = !loading && stats.totalInstances >= stats.maxProfileLimit return (
@@ -182,33 +159,6 @@ export function DashboardPage() {
))}
- -
-

扩容系统

-
-
-
-

当前容量

-

- {capacityText} -

-
- -
-

- 点亮 GitHub Star 后领取 50 个永久额度 -

-
-
diff --git a/frontend/src/modules/dashboard/api.ts b/frontend/src/modules/dashboard/api.ts index 082d828f..0387a10f 100644 --- a/frontend/src/modules/dashboard/api.ts +++ b/frontend/src/modules/dashboard/api.ts @@ -1,4 +1,4 @@ -import type { DashboardStats } from './types' +import type { DashboardStats } from './types' const getBindings = async () => { try { @@ -13,42 +13,19 @@ export async function fetchDashboardStats(): Promise { if (bindings?.GetDashboardStats) { try { const data = await bindings.GetDashboardStats() - let maxProfileLimit = Number(data?.maxProfileLimit) || 0 - if (!maxProfileLimit && bindings.GetLicenseStatus) { - try { - const licenseStatus = await bindings.GetLicenseStatus() - maxProfileLimit = Number(licenseStatus?.maxLimit) || 0 - } catch { - maxProfileLimit = 0 - } - } return { totalInstances: data?.totalInstances ?? 0, runningInstances: data?.runningInstances ?? 0, proxyCount: data?.proxyCount ?? 0, coreCount: data?.coreCount ?? 0, memUsedMB: data?.memUsedMB ?? 0, - maxProfileLimit: maxProfileLimit || 20, appVersion: data?.appVersion ?? 'unknown', } } catch (e) { console.error('fetchDashboardStats error:', e) } } - return { totalInstances: 0, runningInstances: 0, proxyCount: 0, coreCount: 0, memUsedMB: 0, maxProfileLimit: 20, appVersion: 'unknown' } -} - -export async function redeemGithubStar(): Promise<{ success: boolean, message?: string }> { - const bindings: any = await getBindings() - if (bindings?.RedeemGithubStar) { - try { - await bindings.RedeemGithubStar() - return { success: true } - } catch (e: any) { - return { success: false, message: e.message || '领取失败' } - } - } - return { success: false, message: '系统 API 未就绪' } + return { totalInstances: 0, runningInstances: 0, proxyCount: 0, coreCount: 0, memUsedMB: 0, appVersion: 'unknown' } } export async function reloadConfig(): Promise { @@ -61,16 +38,3 @@ export async function reloadConfig(): Promise { } } } - -export async function generateCDKeys(count: number): Promise<{ success: boolean, keys: string[], message?: string }> { - const bindings: any = await getBindings() - if (bindings?.GenerateCDKeys) { - try { - const keys = await bindings.GenerateCDKeys(count) - return { success: true, keys: keys || [] } - } catch (e: any) { - return { success: false, keys: [], message: e.message || '生成失败' } - } - } - return { success: false, keys: [], message: '系统 API 未就绪' } -} diff --git a/frontend/src/modules/dashboard/types.ts b/frontend/src/modules/dashboard/types.ts index 3befe2b3..f7e4e338 100644 --- a/frontend/src/modules/dashboard/types.ts +++ b/frontend/src/modules/dashboard/types.ts @@ -4,6 +4,5 @@ export interface DashboardStats { proxyCount: number coreCount: number memUsedMB: number - maxProfileLimit: number appVersion: string } diff --git a/frontend/src/modules/profile/AdminKeygenPage.tsx b/frontend/src/modules/profile/AdminKeygenPage.tsx deleted file mode 100644 index 56df1389..00000000 --- a/frontend/src/modules/profile/AdminKeygenPage.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { FormEvent, useState } from 'react' -import { useNavigate } from 'react-router-dom' -import { Card, Button, Input, Modal, toast, Textarea } from '../../shared/components' -import { Key } from 'lucide-react' -import { generateCDKeys } from '../dashboard/api' - -const ADMIN_PAGE_PASSWORD = '志字辈小蚂蚁' - -export function AdminKeygenPage() { - const navigate = useNavigate() - const [count, setCount] = useState(10) - const [keys, setKeys] = useState([]) - const [loading, setLoading] = useState(false) - const [accessGranted, setAccessGranted] = useState(false) - const [passwordInput, setPasswordInput] = useState('') - - const handleGenerate = async () => { - if (count <= 0 || count > 1000) { - toast.error('生成数量必须在 1 ~ 1000 之间') - return - } - - setLoading(true) - const res = await generateCDKeys(count) - setLoading(false) - - if (res.success) { - setKeys(res.keys) - toast.success(`成功生成 ${res.keys.length} 个兑换码`) - } else { - toast.error(res.message || '生成失败') - } - } - - const handleCopyAll = async () => { - if (keys.length === 0) return - try { - await navigator.clipboard.writeText(keys.join('\n')) - toast.success('已复制全部兑换码到剪贴板') - } catch { - toast.error('复制失败,请手动选择复制') - } - } - - const handleVerifyPassword = (event: FormEvent) => { - event.preventDefault() - - if (passwordInput.trim() === ADMIN_PAGE_PASSWORD) { - setAccessGranted(true) - setPasswordInput('') - toast.success('验证通过,已进入兑换码生成页面') - return - } - - toast.error('密码错误,请重试') - setPasswordInput('') - } - - return ( - <> - navigate('/profile')} - title="管理员验证" - width="420px" - closable={false} - > -
-

- 请输入访问密码后继续。 -

- setPasswordInput(e.target.value)} - placeholder="请输入密码" - autoFocus - autoComplete="off" - inputMode="text" - spellCheck={false} - /> -
- - -
-
-
- - {accessGranted && ( -
-
-

系统核心管理 - CDKey 生成器

-

隐藏管理员工具。生成的每个兑换码均可为客户端增加 10 个永久额度。

-
- - -
-
-
- - setCount(parseInt(e.target.value) || 0)} - placeholder="10" - /> -
- -
- -
-
- 生成结果 - -
-