Golang写的,还在测试。我先用几天,没问题的话,到时候搞个release和教程。

// Command tieba-auto-sign is a simple Tieba auto-sign tool.// Usage:////      ./tieba-auto-sign -i 5 "BDUSS=...;" "BDUSS=...;"//      ./tieba-auto-sign -c /path/to/config.jsonpackage mainimport (        "context"        "encoding/json"        "flag"        "fmt"        "io"        "log"        "net/http"        "net/http/cookiejar"        "net/url"        "os"        "path/filepath"        "strings"        "time")const (        baseURL        = "https://tieba.baidu.com"        pathInfo       = "/mo/q/newmoindex"        pathSignAdd    = "/sign/add"        pathOneKeySign = "/tbmall/onekeySignin1")// Config 配置文件结构type Config struct {        Accounts []string `json:"accounts"` // BDUSS列表        Interval int      `json:"interval"` // 签到间隔(秒)        LogFile  string   `json:"log_file"` // 日志文件路径}type apiResp struct {        No    int             `json:"no"`        Error string          `json:"error"`        Data  json.RawMessage `json:"data"`}type infoData struct {        LikeForum []struct {                ForumName string `json:"forum_name"`                IsSign    int    `json:"is_sign"`        } `json:"like_forum"`        ItbTbs string `json:"itb_tbs"`}type TiebaClient struct {        cli   *http.Client        bduss string}func NewTiebaClient(bduss string) *TiebaClient {        jar, _ := cookiejar.New(nil)        cli := &http.Client{Timeout: 20 * time.Second, Jar: jar}        // 设置BDUSS cookie        bduss = strings.TrimSpace(bduss)        bduss = strings.TrimPrefix(bduss, "BDUSS=")        bduss = strings.TrimSuffix(bduss, ";")        u, _ := url.Parse(baseURL)        jar.SetCookies(u, []*http.Cookie{                {Name: "BDUSS", Value: bduss, Path: "/", Domain: ".baidu.com"},        })        return &TiebaClient{cli: cli, bduss: bduss}}func (c *TiebaClient) AutoSign(ctx context.Context, interval time.Duration) error {        // 获取贴吧信息和tbs        _, tbs, err := c.getForumInfo(ctx)        if err != nil {                return fmt.Errorf("获取贴吧信息失败: %v", err)        }        // 一键签到        if err := c.oneKeySign(ctx, tbs); err != nil {                log.Printf("一键签到失败: %v", err)        }        // 获取更新后的签到状态        forums, _, err := c.getForumInfo(ctx)        if err != nil {                return fmt.Errorf("获取更新状态失败: %v", err)        }        // 逐个签到未签的贴吧        unsigned := []string{}        for _, f := range forums {                if f.IsSign == 0 {                        unsigned = append(unsigned, f.ForumName)                }        }        log.Printf("开始逐个签到,共 %d 个未签贴吧", len(unsigned))        for i, name := range unsigned {                if err := c.signForum(ctx, name); err != nil {                        log.Printf("签到 %s 失败: %v", name, err)                }                if i < len(unsigned)-1 {                        time.Sleep(interval)                }        }        return nil}func (c *TiebaClient) getForumInfo(ctx context.Context) ([]struct {        ForumName string `json:"forum_name"`        IsSign    int    `json:"is_sign"`}, string, error) {        req, err := http.NewRequestWithContext(ctx, "GET", baseURL+pathInfo, nil)        if err != nil {                return nil, "", err        }        req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0")        resp, err := c.cli.Do(req)        if err != nil {                return nil, "", err        }        defer resp.Body.Close()        var apiResp apiResp        if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {                return nil, "", err        }        if apiResp.No != 0 {                return nil, "", fmt.Errorf("API错误: %s", apiResp.Error)        }        var data infoData        if err := json.Unmarshal(apiResp.Data, &data); err != nil {                return nil, "", err        }        log.Printf("共关注 %d 个贴吧", len(data.LikeForum))        signed, unsigned := 0, 0        for _, f := range data.LikeForum {                if f.IsSign == 1 {                        signed++                } else {                        unsigned++                }        }        log.Printf("已签到: %d, 未签到: %d", signed, unsigned)        return data.LikeForum, data.ItbTbs, nil}func (c *TiebaClient) oneKeySign(ctx context.Context, tbs string) error {        form := url.Values{"ie": {"utf-8"}, "tbs": {tbs}}        req, err := http.NewRequestWithContext(ctx, "POST", baseURL+pathOneKeySign,                strings.NewReader(form.Encode()))        if err != nil {                return err        }        req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")        req.Header.Set("Content-Type", "application/x-www-form-urlencoded")        resp, err := c.cli.Do(req)        if err != nil {                return err        }        defer resp.Body.Close()        var apiResp apiResp        if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {                return err        }        if apiResp.No == 0 || apiResp.No == 2280006 { // 成功或部分成功                log.Printf("一键签到完成")                return nil        }        return fmt.Errorf("一键签到失败: %s", apiResp.Error)}func (c *TiebaClient) signForum(ctx context.Context, forum string) error {        form := url.Values{"ie": {"utf-8"}, "kw": {forum}}        req, err := http.NewRequestWithContext(ctx, "POST", baseURL+pathSignAdd,                strings.NewReader(form.Encode()))        if err != nil {                return err        }        req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")        req.Header.Set("Content-Type", "application/x-www-form-urlencoded")        resp, err := c.cli.Do(req)        if err != nil {                return err        }        defer resp.Body.Close()        var apiResp apiResp        if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {                return err        }        if apiResp.No == 0 {                log.Printf("%s吧 签到成功", forum)                return nil        }        return fmt.Errorf("签到失败: %s", apiResp.Error)}// loadConfig 从文件加载配置func loadConfig(configPath string) (*Config, error) {        data, err := os.ReadFile(configPath)        if err != nil {                return nil, fmt.Errorf("读取配置文件失败: %v", err)        }        var config Config        if err := json.Unmarshal(data, &config); err != nil {                return nil, fmt.Errorf("解析配置文件失败: %v", err)        }        return &config, nil}// setupLogger 设置日志输出func setupLogger(logFile string) (*os.File, error) {        if logFile == "" {                return nil, nil // 使用默认输出        }        // 确保日志目录存在        dir := filepath.Dir(logFile)        if err := os.MkdirAll(dir, 0755); err != nil {                return nil, fmt.Errorf("创建日志目录失败: %v", err)        }        // 打开日志文件        file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)        if err != nil {                return nil, fmt.Errorf("打开日志文件失败: %v", err)        }        // 设置日志输出到文件和控制台        multiWriter := io.MultiWriter(os.Stdout, file)        log.SetOutput(multiWriter)        log.SetFlags(log.LstdFlags)        return file, nil}func main() {        var (                interval   = flag.Int("i", 5, "签到间隔秒数")                configPath = flag.String("c", "", "配置文件路径 (JSON格式)")        )        flag.Parse()        var accounts []string        var logFile string        var intervalSec int        if *configPath != "" {                // 从配置文件读取                config, err := loadConfig(*configPath)                if err != nil {                        log.Fatalf("加载配置文件失败: %v", err)                }                accounts = config.Accounts                logFile = config.LogFile                if config.Interval > 0 {                        intervalSec = config.Interval                } else {                        intervalSec = *interval                }        } else {                // 从命令行参数读取                accounts = flag.Args()                intervalSec = *interval        }        if len(accounts) == 0 {                fmt.Fprintf(os.Stderr, "使用方法:\n")                fmt.Fprintf(os.Stderr, "  %s -i 5 \"BDUSS=...;\" \"BDUSS=...;\"\n", os.Args[0])                fmt.Fprintf(os.Stderr, "  %s -c /path/to/config.json\n", os.Args[0])                fmt.Fprintf(os.Stderr, "\n配置文件格式:\n")                fmt.Fprintf(os.Stderr, "{\n")                fmt.Fprintf(os.Stderr, "  \"accounts\": [\"BDUSS=...;\", \"BDUSS=...;\"],\n")                fmt.Fprintf(os.Stderr, "  \"interval\": 5,\n")                fmt.Fprintf(os.Stderr, "  \"log_file\": \"/path/to/sign.log\"\n")                fmt.Fprintf(os.Stderr, "}\n")                os.Exit(1)        }        // 设置日志输出        logFileHandle, err := setupLogger(logFile)        if err != nil {                log.Fatalf("设置日志失败: %v", err)        }        if logFileHandle != nil {                defer logFileHandle.Close()                log.Printf("日志将保存到: %s", logFile)        }        log.Printf("开始批量签到,共 %d 个账号,间隔 %d 秒", len(accounts), intervalSec)        ctx := context.Background()        for idx, bduss := range accounts {                log.Printf("[%d/%d] 开始处理账号...", idx+1, len(accounts))                client := NewTiebaClient(bduss)                if err := client.AutoSign(ctx, time.Duration(intervalSec)*time.Second); err != nil {                        log.Printf("账号 %d 签到失败: %v", idx+1, err)                } else {                        log.Printf("账号 %d 签到完成", idx+1)                }        }        log.Printf("所有账号处理完成")}