commit 002419a7ed23fffcedd5c615692f218a66ac6683 Author: zyj <18107291228@163.com> Date: Sat Sep 13 10:22:49 2025 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40f1c2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.DS_Store +node_modules/ +/dist/ +/user-data/ +/log/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +config.yaml + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln diff --git a/api/task.go b/api/task.go new file mode 100644 index 0000000..3322bdb --- /dev/null +++ b/api/task.go @@ -0,0 +1,134 @@ +package api + +import ( + "fmt" + "go-account-register/libs" + "go-account-register/task" + paramsTypes "go-account-register/types" + "net/http" + + "github.com/gin-gonic/gin" +) + +type Task struct { +} + +type CreateRequestParams struct { + ID string `form:"id" json:"id" uri:"id" xml:"id"` + Name string `form:"name" json:"name" uri:"name" xml:"name"` + RunStatus int `form:"runStatus" json:"runStatus" uri:"runStatus" xml:"runStatus" binding:"required"` + Type int `form:"type" json:"type" uri:"type" xml:"type" binding:"required"` + AccountClassify string `form:"accountClassify" json:"accountClassify" uri:"accountClassify" xml:"accountClassify" binding:"required"` + ClassifyId []int `form:"classifyId" json:"classifyId" uri:"classifyId" xml:"classifyId"` + Keyword string `form:"keyword" json:"keyword" uri:"keyword" xml:"keyword"` + TwitterId string `form:"twitterId" json:"twitterId" uri:"twitterId" xml:"twitterId"` + Compensate int `form:"compensate" json:"compensate" uri:"compensate" xml:"compensate"` +} + +type StartRequestParams struct { + TaskId string `form:"taskId" json:"taskId" uri:"taskId" xml:"taskId" binding:"required"` +} + +// 创建任务 +func (*Task) Create(ctx *gin.Context) { + // 参数 + var res *libs.ErrorInfo + var paramsJson CreateRequestParams + if err := ctx.ShouldBindJSON(¶msJson); err != nil { + res = libs.ErrorCode["ParamsError"] + fmt.Println(err) + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": res.Code, + "msg": res.Msg, + "data": res.Data, + }) + return + } + var scheduler = task.GetScheduler() + var status string + if paramsJson.RunStatus == 0 { + status = "stop" + } else { + status = "pending" + } + taskId := scheduler.AddTask(¶msTypes.TaskParams{ + ID: paramsJson.ID, + Name: paramsJson.Name, + Status: status, + Type: paramsJson.Type, + AccountClassify: paramsJson.AccountClassify, + DataClassify: paramsJson.ClassifyId, + Keyword: paramsJson.Keyword, + TwitterId: paramsJson.TwitterId, + Compensate: paramsJson.Compensate, + }) + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": 200, + "msg": "成功", + "data": taskId, + }) + +} + +// 开始任务 +func (*Task) Start(ctx *gin.Context) { + // 参数 + var res *libs.ErrorInfo + var paramsJson StartRequestParams + if err := ctx.ShouldBindJSON(¶msJson); err != nil { + res = libs.ErrorCode["ParamsError"] + fmt.Println(err) + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": res.Code, + "msg": res.Msg, + "data": res.Data, + }) + return + } +} + +// 暂停任务 +func (*Task) Stop(ctx *gin.Context) { + // 参数 + var res *libs.ErrorInfo + var paramsJson StartRequestParams + if err := ctx.ShouldBindJSON(¶msJson); err != nil { + res = libs.ErrorCode["ParamsError"] + fmt.Println(err) + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": res.Code, + "msg": res.Msg, + "data": res.Data, + }) + return + } + var scheduler = task.GetScheduler() + scheduler.StopTask(paramsJson.TaskId) + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": 200, + "msg": "成功", + "data": "", + }) +} + +// 删除任务 +func (*Task) Delete(ctx *gin.Context) { + // 参数 + var res *libs.ErrorInfo + var paramsJson StartRequestParams + if err := ctx.ShouldBindJSON(¶msJson); err != nil { + res = libs.ErrorCode["ParamsError"] + fmt.Println(err) + ctx.JSON(http.StatusBadRequest, gin.H{ + "code": res.Code, + "msg": res.Msg, + "data": res.Data, + }) + return + } +} + +// 获取任务信息 +func (*Task) GetTaskInfo(ctx *gin.Context) { + +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..df668a9 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "go-account-register/config" + "go-account-register/router" + "go-account-register/utils" + "log" + "os" + "os/signal" + "strconv" + "syscall" + + "github.com/gin-gonic/gin" +) + +func main() { + appConfig, err := config.LoadConfig() + if err != nil { + fmt.Println(err) + fmt.Println("读取配置失败") + } + app := gin.Default() + // 注册路由 + router.Init(app) + utils.Connect() + go handleShutdownSignals() + + app.Run(":" + strconv.Itoa(appConfig.Port)) +} + +// 处理关闭信号的函数 +func handleShutdownSignals() { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + + // 阻塞等待信号 + sig := <-sigChan + log.Printf("接收到信号: %v, 关闭所有浏览器...", sig) + + // 强制退出程序 + os.Exit(0) +} diff --git a/config/index.go b/config/index.go new file mode 100644 index 0000000..6c57c6f --- /dev/null +++ b/config/index.go @@ -0,0 +1,57 @@ +package config + +import ( + "os" + + "gopkg.in/yaml.v3" +) + +type DatabaseConfig struct { + Username string `yaml:"username"` + Password string `yaml:"password"` + Host string `yaml:"host"` + Port int `yaml:"port"` + Database string `yaml:"database"` +} + +type Limit struct { + CommentNumMin int `yaml:"commentNumMin"` + CommentNumMax int `yaml:"commentNumMax"` + LikeNumMin int `yaml:"likeNumMin"` + LikeNumMax int `yaml:"likeNumMax"` + FollowNumMin int `yaml:"followNumMin"` + FollowNumMax int `yaml:"followNumMax"` + ViweTimeMax int `yaml:"viweTimeMax"` + ViweTimeMin int `yaml:"viweTimeMin"` + BrowserRunMax int `yaml:"browserRunMax"` +} + +type Clash struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + Secret string `yaml:"secret"` + ExePath string `yaml:"exePath"` +} + +type AppConfig struct { + Port int `yaml:"port"` + Database DatabaseConfig `yaml:"database"` + Limit Limit `yaml:"limit"` + Clash Clash `yaml:"clash"` +} + +func LoadConfig() (*AppConfig, error) { + // 读取文件内容 + data, err := os.ReadFile("config.yaml") + if err != nil { + return nil, err + } + + // 解析 YAML + var cfg AppConfig + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, err + } + + return &cfg, nil +} diff --git a/fonts/AlibabaPuHuiTi-3-115-Black.ttf b/fonts/AlibabaPuHuiTi-3-115-Black.ttf new file mode 100644 index 0000000..2a8d612 Binary files /dev/null and b/fonts/AlibabaPuHuiTi-3-115-Black.ttf differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..fc8e91f --- /dev/null +++ b/go.mod @@ -0,0 +1,57 @@ +module go-account-register + +go 1.24.3 + +require ( + github.com/fatih/structs v1.1.0 + github.com/gin-gonic/gin v1.10.1 + github.com/go-resty/resty/v2 v2.16.5 + github.com/go-rod/rod v0.116.2 + github.com/go-rod/stealth v0.4.9 + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 + github.com/google/uuid v1.6.0 + github.com/redis/go-redis/v9 v9.14.0 + golang.org/x/image v0.31.0 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/driver/mysql v1.6.0 + gorm.io/gorm v1.31.0 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/ysmood/fetchup v0.2.3 // indirect + github.com/ysmood/goob v0.4.0 // indirect + github.com/ysmood/got v0.40.0 // indirect + github.com/ysmood/gson v0.7.3 // indirect + github.com/ysmood/leakless v0.9.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.29.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..37b7474 --- /dev/null +++ b/go.sum @@ -0,0 +1,145 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= +github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= +github.com/go-rod/rod v0.113.0/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw= +github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= +github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= +github.com/go-rod/stealth v0.4.9 h1:X2PmQk4DUF2wzw6GOsWjW/glb8K5ebnftbEvLh7MlZ4= +github.com/go-rod/stealth v0.4.9/go.mod h1:eAzyvw8c0iAd5nJJsSWeh0fQ5z94vCIfdi1hUmYDimc= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.14.0 h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE= +github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= +github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= +github.com/ysmood/gop v0.0.2/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= +github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= +github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= +github.com/ysmood/got v0.34.1/go.mod h1:yddyjq/PmAf08RMLSwDjPyCvHvYed+WjHnQxpH851LM= +github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= +github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= +github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= +github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= +github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= +github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +github.com/ysmood/leakless v0.8.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= +github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= +github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA= +golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= +gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/libs/browser-fingerprint/canvas.go b/libs/browser-fingerprint/canvas.go new file mode 100644 index 0000000..95c9e72 --- /dev/null +++ b/libs/browser-fingerprint/canvas.go @@ -0,0 +1,65 @@ +package browserfingerprint + +import "fmt" + +func GetChangeCanvasJavaScript(rgba [4]int) string { + return fmt.Sprintf(` + const getImageData = CanvasRenderingContext2D.prototype.getImageData; + + const noisify = (canvas, context) => { + if (context) { + const shift = { + r: %d, + g: %d, + b: %d, + a: %d, + }; + const width = canvas.width; + const height = canvas.height; + + if (width && height) { + const imageData = getImageData.apply(context, [0, 0, width, height]); + + for (let i = 0; i < height; i++) + for (let j = 0; j < width; j++) { + const n = i * (width * 4) + j * 4; + imageData.data[n + 0] = imageData.data[n + 0] + shift.r; + imageData.data[n + 1] = imageData.data[n + 1] + shift.g; + imageData.data[n + 2] = imageData.data[n + 2] + shift.b; + imageData.data[n + 3] = imageData.data[n + 3] + shift.a; + } + + + context.putImageData(imageData, 0, 0); + } + } + }; + + HTMLCanvasElement.prototype.toBlob = new Proxy(HTMLCanvasElement.prototype.toBlob, { + apply(target, self, args) { + noisify(self, self.getContext('2d')); + + return Reflect.apply(target, self, args); + }, + }); + + HTMLCanvasElement.prototype.toDataURL = new Proxy(HTMLCanvasElement.prototype.toDataURL, { + apply(target, self, args) { + noisify(self, self.getContext('2d')); + + return Reflect.apply(target, self, args); + }, + }); + + CanvasRenderingContext2D.prototype.getImageData = new Proxy( + CanvasRenderingContext2D.prototype.getImageData, + { + apply(target, self, args) { + noisify(self.canvas, self); + + return Reflect.apply(target, self, args); + }, + } + ); +`, rgba[0], rgba[1], rgba[2], rgba[3]) +} diff --git a/libs/browser-fingerprint/index.go b/libs/browser-fingerprint/index.go new file mode 100644 index 0000000..f063878 --- /dev/null +++ b/libs/browser-fingerprint/index.go @@ -0,0 +1,42 @@ +package browserfingerprint + +import ( + paramsTypes "go-account-register/types" + "math/rand" + + "github.com/go-rod/rod" +) + +type BrowserFingerprint struct { +} + +func (b *BrowserFingerprint) SetBrowserFingerprint(page *rod.Page, params *paramsTypes.BrowserFingerprintParams) *rod.Page { + var p *rod.Page = page + if params.Canvas { + p = setCanvas(p) + } + if params.TimeZone != "" { + p = setTimezoneAndLangAndGeo(p, params) + } + + return p +} + +func setCanvas(page *rod.Page) *rod.Page { + // 注入噪声脚本 + // 生成随机 RGBA 偏移值 (Go 端控制) + rgba := [4]int{ + rand.Intn(10) - 5, // R: -5 ~ +4 + rand.Intn(10) - 5, // G + rand.Intn(10) - 5, // B + rand.Intn(10) - 5, // A + } + // 使用更隐蔽的 Canvas 指纹修改方法 + page.MustEvalOnNewDocument(GetChangeCanvasJavaScript(rgba)) + return page +} + +func setTimezoneAndLangAndGeo(page *rod.Page, params *paramsTypes.BrowserFingerprintParams) *rod.Page { + page.MustEvalOnNewDocument(GetChangeTimezoneJavaScript(params.TimeZone, params.Language, params.GeoLocation)) + return page +} diff --git a/libs/browser-fingerprint/timezone.go b/libs/browser-fingerprint/timezone.go new file mode 100644 index 0000000..d192c47 --- /dev/null +++ b/libs/browser-fingerprint/timezone.go @@ -0,0 +1,98 @@ +package browserfingerprint + +import ( + "fmt" + "log" + "time" + _ "time/tzdata" +) + +func GetChangeTimezoneJavaScript(timezone string, lang string, geo string) string { + return fmt.Sprintf(` + (() => { + // 设置全局时区配置 + window.__rodTZConfig = { + timeZone: '%s', + language: '%s', + geo: '%s' + }; + + // 覆盖 Date 对象 + const OriginalDate = window.Date; + window.Date = function(...args) { + if (new.target) { + // 作为构造函数 + if (args.length === 0) { + // 无参数调用时应用时区偏移 + const now = new OriginalDate(); + const targetOffset = -480; // %s 的 UTC 偏移(分钟) + return new OriginalDate(now.getTime() + (targetOffset - now.getTimezoneOffset()) * 60000); + } + return new OriginalDate(...args); + } + // 作为函数调用 + return OriginalDate(); + }; + + // 复制静态方法和属性 + Object.defineProperties(Date, Object.getOwnPropertyDescriptors(OriginalDate)); + + // 覆盖 Date.now() + const origDateNow = Date.now; + Date.now = function() { + return origDateNow() + (%d * 60000); // 添加时区偏移 + }; + + // 覆盖 getTimezoneOffset + const origGetTimezoneOffset = Date.prototype.getTimezoneOffset; + Date.prototype.getTimezoneOffset = function() { + return %d; // %s 的 UTC 偏移(分钟) + }; + + // 覆盖 Intl.DateTimeFormat + const origDateTimeFormat = Intl.DateTimeFormat; + Intl.DateTimeFormat = function(locales, options) { + options = options || {}; + options.timeZone = options.timeZone || window.__rodTZConfig.timeZone; + return new origDateTimeFormat(locales, options); + }; + + // 覆盖 navigator 属性 + Object.defineProperty(navigator, 'language', { + get: () => window.__rodTZConfig.language, + configurable: false + }); + + Object.defineProperty(navigator, 'languages', { + get: () => [window.__rodTZConfig.language], + configurable: false + }); + + // 覆盖控制台时区 + if (console._timeZone) { + console._timeZone = window.__rodTZConfig.timeZone; + } + })(); + `, + timezone, + lang, + geo, + timezone, // 用于注释 + getUTCOffsetMinutes(timezone), // 时区偏移分钟数 + getUTCOffsetMinutes(timezone), // 时区偏移分钟数 + timezone) +} + +// getUTCOffsetMinutes 获取时区的UTC偏移分钟数 +func getUTCOffsetMinutes(timeZone string) int { + // 使用时区名称获取实际偏移 + loc, err := time.LoadLocation(timeZone) + if err != nil { + log.Printf("无法加载时区 %s: %v, 使用默认值 -480", timeZone, err) + return -480 // 默认太平洋时间 + } + + // 获取当前时间的偏移 + _, offset := time.Now().In(loc).Zone() + return -offset / 60 // 转换为分钟并取反 +} diff --git a/libs/browser.go b/libs/browser.go new file mode 100644 index 0000000..a103938 --- /dev/null +++ b/libs/browser.go @@ -0,0 +1,95 @@ +package libs + +import ( + "sync" + + "github.com/go-rod/rod" +) + +type Browser struct{} + +type BrowserInfo struct { + Id int + Browser *rod.Browser + Url string + Page *rod.Page +} + +// 浏览器指纹数据 +type Fingerprint struct { + Viewport []struct { + Height int + Width int + } + Proxy string + Lang string +} + +var ( + BrowserPool = make(map[int]*BrowserInfo) // 初始化map + poolMutex sync.RWMutex // 添加读写锁保证并发安全 +) + +var ChromeExample Chrome + +func (*Browser) GetBrowser(id int, params *Fingerprint) *BrowserInfo { + poolMutex.RLock() + value, ok := BrowserPool[id] + poolMutex.RUnlock() + + if ok { + return value + } + + // 未找到时获取写锁(互斥) + poolMutex.Lock() + defer poolMutex.Unlock() + + // 双检查:避免在等待锁期间其他goroutine已创建 + if value, ok := BrowserPool[id]; ok { + return value + } + + // 创建新实例(补充CancelFunc) + var ChromeExample Chrome + browser, url, page := ChromeExample.Create(id, params) + browserInfo := &BrowserInfo{ + Id: id, + Browser: browser, + Url: url, + Page: page, + } + BrowserPool[id] = browserInfo + return browserInfo +} + +func (*Browser) GetNewBrowser(params *Fingerprint) *BrowserInfo { + // 创建新实例(补充CancelFunc) + var ChromeExample Chrome + browser, url, page := ChromeExample.TemporaryCreate(params) + browserInfo := &BrowserInfo{ + Browser: browser, + Url: url, + Page: page, + } + return browserInfo +} + +func (*Browser) GetAllBrowser() map[int]*BrowserInfo { + return BrowserPool +} + +func (*Browser) CancelAllBrowser() { + for _, v := range BrowserPool { + v.Browser.MustClose() + } +} + +func (*Browser) CancelBrowser(id int) { + browserInfo := BrowserPool[id] + if browserInfo == nil { + return + } + browserInfo.Browser.MustClose() + delete(BrowserPool, id) +} diff --git a/libs/chrome.go b/libs/chrome.go new file mode 100644 index 0000000..bf08a20 --- /dev/null +++ b/libs/chrome.go @@ -0,0 +1,204 @@ +package libs + +import ( + browserfingerprint "go-account-register/libs/browser-fingerprint" + paramsTypes "go-account-register/types" + "go-account-register/utils/external" + "log" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/go-rod/rod" + "github.com/go-rod/rod/lib/launcher" + "github.com/go-rod/rod/lib/proto" + "github.com/go-rod/stealth" +) + +type Chrome struct{} + +func (c *Chrome) Create(accountId int, params *Fingerprint) (*rod.Browser, string, *rod.Page) { + var accountIdStr = strconv.Itoa(accountId) + userDataDir := "user-data/" + accountIdStr + absPath, _ := filepath.Abs(userDataDir) + log.Printf("用户数据目录: %s", absPath) + path, _ := launcher.LookPath() // 获取当前系统的浏览器目录 + // 判断是否传递代理 + // var proxyHandle utils.ProxyHandle + // protocol, username, password, ip, port, errProxy := proxyHandle.ParseProxy(params.Proxy) + + ipInfo := external.GetIpInfo(strings.TrimSpace(string(params.Proxy))) + + l := launcher.New(). + Bin(path). + // Proxy("210.51.27.121:50003"). // 直接传入完整认证URL + Delete("use-mock-keychain"). // delete flag "--use-mock-keychain" + Set("disable-blink-features", "AutomationControlled"). // 绕过自动化检测 + // Set("incognito"). // 无痕模式 + Set("user-data-dir", absPath). // 数据持久化目录 + Set("window-size", "1920,1480"). // 窗口尺寸 + Set("disable-infobars", "true"). // 隐藏自动化提示栏 + Set("no-sandbox", "true"). + Set("excludeSwitches", "enable-automation"). + Set("enable-gpu"). // 启用 GPU 加速 + Set("ignore-certificate-errors"). // 忽略证书错误 + Set("use-fake-ui-for-media-stream"). // 允许媒体流 + Set("autoplay-policy", "no-user-gesture-required"). // 自动播放 + Set("ignore-certificate-errors"). + Set("disable-application-cache"). + Set("disable-dev-shm-usage"). + // 禁用webrtc + Set("disable-webrtc", "true"). // 核心禁用参数 + Set("disable-features", "WebRtcHideLocalIpsWithMdns"). // 隐藏本地IP + Set("webrtc-ip-handling-policy", "disable_non_proxied_udp"). // 禁用非代理UDP + Set("force-webrtc-ip-handling-policy"). + Headless(false) // 关闭无头模式 + + // 修改语言 + l.Set("lang", "en-US") + l.Set("accept-lang", "en-US") + + // 修改时区 + l.Set("timezone", ipInfo.Timezone) + l.Set("disable-geolocation", "true") // 启用地理定位API + l.Set("disable-web-security", "true") // 允许跨域(某些网站需要) + l.Set("disable-notifications", "true") + + // 设置环境变量 - 核心时区设置 + l.Env(append(os.Environ(), "TZ="+ipInfo.Timezone)...) + l. + Set("timezone", ipInfo.Timezone). + Set("geolocation", ipInfo.Lat+","+ipInfo.Lon) + + // if errProxy == nil { + // if strings.Contains(protocol, "http") { + // log.Println(protocol + "://" + ip + ":" + port) + // l = l.Proxy(ip + ":" + port) + // } + // } + + uri := l.MustLaunch() + + browser := rod.New(). + ControlURL(uri). + MustConnect().NoDefaultDevice() + browser.MustIgnoreCertErrors(true) + // if errProxy == nil { + // if strings.Contains(protocol, "http") { + // log.Println(username, password) + // go browser.MustHandleAuth(username, password)() + // } + // } + page := stealth.MustPage(browser) + // page := browser.MustPage() + + go browser.EachEvent(func(e *proto.TargetTargetCreated) { + if e.TargetInfo.Type != proto.TargetTargetInfoTypePage { + return + } + browser.MustPageFromTargetID(e.TargetInfo.TargetID).MustEvalOnNewDocument(stealth.JS) + })() + + // 在页面加载前注入时区覆盖代码 + var browserFingerprint browserfingerprint.BrowserFingerprint + page = browserFingerprint.SetBrowserFingerprint(page, ¶msTypes.BrowserFingerprintParams{ + Canvas: true, + TimeZone: ipInfo.Timezone, // 太平洋时间 (UTC-8) + Language: "en-US", + GeoLocation: ipInfo.Lat + "," + ipInfo.Lon, // 洛杉矶坐标 + }) + page.MustSetViewport(1920, 1480, 1.0, false) + + return browser, uri, page +} + +func (c *Chrome) TemporaryCreate(params *Fingerprint) (*rod.Browser, string, *rod.Page) { + path, _ := launcher.LookPath() // 获取当前系统的浏览器目录 + // 判断是否传递代理 + // var proxyHandle utils.ProxyHandle + // protocol, username, password, ip, port, errProxy := proxyHandle.ParseProxy(params.Proxy) + + ipInfo := external.GetIpInfo(strings.TrimSpace(string(params.Proxy))) + + l := launcher.New(). + Bin(path). + // Proxy("210.51.27.121:50003"). // 直接传入完整认证URL + Delete("use-mock-keychain"). // delete flag "--use-mock-keychain" + Set("disable-blink-features", "AutomationControlled"). // 绕过自动化检测 + // Set("incognito"). // 无痕模式 + Set("window-size", "1920,1480"). // 窗口尺寸 + Set("disable-infobars", "true"). // 隐藏自动化提示栏 + Set("no-sandbox", "true"). + Set("excludeSwitches", "enable-automation"). + Set("enable-gpu"). // 启用 GPU 加速 + Set("ignore-certificate-errors"). // 忽略证书错误 + Set("use-fake-ui-for-media-stream"). // 允许媒体流 + Set("autoplay-policy", "no-user-gesture-required"). // 自动播放 + Set("ignore-certificate-errors"). + Set("disable-application-cache"). + Set("disable-dev-shm-usage"). + // 禁用webrtc + Set("disable-webrtc", "true"). // 核心禁用参数 + Set("disable-features", "WebRtcHideLocalIpsWithMdns"). // 隐藏本地IP + Set("webrtc-ip-handling-policy", "disable_non_proxied_udp"). // 禁用非代理UDP + Set("force-webrtc-ip-handling-policy"). + Headless(false) // 关闭无头模式 + + // 修改语言 + l.Set("lang", "en-US") + l.Set("accept-lang", "en-US") + + // 修改时区 + l.Set("timezone", ipInfo.Timezone) + l.Set("disable-geolocation", "true") // 启用地理定位API + l.Set("disable-web-security", "true") // 允许跨域(某些网站需要) + l.Set("disable-notifications", "true") + + // 设置环境变量 - 核心时区设置 + l.Env(append(os.Environ(), "TZ="+ipInfo.Timezone)...) + l. + Set("timezone", ipInfo.Timezone). + Set("geolocation", ipInfo.Lat+","+ipInfo.Lon) + + // if errProxy == nil { + // if strings.Contains(protocol, "http") { + // log.Println(protocol + "://" + ip + ":" + port) + // l = l.Proxy(ip + ":" + port) + // } + // } + + uri := l.MustLaunch() + + browser := rod.New(). + ControlURL(uri). + MustConnect().NoDefaultDevice() + browser.MustIgnoreCertErrors(true) + // if errProxy == nil { + // if strings.Contains(protocol, "http") { + // log.Println(username, password) + // go browser.MustHandleAuth(username, password)() + // } + // } + page := stealth.MustPage(browser) + // page := browser.MustPage() + + go browser.EachEvent(func(e *proto.TargetTargetCreated) { + if e.TargetInfo.Type != proto.TargetTargetInfoTypePage { + return + } + browser.MustPageFromTargetID(e.TargetInfo.TargetID).MustEvalOnNewDocument(stealth.JS) + })() + + // 在页面加载前注入时区覆盖代码 + var browserFingerprint browserfingerprint.BrowserFingerprint + page = browserFingerprint.SetBrowserFingerprint(page, ¶msTypes.BrowserFingerprintParams{ + Canvas: true, + TimeZone: ipInfo.Timezone, // 太平洋时间 (UTC-8) + Language: "en-US", + GeoLocation: ipInfo.Lat + "," + ipInfo.Lon, // 洛杉矶坐标 + }) + page.MustSetViewport(1920, 1480, 1.0, false) + + return browser, uri, page +} diff --git a/libs/error_code.go b/libs/error_code.go new file mode 100644 index 0000000..57bf7c5 --- /dev/null +++ b/libs/error_code.go @@ -0,0 +1,33 @@ +package libs + +type ErrorInfo struct { + Code int + Data any + Msg string +} + +var ErrorCode = map[string]*ErrorInfo{ + "LoginFailed": {Code: 0, Data: "", Msg: "账号异常"}, + "LoginSuccessful": {Code: 200, Data: "", Msg: "登录成功"}, + "NetworkTimeout": {Code: 1, Data: "", Msg: "网络超时"}, + "AccountHasBeenTakenOffline": {Code: 2, Data: "", Msg: "账号已下线,请重新登录"}, + "OtherError": {Code: 3, Data: "", Msg: "其他错误"}, + "ChangeSuccessful": {Code: 200, Data: "", Msg: "修改成功"}, + "ChangeFailed": {Code: 0, Data: "", Msg: "修改失败"}, + "SendPostSuccessful": {Code: 200, Data: "", Msg: "发推成功"}, + "SendPostFailed": {Code: 200, Data: "", Msg: "发推失败"}, + "SimulationSuccessful": {Code: 200, Data: "", Msg: "模拟成功"}, + "FollowSuccessful": {Code: 200, Data: "", Msg: "关注成功"}, + "FollowFailed": {Code: 200, Data: "", Msg: "关注失败"}, + "CommentSuccessful": {Code: 200, Data: "", Msg: "评论成功"}, + "CommentFailed": {Code: 200, Data: "", Msg: "评论失败"}, + "ParamsError": {Code: 4, Data: "", Msg: "参数错误"}, + "SearchSuccessful": {Code: 200, Data: "", Msg: "搜索成功"}, + "SearchFailed": {Code: 200, Data: "", Msg: "搜索失败"}, + "ChangeLangSuccessful": {Code: 200, Data: "", Msg: "修改语言成功"}, + "ChangeLangFailed": {Code: 0, Data: "", Msg: "修改语言失败"}, + "MaintainAccountSuccessful": {Code: 200, Data: "", Msg: "养号运行成功"}, + "MaintainAccountFailed": {Code: 0, Data: "", Msg: "养号运行失败"}, + "RegisterFailed": {Code: 0, Data: "", Msg: "注册失败"}, + "RegisterSuccessful": {Code: 0, Data: "", Msg: "注册成功"}, +} diff --git a/libs/file.go b/libs/file.go new file mode 100644 index 0000000..854a60f --- /dev/null +++ b/libs/file.go @@ -0,0 +1,36 @@ +package libs + +import ( + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "time" +) + +type File struct { +} + +// 下载图片到临时文件 +func (*File) DownloadImage(url string) (string, error) { + resp, err := http.Get(url) + if err != nil { + return "", err + } + defer resp.Body.Close() + + // 创建临时文件 + tempDir := os.TempDir() + tempFile := filepath.Join(tempDir, fmt.Sprintf("upload-%d.jpg", time.Now().UnixNano())) + + out, err := os.Create(tempFile) + if err != nil { + return "", err + } + defer out.Close() + + // 复制文件内容 + _, err = io.Copy(out, resp.Body) + return tempFile, err +} diff --git a/models/ip.go b/models/ip.go new file mode 100644 index 0000000..0dc24c3 --- /dev/null +++ b/models/ip.go @@ -0,0 +1,34 @@ +package models + +type Ip struct { + ID int64 `gorm:"id;primary_key;auto_increment"` + As string `gorm:"as"` + Asname string `gorm:"asname"` + City string `gorm:"city"` + Continent string `gorm:"continent"` + ContinentCode string `gorm:"continent_code"` + Country string `gorm:"country"` + CountryCode string `gorm:"country_code"` + Currency string `gorm:"currency"` + District string `gorm:"district"` + Hosting int64 `gorm:"hosting"` + Isp string `gorm:"isp"` + Lat string `gorm:"lat"` + Lon string `gorm:"lon"` + Mobile int64 `gorm:"mobile"` + Offset string `gorm:"offset"` + Org string `gorm:"org"` + Proxy int64 `gorm:"proxy"` + Query string `gorm:"query"` + Region string `gorm:"region"` + RegionName string `gorm:"region_name"` + Timezone string `gorm:"timezone"` + Zip string `gorm:"zip"` + UpdateTime int64 `gorm:"update_time"` + CreateTime int64 `gorm:"create_time"` +} + +// 实现 TableName 方法指定表名 +func (Ip) TableName() string { + return "j_ip" +} diff --git a/router/index.go b/router/index.go new file mode 100644 index 0000000..bee2232 --- /dev/null +++ b/router/index.go @@ -0,0 +1,14 @@ +package router + +import ( + "github.com/gin-gonic/gin" +) + +var task Task + +type Router struct { +} + +func Init(app *gin.Engine) { + task.TaskRouterInit(app) +} diff --git a/router/task.go b/router/task.go new file mode 100644 index 0000000..c04eb73 --- /dev/null +++ b/router/task.go @@ -0,0 +1,19 @@ +package router + +import ( + "go-account-register/api" + + "github.com/gin-gonic/gin" +) + +type Task struct{} + +var apiTask api.Task + +func (t *Task) TaskRouterInit(app *gin.Engine) { + task := app.Group("/task") + task.POST("/create", apiTask.Create) + task.POST("/start", apiTask.Start) + task.POST("/stop", apiTask.Stop) + task.POST("/delete", apiTask.Delete) +} diff --git a/task/scheduler.go b/task/scheduler.go new file mode 100644 index 0000000..df744cc --- /dev/null +++ b/task/scheduler.go @@ -0,0 +1,206 @@ +package task + +import ( + "context" + "log" + "sort" + "sync" + + paramsTypes "go-account-register/types" + + "github.com/fatih/structs" + "github.com/google/uuid" +) + +type Scheduler struct { + TaskQueue map[string]*paramsTypes.Task // 待处理的任务 + mu sync.Mutex + ActiveProcesses map[string]*paramsTypes.Task // 正在运行的任务 + PausedTasks map[string]*paramsTypes.Task // 暂停的任务 +} + +var ( + StopStatus string = "stop" // 暂停状态 + PendingStatus string = "pending" // 运行状态 +) + +var MaxTaskNum int = 10 + +// 全局单例实例 +var globalScheduler *Scheduler +var once sync.Once + +// 获取全局调度器实例 +func GetScheduler() *Scheduler { + once.Do(func() { + globalScheduler = &Scheduler{ + TaskQueue: make(map[string]*paramsTypes.Task), + ActiveProcesses: make(map[string]*paramsTypes.Task), + PausedTasks: make(map[string]*paramsTypes.Task), + } + }) + return globalScheduler +} + +// 添加任务 +func (s *Scheduler) AddTask(params *paramsTypes.TaskParams) string { + s.mu.Lock() + ctx, cancel := context.WithCancel(context.Background()) + var uuidV4 string + is := structs.New(params) + if is.Field("ID").IsZero() { + uuidV4 = uuid.NewString() + } else { + uuidV4 = params.ID + } + task := ¶msTypes.Task{ + ID: uuidV4, + Name: params.Name, + Status: params.Status, + Cancel: cancel, + Ctx: ctx, + Message: make(chan string, 1), + Type: params.Type, + Data: ¶msTypes.TaskData{ + AccountClassify: params.AccountClassify, + DataClassify: params.DataClassify, + Keyword: params.Keyword, + TwitterId: params.TwitterId, + Compensate: params.Compensate, + }} + switch params.Status { + case StopStatus: + s.PausedTasks[uuidV4] = task + case PendingStatus: + s.TaskQueue[uuidV4] = task + default: + s.TaskQueue[uuidV4] = task + } + log.Println("创建任务:" + uuidV4) + log.Println("状态:" + params.Status) + s.mu.Unlock() + s.processQueue() + return uuidV4 +} + +// 运行任务 +func (s *Scheduler) RunTask(id string) { + s.mu.Lock() + defer s.mu.Unlock() + // 查询暂停的任务列表中是否包含 + v, ok := s.PausedTasks[id] + if ok { + s.TaskQueue[id] = v + delete(s.PausedTasks, id) + } + s.processQueue() +} + +// 暂停任务 +func (s *Scheduler) StopTask(id string) { + s.mu.Lock() + defer s.mu.Unlock() + // 查询任务列表中是否包含 + v, ok := s.TaskQueue[id] + if ok { + s.PausedTasks[id] = v + v.Status = StopStatus + v.Cancel() // 停止上下文 + // v.Message <- "Error" + delete(s.TaskQueue, id) + } + // 查询运行中的任务列表中是否包含 + value, ok1 := s.ActiveProcesses[id] + if ok1 { + s.PausedTasks[id] = value + value.Status = StopStatus + log.Println("向通道中发送消息") + value.Cancel() // 停止上下文 + // value.Message <- "Error" + delete(s.ActiveProcesses, id) + } +} + +// 删除任务 +func (s *Scheduler) RemoveTask(id string) { + s.mu.Lock() + defer s.mu.Unlock() + // 查询任务列表中是否包含 + _, ok := s.TaskQueue[id] + if ok { + delete(s.TaskQueue, id) + } + + // 查询运行中的任务列表中是否包含 + _, ok1 := s.ActiveProcesses[id] + if ok1 { + delete(s.ActiveProcesses, id) + } + + // 查询暂停中的任务列表中是否包含 + _, ok2 := s.PausedTasks[id] + if ok2 { + delete(s.PausedTasks, id) + } +} + +// 处理任务 +func (s *Scheduler) processQueue() { + + s.mu.Lock() + log.Println("开始处理任务") + defer s.mu.Unlock() + for len(s.ActiveProcesses) < MaxTaskNum && len(s.TaskQueue) > 0 { + keys := make([]string, 0, len(s.TaskQueue)) + for k := range s.TaskQueue { + keys = append(keys, k) + } + + // 排序键 + sort.Strings(keys) + // 获取第一个键值对 + firstKey := keys[0] + go s.executeTask(s.TaskQueue[firstKey]) + s.ActiveProcesses[firstKey] = s.TaskQueue[firstKey] + delete(s.TaskQueue, firstKey) + } +} + +func (s *Scheduler) executeTask(task *paramsTypes.Task) { + // 根据类型判断任务 + // switch task.Type { + // case 6: + // var taskNurturingClashPlugin plugin.TaskNurturingClashPlugin + // go taskNurturingClashPlugin.Run(task) + // case 1: + // var taskChangeAccoutnClashPlugin plugin.TaskChangeAccoutnClashPlugin + // go taskChangeAccoutnClashPlugin.Run(task) + // case 3: + // var taskFollowClashPlugin plugin.TaskFollowClashPlugin + // go taskFollowClashPlugin.Run(task) + // case 7: + // var taskSendGifPlugin plugin.TaskSendGifPlugin + // go taskSendGifPlugin.Run(task) + // } + + // 创建一个阻塞获取当前任务 + select { + case message := <-task.Message: + if message == "Success" { + log.Println("任务成功") + // 运行结束 + s.PausedTasks[task.ID] = task + delete(s.ActiveProcesses, task.ID) + } + if message == "Error" { + log.Println("任务失败") + // 运行结束 + s.PausedTasks[task.ID] = task + delete(s.ActiveProcesses, task.ID) + } + case <-task.Ctx.Done(): + // 运行结束 + s.PausedTasks[task.ID] = task + delete(s.ActiveProcesses, task.ID) + } +} diff --git a/types/fingerprint_type.go b/types/fingerprint_type.go new file mode 100644 index 0000000..00c441a --- /dev/null +++ b/types/fingerprint_type.go @@ -0,0 +1,9 @@ +package types + +type BrowserFingerprintParams struct { + Canvas bool // 是否随机指纹 + TimeZone string // 时区名称,如 "America/New_York" + Language string // 语言,如 "en-US" + UserAgent string // 用户代理字符串 + GeoLocation string // 地理位置坐标,如 "40.7128,-74.0060" +} diff --git a/types/task_type.go b/types/task_type.go new file mode 100644 index 0000000..707c147 --- /dev/null +++ b/types/task_type.go @@ -0,0 +1,37 @@ +package types + +import "context" + +// 任务信息 +type Task struct { + ID string + Name string + Status string + Message chan string + Cancel context.CancelFunc + Ctx context.Context + Type int + Data *TaskData +} + +// 添加任务传递的参数 +type TaskParams struct { + ID string + Name string + Status string + Type int + AccountClassify string + DataClassify []int + Keyword string + TwitterId string + Compensate int +} + +// 任务携带的数据 +type TaskData struct { + AccountClassify string + DataClassify []int + Keyword string + TwitterId string + Compensate int +} diff --git a/utils/ai_request.go b/utils/ai_request.go new file mode 100644 index 0000000..8c1bc6b --- /dev/null +++ b/utils/ai_request.go @@ -0,0 +1,94 @@ +package utils + +import ( + "encoding/json" + "fmt" + + "github.com/go-resty/resty/v2" +) + +type AiReqeust struct{} + +type DeepseekResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []struct { + Index int `json:"index"` + Message struct { + Role string `json:"role"` + Content string `json:"content"` + } `json:"message"` + Logprobs interface{} `json:"logprobs"` // 可能是null或复杂结构 + FinishReason string `json:"finish_reason"` + } `json:"choices"` + Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + PromptTokensDetails struct { + CachedTokens int `json:"cached_tokens"` + } `json:"prompt_tokens_details"` + PromptCacheHitTokens int `json:"prompt_cache_hit_tokens"` + PromptCacheMissTokens int `json:"prompt_cache_miss_tokens"` + } `json:"usage"` + SystemFingerprint string `json:"system_fingerprint"` +} + +func (*AiReqeust) Deepseek(text string) string { + // 定义结构体 + type Message struct { + Role string `json:"role"` + Content string `json:"content"` + } + + type ChatRequest struct { + Model string `json:"model"` + Messages []Message `json:"messages"` + } + // 创建 resty 客户端 + client := resty.New() + + // 目标URL + url := "https://api.deepseek.com/v1/chat/completions" + + request := ChatRequest{ + Model: "deepseek-chat", + Messages: []Message{ + { + Role: "user", + Content: text, + }, + }, + } + + // 发送 POST 请求 + resp, err := client.R(). + SetHeader("Content-Type", "application/json"). // 必填头 + SetHeader("Authorization", "Bearer sk-9ca769cb45c249559276b979a2e1a2cd"). // 认证头 + SetBody(request). // 设置JSON体 + Post(url) + + if err != nil { + fmt.Println("请求失败:", err) + return "" + } + + // 解析响应 + var response DeepseekResponse + if err := json.Unmarshal(resp.Body(), &response); err != nil { + fmt.Println("解析失败:", err) + return "" + } + + // 提取关键信息 + if len(response.Choices) > 0 { + firstChoice := response.Choices[0] + // fmt.Printf("\nAI回复 (%s):\n%s\n", + // firstChoice.Message.Role, + // firstChoice.Message.Content) + return firstChoice.Message.Content + } + return "" +} diff --git a/utils/db.go b/utils/db.go new file mode 100644 index 0000000..623619d --- /dev/null +++ b/utils/db.go @@ -0,0 +1,60 @@ +package utils + +import ( + "fmt" + "go-account-register/config" + "log" + "time" + + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +var DB *gorm.DB + +func Connect() (*gorm.DB, error) { + appConfig, dberr := config.LoadConfig() + if dberr != nil { + fmt.Println("读取配置失败") + } + + dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", + appConfig.Database.Username, + appConfig.Database.Password, + appConfig.Database.Host, + appConfig.Database.Port, + appConfig.Database.Database) + + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + if err != nil { + return nil, fmt.Errorf("failed to connect to database: %w", err) + } + + sqlDB, err := db.DB() + if err != nil { + return nil, fmt.Errorf("failed to get sql.DB: %w", err) + } + + sqlDB.SetMaxIdleConns(10) + sqlDB.SetMaxOpenConns(100) + sqlDB.SetConnMaxLifetime(time.Hour) + + log.Println("Database connection established") + DB = db + return db, nil +} + +func GetDB() *gorm.DB { + if DB == nil { + panic("database connection is not initialized") + } + return DB +} + +func Close() error { + sqlDB, err := DB.DB() + if err != nil { + return err + } + return sqlDB.Close() +} diff --git a/utils/external/index.go b/utils/external/index.go new file mode 100644 index 0000000..dfa2a90 --- /dev/null +++ b/utils/external/index.go @@ -0,0 +1,69 @@ +package external + +import ( + "fmt" + "go-account-register/models" + "go-account-register/utils" + "time" +) + +func GetIpInfo(ip string) models.Ip { + db := utils.GetDB() + var ipInfo models.Ip + if ip == "" { + var ipRequest utils.IpRequest + res, err := ipRequest.IpApi(ip) + if err != nil { + } + resIp := db.Model(&models.Ip{}).Where("query = ?", res.Query).First(&ipInfo) + if resIp.Error != nil { + createIp(res) + db.Model(&models.Ip{}).Where("query = ?", res.Query).First(&ipInfo) + return ipInfo + } + return ipInfo + } + resIp := db.Model(&models.Ip{}).Where("query = ?", ip).First(&ipInfo) + if resIp.Error != nil { + var ipRequest utils.IpRequest + res, err := ipRequest.IpApi(ip) + if err != nil { + } + createIp(res) + db.Model(&models.Ip{}).Where("query = ?", ip).First(&ipInfo) + return ipInfo + } + + return ipInfo +} + +func createIp(res utils.IPAPIResponse) { + db := utils.GetDB() + + db.Model(&models.Ip{}).Create(&models.Ip{ + As: res.As, + Asname: res.Asname, + City: res.City, + Continent: res.Continent, + ContinentCode: res.ContinentCode, + Country: res.Country, + CountryCode: res.CountryCode, + Currency: res.Currency, + District: res.District, + Hosting: utils.BoolToInt(res.Hosting), + Isp: res.Isp, + Lat: fmt.Sprint(res.Lat), + Lon: fmt.Sprint(res.Lon), + Mobile: utils.BoolToInt(res.Mobile), + Offset: fmt.Sprint(res.Offset), + Org: res.Org, + Proxy: utils.BoolToInt(res.Proxy), + Query: res.Query, + Region: res.Region, + RegionName: res.RegionName, + Timezone: res.Timezone, + Zip: res.Zip, + UpdateTime: time.Now().Unix(), + CreateTime: time.Now().Unix(), + }) +} diff --git a/utils/index.go b/utils/index.go new file mode 100644 index 0000000..ce463d7 --- /dev/null +++ b/utils/index.go @@ -0,0 +1,75 @@ +package utils + +import ( + "math/rand" + "time" +) + +type RandDateParams struct { + Year int + Month int + Day int +} + +const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +const ( + letterBits = 6 // 52个字母需6位(2^6=64) + letterMask = 1<>6) // 关键:修正垂直偏移 + pt := freetype.Pt(x, yBase) + + if _, err := c.DrawString(text, pt); err != nil { + log.Fatal("文字绘制失败:", err) + } + + outFile, err := os.Create(newImage) + if err != nil { + log.Fatal("输出文件创建失败:", err) + return "", err + } + defer outFile.Close() + if err := png.Encode(outFile, rgba); err != nil { + log.Fatal("图片保存失败:", err) + return "", err + } + log.Println("成功生成:output.png") + return newImage, nil +}