mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
channel: master version: 1.1.0 source-ref: master published-at-utc: 2026-03-20T14:14:07Z
502 lines
15 KiB
Go
502 lines
15 KiB
Go
package browser
|
||
|
||
import (
|
||
"ant-chrome/backend/internal/logger"
|
||
"fmt"
|
||
"os/exec"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
// InitData 初始化浏览器数据
|
||
func (m *Manager) InitData() {
|
||
m.Mutex.Lock()
|
||
defer m.Mutex.Unlock()
|
||
if m.Profiles == nil {
|
||
m.Profiles = make(map[string]*Profile)
|
||
}
|
||
if m.BrowserProcesses == nil {
|
||
m.BrowserProcesses = make(map[string]*exec.Cmd)
|
||
}
|
||
if m.XrayBridges == nil {
|
||
m.XrayBridges = make(map[string]*XrayBridge)
|
||
}
|
||
// 执行配置迁移
|
||
m.MigrateConfig()
|
||
if len(m.Profiles) > 0 {
|
||
return
|
||
}
|
||
m.loadProfiles()
|
||
}
|
||
|
||
func (m *Manager) loadProfiles() {
|
||
log := logger.New("Browser")
|
||
|
||
// 优先从 DAO(SQLite)加载
|
||
if m.ProfileDAO != nil {
|
||
profiles, err := m.ProfileDAO.List()
|
||
if err != nil {
|
||
log.Error("从数据库加载实例配置失败", logger.F("error", err))
|
||
} else {
|
||
// SQLite 模式:无论是否为空都直接使用,不自动创建默认实例
|
||
for _, p := range profiles {
|
||
p.CoreId = normalizeProfileCoreID(p.CoreId)
|
||
m.Profiles[p.ProfileId] = p
|
||
}
|
||
if len(profiles) > 0 {
|
||
log.Info("实例配置从数据库加载完成", logger.F("count", len(profiles)))
|
||
} else {
|
||
log.Info("实例表为空,用户可手动创建新实例")
|
||
}
|
||
return
|
||
}
|
||
}
|
||
|
||
// 降级:从 config.yaml 加载(仅在无 SQLite 时使用)
|
||
if len(m.Config.Browser.Profiles) == 0 {
|
||
// 不自动创建默认实例,保持空列表
|
||
log.Info("实例配置为空,用户可手动创建新实例")
|
||
return
|
||
}
|
||
now := time.Now().Format(time.RFC3339)
|
||
for _, item := range m.Config.Browser.Profiles {
|
||
profileId := strings.TrimSpace(item.ProfileId)
|
||
if profileId == "" {
|
||
continue
|
||
}
|
||
createdAt := strings.TrimSpace(item.CreatedAt)
|
||
if createdAt == "" {
|
||
createdAt = now
|
||
}
|
||
updatedAt := strings.TrimSpace(item.UpdatedAt)
|
||
if updatedAt == "" {
|
||
updatedAt = createdAt
|
||
}
|
||
m.Profiles[profileId] = &Profile{
|
||
ProfileId: profileId,
|
||
ProfileName: item.ProfileName,
|
||
UserDataDir: item.UserDataDir,
|
||
CoreId: normalizeProfileCoreID(item.CoreId),
|
||
FingerprintArgs: append([]string{}, item.FingerprintArgs...),
|
||
ProxyId: item.ProxyId,
|
||
ProxyConfig: item.ProxyConfig,
|
||
ProxyBindSourceID: item.ProxyBindSourceID,
|
||
ProxyBindSourceURL: item.ProxyBindSourceURL,
|
||
ProxyBindName: item.ProxyBindName,
|
||
ProxyBindUpdatedAt: item.ProxyBindUpdatedAt,
|
||
LaunchArgs: append([]string{}, item.LaunchArgs...),
|
||
Tags: append([]string{}, item.Tags...),
|
||
Keywords: append([]string{}, item.Keywords...),
|
||
Running: false,
|
||
DebugPort: 0,
|
||
Pid: 0,
|
||
LastError: "",
|
||
CreatedAt: createdAt,
|
||
UpdatedAt: updatedAt,
|
||
}
|
||
}
|
||
log.Info("浏览器配置从文件加载完成", logger.F("count", len(m.Profiles)))
|
||
}
|
||
|
||
// SaveProfiles 保存所有实例配置(DAO 模式:逐条 upsert)
|
||
func (m *Manager) SaveProfiles() error {
|
||
log := logger.New("Browser")
|
||
if m.ProfileDAO != nil {
|
||
for _, profile := range m.Profiles {
|
||
profile.CoreId = normalizeProfileCoreID(profile.CoreId)
|
||
if err := m.ProfileDAO.Upsert(profile); err != nil {
|
||
log.Error("实例配置持久化失败", logger.F("profile_id", profile.ProfileId), logger.F("error", err))
|
||
return err
|
||
}
|
||
}
|
||
log.Info("实例配置持久化成功", logger.F("count", len(m.Profiles)))
|
||
return nil
|
||
}
|
||
|
||
// 降级:写回 config.yaml
|
||
profiles := make([]ProfileConfig, 0, len(m.Profiles))
|
||
for _, profile := range m.Profiles {
|
||
profiles = append(profiles, ProfileConfig{
|
||
ProfileId: profile.ProfileId,
|
||
ProfileName: profile.ProfileName,
|
||
UserDataDir: profile.UserDataDir,
|
||
CoreId: normalizeProfileCoreID(profile.CoreId),
|
||
FingerprintArgs: append([]string{}, profile.FingerprintArgs...),
|
||
ProxyId: profile.ProxyId,
|
||
ProxyConfig: profile.ProxyConfig,
|
||
ProxyBindSourceID: profile.ProxyBindSourceID,
|
||
ProxyBindSourceURL: profile.ProxyBindSourceURL,
|
||
ProxyBindName: profile.ProxyBindName,
|
||
ProxyBindUpdatedAt: profile.ProxyBindUpdatedAt,
|
||
LaunchArgs: append([]string{}, profile.LaunchArgs...),
|
||
Tags: append([]string{}, profile.Tags...),
|
||
Keywords: append([]string{}, profile.Keywords...),
|
||
CreatedAt: profile.CreatedAt,
|
||
UpdatedAt: profile.UpdatedAt,
|
||
})
|
||
}
|
||
m.Config.Browser.Profiles = profiles
|
||
if err := m.Config.Save(m.ResolveRelativePath("config.yaml")); err != nil {
|
||
log.Error("浏览器配置持久化失败", logger.F("error", err))
|
||
return err
|
||
}
|
||
log.Info("浏览器配置持久化成功(文件)", logger.F("count", len(profiles)))
|
||
return nil
|
||
}
|
||
|
||
// List 获取配置列表
|
||
func (m *Manager) List() []Profile {
|
||
log := logger.New("Browser")
|
||
m.InitData()
|
||
m.Mutex.Lock()
|
||
defer m.Mutex.Unlock()
|
||
list := make([]Profile, 0, len(m.Profiles))
|
||
for _, profile := range m.Profiles {
|
||
p := *profile
|
||
if m.CodeProvider != nil {
|
||
if code, err := m.CodeProvider.EnsureCode(p.ProfileId); err == nil {
|
||
p.LaunchCode = code
|
||
}
|
||
}
|
||
list = append(list, p)
|
||
}
|
||
// 按 ProfileId 排序,保持稳定顺序
|
||
sort.Slice(list, func(i, j int) bool {
|
||
return list[i].ProfileId < list[j].ProfileId
|
||
})
|
||
log.Info("浏览器配置列表查询", logger.F("count", len(list)))
|
||
return list
|
||
}
|
||
|
||
// ListByTag 按标签筛选配置列表
|
||
func (m *Manager) ListByTag(tag string) []Profile {
|
||
tag = strings.TrimSpace(tag)
|
||
all := m.List()
|
||
if tag == "" {
|
||
return all
|
||
}
|
||
result := make([]Profile, 0)
|
||
for _, p := range all {
|
||
for _, t := range p.Tags {
|
||
if strings.EqualFold(t, tag) {
|
||
result = append(result, p)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
return result
|
||
}
|
||
|
||
// GetAllTags 获取所有已使用的标签(去重排序)
|
||
func (m *Manager) GetAllTags() []string {
|
||
m.InitData()
|
||
m.Mutex.Lock()
|
||
defer m.Mutex.Unlock()
|
||
seen := make(map[string]struct{})
|
||
for _, p := range m.Profiles {
|
||
for _, t := range p.Tags {
|
||
t = strings.TrimSpace(t)
|
||
if t != "" {
|
||
seen[t] = struct{}{}
|
||
}
|
||
}
|
||
}
|
||
tags := make([]string, 0, len(seen))
|
||
for t := range seen {
|
||
tags = append(tags, t)
|
||
}
|
||
sort.Strings(tags)
|
||
return tags
|
||
}
|
||
|
||
// Create 创建配置
|
||
func (m *Manager) Create(input ProfileInput) (*Profile, error) {
|
||
log := logger.New("Browser")
|
||
m.InitData()
|
||
m.Mutex.Lock()
|
||
defer m.Mutex.Unlock()
|
||
|
||
// Check Profile Limit
|
||
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)
|
||
if userDataDir == "" {
|
||
userDataDir = profileId
|
||
}
|
||
proxyConfig := strings.TrimSpace(input.ProxyConfig)
|
||
proxyId := strings.TrimSpace(input.ProxyId)
|
||
selectedProxy := Proxy{}
|
||
hasSelectedProxy := false
|
||
if proxyId != "" {
|
||
if proxyItem, ok := m.GetProxyByID(proxyId); ok {
|
||
proxyConfig = strings.TrimSpace(proxyItem.ProxyConfig)
|
||
selectedProxy = proxyItem
|
||
hasSelectedProxy = true
|
||
} else {
|
||
log.Error("代理绑定失败", logger.F("profile_id", profileId), logger.F("proxy_id", proxyId))
|
||
}
|
||
}
|
||
coreId := normalizeProfileCoreID(input.CoreId)
|
||
if coreId == "" {
|
||
if defaultCore, ok := m.GetDefaultCore(); ok {
|
||
coreId = defaultCore.CoreId
|
||
}
|
||
}
|
||
if proxyConfig == "" && m.Config.Browser.DefaultProxy != "" {
|
||
proxyConfig = m.Config.Browser.DefaultProxy
|
||
}
|
||
profile := &Profile{
|
||
ProfileId: profileId,
|
||
ProfileName: input.ProfileName,
|
||
UserDataDir: userDataDir,
|
||
CoreId: coreId,
|
||
FingerprintArgs: input.FingerprintArgs,
|
||
ProxyId: proxyId,
|
||
ProxyConfig: proxyConfig,
|
||
LaunchArgs: input.LaunchArgs,
|
||
Tags: input.Tags,
|
||
Keywords: append([]string{}, input.Keywords...),
|
||
GroupId: strings.TrimSpace(input.GroupId),
|
||
Running: false,
|
||
DebugPort: 0,
|
||
Pid: 0,
|
||
LastError: "",
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
}
|
||
if hasSelectedProxy {
|
||
_ = BindProfileToProxy(profile, selectedProxy, true)
|
||
}
|
||
m.Profiles[profileId] = profile
|
||
log.Info("浏览器配置创建", logger.F("profile_id", profileId), logger.F("profile_name", input.ProfileName))
|
||
if err := m.SaveProfiles(); err != nil {
|
||
return nil, err
|
||
}
|
||
if m.CodeProvider != nil {
|
||
if code, err := m.CodeProvider.EnsureCode(profile.ProfileId); err == nil {
|
||
profile.LaunchCode = code
|
||
}
|
||
}
|
||
return profile, nil
|
||
}
|
||
|
||
// Update 更新配置
|
||
func (m *Manager) Update(profileId string, input ProfileInput) (*Profile, error) {
|
||
log := logger.New("Browser")
|
||
m.InitData()
|
||
m.Mutex.Lock()
|
||
defer m.Mutex.Unlock()
|
||
profile, exists := m.Profiles[profileId]
|
||
if !exists {
|
||
log.Error("浏览器配置不存在", logger.F("profile_id", profileId))
|
||
return nil, fmt.Errorf("profile not found")
|
||
}
|
||
profile.ProfileName = input.ProfileName
|
||
profile.UserDataDir = input.UserDataDir
|
||
profile.CoreId = normalizeProfileCoreID(input.CoreId)
|
||
profile.FingerprintArgs = input.FingerprintArgs
|
||
profile.ProxyId = strings.TrimSpace(input.ProxyId)
|
||
if profile.ProxyId != "" {
|
||
if proxyItem, ok := m.GetProxyByID(profile.ProxyId); ok {
|
||
_ = BindProfileToProxy(profile, proxyItem, true)
|
||
} else {
|
||
log.Error("代理绑定失败", logger.F("profile_id", profileId), logger.F("proxy_id", profile.ProxyId))
|
||
}
|
||
} else {
|
||
profile.ProxyConfig = input.ProxyConfig
|
||
_ = ClearProfileProxyBinding(profile)
|
||
}
|
||
profile.LaunchArgs = input.LaunchArgs
|
||
profile.Tags = input.Tags
|
||
profile.Keywords = append([]string{}, input.Keywords...)
|
||
profile.GroupId = strings.TrimSpace(input.GroupId)
|
||
profile.UpdatedAt = time.Now().Format(time.RFC3339)
|
||
log.Info("浏览器配置更新", logger.F("profile_id", profileId), logger.F("profile_name", input.ProfileName))
|
||
if err := m.SaveProfiles(); err != nil {
|
||
return nil, err
|
||
}
|
||
return profile, nil
|
||
}
|
||
|
||
// Delete 删除配置
|
||
func (m *Manager) Delete(profileId string) error {
|
||
log := logger.New("Browser")
|
||
m.InitData()
|
||
m.Mutex.Lock()
|
||
defer m.Mutex.Unlock()
|
||
if _, exists := m.Profiles[profileId]; !exists {
|
||
log.Error("浏览器配置不存在", logger.F("profile_id", profileId))
|
||
return fmt.Errorf("profile not found")
|
||
}
|
||
delete(m.Profiles, profileId)
|
||
log.Info("浏览器配置删除", logger.F("profile_id", profileId))
|
||
|
||
// DAO 删除
|
||
if m.ProfileDAO != nil {
|
||
if err := m.ProfileDAO.Delete(profileId); err != nil {
|
||
log.Error("数据库删除实例失败", logger.F("profile_id", profileId), logger.F("error", err))
|
||
return err
|
||
}
|
||
} else {
|
||
if err := m.SaveProfiles(); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
if m.CodeProvider != nil {
|
||
_ = m.CodeProvider.Remove(profileId)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ApplyDefaults 应用默认配置
|
||
func (m *Manager) ApplyDefaults(profile *Profile) bool {
|
||
log := logger.New("Browser")
|
||
if profile.FingerprintArgs == nil || len(profile.FingerprintArgs) == 0 {
|
||
profile.FingerprintArgs = append([]string{}, m.Config.Browser.DefaultFingerprintArgs...)
|
||
}
|
||
if profile.LaunchArgs == nil || len(profile.LaunchArgs) == 0 {
|
||
profile.LaunchArgs = append([]string{}, m.Config.Browser.DefaultLaunchArgs...)
|
||
}
|
||
if strings.TrimSpace(profile.UserDataDir) == "" {
|
||
profile.UserDataDir = profile.ProfileId
|
||
}
|
||
profile.CoreId = normalizeProfileCoreID(profile.CoreId)
|
||
if profile.CoreId == "" {
|
||
if defaultCore, ok := m.GetDefaultCore(); ok {
|
||
profile.CoreId = defaultCore.CoreId
|
||
}
|
||
}
|
||
proxyChanged := false
|
||
bindChanged, boundInPool, bindMode := m.ResolveProfileProxyBinding(profile)
|
||
if bindChanged {
|
||
proxyChanged = true
|
||
}
|
||
if bindMode != "" && bindMode != "proxy_id" {
|
||
log.Info("实例代理自动重关联",
|
||
logger.F("profile_id", profile.ProfileId),
|
||
logger.F("proxy_id", profile.ProxyId),
|
||
logger.F("mode", bindMode),
|
||
)
|
||
}
|
||
if profile.ProxyId != "" && !boundInPool {
|
||
if strings.TrimSpace(profile.ProxyConfig) == "" {
|
||
log.Error("实例代理未找到", logger.F("profile_id", profile.ProfileId), logger.F("proxy_id", profile.ProxyId))
|
||
} else {
|
||
log.Warn("实例代理未找到,回退使用历史代理配置", logger.F("profile_id", profile.ProfileId), logger.F("proxy_id", profile.ProxyId))
|
||
}
|
||
}
|
||
if profile.ProxyConfig == "" && m.Config.Browser.DefaultProxy != "" {
|
||
profile.ProxyConfig = m.Config.Browser.DefaultProxy
|
||
proxyChanged = true
|
||
}
|
||
return proxyChanged
|
||
}
|
||
|
||
// Copy 复制实例配置(除指纹参数外全部复制,指纹使用默认值生成新种子)
|
||
func (m *Manager) Copy(profileId string, newName string) (*Profile, error) {
|
||
log := logger.New("Browser")
|
||
m.InitData()
|
||
m.Mutex.Lock()
|
||
defer m.Mutex.Unlock()
|
||
|
||
// Check Profile Limit
|
||
if m.Config.App.MaxProfileLimit > 0 && len(m.Profiles) >= m.Config.App.MaxProfileLimit {
|
||
log.Error("复制实例失败: 达到数量上限", logger.F("limit", m.Config.App.MaxProfileLimit))
|
||
return nil, fmt.Errorf("实例数量已达上限 (%d个),无法复制实例。请兑换额度后重试!", m.Config.App.MaxProfileLimit)
|
||
}
|
||
|
||
src, exists := m.Profiles[profileId]
|
||
if !exists {
|
||
log.Error("源实例不存在", logger.F("profile_id", profileId))
|
||
return nil, fmt.Errorf("profile not found")
|
||
}
|
||
|
||
now := time.Now().Format(time.RFC3339)
|
||
newId := uuid.NewString()
|
||
|
||
// 处理名称
|
||
profileName := strings.TrimSpace(newName)
|
||
if profileName == "" {
|
||
profileName = src.ProfileName + " (副本)"
|
||
}
|
||
|
||
// 复制配置,指纹参数使用默认值(新种子)
|
||
profile := &Profile{
|
||
ProfileId: newId,
|
||
ProfileName: profileName,
|
||
UserDataDir: newId, // 新的用户数据目录
|
||
CoreId: normalizeProfileCoreID(src.CoreId),
|
||
FingerprintArgs: append([]string{}, m.Config.Browser.DefaultFingerprintArgs...), // 使用默认指纹(新种子)
|
||
ProxyId: src.ProxyId,
|
||
ProxyConfig: src.ProxyConfig,
|
||
ProxyBindSourceID: src.ProxyBindSourceID,
|
||
ProxyBindSourceURL: src.ProxyBindSourceURL,
|
||
ProxyBindName: src.ProxyBindName,
|
||
ProxyBindUpdatedAt: src.ProxyBindUpdatedAt,
|
||
LaunchArgs: append([]string{}, src.LaunchArgs...),
|
||
Tags: append([]string{}, src.Tags...),
|
||
Keywords: append([]string{}, src.Keywords...),
|
||
GroupId: src.GroupId, // 复制分组
|
||
Running: false,
|
||
DebugPort: 0,
|
||
Pid: 0,
|
||
LastError: "",
|
||
CreatedAt: now,
|
||
UpdatedAt: now,
|
||
}
|
||
|
||
m.Profiles[newId] = profile
|
||
log.Info("实例复制成功", logger.F("src_id", profileId), logger.F("new_id", newId), logger.F("new_name", profileName))
|
||
|
||
if err := m.SaveProfiles(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if m.CodeProvider != nil {
|
||
if code, err := m.CodeProvider.EnsureCode(profile.ProfileId); err == nil {
|
||
profile.LaunchCode = code
|
||
}
|
||
}
|
||
|
||
return profile, nil
|
||
}
|
||
|
||
// SetKeywords 设置实例关键字(独立接口,不影响其他字段)
|
||
func (m *Manager) SetKeywords(profileId string, keywords []string) (*Profile, error) {
|
||
log := logger.New("Browser")
|
||
m.InitData()
|
||
m.Mutex.Lock()
|
||
defer m.Mutex.Unlock()
|
||
profile, exists := m.Profiles[profileId]
|
||
if !exists {
|
||
return nil, fmt.Errorf("profile not found")
|
||
}
|
||
profile.Keywords = append([]string{}, keywords...)
|
||
profile.UpdatedAt = time.Now().Format(time.RFC3339)
|
||
log.Info("关键字更新", logger.F("profile_id", profileId))
|
||
if err := m.SaveProfiles(); err != nil {
|
||
return nil, err
|
||
}
|
||
return profile, nil
|
||
}
|
||
|
||
// copyKeywords 深拷贝 keywords map
|
||
func copyKeywords(src map[string]string) map[string]string {
|
||
if src == nil {
|
||
return nil
|
||
}
|
||
dst := make(map[string]string, len(src))
|
||
for k, v := range src {
|
||
dst[k] = v
|
||
}
|
||
return dst
|
||
}
|