├── .github ├── last_activity.md └── workflows │ ├── keep-alive.yml │ ├── build.yml │ ├── tieba-signin.yml │ └── sync-upstream.yml ├── dist ├── types │ ├── notify.types.js │ ├── utils.types.js │ ├── apiService.types.js │ ├── dataProcessor.types.js │ └── index.js ├── local-test.js ├── utils.js ├── dataProcessor.js ├── apiService.js ├── notify.js └── index.js ├── src ├── types │ ├── index.ts │ ├── utils.types.ts │ ├── dataProcessor.types.ts │ ├── notify.types.ts │ └── apiService.types.ts ├── local-test.ts ├── utils.ts ├── dataProcessor.ts ├── apiService.ts ├── notify.ts └── index.ts ├── js-backup ├── local-test.js ├── utils.js ├── dataProcessor.js ├── apiService.js ├── notify.js └── index.js ├── .env.example ├── .gitignore ├── package.json ├── LICENSE ├── README.md └── tsconfig.json /.github/last_activity.md: -------------------------------------------------------------------------------- 1 | 上次活动时间: 2025-12-01 10:28:14 2 | -------------------------------------------------------------------------------- /dist/types/notify.types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/types/utils.types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/types/apiService.types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /dist/types/dataProcessor.types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './apiService.types'; 2 | export * from './dataProcessor.types'; 3 | export * from './notify.types'; 4 | export * from './utils.types'; -------------------------------------------------------------------------------- /src/types/utils.types.ts: -------------------------------------------------------------------------------- 1 | export interface DateFormatOptions { 2 | timeZone: string; 3 | utcOffset: string; 4 | } 5 | 6 | export type DateFormatTemplateFunction = (date: Date, options?: DateFormatOptions) => string; -------------------------------------------------------------------------------- /js-backup/local-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 本地测试模块 - 加载环境变量后调用主程序 3 | */ 4 | // 加载.env文件中的环境变量 5 | require('dotenv').config(); 6 | 7 | console.log('=========================================='); 8 | console.log('🧪 本地测试模式启动'); 9 | 10 | // 测试环境设置小批量参数,如果.env中没有设置的话 11 | if (!process.env.BATCH_SIZE) { 12 | process.env.BATCH_SIZE = '5'; // 本地测试使用较小的批次 13 | } 14 | 15 | if (!process.env.BATCH_INTERVAL) { 16 | process.env.BATCH_INTERVAL = '500'; // 本地测试使用较短的间隔 17 | } 18 | 19 | // 调用主程序 20 | require('./index'); -------------------------------------------------------------------------------- /src/local-test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 本地测试模块 - 加载环境变量后调用主程序 3 | */ 4 | // 加载.env文件中的环境变量 5 | import * as dotenv from 'dotenv'; 6 | dotenv.config(); 7 | 8 | console.log('=========================================='); 9 | console.log('🧪 本地测试模式启动'); 10 | 11 | // 测试环境设置小批量参数,如果.env中没有设置的话 12 | if (!process.env.BATCH_SIZE) { 13 | process.env.BATCH_SIZE = '5'; // 本地测试使用较小的批次 14 | } 15 | 16 | if (!process.env.BATCH_INTERVAL) { 17 | process.env.BATCH_INTERVAL = '500'; // 本地测试使用较短的间隔 18 | } 19 | 20 | // 调用主程序 21 | import './index'; -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # 百度贴吧登录凭证(必需) 2 | BDUSS= 3 | 4 | # 批量签到配置 5 | BATCH_SIZE=5 # 每批签到的贴吧数量 6 | BATCH_INTERVAL=500 # 批次间隔(毫秒) 7 | 8 | # 重试机制配置 9 | MAX_RETRIES=3 # 最大重试次数 10 | RETRY_INTERVAL=5000 # 重试间隔(毫秒) 11 | 12 | # 通知功能配置 13 | ENABLE_NOTIFY=false # 是否启用通知推送 14 | 15 | # 通知相关环境变量(可选) 16 | # Server酱 17 | # SERVERCHAN_KEY= 18 | 19 | # Bark 20 | # BARK_KEY= 21 | 22 | # Telegram 23 | # TG_BOT_TOKEN= 24 | # TG_CHAT_ID= 25 | 26 | # 钉钉 27 | # DINGTALK_WEBHOOK= 28 | # DINGTALK_SECRET= 29 | 30 | # 企业微信 31 | # WECOM_KEY= 32 | 33 | # PushPlus 34 | # PUSHPLUS_TOKEN= -------------------------------------------------------------------------------- /src/types/dataProcessor.types.ts: -------------------------------------------------------------------------------- 1 | export interface ProcessedSignResult { 2 | success: boolean; 3 | message: string; 4 | info: { 5 | rank?: number | string; 6 | continueCount?: number | string; 7 | totalCount?: number | string; 8 | [key: string]: any; 9 | }; 10 | } 11 | 12 | export interface SignResultItem extends ProcessedSignResult { 13 | name: string; 14 | index: number; 15 | retried?: boolean; 16 | retryCount?: number; 17 | } 18 | 19 | export interface SignResultSummary { 20 | totalCount: number; 21 | successCount: number; 22 | alreadySignedCount: number; 23 | failedCount: number; 24 | signResults: { 25 | success: SignResultItem[]; 26 | failed: SignResultItem[]; 27 | }; 28 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 依赖目录 2 | node_modules/ 3 | npm-debug.log 4 | yarn-debug.log 5 | yarn-error.log 6 | # package-lock.json(CI/CD需要此文件) 7 | yarn.lock 8 | pnpm-lock.yaml 9 | 10 | # 运行时数据 11 | .env 12 | .env.local 13 | .env.development.local 14 | .env.test.local 15 | .env.production.local 16 | 17 | # IDE和编辑器 18 | .idea/ 19 | .vscode/ 20 | *.swp 21 | *.swo 22 | .DS_Store 23 | *.sublime-project 24 | *.sublime-workspace 25 | .project 26 | .atom/ 27 | .vscode/ 28 | *.code-workspace 29 | 30 | # 日志 31 | logs 32 | *.log 33 | npm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | lerna-debug.log* 37 | 38 | # 构建和输出目录 39 | # dist/ (由build.yml工作流自动构建并提交) 40 | build/ 41 | out/ 42 | output/ 43 | public/dist/ 44 | coverage/ 45 | 46 | # 临时文件 47 | tmp/ 48 | temp/ 49 | .tmp/ 50 | .temp/ 51 | .cache/ 52 | .nyc_output/ 53 | 54 | # 系统文件 55 | Thumbs.db 56 | ehthumbs.db 57 | Desktop.ini 58 | $RECYCLE.BIN/ 59 | .directory 60 | 61 | # 其他 62 | .coverage 63 | .nyc_output 64 | .cache 65 | .parcel-cache 66 | 67 | # TypeScript build output 68 | *.tsbuildinfo -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tieba", 3 | "version": "1.0.0", 4 | "description": "百度贴吧自动签到 GitHub Actions 脚本", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "start": "npm run build && node dist/index.js", 9 | "test": "npm run build && node dist/local-test.js", 10 | "dev": "ts-node src/index.ts" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/chiupam/tieba.git" 15 | }, 16 | "keywords": [ 17 | "github-actions", 18 | "automation", 19 | "baidu-tieba", 20 | "tieba", 21 | "sign-in" 22 | ], 23 | "author": "chiupam", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/chiupam/tieba/issues" 27 | }, 28 | "homepage": "https://github.com/chiupam/tieba#readme", 29 | "dependencies": { 30 | "axios": "^1.6.0" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^20.12.0", 34 | "dotenv": "^16.4.0", 35 | "ts-node": "^10.9.2", 36 | "typescript": "^5.4.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /dist/types/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __exportStar = (this && this.__exportStar) || function(m, exports) { 14 | for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); 15 | }; 16 | Object.defineProperty(exports, "__esModule", { value: true }); 17 | __exportStar(require("./apiService.types"), exports); 18 | __exportStar(require("./dataProcessor.types"), exports); 19 | __exportStar(require("./notify.types"), exports); 20 | __exportStar(require("./utils.types"), exports); 21 | -------------------------------------------------------------------------------- /.github/workflows/keep-alive.yml: -------------------------------------------------------------------------------- 1 | name: 保持仓库活跃 2 | 3 | on: 4 | schedule: 5 | # 每月1日运行一次 6 | - cron: '0 0 1 * *' 7 | # 允许手动触发 8 | workflow_dispatch: 9 | 10 | # 设置权限 11 | permissions: 12 | contents: write # 需要写入权限才能提交 13 | 14 | jobs: 15 | keep-alive: 16 | runs-on: ubuntu-latest 17 | env: 18 | TZ: Asia/Shanghai # 设置时区为中国标准时间 19 | steps: 20 | - name: 检出代码 21 | uses: actions/checkout@v3 22 | 23 | - name: 获取当前时间 24 | id: date 25 | run: echo "NOW=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT 26 | 27 | - name: 创建临时文件 28 | run: | 29 | mkdir -p .github 30 | echo "上次活动时间: ${{ steps.date.outputs.NOW }}" > .github/last_activity.md 31 | 32 | - name: 提交更改 33 | run: | 34 | git config --global user.name 'github-actions[bot]' 35 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 36 | git add .github/last_activity.md 37 | git commit -m "保持仓库活跃: ${{ steps.date.outputs.NOW }}" || echo "没有更改,无需提交" 38 | git push -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 chiupam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/types/notify.types.ts: -------------------------------------------------------------------------------- 1 | export interface NotifyOptions { 2 | title?: string; 3 | content: string; 4 | [key: string]: any; 5 | } 6 | 7 | export interface NotifyResult { 8 | success: boolean; 9 | message: string; 10 | channel?: string; 11 | } 12 | 13 | export interface ServerChanOptions { 14 | key: string; 15 | title?: string; 16 | content: string; 17 | } 18 | 19 | export interface BarkOptions { 20 | key: string; 21 | title?: string; 22 | content: string; 23 | sound?: string; 24 | icon?: string; 25 | } 26 | 27 | export interface TelegramOptions { 28 | botToken: string; 29 | chatId: string; 30 | message: string; 31 | parseMode?: 'HTML' | 'Markdown' | 'MarkdownV2'; 32 | } 33 | 34 | export interface DingTalkOptions { 35 | webhook: string; 36 | secret?: string; 37 | content: string; 38 | title?: string; 39 | } 40 | 41 | export interface WeComOptions { 42 | key: string; 43 | content: string; 44 | title?: string; 45 | } 46 | 47 | export interface PushPlusOptions { 48 | token: string; 49 | title?: string; 50 | content: string; 51 | template?: 'html' | 'json' | 'markdown' | 'txt'; 52 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 构建TypeScript 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - 'src/**' 8 | - 'tsconfig.json' 9 | - 'package.json' 10 | - '.github/workflows/build.yml' 11 | workflow_dispatch: 12 | 13 | # 添加权限配置 14 | permissions: 15 | contents: write # 需要写入权限才能提交 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | with: 24 | # 确保获取完整历史以便于推送 25 | fetch-depth: 0 26 | 27 | - name: 设置Node.js环境 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: '16' 31 | cache: 'npm' 32 | 33 | - name: 安装依赖 34 | run: npm ci 35 | 36 | - name: 构建TypeScript 37 | run: npm run build 38 | 39 | - name: 提交编译后的文件 40 | run: | 41 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 42 | git config --local user.name "github-actions[bot]" 43 | git add -f dist/ 44 | if git diff --staged --quiet; then 45 | echo "没有变更需要提交" 46 | else 47 | git commit -m "自动构建: 更新编译后的JavaScript文件" 48 | # 添加调试信息 49 | echo "开始推送更改..." 50 | git push 51 | echo "✅ 编译后的文件已提交" 52 | fi -------------------------------------------------------------------------------- /src/types/apiService.types.ts: -------------------------------------------------------------------------------- 1 | export interface UserInfo { 2 | status: number; 3 | message?: string; 4 | displayname?: string; 5 | userId?: string; 6 | bduss?: string; 7 | isValid?: boolean; 8 | deviceId?: string; 9 | [key: string]: any; 10 | } 11 | 12 | export interface TiebaInfo { 13 | forum_id: number; 14 | forum_name: string; 15 | is_sign: number; 16 | level_id: number; 17 | user_level: number; 18 | user_exp: number; 19 | cur_score: number; 20 | levelup_score: number; 21 | [key: string]: any; 22 | } 23 | 24 | export interface TiebaList extends Array {} 25 | 26 | export interface SignResult { 27 | error_code?: string | number; 28 | error_msg?: string; 29 | error?: string; 30 | no?: number; 31 | data?: { 32 | errno?: number; 33 | errmsg?: string; 34 | uinfo?: { 35 | user_sign_rank?: number; 36 | cont_sign_num?: number; 37 | }; 38 | [key: string]: any; 39 | }; 40 | user_info?: { 41 | user_id?: number | string; 42 | is_sign_in?: number; 43 | user_sign_rank?: number; 44 | sign_time?: number | string; 45 | cont_sign_num?: number; 46 | total_sign_num?: number; 47 | cout_total_sing_num?: number; 48 | hun_sign_num?: number; 49 | total_resign_num?: number; 50 | is_org_name?: number; 51 | }; 52 | [key: string]: any; 53 | } 54 | 55 | export interface TbsResult { 56 | tbs: string; 57 | is_login: number; 58 | [key: string]: any; 59 | } -------------------------------------------------------------------------------- /.github/workflows/tieba-signin.yml: -------------------------------------------------------------------------------- 1 | name: 百度贴吧自动签到 2 | 3 | on: 4 | schedule: 5 | - cron: '0 2,3 * * *' # 每天 UTC 2:00 3:00 运行 (北京时间 10:00 11:00) 6 | workflow_dispatch: # 支持手动触发 7 | 8 | jobs: 9 | signin: 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 15 # 设置超时限制 12 | 13 | env: 14 | TZ: Asia/Shanghai # 设置时区为中国标准时间 15 | 16 | steps: 17 | - name: 检出代码 18 | uses: actions/checkout@v3 19 | 20 | - name: 设置 Node.js 环境 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: '16' 24 | 25 | - name: 安装依赖 26 | run: npm ci 27 | 28 | - name: 运行签到脚本 29 | env: 30 | # 基础配置 31 | BDUSS: ${{ secrets.BDUSS }} 32 | BATCH_SIZE: ${{ secrets.BATCH_SIZE || '5' }} 33 | BATCH_INTERVAL: ${{ secrets.BATCH_INTERVAL || '1000' }} 34 | 35 | # 重试配置 36 | MAX_RETRIES: ${{ secrets.MAX_RETRIES || '3' }} 37 | RETRY_INTERVAL: ${{ secrets.RETRY_INTERVAL || '5000' }} 38 | 39 | # 通知相关环境变量 40 | ENABLE_NOTIFY: ${{ secrets.ENABLE_NOTIFY || 'false' }} 41 | SERVERCHAN_KEY: ${{ secrets.SERVERCHAN_KEY }} 42 | BARK_KEY: ${{ secrets.BARK_KEY }} 43 | TG_BOT_TOKEN: ${{ secrets.TG_BOT_TOKEN }} 44 | TG_CHAT_ID: ${{ secrets.TG_CHAT_ID }} 45 | DINGTALK_WEBHOOK: ${{ secrets.DINGTALK_WEBHOOK }} 46 | DINGTALK_SECRET: ${{ secrets.DINGTALK_SECRET }} 47 | WECOM_KEY: ${{ secrets.WECOM_KEY }} 48 | PUSHPLUS_TOKEN: ${{ secrets.PUSHPLUS_TOKEN }} 49 | run: node dist/index.js 50 | 51 | - name: 处理结果 52 | if: always() 53 | run: | 54 | echo "当前时间: $(date "+%Y/%m/%d %H:%M:%S")" 55 | echo "工作流执行状态: ${{ job.status }}" -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 工具函数集合 3 | */ 4 | import { DateFormatOptions } from './types/utils.types'; 5 | 6 | /** 7 | * 将对象转换为URL查询字符串 8 | * @param obj - 要转换的对象 9 | * @returns URL查询字符串 10 | */ 11 | export function toQueryString(obj: Record): string { 12 | return Object.keys(obj).map(key => `${key}=${encodeURIComponent(obj[key])}`).join('&'); 13 | } 14 | 15 | /** 16 | * 获取指定范围内的随机整数 17 | * @param min - 最小值 18 | * @param max - 最大值 19 | * @returns 随机整数 20 | */ 21 | export function getRandomInt(min: number, max: number): number { 22 | min = Math.ceil(min); 23 | max = Math.floor(max); 24 | return Math.floor(Math.random() * (max - min + 1)) + min; 25 | } 26 | 27 | /** 28 | * 格式化时间 29 | * @param date - 日期对象 30 | * @param timezone - 时区 31 | * @param offset - 时区偏移显示值 32 | * @returns 格式化后的时间字符串 33 | */ 34 | export function formatDate(date: Date, timezone: string, offset: string): string { 35 | const options: Intl.DateTimeFormatOptions = { 36 | year: 'numeric', 37 | month: 'numeric', 38 | day: 'numeric', 39 | hour: '2-digit', 40 | minute: '2-digit', 41 | second: '2-digit', 42 | hour12: false, 43 | timeZone: timezone 44 | }; 45 | 46 | const formattedDate = date.toLocaleString('zh-CN', options); 47 | return `${formattedDate} (${offset})`; 48 | } 49 | 50 | /** 51 | * 生成随机设备ID 52 | * @returns 随机设备ID 53 | */ 54 | export function generateDeviceId(): string { 55 | return `BDTIEBA_${Math.random().toString(36).substring(2, 15)}`; 56 | } 57 | 58 | /** 59 | * 对贴吧名称进行脱敏处理 60 | * @param name - 贴吧名称 61 | * @returns 脱敏后的名称 62 | */ 63 | export function maskTiebaName(name: string): string { 64 | if (!name) return '未知贴吧'; 65 | 66 | // 对于极短的名称,保留第一个字符 67 | if (name.length <= 2) { 68 | return name[0] + '*'; 69 | } 70 | 71 | // 对于普通长度的名称,保留首尾字符,中间用星号代替 72 | if (name.length <= 5) { 73 | return name[0] + '*'.repeat(name.length - 2) + name[name.length - 1]; 74 | } 75 | 76 | // 对于较长的名称,保留前两个字符和最后一个字符 77 | return name.substring(0, 2) + '*'.repeat(3) + name.substring(name.length - 1); 78 | } -------------------------------------------------------------------------------- /dist/local-test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || (function () { 19 | var ownKeys = function(o) { 20 | ownKeys = Object.getOwnPropertyNames || function (o) { 21 | var ar = []; 22 | for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; 23 | return ar; 24 | }; 25 | return ownKeys(o); 26 | }; 27 | return function (mod) { 28 | if (mod && mod.__esModule) return mod; 29 | var result = {}; 30 | if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); 31 | __setModuleDefault(result, mod); 32 | return result; 33 | }; 34 | })(); 35 | Object.defineProperty(exports, "__esModule", { value: true }); 36 | /** 37 | * 本地测试模块 - 加载环境变量后调用主程序 38 | */ 39 | // 加载.env文件中的环境变量 40 | const dotenv = __importStar(require("dotenv")); 41 | dotenv.config(); 42 | console.log('=========================================='); 43 | console.log('🧪 本地测试模式启动'); 44 | // 测试环境设置小批量参数,如果.env中没有设置的话 45 | if (!process.env.BATCH_SIZE) { 46 | process.env.BATCH_SIZE = '5'; // 本地测试使用较小的批次 47 | } 48 | if (!process.env.BATCH_INTERVAL) { 49 | process.env.BATCH_INTERVAL = '500'; // 本地测试使用较短的间隔 50 | } 51 | // 调用主程序 52 | require("./index"); 53 | -------------------------------------------------------------------------------- /js-backup/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 工具函数集合 3 | */ 4 | 5 | /** 6 | * 将对象转换为URL查询字符串 7 | * @param {Object} obj - 要转换的对象 8 | * @returns {string} URL查询字符串 9 | */ 10 | function toQueryString(obj) { 11 | return Object.keys(obj).map(key => `${key}=${encodeURIComponent(obj[key])}`).join('&'); 12 | } 13 | 14 | /** 15 | * 获取指定范围内的随机整数 16 | * @param {number} min - 最小值 17 | * @param {number} max - 最大值 18 | * @returns {number} 随机整数 19 | */ 20 | function getRandomInt(min, max) { 21 | min = Math.ceil(min); 22 | max = Math.floor(max); 23 | return Math.floor(Math.random() * (max - min + 1)) + min; 24 | } 25 | 26 | /** 27 | * 格式化时间 28 | * @param {Date} date - 日期对象 29 | * @param {string} timezone - 时区 30 | * @param {string} offset - 时区偏移显示值 31 | * @returns {string} 格式化后的时间字符串 32 | */ 33 | function formatDate(date, timezone, offset) { 34 | const options = { 35 | year: 'numeric', 36 | month: 'numeric', 37 | day: 'numeric', 38 | hour: '2-digit', 39 | minute: '2-digit', 40 | second: '2-digit', 41 | hour12: false, 42 | timeZone: timezone 43 | }; 44 | 45 | const formattedDate = date.toLocaleString('zh-CN', options); 46 | return `${formattedDate} (${offset})`; 47 | } 48 | 49 | /** 50 | * 生成随机设备ID 51 | * @returns {string} 随机设备ID 52 | */ 53 | function generateDeviceId() { 54 | return `BDTIEBA_${Math.random().toString(36).substring(2, 15)}`; 55 | } 56 | 57 | /** 58 | * 对贴吧名称进行脱敏处理 59 | * @param {string} name - 贴吧名称 60 | * @returns {string} 脱敏后的名称 61 | */ 62 | function maskTiebaName(name) { 63 | if (!name) return '未知贴吧'; 64 | 65 | // 对于极短的名称,保留第一个字符 66 | if (name.length <= 2) { 67 | return name[0] + '*'; 68 | } 69 | 70 | // 对于普通长度的名称,保留首尾字符,中间用星号代替 71 | if (name.length <= 5) { 72 | return name[0] + '*'.repeat(name.length - 2) + name[name.length - 1]; 73 | } 74 | 75 | // 对于较长的名称,保留前两个字符和最后一个字符 76 | return name.substring(0, 2) + '*'.repeat(3) + name.substring(name.length - 1); 77 | } 78 | 79 | module.exports = { 80 | toQueryString, 81 | getRandomInt, 82 | formatDate, 83 | generateDeviceId, 84 | maskTiebaName 85 | }; -------------------------------------------------------------------------------- /dist/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.toQueryString = toQueryString; 4 | exports.getRandomInt = getRandomInt; 5 | exports.formatDate = formatDate; 6 | exports.generateDeviceId = generateDeviceId; 7 | exports.maskTiebaName = maskTiebaName; 8 | /** 9 | * 将对象转换为URL查询字符串 10 | * @param obj - 要转换的对象 11 | * @returns URL查询字符串 12 | */ 13 | function toQueryString(obj) { 14 | return Object.keys(obj).map(key => `${key}=${encodeURIComponent(obj[key])}`).join('&'); 15 | } 16 | /** 17 | * 获取指定范围内的随机整数 18 | * @param min - 最小值 19 | * @param max - 最大值 20 | * @returns 随机整数 21 | */ 22 | function getRandomInt(min, max) { 23 | min = Math.ceil(min); 24 | max = Math.floor(max); 25 | return Math.floor(Math.random() * (max - min + 1)) + min; 26 | } 27 | /** 28 | * 格式化时间 29 | * @param date - 日期对象 30 | * @param timezone - 时区 31 | * @param offset - 时区偏移显示值 32 | * @returns 格式化后的时间字符串 33 | */ 34 | function formatDate(date, timezone, offset) { 35 | const options = { 36 | year: 'numeric', 37 | month: 'numeric', 38 | day: 'numeric', 39 | hour: '2-digit', 40 | minute: '2-digit', 41 | second: '2-digit', 42 | hour12: false, 43 | timeZone: timezone 44 | }; 45 | const formattedDate = date.toLocaleString('zh-CN', options); 46 | return `${formattedDate} (${offset})`; 47 | } 48 | /** 49 | * 生成随机设备ID 50 | * @returns 随机设备ID 51 | */ 52 | function generateDeviceId() { 53 | return `BDTIEBA_${Math.random().toString(36).substring(2, 15)}`; 54 | } 55 | /** 56 | * 对贴吧名称进行脱敏处理 57 | * @param name - 贴吧名称 58 | * @returns 脱敏后的名称 59 | */ 60 | function maskTiebaName(name) { 61 | if (!name) 62 | return '未知贴吧'; 63 | // 对于极短的名称,保留第一个字符 64 | if (name.length <= 2) { 65 | return name[0] + '*'; 66 | } 67 | // 对于普通长度的名称,保留首尾字符,中间用星号代替 68 | if (name.length <= 5) { 69 | return name[0] + '*'.repeat(name.length - 2) + name[name.length - 1]; 70 | } 71 | // 对于较长的名称,保留前两个字符和最后一个字符 72 | return name.substring(0, 2) + '*'.repeat(3) + name.substring(name.length - 1); 73 | } 74 | -------------------------------------------------------------------------------- /.github/workflows/sync-upstream.yml: -------------------------------------------------------------------------------- 1 | name: 自动同步上游仓库 2 | 3 | on: 4 | schedule: 5 | # 每天凌晨2点执行(UTC时间,对应北京时间10点) 6 | - cron: '0 2 * * *' 7 | # 允许手动触发 8 | workflow_dispatch: 9 | 10 | jobs: 11 | sync-upstream: 12 | runs-on: ubuntu-latest 13 | name: 同步上游仓库更新 14 | 15 | # 仅在Fork仓库中运行此工作流 16 | if: github.repository != 'chiupam/tieba' 17 | 18 | steps: 19 | - name: 检出代码 20 | uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | # 使用默认的GITHUB_TOKEN,它对当前仓库有写权限 24 | token: ${{ github.token }} 25 | 26 | - name: 配置Git 27 | run: | 28 | git config --global user.name 'github-actions[bot]' 29 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 30 | 31 | - name: 添加上游仓库 32 | run: | 33 | # 添加chiupam/tieba作为上游仓库 34 | git remote add upstream https://github.com/chiupam/tieba.git 35 | git remote -v 36 | 37 | - name: 获取上游更新 38 | run: | 39 | git fetch upstream 40 | echo "✅ 成功获取上游仓库的最新代码" 41 | 42 | - name: 备份需要保留的文件 43 | run: | 44 | if [ -f "last_activity.md" ]; then 45 | echo "📦 备份本地的 last_activity.md 文件" 46 | cp last_activity.md last_activity.md.backup 47 | fi 48 | 49 | - name: 合并上游变更 50 | run: | 51 | # 检查当前分支 52 | CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) 53 | echo "当前分支: $CURRENT_BRANCH" 54 | 55 | # 执行合并 56 | if git merge upstream/$CURRENT_BRANCH --no-edit; then 57 | echo "✅ 已成功合并上游仓库的变更" 58 | else 59 | echo "⚠️ 合并过程中可能存在冲突,尝试使用策略解决..." 60 | # 尝试使用theirs策略解决冲突(接受上游更改) 61 | git merge --abort 62 | git merge upstream/$CURRENT_BRANCH -X theirs --no-edit 63 | echo "⚠️ 使用theirs合并策略完成,如有异常请手动检查" 64 | fi 65 | 66 | - name: 恢复需要保留的文件 67 | run: | 68 | if [ -f "last_activity.md.backup" ]; then 69 | echo "📦 恢复本地的 last_activity.md 文件" 70 | mv last_activity.md.backup last_activity.md 71 | git add last_activity.md 72 | git commit -m "保留本地 last_activity.md 文件" || echo "没有需要提交的更改" 73 | fi 74 | 75 | - name: 推送更新 76 | run: | 77 | # 推送到当前分支 78 | CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) 79 | 80 | # 只推送到Fork的仓库,不尝试推送到上游仓库 81 | if git push origin $CURRENT_BRANCH; then 82 | echo "✅ 已将更新推送到您的仓库" 83 | else 84 | echo "❌ 推送失败,可能需要手动检查" 85 | exit 1 86 | fi 87 | 88 | - name: 同步完成 89 | run: | 90 | echo "🎉 您的仓库已与上游仓库同步完成!" 91 | echo "📝 最新提交信息:" 92 | git log -1 --pretty=format:"%h - %an: %s" -------------------------------------------------------------------------------- /js-backup/dataProcessor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 数据处理模块 3 | */ 4 | 5 | /** 6 | * 处理签到结果数据 7 | * @param {Object} signResult - 签到结果 8 | * @returns {Object} 处理后的签到结果 9 | */ 10 | function processSignResult(signResult) { 11 | // 解析签到结果 12 | if (!signResult) { 13 | return { success: false, message: '签到结果为空' }; 14 | } 15 | 16 | // 处理不同的成功状态码 17 | if (signResult.no === 0 && signResult.data.errmsg === 'success' && signResult.data.errno === 0) { 18 | return { 19 | success: true, 20 | message: '签到成功', 21 | info: { 22 | rank: signResult.data.uinfo.user_sign_rank, 23 | signDays: signResult.data.uinfo.cont_sign_num 24 | } 25 | }; 26 | } else if (signResult.no === 1101) { 27 | return { 28 | success: true, 29 | message: '已经签到过了', 30 | info: {} 31 | }; 32 | } else if (signResult.no === 2150040) { 33 | return { 34 | success: false, 35 | message: '签到失败,需要验证码', 36 | code: signResult.no, 37 | info: {} 38 | }; 39 | } else if (signResult.no === 1011) { 40 | return { 41 | success: false, 42 | message: '未加入此吧或等级不够', 43 | code: signResult.no, 44 | info: {} 45 | }; 46 | } else if (signResult.no === 1102) { 47 | return { 48 | success: false, 49 | message: '签到过快', 50 | code: signResult.no, 51 | info: {} 52 | }; 53 | } else if (signResult.no === 1010) { 54 | return { 55 | success: false, 56 | message: '目录出错', 57 | code: signResult.no, 58 | info: {} 59 | }; 60 | } else { 61 | return { 62 | success: false, 63 | message: signResult.error || `签到失败,错误码: ${signResult.no}`, 64 | code: signResult.no || -1, 65 | info: signResult.data || {} 66 | }; 67 | } 68 | } 69 | 70 | /** 71 | * 汇总签到结果 72 | * @param {Array} results - 所有贴吧的签到结果 73 | * @returns {Object} 汇总结果 74 | */ 75 | function summarizeResults(results) { 76 | if (!results || !Array.isArray(results)) { 77 | return { 78 | total: 0, 79 | success: 0, 80 | alreadySigned: 0, 81 | failed: 0, 82 | errorMessages: {} 83 | }; 84 | } 85 | 86 | const summary = { 87 | total: results.length, 88 | success: 0, 89 | alreadySigned: 0, 90 | failed: 0, 91 | errorMessages: {} 92 | }; 93 | 94 | // 统计各种错误类型的数量 95 | results.forEach(result => { 96 | if (result.success) { 97 | if (result.message === '已经签到过了') { 98 | summary.alreadySigned += 1; 99 | } else { 100 | summary.success += 1; 101 | } 102 | } else { 103 | summary.failed += 1; 104 | 105 | // 统计错误消息类型数量 106 | if (!summary.errorMessages[result.message]) { 107 | summary.errorMessages[result.message] = 1; 108 | } else { 109 | summary.errorMessages[result.message]++; 110 | } 111 | } 112 | }); 113 | 114 | return summary; 115 | } 116 | 117 | /** 118 | * 格式化汇总结果为文本 119 | * @param {Object} summary - 汇总结果 120 | * @returns {string} 格式化后的文本 121 | */ 122 | function formatSummary(summary) { 123 | let text = `📊 签到统计:\n`; 124 | text += `总计: ${summary.total} 个贴吧\n`; 125 | text += `✅ 成功: ${summary.success} 个\n`; 126 | text += `📌 已签: ${summary.alreadySigned} 个\n`; 127 | text += `❌ 失败: ${summary.failed} 个`; 128 | 129 | // 添加失败原因统计 130 | if (summary.failed > 0) { 131 | text += `\n\n❌ 失败原因:\n`; 132 | for (const [errorMessage, count] of Object.entries(summary.errorMessages)) { 133 | text += `- ${errorMessage}: ${count} 个\n`; 134 | } 135 | } 136 | 137 | return text; 138 | } 139 | 140 | module.exports = { 141 | processSignResult, 142 | summarizeResults, 143 | formatSummary 144 | }; -------------------------------------------------------------------------------- /src/dataProcessor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 数据处理模块 3 | */ 4 | import { 5 | ProcessedSignResult, 6 | SignResultItem, 7 | SignResultSummary 8 | } from './types/dataProcessor.types'; 9 | import { SignResult } from './types/apiService.types'; 10 | 11 | /** 12 | * 处理签到结果数据 13 | * @param signResult - 签到结果 14 | * @returns 处理后的签到结果 15 | */ 16 | export function processSignResult(signResult: SignResult): ProcessedSignResult { 17 | // 解析签到结果 18 | if (!signResult) { 19 | return { success: false, message: '签到结果为空', info: {} }; 20 | } 21 | 22 | // 处理不同的成功状态码 23 | if (signResult.no === 0 && signResult.data?.errmsg === 'success' && signResult.data?.errno === 0) { 24 | return { 25 | success: true, 26 | message: '签到成功', 27 | info: { 28 | rank: signResult.data?.uinfo?.user_sign_rank, 29 | continueCount: signResult.data?.uinfo?.cont_sign_num 30 | } 31 | }; 32 | } else if (signResult.no === 1101) { 33 | return { 34 | success: true, 35 | message: '已经签到过了', 36 | info: {} 37 | }; 38 | } else if (signResult.no === 2150040) { 39 | return { 40 | success: false, 41 | message: '签到失败,需要验证码', 42 | info: { code: signResult.no } 43 | }; 44 | } else if (signResult.no === 1011) { 45 | return { 46 | success: false, 47 | message: '未加入此吧或等级不够', 48 | info: { code: signResult.no } 49 | }; 50 | } else if (signResult.no === 1102) { 51 | return { 52 | success: false, 53 | message: '签到过快', 54 | info: { code: signResult.no } 55 | }; 56 | } else if (signResult.no === 1010) { 57 | return { 58 | success: false, 59 | message: '目录出错', 60 | info: { code: signResult.no } 61 | }; 62 | } else { 63 | return { 64 | success: false, 65 | message: signResult.error || `签到失败,错误码: ${signResult.no || '未知'}`, 66 | info: { 67 | code: signResult.no ?? -1, 68 | data: signResult.data 69 | } 70 | }; 71 | } 72 | } 73 | 74 | interface SummaryStats { 75 | total: number; 76 | success: number; 77 | alreadySigned: number; 78 | failed: number; 79 | errorMessages: Record; 80 | signResults?: { 81 | success: SignResultItem[]; 82 | failed: SignResultItem[]; 83 | }; 84 | } 85 | 86 | /** 87 | * 汇总签到结果 88 | * @param results - 所有贴吧的签到结果 89 | * @returns 汇总结果 90 | */ 91 | export function summarizeResults(results: SignResultItem[]): SignResultSummary { 92 | if (!results || !Array.isArray(results)) { 93 | return { 94 | totalCount: 0, 95 | successCount: 0, 96 | alreadySignedCount: 0, 97 | failedCount: 0, 98 | signResults: { 99 | success: [], 100 | failed: [] 101 | } 102 | }; 103 | } 104 | 105 | const summary: SignResultSummary = { 106 | totalCount: results.length, 107 | successCount: 0, 108 | alreadySignedCount: 0, 109 | failedCount: 0, 110 | signResults: { 111 | success: [], 112 | failed: [] 113 | } 114 | }; 115 | 116 | // 统计各种错误类型的数量 117 | results.forEach(result => { 118 | if (result.success) { 119 | if (result.message === '已经签到过了') { 120 | summary.alreadySignedCount += 1; 121 | summary.signResults.success.push(result); 122 | } else { 123 | summary.successCount += 1; 124 | summary.signResults.success.push(result); 125 | } 126 | } else { 127 | summary.failedCount += 1; 128 | summary.signResults.failed.push(result); 129 | } 130 | }); 131 | 132 | return summary; 133 | } 134 | 135 | /** 136 | * 格式化汇总结果为文本 137 | * @param summary - 汇总结果 138 | * @returns 格式化后的文本 139 | */ 140 | export function formatSummary(summary: SignResultSummary): string { 141 | let text = `📊 签到统计:\n`; 142 | text += `总计: ${summary.totalCount} 个贴吧\n`; 143 | text += `✅ 成功: ${summary.successCount} 个\n`; 144 | text += `📌 已签: ${summary.alreadySignedCount} 个\n`; 145 | text += `❌ 失败: ${summary.failedCount} 个`; 146 | 147 | // 添加失败原因统计 148 | if (summary.failedCount > 0) { 149 | // 整理失败原因 150 | const errorMessageCount: Record = {}; 151 | summary.signResults.failed.forEach(item => { 152 | if (!errorMessageCount[item.message]) { 153 | errorMessageCount[item.message] = 1; 154 | } else { 155 | errorMessageCount[item.message]++; 156 | } 157 | }); 158 | 159 | text += `\n\n❌ 失败原因:\n`; 160 | for (const [errorMessage, count] of Object.entries(errorMessageCount)) { 161 | text += `- ${errorMessage}: ${count} 个\n`; 162 | } 163 | } 164 | 165 | return text; 166 | } -------------------------------------------------------------------------------- /dist/dataProcessor.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.processSignResult = processSignResult; 4 | exports.summarizeResults = summarizeResults; 5 | exports.formatSummary = formatSummary; 6 | /** 7 | * 处理签到结果数据 8 | * @param signResult - 签到结果 9 | * @returns 处理后的签到结果 10 | */ 11 | function processSignResult(signResult) { 12 | var _a, _b, _c, _d, _e, _f, _g; 13 | // 解析签到结果 14 | if (!signResult) { 15 | return { success: false, message: '签到结果为空', info: {} }; 16 | } 17 | // 处理不同的成功状态码 18 | if (signResult.no === 0 && ((_a = signResult.data) === null || _a === void 0 ? void 0 : _a.errmsg) === 'success' && ((_b = signResult.data) === null || _b === void 0 ? void 0 : _b.errno) === 0) { 19 | return { 20 | success: true, 21 | message: '签到成功', 22 | info: { 23 | rank: (_d = (_c = signResult.data) === null || _c === void 0 ? void 0 : _c.uinfo) === null || _d === void 0 ? void 0 : _d.user_sign_rank, 24 | continueCount: (_f = (_e = signResult.data) === null || _e === void 0 ? void 0 : _e.uinfo) === null || _f === void 0 ? void 0 : _f.cont_sign_num 25 | } 26 | }; 27 | } 28 | else if (signResult.no === 1101) { 29 | return { 30 | success: true, 31 | message: '已经签到过了', 32 | info: {} 33 | }; 34 | } 35 | else if (signResult.no === 2150040) { 36 | return { 37 | success: false, 38 | message: '签到失败,需要验证码', 39 | info: { code: signResult.no } 40 | }; 41 | } 42 | else if (signResult.no === 1011) { 43 | return { 44 | success: false, 45 | message: '未加入此吧或等级不够', 46 | info: { code: signResult.no } 47 | }; 48 | } 49 | else if (signResult.no === 1102) { 50 | return { 51 | success: false, 52 | message: '签到过快', 53 | info: { code: signResult.no } 54 | }; 55 | } 56 | else if (signResult.no === 1010) { 57 | return { 58 | success: false, 59 | message: '目录出错', 60 | info: { code: signResult.no } 61 | }; 62 | } 63 | else { 64 | return { 65 | success: false, 66 | message: signResult.error || `签到失败,错误码: ${signResult.no || '未知'}`, 67 | info: { 68 | code: (_g = signResult.no) !== null && _g !== void 0 ? _g : -1, 69 | data: signResult.data 70 | } 71 | }; 72 | } 73 | } 74 | /** 75 | * 汇总签到结果 76 | * @param results - 所有贴吧的签到结果 77 | * @returns 汇总结果 78 | */ 79 | function summarizeResults(results) { 80 | if (!results || !Array.isArray(results)) { 81 | return { 82 | totalCount: 0, 83 | successCount: 0, 84 | alreadySignedCount: 0, 85 | failedCount: 0, 86 | signResults: { 87 | success: [], 88 | failed: [] 89 | } 90 | }; 91 | } 92 | const summary = { 93 | totalCount: results.length, 94 | successCount: 0, 95 | alreadySignedCount: 0, 96 | failedCount: 0, 97 | signResults: { 98 | success: [], 99 | failed: [] 100 | } 101 | }; 102 | // 统计各种错误类型的数量 103 | results.forEach(result => { 104 | if (result.success) { 105 | if (result.message === '已经签到过了') { 106 | summary.alreadySignedCount += 1; 107 | summary.signResults.success.push(result); 108 | } 109 | else { 110 | summary.successCount += 1; 111 | summary.signResults.success.push(result); 112 | } 113 | } 114 | else { 115 | summary.failedCount += 1; 116 | summary.signResults.failed.push(result); 117 | } 118 | }); 119 | return summary; 120 | } 121 | /** 122 | * 格式化汇总结果为文本 123 | * @param summary - 汇总结果 124 | * @returns 格式化后的文本 125 | */ 126 | function formatSummary(summary) { 127 | let text = `📊 签到统计:\n`; 128 | text += `总计: ${summary.totalCount} 个贴吧\n`; 129 | text += `✅ 成功: ${summary.successCount} 个\n`; 130 | text += `📌 已签: ${summary.alreadySignedCount} 个\n`; 131 | text += `❌ 失败: ${summary.failedCount} 个`; 132 | // 添加失败原因统计 133 | if (summary.failedCount > 0) { 134 | // 整理失败原因 135 | const errorMessageCount = {}; 136 | summary.signResults.failed.forEach(item => { 137 | if (!errorMessageCount[item.message]) { 138 | errorMessageCount[item.message] = 1; 139 | } 140 | else { 141 | errorMessageCount[item.message]++; 142 | } 143 | }); 144 | text += `\n\n❌ 失败原因:\n`; 145 | for (const [errorMessage, count] of Object.entries(errorMessageCount)) { 146 | text += `- ${errorMessage}: ${count} 个\n`; 147 | } 148 | } 149 | return text; 150 | } 151 | -------------------------------------------------------------------------------- /js-backup/apiService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 百度贴吧API服务 3 | * 包含与百度贴吧API通信的核心功能 4 | */ 5 | const axios = require('axios'); 6 | const { toQueryString, generateDeviceId } = require('./utils'); 7 | 8 | // 辅助函数:延时等待 9 | const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); 10 | 11 | // 全局配置 12 | const MAX_RETRIES = 3; // 最大重试次数 13 | const RETRY_DELAY = 3000; // 重试延迟(ms) 14 | const RETRY_MULTIPLIER = 2; // 重试延迟倍数 15 | 16 | /** 17 | * 带重试机制的请求函数 18 | * @param {Function} requestFn - 请求函数 19 | * @param {string} operationName - 操作名称 20 | * @param {number} maxRetries - 最大重试次数 21 | * @param {number} initialDelay - 初始延迟(ms) 22 | * @param {number} delayMultiplier - 延迟倍数 23 | * @returns {Promise} 请求结果 24 | */ 25 | async function withRetry(requestFn, operationName, maxRetries = MAX_RETRIES, initialDelay = RETRY_DELAY, delayMultiplier = RETRY_MULTIPLIER) { 26 | let retries = 0; 27 | let delay = initialDelay; 28 | 29 | while (true) { 30 | try { 31 | return await requestFn(); 32 | } catch (error) { 33 | retries++; 34 | 35 | // 429错误特殊处理 36 | const isRateLimited = error.response && error.response.status === 429; 37 | 38 | if (retries > maxRetries || (!isRateLimited && error.response && error.response.status >= 400 && error.response.status < 500)) { 39 | console.error(`❌ ${operationName}失败(尝试 ${retries}次): ${error.message}`); 40 | throw error; 41 | } 42 | 43 | // 计算下次重试延迟 44 | if (isRateLimited) { 45 | // 限流时使用更长的延迟 46 | delay = delay * delayMultiplier * 2; 47 | console.warn(`⏳ 请求被限流,将在 ${delay}ms 后重试 (${retries}/${maxRetries})...`); 48 | } else { 49 | delay = delay * delayMultiplier; 50 | console.warn(`⏳ ${operationName}失败,将在 ${delay}ms 后重试 (${retries}/${maxRetries})...`); 51 | } 52 | 53 | await sleep(delay); 54 | } 55 | } 56 | } 57 | 58 | /** 59 | * 验证BDUSS是否有效并获取用户信息 60 | * @param {string} bduss - 百度BDUSS 61 | * @returns {Promise} 用户信息 62 | */ 63 | async function login(bduss) { 64 | return withRetry(async () => { 65 | // 通过获取用户同步信息来验证BDUSS是否有效 66 | const url = 'https://tieba.baidu.com/mo/q/sync'; 67 | const headers = { 68 | 'Cookie': `BDUSS=${bduss}`, 69 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 70 | 'Accept-Encoding': 'gzip, deflate, br', 71 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 72 | 'Connection': 'keep-alive', 73 | 'Host': 'tieba.baidu.com', 74 | 'Referer': 'https://tieba.baidu.com/home/main', 75 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari/604.1' 76 | }; 77 | 78 | const response = await axios.get(url, { 79 | headers: headers 80 | }); 81 | 82 | if (!response.data || response.data.no !== 0 || response.data.error !== 'success') { 83 | throw new Error('验证BDUSS失败,可能已过期'); 84 | } 85 | 86 | const userId = response.data.data.user_id; 87 | 88 | const userInfo = { 89 | status: 200, 90 | bduss: bduss, 91 | userId: userId, 92 | isValid: true, 93 | deviceId: generateDeviceId() 94 | }; 95 | 96 | console.log('🔐 验证BDUSS成功'); 97 | return userInfo; 98 | }, '验证BDUSS'); 99 | } 100 | 101 | /** 102 | * 获取用户关注的贴吧列表及TBS 103 | * @param {string} bduss - 百度BDUSS 104 | * @returns {Promise} 贴吧列表和TBS 105 | */ 106 | async function getTiebaList(bduss) { 107 | return withRetry(async () => { 108 | const url = 'https://tieba.baidu.com/mo/q/newmoindex'; 109 | const headers = { 110 | 'Cookie': `BDUSS=${bduss}`, 111 | 'Content-Type': 'application/octet-stream', 112 | 'Referer': 'https://tieba.baidu.com/index/tbwise/forum', 113 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari/604.1' 114 | }; 115 | 116 | const response = await axios.get(url, { 117 | headers: headers 118 | }); 119 | 120 | if (!response.data || response.data.error !== 'success') { 121 | throw new Error(`获取贴吧列表失败: ${response.data?.error_msg || '未知错误'}`); 122 | } 123 | 124 | // 获取TBS和贴吧列表 125 | const tbs = response.data.data.tbs; 126 | const tiebaList = response.data.data.like_forum || []; 127 | 128 | console.log(`🔍 获取贴吧列表成功, 共 ${tiebaList.length} 个贴吧`); 129 | 130 | return tiebaList; 131 | }, '获取贴吧列表'); 132 | } 133 | 134 | /** 135 | * 获取TBS参数 136 | * @param {string} bduss - 百度BDUSS 137 | * @returns {Promise} tbs参数 138 | */ 139 | async function getTbs(bduss) { 140 | return withRetry(async () => { 141 | const url = 'http://tieba.baidu.com/dc/common/tbs'; 142 | const headers = { 143 | 'Cookie': `BDUSS=${bduss}`, 144 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari/604.1' 145 | }; 146 | 147 | const response = await axios.get(url, { 148 | headers: headers 149 | }); 150 | 151 | if (!response.data || !response.data.tbs) { 152 | throw new Error('获取tbs失败'); 153 | } 154 | 155 | return response.data.tbs; 156 | }, '获取TBS参数'); 157 | } 158 | 159 | /** 160 | * 签到单个贴吧 161 | * @param {string} bduss - 百度BDUSS 162 | * @param {string} tiebaName - 贴吧名称 163 | * @param {string} tbs - 签到所需的tbs参数 164 | * @param {number} index - 贴吧索引号 165 | * @returns {Promise} 签到结果 166 | */ 167 | async function signTieba(bduss, tiebaName, tbs, index) { 168 | return withRetry(async () => { 169 | const url = 'https://tieba.baidu.com/sign/add'; 170 | const headers = { 171 | 'Cookie': `BDUSS=${bduss}`, 172 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 173 | 'Accept-Encoding': 'gzip,deflate,br', 174 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 175 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 176 | 'Connection': 'keep-alive', 177 | 'Host': 'tieba.baidu.com', 178 | 'Referer': 'https://tieba.baidu.com/', 179 | 'x-requested-with': 'XMLHttpRequest', 180 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36 Edg/84.0.522.63' 181 | }; 182 | 183 | const data = { 184 | tbs: tbs, 185 | kw: tiebaName, 186 | ie: 'utf-8' 187 | }; 188 | 189 | const response = await axios.post(url, toQueryString(data), { 190 | headers: headers 191 | }); 192 | 193 | if (!response.data) { 194 | throw new Error('签到响应数据为空'); 195 | } 196 | 197 | return response.data; 198 | }, `签到操作`); 199 | } 200 | 201 | module.exports = { 202 | login, 203 | getTiebaList, 204 | signTieba, 205 | getTbs 206 | }; -------------------------------------------------------------------------------- /src/apiService.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 百度贴吧API服务 3 | * 包含与百度贴吧API通信的核心功能 4 | */ 5 | import axios from 'axios'; 6 | import type { AxiosResponse, AxiosError } from 'axios'; 7 | import { toQueryString, generateDeviceId } from './utils'; 8 | import { 9 | UserInfo, 10 | TiebaInfo, 11 | TiebaList, 12 | SignResult, 13 | TbsResult 14 | } from './types/apiService.types'; 15 | 16 | // 辅助函数:延时等待 17 | const sleep = (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)); 18 | 19 | // 全局配置 20 | const MAX_RETRIES = 3; // 最大重试次数 21 | const RETRY_DELAY = 3000; // 重试延迟(ms) 22 | const RETRY_MULTIPLIER = 2; // 重试延迟倍数 23 | 24 | /** 25 | * 带重试机制的请求函数 26 | * @param requestFn - 请求函数 27 | * @param operationName - 操作名称 28 | * @param maxRetries - 最大重试次数 29 | * @param initialDelay - 初始延迟(ms) 30 | * @param delayMultiplier - 延迟倍数 31 | * @returns 请求结果 32 | */ 33 | async function withRetry( 34 | requestFn: () => Promise, 35 | operationName: string, 36 | maxRetries: number = MAX_RETRIES, 37 | initialDelay: number = RETRY_DELAY, 38 | delayMultiplier: number = RETRY_MULTIPLIER 39 | ): Promise { 40 | let retries = 0; 41 | let delay = initialDelay; 42 | 43 | while (true) { 44 | try { 45 | return await requestFn(); 46 | } catch (error) { 47 | retries++; 48 | 49 | const axiosError = error as AxiosError; 50 | 51 | // 429错误特殊处理 52 | const isRateLimited = axiosError.response && axiosError.response.status === 429; 53 | 54 | if (retries > maxRetries || (!isRateLimited && axiosError.response && axiosError.response.status >= 400 && axiosError.response.status < 500)) { 55 | console.error(`❌ ${operationName}失败(尝试 ${retries}次): ${axiosError.message}`); 56 | throw error; 57 | } 58 | 59 | // 计算下次重试延迟 60 | if (isRateLimited) { 61 | // 限流时使用更长的延迟 62 | delay = delay * delayMultiplier * 2; 63 | console.warn(`⏳ 请求被限流,将在 ${delay}ms 后重试 (${retries}/${maxRetries})...`); 64 | } else { 65 | delay = delay * delayMultiplier; 66 | console.warn(`⏳ ${operationName}失败,将在 ${delay}ms 后重试 (${retries}/${maxRetries})...`); 67 | } 68 | 69 | await sleep(delay); 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * 验证BDUSS是否有效并获取用户信息 76 | * @param bduss - 百度BDUSS 77 | * @returns 用户信息 78 | */ 79 | export async function login(bduss: string): Promise { 80 | return withRetry(async () => { 81 | // 通过获取用户同步信息来验证BDUSS是否有效 82 | const url = 'https://tieba.baidu.com/mo/q/sync'; 83 | const headers = { 84 | 'Cookie': `BDUSS=${bduss}`, 85 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 86 | 'Accept-Encoding': 'gzip, deflate, br', 87 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 88 | 'Connection': 'keep-alive', 89 | 'Host': 'tieba.baidu.com', 90 | 'Referer': 'https://tieba.baidu.com/home/main', 91 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari/604.1' 92 | }; 93 | 94 | const response = await axios.get(url, { 95 | headers: headers 96 | }); 97 | 98 | if (!response.data || response.data.no !== 0 || response.data.error !== 'success') { 99 | throw new Error('验证BDUSS失败,可能已过期'); 100 | } 101 | 102 | const userId = response.data.data.user_id; 103 | 104 | const userInfo: UserInfo = { 105 | status: 200, 106 | bduss: bduss, 107 | userId: userId, 108 | isValid: true, 109 | deviceId: generateDeviceId() 110 | }; 111 | 112 | console.log('🔐 验证BDUSS成功'); 113 | return userInfo; 114 | }, '验证BDUSS'); 115 | } 116 | 117 | /** 118 | * 获取用户关注的贴吧列表及TBS 119 | * @param bduss - 百度BDUSS 120 | * @returns 贴吧列表和TBS 121 | */ 122 | export async function getTiebaList(bduss: string): Promise { 123 | return withRetry(async () => { 124 | const url = 'https://tieba.baidu.com/mo/q/newmoindex'; 125 | const headers = { 126 | 'Cookie': `BDUSS=${bduss}`, 127 | 'Content-Type': 'application/octet-stream', 128 | 'Referer': 'https://tieba.baidu.com/index/tbwise/forum', 129 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari/604.1' 130 | }; 131 | 132 | const response = await axios.get(url, { 133 | headers: headers 134 | }); 135 | 136 | if (!response.data || response.data.error !== 'success') { 137 | throw new Error(`获取贴吧列表失败: ${response.data?.error_msg || '未知错误'}`); 138 | } 139 | 140 | // 获取TBS和贴吧列表 141 | const tiebaList = response.data.data.like_forum || []; 142 | 143 | console.log(`🔍 获取贴吧列表成功, 共 ${tiebaList.length} 个贴吧`); 144 | 145 | return tiebaList; 146 | }, '获取贴吧列表'); 147 | } 148 | 149 | /** 150 | * 获取TBS参数 151 | * @param bduss - 百度BDUSS 152 | * @returns tbs参数 153 | */ 154 | export async function getTbs(bduss: string): Promise { 155 | return withRetry(async () => { 156 | const url = 'http://tieba.baidu.com/dc/common/tbs'; 157 | const headers = { 158 | 'Cookie': `BDUSS=${bduss}`, 159 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari/604.1' 160 | }; 161 | 162 | const response = await axios.get(url, { 163 | headers: headers 164 | }); 165 | 166 | if (!response.data || !response.data.tbs) { 167 | throw new Error('获取tbs失败'); 168 | } 169 | 170 | return response.data.tbs; 171 | }, '获取TBS参数'); 172 | } 173 | 174 | /** 175 | * 签到单个贴吧 176 | * @param bduss - 百度BDUSS 177 | * @param tiebaName - 贴吧名称 178 | * @param tbs - 签到所需的tbs参数 179 | * @param index - 贴吧索引号 180 | * @returns 签到结果 181 | */ 182 | export async function signTieba(bduss: string, tiebaName: string, tbs: string, index: number): Promise { 183 | return withRetry(async () => { 184 | const url = 'https://tieba.baidu.com/sign/add'; 185 | const headers = { 186 | 'Cookie': `BDUSS=${bduss}`, 187 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 188 | 'Accept-Encoding': 'gzip,deflate,br', 189 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 190 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 191 | 'Connection': 'keep-alive', 192 | 'Host': 'tieba.baidu.com', 193 | 'Referer': 'https://tieba.baidu.com/', 194 | 'x-requested-with': 'XMLHttpRequest', 195 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36 Edg/84.0.522.63' 196 | }; 197 | 198 | const data = { 199 | tbs: tbs, 200 | kw: tiebaName, 201 | ie: 'utf-8' 202 | }; 203 | 204 | const response = await axios.post(url, toQueryString(data), { 205 | headers: headers 206 | }); 207 | 208 | if (!response.data) { 209 | throw new Error('签到响应数据为空'); 210 | } 211 | 212 | return response.data; 213 | }, `签到操作`); 214 | } -------------------------------------------------------------------------------- /js-backup/notify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 通知模块 - 支持多种推送渠道发送脚本运行结果通知 3 | */ 4 | const axios = require('axios'); 5 | const { formatDate } = require('./utils'); 6 | 7 | /** 8 | * 构建通知标题 9 | * @returns {string} 通知标题 10 | */ 11 | function getNotifyTitle() { 12 | return `百度贴吧自动签到 - ${formatDate(new Date(), 'Asia/Shanghai', '+8').split(' ')[0]}`; 13 | } 14 | 15 | /** 16 | * 处理通知发送的结果 17 | * @param {string} platform - 平台名称 18 | * @param {Object} response - 响应结果 19 | */ 20 | function handleNotifyResult(platform, response) { 21 | if (response && response.status === 200) { 22 | console.log(`✅ ${platform}通知发送成功`); 23 | } else { 24 | console.log(`⚠️ ${platform}通知发送失败: ${response?.statusText || '未知错误'}`); 25 | } 26 | } 27 | 28 | /** 29 | * Server酱通知 (ServerChan) 30 | * @param {string} key - Server酱发送KEY (SCKey) 31 | * @param {string} title - 通知标题 32 | * @param {string} content - 通知内容 33 | * @returns {Promise} 发送结果 34 | */ 35 | async function sendServerChan(key, title, content) { 36 | if (!key) return { success: false, message: 'Server酱KEY未设置' }; 37 | 38 | try { 39 | const url = `https://sctapi.ftqq.com/${key}.send`; 40 | const response = await axios.post(url, { 41 | title: title, 42 | desp: content 43 | }); 44 | handleNotifyResult('Server酱', response); 45 | return { success: true }; 46 | } catch (error) { 47 | console.error(`❌ Server酱通知发送失败: ${error.message}`); 48 | return { success: false, message: error.message }; 49 | } 50 | } 51 | 52 | /** 53 | * Bark通知 54 | * @param {string} key - Bark推送KEY 55 | * @param {string} title - 通知标题 56 | * @param {string} content - 通知内容 57 | * @returns {Promise} 发送结果 58 | */ 59 | async function sendBark(key, title, content) { 60 | if (!key) return { success: false, message: 'Bark KEY未设置' }; 61 | 62 | try { 63 | // 处理Bark地址,兼容自建服务和官方服务 64 | let barkUrl = key; 65 | if (!barkUrl.startsWith('http')) { 66 | barkUrl = `https://api.day.app/${key}`; 67 | } 68 | if (!barkUrl.endsWith('/')) { 69 | barkUrl += '/'; 70 | } 71 | 72 | const url = `${barkUrl}${encodeURIComponent(title)}/${encodeURIComponent(content)}`; 73 | const response = await axios.get(url); 74 | handleNotifyResult('Bark', response); 75 | return { success: true }; 76 | } catch (error) { 77 | console.error(`❌ Bark通知发送失败: ${error.message}`); 78 | return { success: false, message: error.message }; 79 | } 80 | } 81 | 82 | /** 83 | * Telegram Bot通知 84 | * @param {string} botToken - Telegram Bot Token 85 | * @param {string} chatId - Telegram Chat ID 86 | * @param {string} title - 通知标题 87 | * @param {string} content - 通知内容 88 | * @returns {Promise} 发送结果 89 | */ 90 | async function sendTelegram(botToken, chatId, title, content) { 91 | if (!botToken || !chatId) return { success: false, message: 'Telegram配置不完整' }; 92 | 93 | try { 94 | const url = `https://api.telegram.org/bot${botToken}/sendMessage`; 95 | const text = `${title}\n\n${content}`; 96 | 97 | const response = await axios.post(url, { 98 | chat_id: chatId, 99 | text: text, 100 | parse_mode: 'Markdown' 101 | }); 102 | handleNotifyResult('Telegram', response); 103 | return { success: true }; 104 | } catch (error) { 105 | console.error(`❌ Telegram通知发送失败: ${error.message}`); 106 | return { success: false, message: error.message }; 107 | } 108 | } 109 | 110 | /** 111 | * 钉钉机器人通知 112 | * @param {string} webhook - 钉钉Webhook地址 113 | * @param {string} secret - 钉钉安全密钥 114 | * @param {string} title - 通知标题 115 | * @param {string} content - 通知内容 116 | * @returns {Promise} 发送结果 117 | */ 118 | async function sendDingTalk(webhook, secret, title, content) { 119 | if (!webhook) return { success: false, message: '钉钉Webhook未设置' }; 120 | 121 | try { 122 | // 如果有安全密钥,需要计算签名 123 | let url = webhook; 124 | if (secret) { 125 | const crypto = require('crypto'); 126 | const timestamp = Date.now(); 127 | const hmac = crypto.createHmac('sha256', secret); 128 | const sign = encodeURIComponent(hmac.update(`${timestamp}\n${secret}`).digest('base64')); 129 | url = `${webhook}×tamp=${timestamp}&sign=${sign}`; 130 | } 131 | 132 | const response = await axios.post(url, { 133 | msgtype: 'markdown', 134 | markdown: { 135 | title: title, 136 | text: `### ${title}\n${content}` 137 | } 138 | }); 139 | handleNotifyResult('钉钉', response); 140 | return { success: true }; 141 | } catch (error) { 142 | console.error(`❌ 钉钉通知发送失败: ${error.message}`); 143 | return { success: false, message: error.message }; 144 | } 145 | } 146 | 147 | /** 148 | * 企业微信通知 149 | * @param {string} key - 企业微信Webhook Key 150 | * @param {string} title - 通知标题 151 | * @param {string} content - 通知内容 152 | * @returns {Promise} 发送结果 153 | */ 154 | async function sendWecom(key, title, content) { 155 | if (!key) return { success: false, message: '企业微信KEY未设置' }; 156 | 157 | try { 158 | const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${key}`; 159 | const response = await axios.post(url, { 160 | msgtype: 'markdown', 161 | markdown: { 162 | content: `### ${title}\n${content}` 163 | } 164 | }); 165 | handleNotifyResult('企业微信', response); 166 | return { success: true }; 167 | } catch (error) { 168 | console.error(`❌ 企业微信通知发送失败: ${error.message}`); 169 | return { success: false, message: error.message }; 170 | } 171 | } 172 | 173 | /** 174 | * PushPlus通知 175 | * @param {string} token - PushPlus Token 176 | * @param {string} title - 通知标题 177 | * @param {string} content - 通知内容 178 | * @returns {Promise} 发送结果 179 | */ 180 | async function sendPushPlus(token, title, content) { 181 | if (!token) return { success: false, message: 'PushPlus Token未设置' }; 182 | 183 | try { 184 | const url = 'https://www.pushplus.plus/send'; 185 | const response = await axios.post(url, { 186 | token: token, 187 | title: title, 188 | content: content, 189 | template: 'markdown' 190 | }); 191 | handleNotifyResult('PushPlus', response); 192 | return { success: true }; 193 | } catch (error) { 194 | console.error(`❌ PushPlus通知发送失败: ${error.message}`); 195 | return { success: false, message: error.message }; 196 | } 197 | } 198 | 199 | /** 200 | * 发送通知到所有已配置的平台 201 | * @param {string} summary - 签到结果摘要 202 | */ 203 | async function sendNotification(summary) { 204 | try { 205 | console.log('📣 正在发送通知...'); 206 | const title = getNotifyTitle(); 207 | const content = summary; 208 | 209 | const notifyTasks = []; 210 | let notifyCount = 0; 211 | 212 | // Server酱通知 213 | if (process.env.SERVERCHAN_KEY) { 214 | notifyTasks.push(sendServerChan(process.env.SERVERCHAN_KEY, title, content)); 215 | notifyCount++; 216 | } 217 | 218 | // Bark通知 219 | if (process.env.BARK_KEY) { 220 | notifyTasks.push(sendBark(process.env.BARK_KEY, title, content)); 221 | notifyCount++; 222 | } 223 | 224 | // Telegram通知 225 | if (process.env.TG_BOT_TOKEN && process.env.TG_CHAT_ID) { 226 | notifyTasks.push(sendTelegram(process.env.TG_BOT_TOKEN, process.env.TG_CHAT_ID, title, content)); 227 | notifyCount++; 228 | } 229 | 230 | // 钉钉通知 231 | if (process.env.DINGTALK_WEBHOOK) { 232 | notifyTasks.push(sendDingTalk(process.env.DINGTALK_WEBHOOK, process.env.DINGTALK_SECRET, title, content)); 233 | notifyCount++; 234 | } 235 | 236 | // 企业微信通知 237 | if (process.env.WECOM_KEY) { 238 | notifyTasks.push(sendWecom(process.env.WECOM_KEY, title, content)); 239 | notifyCount++; 240 | } 241 | 242 | // PushPlus通知 243 | if (process.env.PUSHPLUS_TOKEN) { 244 | notifyTasks.push(sendPushPlus(process.env.PUSHPLUS_TOKEN, title, content)); 245 | notifyCount++; 246 | } 247 | 248 | if (notifyCount === 0) { 249 | console.log('ℹ️ 未配置任何通知渠道,跳过通知发送'); 250 | return; 251 | } 252 | 253 | // 等待所有通知任务完成 254 | const results = await Promise.all(notifyTasks); 255 | const successCount = results.filter(r => r.success).length; 256 | 257 | console.log(`📬 通知发送完成: ${successCount}/${notifyCount} 个渠道成功`); 258 | } catch (error) { 259 | console.error(`❌ 发送通知时发生错误: ${error.message}`); 260 | } 261 | } 262 | 263 | module.exports = { 264 | sendNotification 265 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 百度贴吧自动签到 4 | 5 | [![百度贴吧](https://img.shields.io/badge/百度贴吧-passing-success.svg?style=flat-square&logo=baidu&logoWidth=20&logoColor=white)](https://github.com/chiupam/tieba/actions) 6 | [![JavaScript](https://img.shields.io/badge/JavaScript-ES6-yellow.svg?style=flat-square&logo=javascript)](https://www.javascript.com/) 7 | [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg?style=flat-square&logo=typescript)](https://www.typescriptlang.org/) 8 | [![Node.js](https://img.shields.io/badge/Node.js-16.x-green.svg?style=flat-square&logo=node.js)](https://nodejs.org/) 9 | [![GitHub stars](https://img.shields.io/github/stars/chiupam/tieba?style=flat-square&logo=github)](https://github.com/chiupam/tieba/stargazers) 10 | [![GitHub forks](https://img.shields.io/github/forks/chiupam/tieba?style=flat-square&logo=github)](https://github.com/chiupam/tieba/network/members) 11 | [![License](https://img.shields.io/github/license/chiupam/tieba?style=flat-square)](LICENSE) 12 | 13 | 这是一个基于 GitHub Actions 的百度贴吧自动签到工具,可以帮助你每天自动完成贴吧签到,不会错过任何奖励。 14 | 15 |
16 | 17 | ## ✨ 功能特点 18 | 19 | - 🔄 自动签到:每天自动完成所有关注贴吧的签到 20 | - 🔐 安全可靠:只需配置BDUSS环境变量,无需泄露账号密码 21 | - 📊 详细统计:签到后生成详细的统计报告,包含签到排名和连签天数 22 | - 🚀 部署简单:一次配置,持续运行,无需服务器 23 | - ⚡ 批量处理:支持批量签到,提高效率并避免请求限制 24 | - 🔁 智能重试:对签到失败的贴吧自动进行多次重试,提高签到成功率 25 | - 🧰 TypeScript:使用TypeScript开发,提供更好的类型安全和代码可维护性 26 | 27 | ## 📝 使用方法 28 | 29 | ### 🔑 1. 获取百度 BDUSS 30 | 31 | 首先需要获取百度的 BDUSS,这是百度贴吧 API 的登录凭证。 32 | 33 | 获取方法: 34 | 1. 登录百度贴吧网页版 35 | 2. 打开浏览器开发者工具(F12) 36 | 3. 切换到 "Application" 或 "应用" 标签 37 | 4. 在左侧找到 "Cookies" 38 | 5. 找到并复制 BDUSS 的值 39 | 40 | > ⚠️ 注意:请勿泄露BDUSS,它相当于您的登录凭证! 41 | 42 | ### 🔱 2. Fork 本仓库 43 | 44 | 点击本仓库右上角的 "Fork" 按钮,将本项目复制到您自己的 GitHub 账号下。 45 | 46 | ### 🔒 3. 配置 GitHub Secrets 47 | 48 | 在您 Fork 的仓库中: 49 | 50 | 1. 点击 "Settings" → "Secrets and variables" → "Actions" 51 | 2. 点击 "New repository secret" 按钮 52 | 3. 添加 Secret: 53 | - 名称:`BDUSS` 54 | - 值:您的百度 BDUSS 值 55 | 56 | ### ▶️ 4. 启用 GitHub Actions 57 | 58 | 1. 在您 Fork 的仓库中,点击 "Actions" 标签 59 | 2. 点击 "I understand my workflows, go ahead and enable them" 60 | 3. 找到 "百度贴吧自动签到" workflow 并启用 61 | 62 | 现在,系统会按照预设的时间(默认每天凌晨)自动运行签到脚本。 63 | 64 | ## 🖱️ 手动触发签到 65 | 66 | 如果你想立即测试签到功能,可以手动触发: 67 | 68 | 1. 进入 "Actions" 标签 69 | 2. 选择 "百度贴吧自动签到" workflow 70 | 3. 点击 "Run workflow" 按钮 71 | 4. 点击 "Run workflow" 确认运行 72 | 73 | > 💡 **提示**:手动触发时会使用您在GitHub Secrets中配置的环境变量,如果没有配置则使用默认值。 74 | 75 | ## ⚙️ 高级设置 76 | 77 | ### 💻 环境变量配置 78 | 79 | 本项目使用环境变量进行配置,可以在GitHub Secrets中设置以下变量: 80 | 81 | #### 基础配置 82 | 83 | | 变量名 | 必填 | 说明 | 默认值 | 84 | | ----- | ---- | ---- | ----- | 85 | | `BDUSS` | ✅ | 百度贴吧登录凭证,用于身份验证 | 无 | 86 | | `BATCH_SIZE` | ❌ | 每批签到的贴吧数量 | 20 | 87 | | `BATCH_INTERVAL` | ❌ | 批次之间的等待时间(毫秒) | 1000 | 88 | | `MAX_RETRIES` | ❌ | 签到失败时的最大重试次数 | 3 | 89 | | `RETRY_INTERVAL` | ❌ | 重试之间的等待时间(毫秒) | 5000 | 90 | | `ENABLE_NOTIFY` | ❌ | 是否启用通知推送功能 | false | 91 | 92 | #### 📲 通知配置 93 | 94 | 启用通知推送功能后(`ENABLE_NOTIFY=true`),可以配置以下通知渠道(至少需要配置一个): 95 | 96 | | 变量名 | 说明 | 参考文档 | 97 | | ----- | ---- | ------- | 98 | | `SERVERCHAN_KEY` | Server酱的推送密钥 | [Server酱文档](https://sct.ftqq.com/) | 99 | | `BARK_KEY` | Bark推送密钥或完整URL | [Bark文档](https://github.com/Finb/Bark) | 100 | | `TG_BOT_TOKEN` | Telegram机器人Token | [Telegram Bot API](https://core.telegram.org/bots/api) | 101 | | `TG_CHAT_ID` | Telegram接收消息的用户或群组ID | [获取Chat ID教程](https://core.telegram.org/bots/features#chat-id) | 102 | | `DINGTALK_WEBHOOK` | 钉钉机器人的Webhook URL | [钉钉自定义机器人文档](https://open.dingtalk.com/document/robots/custom-robot-access) | 103 | | `DINGTALK_SECRET` | 钉钉机器人的安全密钥(可选) | 同上 | 104 | | `WECOM_KEY` | 企业微信机器人的WebHook Key | [企业微信机器人文档](https://developer.work.weixin.qq.com/document/path/91770) | 105 | | `PUSHPLUS_TOKEN` | PushPlus推送Token | [PushPlus文档](https://www.pushplus.plus/) | 106 | 107 | > 💡 **提示**:您可以根据自己的需求配置一个或多个通知渠道。如果配置了多个渠道,脚本将向所有渠道发送通知。 108 | 109 | **设置方法**: 110 | - 在仓库中点击 Settings → Secrets and variables → Actions 111 | - 点击 "New repository secret" 添加以上对应的配置项 112 | 113 | ### ⏰ 自定义签到时间 114 | 115 | 修改 `.github/workflows/tieba-signin.yml` 文件中的 cron 表达式。默认为每天凌晨 2 点(UTC 时间,即北京时间上午 10 点)。 116 | 117 | ## 📈 查看签到结果 118 | 119 | 签到完成后,可以在 Actions 的运行记录中查看详细的签到结果和统计信息,包括: 120 | - ✅ 签到成功数量与贴吧列表 121 | - 📌 已经签到的贴吧数量 122 | - ❌ 签到失败的贴吧及原因 123 | - 🏅 成功签到贴吧的排名和连签天数 124 | 125 | ## 🧪 本地测试 126 | 127 | 您可以在本地环境中测试签到功能,无需依赖GitHub Actions。 128 | 129 | ### 本地测试准备工作 130 | 131 | 1. 克隆此仓库到本地: 132 | ```bash 133 | git clone https://github.com/chiupam/tieba.git 134 | cd tieba 135 | ``` 136 | 137 | 2. 安装依赖: 138 | ```bash 139 | npm install 140 | ``` 141 | 142 | 3. 创建`.env`文件,用于配置本地测试环境变量: 143 | ```bash 144 | cp .env.example .env 145 | ``` 146 | 147 | 4. 编辑`.env`文件,填入您的`BDUSS`和其他配置 148 | 149 | ### 测试命令 150 | 151 | 运行本地测试: 152 | ```bash 153 | npm test 154 | ``` 155 | 156 | 这个命令会先编译TypeScript代码,然后执行local-test.js文件。 157 | 158 | > 💡 **提示**: 159 | > - 要测试BDUSS失效的情况,可以在`.env`文件中填写错误的BDUSS值 160 | > - 本地测试默认使用更小的批次大小和间隔时间 161 | 162 | ### 开发环境运行 163 | 164 | 如果你想在开发过程中直接使用ts-node运行TypeScript文件,可以使用: 165 | 166 | ```bash 167 | npm run dev 168 | ``` 169 | 170 | ### 通知行为 171 | 172 | 本项目的通知逻辑为: 173 | 174 | 1. 只有当以下情况时才会发送通知: 175 | - **贴吧签到失败**且**开启了通知功能**时 176 | - **BDUSS失效**且**开启了通知功能**时 177 | 2. 当所有贴吧签到成功时,即使开启了通知功能也不会发送通知 178 | 179 | > 💡 **提示**:本地测试时BDUSS的有效性会直接影响到结果,如需测试不同场景,可以修改`.env`文件中的BDUSS值 180 | 181 | ## 🔄 仓库同步 182 | 183 | 如果您Fork了本仓库,您可以启用自动同步功能,这样您的Fork仓库将会定期与上游仓库保持同步。 184 | 185 | ### 启用自动同步 186 | 187 | 1. 在您Fork的仓库中,进入 "Actions" 标签页 188 | 2. 您可能会看到一个提示,点击 "I understand my workflows, go ahead and enable them" 189 | 3. 在工作流列表中找到 "同步上游仓库更新" 并启用它 190 | 4. 此工作流将会每天自动运行,也可以通过点击 "Run workflow" 手动触发 191 | 192 | 启用后,您的Fork仓库将会自动与原仓库保持同步,无需手动操作。 193 | 194 | ## 📁 项目结构 195 | 196 | ``` 197 | tieba/ 198 | ├── .github/ # GitHub相关配置 199 | │ └── workflows/ # GitHub Actions工作流 200 | │ ├── build.yml # TypeScript构建工作流 201 | │ ├── keep-alive.yml # 保持仓库活跃工作流 202 | │ ├── sync-upstream.yml # 同步上游仓库工作流 203 | │ └── tieba-signin.yml # 贴吧签到主工作流 204 | ├── src/ # 源代码目录 205 | │ ├── apiService.ts # API服务模块,处理与百度服务器的通信 206 | │ ├── dataProcessor.ts # 数据处理模块,负责处理和生成统计数据 207 | │ ├── index.ts # 主程序入口,协调各模块工作并实现签到逻辑 208 | │ ├── local-test.ts # 本地测试脚本 209 | │ ├── notify.ts # 通知模块,处理各种推送服务的消息发送 210 | │ ├── utils.ts # 工具函数模块,提供通用功能 211 | │ └── types/ # TypeScript类型定义 212 | │ ├── index.ts # 类型定义入口 213 | │ ├── apiService.types.ts # API服务相关类型 214 | │ ├── dataProcessor.types.ts # 数据处理相关类型 215 | │ ├── notify.types.ts # 通知相关类型 216 | │ └── utils.types.ts # 工具函数相关类型 217 | ├── dist/ # 编译后的JavaScript代码(由build.yml自动构建并提交) 218 | │ ├── types/ # 编译后的类型定义 219 | │ ├── apiService.js # 编译后的API服务模块 220 | │ ├── dataProcessor.js # 编译后的数据处理模块 221 | │ ├── index.js # 编译后的主脚本文件 222 | │ ├── local-test.js # 编译后的本地测试脚本 223 | │ ├── notify.js # 编译后的通知模块 224 | │ └── utils.js # 编译后的工具函数模块 225 | ├── js-backup/ # JavaScript原始文件备份 226 | ├── .env.example # 环境变量示例文件 227 | ├── tsconfig.json # TypeScript配置文件 228 | ├── .gitignore # Git忽略文件配置 229 | ├── LICENSE # 开源协议 230 | ├── package.json # 项目配置和依赖管理 231 | ├── README.md # 项目说明文档 232 | └── tsconfig.json # TypeScript配置文件 233 | ``` 234 | 235 | ## ❓ 常见问题 236 | 237 | ### 🔧 签到失败怎么办? 238 | 239 | - 🔍 检查 BDUSS 是否正确且未过期 240 | - 📋 查看 Actions 运行日志,确认具体错误原因 241 | - 🔄 如果 BDUSS 过期,请重新获取并更新 Secret 242 | - ⏱️ 签到过快导致失败时,可以尝试增大批次间隔时间 243 | - 🔁 对于偶发的网络问题,脚本会自动进行重试(最多3次),可通过配置`MAX_RETRIES`和`RETRY_INTERVAL`调整重试次数和间隔 244 | 245 | ### 🆕 如何获取新BDUSS? 246 | 247 | BDUSS一般有效期较长,但如果失效,需要重新获取。方法同初始配置步骤,然后更新GitHub Secrets中的值。 248 | 249 | ## ⚖️ 免责声明 250 | 251 | 本工具仅供学习交流使用,请勿用于任何商业用途。使用本工具产生的任何后果由使用者自行承担。 252 | 253 | ## 📜 开源协议 254 | 255 | MIT 256 | 257 | ## ⚠️ 免责声明 258 | 259 | **请仔细阅读以下声明:** 260 | 261 | 1. 📚 本项目仅供学习和研究目的使用,不得用于商业或非法用途 262 | 2. 📋 使用本项目可能违反百度贴吧的服务条款,请自行评估使用风险 263 | 3. ⚠️ 本项目不保证功能的可用性,也不保证不会被百度官方检测或封禁 264 | 4. 🛡️ 使用本项目造成的任何问题,包括但不限于账号被封禁、数据丢失等,项目作者概不负责 265 | 5. 📢 用户需自行承担使用本项目的全部风险和法律责任 -------------------------------------------------------------------------------- /dist/apiService.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | exports.login = login; 16 | exports.getTiebaList = getTiebaList; 17 | exports.getTbs = getTbs; 18 | exports.signTieba = signTieba; 19 | /** 20 | * 百度贴吧API服务 21 | * 包含与百度贴吧API通信的核心功能 22 | */ 23 | const axios_1 = __importDefault(require("axios")); 24 | const utils_1 = require("./utils"); 25 | // 辅助函数:延时等待 26 | const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); 27 | // 全局配置 28 | const MAX_RETRIES = 3; // 最大重试次数 29 | const RETRY_DELAY = 3000; // 重试延迟(ms) 30 | const RETRY_MULTIPLIER = 2; // 重试延迟倍数 31 | /** 32 | * 带重试机制的请求函数 33 | * @param requestFn - 请求函数 34 | * @param operationName - 操作名称 35 | * @param maxRetries - 最大重试次数 36 | * @param initialDelay - 初始延迟(ms) 37 | * @param delayMultiplier - 延迟倍数 38 | * @returns 请求结果 39 | */ 40 | function withRetry(requestFn_1, operationName_1) { 41 | return __awaiter(this, arguments, void 0, function* (requestFn, operationName, maxRetries = MAX_RETRIES, initialDelay = RETRY_DELAY, delayMultiplier = RETRY_MULTIPLIER) { 42 | let retries = 0; 43 | let delay = initialDelay; 44 | while (true) { 45 | try { 46 | return yield requestFn(); 47 | } 48 | catch (error) { 49 | retries++; 50 | const axiosError = error; 51 | // 429错误特殊处理 52 | const isRateLimited = axiosError.response && axiosError.response.status === 429; 53 | if (retries > maxRetries || (!isRateLimited && axiosError.response && axiosError.response.status >= 400 && axiosError.response.status < 500)) { 54 | console.error(`❌ ${operationName}失败(尝试 ${retries}次): ${axiosError.message}`); 55 | throw error; 56 | } 57 | // 计算下次重试延迟 58 | if (isRateLimited) { 59 | // 限流时使用更长的延迟 60 | delay = delay * delayMultiplier * 2; 61 | console.warn(`⏳ 请求被限流,将在 ${delay}ms 后重试 (${retries}/${maxRetries})...`); 62 | } 63 | else { 64 | delay = delay * delayMultiplier; 65 | console.warn(`⏳ ${operationName}失败,将在 ${delay}ms 后重试 (${retries}/${maxRetries})...`); 66 | } 67 | yield sleep(delay); 68 | } 69 | } 70 | }); 71 | } 72 | /** 73 | * 验证BDUSS是否有效并获取用户信息 74 | * @param bduss - 百度BDUSS 75 | * @returns 用户信息 76 | */ 77 | function login(bduss) { 78 | return __awaiter(this, void 0, void 0, function* () { 79 | return withRetry(() => __awaiter(this, void 0, void 0, function* () { 80 | // 通过获取用户同步信息来验证BDUSS是否有效 81 | const url = 'https://tieba.baidu.com/mo/q/sync'; 82 | const headers = { 83 | 'Cookie': `BDUSS=${bduss}`, 84 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 85 | 'Accept-Encoding': 'gzip, deflate, br', 86 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 87 | 'Connection': 'keep-alive', 88 | 'Host': 'tieba.baidu.com', 89 | 'Referer': 'https://tieba.baidu.com/home/main', 90 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari/604.1' 91 | }; 92 | const response = yield axios_1.default.get(url, { 93 | headers: headers 94 | }); 95 | if (!response.data || response.data.no !== 0 || response.data.error !== 'success') { 96 | throw new Error('验证BDUSS失败,可能已过期'); 97 | } 98 | const userId = response.data.data.user_id; 99 | const userInfo = { 100 | status: 200, 101 | bduss: bduss, 102 | userId: userId, 103 | isValid: true, 104 | deviceId: (0, utils_1.generateDeviceId)() 105 | }; 106 | console.log('🔐 验证BDUSS成功'); 107 | return userInfo; 108 | }), '验证BDUSS'); 109 | }); 110 | } 111 | /** 112 | * 获取用户关注的贴吧列表及TBS 113 | * @param bduss - 百度BDUSS 114 | * @returns 贴吧列表和TBS 115 | */ 116 | function getTiebaList(bduss) { 117 | return __awaiter(this, void 0, void 0, function* () { 118 | return withRetry(() => __awaiter(this, void 0, void 0, function* () { 119 | var _a; 120 | const url = 'https://tieba.baidu.com/mo/q/newmoindex'; 121 | const headers = { 122 | 'Cookie': `BDUSS=${bduss}`, 123 | 'Content-Type': 'application/octet-stream', 124 | 'Referer': 'https://tieba.baidu.com/index/tbwise/forum', 125 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari/604.1' 126 | }; 127 | const response = yield axios_1.default.get(url, { 128 | headers: headers 129 | }); 130 | if (!response.data || response.data.error !== 'success') { 131 | throw new Error(`获取贴吧列表失败: ${((_a = response.data) === null || _a === void 0 ? void 0 : _a.error_msg) || '未知错误'}`); 132 | } 133 | // 获取TBS和贴吧列表 134 | const tiebaList = response.data.data.like_forum || []; 135 | console.log(`🔍 获取贴吧列表成功, 共 ${tiebaList.length} 个贴吧`); 136 | return tiebaList; 137 | }), '获取贴吧列表'); 138 | }); 139 | } 140 | /** 141 | * 获取TBS参数 142 | * @param bduss - 百度BDUSS 143 | * @returns tbs参数 144 | */ 145 | function getTbs(bduss) { 146 | return __awaiter(this, void 0, void 0, function* () { 147 | return withRetry(() => __awaiter(this, void 0, void 0, function* () { 148 | const url = 'http://tieba.baidu.com/dc/common/tbs'; 149 | const headers = { 150 | 'Cookie': `BDUSS=${bduss}`, 151 | 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari/604.1' 152 | }; 153 | const response = yield axios_1.default.get(url, { 154 | headers: headers 155 | }); 156 | if (!response.data || !response.data.tbs) { 157 | throw new Error('获取tbs失败'); 158 | } 159 | return response.data.tbs; 160 | }), '获取TBS参数'); 161 | }); 162 | } 163 | /** 164 | * 签到单个贴吧 165 | * @param bduss - 百度BDUSS 166 | * @param tiebaName - 贴吧名称 167 | * @param tbs - 签到所需的tbs参数 168 | * @param index - 贴吧索引号 169 | * @returns 签到结果 170 | */ 171 | function signTieba(bduss, tiebaName, tbs, index) { 172 | return __awaiter(this, void 0, void 0, function* () { 173 | return withRetry(() => __awaiter(this, void 0, void 0, function* () { 174 | const url = 'https://tieba.baidu.com/sign/add'; 175 | const headers = { 176 | 'Cookie': `BDUSS=${bduss}`, 177 | 'Accept': 'application/json, text/javascript, */*; q=0.01', 178 | 'Accept-Encoding': 'gzip,deflate,br', 179 | 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6', 180 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 181 | 'Connection': 'keep-alive', 182 | 'Host': 'tieba.baidu.com', 183 | 'Referer': 'https://tieba.baidu.com/', 184 | 'x-requested-with': 'XMLHttpRequest', 185 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135 Safari/537.36 Edg/84.0.522.63' 186 | }; 187 | const data = { 188 | tbs: tbs, 189 | kw: tiebaName, 190 | ie: 'utf-8' 191 | }; 192 | const response = yield axios_1.default.post(url, (0, utils_1.toQueryString)(data), { 193 | headers: headers 194 | }); 195 | if (!response.data) { 196 | throw new Error('签到响应数据为空'); 197 | } 198 | return response.data; 199 | }), `签到操作`); 200 | }); 201 | } 202 | -------------------------------------------------------------------------------- /src/notify.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 通知模块 - 支持多种推送渠道发送脚本运行结果通知 3 | */ 4 | import axios from 'axios'; 5 | import type { AxiosResponse } from 'axios'; 6 | import { formatDate } from './utils'; 7 | import { 8 | NotifyOptions, 9 | NotifyResult, 10 | ServerChanOptions, 11 | BarkOptions, 12 | TelegramOptions, 13 | DingTalkOptions, 14 | WeComOptions, 15 | PushPlusOptions 16 | } from './types/notify.types'; 17 | import * as crypto from 'crypto'; 18 | 19 | /** 20 | * 构建通知标题 21 | * @returns 通知标题 22 | */ 23 | export function getNotifyTitle(): string { 24 | return `百度贴吧自动签到 - ${formatDate(new Date(), 'Asia/Shanghai', '+8').split(' ')[0]}`; 25 | } 26 | 27 | /** 28 | * 处理通知发送的结果 29 | * @param platform - 平台名称 30 | * @param response - 响应结果 31 | */ 32 | function handleNotifyResult(platform: string, response: AxiosResponse | undefined): void { 33 | if (response && response.status === 200) { 34 | console.log(`✅ ${platform}通知发送成功`); 35 | } else { 36 | console.log(`⚠️ ${platform}通知发送失败: ${response?.statusText || '未知错误'}`); 37 | } 38 | } 39 | 40 | /** 41 | * Server酱通知 (ServerChan) 42 | * @param options - Server酱配置选项 43 | * @returns 发送结果 44 | */ 45 | export async function sendServerChan(options: ServerChanOptions): Promise { 46 | const { key, title, content } = options; 47 | if (!key) return { success: false, message: 'Server酱KEY未设置' }; 48 | 49 | try { 50 | const url = `https://sctapi.ftqq.com/${key}.send`; 51 | const response = await axios.post(url, { 52 | title: title, 53 | desp: content 54 | }); 55 | handleNotifyResult('Server酱', response); 56 | return { success: true, message: 'Server酱通知发送成功', channel: 'ServerChan' }; 57 | } catch (error) { 58 | const err = error as Error; 59 | console.error(`❌ Server酱通知发送失败: ${err.message}`); 60 | return { success: false, message: err.message, channel: 'ServerChan' }; 61 | } 62 | } 63 | 64 | /** 65 | * Bark通知 66 | * @param options - Bark配置选项 67 | * @returns 发送结果 68 | */ 69 | export async function sendBark(options: BarkOptions): Promise { 70 | const { key, title, content } = options; 71 | if (!key) return { success: false, message: 'Bark KEY未设置', channel: 'Bark' }; 72 | 73 | try { 74 | // 处理Bark地址,兼容自建服务和官方服务 75 | let barkUrl = key; 76 | if (!barkUrl.startsWith('http')) { 77 | barkUrl = `https://api.day.app/${key}`; 78 | } 79 | if (!barkUrl.endsWith('/')) { 80 | barkUrl += '/'; 81 | } 82 | 83 | const url = `${barkUrl}${encodeURIComponent(title || '')}/${encodeURIComponent(content)}`; 84 | const response = await axios.get(url); 85 | handleNotifyResult('Bark', response); 86 | return { success: true, message: 'Bark通知发送成功', channel: 'Bark' }; 87 | } catch (error) { 88 | const err = error as Error; 89 | console.error(`❌ Bark通知发送失败: ${err.message}`); 90 | return { success: false, message: err.message, channel: 'Bark' }; 91 | } 92 | } 93 | 94 | /** 95 | * Telegram Bot通知 96 | * @param options - Telegram配置选项 97 | * @returns 发送结果 98 | */ 99 | export async function sendTelegram(options: TelegramOptions): Promise { 100 | const { botToken, chatId, message, parseMode = 'Markdown' } = options; 101 | if (!botToken || !chatId) return { success: false, message: 'Telegram配置不完整', channel: 'Telegram' }; 102 | 103 | try { 104 | const url = `https://api.telegram.org/bot${botToken}/sendMessage`; 105 | 106 | const response = await axios.post(url, { 107 | chat_id: chatId, 108 | text: message, 109 | parse_mode: parseMode 110 | }); 111 | handleNotifyResult('Telegram', response); 112 | return { success: true, message: 'Telegram通知发送成功', channel: 'Telegram' }; 113 | } catch (error) { 114 | const err = error as Error; 115 | console.error(`❌ Telegram通知发送失败: ${err.message}`); 116 | return { success: false, message: err.message, channel: 'Telegram' }; 117 | } 118 | } 119 | 120 | /** 121 | * 钉钉机器人通知 122 | * @param options - 钉钉配置选项 123 | * @returns 发送结果 124 | */ 125 | export async function sendDingTalk(options: DingTalkOptions): Promise { 126 | const { webhook, secret, title, content } = options; 127 | if (!webhook) return { success: false, message: '钉钉Webhook未设置', channel: 'DingTalk' }; 128 | 129 | try { 130 | // 如果有安全密钥,需要计算签名 131 | let url = webhook; 132 | if (secret) { 133 | const timestamp = Date.now(); 134 | const hmac = crypto.createHmac('sha256', secret); 135 | const sign = encodeURIComponent(hmac.update(`${timestamp}\n${secret}`).digest('base64')); 136 | url = `${webhook}×tamp=${timestamp}&sign=${sign}`; 137 | } 138 | 139 | const response = await axios.post(url, { 140 | msgtype: 'markdown', 141 | markdown: { 142 | title: title || '通知', 143 | text: `### ${title || '通知'}\n${content}` 144 | } 145 | }); 146 | handleNotifyResult('钉钉', response); 147 | return { success: true, message: '钉钉通知发送成功', channel: 'DingTalk' }; 148 | } catch (error) { 149 | const err = error as Error; 150 | console.error(`❌ 钉钉通知发送失败: ${err.message}`); 151 | return { success: false, message: err.message, channel: 'DingTalk' }; 152 | } 153 | } 154 | 155 | /** 156 | * 企业微信通知 157 | * @param options - 企业微信配置选项 158 | * @returns 发送结果 159 | */ 160 | export async function sendWecom(options: WeComOptions): Promise { 161 | const { key, content, title } = options; 162 | if (!key) return { success: false, message: '企业微信KEY未设置', channel: 'WeCom' }; 163 | 164 | try { 165 | const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${key}`; 166 | const response = await axios.post(url, { 167 | msgtype: 'markdown', 168 | markdown: { 169 | content: `### ${title || '通知'}\n${content}` 170 | } 171 | }); 172 | handleNotifyResult('企业微信', response); 173 | return { success: true, message: '企业微信通知发送成功', channel: 'WeCom' }; 174 | } catch (error) { 175 | const err = error as Error; 176 | console.error(`❌ 企业微信通知发送失败: ${err.message}`); 177 | return { success: false, message: err.message, channel: 'WeCom' }; 178 | } 179 | } 180 | 181 | /** 182 | * PushPlus通知 183 | * @param options - PushPlus配置选项 184 | * @returns 发送结果 185 | */ 186 | export async function sendPushPlus(options: PushPlusOptions): Promise { 187 | const { token, title, content, template = 'markdown' } = options; 188 | if (!token) return { success: false, message: 'PushPlus Token未设置', channel: 'PushPlus' }; 189 | 190 | try { 191 | const url = 'https://www.pushplus.plus/send'; 192 | const response = await axios.post(url, { 193 | token: token, 194 | title: title || '通知', 195 | content: content, 196 | template: template 197 | }); 198 | handleNotifyResult('PushPlus', response); 199 | return { success: true, message: 'PushPlus通知发送成功', channel: 'PushPlus' }; 200 | } catch (error) { 201 | const err = error as Error; 202 | console.error(`❌ PushPlus通知发送失败: ${err.message}`); 203 | return { success: false, message: err.message, channel: 'PushPlus' }; 204 | } 205 | } 206 | 207 | /** 208 | * 发送通知到所有已配置的平台 209 | * @param summary - 要发送的通知内容 210 | * @returns 是否有任何通知发送成功 211 | */ 212 | export async function sendNotification(summary: string): Promise { 213 | console.log('📱 开始发送通知...'); 214 | 215 | const title = getNotifyTitle(); 216 | let anySuccess = false; 217 | 218 | // Server酱通知 219 | if (process.env.SERVERCHAN_KEY) { 220 | const result = await sendServerChan({ 221 | key: process.env.SERVERCHAN_KEY, 222 | title: title, 223 | content: summary 224 | }); 225 | if (result.success) anySuccess = true; 226 | } 227 | 228 | // Bark通知 229 | if (process.env.BARK_KEY) { 230 | const result = await sendBark({ 231 | key: process.env.BARK_KEY, 232 | title: title, 233 | content: summary 234 | }); 235 | if (result.success) anySuccess = true; 236 | } 237 | 238 | // Telegram通知 239 | if (process.env.TG_BOT_TOKEN && process.env.TG_CHAT_ID) { 240 | const result = await sendTelegram({ 241 | botToken: process.env.TG_BOT_TOKEN, 242 | chatId: process.env.TG_CHAT_ID, 243 | message: `${title}\n\n${summary}` 244 | }); 245 | if (result.success) anySuccess = true; 246 | } 247 | 248 | // 钉钉通知 249 | if (process.env.DINGTALK_WEBHOOK) { 250 | const result = await sendDingTalk({ 251 | webhook: process.env.DINGTALK_WEBHOOK, 252 | secret: process.env.DINGTALK_SECRET, 253 | title: title, 254 | content: summary 255 | }); 256 | if (result.success) anySuccess = true; 257 | } 258 | 259 | // 企业微信通知 260 | if (process.env.WECOM_KEY) { 261 | const result = await sendWecom({ 262 | key: process.env.WECOM_KEY, 263 | title: title, 264 | content: summary 265 | }); 266 | if (result.success) anySuccess = true; 267 | } 268 | 269 | // PushPlus通知 270 | if (process.env.PUSHPLUS_TOKEN) { 271 | const result = await sendPushPlus({ 272 | token: process.env.PUSHPLUS_TOKEN, 273 | title: title, 274 | content: summary 275 | }); 276 | if (result.success) anySuccess = true; 277 | } 278 | 279 | if (anySuccess) { 280 | console.log('✅ 通知发送完成'); 281 | } else { 282 | console.log('⚠️ 没有通知被发送,请检查通知配置'); 283 | } 284 | 285 | return anySuccess; 286 | } -------------------------------------------------------------------------------- /js-backup/index.js: -------------------------------------------------------------------------------- 1 | // 百度贴吧自动签到 GitHub Action 脚本 2 | const axios = require('axios'); 3 | const { login, getTiebaList, signTieba, getTbs } = require('./apiService'); 4 | const { processSignResult, summarizeResults, formatSummary } = require('./dataProcessor'); 5 | const { formatDate } = require('./utils'); 6 | const { sendNotification } = require('./notify'); 7 | 8 | // 执行主函数 - 使用立即执行的异步函数表达式 9 | (async () => { 10 | const startTime = Date.now(); 11 | try { 12 | console.log('=========================================='); 13 | console.log('🏆 开始执行 百度贴吧自动签到 脚本...'); 14 | console.log('=========================================='); 15 | 16 | // 获取当前时间 17 | const now = new Date(); 18 | 19 | // 标准时间和北京时间 20 | console.log(`📅 标准时间: ${formatDate(now, 'UTC', '+0')}`); 21 | console.log(`📅 北京时间: ${formatDate(now, 'Asia/Shanghai', '+8')}`); 22 | 23 | // 检查必要的环境变量 24 | if (!process.env.BDUSS) { 25 | throw new Error('缺少必要的环境变量: BDUSS'); 26 | } 27 | 28 | const bduss = process.env.BDUSS; 29 | 30 | // 1. 验证登录凭证 31 | console.log('▶️ 步骤1: 验证登录凭证...'); 32 | const userInfo = await login(bduss); 33 | if (userInfo.status !== 200) { 34 | throw new Error('验证BDUSS失败,请检查BDUSS是否有效'); 35 | } 36 | 37 | // 2. 获取贴吧列表和TBS 38 | console.log('▶️ 步骤2: 获取贴吧列表和TBS...'); 39 | const tiebaList = await getTiebaList(bduss); 40 | 41 | if (tiebaList.length === 0) { 42 | console.log('⚠️ 未找到关注的贴吧,可能是登录失效或没有关注贴吧'); 43 | } else { 44 | console.log(`📋 共找到 ${tiebaList.length} 个关注的贴吧`); 45 | } 46 | 47 | // 3. 执行签到过程 48 | console.log('▶️ 步骤3: 开始签到过程...'); 49 | 50 | // 获取TBS 51 | const tbs = await getTbs(bduss); 52 | 53 | // 配置批量签到的大小和间隔 54 | const batchSize = parseInt(process.env.BATCH_SIZE || '20', 10); 55 | const batchInterval = parseInt(process.env.BATCH_INTERVAL || '1000', 10); 56 | 57 | // 配置重试相关参数 58 | const maxRetries = parseInt(process.env.MAX_RETRIES || '3', 10); // 最大重试次数,默认3次 59 | const retryInterval = parseInt(process.env.RETRY_INTERVAL || '5000', 10); // 重试间隔,默认5秒 60 | 61 | // 按批次处理签到 62 | const signResults = []; 63 | let alreadySignedCount = 0; 64 | let successCount = 0; 65 | let failedCount = 0; 66 | 67 | // 开始批量处理 68 | console.log(`📊 开始批量处理签到,每批 ${batchSize} 个,间隔 ${batchInterval}ms`); 69 | 70 | for (let i = 0; i < tiebaList.length; i += batchSize) { 71 | const batchTiebas = tiebaList.slice(i, i + batchSize); 72 | const batchPromises = []; 73 | 74 | const currentBatch = Math.floor(i/batchSize) + 1; 75 | const totalBatches = Math.ceil(tiebaList.length/batchSize); 76 | console.log(`📌 批次 ${currentBatch}/${totalBatches}: 处理 ${batchTiebas.length} 个贴吧`); 77 | 78 | // 记录本批次中需要签到的贴吧 79 | const needSignTiebas = []; 80 | 81 | for (let j = 0; j < batchTiebas.length; j++) { 82 | const tieba = batchTiebas[j]; 83 | const tiebaName = tieba.forum_name; 84 | const tiebaIndex = i + j + 1; // 全局索引,仅用于结果存储 85 | 86 | // 已签到的贴吧跳过 87 | if (tieba.is_sign === 1) { 88 | alreadySignedCount++; 89 | signResults.push({ 90 | success: true, 91 | message: '已经签到过了', 92 | name: tiebaName, 93 | index: tiebaIndex, 94 | info: {} 95 | }); 96 | continue; 97 | } 98 | 99 | // 需要签到的贴吧 100 | needSignTiebas.push({ 101 | tieba, 102 | tiebaName, 103 | tiebaIndex 104 | }); 105 | 106 | // 添加签到任务 107 | const signPromise = (async () => { 108 | try { 109 | const result = await signTieba(bduss, tiebaName, tbs, tiebaIndex); 110 | const processedResult = processSignResult(result); 111 | 112 | // 更新计数 113 | if (processedResult.success) { 114 | if (processedResult.message === '已经签到过了') { 115 | alreadySignedCount++; 116 | } else { 117 | successCount++; 118 | } 119 | } else { 120 | failedCount++; 121 | } 122 | 123 | return { 124 | ...processedResult, 125 | name: tiebaName, 126 | index: tiebaIndex 127 | }; 128 | } catch (error) { 129 | failedCount++; 130 | return { 131 | success: false, 132 | message: error.message, 133 | name: tiebaName, 134 | index: tiebaIndex, 135 | info: {} 136 | }; 137 | } 138 | })(); 139 | 140 | batchPromises.push(signPromise); 141 | } 142 | 143 | // 等待当前批次的签到任务完成 144 | const batchResults = await Promise.all(batchPromises); 145 | 146 | // 收集签到失败的贴吧 147 | const failedTiebas = []; 148 | batchResults.forEach(result => { 149 | if (!result.success) { 150 | // 找到该贴吧的原始信息 151 | const failedTieba = needSignTiebas.find(t => t.tiebaName === result.name); 152 | if (failedTieba) { 153 | failedTiebas.push(failedTieba); 154 | } 155 | } 156 | }); 157 | 158 | // 将当前批次结果添加到总结果中 159 | signResults.push(...batchResults); 160 | 161 | // 每批次后输出简洁的进度统计 162 | console.log(`✅ 批次${currentBatch}完成: ${i + batchTiebas.length}/${tiebaList.length} | ` + 163 | `成功: ${successCount} | 已签: ${alreadySignedCount} | 失败: ${failedCount}`); 164 | 165 | // 如果有失败的贴吧,进行重试 166 | if (failedTiebas.length > 0) { 167 | // 进行多次重试 168 | for (let retryCount = 1; retryCount <= maxRetries; retryCount++) { 169 | if (failedTiebas.length === 0) break; // 如果没有失败的贴吧了,就退出重试循环 170 | 171 | console.log(`🔄 第${retryCount}/${maxRetries}次重试: 检测到 ${failedTiebas.length} 个贴吧签到失败,等待 ${retryInterval/1000} 秒后重试...`); 172 | await new Promise(resolve => setTimeout(resolve, retryInterval)); 173 | 174 | console.log(`🔄 开始第${retryCount}次重试签到失败的贴吧...`); 175 | const retryPromises = []; 176 | const stillFailedTiebas = []; // 保存本次重试后仍然失败的贴吧 177 | 178 | // 对失败的贴吧重新签到 179 | for (const failedTieba of failedTiebas) { 180 | const { tieba, tiebaName, tiebaIndex } = failedTieba; 181 | 182 | const retryPromise = (async () => { 183 | try { 184 | console.log(`🔄 第${retryCount}次重试签到: ${tiebaName}`); 185 | const result = await signTieba(bduss, tiebaName, tbs, tiebaIndex); 186 | const processedResult = processSignResult(result); 187 | 188 | // 更新计数和结果 189 | if (processedResult.success) { 190 | // 找到之前失败的结果并移除 191 | const failedResultIndex = signResults.findIndex( 192 | r => r.name === tiebaName && !r.success 193 | ); 194 | if (failedResultIndex !== -1) { 195 | signResults.splice(failedResultIndex, 1); 196 | } 197 | 198 | // 添加成功的结果 199 | signResults.push({ 200 | ...processedResult, 201 | name: tiebaName, 202 | index: tiebaIndex, 203 | retried: true, 204 | retryCount: retryCount 205 | }); 206 | 207 | // 更新计数 208 | failedCount--; 209 | if (processedResult.message === '已经签到过了') { 210 | alreadySignedCount++; 211 | } else { 212 | successCount++; 213 | } 214 | 215 | console.log(`✅ ${tiebaName} 第${retryCount}次重试签到成功`); 216 | return { success: true, tiebaName }; 217 | } else { 218 | console.log(`❌ ${tiebaName} 第${retryCount}次重试签到仍然失败: ${processedResult.message}`); 219 | // 将此贴吧保存到仍然失败的列表中,准备下一次重试 220 | stillFailedTiebas.push(failedTieba); 221 | return { success: false, tiebaName }; 222 | } 223 | } catch (error) { 224 | console.log(`❌ ${tiebaName} 第${retryCount}次重试签到出错: ${error.message}`); 225 | // 将此贴吧保存到仍然失败的列表中,准备下一次重试 226 | stillFailedTiebas.push(failedTieba); 227 | return { success: false, tiebaName }; 228 | } 229 | })(); 230 | 231 | retryPromises.push(retryPromise); 232 | } 233 | 234 | // 等待所有重试完成 235 | await Promise.all(retryPromises); 236 | 237 | // 更新失败的贴吧列表,用于下一次重试 238 | failedTiebas.length = 0; 239 | failedTiebas.push(...stillFailedTiebas); 240 | 241 | // 重试后统计 242 | console.log(`🔄 第${retryCount}次重试完成,当前统计: 成功: ${successCount} | 已签: ${alreadySignedCount} | 失败: ${failedCount}`); 243 | 244 | // 如果所有贴吧都已成功签到,提前结束重试 245 | if (failedTiebas.length === 0) { 246 | console.log(`🎉 所有贴吧签到成功,不需要继续重试`); 247 | break; 248 | } 249 | 250 | // 如果不是最后一次重试,并且还有失败的贴吧,则增加重试间隔 251 | if (retryCount < maxRetries && failedTiebas.length > 0) { 252 | // 可以选择递增重试间隔 253 | const nextRetryInterval = retryInterval * (retryCount + 1) / retryCount; 254 | console.log(`⏳ 准备第${retryCount+1}次重试,调整间隔为 ${nextRetryInterval/1000} 秒...`); 255 | await new Promise(resolve => setTimeout(resolve, 1000)); // 短暂暂停以便于查看日志 256 | } 257 | } 258 | 259 | // 最终重试结果 260 | if (failedTiebas.length > 0) { 261 | console.log(`⚠️ 经过 ${maxRetries} 次重试后,仍有 ${failedTiebas.length} 个贴吧签到失败`); 262 | } else { 263 | console.log(`🎉 重试成功!所有贴吧都已成功签到`); 264 | } 265 | } 266 | 267 | // 在批次之间添加延迟,除非是最后一批 268 | if (i + batchSize < tiebaList.length) { 269 | console.log(`⏳ 等待 ${batchInterval/1000} 秒后处理下一批...`); 270 | await new Promise(resolve => setTimeout(resolve, batchInterval)); 271 | } 272 | } 273 | 274 | // 4. 汇总结果 275 | console.log('▶️ 步骤4: 汇总签到结果'); 276 | const summary = summarizeResults(signResults); 277 | const summaryText = formatSummary(summary); 278 | 279 | // 完成 280 | console.log('=========================================='); 281 | console.log(summaryText); 282 | console.log('=========================================='); 283 | 284 | // 5. 发送通知 - 只有在有贴吧签到失败时才发送 285 | const shouldNotify = process.env.ENABLE_NOTIFY === 'true' && failedCount > 0; 286 | 287 | if (shouldNotify) { 288 | console.log('▶️ 步骤5: 发送通知 (由于签到失败而触发)'); 289 | await sendNotification(summaryText); 290 | } else if (process.env.ENABLE_NOTIFY === 'true') { 291 | console.log('ℹ️ 签到全部成功,跳过通知发送'); 292 | } else { 293 | console.log('ℹ️ 通知功能未启用,跳过通知发送'); 294 | } 295 | 296 | } catch (error) { 297 | console.error('=========================================='); 298 | console.error(`❌ 错误: ${error.message}`); 299 | if (error.response) { 300 | console.error('📡 服务器响应:'); 301 | console.error(`状态码: ${error.response.status}`); 302 | console.error(`数据: ${JSON.stringify(error.response.data)}`); 303 | } 304 | console.error('=========================================='); 305 | 306 | // 发送错误通知 - BDUSS失效时一定要通知 307 | const isBdussError = error.message.includes('BDUSS') || error.message.includes('登录'); 308 | const shouldNotify = process.env.ENABLE_NOTIFY === 'true' || isBdussError; 309 | 310 | if (shouldNotify) { 311 | try { 312 | console.log('▶️ 步骤5: 发送通知 (由于BDUSS失效或严重错误触发)'); 313 | await sendNotification(`❌ 签到脚本执行失败!\n\n错误信息: ${error.message}`); 314 | } catch (e) { 315 | console.error(`❌ 发送错误通知失败: ${e.message}`); 316 | } 317 | } 318 | 319 | process.exit(1); // 失败时退出程序,退出码为1 320 | } finally { 321 | // 无论成功还是失败都会执行的代码 322 | const endTime = Date.now(); 323 | const executionTime = (endTime - startTime) / 1000; 324 | console.log(`⏱️ 总执行时间: ${executionTime.toFixed(2)}秒`); 325 | console.log('=========================================='); 326 | } 327 | })(); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "libReplacement": true, /* Enable lib replacement. */ 18 | "emitDecoratorMetadata": false, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | "outDir": "./dist", 30 | "rootDir": "./src", 31 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 32 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 33 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 34 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 35 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 36 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 37 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 38 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 39 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 40 | // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ 41 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 42 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 43 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 44 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ 45 | "resolveJsonModule": true, /* Enable importing .json files. */ 46 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 47 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 48 | 49 | /* JavaScript Support */ 50 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 51 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 52 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 53 | 54 | /* Emit */ 55 | "declaration": false, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 56 | "sourceMap": false, /* Create source map files for emitted JavaScript files. */ 57 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 58 | // "noEmit": true, /* Disable emitting files from a compilation. */ 59 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 60 | // "removeComments": true, /* Disable emitting comments. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 63 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 64 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 65 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 66 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 67 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 68 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 69 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 70 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 71 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 72 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 73 | 74 | /* Interop Constraints */ 75 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 76 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 77 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 78 | // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ 92 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 93 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 94 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 95 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 96 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 97 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 98 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 99 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 100 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 101 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 102 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 103 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 104 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 105 | 106 | /* Completeness */ 107 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 108 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */ 109 | "experimentalDecorators": false 110 | }, 111 | "include": ["src/**/*"], 112 | "exclude": ["node_modules", "dist", "js-backup"] 113 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // 百度贴吧自动签到 GitHub Action 脚本 2 | import { login, getTiebaList, signTieba, getTbs } from './apiService'; 3 | import { processSignResult, summarizeResults, formatSummary } from './dataProcessor'; 4 | import { formatDate, maskTiebaName } from './utils'; 5 | import { sendNotification } from './notify'; 6 | import { SignResultItem } from './types/dataProcessor.types'; 7 | import { TiebaInfo } from './types/apiService.types'; 8 | 9 | // 定义贴吧签到跟踪信息接口 10 | interface TiebaTrackInfo { 11 | tieba: TiebaInfo; 12 | tiebaName: string; 13 | tiebaIndex: number; 14 | } 15 | 16 | // 执行主函数 - 使用立即执行的异步函数表达式 17 | (async () => { 18 | const startTime = Date.now(); 19 | try { 20 | console.log('=========================================='); 21 | console.log('🏆 开始执行 百度贴吧自动签到 脚本...'); 22 | console.log('=========================================='); 23 | 24 | // 获取当前时间 25 | const now = new Date(); 26 | 27 | // 标准时间和北京时间 28 | console.log(`📅 标准时间: ${formatDate(now, 'UTC', '+0')}`); 29 | console.log(`📅 北京时间: ${formatDate(now, 'Asia/Shanghai', '+8')}`); 30 | 31 | // 检查必要的环境变量 32 | if (!process.env.BDUSS) { 33 | throw new Error('缺少必要的环境变量: BDUSS'); 34 | } 35 | 36 | const bduss = process.env.BDUSS; 37 | 38 | // 1. 验证登录凭证 39 | console.log('▶️ 步骤1: 验证登录凭证...'); 40 | const userInfo = await login(bduss); 41 | console.log(`🔑 登录凭证验证结果: ${JSON.stringify({ 42 | status: userInfo.status, 43 | userId: userInfo.userId ? String(userInfo.userId).substring(0, 3) + '***' : undefined, 44 | isValid: userInfo.isValid 45 | })}`); 46 | if (userInfo.status === 200) { 47 | console.log('✅ 验证BDUSS成功'); 48 | } else { 49 | throw new Error('验证BDUSS失败,请检查BDUSS是否有效'); 50 | } 51 | 52 | // 2. 获取贴吧列表和TBS 53 | console.log('▶️ 步骤2: 获取贴吧列表和TBS...'); 54 | const tiebaList = await getTiebaList(bduss); 55 | 56 | if (tiebaList.length === 0) { 57 | console.log('⚠️ 未找到关注的贴吧,可能是登录失效或没有关注贴吧'); 58 | } else { 59 | console.log(`📋 共找到 ${tiebaList.length} 个关注的贴吧`); 60 | } 61 | 62 | // 3. 执行签到过程 63 | console.log('▶️ 步骤3: 开始签到过程...'); 64 | 65 | // 获取TBS 66 | const tbs = await getTbs(bduss); 67 | 68 | // 配置批量签到的大小和间隔 69 | const batchSize = parseInt(process.env.BATCH_SIZE || '20', 10); 70 | const batchInterval = parseInt(process.env.BATCH_INTERVAL || '1000', 10); 71 | 72 | // 配置重试相关参数 73 | const maxRetries = parseInt(process.env.MAX_RETRIES || '3', 10); // 最大重试次数,默认3次 74 | const retryInterval = parseInt(process.env.RETRY_INTERVAL || '5000', 10); // 重试间隔,默认5秒 75 | 76 | // 按批次处理签到 77 | const signResults: SignResultItem[] = []; 78 | let alreadySignedCount = 0; 79 | let successCount = 0; 80 | let failedCount = 0; 81 | 82 | // 开始批量处理 83 | console.log(`📊 开始批量处理签到,每批 ${batchSize} 个,间隔 ${batchInterval}ms`); 84 | 85 | for (let i = 0; i < tiebaList.length; i += batchSize) { 86 | const batchTiebas = tiebaList.slice(i, i + batchSize); 87 | const batchPromises: Promise[] = []; 88 | 89 | const currentBatch = Math.floor(i/batchSize) + 1; 90 | const totalBatches = Math.ceil(tiebaList.length/batchSize); 91 | console.log(`📌 批次 ${currentBatch}/${totalBatches}: 处理 ${batchTiebas.length} 个贴吧`); 92 | 93 | // 记录本批次中需要签到的贴吧 94 | const needSignTiebas: TiebaTrackInfo[] = []; 95 | 96 | for (let j = 0; j < batchTiebas.length; j++) { 97 | const tieba = batchTiebas[j]; 98 | const tiebaName = tieba.forum_name; 99 | const tiebaIndex = i + j + 1; // 全局索引,仅用于结果存储 100 | 101 | // 已签到的贴吧跳过 102 | if (tieba.is_sign === 1) { 103 | alreadySignedCount++; 104 | signResults.push({ 105 | success: true, 106 | message: '已经签到过了', 107 | name: tiebaName, 108 | index: tiebaIndex, 109 | info: {} 110 | }); 111 | continue; 112 | } 113 | 114 | // 需要签到的贴吧 115 | needSignTiebas.push({ 116 | tieba, 117 | tiebaName, 118 | tiebaIndex 119 | }); 120 | 121 | // 添加签到任务 122 | const signPromise = (async () => { 123 | try { 124 | const result = await signTieba(bduss, tiebaName, tbs, tiebaIndex); 125 | const processedResult = processSignResult(result); 126 | 127 | // 更新计数 128 | if (processedResult.success) { 129 | if (processedResult.message === '已经签到过了') { 130 | alreadySignedCount++; 131 | } else { 132 | successCount++; 133 | } 134 | } else { 135 | failedCount++; 136 | } 137 | 138 | return { 139 | ...processedResult, 140 | name: tiebaName, 141 | index: tiebaIndex 142 | }; 143 | } catch (error) { 144 | failedCount++; 145 | return { 146 | success: false, 147 | message: (error as Error).message, 148 | name: tiebaName, 149 | index: tiebaIndex, 150 | info: {} 151 | }; 152 | } 153 | })(); 154 | 155 | batchPromises.push(signPromise); 156 | } 157 | 158 | // 等待当前批次的签到任务完成 159 | const batchResults = await Promise.all(batchPromises); 160 | 161 | // 收集签到失败的贴吧 162 | const failedTiebas: TiebaTrackInfo[] = []; 163 | batchResults.forEach(result => { 164 | if (!result.success) { 165 | // 找到该贴吧的原始信息 166 | const failedTieba = needSignTiebas.find(t => t.tiebaName === result.name); 167 | if (failedTieba) { 168 | failedTiebas.push(failedTieba); 169 | } 170 | } 171 | }); 172 | 173 | // 将当前批次结果添加到总结果中 174 | signResults.push(...batchResults); 175 | 176 | // 每批次后输出简洁的进度统计 177 | console.log(`✅ 批次${currentBatch}完成: ${i + batchTiebas.length}/${tiebaList.length} | ` + 178 | `成功: ${successCount} | 已签: ${alreadySignedCount} | 失败: ${failedCount}`); 179 | 180 | // 如果有失败的贴吧,进行重试 181 | if (failedTiebas.length > 0) { 182 | // 进行多次重试 183 | for (let retryCount = 1; retryCount <= maxRetries; retryCount++) { 184 | if (failedTiebas.length === 0) break; // 如果没有失败的贴吧了,就退出重试循环 185 | 186 | console.log(`🔄 第${retryCount}/${maxRetries}次重试: 检测到 ${failedTiebas.length} 个贴吧签到失败,等待 ${retryInterval/1000} 秒后重试...`); 187 | await new Promise(resolve => setTimeout(resolve, retryInterval)); 188 | 189 | console.log(`🔄 开始第${retryCount}次重试签到失败的贴吧...`); 190 | const retryPromises: Promise<{ success: boolean, tiebaName: string }>[] = []; 191 | const stillFailedTiebas: TiebaTrackInfo[] = []; // 保存本次重试后仍然失败的贴吧 192 | 193 | // 对失败的贴吧重新签到 194 | for (const failedTieba of failedTiebas) { 195 | const { tieba, tiebaName, tiebaIndex } = failedTieba; 196 | 197 | const retryPromise = (async () => { 198 | try { 199 | console.log(`🔄 第${retryCount}次重试签到: ${maskTiebaName(tiebaName)}`); 200 | const result = await signTieba(bduss, tiebaName, tbs, tiebaIndex); 201 | const processedResult = processSignResult(result); 202 | 203 | // 更新计数和结果 204 | if (processedResult.success) { 205 | // 找到之前失败的结果并移除 206 | const failedResultIndex = signResults.findIndex( 207 | r => r.name === tiebaName && !r.success 208 | ); 209 | if (failedResultIndex !== -1) { 210 | signResults.splice(failedResultIndex, 1); 211 | } 212 | 213 | // 添加成功的结果 214 | signResults.push({ 215 | ...processedResult, 216 | name: tiebaName, 217 | index: tiebaIndex, 218 | retried: true, 219 | retryCount: retryCount 220 | }); 221 | 222 | // 更新计数 223 | failedCount--; 224 | if (processedResult.message === '已经签到过了') { 225 | alreadySignedCount++; 226 | } else { 227 | successCount++; 228 | } 229 | 230 | console.log(`✅ ${maskTiebaName(tiebaName)} 第${retryCount}次重试签到成功`); 231 | return { success: true, tiebaName }; 232 | } else { 233 | console.log(`❌ ${maskTiebaName(tiebaName)} 第${retryCount}次重试签到仍然失败: ${processedResult.message}`); 234 | // 将此贴吧保存到仍然失败的列表中,准备下一次重试 235 | stillFailedTiebas.push(failedTieba); 236 | return { success: false, tiebaName }; 237 | } 238 | } catch (error) { 239 | console.log(`❌ ${maskTiebaName(tiebaName)} 第${retryCount}次重试签到出错: ${(error as Error).message}`); 240 | // 将此贴吧保存到仍然失败的列表中,准备下一次重试 241 | stillFailedTiebas.push(failedTieba); 242 | return { success: false, tiebaName }; 243 | } 244 | })(); 245 | 246 | retryPromises.push(retryPromise); 247 | } 248 | 249 | // 等待所有重试完成 250 | await Promise.all(retryPromises); 251 | 252 | // 更新失败的贴吧列表,用于下一次重试 253 | failedTiebas.length = 0; 254 | failedTiebas.push(...stillFailedTiebas); 255 | 256 | // 重试后统计 257 | console.log(`🔄 第${retryCount}次重试完成,当前统计: 成功: ${successCount} | 已签: ${alreadySignedCount} | 失败: ${failedCount}`); 258 | 259 | // 如果所有贴吧都已成功签到,提前结束重试 260 | if (failedTiebas.length === 0) { 261 | console.log(`🎉 所有贴吧签到成功,不需要继续重试`); 262 | break; 263 | } 264 | 265 | // 如果不是最后一次重试,并且还有失败的贴吧,则增加重试间隔 266 | if (retryCount < maxRetries && failedTiebas.length > 0) { 267 | // 可以选择递增重试间隔 268 | const nextRetryInterval = retryInterval * (retryCount + 1) / retryCount; 269 | console.log(`⏳ 准备第${retryCount+1}次重试,调整间隔为 ${nextRetryInterval/1000} 秒...`); 270 | await new Promise(resolve => setTimeout(resolve, 1000)); // 短暂暂停以便于查看日志 271 | } 272 | } 273 | 274 | // 最终重试结果 275 | if (failedTiebas.length > 0) { 276 | console.log(`⚠️ 经过 ${maxRetries} 次重试后,仍有 ${failedTiebas.length} 个贴吧签到失败`); 277 | } else { 278 | console.log(`🎉 重试成功!所有贴吧都已成功签到`); 279 | } 280 | } 281 | 282 | // 在批次之间添加延迟,除非是最后一批 283 | if (i + batchSize < tiebaList.length) { 284 | console.log(`⏳ 等待 ${batchInterval/1000} 秒后处理下一批...`); 285 | await new Promise(resolve => setTimeout(resolve, batchInterval)); 286 | } 287 | } 288 | 289 | // 4. 汇总结果 290 | console.log('▶️ 步骤4: 汇总签到结果'); 291 | const summary = summarizeResults(signResults); 292 | const summaryText = formatSummary(summary); 293 | 294 | // 完成 295 | console.log('=========================================='); 296 | console.log(summaryText); 297 | console.log('=========================================='); 298 | 299 | // 5. 发送通知 - 只有在有贴吧签到失败时才发送 300 | const shouldNotify = process.env.ENABLE_NOTIFY === 'true' && failedCount > 0; 301 | 302 | if (shouldNotify) { 303 | console.log('▶️ 步骤5: 发送通知 (由于签到失败而触发)'); 304 | await sendNotification(summaryText); 305 | } else if (process.env.ENABLE_NOTIFY === 'true') { 306 | console.log('ℹ️ 签到全部成功,跳过通知发送'); 307 | } else { 308 | console.log('ℹ️ 通知功能未启用,跳过通知发送'); 309 | } 310 | 311 | } catch (error) { 312 | console.error('=========================================='); 313 | console.error(`❌ 错误: ${(error as Error).message}`); 314 | if ((error as any).response) { 315 | console.error('📡 服务器响应:'); 316 | console.error(`状态码: ${(error as any).response.status}`); 317 | console.error(`数据: ${JSON.stringify((error as any).response.data)}`); 318 | } 319 | console.error('=========================================='); 320 | 321 | // 发送错误通知 - BDUSS失效时一定要通知 322 | const errMsg = (error as Error).message; 323 | const isBdussError = errMsg.includes('BDUSS') || errMsg.includes('登录'); 324 | const shouldNotify = process.env.ENABLE_NOTIFY === 'true' || isBdussError; 325 | 326 | if (shouldNotify) { 327 | try { 328 | console.log('▶️ 步骤5: 发送通知 (由于BDUSS失效或严重错误触发)'); 329 | await sendNotification(`❌ 签到脚本执行失败!\n\n错误信息: ${(error as Error).message}`); 330 | } catch (e) { 331 | console.error(`❌ 发送错误通知失败: ${(e as Error).message}`); 332 | } 333 | } 334 | 335 | process.exit(1); // 失败时退出程序,退出码为1 336 | } finally { 337 | // 无论成功还是失败都会执行的代码 338 | const endTime = Date.now(); 339 | const executionTime = (endTime - startTime) / 1000; 340 | console.log(`⏱️ 总执行时间: ${executionTime.toFixed(2)}秒`); 341 | console.log('=========================================='); 342 | } 343 | })(); -------------------------------------------------------------------------------- /dist/notify.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || (function () { 19 | var ownKeys = function(o) { 20 | ownKeys = Object.getOwnPropertyNames || function (o) { 21 | var ar = []; 22 | for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; 23 | return ar; 24 | }; 25 | return ownKeys(o); 26 | }; 27 | return function (mod) { 28 | if (mod && mod.__esModule) return mod; 29 | var result = {}; 30 | if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); 31 | __setModuleDefault(result, mod); 32 | return result; 33 | }; 34 | })(); 35 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 36 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 37 | return new (P || (P = Promise))(function (resolve, reject) { 38 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 39 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 40 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 41 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 42 | }); 43 | }; 44 | var __importDefault = (this && this.__importDefault) || function (mod) { 45 | return (mod && mod.__esModule) ? mod : { "default": mod }; 46 | }; 47 | Object.defineProperty(exports, "__esModule", { value: true }); 48 | exports.getNotifyTitle = getNotifyTitle; 49 | exports.sendServerChan = sendServerChan; 50 | exports.sendBark = sendBark; 51 | exports.sendTelegram = sendTelegram; 52 | exports.sendDingTalk = sendDingTalk; 53 | exports.sendWecom = sendWecom; 54 | exports.sendPushPlus = sendPushPlus; 55 | exports.sendNotification = sendNotification; 56 | /** 57 | * 通知模块 - 支持多种推送渠道发送脚本运行结果通知 58 | */ 59 | const axios_1 = __importDefault(require("axios")); 60 | const utils_1 = require("./utils"); 61 | const crypto = __importStar(require("crypto")); 62 | /** 63 | * 构建通知标题 64 | * @returns 通知标题 65 | */ 66 | function getNotifyTitle() { 67 | return `百度贴吧自动签到 - ${(0, utils_1.formatDate)(new Date(), 'Asia/Shanghai', '+8').split(' ')[0]}`; 68 | } 69 | /** 70 | * 处理通知发送的结果 71 | * @param platform - 平台名称 72 | * @param response - 响应结果 73 | */ 74 | function handleNotifyResult(platform, response) { 75 | if (response && response.status === 200) { 76 | console.log(`✅ ${platform}通知发送成功`); 77 | } 78 | else { 79 | console.log(`⚠️ ${platform}通知发送失败: ${(response === null || response === void 0 ? void 0 : response.statusText) || '未知错误'}`); 80 | } 81 | } 82 | /** 83 | * Server酱通知 (ServerChan) 84 | * @param options - Server酱配置选项 85 | * @returns 发送结果 86 | */ 87 | function sendServerChan(options) { 88 | return __awaiter(this, void 0, void 0, function* () { 89 | const { key, title, content } = options; 90 | if (!key) 91 | return { success: false, message: 'Server酱KEY未设置' }; 92 | try { 93 | const url = `https://sctapi.ftqq.com/${key}.send`; 94 | const response = yield axios_1.default.post(url, { 95 | title: title, 96 | desp: content 97 | }); 98 | handleNotifyResult('Server酱', response); 99 | return { success: true, message: 'Server酱通知发送成功', channel: 'ServerChan' }; 100 | } 101 | catch (error) { 102 | const err = error; 103 | console.error(`❌ Server酱通知发送失败: ${err.message}`); 104 | return { success: false, message: err.message, channel: 'ServerChan' }; 105 | } 106 | }); 107 | } 108 | /** 109 | * Bark通知 110 | * @param options - Bark配置选项 111 | * @returns 发送结果 112 | */ 113 | function sendBark(options) { 114 | return __awaiter(this, void 0, void 0, function* () { 115 | const { key, title, content } = options; 116 | if (!key) 117 | return { success: false, message: 'Bark KEY未设置', channel: 'Bark' }; 118 | try { 119 | // 处理Bark地址,兼容自建服务和官方服务 120 | let barkUrl = key; 121 | if (!barkUrl.startsWith('http')) { 122 | barkUrl = `https://api.day.app/${key}`; 123 | } 124 | if (!barkUrl.endsWith('/')) { 125 | barkUrl += '/'; 126 | } 127 | const url = `${barkUrl}${encodeURIComponent(title || '')}/${encodeURIComponent(content)}`; 128 | const response = yield axios_1.default.get(url); 129 | handleNotifyResult('Bark', response); 130 | return { success: true, message: 'Bark通知发送成功', channel: 'Bark' }; 131 | } 132 | catch (error) { 133 | const err = error; 134 | console.error(`❌ Bark通知发送失败: ${err.message}`); 135 | return { success: false, message: err.message, channel: 'Bark' }; 136 | } 137 | }); 138 | } 139 | /** 140 | * Telegram Bot通知 141 | * @param options - Telegram配置选项 142 | * @returns 发送结果 143 | */ 144 | function sendTelegram(options) { 145 | return __awaiter(this, void 0, void 0, function* () { 146 | const { botToken, chatId, message, parseMode = 'Markdown' } = options; 147 | if (!botToken || !chatId) 148 | return { success: false, message: 'Telegram配置不完整', channel: 'Telegram' }; 149 | try { 150 | const url = `https://api.telegram.org/bot${botToken}/sendMessage`; 151 | const response = yield axios_1.default.post(url, { 152 | chat_id: chatId, 153 | text: message, 154 | parse_mode: parseMode 155 | }); 156 | handleNotifyResult('Telegram', response); 157 | return { success: true, message: 'Telegram通知发送成功', channel: 'Telegram' }; 158 | } 159 | catch (error) { 160 | const err = error; 161 | console.error(`❌ Telegram通知发送失败: ${err.message}`); 162 | return { success: false, message: err.message, channel: 'Telegram' }; 163 | } 164 | }); 165 | } 166 | /** 167 | * 钉钉机器人通知 168 | * @param options - 钉钉配置选项 169 | * @returns 发送结果 170 | */ 171 | function sendDingTalk(options) { 172 | return __awaiter(this, void 0, void 0, function* () { 173 | const { webhook, secret, title, content } = options; 174 | if (!webhook) 175 | return { success: false, message: '钉钉Webhook未设置', channel: 'DingTalk' }; 176 | try { 177 | // 如果有安全密钥,需要计算签名 178 | let url = webhook; 179 | if (secret) { 180 | const timestamp = Date.now(); 181 | const hmac = crypto.createHmac('sha256', secret); 182 | const sign = encodeURIComponent(hmac.update(`${timestamp}\n${secret}`).digest('base64')); 183 | url = `${webhook}×tamp=${timestamp}&sign=${sign}`; 184 | } 185 | const response = yield axios_1.default.post(url, { 186 | msgtype: 'markdown', 187 | markdown: { 188 | title: title || '通知', 189 | text: `### ${title || '通知'}\n${content}` 190 | } 191 | }); 192 | handleNotifyResult('钉钉', response); 193 | return { success: true, message: '钉钉通知发送成功', channel: 'DingTalk' }; 194 | } 195 | catch (error) { 196 | const err = error; 197 | console.error(`❌ 钉钉通知发送失败: ${err.message}`); 198 | return { success: false, message: err.message, channel: 'DingTalk' }; 199 | } 200 | }); 201 | } 202 | /** 203 | * 企业微信通知 204 | * @param options - 企业微信配置选项 205 | * @returns 发送结果 206 | */ 207 | function sendWecom(options) { 208 | return __awaiter(this, void 0, void 0, function* () { 209 | const { key, content, title } = options; 210 | if (!key) 211 | return { success: false, message: '企业微信KEY未设置', channel: 'WeCom' }; 212 | try { 213 | const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${key}`; 214 | const response = yield axios_1.default.post(url, { 215 | msgtype: 'markdown', 216 | markdown: { 217 | content: `### ${title || '通知'}\n${content}` 218 | } 219 | }); 220 | handleNotifyResult('企业微信', response); 221 | return { success: true, message: '企业微信通知发送成功', channel: 'WeCom' }; 222 | } 223 | catch (error) { 224 | const err = error; 225 | console.error(`❌ 企业微信通知发送失败: ${err.message}`); 226 | return { success: false, message: err.message, channel: 'WeCom' }; 227 | } 228 | }); 229 | } 230 | /** 231 | * PushPlus通知 232 | * @param options - PushPlus配置选项 233 | * @returns 发送结果 234 | */ 235 | function sendPushPlus(options) { 236 | return __awaiter(this, void 0, void 0, function* () { 237 | const { token, title, content, template = 'markdown' } = options; 238 | if (!token) 239 | return { success: false, message: 'PushPlus Token未设置', channel: 'PushPlus' }; 240 | try { 241 | const url = 'https://www.pushplus.plus/send'; 242 | const response = yield axios_1.default.post(url, { 243 | token: token, 244 | title: title || '通知', 245 | content: content, 246 | template: template 247 | }); 248 | handleNotifyResult('PushPlus', response); 249 | return { success: true, message: 'PushPlus通知发送成功', channel: 'PushPlus' }; 250 | } 251 | catch (error) { 252 | const err = error; 253 | console.error(`❌ PushPlus通知发送失败: ${err.message}`); 254 | return { success: false, message: err.message, channel: 'PushPlus' }; 255 | } 256 | }); 257 | } 258 | /** 259 | * 发送通知到所有已配置的平台 260 | * @param summary - 要发送的通知内容 261 | * @returns 是否有任何通知发送成功 262 | */ 263 | function sendNotification(summary) { 264 | return __awaiter(this, void 0, void 0, function* () { 265 | console.log('📱 开始发送通知...'); 266 | const title = getNotifyTitle(); 267 | let anySuccess = false; 268 | // Server酱通知 269 | if (process.env.SERVERCHAN_KEY) { 270 | const result = yield sendServerChan({ 271 | key: process.env.SERVERCHAN_KEY, 272 | title: title, 273 | content: summary 274 | }); 275 | if (result.success) 276 | anySuccess = true; 277 | } 278 | // Bark通知 279 | if (process.env.BARK_KEY) { 280 | const result = yield sendBark({ 281 | key: process.env.BARK_KEY, 282 | title: title, 283 | content: summary 284 | }); 285 | if (result.success) 286 | anySuccess = true; 287 | } 288 | // Telegram通知 289 | if (process.env.TG_BOT_TOKEN && process.env.TG_CHAT_ID) { 290 | const result = yield sendTelegram({ 291 | botToken: process.env.TG_BOT_TOKEN, 292 | chatId: process.env.TG_CHAT_ID, 293 | message: `${title}\n\n${summary}` 294 | }); 295 | if (result.success) 296 | anySuccess = true; 297 | } 298 | // 钉钉通知 299 | if (process.env.DINGTALK_WEBHOOK) { 300 | const result = yield sendDingTalk({ 301 | webhook: process.env.DINGTALK_WEBHOOK, 302 | secret: process.env.DINGTALK_SECRET, 303 | title: title, 304 | content: summary 305 | }); 306 | if (result.success) 307 | anySuccess = true; 308 | } 309 | // 企业微信通知 310 | if (process.env.WECOM_KEY) { 311 | const result = yield sendWecom({ 312 | key: process.env.WECOM_KEY, 313 | title: title, 314 | content: summary 315 | }); 316 | if (result.success) 317 | anySuccess = true; 318 | } 319 | // PushPlus通知 320 | if (process.env.PUSHPLUS_TOKEN) { 321 | const result = yield sendPushPlus({ 322 | token: process.env.PUSHPLUS_TOKEN, 323 | title: title, 324 | content: summary 325 | }); 326 | if (result.success) 327 | anySuccess = true; 328 | } 329 | if (anySuccess) { 330 | console.log('✅ 通知发送完成'); 331 | } 332 | else { 333 | console.log('⚠️ 没有通知被发送,请检查通知配置'); 334 | } 335 | return anySuccess; 336 | }); 337 | } 338 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | // 百度贴吧自动签到 GitHub Action 脚本 13 | const apiService_1 = require("./apiService"); 14 | const dataProcessor_1 = require("./dataProcessor"); 15 | const utils_1 = require("./utils"); 16 | const notify_1 = require("./notify"); 17 | // 执行主函数 - 使用立即执行的异步函数表达式 18 | (() => __awaiter(void 0, void 0, void 0, function* () { 19 | const startTime = Date.now(); 20 | try { 21 | console.log('=========================================='); 22 | console.log('🏆 开始执行 百度贴吧自动签到 脚本...'); 23 | console.log('=========================================='); 24 | // 获取当前时间 25 | const now = new Date(); 26 | // 标准时间和北京时间 27 | console.log(`📅 标准时间: ${(0, utils_1.formatDate)(now, 'UTC', '+0')}`); 28 | console.log(`📅 北京时间: ${(0, utils_1.formatDate)(now, 'Asia/Shanghai', '+8')}`); 29 | // 检查必要的环境变量 30 | if (!process.env.BDUSS) { 31 | throw new Error('缺少必要的环境变量: BDUSS'); 32 | } 33 | const bduss = process.env.BDUSS; 34 | // 1. 验证登录凭证 35 | console.log('▶️ 步骤1: 验证登录凭证...'); 36 | const userInfo = yield (0, apiService_1.login)(bduss); 37 | console.log(`🔑 登录凭证验证结果: ${JSON.stringify({ 38 | status: userInfo.status, 39 | userId: userInfo.userId ? String(userInfo.userId).substring(0, 3) + '***' : undefined, 40 | isValid: userInfo.isValid 41 | })}`); 42 | if (userInfo.status === 200) { 43 | console.log('✅ 验证BDUSS成功'); 44 | } 45 | else { 46 | throw new Error('验证BDUSS失败,请检查BDUSS是否有效'); 47 | } 48 | // 2. 获取贴吧列表和TBS 49 | console.log('▶️ 步骤2: 获取贴吧列表和TBS...'); 50 | const tiebaList = yield (0, apiService_1.getTiebaList)(bduss); 51 | if (tiebaList.length === 0) { 52 | console.log('⚠️ 未找到关注的贴吧,可能是登录失效或没有关注贴吧'); 53 | } 54 | else { 55 | console.log(`📋 共找到 ${tiebaList.length} 个关注的贴吧`); 56 | } 57 | // 3. 执行签到过程 58 | console.log('▶️ 步骤3: 开始签到过程...'); 59 | // 获取TBS 60 | const tbs = yield (0, apiService_1.getTbs)(bduss); 61 | // 配置批量签到的大小和间隔 62 | const batchSize = parseInt(process.env.BATCH_SIZE || '20', 10); 63 | const batchInterval = parseInt(process.env.BATCH_INTERVAL || '1000', 10); 64 | // 配置重试相关参数 65 | const maxRetries = parseInt(process.env.MAX_RETRIES || '3', 10); // 最大重试次数,默认3次 66 | const retryInterval = parseInt(process.env.RETRY_INTERVAL || '5000', 10); // 重试间隔,默认5秒 67 | // 按批次处理签到 68 | const signResults = []; 69 | let alreadySignedCount = 0; 70 | let successCount = 0; 71 | let failedCount = 0; 72 | // 开始批量处理 73 | console.log(`📊 开始批量处理签到,每批 ${batchSize} 个,间隔 ${batchInterval}ms`); 74 | for (let i = 0; i < tiebaList.length; i += batchSize) { 75 | const batchTiebas = tiebaList.slice(i, i + batchSize); 76 | const batchPromises = []; 77 | const currentBatch = Math.floor(i / batchSize) + 1; 78 | const totalBatches = Math.ceil(tiebaList.length / batchSize); 79 | console.log(`📌 批次 ${currentBatch}/${totalBatches}: 处理 ${batchTiebas.length} 个贴吧`); 80 | // 记录本批次中需要签到的贴吧 81 | const needSignTiebas = []; 82 | for (let j = 0; j < batchTiebas.length; j++) { 83 | const tieba = batchTiebas[j]; 84 | const tiebaName = tieba.forum_name; 85 | const tiebaIndex = i + j + 1; // 全局索引,仅用于结果存储 86 | // 已签到的贴吧跳过 87 | if (tieba.is_sign === 1) { 88 | alreadySignedCount++; 89 | signResults.push({ 90 | success: true, 91 | message: '已经签到过了', 92 | name: tiebaName, 93 | index: tiebaIndex, 94 | info: {} 95 | }); 96 | continue; 97 | } 98 | // 需要签到的贴吧 99 | needSignTiebas.push({ 100 | tieba, 101 | tiebaName, 102 | tiebaIndex 103 | }); 104 | // 添加签到任务 105 | const signPromise = (() => __awaiter(void 0, void 0, void 0, function* () { 106 | try { 107 | const result = yield (0, apiService_1.signTieba)(bduss, tiebaName, tbs, tiebaIndex); 108 | const processedResult = (0, dataProcessor_1.processSignResult)(result); 109 | // 更新计数 110 | if (processedResult.success) { 111 | if (processedResult.message === '已经签到过了') { 112 | alreadySignedCount++; 113 | } 114 | else { 115 | successCount++; 116 | } 117 | } 118 | else { 119 | failedCount++; 120 | } 121 | return Object.assign(Object.assign({}, processedResult), { name: tiebaName, index: tiebaIndex }); 122 | } 123 | catch (error) { 124 | failedCount++; 125 | return { 126 | success: false, 127 | message: error.message, 128 | name: tiebaName, 129 | index: tiebaIndex, 130 | info: {} 131 | }; 132 | } 133 | }))(); 134 | batchPromises.push(signPromise); 135 | } 136 | // 等待当前批次的签到任务完成 137 | const batchResults = yield Promise.all(batchPromises); 138 | // 收集签到失败的贴吧 139 | const failedTiebas = []; 140 | batchResults.forEach(result => { 141 | if (!result.success) { 142 | // 找到该贴吧的原始信息 143 | const failedTieba = needSignTiebas.find(t => t.tiebaName === result.name); 144 | if (failedTieba) { 145 | failedTiebas.push(failedTieba); 146 | } 147 | } 148 | }); 149 | // 将当前批次结果添加到总结果中 150 | signResults.push(...batchResults); 151 | // 每批次后输出简洁的进度统计 152 | console.log(`✅ 批次${currentBatch}完成: ${i + batchTiebas.length}/${tiebaList.length} | ` + 153 | `成功: ${successCount} | 已签: ${alreadySignedCount} | 失败: ${failedCount}`); 154 | // 如果有失败的贴吧,进行重试 155 | if (failedTiebas.length > 0) { 156 | // 进行多次重试 157 | for (let retryCount = 1; retryCount <= maxRetries; retryCount++) { 158 | if (failedTiebas.length === 0) 159 | break; // 如果没有失败的贴吧了,就退出重试循环 160 | console.log(`🔄 第${retryCount}/${maxRetries}次重试: 检测到 ${failedTiebas.length} 个贴吧签到失败,等待 ${retryInterval / 1000} 秒后重试...`); 161 | yield new Promise(resolve => setTimeout(resolve, retryInterval)); 162 | console.log(`🔄 开始第${retryCount}次重试签到失败的贴吧...`); 163 | const retryPromises = []; 164 | const stillFailedTiebas = []; // 保存本次重试后仍然失败的贴吧 165 | // 对失败的贴吧重新签到 166 | for (const failedTieba of failedTiebas) { 167 | const { tieba, tiebaName, tiebaIndex } = failedTieba; 168 | const retryPromise = (() => __awaiter(void 0, void 0, void 0, function* () { 169 | try { 170 | console.log(`🔄 第${retryCount}次重试签到: ${(0, utils_1.maskTiebaName)(tiebaName)}`); 171 | const result = yield (0, apiService_1.signTieba)(bduss, tiebaName, tbs, tiebaIndex); 172 | const processedResult = (0, dataProcessor_1.processSignResult)(result); 173 | // 更新计数和结果 174 | if (processedResult.success) { 175 | // 找到之前失败的结果并移除 176 | const failedResultIndex = signResults.findIndex(r => r.name === tiebaName && !r.success); 177 | if (failedResultIndex !== -1) { 178 | signResults.splice(failedResultIndex, 1); 179 | } 180 | // 添加成功的结果 181 | signResults.push(Object.assign(Object.assign({}, processedResult), { name: tiebaName, index: tiebaIndex, retried: true, retryCount: retryCount })); 182 | // 更新计数 183 | failedCount--; 184 | if (processedResult.message === '已经签到过了') { 185 | alreadySignedCount++; 186 | } 187 | else { 188 | successCount++; 189 | } 190 | console.log(`✅ ${(0, utils_1.maskTiebaName)(tiebaName)} 第${retryCount}次重试签到成功`); 191 | return { success: true, tiebaName }; 192 | } 193 | else { 194 | console.log(`❌ ${(0, utils_1.maskTiebaName)(tiebaName)} 第${retryCount}次重试签到仍然失败: ${processedResult.message}`); 195 | // 将此贴吧保存到仍然失败的列表中,准备下一次重试 196 | stillFailedTiebas.push(failedTieba); 197 | return { success: false, tiebaName }; 198 | } 199 | } 200 | catch (error) { 201 | console.log(`❌ ${(0, utils_1.maskTiebaName)(tiebaName)} 第${retryCount}次重试签到出错: ${error.message}`); 202 | // 将此贴吧保存到仍然失败的列表中,准备下一次重试 203 | stillFailedTiebas.push(failedTieba); 204 | return { success: false, tiebaName }; 205 | } 206 | }))(); 207 | retryPromises.push(retryPromise); 208 | } 209 | // 等待所有重试完成 210 | yield Promise.all(retryPromises); 211 | // 更新失败的贴吧列表,用于下一次重试 212 | failedTiebas.length = 0; 213 | failedTiebas.push(...stillFailedTiebas); 214 | // 重试后统计 215 | console.log(`🔄 第${retryCount}次重试完成,当前统计: 成功: ${successCount} | 已签: ${alreadySignedCount} | 失败: ${failedCount}`); 216 | // 如果所有贴吧都已成功签到,提前结束重试 217 | if (failedTiebas.length === 0) { 218 | console.log(`🎉 所有贴吧签到成功,不需要继续重试`); 219 | break; 220 | } 221 | // 如果不是最后一次重试,并且还有失败的贴吧,则增加重试间隔 222 | if (retryCount < maxRetries && failedTiebas.length > 0) { 223 | // 可以选择递增重试间隔 224 | const nextRetryInterval = retryInterval * (retryCount + 1) / retryCount; 225 | console.log(`⏳ 准备第${retryCount + 1}次重试,调整间隔为 ${nextRetryInterval / 1000} 秒...`); 226 | yield new Promise(resolve => setTimeout(resolve, 1000)); // 短暂暂停以便于查看日志 227 | } 228 | } 229 | // 最终重试结果 230 | if (failedTiebas.length > 0) { 231 | console.log(`⚠️ 经过 ${maxRetries} 次重试后,仍有 ${failedTiebas.length} 个贴吧签到失败`); 232 | } 233 | else { 234 | console.log(`🎉 重试成功!所有贴吧都已成功签到`); 235 | } 236 | } 237 | // 在批次之间添加延迟,除非是最后一批 238 | if (i + batchSize < tiebaList.length) { 239 | console.log(`⏳ 等待 ${batchInterval / 1000} 秒后处理下一批...`); 240 | yield new Promise(resolve => setTimeout(resolve, batchInterval)); 241 | } 242 | } 243 | // 4. 汇总结果 244 | console.log('▶️ 步骤4: 汇总签到结果'); 245 | const summary = (0, dataProcessor_1.summarizeResults)(signResults); 246 | const summaryText = (0, dataProcessor_1.formatSummary)(summary); 247 | // 完成 248 | console.log('=========================================='); 249 | console.log(summaryText); 250 | console.log('=========================================='); 251 | // 5. 发送通知 - 只有在有贴吧签到失败时才发送 252 | const shouldNotify = process.env.ENABLE_NOTIFY === 'true' && failedCount > 0; 253 | if (shouldNotify) { 254 | console.log('▶️ 步骤5: 发送通知 (由于签到失败而触发)'); 255 | yield (0, notify_1.sendNotification)(summaryText); 256 | } 257 | else if (process.env.ENABLE_NOTIFY === 'true') { 258 | console.log('ℹ️ 签到全部成功,跳过通知发送'); 259 | } 260 | else { 261 | console.log('ℹ️ 通知功能未启用,跳过通知发送'); 262 | } 263 | } 264 | catch (error) { 265 | console.error('=========================================='); 266 | console.error(`❌ 错误: ${error.message}`); 267 | if (error.response) { 268 | console.error('📡 服务器响应:'); 269 | console.error(`状态码: ${error.response.status}`); 270 | console.error(`数据: ${JSON.stringify(error.response.data)}`); 271 | } 272 | console.error('=========================================='); 273 | // 发送错误通知 - BDUSS失效时一定要通知 274 | const errMsg = error.message; 275 | const isBdussError = errMsg.includes('BDUSS') || errMsg.includes('登录'); 276 | const shouldNotify = process.env.ENABLE_NOTIFY === 'true' || isBdussError; 277 | if (shouldNotify) { 278 | try { 279 | console.log('▶️ 步骤5: 发送通知 (由于BDUSS失效或严重错误触发)'); 280 | yield (0, notify_1.sendNotification)(`❌ 签到脚本执行失败!\n\n错误信息: ${error.message}`); 281 | } 282 | catch (e) { 283 | console.error(`❌ 发送错误通知失败: ${e.message}`); 284 | } 285 | } 286 | process.exit(1); // 失败时退出程序,退出码为1 287 | } 288 | finally { 289 | // 无论成功还是失败都会执行的代码 290 | const endTime = Date.now(); 291 | const executionTime = (endTime - startTime) / 1000; 292 | console.log(`⏱️ 总执行时间: ${executionTime.toFixed(2)}秒`); 293 | console.log('=========================================='); 294 | } 295 | }))(); 296 | --------------------------------------------------------------------------------