Repository: 14790897/auto-read-liunxdo Branch: main Commit: 6a3a2e5b8e11 Files: 63 Total size: 286.4 KB Directory structure: gitextract_34ilwb11/ ├── .dockerignore ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report_template.md │ │ ├── config.yml │ │ └── feature_request_template.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── deploy-to-hf-spaces.yml │ └── workflows/ │ ├── IssueManagementAutomation.yml │ ├── bad-pr.yml │ ├── cron_bypassCF.yaml │ ├── cron_bypassCF_idc.yaml │ ├── cron_bypassCF_likeUser.yaml │ ├── cron_read.yaml │ ├── db_test.yaml │ ├── docker.yml │ ├── release-please.yml │ ├── sync.yml │ ├── update-cron.yml │ ├── validate-issue-template.yml │ └── windows_cron_bypassCF.yaml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── Dockerfile-like-user ├── GITHUB_SECRETS_GUIDE.md ├── LICENSE ├── PROXY_GUIDE.md ├── README.md ├── README_en.md ├── bypasscf.js ├── bypasscf_likeUser_not_use.js ├── bypasscf_playwright.mjs ├── cron.log ├── cron.sh ├── debug_db.js ├── debug_rss.js ├── docker-compose-like-user.yml ├── docker-compose.yml ├── external.js ├── index.js ├── index_likeUser.js ├── index_likeUser_activity.js ├── index_likeUser_random.js ├── index_passage_list_old_not_use.js ├── invite_codecow.js ├── package.json ├── parsed_rss_data.json ├── pass_cf.py ├── postgresql/ │ └── root.crt ├── pteer.js ├── src/ │ ├── browser_retry.js │ ├── db.js │ ├── db.test.js │ ├── format_for_telegram.js │ ├── parse_rss.js │ ├── proxy_config.js │ └── topic_data.js ├── telegram_message.txt ├── test_linuxdo.js ├── test_multi_db.js ├── test_proxy.js ├── test_topic_data.js └── 随笔.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ .env ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report_template.md ================================================ --- name: "🐛 Bug Report" about: 报告一个功能或错误的问题。 title: "[Bug] 请简要描述问题" labels: bug assignees: '' --- ### 问题描述 ### 重现步骤 1. ... 2. ... 3. ... ### 附加信息(必须给出日志) ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: 📖 项目文档 url: https://github.com/14790897/auto-read-liunxdo#readme about: 查看项目文档和使用指南 ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request_template.md ================================================ --- name: "✨ Feature Request" about: 提出一个功能请求。 title: "[Feature] 请简要描述功能请求" labels: enhancement assignees: '' --- ### 背景 ### 功能描述 ### 实现建议 ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## Summary ## Context ================================================ FILE: .github/deploy-to-hf-spaces.yml ================================================ name: Deploy to HuggingFace Spaces on: push: branches: - dev - main paths-ignore: - '.github/workflows/cron_*.yaml' - '.github/workflows/cron_*.yml' workflow_dispatch: jobs: check-secret: runs-on: ubuntu-latest outputs: token-set: ${{ steps.check-key.outputs.defined }} steps: - id: check-key env: HF_TOKEN: ${{ secrets.HF_TOKEN }} if: "${{ env.HF_TOKEN != '' }}" run: echo "defined=true" >> $GITHUB_OUTPUT deploy: runs-on: ubuntu-latest needs: [check-secret] if: needs.check-secret.outputs.token-set == 'true' env: HF_TOKEN: ${{ secrets.HF_TOKEN }} HF_REPO: ${{ secrets.HF_REPO }} HF_USER: ${{ secrets.HF_USER }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Remove git history run: rm -rf .git - name: Prepend YAML front matter to README.md run: | echo "---" > temp_readme.md echo "title: yolo_track_inference" >> temp_readme.md echo "emoji: 🐳" >> temp_readme.md echo "colorFrom: red" >> temp_readme.md echo "colorTo: gray" >> temp_readme.md echo "sdk: docker" >> temp_readme.md echo "app_port: 7860" >> temp_readme.md echo "---" >> temp_readme.md cat README.md >> temp_readme.md mv temp_readme.md README.md - name: Configure git run: | git config --global user.email "liuweiqing147@gmail.com" git config --global user.name "14790897" - name: Set up Git and push to Space run: | git init --initial-branch=main git lfs track "*.ttf" git add . git commit -m "yolo: ${{ github.sha }}" git push --force https://${HF_USER}:${HF_TOKEN}@huggingface.co/spaces/${HF_USER}/${HF_REPO} main ================================================ FILE: .github/workflows/IssueManagementAutomation.yml ================================================ name: IssueManagementAutomation(勿动) on: # schedule: # - cron: "0 1-9/3 * * *" #"0 18 * * *" "0 */6 * * *" workflow_dispatch: push: branches: - main paths-ignore: - '.github/workflows/cron_*.yaml' - '.github/workflows/cron_*.yml' issues: types: [opened] permissions: issues: write contents: read jobs: manage-issues: if: github.repository == '14790897/auto-read-liunxdo' runs-on: ubuntu-latest steps: - name: auto lock baipiao uses: 14790897/auto-lock-baipiao@v0.1.7 with: gh_token: ${{ secrets.GITHUB_TOKEN }} gh_repo: ${{ github.repository }} issue_labels: ${{ secrets.ISSUE_LABELS }} ================================================ FILE: .github/workflows/bad-pr.yml ================================================ name: Cleanup bad PR on: pull_request_target: types: [opened, reopened] permissions: contents: read jobs: close-pr: permissions: pull-requests: write runs-on: ubuntu-latest if: "contains(github.event.pull_request.body, 'by deleting this comment block') || github.event.pull_request.body == ''" steps: - uses: actions-ecosystem/action-add-labels@v1 with: labels: 'Type: Invalid' - uses: actions-ecosystem/action-add-labels@v1 with: labels: 'Type: Spam' - uses: superbrothers/close-pull-request@v3 with: # Optional. Post an issue comment just before closing a pull request. comment: | **You have created a Pull Request to the wrong repository.** This is the repository for [auto-read-linuxdo][1], the free auto read software. See [GitHub Docs: About pull requests][2] if you need help. [1]: https://github.com/14790897/auto-read-liunxdo [2]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests - uses: sudo-bot/action-pull-request-lock@v1.0.5 with: github-token: ${{ secrets.GITHUB_TOKEN }} number: ${{ github.event.pull_request.number }} lock-reason: spam ================================================ FILE: .github/workflows/cron_bypassCF.yaml ================================================ name: readLike(自动阅读随机点赞) # GitHub.secrets优先级最高,即使没有设置对应的变量,它也会读取,这时变量为空值,导致报错,.env读取的变量无法覆盖这个值,使用了${PASSWORD_ESCAPED//\#/\\#}来对#转义,需要两个\,但是我直接在env文件使用这种方法是不行的,GitHub action是有效 on: # schedule: # # 每天 UTC 时间 19:40 运行 # - cron: "26 10 * * *" workflow_dispatch: # 允许手动触发 jobs: build: runs-on: windows-latest timeout-minutes: 35 # 设置作业超时时间为20分钟 strategy: matrix: node-version: [20.x] env: # 在作业级别设置环境变量 USERNAMES: ${{ secrets.USERNAMES }} PASSWORDS: ${{ secrets.PASSWORDS }} COOKIES: ${{ secrets.COOKIES }} WEBSITE: ${{ secrets.WEBSITE }} RUN_TIME_LIMIT_MINUTES: ${{ secrets.RUN_TIME_LIMIT_MINUTES }} MAX_CONCURRENT_ACCOUNTS: ${{ secrets.MAX_CONCURRENT_ACCOUNTS }} TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} TELEGRAM_GROUP_ID: ${{ secrets.TELEGRAM_GROUP_ID }} SPECIFIC_USER: ${{ secrets.SPECIFIC_USER }} HF_TOKEN: ${{ secrets.HF_TOKEN }} LIKE_SPECIFIC_USER: ${{ secrets.LIKE_SPECIFIC_USER }} ENABLE_RSS_FETCH: ${{ secrets.ENABLE_RSS_FETCH || '' }} ENABLE_TOPIC_DATA_FETCH: ${{ secrets.ENABLE_TOPIC_DATA_FETCH || '' }} HEALTH_PORT: ${{ secrets.HEALTH_PORT }} # 代理配置 PROXY_URL: ${{ secrets.PROXY_URL }} PROXY_TYPE: ${{ secrets.PROXY_TYPE }} PROXY_HOST: ${{ secrets.PROXY_HOST }} PROXY_PORT: ${{ secrets.PROXY_PORT }} PROXY_USERNAME: ${{ secrets.PROXY_USERNAME }} PROXY_PASSWORD: ${{ secrets.PROXY_PASSWORD }} # 数据库配置 POSTGRES_URI: ${{ secrets.POSTGRES_URI }} COCKROACH_URI: ${{ secrets.COCKROACH_URI }} NEON_URI: ${{ secrets.NEON_URI }} AIVEN_MYSQL_URI: ${{ secrets.AIVEN_MYSQL_URI }} MONGO_URI: ${{ secrets.MONGO_URI }} steps: - uses: actions/checkout@v3 # 检出仓库 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: Cache node modules uses: actions/cache@v3 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - name: Install dependencies run: | npm install - name: Run a script run: node bypasscf.js ================================================ FILE: .github/workflows/cron_bypassCF_idc.yaml ================================================ name: readLike(需手动取消schedule注释)(自动阅读idc随机点赞) #只是固定了WEBSITE变量为idcflare.com # GitHub.secrets优先级最高,即使没有设置对应的变量,它也会读取,这时变量为空值,导致报错,.env读取的变量无法覆盖这个值,使用了${PASSWORD_ESCAPED//\#/\\#}来对#转义,需要两个\,但是我直接在env文件使用这种方法是不行的,GitHub action是有效 on: # schedule: # # 每天 UTC 时间 19:40 运行 # - cron: "26 10 * * *" workflow_dispatch: # 允许手动触发 jobs: build: runs-on: windows-latest timeout-minutes: 35 # 设置作业超时时间为20分钟 strategy: matrix: node-version: [20.x] env: # 在作业级别设置环境变量 USERNAMES: ${{ secrets.USERNAMES }} PASSWORDS: ${{ secrets.PASSWORDS }} COOKIES: ${{ secrets.COOKIES }} WEBSITE: "https://idcflare.com" RUN_TIME_LIMIT_MINUTES: ${{ secrets.RUN_TIME_LIMIT_MINUTES }} MAX_CONCURRENT_ACCOUNTS: ${{ secrets.MAX_CONCURRENT_ACCOUNTS }} TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} TELEGRAM_GROUP_ID: ${{ secrets.TELEGRAM_GROUP_ID }} SPECIFIC_USER: ${{ secrets.SPECIFIC_USER }} HF_TOKEN: ${{ secrets.HF_TOKEN }} LIKE_SPECIFIC_USER: ${{ secrets.LIKE_SPECIFIC_USER }} ENABLE_RSS_FETCH: ${{ secrets.ENABLE_RSS_FETCH || '' }} ENABLE_TOPIC_DATA_FETCH: ${{ secrets.ENABLE_TOPIC_DATA_FETCH || '' }} HEALTH_PORT: ${{ secrets.HEALTH_PORT }} # 代理配置 PROXY_URL: ${{ secrets.PROXY_URL }} PROXY_TYPE: ${{ secrets.PROXY_TYPE }} PROXY_HOST: ${{ secrets.PROXY_HOST }} PROXY_PORT: ${{ secrets.PROXY_PORT }} PROXY_USERNAME: ${{ secrets.PROXY_USERNAME }} PROXY_PASSWORD: ${{ secrets.PROXY_PASSWORD }} # 数据库配置 POSTGRES_URI: ${{ secrets.POSTGRES_URI }} COCKROACH_URI: ${{ secrets.COCKROACH_URI }} NEON_URI: ${{ secrets.NEON_URI }} AIVEN_MYSQL_URI: ${{ secrets.AIVEN_MYSQL_URI }} MONGO_URI: ${{ secrets.MONGO_URI }} steps: - uses: actions/checkout@v3 # 检出仓库 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: Cache node modules uses: actions/cache@v3 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - name: Install dependencies run: | npm install - name: Run a script run: node bypasscf.js ================================================ FILE: .github/workflows/cron_bypassCF_likeUser.yaml ================================================ name: likeUser (点赞特定用户) # GitHub.secrets优先级最高,即使没有设置对应的变量,它也会读取,这时变量为空值,导致报错,.env读取的变量无法覆盖这个值,使用了${PASSWORD_ESCAPED//\#/\\#}来对#转义,需要两个\,但是我直接在env文件使用这种方法是不行的,GitHub action是有效 on: # schedule: # # 每天 UTC 时间 18:00 运行 # - cron: '21 13 * * *' workflow_dispatch: # 允许手动触发 jobs: build: runs-on: windows-latest timeout-minutes: 35 # 设置作业超时时间为20分钟 strategy: matrix: node-version: [20.x] env: # 在作业级别设置环境变量 USERNAMES: ${{ secrets.USERNAMES }} PASSWORDS: ${{ secrets.PASSWORDS }} COOKIES: ${{ secrets.COOKIES }} WEBSITE: ${{ secrets.WEBSITE }} RUN_TIME_LIMIT_MINUTES: ${{ secrets.RUN_TIME_LIMIT_MINUTES }} MAX_CONCURRENT_ACCOUNTS: ${{ secrets.MAX_CONCURRENT_ACCOUNTS }} TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} TELEGRAM_GROUP_ID: ${{ secrets.TELEGRAM_GROUP_ID }} SPECIFIC_USER: ${{ secrets.SPECIFIC_USER }} HF_TOKEN: ${{ secrets.HF_TOKEN }} LIKE_SPECIFIC_USER: ${{ secrets.LIKE_SPECIFIC_USER }} ENABLE_RSS_FETCH: ${{ secrets.ENABLE_RSS_FETCH || '' }} ENABLE_TOPIC_DATA_FETCH: ${{ secrets.ENABLE_TOPIC_DATA_FETCH || '' }} HEALTH_PORT: ${{ secrets.HEALTH_PORT }} # 代理配置 PROXY_URL: ${{ secrets.PROXY_URL }} PROXY_TYPE: ${{ secrets.PROXY_TYPE }} PROXY_HOST: ${{ secrets.PROXY_HOST }} PROXY_PORT: ${{ secrets.PROXY_PORT }} PROXY_USERNAME: ${{ secrets.PROXY_USERNAME }} PROXY_PASSWORD: ${{ secrets.PROXY_PASSWORD }} # 数据库配置 POSTGRES_URI: ${{ secrets.POSTGRES_URI }} COCKROACH_URI: ${{ secrets.COCKROACH_URI }} NEON_URI: ${{ secrets.NEON_URI }} AIVEN_MYSQL_URI: ${{ secrets.AIVEN_MYSQL_URI }} MONGO_URI: ${{ secrets.MONGO_URI }} steps: - uses: actions/checkout@v3 # 检出仓库 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: Cache node modules uses: actions/cache@v3 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - name: Install dependencies run: | npm install #github action设置的action环境变量会完全替换掉env文件的读取,所以需要在action里手动进行加载env文件 # - name: Load environment variables # run: | # echo "Debug: Checking if .env file exists..." # # 检查 .env 文件是否存在 # if [ -f .env ]; then # echo ".env file found. Loading environment variables from .env file" # # 加载 .env 文件中的默认值 # set -a # source .env # set +a # echo "Loaded .env variables:" # else # echo ".env file not found. Skipping loading." # fi # if [ -n "${{ secrets.WEBSITE }}" ] && [ ! -z "${{ secrets.WEBSITE }}" ]; then # echo "Using GitHub Secret for WEBSITE" # echo "WEBSITE=${{ secrets.WEBSITE }}" >> $GITHUB_ENV # else # echo "WEBSITE=${WEBSITE}" >> $GITHUB_ENV # fi # shell: bash # - name: Run a script(linux) # run: LIKE_SPECIFIC_USER=true node bypasscf.js --USERNAMES "$USERNAMES" --PASSWORDS "$PASSWORDS" --WEBSITE "$WEBSITE" - name: Run a script(windows) run: | $env:LIKE_SPECIFIC_USER="true" node bypasscf.js shell: pwsh ================================================ FILE: .github/workflows/cron_read.yaml ================================================ name: read cron (勿动) on: # schedule: # # 每天 UTC 时间 18:00 运行 # - cron: "0 18 * * *" workflow_dispatch: # 添加这行以允许手动触发 jobs: build: runs-on: ubuntu-latest timeout-minutes: 20 # 设置作业超时时间为20分钟 strategy: matrix: node-version: [20.x] # 选择你需要的 Node.js 版本 env: # 在作业级别设置环境变量 USERNAMES: ${{ secrets.USERNAMES }} PASSWORDS: ${{ secrets.PASSWORDS }} WEBSITE: ${{secrets.WEBSITE}} steps: - uses: actions/checkout@v3 # 检出你的仓库 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: Install dependencies run: | npm install sudo apt install -y xvfb - name: Run auto read run: node bypasscf.js # 替换为你想运行的脚本的实际名称 ================================================ FILE: .github/workflows/db_test.yaml ================================================ name: db-test on: push: branches: - '**' workflow_dispatch: jobs: db-test: if: github.repository == '14790897/auto-read-liunxdo' runs-on: windows-latest timeout-minutes: 10 strategy: matrix: node-version: [20.x] env: POSTGRES_URI: ${{ secrets.POSTGRES_URI }} COCKROACH_URI: ${{ secrets.COCKROACH_URI }} steps: - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: Cache node modules uses: actions/cache@v3 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - name: Install dependencies run: | npm install - name: Run DB test run: node src/db.test.js ================================================ FILE: .github/workflows/docker.yml ================================================ name: AutoRead Docker Image Push (勿动) on: push: branches: - main paths-ignore: - '.github/workflows/cron_*.yaml' - '.github/workflows/cron_*.yml' jobs: build-and-push: if: github.repository == '14790897/auto-read-liunxdo' runs-on: ubuntu-latest steps: - name: Check Out Repo uses: actions/checkout@v2 - name: Login to Docker Hub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Build and Push auto-read uses: docker/build-push-action@v2 with: context: ./ file: ./Dockerfile push: true tags: 14790897/auto-read:latest - name: Build and Push auto-like-user uses: docker/build-push-action@v2 with: context: ./ file: ./Dockerfile-like-user push: true tags: 14790897/auto-like-user:latest ================================================ FILE: .github/workflows/release-please.yml ================================================ name: Release Please(勿动) on: push: branches: - main paths-ignore: - '.github/workflows/cron_*.yaml' - '.github/workflows/cron_*.yml' jobs: release-please: runs-on: ubuntu-latest steps: - uses: GoogleCloudPlatform/release-please-action@v2 with: token: ${{ secrets.GH_TOKEN }} release-type: node package-name: auto-read permissions: contents: write ================================================ FILE: .github/workflows/sync.yml ================================================ name: Upstream Sync permissions: contents: write on: schedule: - cron: '0 0 * * *' # every day workflow_dispatch: jobs: sync_latest_from_upstream: name: Sync latest commits from upstream repo runs-on: ubuntu-latest if: ${{ github.event.repository.fork }} steps: # Step 1: run a standard checkout action - name: Checkout target repo uses: actions/checkout@v3 # Step 2: run the sync action - name: Sync upstream changes id: sync uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 with: upstream_sync_repo: 14790897/auto-read-liunxdo upstream_sync_branch: main target_sync_branch: main target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set # Set test_mode true to run tests instead of the true action!! test_mode: false - name: Sync check if: failure() run: | echo "[Error] 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次,这是 GitHub 的平台安全策略,详细教程请查看:https://github.com/Yidadaa/ChatGPT-Next-Web/blob/main/README_CN.md#%E6%89%93%E5%BC%80%E8%87%AA%E5%8A%A8%E6%9B%B4%E6%96%B0" echo "[Error] Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. Please refer to the detailed tutorial for instructions: https://github.com/Yidadaa/ChatGPT-Next-Web#enable-automatic-updates" exit 1 ================================================ FILE: .github/workflows/update-cron.yml ================================================ name: Update Workflow Cron on: # schedule: # - cron: '0 16 * * *' workflow_dispatch: # 允许手动触发 # push: # branches: # - main env: PAT_TOKEN: ${{ secrets.PAT_TOKEN }} # Workflows Update GitHub Action workflow files. Learn more. read and write access to the repository contents. classic s permissions: contents: write actions: write jobs: update-cron: runs-on: ubuntu-latest steps: - name: checkout repository uses: actions/checkout@v4 with: token: ${{ secrets.PAT_TOKEN }} # formula: RANDOM_NUM=$((RANDOM % (B - A + 1) + A)) - name: generate random time id: random run: | HOUR=$((RANDOM % 17 + 7)) MIN=$((RANDOM % 60)) UTC_HOUR=$(( (HOUR + 24 - 8) % 24 )) echo "hour=$HOUR" >> $GITHUB_OUTPUT echo "min=$MIN" >> $GITHUB_OUTPUT echo "utc_hour=$UTC_HOUR" >> $GITHUB_OUTPUT echo "北京时间 $HOUR:$MIN, UTC 时间 $UTC_HOUR:$MIN" - name: replace cron expression run: | sed -i "s/cron: '[^']*'/cron: '${{ steps.random.outputs.min }} ${{ steps.random.outputs.hour }} * * *'/" .github/workflows/cron_bypassCF_likeUser.yaml sed -i "s/cron: '[^']*'/cron: '${{ steps.random.outputs.min }} ${{ steps.random.outputs.utc_hour }} * * *'/" .github/workflows/cron_bypassCF.yaml - name: Check PAT run: | echo "PAT_TOKEN starts with: ${PAT_TOKEN:0:5}" - name: commit and push changes run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" git add . git diff --cached --quiet || git commit -m "update cron ${{ steps.random.outputs.min }} ${{ steps.random.outputs.hour }} * * *" git push "https://x-access-token:${{ secrets.PAT_TOKEN }}@github.com/${{ github.repository }}.git" HEAD:main ================================================ FILE: .github/workflows/validate-issue-template.yml ================================================ name: Issue Template Validator on: issues: types: [opened, edited] permissions: issues: write jobs: validate-issue: if: github.repository == '14790897/auto-read-liunxdo' runs-on: ubuntu-latest steps: - name: Validate Issue Template uses: actions/github-script@v7 with: script: | const issue = context.payload.issue; const issueBody = issue.body || ''; const issueTitle = issue.title || ''; // Check for Bug Report template sections const hasBugDescription = issueBody.includes('### 问题描述'); const hasBugSteps = issueBody.includes('### 重现步骤'); const hasBugAdditionalInfo = issueBody.includes('### 附加信息'); // Check for Feature Request template sections const hasFeatureBackground = issueBody.includes('### 背景'); const hasFeatureDescription = issueBody.includes('### 功能描述'); let isValid = false; let missingFields = []; // Determine template type by checking required sections // Bug Report requires all 3 sections const isBugReport = hasBugDescription && hasBugSteps && hasBugAdditionalInfo; // Feature Request requires both sections const isFeatureRequest = hasFeatureBackground && hasFeatureDescription; if (isBugReport) { // All required sections present for Bug Report isValid = true; } else if (isFeatureRequest) { // All required sections present for Feature Request isValid = true; } else { // Check which template was attempted and what's missing const bugSectionCount = [hasBugDescription, hasBugSteps, hasBugAdditionalInfo].filter(Boolean).length; const featureSectionCount = [hasFeatureBackground, hasFeatureDescription].filter(Boolean).length; if (bugSectionCount > 0 || issueTitle.toLowerCase().includes('[bug]')) { // Attempted Bug Report if (!hasBugDescription) missingFields.push('问题描述'); if (!hasBugSteps) missingFields.push('重现步骤'); if (!hasBugAdditionalInfo) missingFields.push('附加信息'); } else if (featureSectionCount > 0 || issueTitle.toLowerCase().includes('[feature]')) { // Attempted Feature Request if (!hasFeatureBackground) missingFields.push('背景'); if (!hasFeatureDescription) missingFields.push('功能描述'); } else { // No template sections found missingFields.push('未使用任何模板'); } isValid = false; } if (!isValid) { // Close the issue with a comment const closeMessage = [ '👋 你好!感谢你提交 Issue。', '', '⚠️ 此 Issue 未按照规定的模板填写,已自动关闭。', '', `**缺少的字段:** ${missingFields.join('、')}`, '', '请使用以下模板之一重新提交:', `- 🐛 [Bug Report 模板](https://github.com/${context.repo.owner}/${context.repo.repo}/issues/new?template=bug_report_template.md)`, `- ✨ [Feature Request 模板](https://github.com/${context.repo.owner}/${context.repo.repo}/issues/new?template=feature_request_template.md)`, '', '提供完整的信息有助于我们更快地处理你的问题。谢谢!' ].join('\n'); await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, body: closeMessage }); await github.rest.issues.update({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issue.number, state: 'closed', state_reason: 'not_planned' }); core.setFailed('Issue does not follow template and has been closed.'); } else { console.log('✅ Issue follows the template format'); } ================================================ FILE: .github/workflows/windows_cron_bypassCF.yaml ================================================ name: readLike 小众论坛(勿动) # GitHub.secrets优先级最高,即使没有设置对应的变量,它也会读取,这时变量为空值,导致报错,.env读取的变量无法覆盖这个值,使用了${PASSWORD_ESCAPED//\#/\\#}来对#转义,需要两个\,但是我直接在env文件使用这种方法是不行的,GitHub action是有效 on: # schedule: # 每天 UTC 时间 18:40 运行 # - cron: "40 18 * * *" workflow_dispatch: # 允许手动触发 jobs: build: runs-on: windows-latest timeout-minutes: 35 # 设置作业超时时间为20分钟 strategy: matrix: node-version: [20.x] env: # 在作业级别设置环境变量 USERNAMES: ${{ secrets.USERNAMES }} PASSWORDS: ${{ secrets.PASSWORDS }} RUN_TIME_LIMIT_MINUTES: ${{secrets.RUN_TIME_LIMIT_MINUTES}} TELEGRAM_BOT_TOKEN: ${{secrets.TELEGRAM_BOT_TOKEN}} TELEGRAM_CHAT_ID: ${{secrets.TELEGRAM_CHAT_ID}} steps: - uses: actions/checkout@v3 # 检出仓库 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - name: Install dependencies run: | npm install #github action设置的action环境变量会完全替换掉env文件的读取,所以需要在action里手动进行加载env文件 - name: Load environment variables run: | echo "Debug: Checking if .env file exists..." # 检查 .env 文件是否存在 if [ -f .env ]; then echo ".env file found. Loading environment variables from .env file" # 加载 .env 文件中的默认值 set -a source .env set +a echo "Loaded .env variables:" else echo ".env file not found. Skipping loading." fi if [ -n "${{ secrets.WEBSITE }}" ] && [ ! -z "${{ secrets.WEBSITE }}" ]; then echo "Using GitHub Secret for WEBSITE" echo "WEBSITE=${{ secrets.WEBSITE }}" >> $GITHUB_ENV else echo "WEBSITE=${WEBSITE}" >> $GITHUB_ENV fi shell: bash - name: Run a script run: node bypasscf.js --USERNAMES "$USERNAMES" --PASSWORDS "$PASSWORDS" --WEBSITE "$WEBSITE" ================================================ FILE: .gitignore ================================================ node_modules/ .env.local* screenshots .env ================================================ FILE: CHANGELOG.md ================================================ # Changelog ### [1.15.1](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.15.0...v1.15.1) (2026-02-11) ### Bug Fixes * db测试失败因为没有环境变量 ([5f198a3](https://www.github.com/14790897/auto-read-liunxdo/commit/5f198a302203b18e0f4db85be4630e7fdb72d8cf)) ## [1.15.0](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.14.4...v1.15.0) (2026-01-21) ### Features * add new script for user login automation and health check endpoint ([c10b990](https://www.github.com/14790897/auto-read-liunxdo/commit/c10b990a3494255c5e53585c403da65df49ccfc3)) * add root certificate files for PostgreSQL and MongoDB ([5584234](https://www.github.com/14790897/auto-read-liunxdo/commit/5584234d80c66c8c2615392b0b12866bad710837)) * enhance environment variable handling for RSS and topic data fetch features ([4c446fd](https://www.github.com/14790897/auto-read-liunxdo/commit/4c446fdaa5ceb76abd244bbb57cb2f7d8f89be28)) ### Bug Fixes * add repository condition to workflow jobs for consistency ([bc2c5ba](https://www.github.com/14790897/auto-read-liunxdo/commit/bc2c5ba1efe076d5d55f233f42df4067cca100f6)) * ensure environment variables for RSS and topic data fetch features are set to empty string if not defined ([7759c6a](https://www.github.com/14790897/auto-read-liunxdo/commit/7759c6aef670957f7eb76a4feefd24eaf93feeec)) * increase connection timeout to 15 seconds for better handling of cross-region network latency ([1e914c5](https://www.github.com/14790897/auto-read-liunxdo/commit/1e914c5264c95642453fb5a40e34ec9af8c6d735)) * update primary database query to use CockroachDB instead of Aiven PostgreSQL ([2d4419b](https://www.github.com/14790897/auto-read-liunxdo/commit/2d4419b6cd7792bbcc15eb01714788bf21d12427)) * update RSS and topic data fetch environment variable handling for clarity ([3ae4ca6](https://www.github.com/14790897/auto-read-liunxdo/commit/3ae4ca66b16095d0e40d5a6ed541fc12990b5707)) ### [1.14.4](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.14.3...v1.14.4) (2025-11-03) ### Bug Fixes * rm hg ([0cd3ab6](https://www.github.com/14790897/auto-read-liunxdo/commit/0cd3ab642fe9cfca109a1732e4ec1e55b3412b63)) ### [1.14.3](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.14.2...v1.14.3) (2025-10-26) ### Bug Fixes * ignore cron push ([fbdb7dd](https://www.github.com/14790897/auto-read-liunxdo/commit/fbdb7ddb052c75c7be57c169e6e7f0af3cc82d01)) ### [1.14.2](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.14.1...v1.14.2) (2025-09-26) ### Bug Fixes * // [@exclude](https://www.github.com/exclude) https://linux.do/a/9611/0 ([150d94a](https://www.github.com/14790897/auto-read-liunxdo/commit/150d94a49d6f47468ec95838ee6f70a17eb0d13b)) * TargetCloseError: Protocol error (Page.addScriptToEvaluateOnNewDocument): Target closed ([a0633d0](https://www.github.com/14790897/auto-read-liunxdo/commit/a0633d06237c60aa49509b5359c30e992fea6ab7)) ### [1.14.1](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.14.0...v1.14.1) (2025-06-25) ### Bug Fixes * 变量错误 ([a0cd8d2](https://www.github.com/14790897/auto-read-liunxdo/commit/a0cd8d2336e4ec7782e42c5e784d96b09b234415)) * 环境变量错误 ([ca0426e](https://www.github.com/14790897/auto-read-liunxdo/commit/ca0426e1651f50d8bcf7b0a689fe833b6051fe45)) ## [1.14.0](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.13.0...v1.14.0) (2025-06-11) ### Features * json抓取 ([cc0bbaa](https://www.github.com/14790897/auto-read-liunxdo/commit/cc0bbaa6e24edc5cc1e80d1df81064a734b2f410)) * mongodb ([acf7be0](https://www.github.com/14790897/auto-read-liunxdo/commit/acf7be0e65317f7237eea216fa846fe3c3c025ae)) * mysql ([ad79373](https://www.github.com/14790897/auto-read-liunxdo/commit/ad79373b94c7f1deb28cd89fbe553b2e5dd9d1f8)) * 修复保存逻辑 ([b36e628](https://www.github.com/14790897/auto-read-liunxdo/commit/b36e62818cf7cc497c94dbcd9177974882cb4398)) * 多数据库 ([6a9a857](https://www.github.com/14790897/auto-read-liunxdo/commit/6a9a857ecd14cc3fd53f6b82147599a1cf3b64c1)) ## [1.13.0](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.12.0...v1.13.0) (2025-06-05) ### Features * 优化发送逻辑 ([6387c9e](https://www.github.com/14790897/auto-read-liunxdo/commit/6387c9e5daa0954760c6543281a207d46849cb36)) * 保存到数据库 ([ded9f4f](https://www.github.com/14790897/auto-read-liunxdo/commit/ded9f4fe7e378854ea2d9b31521f5b0bf699f18b)) * 去除锁,靠数据库排斥相同数据 ([2970b3a](https://www.github.com/14790897/auto-read-liunxdo/commit/2970b3a9910234ecba5ebeb69e0ce7bf12a91ef5)) * 抓取rss内容发送到电报 ([ac66311](https://www.github.com/14790897/auto-read-liunxdo/commit/ac66311b01c231a0d0088b8672bd6fca7ed2194b)) * 随机点赞时间 ([7630dd7](https://www.github.com/14790897/auto-read-liunxdo/commit/7630dd78ac4f101a08a03463efec486059ae8e94)) ### Bug Fixes * 之前没有添加action的其它环境变量 ([353edb3](https://www.github.com/14790897/auto-read-liunxdo/commit/353edb34fbde487642116e7bd13b0c35633d4243)) * 先保存后查重 ([932e9e6](https://www.github.com/14790897/auto-read-liunxdo/commit/932e9e65f07742cc22930ee380dbd3d8ec5a4ea6)) * 字符串 "false" 在 JavaScript 中是真值(truthy) ([3c0bcad](https://www.github.com/14790897/auto-read-liunxdo/commit/3c0bcadb26c9385923c536592a64421222d77fbf)) * 存在死锁问题 ([d45d7a3](https://www.github.com/14790897/auto-read-liunxdo/commit/d45d7a3b22bb6305905c220174354320350f56ac)) * 执行顺序 ([d0440b8](https://www.github.com/14790897/auto-read-liunxdo/commit/d0440b8242c99e959c39dfe403da228152e9bcb7)) * 电报未渲染 ([e3bc349](https://www.github.com/14790897/auto-read-liunxdo/commit/e3bc34938805d6545abcb4d9bfab40461a1ec489)) * 电报未渲染 ([27b40e2](https://www.github.com/14790897/auto-read-liunxdo/commit/27b40e28c36bcbb1e3fea4339dc273146ad9e536)) ## [1.12.0](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.11.0...v1.12.0) (2025-05-31) ### Features * random time ([42253d7](https://www.github.com/14790897/auto-read-liunxdo/commit/42253d72cd4f9f5f385a2058a34afac1b7d5d1a7)) ### Bug Fixes * add 错误 ([29106ab](https://www.github.com/14790897/auto-read-liunxdo/commit/29106ab6f76fac997f25b2533b63ffc776535b5c)) * 文件名错误 ([e58bc8f](https://www.github.com/14790897/auto-read-liunxdo/commit/e58bc8f54ed107308c3327f6e89d21962ac8ad83)) * 文件名错误 ([f02dcdb](https://www.github.com/14790897/auto-read-liunxdo/commit/f02dcdb9d7d60dd7b7f7bfb383b29a6e7f50e3dd)) * 无写入权限 ([e10a452](https://www.github.com/14790897/auto-read-liunxdo/commit/e10a452297d886d6cbc1507278ee1eb9b6035fe8)) * 未使用单引号 ([1089f3d](https://www.github.com/14790897/auto-read-liunxdo/commit/1089f3dcd2ddfb8b416fb4ada24aa42a9223269e)) * 权限问题 ([975f842](https://www.github.com/14790897/auto-read-liunxdo/commit/975f842556341a83c3f0cfcde2633fe47f811bf4)) * 权限问题 ([0a3890b](https://www.github.com/14790897/auto-read-liunxdo/commit/0a3890bfc079222b566ded7ee07839a74e303bb0)) * 权限问题 ([ab0d35a](https://www.github.com/14790897/auto-read-liunxdo/commit/ab0d35ab84229146dacee3ed5bfe2ffd618882d0)) * 权限问题 ([a2be5de](https://www.github.com/14790897/auto-read-liunxdo/commit/a2be5de1f25fff8b13b4970174bc608fa2058643)) * 权限问题 ([9bedc0c](https://www.github.com/14790897/auto-read-liunxdo/commit/9bedc0cbe2f06b90aab2a451f2abf5daddc1aecc)) * 权限问题 ([391f6c1](https://www.github.com/14790897/auto-read-liunxdo/commit/391f6c16fcabd4a8172128341914e0b0f490d59e)) * 权限问题 ([367e805](https://www.github.com/14790897/auto-read-liunxdo/commit/367e805f8085422fd49a9f4d4af57f7d294a7747)) * 权限问题 ([c046c1d](https://www.github.com/14790897/auto-read-liunxdo/commit/c046c1d47c299d0e991d3219fd3c6f68c8dd0b6a)) * 权限问题 ([da69937](https://www.github.com/14790897/auto-read-liunxdo/commit/da69937f6b52721223c961e74e8ea238413aa331)) * 权限问题 ([4a99c19](https://www.github.com/14790897/auto-read-liunxdo/commit/4a99c193afe05a96950ac82e61351ba845992641)) * 权限问题 ([ee6115e](https://www.github.com/14790897/auto-read-liunxdo/commit/ee6115e240e0ee717b0b4c535961a8c74f7ef22b)) * 权限问题 ([8aa6061](https://www.github.com/14790897/auto-read-liunxdo/commit/8aa6061d74d105cf1ca96aedf9d85186faffb22c)) ## [1.11.0](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.10.0...v1.11.0) (2025-05-30) ### Features * action缓存包 ([b815418](https://www.github.com/14790897/auto-read-liunxdo/commit/b815418120ec84678fb6a08480103ece8f3b920c)) ### Bug Fixes * remove apt ([9dbea4c](https://www.github.com/14790897/auto-read-liunxdo/commit/9dbea4c00cd3a78d9ba03c3ae3511858ed2da721)) ## [1.10.0](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.9.0...v1.10.0) (2025-03-26) ### Features * docker 端口 映射 ([83cd9f2](https://www.github.com/14790897/auto-read-liunxdo/commit/83cd9f26d8991101907c66ae774f48932ee09a67)) * 定时清理容器的/tmp ([4265354](https://www.github.com/14790897/auto-read-liunxdo/commit/42653542540bb3ae96c77cbf701967c1e1b9941f)) * 随机加载点赞脚本 ([1878a94](https://www.github.com/14790897/auto-read-liunxdo/commit/1878a94065dda661977919606c5a7c037cb74e58)) ### Bug Fixes * env端口错误 ([4fd5d39](https://www.github.com/14790897/auto-read-liunxdo/commit/4fd5d39404a369c9b1b9ac216d7fd06a78183be3)) * hg ([8b7f5a0](https://www.github.com/14790897/auto-read-liunxdo/commit/8b7f5a09ec89e3bb1285974fa85fe3446e0e20fc)) * 伪装huggingface ([a9856dc](https://www.github.com/14790897/auto-read-liunxdo/commit/a9856dce85f062ce8867def16f7acfe8937ca1d8)) * 禁止生成core dump ([895760e](https://www.github.com/14790897/auto-read-liunxdo/commit/895760e11d493df9aa70065059e0a2f6197ca632)) ## [1.9.0](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.8.2...v1.9.0) (2025-02-12) ### Features * 支持禁用自动点赞 ([f6f5e26](https://www.github.com/14790897/auto-read-liunxdo/commit/f6f5e2692eb6974117f0720c0be0623105c656b4)) ### [1.8.2](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.8.1...v1.8.2) (2025-02-12) ### Bug Fixes * 存在无法点击登录按钮的情况,原因未知,在等待跳转前,加入按钮点击 ([1d67b5e](https://www.github.com/14790897/auto-read-liunxdo/commit/1d67b5e0fa531148ceff3c8c3f083d253a98db75)) ### [1.8.1](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.8.0...v1.8.1) (2025-01-15) ### Bug Fixes * 展示信息 ([27ffc94](https://www.github.com/14790897/auto-read-liunxdo/commit/27ffc94610e88df9d06e847461cddb5faff27b6b)) ## [1.8.0](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.7.0...v1.8.0) (2025-01-14) ### Features * 登录失败重试三次 ([2fdee4e](https://www.github.com/14790897/auto-read-liunxdo/commit/2fdee4e6f23f16ae565594a35008101596400b36)) * 超时和密码错误逻辑分开 ([c2f61e3](https://www.github.com/14790897/auto-read-liunxdo/commit/c2f61e35f16b8fb055aa3b612c2b3c1de85194e8)) ### Bug Fixes * error ([cff54ac](https://www.github.com/14790897/auto-read-liunxdo/commit/cff54ac99fe95466b7e7b5fd2f5356193465fcec)) * error ([a106c34](https://www.github.com/14790897/auto-read-liunxdo/commit/a106c34866588a0e401c5c3e8abe0619422e349c)) * error ([e689db0](https://www.github.com/14790897/auto-read-liunxdo/commit/e689db064937e9527d6208f009c5f85b2a2ae1ce)) * xvfb包 ([da5549a](https://www.github.com/14790897/auto-read-liunxdo/commit/da5549abfb71f0651ea51dc502b039c5375ef094)) * 未使用await ([2505906](https://www.github.com/14790897/auto-read-liunxdo/commit/2505906771302de2a7f1f49abd5f7e9b75421f53)) * 检测密码错误提示 ([22d3766](https://www.github.com/14790897/auto-read-liunxdo/commit/22d3766248481d1290b83e7e3c0eddd34def4573)) * 登录失败throw error ([2880de3](https://www.github.com/14790897/auto-read-liunxdo/commit/2880de320a1829b6cb070ab8f1f647c2e2e35356)) ## [1.7.0](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.6.0...v1.7.0) (2024-11-29) ### Features * 健康探针 ([0768f30](https://www.github.com/14790897/auto-read-liunxdo/commit/0768f303e6c4a4074f3528970f5329c53d55ae07)) * 加上首页 ([b561fde](https://www.github.com/14790897/auto-read-liunxdo/commit/b561fde351ad031f4e0e1396aa4683ea0572ab1a)) ### Bug Fixes * 尝试改进dockerfile ([533a5c9](https://www.github.com/14790897/auto-read-liunxdo/commit/533a5c9d21c35c1b3c8d2d3d2c46a0dedd070493)) * 暴露端口问题 ([78a1aca](https://www.github.com/14790897/auto-read-liunxdo/commit/78a1aca637909f65909eadd85b45a36704646020)) ## [1.6.0](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.5.0...v1.6.0) (2024-10-10) ### Features * last_read_post_number完善自动阅读跳转 ([f872df4](https://www.github.com/14790897/auto-read-liunxdo/commit/f872df46d04c5726c023a6f1a3d464bf21607cc1)) * 先点赞再阅读 ([a9f016b](https://www.github.com/14790897/auto-read-liunxdo/commit/a9f016be196074d75f549ed408892ed0809e6095)) * 增加登录成功通知 ([7d4c96d](https://www.github.com/14790897/auto-read-liunxdo/commit/7d4c96dd78f7d5332a8d9d36729f46d0746c83ef)) * 自动点赞的docker ([d4b7195](https://www.github.com/14790897/auto-read-liunxdo/commit/d4b7195047ca12f4991583d93bd1a514d56a960a)) ### Bug Fixes * action不能指定点赞用户 ([4fb63a3](https://www.github.com/14790897/auto-read-liunxdo/commit/4fb63a3b035f01f05316c3b37b4cc1c305e1b933)) * docker读取.env.local变量 ([c5a61bf](https://www.github.com/14790897/auto-read-liunxdo/commit/c5a61bf5ac38cd05fbde5204976d52541f3c6dfb)) * 启动间隔时间修复 ([3d32bba](https://www.github.com/14790897/auto-read-liunxdo/commit/3d32bba8c471750b7e0e9b508eaca9e49f258c52)) ## [1.5.0](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.4.0...v1.5.0) (2024-09-12) ### Features * bypasscf使用更好的脚本 ([0fd5340](https://www.github.com/14790897/auto-read-liunxdo/commit/0fd534051629d85f9a7f31d106abf42ca4743576)) * 优化dockercompose 使得它可以直接读取env文件 ([08a9a4f](https://www.github.com/14790897/auto-read-liunxdo/commit/08a9a4fb044350d72b939531b9398fc6467cdcff)) * 使用api获得文章列表 ([ccca920](https://www.github.com/14790897/auto-read-liunxdo/commit/ccca9208b28f155af58c2513e2a0040b26b77905)) * 使用search获得不重复的post ([046e1e6](https://www.github.com/14790897/auto-read-liunxdo/commit/046e1e6026a6484ccfab4fea134f7893782c1dd5)) * 可以设置结束时间,避免action报错,默认15分钟 ([8f19e92](https://www.github.com/14790897/auto-read-liunxdo/commit/8f19e92adb2ce422787be6a4bf0abece9c385b4d)) * 多账号分批处理 ([26e516f](https://www.github.com/14790897/auto-read-liunxdo/commit/26e516fbb451769383a31804739a80786ff5c563)) * 多账号分批处理 ([a5002fb](https://www.github.com/14790897/auto-read-liunxdo/commit/a5002fb9bb104013dd5094f0a32ae37d67107e03)) * 多账号分批处理 ([500c34e](https://www.github.com/14790897/auto-read-liunxdo/commit/500c34e840c3781f6ce3236e22b5d7a2649639de)) * 点赞特定用户cron ([214b3b9](https://www.github.com/14790897/auto-read-liunxdo/commit/214b3b9507d74df9a30da8df3db0fe1459d3853f)) * 电报机器人消息推送 ([43f1f42](https://www.github.com/14790897/auto-read-liunxdo/commit/43f1f425a94589267bc716e144549088359c0ff4)) * 自动点赞特定用户 ([e2340d4](https://www.github.com/14790897/auto-read-liunxdo/commit/e2340d4433fdd8b4a05af782e89dcd6285f9b9ef)) ### Bug Fixes * cron中变量的默认值 ([10499e2](https://www.github.com/14790897/auto-read-liunxdo/commit/10499e2aa7fb02075ee7925cbeb2125a59a2d922)) * cron中变量的默认值 ([9d0302a](https://www.github.com/14790897/auto-read-liunxdo/commit/9d0302a9098267ea587456a7f607e547cd004d86)) * cron中变量的默认值 ([09fd9bd](https://www.github.com/14790897/auto-read-liunxdo/commit/09fd9bd7a0f0e2b047d3c02bd7f4d93df22503dd)) * cron中变量的默认值 ([c1c07ca](https://www.github.com/14790897/auto-read-liunxdo/commit/c1c07cae1bd109f8785907b35ebc63b28dce6e93)) * cron中变量的默认值 ([6bc8bce](https://www.github.com/14790897/auto-read-liunxdo/commit/6bc8bce2a42bbb414450454f9bd36c5a07da8fc5)) * cron中变量的默认值 ([b2227b6](https://www.github.com/14790897/auto-read-liunxdo/commit/b2227b60a0fd3af4e5abe0dbe19b85bc42ba855c)) * cron中变量的默认值 ([b9b72fc](https://www.github.com/14790897/auto-read-liunxdo/commit/b9b72fce05be1046b7f917d9820ba3d9260ab476)) * cron中变量的默认值 ([4cc640a](https://www.github.com/14790897/auto-read-liunxdo/commit/4cc640a7c15f30c0f6455a2e6343fd4681960a67)) * cron中变量的默认值 ([3b044c1](https://www.github.com/14790897/auto-read-liunxdo/commit/3b044c1cbc57cb043e53a0de15287cbd35f0fce1)) * cron中变量的默认值 ([72af582](https://www.github.com/14790897/auto-read-liunxdo/commit/72af5821a782b3eb6174372d89dbff5a78656ecf)) * cron中变量的默认值 ([8fedb7b](https://www.github.com/14790897/auto-read-liunxdo/commit/8fedb7be3930f02fa2b89a49a35484e0f8cfd273)) * env.local后才能读取环境变量,page.evaluate变量必须从外部显示的传入, 因为在浏览器上下文它是读取不了的 ([f57d512](https://www.github.com/14790897/auto-read-liunxdo/commit/f57d5128dae5e3cffc9928589cf9c427e84d648c)) * env写错了 ([19d3b64](https://www.github.com/14790897/auto-read-liunxdo/commit/19d3b644445bdd26bb4a3e5a5ebc480ed085cbee)) * run-at document-end可以修复有时候脚本不运行的问题 ([a0c35f2](https://www.github.com/14790897/auto-read-liunxdo/commit/a0c35f26a2fb187950d4a220ed096fd419e59c88)) * throw error导致无法运行 ([67811e3](https://www.github.com/14790897/auto-read-liunxdo/commit/67811e35394bf02ef1af6850dffd6b888c4091ae)) * 保存用户的时候需要清除 localStorage.removeItem("lastOffset"); ([edb72ac](https://www.github.com/14790897/auto-read-liunxdo/commit/edb72ac66ac6bcd864781d2c85d7795bc15881d9)) * 其实不需要 !topic.unseen ([d2a0ab3](https://www.github.com/14790897/auto-read-liunxdo/commit/d2a0ab3342fcd26498fab9c0e6ebac815b4c353c)) * 只有一个账号会立刻停止的问题 ([672ebee](https://www.github.com/14790897/auto-read-liunxdo/commit/672ebee00c954cd41661751a35a08868eb3d239d)) * 密码转义 ([e5802ab](https://www.github.com/14790897/auto-read-liunxdo/commit/e5802abbf770c5a65cfaee3515529d75558b8068)) * 直接使用油猴脚本 ([d429ca9](https://www.github.com/14790897/auto-read-liunxdo/commit/d429ca931cf8bddfbd14788a451e0c6d2cf05313)) * 返回 ([9f5d398](https://www.github.com/14790897/auto-read-liunxdo/commit/9f5d39814940c88628bdd648b7766143734f0201)) * 通过在主进程直接设置localstorage变量,避免单独设置 ([99b6725](https://www.github.com/14790897/auto-read-liunxdo/commit/99b67252ba7536a75708b6eb19956ace04a71122)) * 重置用户的时候需要清空post列表 ([5980c9a](https://www.github.com/14790897/auto-read-liunxdo/commit/5980c9a3b205af32fbceedc157400330eb77f3b0)) ## [1.4.0](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.3.0...v1.4.0) (2024-08-08) ### Features * action中如果secrets未定义则使用env文件 ([cc2812f](https://www.github.com/14790897/auto-read-liunxdo/commit/cc2812f6a1bdf43bc03c676a963b00ce8271f732)) * 增加了对小众软件论坛的支持(https://linux.do/t/topic/169209/166) ([598913c](https://www.github.com/14790897/auto-read-liunxdo/commit/598913c09b9bc9b880fe9f974c3da490acb6ca55)) ### Bug Fixes * action 中secret读取特殊字符处理 ([5457abc](https://www.github.com/14790897/auto-read-liunxdo/commit/5457abce09c0b26a54ef6b67b0563b49ca567e97)) * docker-compose.yml命令错误 ([ec3cedc](https://www.github.com/14790897/auto-read-liunxdo/commit/ec3cedc83895cbf8dc759770c1203fa718b52dfd)) * docker-compose.yml命令错误 ([2b4d73d](https://www.github.com/14790897/auto-read-liunxdo/commit/2b4d73de2becd56f8a1c4d7ecc8aff71de619225)) * docker-compose.yml命令错误 ([2d8a099](https://www.github.com/14790897/auto-read-liunxdo/commit/2d8a099990d986741189129dfb67ec8e8869325e)) * docker-compose.yml命令错误(https://linux.do/t/topic/169209/158) ([b83b09c](https://www.github.com/14790897/auto-read-liunxdo/commit/b83b09cbc0b96b846d78d8aa1ef242e4429bac9b)) * docker命令执行的代码 ([aa36a2b](https://www.github.com/14790897/auto-read-liunxdo/commit/aa36a2b754e2591c65dfdd9314e8676aeba60b2b)) * loginbutton作用域问题 ([1f626aa](https://www.github.com/14790897/auto-read-liunxdo/commit/1f626aa8cd8299086f91a4019779500cbd9abbfb)) * Windows需要等待cf的完成 ([bdcbeaf](https://www.github.com/14790897/auto-read-liunxdo/commit/bdcbeaff2403123b74a0e031c28560b16265798b)) * 似乎不需要特殊处理 ([dc96005](https://www.github.com/14790897/auto-read-liunxdo/commit/dc960051002d88f27e5fd5ccde5a22d6be511250)) * 增加navigation超时时长 ([7c92ff0](https://www.github.com/14790897/auto-read-liunxdo/commit/7c92ff0b3d58753571674f133eaf3bd88d9c75de)) * 增加点赞间隔 ([dc472be](https://www.github.com/14790897/auto-read-liunxdo/commit/dc472be03be350e2a69d7adc25ae628f1193f241)) * 增加点赞间隔,避免频繁429 ([706198d](https://www.github.com/14790897/auto-read-liunxdo/commit/706198d1359157da83bb839827278a6f0b61c01c)) * 还是要找button ([ba801c9](https://www.github.com/14790897/auto-read-liunxdo/commit/ba801c9cc82b6ba5825fc79111c5d8d319c50cf3)) ## [1.3.0](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.2.1...v1.3.0) (2024-08-04) ### Features * bypasscf可以绕过cf了 ([197e04f](https://www.github.com/14790897/auto-read-liunxdo/commit/197e04f1b67164ccabdb8e8347039c2ceb51d8e7)) * 截图记录功能 ([2e98654](https://www.github.com/14790897/auto-read-liunxdo/commit/2e986540f9170ef345b8d2a3e8df7b4b7a8a00c2)) * 新的cron ([5e005eb](https://www.github.com/14790897/auto-read-liunxdo/commit/5e005eb4591f193c3b45ba4029e4363c7356f62e)) ### Bug Fixes * action大小写问题 ([aeeb918](https://www.github.com/14790897/auto-read-liunxdo/commit/aeeb918fe199003b66279aa1e182496ed4b4d683)) * auto加双引号 ([b38c22e](https://www.github.com/14790897/auto-read-liunxdo/commit/b38c22ee52165cecdae63c296434f564af7f95f0)) * docker compose环境变量配置 ([b67d946](https://www.github.com/14790897/auto-read-liunxdo/commit/b67d94633920a9c8f2c5eaf6a8a08590d443cf7c)) * docker compose环境变量配置 ([a7f19bb](https://www.github.com/14790897/auto-read-liunxdo/commit/a7f19bbd3d139e5ede1402127fed13ce5bfea9f1)) * es6 dirname不存在 ([b5f02b4](https://www.github.com/14790897/auto-read-liunxdo/commit/b5f02b44c4b2c1d02d1ac83d1bcfe8d2f2e55096)) * Windows有头,Linux无头 ([5a39ded](https://www.github.com/14790897/auto-read-liunxdo/commit/5a39ded0ca6afc3790d891d70928cc7a863c5e01)) * 使用{ waitUntil: "domcontentloaded" }避免超时错误 ([8c3e38a](https://www.github.com/14790897/auto-read-liunxdo/commit/8c3e38a73efbc51c02191f0bb71350ff4486d9f5)) ### [1.2.1](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.2.0...v1.2.1) (2024-05-20) ### Bug Fixes * env注释 ([22b118f](https://www.github.com/14790897/auto-read-liunxdo/commit/22b118f5c18f24471feb48890bcbc32f905037a0)) * 使用domcontentloaded等待页面跳转 ([611290c](https://www.github.com/14790897/auto-read-liunxdo/commit/611290c4c64aa7d75736156dae61674284d67499)) * 点赞每日重置,修复只能启动一次自动点赞 ([15529df](https://www.github.com/14790897/auto-read-liunxdo/commit/15529df487d307cd421008cb9477e369cda6075a)) * 错误处理 ([c1ad79f](https://www.github.com/14790897/auto-read-liunxdo/commit/c1ad79ff26d3a806b0e9ae450b4d36a4f8e134c1)) ## [1.2.0](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.1.0...v1.2.0) (2024-05-07) ### Features * 增加local变量的读取方便调试 ([08c5631](https://www.github.com/14790897/auto-read-liunxdo/commit/08c563143d7fd1ae868e90a950cbd51ea46fe279)) ### Bug Fixes * headless改为true ([77bee42](https://www.github.com/14790897/auto-read-liunxdo/commit/77bee42accad682cc9ed4e680b1d529d7274d015)) ## [1.1.0](https://www.github.com/14790897/auto-read-liunxdo/compare/v1.0.0...v1.1.0) (2024-04-30) ### Features * cron的docker ([3f96acc](https://www.github.com/14790897/auto-read-liunxdo/commit/3f96accce19c263263ad953f3c55f422a0c16c37)) * docker 定时运行 ([963a2ed](https://www.github.com/14790897/auto-read-liunxdo/commit/963a2edf466cf113908493e1144f23c1ea9c95c6)) * docker输出日志 ([a0a13e6](https://www.github.com/14790897/auto-read-liunxdo/commit/a0a13e6f09d6226f3811cb82425852f4283f48f5)) * 环境变量可以配置阅读网站 ([fc1e52b](https://www.github.com/14790897/auto-read-liunxdo/commit/fc1e52b20fe8ad17a3c5215a3732da5d91a49d12)) * 适用于meta.discourse ([ada117a](https://www.github.com/14790897/auto-read-liunxdo/commit/ada117af31053029fcec28292844175db6b5d6a6)) ### Bug Fixes * cron ([13e2181](https://www.github.com/14790897/auto-read-liunxdo/commit/13e21815c8b4f081cef4c76d113781451e738030)) * cron ([3986f1d](https://www.github.com/14790897/auto-read-liunxdo/commit/3986f1d22c2c6be1ff0e373fad5862dcf2f3b4e5)) * cron ([d428a63](https://www.github.com/14790897/auto-read-liunxdo/commit/d428a6357f2c705c6b288188909fa5f62f35ab5a)) * cron ([82376ca](https://www.github.com/14790897/auto-read-liunxdo/commit/82376ca52bbd374ce8596b4583bc3e09e30d741c)) * cron ([b3f17c7](https://www.github.com/14790897/auto-read-liunxdo/commit/b3f17c75fbc83b2f4be005e6de8d683a4d93ad70)) * cron ([feec05e](https://www.github.com/14790897/auto-read-liunxdo/commit/feec05e10149f7314894b3d6284285f2aaa564d5)) * cron ([1ba6a4d](https://www.github.com/14790897/auto-read-liunxdo/commit/1ba6a4d7862f6d9c46b464be107b8ac7aaed66b3)) * cron ([bdd3db0](https://www.github.com/14790897/auto-read-liunxdo/commit/bdd3db0d9e0bf3109fbe2af3fee3731c9fd8478f)) * cron ([51d9566](https://www.github.com/14790897/auto-read-liunxdo/commit/51d95663b21121328ddd257e87c529fd807b7155)) * cron ([51326fe](https://www.github.com/14790897/auto-read-liunxdo/commit/51326fec312ada029e29fb8fe7b46996ae17d3a6)) * cron ([69b1801](https://www.github.com/14790897/auto-read-liunxdo/commit/69b1801b11af87685761927488937ac18592b42c)) * cron bug ([e749db7](https://www.github.com/14790897/auto-read-liunxdo/commit/e749db7d50586082e8efae2e3e3cd8b8a1f3dd56)) * cron docker ([a572a72](https://www.github.com/14790897/auto-read-liunxdo/commit/a572a720be95a8d1496699fd2203cace0cc53041)) * cron添加执行权限 ([c57da15](https://www.github.com/14790897/auto-read-liunxdo/commit/c57da15f3bd2e5f119a8f145adfb9c97d1a624e1)) * docker ([effbaa1](https://www.github.com/14790897/auto-read-liunxdo/commit/effbaa1b6982b0dd90494c0ddc9726481a824e73)) * docker 环境变量配置 ([b651584](https://www.github.com/14790897/auto-read-liunxdo/commit/b651584caa5fed554f9b95707d87e0465e3ed698)) * remove button in pteer ([edc8ef0](https://www.github.com/14790897/auto-read-liunxdo/commit/edc8ef04eb4a9034d46194722864d00f32aaadf1)) * workdir ([6083de8](https://www.github.com/14790897/auto-read-liunxdo/commit/6083de8418e1f2f8f34937f73716d30a40b673bd)) * 权限 ([4c33ce9](https://www.github.com/14790897/auto-read-liunxdo/commit/4c33ce93f29013f88611ed29d4177d2ed935fe98)) ## 1.0.0 (2024-04-05) ### Features * puppeteer ([8952911](https://www.github.com/14790897/auto-read-liunxdo/commit/895291148807dc669c10a9e0481cb9a024c57577)) * 再加一个链接 ([60cc6b0](https://www.github.com/14790897/auto-read-liunxdo/commit/60cc6b03fe884ca700b8645f646801f8d7ef088e)) * 增加脚本图标 ([61e2d35](https://www.github.com/14790897/auto-read-liunxdo/commit/61e2d354ce5b8e7c54f65233ffb2f0d89e7534fe)) * 多浏览器间隔启动 ([0647c0b](https://www.github.com/14790897/auto-read-liunxdo/commit/0647c0b721972db19451ba73a53dbe4a6831e52a)) * 多账户功能 ([c922667](https://www.github.com/14790897/auto-read-liunxdo/commit/c9226675ca22c826e09959989154cd91309d027a)) * 完善登录等待逻辑 ([6134567](https://www.github.com/14790897/auto-read-liunxdo/commit/6134567566ef695cb33244e52c08d4a0e0b1f8a7)) * 找按钮逻辑优化 ([4e392a1](https://www.github.com/14790897/auto-read-liunxdo/commit/4e392a125b6ecc7e994fa91ff67dd64cc3e01eeb)) * 收集报错 ([3b12c3b](https://www.github.com/14790897/auto-read-liunxdo/commit/3b12c3bcef358df0e7b12ccd5263ace8c9fc4eb7)) * 更好的寻找未读过的文章,但可能有问题 ([5e1e9ce](https://www.github.com/14790897/auto-read-liunxdo/commit/5e1e9ce390e886259f7e92a19a7b02201e1e1f74)) * 检查avatarImg判断登录状况 ([19efb2f](https://www.github.com/14790897/auto-read-liunxdo/commit/19efb2f74918e1e4dc8b72992f2e06a4d1d217eb)) * **点赞:** 防止已赞过的被取消 ([fcc2b40](https://www.github.com/14790897/auto-read-liunxdo/commit/fcc2b40c70c8475f83be4af2b4a7c5be601373bb)) * 自动点赞 ([a9dd8d7](https://www.github.com/14790897/auto-read-liunxdo/commit/a9dd8d74d5bcbcd9836ff0fd5df3c5014188c5a8)) * 自动点赞按钮 ([843f61f](https://www.github.com/14790897/auto-read-liunxdo/commit/843f61fe5178d7a6c4ae968a5aef2457efbda238)) * 设置正常的请求头 ([3eb58de](https://www.github.com/14790897/auto-read-liunxdo/commit/3eb58dec6e069182a852408c2900dff1b5f7fe83)) * 设置点赞上限 ([15a6ba9](https://www.github.com/14790897/auto-read-liunxdo/commit/15a6ba9cf5bccbea6ff33a5c0655e58b30e44854)) ### Bug Fixes * headless ([184461e](https://www.github.com/14790897/auto-read-liunxdo/commit/184461e27b62d0e57e0da4679b56b75e3f3a6535)) * localstorage无法访问 ([f2c1e9f](https://www.github.com/14790897/auto-read-liunxdo/commit/f2c1e9ff9ca27bd6d48296d3a3a0931b6184fba0)) * 不要刷新就启动 ([670992a](https://www.github.com/14790897/auto-read-liunxdo/commit/670992a91c031387c555682b0327cc782d309dbf)) * 修复略过已点赞按钮的逻辑错误 ([8e3089c](https://www.github.com/14790897/auto-read-liunxdo/commit/8e3089c7339fde603c26b67b0fcbb5fdc0138b3d)) * 修改等待元素 ([57ad719](https://www.github.com/14790897/auto-read-liunxdo/commit/57ad7190b0221181d746e88f1d83838b46a58dca)) * 去掉link限制,延迟2秒执行 ([98b0c93](https://www.github.com/14790897/auto-read-liunxdo/commit/98b0c936a359040ea5f5f68ed26dc02b72784c25)) * 去掉new ([f4d8c27](https://www.github.com/14790897/auto-read-liunxdo/commit/f4d8c270c20536bb60877183e9757e8069778dcb)) * 去除unread,因为可能没有文章 ([531d5b0](https://www.github.com/14790897/auto-read-liunxdo/commit/531d5b0923f4c676ff31fc1e6d5cdf43bc907443)) * 去除监听request ([32d4637](https://www.github.com/14790897/auto-read-liunxdo/commit/32d4637e79f78169f8f11f5970490a9052168b4d)) * 增加元素等待时间 ([e574e50](https://www.github.com/14790897/auto-read-liunxdo/commit/e574e509bfb676b43a5cb35bf34225ed6f7b5747)) * 增加等待时间 ([e439eee](https://www.github.com/14790897/auto-read-liunxdo/commit/e439eee13a631856fe8d524d1e7ab79eb2d618cd)) * 按钮位置移到到左下角 ([afd3394](https://www.github.com/14790897/auto-read-liunxdo/commit/afd33947af7bc86422857ad1452cb692a83707ca)) * 改变帖子位置 ([37c52ee](https://www.github.com/14790897/auto-read-liunxdo/commit/37c52eeee9296197334e0d929fd2249b8ef9adee)) * 改变帖子位置 ([0bd3aed](https://www.github.com/14790897/auto-read-liunxdo/commit/0bd3aede6a937c623d687e2edf59089511efa7e0)) * 暗色模式下看不清的问题 ([1616eb3](https://www.github.com/14790897/auto-read-liunxdo/commit/1616eb33b9432ee1636ee124acfd1860d4940669)) * 更新名字 ([3024a4c](https://www.github.com/14790897/auto-read-liunxdo/commit/3024a4c0b9ef9a691ef96b24c0e0943956e4b90d)) * 点赞429 ([a0d809c](https://www.github.com/14790897/auto-read-liunxdo/commit/a0d809ce4faeeb98c49f611eb78d384dc195b1e4)) * 点赞跳过加上英文title判断 ([36e05fb](https://www.github.com/14790897/auto-read-liunxdo/commit/36e05fb33ad9d507faae042e05a6a7821937c432)) * 环境变量名字错误 ([a3f8f1e](https://www.github.com/14790897/auto-read-liunxdo/commit/a3f8f1e1c123ff813c5443acb5a0f512493dc58f)) * 调整了浏览的速度 ([e85425f](https://www.github.com/14790897/auto-read-liunxdo/commit/e85425f3138c94a603793a1111dfabeb1c22e3c5)) * 阅读位置 ([2a3d1a3](https://www.github.com/14790897/auto-read-liunxdo/commit/2a3d1a3a25537cf9bacea3e21b2df646650fb67f)) * 页面刷新之后保持之前的状态 ([d1817b8](https://www.github.com/14790897/auto-read-liunxdo/commit/d1817b81fb9085bad392675422c1e56f5e01ce90)) * 默认不启动自动点赞 ([be0ca10](https://www.github.com/14790897/auto-read-liunxdo/commit/be0ca10aecb6ec1bcfb61af19e97b2536bfa1ad8)) ================================================ FILE: Dockerfile ================================================ FROM node:22-slim WORKDIR /app COPY package*.json ./ RUN apt update && apt install -y \ cron \ wget \ gnupg2 \ ca-certificates \ fonts-liberation \ libappindicator3-1 \ libasound2 \ libatk-bridge2.0-0 \ libatk1.0-0 \ libc6 \ libcairo2 \ libcups2 \ libdbus-1-3 \ libexpat1 \ libfontconfig1 \ libgbm1 \ libgcc1 \ libgdk-pixbuf2.0-0 \ libglib2.0-0 \ libgtk-3-0 \ libnspr4 \ libnss3 \ libpango-1.0-0 \ libpangocairo-1.0-0 \ libstdc++6 \ libx11-6 \ libx11-xcb1 \ libxcb1 \ libxcomposite1 \ libxcursor1 \ libxdamage1 \ libxext6 \ libxfixes3 \ libxi6 \ libxrandr2 \ libxrender1 \ libxss1 \ libxtst6 \ lsb-release \ xdg-utils \ xvfb \ findutils && \ rm -rf /var/lib/apt/lists/* RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-linux-signing-key.gpg && \ echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-linux-signing-key.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list && \ apt update && apt install -y google-chrome-stable && \ rm -rf /var/lib/apt/lists/* ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \ PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome ENV TZ=Asia/Shanghai RUN npm install COPY . . RUN chmod -R 777 /app # 创建清理脚本 RUN echo '#!/bin/bash\nfind /tmp -type f -atime +1 -delete' > /usr/local/bin/clean_tmp.sh && \ chmod +x /usr/local/bin/clean_tmp.sh # 设置 cron 任务 (每天凌晨 3:00 执行) RUN (crontab -l ; echo "0 3 * * * /usr/local/bin/clean_tmp.sh") | crontab - # 启动 cron 并运行主程序 (使用 CMD 作为入口点) CMD ["sh", "-c", "ulimit -c 0 && service cron start && node /app/bypasscf.js"] ================================================ FILE: Dockerfile-like-user ================================================ # 使用官方 Node.js 作为父镜像 FROM node:22-slim # 设置工作目录 WORKDIR /app # 复制 package.json 和 package-lock.json (如果存在) COPY package*.json ./ # 安装 Puppeteer 依赖 RUN apt update && apt install -y \ cron\ wget \ ca-certificates \ fonts-liberation \ libappindicator3-1 \ libasound2 \ libatk-bridge2.0-0 \ libatk1.0-0 \ libc6 \ libcairo2 \ libcups2 \ libdbus-1-3 \ libexpat1 \ libfontconfig1 \ libgbm1 \ libgcc1 \ libgdk-pixbuf2.0-0 \ libglib2.0-0 \ libgtk-3-0 \ libnspr4 \ libnss3 \ libpango-1.0-0 \ libpangocairo-1.0-0 \ libstdc++6 \ libx11-6 \ libx11-xcb1 \ libxcb1 \ libxcomposite1 \ libxcursor1 \ libxdamage1 \ libxext6 \ libxfixes3 \ libxi6 \ libxrandr2 \ libxrender1 \ libxss1 \ libxtst6 \ lsb-release \ xdg-utils \ --no-install-recommends \ xvfb \ && rm -rf /var/lib/apt/lists/* #时区为中国 ENV TZ=Asia/Shanghai # 安装 Node.js 依赖 RUN npm install # 将根目录复制到容器中 COPY . . # 设置容器启动时运行的命令 CMD ["node", "/app/bypasscf_likeUser.js"] ================================================ FILE: GITHUB_SECRETS_GUIDE.md ================================================ # ============================= 环境变量文档 ============================= # # 在 GitHub 仓库的 Settings -> Secrets and variables -> Actions 中配置以下 secrets: # # 【必需变量】 # USERNAMES - 用户名列表,多个用逗号分隔 (例如: user1,user2,user3) # PASSWORDS - 密码列表,与用户名对应,多个用逗号分隔 # WEBSITE - 目标网站 (例如: https://linux.do) # # 【功能开关】(可选) # AUTO_LIKE - 是否自动点赞 (true/false,默认: true) # HIDE_ACCOUNT_INFO - 是否在日志中隐藏账号名 (true/false,默认: true,即隐藏) # LIKE_SPECIFIC_USER - 是否只点赞特定用户 (true/false,默认: false) # ENABLE_RSS_FETCH - 是否开启RSS抓取 (true/false,默认: false) # ENABLE_TOPIC_DATA_FETCH - 是否开启话题数据抓取 (true/false,默认: false) # # 【运行配置】(可选) # RUN_TIME_LIMIT_MINUTES - 运行时间限制(分钟) (默认: 20) # SPECIFIC_USER - 特定用户ID (默认: 14790897) # HEALTH_PORT - 健康检查端口 (默认: 7860) # # 【Telegram通知】(可选) # TELEGRAM_BOT_TOKEN - Telegram机器人令牌 # TELEGRAM_CHAT_ID - Telegram聊天ID # TELEGRAM_GROUP_ID - Telegram群组ID # # 【代理配置】(可选 - 两种方式任选其一) # 方式1: 使用代理URL (推荐) # PROXY_URL - 代理URL (例如: http://user:pass@proxy.com:8080 或 socks5://user:pass@proxy.com:1080) # # 方式2: 分别配置各项 # PROXY_TYPE - 代理类型 (http/socks5) # PROXY_HOST - 代理主机地址 # PROXY_PORT - 代理端口 # PROXY_USERNAME - 代理用户名 # PROXY_PASSWORD - 代理密码 # # 【数据库配置】(可选) # POSTGRES_URI - PostgreSQL连接字符串 (主数据库) # COCKROACH_URI - CockroachDB连接字符串 (备用数据库) # NEON_URI - Neon数据库连接字符串 (备用数据库) # AIVEN_MYSQL_URI - Aiven MySQL连接字符串 # MONGO_URI - MongoDB连接字符串 # # 【已废弃】 # HF_TOKEN - HuggingFace令牌 (已失效,无需设置) # # 注意: GitHub.secrets优先级最高,即使没有设置对应的变量,它也会读取,这时变量为空值, # 导致报错,.env读取的变量无法覆盖这个值 # ======================================================================== ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 liuweiqing Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: PROXY_GUIDE.md ================================================ # 🚀 自动阅读 Linux.do 代理配置指南 ## 📖 概述 本项目支持多种代理协议,帮助您绕过网络限制,提高访问稳定性。 ## 🛠️ 支持的代理类型 - **HTTP 代理**: 最常见的代理类型 - **HTTPS 代理**: 安全的HTTP代理 - **SOCKS4 代理**: 更底层的代理协议 - **SOCKS5 代理**: 功能最强大的代理协议(推荐) ## ⚙️ 配置方法 ### 方法 1: 使用代理URL (推荐) 在 `.env` 或 `.env.local` 文件中设置: ```bash # HTTP代理 (带认证) PROXY_URL=http://username:password@proxy.example.com:8080 # SOCKS5代理 (带认证) PROXY_URL=socks5://username:password@proxy.example.com:1080 # 免费代理 (无认证) PROXY_URL=http://free-proxy.example.com:8080 ``` ### 方法 2: 分别配置参数 ```bash PROXY_TYPE=socks5 PROXY_HOST=proxy.example.com PROXY_PORT=1080 PROXY_USERNAME=your_username PROXY_PASSWORD=your_password ``` ## 🔍 代理测试 运行测试命令检查代理配置: ```bash node test_proxy.js ``` 测试工具会: - ✅ 检查代理配置是否正确 - 🌐 测试代理连接性能 - 📍 显示当前IP地址 - 💡 提供故障排查建议 ## 🌟 推荐的代理服务 ### 免费代理 - [FreeProxyList](https://www.freeproxy.world/) - [ProxyScrape](https://proxyscrape.com/free-proxy-list) - [HideMy.name](https://hidemy.name/en/proxy-list/) ### 付费代理(稳定性更好) - [Bright Data](https://brightdata.com/) - [Smartproxy](https://smartproxy.com/) - [Oxylabs](https://oxylabs.io/) - [ProxyMesh](https://proxymesh.com/) ## 🚨 故障排查 ### 常见错误及解决方案 #### 1. 超时错误 (Timeout) ``` ProtocolError: Network.enable timed out ``` **解决方案:** - 检查代理服务器响应速度 - 尝试其他代理服务器 - 增加超时时间设置 - 检查网络连接稳定性 #### 2. 连接被拒绝 (Connection Refused) ``` Error: connect ECONNREFUSED ``` **解决方案:** - 验证代理地址和端口 - 检查代理服务器状态 - 确认防火墙设置 - 检查IP是否被封禁 #### 3. 认证失败 (Authentication Failed) ``` Error: Proxy authentication required ``` **解决方案:** - 检查用户名和密码 - 确认账户状态 - 验证IP白名单设置 #### 4. DNS解析失败 ``` Error: getaddrinfo ENOTFOUND ``` **解决方案:** - 检查代理域名是否正确 - 尝试使用IP地址替代域名 - 检查DNS服务器设置 ## 🎯 最佳实践 ### 1. 代理选择建议 - **速度优先**: 选择地理位置近的代理 - **稳定性优先**: 选择付费代理服务 - **隐私优先**: 选择SOCKS5代理 ### 2. 配置优化 - 使用 `.env.local` 文件避免提交敏感信息 - 定期测试代理连接性 - 准备备用代理服务器 ### 3. 安全注意事项 - 不要在公共场所使用免费代理 - 定期更换代理密码 - 避免通过代理传输敏感信息 ## 📝 配置示例 ### 完整的 .env.local 示例 ```bash # 基本配置 USERNAMES=your_username1,your_username2 PASSWORDS=your_password1,your_password2 WEBSITE=https://linux.do # 代理配置 (选择其中一种方式) # 方式1: 使用代理URL PROXY_URL=socks5://username:password@proxy.example.com:1080 # 方式2: 分别配置 # PROXY_TYPE=socks5 # PROXY_HOST=proxy.example.com # PROXY_PORT=1080 # PROXY_USERNAME=username # PROXY_PASSWORD=password # Telegram配置 (可选) TELEGRAM_BOT_TOKEN=your_bot_token TELEGRAM_CHAT_ID=your_chat_id # 其他配置 RUN_TIME_LIMIT_MINUTES=30 AUTO_LIKE=true ``` ## 🔧 高级配置 ### 自定义超时设置 如果遇到频繁超时,可以在代码中调整: ```javascript // 在 browserOptions 中设置 browserOptions.protocolTimeout = 300000; // 5分钟 browserOptions.timeout = 120000; // 2分钟 ``` ### 代理轮换 实现多代理轮换使用: ```bash # 设置多个代理URL,用逗号分隔 PROXY_URLS=socks5://user1:pass1@proxy1.com:1080,http://user2:pass2@proxy2.com:8080 ``` ## 💬 技术支持 如果遇到问题,请: 1. 运行 `node test_proxy.js` 获取详细错误信息 2. 检查代理服务商的文档 3. 在项目 Issues 中报告问题 4. 提供完整的错误日志(隐藏敏感信息) ## 🎉 快速开始 1. 复制 `.env.example` 为 `.env.local` 2. 填入你的代理配置 3. 运行 `node test_proxy.js` 测试连接 4. 运行 `node bypasscf.js` 开始使用 祝你使用愉快! 🚀 ================================================ FILE: README.md ================================================ [英文文档](./README_en.md) ## 注意事项 1. 不显示脚本运行日志,只有登录结果 2. 阅读量统计有延迟,建议看点赞记录 ## 彩蛋 https://t.me/linuxdoSQL 每天随机抓取帖子发布在此频道 ## 使用方法一:油猴脚本(火狐不兼容,谷歌可以用) ### 油猴失去焦点后会停止运行,适合前台运行 油猴脚本代码在 index 开头的文件 中,**建议在使用前将浏览器页面缩小**,这样子可以一次滚动更多页面,读更多的回复 油猴脚本安装地址: 1. https://greasyfork.org/en/scripts/489464-auto-read 自动阅读随机点赞 2. https://greasyfork.org/en/scripts/506371-auto-like-specific-user 基于搜索到的帖子自动点赞特定用户 3. https://greasyfork.org/zh-CN/scripts/506567-auto-like-specific-user-base-on-activity 基于用户的活动自动点赞特定用户 ## 使用方法二:本地运行(Windows 默认有头浏览器(适合后台运行),Linux 默认无头浏览器) ### 1.设置环境变量 .env 里面设置用户名 密码 COOKIES 以及其它 env 里面指明的信息 设置COOKIES后无需设置密码,目前密码登录方式由于验证已失效,建议使用cookie登录,获取cookie的方法是在浏览器中打开需要阅读的网站,按F12打开开发者工具,找到Application(应用程序)选项卡,在左侧的Storage(存储)部分选择Cookies,然后找到以_t=开头的cookie值,将其复制到.env文件中的COOKIES变量中,如果有多个账户需要登录,按照逗号分隔开,例如:COOKIES="_t=lnm123,_t=abc123,_t=def456" ### 2.运行 #### Windows ```sh npm install # 自动阅读随机点赞 node .\bypasscf.js # 自动点赞特定用户 ## Windows cmd set LIKE_SPECIFIC_USER=true && node .\bypasscf.js ## Windows powershell $env:LIKE_SPECIFIC_USER = "true" node .\bypasscf.js ``` ## 使用方法三:GitHub Action 每天 随机时间 阅读 #### 说明: 每天运行,每次三十分钟(可自行调整持续时间,代码.github\workflows\cron_bypassCF.yaml 和 .github\workflows\cron_bypassCF_likeUser.yaml,持续时间由环境变量的RUN_TIME_LIMIT_MINUTES和yaml配置的timeout-minutes的最小值决定,启动时间目前为随机无法修改) **目前需要一个额外变量 `PAT_TOKEN`,用于随机时间执行阅读任务。教程:** 在 https://github.com/settings/tokens 生成一个 classic token,**需要包含 workflow 权限**,然后加入 actions 的 secrets 中,和 README 中添加其它 secrets 的过程一致。 ### 1. fork 仓库 ### 2.设置环境变量 在 GitHub action 的 secrets 设置用户名密码(变量名参考.env 中给出的),这里无法读取.env 变量 ![设置环境变量教程](image2.png) 除此之外要修改时间还要改action的时间变量: https://github.com/14790897/auto-read-liunxdo/blob/117af32dfdd0d3a6c2daf08dcd69e1aa3b7c4d00/.github/workflows/cron_bypassCF.yaml#L12 ### 3.启动 workflow 教程:https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web?tab=readme-ov-file#enable-automatic-updates 以下两个任务用于阅读 readLike(自动阅读随机点赞)和 likeUser (点赞特定用户) ## 如何增加基于 discourse 的其它网站的支持? 1. 修改 index_passage_list 中的// @match ,根据其它示例网站,填写新的 url,此外在脚本开头的 possibleBaseURLs 中也添加 url 2. 服务器运行时,还需要修改.env 下的 WEBSITE 变量为对应的网址(如果网址是不存在原先脚本的,需要修改 external.js 中对应的部分,重新构建镜像) 3. 小众软件论坛只能在 Windows 下运行,所以需要使用定制版 action: [.github\workflows\windows_cron_bypassCF.yaml](https://github.com/14790897/auto-read-liunxdo/blob/main/.github/workflows/windows_cron_bypassCF.yaml) #### 随笔 开发中遇到的问题: 问:TimeoutError: Navigation timeout of 30000 ms exceeded 为什么 puppeteer 经常出现这个错误? 答:linux 使用{waitUntil: 'domcontentloaded'}后,情况大大好转,但还是有时出现,Windows 未曾出现此问题 [见文章分析](随笔.md) 目前发现存在不点击登录按钮导致超时,已解决(原因未知) 这个也可能是因为登陆太频繁导致的,太快的登陆太多的账号 更少见的情况其实是密码错误,还有账户未激活 3.06.2026:在添加环境变量的时候,最后记得需要在action的yaml里面的env部分添加 #### 待做 1. TimeoutError 时候可以捕获错误然后关掉当前浏览器重新再开一次(已经实现刷新页面重新登录但是效果不好) 2. 自动阅读脚本可以加一个阅读速度选项(快,慢,始终),因为有用户反应读的太快了(应该是他们屏幕太小) 3. https://github.com/14790897/auto-read-liunxdo/issues/67 ## 感谢 https://linux.do/t/topic/106471 #### 使用 index_likeUser 点赞记录 9.2 handsome 9.3 lwyt 9.4 hindex 9.5 endercat 9.6 mrliushaopu 9.6 MonsterKing 9.7 zhiyang 9.8 xibalama 9.9 seeyourface LangYnn 9.10 YYWD 9.11 zhong_little 9.12 LangYnn 9.13 YYWD 9.14 wii 9.15 RunningSnail 9.16 ll0, mojihua,ywxh 9.17 GlycoProtein 9.18 Clarke.L Vyvx 9.19 azrael 9.20 Philippa shenchong 9.21lllyz hwang 9.22 include Unique 9.24 taobug 9.25 CoolMan 9.26 Madara jonty 9.27 jonty(不小心点了两次) 9.29 haoc louis miku8miku 9.30 horrZzz zxcv 10.1 bbb 10.2 zyzcom 10.4 jeff0319 Game0526 LeoMeng 10.5 kobe1 pangbaibai 10.6 xfgb lentikr 10.7 PlayMcBKuwu Tim88 10.10 elfmaid 10.11 yu_sheng orxvan l444736 time-wanderer 10.14 time-wanderer OrangeQiu Timmy_0 SINOPEC onePiece HelShiJiasi delph1s [![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](https://dartnode.com "Powered by DartNode - Free VPS for Open Source") ================================================ FILE: README_en.md ================================================ [中文文档](./README_zh.md) ## Method 1: Tampermonkey Script The Tampermonkey script can be accessed in the `index_passage_list`. You can find and install the script from Greasy Fork: ![alt text](image.png) [Tampermonkey Script: Auto Read](https://greasyfork.org/en/scripts/489464-auto-read) ## Method 2: Headless Execution with Puppeteer ### 1. Setting Environment Variables Set your username and password in the `.env` file. ### 2. Execution #### For Windows Run the following commands: ```sh npm install node .\pteer.js ``` #### For Linux (additional packages needed) Install the required packages and run the same commands as for Windows: ```sh sudo apt-get update sudo apt install nodejs npm -y sudo apt-get install -y wget unzip fontconfig locales gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget ``` ## Method 3: GitHub Actions for Daily Reading at Midnight Modify the start time and duration as needed. The code is located in `.github/workflows/cron_read.yaml`. ### 1. Fork the Repository ### 2. Set Environment Variables Set the username and password in the secrets of GitHub actions (variable names can be referred from `.env`). Note that setting the environment variables in `.env` here does not work for GitHub actions. ![alt text](image2.png) ### 3. Start the Workflow Tutorial: [Enable Automatic Updates](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web?tab=readme-ov-file#enable-automatic-updates) ## Method 4: Docker Execution ### 1. Immediate Execution Clone the repository, set environment variables in `docker-compose.yml`, and run: ```sh docker-compose up -d ``` To view logs: ```sh docker-compose logs -f ``` ### 2. Scheduled Execution Set permissions and edit the crontab: ```sh chmod +x cron.sh crontab -e ``` Manually add the following entry (to execute daily at 6 AM, adjust the directory as needed): ```sh 0 6 * * * /root/auto-read-linuxdo/cron.sh # Note this is a sample directory, change to your repository's cron.sh directory (use pwd to find your directory) ``` #### Additional Information The external script is used for puppeteer and is modified from `index_passage_list.js`. Main modifications include removing buttons and setting automatic reading and liking to start by default: ```sh localStorage.setItem("read", "true"); // Initially disables auto-scroll localStorage.setItem("autoLikeEnabled", "true"); // Auto-liking is enabled by default ``` ================================================ FILE: bypasscf.js ================================================ import fs from "fs"; import path from "path"; import puppeteer from "puppeteer-extra"; import StealthPlugin from "puppeteer-extra-plugin-stealth"; import dotenv from "dotenv"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import TelegramBot from "node-telegram-bot-api"; import fetch from "node-fetch"; import { parseStringPromise } from "xml2js"; import { parseRss } from "./src/parse_rss.js"; import { processAndSaveTopicData } from "./src/topic_data.js"; import { getProxyConfig, getPuppeteerProxyArgs, testProxyConnection, getCurrentIP, } from "./src/proxy_config.js"; dotenv.config(); // 捕获未处理的异常/Promise拒绝,避免因 Target closed 之类错误导致进程退出 process.on("unhandledRejection", (reason) => { try { const msg = (reason && reason.message) ? reason.message : String(reason); console.warn("[unhandledRejection]", msg); } catch { console.warn("[unhandledRejection] (non-string reason)"); } }); process.on("uncaughtException", (err) => { try { const msg = (err && err.message) ? err.message : String(err); console.warn("[uncaughtException]", msg); } catch { console.warn("[uncaughtException] (non-string error)"); } }); // 截图保存的文件夹 // const screenshotDir = "screenshots"; // if (!fs.existsSync(screenshotDir)) { // fs.mkdirSync(screenshotDir); // } puppeteer.use(StealthPlugin()); // Load the default .env file if (fs.existsSync(".env.local")) { console.log("Using .env.local file to supply config environment variables"); const envConfig = dotenv.parse(fs.readFileSync(".env.local")); for (const k in envConfig) { process.env[k] = envConfig[k]; } } else { console.log( "Using .env file to supply config environment variables, you can create a .env.local file to overwrite defaults, it doesn't upload to git" ); } // 读取以分钟为单位的运行时间限制 const runTimeLimitMinutes = process.env.RUN_TIME_LIMIT_MINUTES || 20; // 将分钟转换为毫秒 const runTimeLimitMillis = runTimeLimitMinutes * 60 * 1000; console.log( `运行时间限制为:${runTimeLimitMinutes} 分钟 (${runTimeLimitMillis} 毫秒)` ); // 设置一个定时器,在运行时间到达时终止进程 const shutdownTimer = setTimeout(() => { console.log("时间到,Reached time limit, shutting down the process..."); process.exit(0); // 退出进程 }, runTimeLimitMillis); const token = process.env.TELEGRAM_BOT_TOKEN; const chatId = process.env.TELEGRAM_CHAT_ID; const groupId = process.env.TELEGRAM_GROUP_ID; const specificUser = process.env.SPECIFIC_USER || "14790897"; const maxConcurrentAccounts = parseInt(process.env.MAX_CONCURRENT_ACCOUNTS) || 3; // 每批最多同时运行的账号数 const usernames = process.env.USERNAMES.split(","); const passwords = process.env.PASSWORDS ? process.env.PASSWORDS.split(",") : []; // 读取每个账号对应的Cookie(逗号分隔,与USERNAMES一一对应),有Cookie则跳过表单登录 const cookiesEnv = process.env.COOKIES ? process.env.COOKIES.split(",") : []; const loginUrl = process.env.WEBSITE || "https://linux.do"; //在GitHub action环境里它不能读取默认环境变量,只能在这里设置默认值 const delayBetweenInstances = 10000; const totalAccounts = usernames.length; // 总的账号数 const delayBetweenBatches = runTimeLimitMillis / Math.ceil(totalAccounts / maxConcurrentAccounts); const isLikeSpecificUser = process.env.LIKE_SPECIFIC_USER === "true"; // 只有明确设置为"true"才开启 const isAutoLike = process.env.AUTO_LIKE !== "false"; // 默认开启,只有明确设置为"false"才关闭 const hideAccountInfo = process.env.HIDE_ACCOUNT_INFO !== "false"; // 默认隐藏账号信息,只有明确设置为"false"才显示 const enableRssFetch = process.env.ENABLE_RSS_FETCH === "true"; // 是否开启抓取RSS,只有明确设置为"true"才开启,默认为false const enableTopicDataFetch = process.env.ENABLE_TOPIC_DATA_FETCH === "true"; // 是否开启抓取话题数据,只有明确设置为"true"才开启,默认为false // 账号名脱敏函数,默认仅显示首字母加*** function maskUsername(username) { if (!hideAccountInfo) return username; if (!username || username.length === 0) return "***"; return username[0] + "***"; } console.log( `RSS抓取功能状态: ${enableRssFetch ? "开启" : "关闭"} (环境变量值: "${process.env.ENABLE_RSS_FETCH || ''}"),勿设置` ); console.log( `话题数据抓取功能状态: ${ enableTopicDataFetch ? "开启" : "关闭" } (环境变量值: "${process.env.ENABLE_TOPIC_DATA_FETCH || ''}"),勿设置` ); // 代理配置 const proxyConfig = getProxyConfig(); if (proxyConfig) { console.log( `代理配置: ${proxyConfig.type}://${proxyConfig.host}:${proxyConfig.port}` ); // 测试代理连接 console.log("正在测试代理连接..."); const proxyWorking = await testProxyConnection(proxyConfig); if (proxyWorking) { console.log("✅ 代理连接测试成功"); } else { console.log("❌ 代理连接测试失败,将使用直连"); } } else { console.log("未配置代理,使用直连"); const currentIP = await getCurrentIP(); if (currentIP) { console.log(`当前IP地址: ${currentIP}`); } } let bot; if (token && (chatId || groupId)) { bot = new TelegramBot(token); } // 简单的 Telegram 发送重试 async function tgSendWithRetry(id, message, maxRetries = 3) { let lastErr; for (let i = 0; i < maxRetries; i++) { try { await bot.sendMessage(id, message); return true; } catch (e) { lastErr = e; const delay = 1500 * (i + 1); console.error( `Telegram send failed (attempt ${i + 1}/${maxRetries}): ${ e && e.message ? e.message : e }` ); await new Promise((r) => setTimeout(r, delay)); } } throw lastErr; } async function sendToTelegram(message) { if (!bot || !chatId) return; try { await tgSendWithRetry(chatId, message, 3); console.log("Telegram message sent successfully"); } catch (error) { console.error( "Error sending Telegram message:", error && error.code ? error.code : "", error && error.message ? error.message.slice(0, 100) : String(error).slice(0, 100) ); } } async function sendToTelegramGroup(message) { if (!bot || !groupId) { console.error("sendToTelegramGroup: bot 或 groupId 不存在"); return; } // 过滤空内容,避免 Telegram 400 错误 if (!message || !String(message).trim()) { console.warn("Telegram 群组推送内容为空,跳过发送"); return; } // 分割长消息,Telegram单条最大4096字符 const MAX_LEN = 4000; if (typeof message === "string" && message.length > MAX_LEN) { let start = 0; let part = 1; while (start < message.length) { const chunk = message.slice(start, start + MAX_LEN); try { await tgSendWithRetry(groupId, chunk, 3); console.log(`Telegram group message part ${part} sent successfully`); } catch (error) { console.error( `Error sending Telegram group message part ${part}:`, error ); } start += MAX_LEN; part++; } } else { try { await tgSendWithRetry(groupId, message, 3); console.log("Telegram group message sent successfully"); } catch (error) { console.error("Error sending Telegram group message:", error); } } } //随机等待时间 function delayClick(time) { return new Promise(function (resolve) { setTimeout(resolve, time); }); } (async () => { try { // 有Cookie则跳过密码数量校验 if ( cookiesEnv.filter((c) => c && c.trim()).length === 0 && passwords.length !== usernames.length ) { console.log( `usernames: ${usernames.length}, passwords: ${passwords.length}`, ); throw new Error("用户名和密码的数量不匹配!"); } // 并发启动浏览器实例进行登录 const loginTasks = usernames.map((username, index) => { const password = passwords[index] || ""; const cookie = cookiesEnv[index] ? cookiesEnv[index].trim() : null; const delay = (index % maxConcurrentAccounts) * delayBetweenInstances; // 使得每一组内的浏览器可以分开启动 return () => { // 确保这里返回的是函数,因为settimeout本身是异步的所以必须在外面给他一个promise await才能让它同步的等待这个时间才能执行 // 可以改为 return async () => { // await new Promise((resolve) => setTimeout(resolve, delay)); // return await launchBrowserForUser(username, password, cookie); // }; 更好理解 return new Promise((resolve, reject) => { setTimeout(() => { launchBrowserForUser(username, password, cookie) .then(resolve) .catch(reject); }, delay); }); }; }); // 依次执行每个批次的任务 for (let i = 0; i < totalAccounts; i += maxConcurrentAccounts) { console.log(`当前批次:${i + 1} - ${i + maxConcurrentAccounts}`); // 执行每批次最多 4 个账号 const batch = loginTasks .slice(i, i + maxConcurrentAccounts) .map(async (task) => { const { browser } = await task(); // 运行任务并获取浏览器实例 return browser; }); // 等待当前批次的任务完成 const browsers = await Promise.all(batch); // Task里面的任务本身是没有进行await的, 所以会继续执行下面的代码 // 如果还有下一个批次,等待指定的时间,同时,如果总共只有一个账号,也需要继续运行 if (i + maxConcurrentAccounts < totalAccounts || i === 0) { console.log(`等待 ${delayBetweenBatches / 1000} 秒`); await new Promise((resolve) => setTimeout(resolve, delayBetweenBatches), ); } else { console.log("没有下一个批次,即将结束"); } console.log( `批次 ${ Math.floor(i / maxConcurrentAccounts) + 1 } 完成,关闭浏览器...,浏览器对象:${browsers}`, ); // 关闭所有浏览器实例 for (const browser of browsers) { await browser.close(); } } console.log("所有账号登录操作已完成"); // 等待所有登录操作完成 // await Promise.all(loginTasks); } catch (error) { // 错误处理逻辑 console.error("发生错误:", error); if (token && chatId) { sendToTelegram(`${error.message}`); } } })(); // 将浏览器Cookie字符串(如 "name=value; name2=value2")解析为 puppeteer setCookie 所需的对象数组 function parseCookieString(cookieStr, domain) { return cookieStr .split(";") .map((part) => part.trim()) .filter((part) => part.includes("=")) .map((part) => { const eqIndex = part.indexOf("="); const name = part.substring(0, eqIndex).trim(); const value = part.substring(eqIndex + 1).trim(); return { name, value, domain, path: "/" }; }); } async function launchBrowserForUser(username, password, cookie = null) { let browser = null; // 在 try 之外声明 browser 变量 try { console.log("当前用户:", maskUsername(username)); const browserOptions = { headless: "auto", args: ["--no-sandbox", "--disable-setuid-sandbox"], // Linux 需要的安全设置 }; // 添加代理配置到浏览器选项 const proxyConfig = getProxyConfig(); if (proxyConfig) { const proxyArgs = getPuppeteerProxyArgs(proxyConfig); browserOptions.args.push(...proxyArgs); console.log( `为用户 ${maskUsername(username)} 启用代理: ${proxyConfig.type}://${proxyConfig.host}:${proxyConfig.port}` ); // 如果有用户名密码,puppeteer-real-browser会自动处理 if (proxyConfig.username && proxyConfig.password) { browserOptions.proxy = { host: proxyConfig.host, port: proxyConfig.port, username: proxyConfig.username, password: proxyConfig.password, }; } } var { connect } = await import("puppeteer-real-browser"); const { page, browser: newBrowser } = await connect(browserOptions); browser = newBrowser; // 将 browser 初始化 // 启动截图功能 // takeScreenshots(page); //登录操作 await navigatePage(loginUrl, page, browser); await delayClick(8000); // 设置额外的 headers await page.setExtraHTTPHeaders({ "accept-language": "en-US,en;q=0.9", }); // 验证 `navigator.webdriver` 属性是否为 undefined // const isWebDriverUndefined = await page.evaluate(() => { // return `${navigator.webdriver}`; // }); // console.log("navigator.webdriver is :", isWebDriverUndefined); // 输出应为 false page.on("pageerror", (error) => { console.error(`Page error: ${error.message}`); }); page.on("error", async (error) => { // console.error(`Error: ${error.message}`); // 检查是否是 localStorage 的访问权限错误 if ( error.message.includes( "Failed to read the 'localStorage' property from 'Window'" ) ) { console.log("Trying to refresh the page to resolve the issue..."); await page.reload(); // 刷新页面 // 重新尝试你的操作... } }); page.on("console", async (msg) => { // console.log("PAGE LOG:", msg.text()); // 使用一个标志变量来检测是否已经刷新过页面 if ( !page._isReloaded && msg.text().includes("the server responded with a status of 429") ) { // 设置标志变量为 true,表示即将刷新页面 page._isReloaded = true; //由于油候脚本它这个时候可能会导航到新的网页,会导致直接执行代码报错,所以使用这个来在每个新网页加载之前来执行 try { await page.evaluateOnNewDocument(() => { localStorage.setItem("autoLikeEnabled", "false"); }); } catch (e) { // Fallback to immediate evaluate when target already navigated/closed try { if (!page.isClosed || !page.isClosed()) { await page.evaluate(() => { localStorage.setItem("autoLikeEnabled", "false"); }); } } catch (e2) { console.warn( `Skip disabling autoLike due to closed target: ${ (e2 && e2.message) ? e2.message : e2 }` ); } } // 等待一段时间,比如 3 秒 await new Promise((resolve) => setTimeout(resolve, 3000)); console.log("Retrying now..."); // 尝试刷新页面 // await page.reload(); } }); // 登录操作:优先使用Cookie,否则使用表单登录 if (cookie) { console.log("检测到Cookie,跳过表单登录,直接设置Cookie"); const domain = new URL(loginUrl).hostname; const cookieObjects = parseCookieString(cookie, domain); await page.setCookie(...cookieObjects); console.log(`已设置 ${cookieObjects.length} 个Cookie,正在刷新页面...`); await page.reload({ waitUntil: "domcontentloaded" }); await delayClick(2000); } else { console.log("登录操作"); await login(page, username, password); } // 查找具有类名 "avatar" 的 img 元素验证登录是否成功 // 若存在 span.auth-buttons 则说明处于未登录状态 const avatarImg = await page.$("img.avatar"); const authButtons = await page.$("span.auth-buttons"); if (authButtons) { console.log("找到 auth-buttons,用户未登录,登录失败"); throw new Error("登录失败:页面显示未登录状态(auth-buttons)"); } else if (avatarImg) { console.log("找到avatarImg,登录成功"); } else { console.log("未找到avatarImg,登录失败"); throw new Error("登录失败"); } //真正执行阅读脚本 let externalScriptPath; if (isLikeSpecificUser === "true") { const randomChoice = Math.random() < 0.5; // 生成一个随机数,50% 概率为 true if (randomChoice) { externalScriptPath = path.join( dirname(fileURLToPath(import.meta.url)), "index_likeUser_activity.js" ); console.log("使用index_likeUser_activity"); } else { externalScriptPath = path.join( dirname(fileURLToPath(import.meta.url)), "index_likeUser.js" ); console.log("使用index_likeUser"); } } else { externalScriptPath = path.join( dirname(fileURLToPath(import.meta.url)), "index.js" ); } const externalScript = fs.readFileSync(externalScriptPath, "utf8"); // 在每个新的文档加载时执行外部脚本 await page.evaluateOnNewDocument( (...args) => { const [specificUser, scriptToEval, isAutoLike] = args; localStorage.setItem("read", true); localStorage.setItem("specificUser", specificUser); localStorage.setItem("isFirstRun", "false"); localStorage.setItem("autoLikeEnabled", isAutoLike); console.log("当前点赞用户:", specificUser); eval(scriptToEval); }, specificUser, externalScript, isAutoLike ); //变量必须从外部显示的传入, 因为在浏览器上下文它是读取不了的 // 添加一个监听器来监听每次页面加载完成的事件 page.on("load", async () => { // await page.evaluate(externalScript); //因为这个是在页面加载好之后执行的,而脚本是在页面加载好时刻来判断是否要执行,由于已经加载好了,脚本就不会起作用 }); // 如果是Linuxdo,就导航到我的帖子,但我感觉这里写没什么用,因为外部脚本已经定义好了,不对,这里不会点击按钮,所以不会跳转,需要手动跳转 if (loginUrl == "https://linux.do") { await page.goto("https://linux.do/t/topic/13716/790", { waitUntil: "domcontentloaded", timeout: parseInt(process.env.NAV_TIMEOUT_MS || process.env.NAV_TIMEOUT || "120000", 10), }); } else if (loginUrl == "https://meta.appinn.net") { await page.goto("https://meta.appinn.net/t/topic/52006", { waitUntil: "domcontentloaded", timeout: parseInt(process.env.NAV_TIMEOUT_MS || process.env.NAV_TIMEOUT || "120000", 10), }); } else { await page.goto(`${loginUrl}/t/topic/1`, { waitUntil: "domcontentloaded", timeout: parseInt(process.env.NAV_TIMEOUT_MS || process.env.NAV_TIMEOUT || "120000", 10), }); } // Ensure automation injected after navigation (fallback in case init-script failed) try { await page.evaluate( (specificUser, scriptToEval, isAutoLike) => { if (!window.__autoInjected) { localStorage.setItem("read", true); localStorage.setItem("specificUser", specificUser); localStorage.setItem("isFirstRun", "false"); localStorage.setItem("autoLikeEnabled", isAutoLike); try { eval(scriptToEval); } catch (e) { console.error("eval external script failed", e); } window.__autoInjected = true; } }, specificUser, externalScript, isAutoLike ); } catch (e) { console.warn(`Post-navigation inject failed: ${e && e.message ? e.message : e}`); } if (token && chatId) { sendToTelegram(`${username} 登录成功`); } // 监听页面跳转到新话题,自动推送RSS example:https://linux.do/t/topic/525305.rss // 记录已推送过的 topicId,防止重复推送 if (enableRssFetch || enableTopicDataFetch) { const pushedTopicIds = new Set(); const processedTopicIds = new Set(); // 用于话题数据处理的记录 page.on("framenavigated", async (frame) => { if (frame.parentFrame() !== null) return; const url = frame.url(); const match = url.match(/https:\/\/linux\.do\/t\/topic\/(\d+)/); if (match) { const topicId = match[1]; // RSS抓取处理 if (enableRssFetch && !pushedTopicIds.has(topicId)) { pushedTopicIds.add(topicId); const rssUrl = `https://linux.do/t/topic/${topicId}.rss`; console.log("检测到话题跳转,抓取RSS:", rssUrl); try { // 停顿1.5秒再抓取 await new Promise((r) => setTimeout(r, 1500)); const rssPage = await browser.newPage(); await rssPage.goto(rssUrl, { waitUntil: "domcontentloaded", timeout: 20000, }); // 停顿0.5秒再获取内容,确保页面渲染完成 await new Promise((r) => setTimeout(r, 1000)); const xml = await rssPage.evaluate(() => document.body.innerText); await rssPage.close(); const parsedData = await parseRss(xml); sendToTelegramGroup(parsedData); } catch (e) { console.error("抓取或发送RSS失败:", e, "可能是非公开话题"); } } // 话题数据抓取处理 if (enableTopicDataFetch && !processedTopicIds.has(topicId)) { processedTopicIds.add(topicId); console.log("检测到话题跳转,抓取话题数据:", url); try { // 停顿1秒再处理话题数据 await new Promise((r) => setTimeout(r, 1000)); await processAndSaveTopicData(page, url); } catch (e) { console.error("抓取或保存话题数据失败:", e); } } } // 停顿0.5秒后允许下次抓取 await new Promise((r) => setTimeout(r, 500)); }); } return { browser }; } catch (err) { // throw new Error(err); console.log("Error in launchBrowserForUser:", err); if (token && chatId) { sendToTelegram(`${err.message}`); } return { browser }; // 错误时仍然返回 browser } } async function login(page, username, password, retryCount = 3) { // 使用XPath查询找到包含"登录"或"login"文本的按钮 let loginButtonFound = await page.evaluate(() => { let loginButton = Array.from(document.querySelectorAll("button")).find( (button) => button.textContent.includes("登录") || button.textContent.includes("login") ); // 注意loginButton 变量在外部作用域中是无法被 page.evaluate 内部的代码直接修改的。page.evaluate 的代码是在浏览器环境中执行的,这意味着它们无法直接影响 Node.js 环境中的变量 // 如果没有找到,尝试根据类名查找 if (!loginButton) { loginButton = document.querySelector(".login-button"); } if (loginButton) { loginButton.click(); console.log("Login button clicked."); return true; // 返回true表示找到了按钮并点击了 } else { console.log("Login button not found."); return false; // 返回false表示没有找到按钮 } }); if (!loginButtonFound) { if (loginUrl == "https://meta.appinn.net") { await page.goto("https://meta.appinn.net/t/topic/52006", { waitUntil: "domcontentloaded", timeout: parseInt(process.env.NAV_TIMEOUT_MS || process.env.NAV_TIMEOUT || "120000", 10), }); await page.click(".discourse-reactions-reaction-button"); } else { await page.goto(`${loginUrl}/t/topic/1`, { waitUntil: "domcontentloaded", timeout: parseInt(process.env.NAV_TIMEOUT_MS || process.env.NAV_TIMEOUT || "120000", 10), }); try { await page.click(".discourse-reactions-reaction-button"); } catch (error) { console.log("没有找到点赞按钮,可能是页面没有加载完成或按钮不存在"); } } } // 等待用户名输入框加载 await page.waitForSelector("#login-account-name"); // 模拟人类在找到输入框后的短暂停顿 await delayClick(1000); // 延迟500毫秒 // 清空输入框并输入用户名 await page.click("#login-account-name", { clickCount: 3 }); await page.type("#login-account-name", username, { delay: 100, }); // 输入时在每个按键之间添加额外的延迟 await delayClick(1000); // 等待密码输入框加载 // await page.waitForSelector("#login-account-password"); // 模拟人类在输入用户名后的短暂停顿 // delayClick; // 清空输入框并输入密码 await page.click("#login-account-password", { clickCount: 3 }); await page.type("#login-account-password", password, { delay: 100, }); // 模拟人类在输入完成后思考的短暂停顿 await delayClick(1000); // 假设登录按钮的ID是'login-button',点击登录按钮 await page.waitForSelector("#login-button"); await delayClick(1000); // 模拟在点击登录按钮前的短暂停顿 await page.click("#login-button"); try { await Promise.all([ page.waitForNavigation({ waitUntil: "domcontentloaded" }), // 等待 页面跳转 DOMContentLoaded 事件 // 去掉上面一行会报错:Error: Execution context was destroyed, most likely because of a navigation. 可能是因为之后没等页面加载完成就执行了脚本 page.click("#login-button", { force: true }), // 点击登录按钮触发跳转 ]); //注意如果登录失败,这里会一直等待跳转,导致脚本执行失败 这点四个月之前你就发现了结果今天又遇到(有个用户遇到了https://linux.do/t/topic/169209/82),但是你没有在这个报错你提示我8.5 } catch (error) { const alertError = await page.$(".alert.alert-error"); if (alertError) { const alertText = await page.evaluate((el) => el.innerText, alertError); // 使用 evaluate 获取 innerText if ( alertText.includes("incorrect") || alertText.includes("Incorrect ") || alertText.includes("不正确") ) { throw new Error( `非超时错误,请检查用户名密码是否正确,失败用户 ${maskUsername(username)}, 错误信息:${alertText}` ); } else { throw new Error( `非超时错误,也不是密码错误,可能是IP导致,需使用中国美国香港台湾IP,失败用户 ${maskUsername(username)},错误信息:${alertText}` ); } } else { if (retryCount > 0) { console.log("Retrying login..."); await page.reload({ waitUntil: "domcontentloaded", timeout: parseInt(process.env.NAV_TIMEOUT_MS || process.env.NAV_TIMEOUT || "120000", 10) }); await delayClick(2000); // 增加重试前的延迟 return await login(page, username, password, retryCount - 1); } else { throw new Error( `Navigation timed out in login.超时了,可能是IP质量问题,失败用户 ${maskUsername(username)}, ${error}` ); //{password} } } } await delayClick(1000); } async function navigatePage(url, page, browser) { try { page.setDefaultNavigationTimeout( parseInt(process.env.NAV_TIMEOUT_MS || process.env.NAV_TIMEOUT || "120000", 10) ); } catch {} await page.goto(url, { waitUntil: "domcontentloaded" }); //如果使用默认的load,linux下页面会一直加载导致无法继续执行 const startTime = Date.now(); // 记录开始时间 let pageTitle = await page.title(); // 获取当前页面标题 while (pageTitle.includes("Just a moment") || pageTitle.includes("请稍候")) { console.log("The page is under Cloudflare protection. Waiting..."); await delayClick(2000); // 每次检查间隔2秒 // 重新获取页面标题 pageTitle = await page.title(); // 检查是否超过15秒 if (Date.now() - startTime > 35000) { console.log("Timeout exceeded, aborting actions."); sendToTelegram(`超时了,无法通过Cloudflare验证`); await browser.close(); // todo: 这里其实不能关的m因为我们是在最后统一关的你不能在这里关m如果你在这关,后面就会触发attempted to sue detached frame return; // 超时则退出函数 } } console.log("页面标题:", pageTitle); } // 每秒截图功能 async function takeScreenshots(page) { let screenshotIndex = 0; setInterval(async () => { screenshotIndex++; const screenshotPath = path.join( screenshotDir, `screenshot-${screenshotIndex}.png` ); try { await page.screenshot({ path: screenshotPath, fullPage: true }); console.log(`Screenshot saved: ${screenshotPath}`); } catch (error) { console.error("Error taking screenshot:", error); } }, 1000); // 注册退出时删除文件夹的回调函数 process.on("exit", () => { try { fs.rmdirSync(screenshotDir, { recursive: true }); console.log(`Deleted folder: ${screenshotDir}`); } catch (error) { console.error(`Error deleting folder ${screenshotDir}:`, error); } }); } import express from "express"; const healthApp = express(); const HEALTH_PORT = process.env.HEALTH_PORT || 7860; // 健康探针路由 healthApp.get("/health", (req, res) => { const memoryUsage = process.memoryUsage(); // 将字节转换为MB const memoryUsageMB = { rss: `${(memoryUsage.rss / (1024 * 1024)).toFixed(2)} MB`, // 转换为MB并保留两位小数 heapTotal: `${(memoryUsage.heapTotal / (1024 * 1024)).toFixed(2)} MB`, heapUsed: `${(memoryUsage.heapUsed / (1024 * 1024)).toFixed(2)} MB`, external: `${(memoryUsage.external / (1024 * 1024)).toFixed(2)} MB`, arrayBuffers: `${(memoryUsage.arrayBuffers / (1024 * 1024)).toFixed(2)} MB`, }; const healthData = { status: "OK", timestamp: new Date().toISOString(), memoryUsage: memoryUsageMB, uptime: process.uptime().toFixed(2), // 保留两位小数 }; res.status(200).json(healthData); }); healthApp.get("/", (req, res) => { res.send(` Auto Read

Welcome to the Auto Read App

You can check the server's health at /health.

GitHub: https://github.com/14790897/auto-read-liunxdo

`); }); healthApp.listen(HEALTH_PORT, () => { console.log( `Health check endpoint is running at http://localhost:${HEALTH_PORT}/health` ); }); ================================================ FILE: bypasscf_likeUser_not_use.js ================================================ import fs from "fs"; import path from "path"; import puppeteer from "puppeteer-extra"; import StealthPlugin from "puppeteer-extra-plugin-stealth"; import dotenv from "dotenv"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import TelegramBot from "node-telegram-bot-api"; dotenv.config(); // 截图保存的文件夹 // const screenshotDir = "screenshots"; // if (!fs.existsSync(screenshotDir)) { // fs.mkdirSync(screenshotDir); // } puppeteer.use(StealthPlugin()); // Load the default .env file if (fs.existsSync(".env.local")) { console.log("Using .env.local file to supply config environment variables"); const envConfig = dotenv.parse(fs.readFileSync(".env.local")); for (const k in envConfig) { process.env[k] = envConfig[k]; } } else { console.log( "Using .env file to supply config environment variables, you can create a .env.local file to overwrite defaults, it doesn't upload to git" ); } // 读取以分钟为单位的运行时间限制 const runTimeLimitMinutes = process.env.RUN_TIME_LIMIT_MINUTES || 20; // 将分钟转换为毫秒 const runTimeLimitMillis = runTimeLimitMinutes * 60 * 1000; console.log( `运行时间限制为:${runTimeLimitMinutes} 分钟 (${runTimeLimitMillis} 毫秒)` ); // 设置一个定时器,在运行时间到达时终止进程 const shutdownTimer = setTimeout(() => { console.log("时间到,Reached time limit, shutting down the process..."); process.exit(0); // 退出进程 }, runTimeLimitMillis); const token = process.env.TELEGRAM_BOT_TOKEN; const chatId = process.env.TELEGRAM_CHAT_ID; const specificUser = process.env.SPECIFIC_USER || "14790897"; const maxConcurrentAccounts = 4; // 每批最多同时运行的账号数 const usernames = process.env.USERNAMES.split(","); const passwords = process.env.PASSWORDS.split(","); const loginUrl = process.env.WEBSITE || "https://linux.do"; //在GitHub action环境里它不能读取默认环境变量,只能在这里设置默认值 const delayBetweenInstances = 10000; const totalAccounts = usernames.length; // 总的账号数 const delayBetweenBatches = runTimeLimitMillis / Math.ceil(totalAccounts / maxConcurrentAccounts); let bot; if (token && chatId) { bot = new TelegramBot(token); } function sendToTelegram(message) { if (!bot) return; bot .sendMessage(chatId, message) .then(() => { console.log("Telegram message sent successfully"); }) .catch((error) => { console.error("Error sending Telegram message:", error); }); } //随机等待时间 function delayClick(time) { return new Promise(function (resolve) { setTimeout(resolve, time); }); } (async () => { try { if (usernames.length !== passwords.length) { console.log(usernames.length, usernames, passwords.length, passwords); throw new Error("用户名和密码的数量不匹配!"); } // 并发启动浏览器实例进行登录 const loginTasks = usernames.map((username, index) => { const password = passwords[index]; const delay = (index % maxConcurrentAccounts) * delayBetweenInstances; // 使得每一组内的浏览器可以分开启动 return () => { // 确保这里返回的是函数 return new Promise((resolve, reject) => { setTimeout(() => { launchBrowserForUser(username, password) .then(resolve) .catch(reject); }, delay); }); }; }); // 依次执行每个批次的任务 for (let i = 0; i < totalAccounts; i += maxConcurrentAccounts) { console.log(`当前批次:${i + 1} - ${i + maxConcurrentAccounts}`); // 执行每批次最多 4 个账号 const batch = loginTasks .slice(i, i + maxConcurrentAccounts) .map(async (task) => { const { browser } = await task(); // 运行任务并获取浏览器实例 return browser; }); // 等待当前批次的任务完成 const browsers = await Promise.all(batch); // Task里面的任务本身是没有进行await的, 所以会继续执行下面的代码 // 如果还有下一个批次,等待指定的时间,同时,如果总共只有一个账号,也需要继续运行 if (i + maxConcurrentAccounts < totalAccounts || i === 0) { console.log(`等待 ${delayBetweenBatches / 1000} 秒`); await new Promise((resolve) => setTimeout(resolve, delayBetweenBatches) ); } else { console.log("没有下一个批次,即将结束"); } console.log( `批次 ${ Math.floor(i / maxConcurrentAccounts) + 1 } 完成,关闭浏览器...,浏览器对象:${browsers}` ); // 关闭所有浏览器实例 for (const browser of browsers) { await browser.close(); } } console.log("所有账号登录操作已完成"); // 等待所有登录操作完成 // await Promise.all(loginTasks); } catch (error) { // 错误处理逻辑 console.error("发生错误:", error); if (token && chatId) { sendToTelegram(`${error.message}`); } } })(); async function launchBrowserForUser(username, password) { let browser = null; // 在 try 之外声明 browser 变量 try { console.log("当前用户:", username); const browserOptions = { headless: "auto", args: ["--no-sandbox", "--disable-setuid-sandbox"], // Linux 需要的安全设置 }; // 如果环境变量不是 'dev',则添加代理配置 // if (process.env.ENVIRONMENT !== "dev") { // browserOptions["proxy"] = { // host: "38.154.227.167", // port: "5868", // username: "pqxujuyl", // password: "y1nmb5kjbz9t", // }; // } var { connect } = await import("puppeteer-real-browser"); const { page, browser: newBrowser } = await connect(browserOptions); browser = newBrowser; // 将 browser 初始化 // 启动截图功能 // takeScreenshots(page); //登录操作 await navigatePage(loginUrl, page, browser); await delayClick(8000); // 设置额外的 headers await page.setExtraHTTPHeaders({ "accept-language": "en-US,en;q=0.9", }); // 验证 `navigator.webdriver` 属性是否为 undefined // const isWebDriverUndefined = await page.evaluate(() => { // return `${navigator.webdriver}`; // }); // console.log("navigator.webdriver is :", isWebDriverUndefined); // 输出应为 false page.on("pageerror", (error) => { console.error(`Page error: ${error.message}`); }); page.on("error", async (error) => { // console.error(`Error: ${error.message}`); // 检查是否是 localStorage 的访问权限错误 if ( error.message.includes( "Failed to read the 'localStorage' property from 'Window'" ) ) { console.log("Trying to refresh the page to resolve the issue..."); await page.reload(); // 刷新页面 // 重新尝试你的操作... } }); page.on("console", async (msg) => { // console.log("PAGE LOG:", msg.text()); // 使用一个标志变量来检测是否已经刷新过页面 if ( !page._isReloaded && msg.text().includes("the server responded with a status of 429") ) { // 设置标志变量为 true,表示即将刷新页面 page._isReloaded = true; //由于油候脚本它这个时候可能会导航到新的网页,会导致直接执行代码报错,所以使用这个来在每个新网页加载之前来执行 await page.evaluateOnNewDocument(() => { localStorage.setItem("autoLikeEnabled", "false"); }); // 等待一段时间,比如 3 秒 await new Promise((resolve) => setTimeout(resolve, 3000)); console.log("Retrying now..."); // 尝试刷新页面 // await page.reload(); } }); // //登录操作 console.log("登录操作"); await login(page, username, password); // 查找具有类名 "avatar" 的 img 元素验证登录是否成功 const avatarImg = await page.$("img.avatar"); if (avatarImg) { console.log("找到avatarImg,登录成功"); } else { console.log("未找到avatarImg,登录失败"); throw new Error("登录失败"); } //真正执行阅读脚本 const externalScriptPath = path.join( dirname(fileURLToPath(import.meta.url)), "index_likeUser.js" ); const externalScript = fs.readFileSync(externalScriptPath, "utf8"); // 在每个新的文档加载时执行外部脚本 await page.evaluateOnNewDocument( (...args) => { const [specificUser, scriptToEval] = args; localStorage.setItem("read", true); localStorage.setItem("specificUser", specificUser); localStorage.setItem("isFirstRun", "false"); console.log("当前点赞用户:", specificUser); eval(scriptToEval); }, specificUser, externalScript ); //变量必须从外部显示的传入, 因为在浏览器上下文它是读取不了的 // 添加一个监听器来监听每次页面加载完成的事件 page.on("load", async () => { // await page.evaluate(externalScript); //因为这个是在页面加载好之后执行的,而脚本是在页面加载好时刻来判断是否要执行,由于已经加载好了,脚本就不会起作用 }); // 如果是Linuxdo,就导航到我的帖子,但我感觉这里写没什么用,因为外部脚本已经定义好了,不对,这里不会点击按钮,所以不会跳转,需要手动跳转 if (loginUrl == "https://linux.do") { await page.goto("https://linux.do/t/topic/13716/630", { waitUntil: "domcontentloaded", }); } else if (loginUrl == "https://meta.appinn.net") { await page.goto("https://meta.appinn.net/t/topic/52006", { waitUntil: "domcontentloaded", }); } else { await page.goto(`${loginUrl}/t/topic/1`, { waitUntil: "domcontentloaded", }); } if (token && chatId) { sendToTelegram(`${username} 登录成功`); } return { browser }; } catch (err) { // throw new Error(err); console.log("Error in launchBrowserForUser:", err); if (token && chatId) { sendToTelegram(`${err.message}`); } return { browser }; // 错误时仍然返回 browser } } async function login(page, username, password, retryCount = 3) { // 使用XPath查询找到包含"登录"或"login"文本的按钮 let loginButtonFound = await page.evaluate(() => { let loginButton = Array.from(document.querySelectorAll("button")).find( (button) => button.textContent.includes("登录") || button.textContent.includes("login") ); // 注意loginButton 变量在外部作用域中是无法被 page.evaluate 内部的代码直接修改的。page.evaluate 的代码是在浏览器环境中执行的,这意味着它们无法直接影响 Node.js 环境中的变量 // 如果没有找到,尝试根据类名查找 if (!loginButton) { loginButton = document.querySelector(".login-button"); } if (loginButton) { loginButton.click(); console.log("Login button clicked."); return true; // 返回true表示找到了按钮并点击了 } else { console.log("Login button not found."); return false; // 返回false表示没有找到按钮 } }); if (!loginButtonFound) { if (loginUrl == "https://meta.appinn.net") { await page.goto("https://meta.appinn.net/t/topic/52006", { waitUntil: "domcontentloaded", }); await page.click(".discourse-reactions-reaction-button"); } else { await page.goto(`${loginUrl}/t/topic/1`, { waitUntil: "domcontentloaded", }); await page.click(".discourse-reactions-reaction-button"); } } // 等待用户名输入框加载 await page.waitForSelector("#login-account-name"); // 模拟人类在找到输入框后的短暂停顿 await delayClick(500); // 延迟500毫秒 // 清空输入框并输入用户名 await page.click("#login-account-name", { clickCount: 3 }); await page.type("#login-account-name", username, { delay: 100, }); // 输入时在每个按键之间添加额外的延迟 // 等待密码输入框加载 await page.waitForSelector("#login-account-password"); // 模拟人类在输入用户名后的短暂停顿 // delayClick; // 清空输入框并输入密码 await page.click("#login-account-password", { clickCount: 3 }); await page.type("#login-account-password", password, { delay: 100, }); // 模拟人类在输入完成后思考的短暂停顿 await delayClick(1000); // 假设登录按钮的ID是'login-button',点击登录按钮 await page.waitForSelector("#login-button"); await page.click("#login-button"); await delayClick(500); // 模拟在点击登录按钮前的短暂停顿 try { await Promise.all([ page.waitForNavigation({ waitUntil: "domcontentloaded" }), // 等待 页面跳转 DOMContentLoaded 事件 // 去掉上面一行会报错:Error: Execution context was destroyed, most likely because of a navigation. 可能是因为之后没等页面加载完成就执行了脚本 page.click("#login-button", { force: true }), // 点击登录按钮触发跳转 ]); //注意如果登录失败,这里会一直等待跳转,导致脚本执行失败 这点四个月之前你就发现了结果今天又遇到(有个用户遇到了https://linux.do/t/topic/169209/82),但是你没有在这个报错你提示我8.5 } catch (error) { const alertError = await page.$(".alert.alert-error"); if (alertError) { const alertText = await page.evaluate((el) => el.innerText, alertError); // 使用 evaluate 获取 innerText if ( alertText.includes("incorrect") || alertText.includes("Incorrect ") || alertText.includes("不正确") ) { throw new Error( `非超时错误,请检查用户名密码是否正确,失败用户 ${username}, 错误信息:${alertText}` ); } else { throw new Error( `非超时错误,也不是密码错误,失败用户 ${username},错误信息:${alertText}` ); } } else { if (retryCount > 0) { console.log("Retrying login..."); await delayClick(2000); // 增加重试前的延迟 return await login(page, username, password, retryCount - 1); } else { throw new Error( `Navigation timed out in login.超时了,可能是IP质量问题,失败用户 ${username}, ${error}` ); //{password} } } } await delayClick(1000); } async function navigatePage(url, page, browser) { await page.goto(url, { waitUntil: "domcontentloaded" }); //如果使用默认的load,linux下页面会一直加载导致无法继续执行 const startTime = Date.now(); // 记录开始时间 let pageTitle = await page.title(); // 获取当前页面标题 while (pageTitle.includes("Just a moment")) { console.log("The page is under Cloudflare protection. Waiting..."); await delayClick(2000); // 每次检查间隔2秒 // 重新获取页面标题 pageTitle = await page.title(); // 检查是否超过15秒 if (Date.now() - startTime > 35000) { console.log("Timeout exceeded, aborting actions."); await browser.close(); return; // 超时则退出函数 } } console.log("页面标题:", pageTitle); } // 每秒截图功能 async function takeScreenshots(page) { let screenshotIndex = 0; setInterval(async () => { screenshotIndex++; const screenshotPath = path.join( screenshotDir, `screenshot-${screenshotIndex}.png` ); try { await page.screenshot({ path: screenshotPath, fullPage: true }); console.log(`Screenshot saved: ${screenshotPath}`); } catch (error) { console.error("Error taking screenshot:", error); } }, 1000); // 注册退出时删除文件夹的回调函数 process.on("exit", () => { try { fs.rmdirSync(screenshotDir, { recursive: true }); console.log(`Deleted folder: ${screenshotDir}`); } catch (error) { console.error(`Error deleting folder ${screenshotDir}:`, error); } }); } import express from "express"; const healthApp = express(); const HEALTH_PORT = process.env.HEALTH_PORT || 7860; // 健康探针路由 healthApp.get("/health", (req, res) => { const memoryUsage = process.memoryUsage(); // 将字节转换为MB const memoryUsageMB = { rss: `${(memoryUsage.rss / (1024 * 1024)).toFixed(2)} MB`, // 转换为MB并保留两位小数 heapTotal: `${(memoryUsage.heapTotal / (1024 * 1024)).toFixed(2)} MB`, heapUsed: `${(memoryUsage.heapUsed / (1024 * 1024)).toFixed(2)} MB`, external: `${(memoryUsage.external / (1024 * 1024)).toFixed(2)} MB`, arrayBuffers: `${(memoryUsage.arrayBuffers / (1024 * 1024)).toFixed(2)} MB`, }; const healthData = { status: "OK", timestamp: new Date().toISOString(), memoryUsage: memoryUsageMB, uptime: process.uptime().toFixed(2), // 保留两位小数 }; res.status(200).json(healthData); }); healthApp.get("/", (req, res) => { res.send(` Auto Read

Welcome to the Auto Read App

You can check the server's health at /health.

GitHub: https://github.com/14790897/auto-read-liunxdo

`); }); healthApp.listen(HEALTH_PORT, () => { console.log( `Health check endpoint is running at http://localhost:${HEALTH_PORT}/health` ); }); ================================================ FILE: bypasscf_playwright.mjs ================================================ /* Auto Read App (Playwright 版本) --------------------------------- 将原 Puppeteer + puppeteer-extra + puppeteer-real-browser 的脚本整体迁移到 Playwright。 主要差异: 1. 使用 playwright‑extra + stealth 插件来隐藏自动化特征。 2. Puppeteer 的 page.evaluateOnNewDocument → Playwright 的 page.addInitScript。 3. Puppeteer 的 type/click 改为 Playwright 的 fill/click/locator 组合。 4. Browser 实例 → Context → Page 三层模型。 5. 其余业务逻辑、并发调度、Telegram、Express 健康探针保持不变。 依赖安装: ---------------- npm i playwright-extra playwright-extra-plugin-stealth playwright @playwright/test node-telegram-bot-api dotenv express # 若需要下载浏览器: npx playwright install chromium 运行: node auto-read-playwright.mjs */ import fs from "fs"; import path, { dirname, join } from "path"; import { fileURLToPath } from "url"; import dotenv from "dotenv"; import TelegramBot from "node-telegram-bot-api"; import express from "express"; // --- Playwright & Stealth ---------------------------------------------- import { chromium } from "playwright-extra"; import StealthPlugin from 'puppeteer-extra-plugin-stealth' const stealth = StealthPlugin(); chromium.use(stealth); // 代理配置 import { getProxyConfig, getPlaywrightProxyConfig, testProxyConnection, getCurrentIP } from "./src/proxy_config.js"; // ----------------------------------------------------------------------- // 环境变量加载 // ----------------------------------------------------------------------- if (fs.existsSync(".env.local")) { console.log("Using .env.local file to supply config environment variables"); const envConfig = dotenv.parse(fs.readFileSync(".env.local")); for (const k in envConfig) process.env[k] = envConfig[k]; } else { dotenv.config(); console.log("Using .env file to supply config environment variables…"); } // 运行时间限制 const runTimeLimitMinutes = Number(process.env.RUN_TIME_LIMIT_MINUTES || 20); const runTimeLimitMillis = runTimeLimitMinutes * 60 * 1000; console.log(`运行时间限制:${runTimeLimitMinutes} 分钟 (${runTimeLimitMillis} ms)`); setTimeout(() => { console.log("Reached time limit, shutting down…"); process.exit(0); }, runTimeLimitMillis); // Telegram -------------------------------------------------------------- const token = process.env.TELEGRAM_BOT_TOKEN; const chatId = process.env.TELEGRAM_CHAT_ID; let bot = null; if (token && chatId) bot = new TelegramBot(token); const sendToTelegram = (msg) => bot?.sendMessage(chatId, msg).catch(console.error); // 全局配置 -------------------------------------------------------------- const maxConcurrentAccounts = 4; const usernames = (process.env.USERNAMES || "").split(",").filter(Boolean); const passwords = (process.env.PASSWORDS || "").split(",").filter(Boolean); if (usernames.length !== passwords.length) throw new Error("USERNAMES 与 PASSWORDS 数量不一致"); const loginUrl = process.env.WEBSITE || "https://linux.do"; const totalAccounts = usernames.length; const delayBetweenInstances = 10_000; // 每批账号实例间隔 const delayBetweenBatches = runTimeLimitMillis / Math.ceil(totalAccounts / maxConcurrentAccounts); const specificUser = process.env.SPECIFIC_USER || "14790897"; const isLikeSpecificUser = process.env.LIKE_SPECIFIC_USER === "true"; const isAutoLike = process.env.AUTO_LIKE !== "false"; // 默认自动 const delay = (ms) => new Promise((r) => setTimeout(r, ms)); // ----------------------------------------------------------------------- // 主流程 // ----------------------------------------------------------------------- (async () => { try { // 代理配置检查 const proxyConfig = getProxyConfig(); if (proxyConfig) { console.log(`代理配置: ${proxyConfig.type}://${proxyConfig.host}:${proxyConfig.port}`); // 测试代理连接 console.log("正在测试代理连接..."); const proxyWorking = await testProxyConnection(proxyConfig); if (proxyWorking) { console.log("✅ 代理连接测试成功"); } else { console.log("❌ 代理连接测试失败,将使用直连"); } } else { console.log("未配置代理,使用直连"); const currentIP = await getCurrentIP(); if (currentIP) { console.log(`当前IP地址: ${currentIP}`); } } const batches = []; for (let i = 0; i < totalAccounts; i += maxConcurrentAccounts) { batches.push(usernames.slice(i, i + maxConcurrentAccounts).map((u, idx) => ({ username: u, password: passwords[i + idx], delay: idx * delayBetweenInstances, }))); } // 按批次顺序执行 for (let b = 0; b < batches.length; b++) { console.log(`开始第 ${b + 1}/${batches.length} 批`); const browsers = await Promise.all( batches[b].map(({ username, password, delay: d }) => launchBrowserForUser(username, password, d) ) ); // 若有下批次则等待 if (b <= batches.length - 1) { console.log(`等待 ${delayBetweenBatches / 1000}s 进入下一批…`); await delay(delayBetweenBatches); } // 关闭浏览器 for (const br of browsers) await br?.close(); } console.log("所有账号处理完成 ✨"); } catch (err) { console.error(err); sendToTelegram?.(`脚本异常: ${err.message}`); } })(); // ----------------------------------------------------------------------- // 核心:为单个账号启动 Playwright、完成登录与业务逻辑 // ----------------------------------------------------------------------- async function launchBrowserForUser(username, password, instanceDelay) { await delay(instanceDelay); // 分散启动时序 const launchOpts = { headless: false, // 改为 false,显示浏览器窗口 args: ["--no-sandbox", "--disable-setuid-sandbox"], }; // 添加代理配置 const proxyConfig = getProxyConfig(); const playwrightProxy = getPlaywrightProxyConfig(proxyConfig); const browser = await chromium.launch(launchOpts); const contextOptions = { locale: "en-US", userAgent: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36`, viewport: { width: 1280 + Math.floor(Math.random() * 100), height: 720 + Math.floor(Math.random() * 100), }, }; // 如果有代理配置,添加到context选项 if (playwrightProxy) { contextOptions.proxy = playwrightProxy; console.log(`为用户 ${username} 启用代理: ${playwrightProxy.server}`); } const context = await browser.newContext(contextOptions); await context.setExtraHTTPHeaders({ "accept-language": "en-US,en;q=0.9" }); const page = await context.newPage(); page.on("pageerror", (err) => console.error("Page error:", err.message)); page.on("console", (msg) => console.log("[console]", msg.text())); // Cloudflare challenge 处理 await navigatePage(loginUrl, page); // 登录 await login(page, username, password); await page.waitForTimeout(5000); // 登录后等待2秒 // 跳到目标帖子 const target = loginUrl === "https://linux.do" ? "https://linux.do/t/topic/13716/700" : loginUrl === "https://meta.appinn.net" ? "https://meta.appinn.net/t/topic/52006" : `${loginUrl}/t/topic/1`; await page.goto(target, { waitUntil: "domcontentloaded" }); await page.waitForTimeout(2000); // 跳转后等待2秒 // 登录成功后注入业务脚本 const scriptPath = resolveBusinessScript(); const externalScript = fs.readFileSync(scriptPath, "utf8"); await page.addInitScript( ([u, script, autoLike]) => { localStorage.setItem("read", true); localStorage.setItem("specificUser", u); localStorage.setItem("isFirstRun", "false"); localStorage.setItem("autoLikeEnabled", autoLike); console.log("当前点赞用户:", u); eval(script); }, [specificUser, externalScript, isAutoLike] ); sendToTelegram?.(`${username} 登录成功`); return browser; } function resolveBusinessScript() { const base = dirname(fileURLToPath(import.meta.url)); if (isLikeSpecificUser) { return join(base, Math.random() < 0.5 ? "index_likeUser_activity.js" : "index_likeUser.js"); } return join(base, "index.js"); } // ----------------------------------------------------------------------- // Cloudflare challenge 简易检测:页面标题含 Just a moment // ----------------------------------------------------------------------- async function navigatePage(url, page) { await page.goto(url, { waitUntil: "domcontentloaded" }); const start = Date.now(); while ((await page.title()).includes("Just a moment")) { console.log("Cloudflare challenge… 等待中"); await delay(2000); if (Date.now() - start > 35_000) throw new Error("Cloudflare 验证超时"); } console.log("已通过 Cloudflare, 页面标题:", await page.title()); } // ----------------------------------------------------------------------- // 登录逻辑:根据页面实际结构适当调整选择器 // ----------------------------------------------------------------------- async function login(page, username, password, retry = 3) { // 点击登录按钮(中文/英文)或者类 .login-button const loginBtn = page.locator("button:text-is('登录'), button:text-is('login'), .login-button"); if (await loginBtn.count()) await loginBtn.first().click(); await page.locator("#login-account-name").waitFor(); await page.fill("#login-account-name", username); await delay(1000); // 填写用户名后等待1秒 await page.fill("#login-account-password", password); await delay(1000); // 填写密码后等待1秒 await delay(800); await Promise.all([ page.waitForNavigation({ waitUntil: "domcontentloaded" }), page.click("#login-button"), ]).catch(async (err) => { if (retry > 0) { console.log("登录失败,重试…", err.message); await page.reload({ waitUntil: "domcontentloaded" }); await delay(2000); return login(page, username, password, retry - 1); } throw new Error(`登录失败(${username}): ${err.message}`); }); // 检查头像 if (!(await page.locator("img.avatar").count())) throw new Error("登录后未找到头像, 疑似失败"); } // ----------------------------------------------------------------------- // Express 健康监测 // ----------------------------------------------------------------------- const HEALTH_PORT = process.env.HEALTH_PORT || 7860; const app = express(); app.get("/health", (_req, res) => { const m = process.memoryUsage(); const fmt = (b) => `${(b / 1024 / 1024).toFixed(2)} MB`; res.json({ status: "OK", timestamp: new Date().toISOString(), uptime: process.uptime().toFixed(2), memory: Object.fromEntries(Object.entries(m).map(([k, v]) => [k, fmt(v)])), }); }); app.get("/", (_req, res) => res.send(`

Auto Read (Playwright)

Health: /health

`)); app.listen(HEALTH_PORT, () => console.log(`Health endpoint: http://localhost:${HEALTH_PORT}/health`)); ================================================ FILE: cron.log ================================================ ================================================ FILE: cron.sh ================================================ #!/bin/bash #设置为中文 export LANG=zh_CN.UTF-8 export LC_ALL=zh_CN.UTF-8 # 获取当前工作目录 WORKDIR=$(dirname $(readlink -f $0)) # 进入工作目录 cd $WORKDIR # 停止 Docker Compose /usr/local/bin/docker-compose down --remove-orphans --volumes # 重新启动 Docker Compose /usr/local/bin/docker-compose up -d >> ./cron.log 2>&1 # 等待20分钟 sleep 20m /usr/local/bin/docker-compose logs >> ./cron.log 2>&1 # 停止 Docker Compose /usr/local/bin/docker-compose down --remove-orphans --volumes >> ./cron.log 2>&1 ================================================ FILE: debug_db.js ================================================ import { Pool } from 'pg'; import fs from 'fs'; import dotenv from 'dotenv'; dotenv.config(); if (fs.existsSync(".env.local")) { console.log("Using .env.local file to supply config environment variables"); const envConfig = dotenv.parse(fs.readFileSync(".env.local")); for (const k in envConfig) { process.env[k] = envConfig[k]; } } const pool = new Pool({ connectionString: process.env.POSTGRES_URI, max: 5, idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, ssl: { rejectUnauthorized: false } }); async function debugDatabase() { try { // 检查最近的一些posts const res = await pool.query('SELECT guid, title, created_at FROM posts ORDER BY created_at DESC LIMIT 10'); console.log('Recent posts in database:'); res.rows.forEach(row => { console.log(`GUID: ${row.guid}, Title: ${row.title.substring(0, 50)}..., Created: ${row.created_at}`); }); // 检查特定GUID是否存在 const testGuids = [ 'https://linux.do/t/topic/298776', 'https://linux.do/t/topic/298777', 'https://linux.do/t/topic/298778' ]; console.log('\nChecking specific GUIDs:'); for (const guid of testGuids) { const check = await pool.query('SELECT COUNT(*) FROM posts WHERE guid = $1', [guid]); console.log(`GUID ${guid}: exists = ${check.rows[0].count > 0}`); } // 查看总记录数 const countRes = await pool.query('SELECT COUNT(*) FROM posts'); console.log(`\nTotal posts in database: ${countRes.rows[0].count}`); } catch (error) { console.error('Database error:', error); } finally { await pool.end(); } } debugDatabase(); ================================================ FILE: debug_rss.js ================================================ import { parseRss } from './src/parse_rss.js'; import { isGuidExists } from './src/db.js'; import fs from 'fs'; import dotenv from 'dotenv'; dotenv.config(); if (fs.existsSync(".env.local")) { const envConfig = dotenv.parse(fs.readFileSync(".env.local")); for (const k in envConfig) { process.env[k] = envConfig[k]; } } async function debugRssAndGuids() { try { // 模拟获取RSS数据 const response = await fetch('https://linux.do/latest.rss'); const xmlData = await response.text(); // 分析RSS原始结构 const xml2js = await import('xml2js'); const parser = new xml2js.Parser({ explicitArray: false, trim: true }); const result = await parser.parseStringPromise(xmlData); const items = result.rss.channel.item; console.log('原始RSS数据分析:'); console.log('Items数量:', Array.isArray(items) ? items.length : 1); if (Array.isArray(items)) { console.log('\n前3个items的GUID信息:'); for (let i = 0; i < Math.min(3, items.length); i++) { const item = items[i]; console.log(`Item ${i + 1}:`); console.log(' title:', item.title); console.log(' link:', item.link); console.log(' guid对象:', JSON.stringify(item.guid, null, 2)); console.log(' guid._:', item.guid?._); console.log(' guid.isPermaLink:', item.guid?.$?.isPermaLink); // 检查这个GUID是否在数据库中存在 const guid = item.guid?._|| item.guid; console.log(' 提取的guid:', guid); if (guid) { const exists = await isGuidExists(guid); console.log(' 数据库中存在:', exists); } console.log(''); } } } catch (error) { console.error('Error:', error); } } debugRssAndGuids(); ================================================ FILE: docker-compose-like-user.yml ================================================ services: autolikeuser: image: 14790897/auto-like-user:latest container_name: auto-like-user env_file: - ./.env - ./.env.local restart: unless-stopped # 容器退出时重启策略 ================================================ FILE: docker-compose.yml ================================================ services: autoread: image: 14790897/auto-read:latest container_name: auto-read env_file: - path: ./.env required: true # 这个文件必须存在,否则报错 - path: ./.env.local required: false # 这个文件是可选的:存在就覆盖前者,不存在就忽略 restart: unless-stopped ports: - "7860:7860" ================================================ FILE: external.js ================================================ // ==UserScript== // @name Auto Read // @namespace http://tampermonkey.net/ // @version 1.3.1 // @description 自动刷linuxdo文章 // @author liuweiqing // @match https://meta.discourse.org/* // @match https://linux.do/* // @match https://meta.appinn.net/* // @match https://community.openai.com/ // @grant none // @license MIT // @icon https://www.google.com/s2/favicons?domain=linux.do // ==/UserScript== (function () { ("use strict"); // 定义可能的基本URL const possibleBaseURLs = [ "https://meta.discourse.org", "https://linux.do", "https://meta.appinn.net", "https://community.openai.com", ]; // 获取当前页面的URL const currentURL = window.location.href; // 确定当前页面对应的BASE_URL let BASE_URL = possibleBaseURLs.find((url) => currentURL.startsWith(url)); // 环境变量:阅读网址,如果没有找到匹配的URL,则默认为第一个 if (!BASE_URL) { BASE_URL = possibleBaseURLs[0]; console.log("默认BASE_URL设置为: " + BASE_URL); } else { console.log("当前BASE_URL是: " + BASE_URL); } // 以下是脚本的其余部分 console.log("脚本正在运行在: " + BASE_URL); //1.进入网页 https://linux.do/t/topic/数字(1,2,3,4) //2.使滚轮均衡的往下移动模拟刷文章 // 检查是否是第一次运行脚本 function checkFirstRun() { if (localStorage.getItem("isFirstRun") === null) { // 是第一次运行,执行初始化操作 console.log("脚本第一次运行,执行初始化操作..."); updateInitialData(); // 设置 isFirstRun 标记为 false localStorage.setItem("isFirstRun", "false"); } else { // 非第一次运行 console.log("脚本非第一次运行"); } } // 更新初始数据的函数 function updateInitialData() { localStorage.setItem("read", "true"); // 开始时自动滚动关闭 localStorage.setItem("autoLikeEnabled", "true"); //默认关闭自动点赞 console.log("执行了初始数据更新操作"); } const delay = 2000; // 滚动检查的间隔(毫秒) let scrollInterval = null; let checkScrollTimeout = null; let autoLikeInterval = null; function scrollToBottomSlowly( stopDistance = 9999999999, callback = undefined, distancePerStep = 20, delayPerStep = 50 ) { if (scrollInterval !== null) { clearInterval(scrollInterval); } scrollInterval = setInterval(() => { if ( window.innerHeight + window.scrollY >= document.body.offsetHeight - 100 || window.innerHeight + window.scrollY >= stopDistance ) { clearInterval(scrollInterval); scrollInterval = null; if (typeof callback === "function") { callback(); // 当滚动结束时调用回调函数 } } else { window.scrollBy(0, distancePerStep); } }, delayPerStep); } // 功能:跳转到下一个话题 function navigateToNextTopic() { // 定义包含文章列表的数组 const urls = [ `${BASE_URL}/latest`, `${BASE_URL}/top`, `${BASE_URL}/latest?ascending=false&order=posts`, // `${BASE_URL}/unread`, // 示例:如果你想将这个URL启用,只需去掉前面的注释 ]; // 生成一个随机索引 const randomIndex = Math.floor(Math.random() * urls.length); // 根据随机索引选择一个URL const nextTopicURL = urls[randomIndex]; // 在跳转之前,标记即将跳转到下一个话题 localStorage.setItem("navigatingToNextTopic", "true"); // 尝试导航到下一个话题 window.location.href = nextTopicURL; } // 检查是否已滚动到底部(不断重复执行) function checkScroll() { if (localStorage.getItem("read")) { if ( window.innerHeight + window.scrollY >= document.body.offsetHeight - 100 ) { console.log("已滚动到底部"); navigateToNextTopic(); } else { scrollToBottomSlowly(); if (checkScrollTimeout !== null) { clearTimeout(checkScrollTimeout); } checkScrollTimeout = setTimeout(checkScroll, delay); } } } // 入口函数 window.addEventListener("load", () => { checkFirstRun(); console.log( "autoRead", localStorage.getItem("read"), "autoLikeEnabled", localStorage.getItem("autoLikeEnabled") ); if (localStorage.getItem("read") === "true") { // 检查是否正在导航到下一个话题 if (localStorage.getItem("navigatingToNextTopic") === "true") { console.log("正在导航到下一个话题"); // 等待一段时间或直到页面完全加载 // 页面加载完成后,移除标记 localStorage.removeItem("navigatingToNextTopic"); // 使用setTimeout延迟执行 setTimeout(() => { // 先随机滚动一段距离然后再查找链接 scrollToBottomSlowly( Math.random() * document.body.offsetHeight * 3, searchLinkClick, 20, 20 ); }, 2000); // 延迟2000毫秒(即2秒) } else { console.log("执行正常的滚动和检查逻辑"); // 执行正常的滚动和检查逻辑 checkScroll(); if (isAutoLikeEnabled()) { //自动点赞 autoLike(); } } } }); // 创建一个控制滚动的按钮 function searchLinkClick() { // 在新页面加载后执行检查 // 使用CSS属性选择器寻找href属性符合特定格式的标签 const links = document.querySelectorAll('a[href^="/t/"]'); // const alreadyReadLinks = JSON.parse( // localStorage.getItem("alreadyReadLinks") || "[]" // ); // 获取已阅读链接列表 // 筛选出未阅读的链接 const unreadLinks = Array.from(links).filter((link) => { // 检查链接是否已经被读过 // const isAlreadyRead = alreadyReadLinks.includes(link.href); // if (isAlreadyRead) { // return false; // 如果链接已被读过,直接排除 // } // 向上遍历DOM树,查找包含'visited'类的父级元素,最多查找三次 let parent = link.parentElement; let times = 0; // 查找次数计数器 while (parent && times < 3) { if (parent.classList.contains("visited")) { // 如果找到包含'visited'类的父级元素,中断循环 return false; // 父级元素包含'visited'类,排除这个链接 } parent = parent.parentElement; // 继续向上查找 times++; // 增加查找次数 } // 如果链接未被读过,且在向上查找三次内,其父级元素中没有包含'visited'类,则保留这个链接 return true; }); // 如果找到了这样的链接 if (unreadLinks.length > 0) { // 从所有匹配的链接中随机选择一个 const randomIndex = Math.floor(Math.random() * unreadLinks.length); const link = unreadLinks[randomIndex]; // 打印找到的链接(可选) console.log("Found link:", link.href); // // 模拟点击该链接 // setTimeout(() => { // link.click(); // }, delay); // 将链接添加到已阅读列表并更新localStorage // alreadyReadLinks.push(link.href); // localStorage.setItem( // "alreadyReadLinks", // JSON.stringify(alreadyReadLinks) // ); // 导航到该链接 window.location.href = link.href; } else { // 如果没有找到符合条件的链接,打印消息(可选) console.log("No link with the specified format was found."); scrollToBottomSlowly( Math.random() * document.body.offsetHeight * 3, searchLinkClick ); } } // 获取当前时间戳 const currentTime = Date.now(); // 获取存储的时间戳 const defaultTimestamp = new Date("1999-01-01T00:00:00Z").getTime(); //默认值为1999年 const storedTime = parseInt( localStorage.getItem("clickCounterTimestamp") || defaultTimestamp.toString(), 10 ); // 获取当前的点击计数,如果不存在则初始化为0 let clickCounter = parseInt(localStorage.getItem("clickCounter") || "0", 10); // 检查是否超过24小时(24小时 = 24 * 60 * 60 * 1000 毫秒) if (currentTime - storedTime > 24 * 60 * 60 * 1000) { // 超过24小时,清空点击计数器并更新时间戳 clickCounter = 0; localStorage.setItem("clickCounter", "0"); localStorage.setItem("clickCounterTimestamp", currentTime.toString()); } console.log(`Initial clickCounter: ${clickCounter}`); function triggerClick(button) { const event = new MouseEvent("click", { bubbles: true, cancelable: true, view: window, }); button.dispatchEvent(event); } function autoLike() { console.log(`Initial clickCounter: ${clickCounter}`); // 寻找所有的discourse-reactions-reaction-button const buttons = document.querySelectorAll( ".discourse-reactions-reaction-button" ); if (buttons.length === 0) { console.error( "No buttons found with the selector '.discourse-reactions-reaction-button'" ); return; } console.log(`Found ${buttons.length} buttons.`); // 调试信息 // 逐个点击找到的按钮 buttons.forEach((button, index) => { if ( (button.title !== "点赞此帖子" && button.title !== "Like this post") || clickCounter >= 50 ) { return; } // 使用setTimeout来错开每次点击的时间,避免同时触发点击 autoLikeInterval = setTimeout(() => { // 模拟点击 triggerClick(button); // 使用自定义的触发点击方法 console.log(`Clicked like button ${index + 1}`); clickCounter++; // 更新点击计数器 // 将新的点击计数存储到localStorage localStorage.setItem("clickCounter", clickCounter.toString()); // 如果点击次数达到50次,则设置点赞变量为false if (clickCounter === 50) { console.log("Reached 50 likes, setting the like variable to false."); localStorage.setItem("autoLikeEnabled", "false"); // 使用localStorage存储点赞变量状态 } else { console.log("clickCounter:", clickCounter); } }, index * 3000); // 这里的3000毫秒是两次点击之间的间隔,可以根据需要调整 }); } const button = document.createElement("button"); // 初始化按钮文本基于当前的阅读状态 button.textContent = localStorage.getItem("read") === "true" ? "停止阅读" : "开始阅读"; button.style.position = "fixed"; button.style.bottom = "10px"; // 之前是 top button.style.left = "10px"; // 之前是 right button.style.zIndex = 1000; button.style.backgroundColor = "#f0f0f0"; // 浅灰色背景 button.style.color = "#000"; // 黑色文本 button.style.border = "1px solid #ddd"; // 浅灰色边框 button.style.padding = "5px 10px"; // 内边距 button.style.borderRadius = "5px"; // 圆角 // document.body.appendChild(button); button.onclick = function () { const currentlyReading = localStorage.getItem("read") === "true"; const newReadState = !currentlyReading; localStorage.setItem("read", newReadState.toString()); button.textContent = newReadState ? "停止阅读" : "开始阅读"; if (!newReadState) { if (scrollInterval !== null) { clearInterval(scrollInterval); scrollInterval = null; } if (checkScrollTimeout !== null) { clearTimeout(checkScrollTimeout); checkScrollTimeout = null; } localStorage.removeItem("navigatingToNextTopic"); } else { // 如果是Linuxdo,就导航到我的帖子 if (BASE_URL == "https://linux.do") { window.location.href = "https://linux.do/t/topic/13716/340"; } else if (BASE_URL == "https://meta.appinn.net") { window.location.href = "https://meta.appinn.net/t/topic/52006"; } else { window.location.href = `${BASE_URL}/t/topic/1`; } checkScroll(); } }; //自动点赞按钮 // 在页面上添加一个控制自动点赞的按钮 const toggleAutoLikeButton = document.createElement("button"); toggleAutoLikeButton.textContent = isAutoLikeEnabled() ? "禁用自动点赞" : "启用自动点赞"; toggleAutoLikeButton.style.position = "fixed"; toggleAutoLikeButton.style.bottom = "50px"; // 之前是 top,且与另一个按钮错开位置 toggleAutoLikeButton.style.left = "10px"; // 之前是 right toggleAutoLikeButton.style.zIndex = "1000"; toggleAutoLikeButton.style.backgroundColor = "#f0f0f0"; // 浅灰色背景 toggleAutoLikeButton.style.color = "#000"; // 黑色文本 toggleAutoLikeButton.style.border = "1px solid #ddd"; // 浅灰色边框 toggleAutoLikeButton.style.padding = "5px 10px"; // 内边距 toggleAutoLikeButton.style.borderRadius = "5px"; // 圆角 // document.body.appendChild(toggleAutoLikeButton); // 为按钮添加点击事件处理函数 toggleAutoLikeButton.addEventListener("click", () => { const isEnabled = !isAutoLikeEnabled(); setAutoLikeEnabled(isEnabled); toggleAutoLikeButton.textContent = isEnabled ? "禁用自动点赞" : "启用自动点赞"; }); // 判断是否启用自动点赞 function isAutoLikeEnabled() { // 从localStorage获取autoLikeEnabled的值,如果未设置,默认为"true" return localStorage.getItem("autoLikeEnabled") !== "false"; } // 设置自动点赞的启用状态 function setAutoLikeEnabled(enabled) { localStorage.setItem("autoLikeEnabled", enabled ? "true" : "false"); } })(); ================================================ FILE: index.js ================================================ // ==UserScript== // @name Auto Read // @namespace http://tampermonkey.net/ // @version 1.4.6 // @description 自动刷linuxdo文章 // @author liuweiqing // @match https://meta.discourse.org/* // @match https://linux.do/* // @match https://meta.appinn.net/* // @match https://community.openai.com/ // @match https://idcflare.com/* // @exclude https://linux.do/a/9611/0 // @grant none // @license MIT // @icon https://www.google.com/s2/favicons?domain=linux.do // @downloadURL https://update.greasyfork.org/scripts/489464/Auto%20Read.user.js // @updateURL https://update.greasyfork.org/scripts/489464/Auto%20Read.meta.js // ==/UserScript== (function () { ("use strict"); // 定义可能的基本URL const possibleBaseURLs = [ "https://linux.do", "https://meta.discourse.org", "https://meta.appinn.net", "https://community.openai.com", "https://idcflare.com/", ]; const commentLimit = 1000; const topicListLimit = 100; const likeLimit = 50; // 获取当前页面的URL const currentURL = window.location.href; // 确定当前页面对应的BASE_URL let BASE_URL = possibleBaseURLs.find((url) => currentURL.startsWith(url)); console.log("currentURL:", currentURL); // 环境变量:阅读网址,如果没有找到匹配的URL,则默认为第一个 if (!BASE_URL) { BASE_URL = possibleBaseURLs[0]; console.log("默认BASE_URL设置为: " + BASE_URL); } else { console.log("当前BASE_URL是: " + BASE_URL); } console.log("脚本正在运行在: " + BASE_URL); //1.进入网页 https://linux.do/t/topic/数字(1,2,3,4) //2.使滚轮均衡的往下移动模拟刷文章 // 检查是否是第一次运行脚本 function checkFirstRun() { if (localStorage.getItem("isFirstRun") === null) { console.log("脚本第一次运行,执行初始化操作..."); updateInitialData(); localStorage.setItem("isFirstRun", "false"); } else { console.log("脚本非第一次运行"); } } // 更新初始数据的函数 function updateInitialData() { localStorage.setItem("read", "false"); // 开始时自动滚动关闭 localStorage.setItem("autoLikeEnabled", "false"); //默认关闭自动点赞 console.log("执行了初始数据更新操作"); } const delay = 2000; // 滚动检查的间隔(毫秒) let scrollInterval = null; let checkScrollTimeout = null; let autoLikeInterval = null; function scrollToBottomSlowly(distancePerStep = 20, delayPerStep = 50) { if (scrollInterval !== null) { clearInterval(scrollInterval); } scrollInterval = setInterval(() => { window.scrollBy(0, distancePerStep); }, delayPerStep); // 每50毫秒滚动20像素 } function getLatestTopic() { let latestPage = Number(localStorage.getItem("latestPage")) || 0; let topicList = []; let isDataSufficient = false; while (!isDataSufficient) { latestPage++; const url = `${BASE_URL}/latest.json?no_definitions=true&page=${latestPage}`; $.ajax({ url: url, async: false, success: function (result) { if ( result && result.topic_list && result.topic_list.topics.length > 0 ) { result.topic_list.topics.forEach((topic) => { // 未读且评论数小于 commentLimit if (commentLimit > topic.posts_count) { //其实不需要 !topic.unseen && topicList.push(topic); } }); // 检查是否已获得足够的 topics if (topicList.length >= topicListLimit) { isDataSufficient = true; } } else { isDataSufficient = true; // 没有更多内容时停止请求 } }, error: function (XMLHttpRequest, textStatus, errorThrown) { console.error(XMLHttpRequest, textStatus, errorThrown); isDataSufficient = true; // 遇到错误时也停止请求 }, }); } if (topicList.length > topicListLimit) { topicList = topicList.slice(0, topicListLimit); } // 其实不需要对latestPage操作 // localStorage.setItem("latestPage", latestPage); localStorage.setItem("topicList", JSON.stringify(topicList)); } function openNewTopic() { let topicListStr = localStorage.getItem("topicList"); let topicList = topicListStr ? JSON.parse(topicListStr) : []; // 如果列表为空,则获取最新文章 if (topicList.length === 0) { getLatestTopic(); topicListStr = localStorage.getItem("topicList"); topicList = topicListStr ? JSON.parse(topicListStr) : []; } // 如果获取到新文章,打开第一个 if (topicList.length > 0) { const topic = topicList.shift(); localStorage.setItem("topicList", JSON.stringify(topicList)); if (topic.last_read_post_number) { window.location.href = `${BASE_URL}/t/topic/${topic.id}/${topic.last_read_post_number}`; } else { window.location.href = `${BASE_URL}/t/topic/${topic.id}`; } } } // 检查是否已滚动到底部(不断重复执行),到底部时跳转到下一个话题 function checkScroll() { if (localStorage.getItem("read")) { if ( window.innerHeight + window.scrollY >= document.body.offsetHeight - 100 ) { console.log("已滚动到底部"); openNewTopic(); } else { scrollToBottomSlowly(); if (checkScrollTimeout !== null) { clearTimeout(checkScrollTimeout); } checkScrollTimeout = setTimeout(checkScroll, delay); } } } // 入口函数 window.addEventListener("load", () => { checkFirstRun(); console.log( "autoRead", localStorage.getItem("read"), "autoLikeEnabled", localStorage.getItem("autoLikeEnabled") ); if (localStorage.getItem("read") === "true") { console.log("执行正常的滚动和检查逻辑"); checkScroll(); if (isAutoLikeEnabled()) { autoLike(); } } }); // 获取当前时间戳 const currentTime = Date.now(); // 获取存储的时间戳 const defaultTimestamp = new Date("1999-01-01T00:00:00Z").getTime(); //默认值为1999年 const storedTime = parseInt( localStorage.getItem("clickCounterTimestamp") || defaultTimestamp.toString(), 10 ); // 获取当前的点击计数,如果不存在则初始化为0 let clickCounter = parseInt(localStorage.getItem("clickCounter") || "0", 10); // 检查是否超过24小时(24小时 = 24 * 60 * 60 * 1000 毫秒) if (currentTime - storedTime > 24 * 60 * 60 * 1000) { // 超过24小时,清空点击计数器并更新时间戳 clickCounter = 0; localStorage.setItem("clickCounter", "0"); localStorage.setItem("clickCounterTimestamp", currentTime.toString()); } console.log(`Initial clickCounter: ${clickCounter}`); function triggerClick(button) { const event = new MouseEvent("click", { bubbles: true, cancelable: true, view: window, }); button.dispatchEvent(event); } function autoLike() { console.log(`Initial clickCounter: ${clickCounter}`); // 寻找所有的discourse-reactions-reaction-button const buttons = document.querySelectorAll( ".discourse-reactions-reaction-button" ); if (buttons.length === 0) { console.error( "No buttons found with the selector '.discourse-reactions-reaction-button'" ); return; } console.log(`Found ${buttons.length} buttons.`); // 调试信息 // 逐个点击找到的按钮 buttons.forEach((button, index) => { if ( (button.title !== "点赞此帖子" && button.title !== "Like this post") || clickCounter >= likeLimit ) { return; } // 新增:点赞前加一个随机概率判断(如30%概率) const likeProbability = 0.3; // 0~1之间,0.3表示30%概率 if (Math.random() > likeProbability) { console.log(`跳过第${index + 1}个按钮(未通过概率判断)`); return; } // 点赞间隔时间也随机(2~5秒之间) const randomDelay = 2000 + Math.floor(Math.random() * 3000); autoLikeInterval = setTimeout(() => { // 模拟点击 triggerClick(button); // 使用自定义的触发点击方法 console.log(`Clicked like button ${index + 1}`); clickCounter++; // 更新点击计数器 // 将新的点击计数存储到localStorage localStorage.setItem("clickCounter", clickCounter.toString()); // 如果点击次数达到likeLimit次,则设置点赞变量为false if (clickCounter === likeLimit) { console.log( `Reached ${likeLimit} likes, setting the like variable to false.` ); localStorage.setItem("autoLikeEnabled", "false"); // 使用localStorage存储点赞变量状态 } else { console.log("clickCounter:", clickCounter); } }, index * randomDelay); // 每次点赞的延迟为随机值 }); } const button = document.createElement("button"); // 初始化按钮文本基于当前的阅读状态 button.textContent = localStorage.getItem("read") === "true" ? "停止阅读" : "开始阅读"; button.style.position = "fixed"; button.style.bottom = "10px"; // 之前是 top button.style.left = "10px"; // 之前是 right button.style.zIndex = 1000; button.style.backgroundColor = "#f0f0f0"; // 浅灰色背景 button.style.color = "#000"; // 黑色文本 button.style.border = "1px solid #ddd"; // 浅灰色边框 button.style.padding = "5px 10px"; // 内边距 button.style.borderRadius = "5px"; // 圆角 document.body.appendChild(button); button.onclick = function () { const currentlyReading = localStorage.getItem("read") === "true"; const newReadState = !currentlyReading; localStorage.setItem("read", newReadState.toString()); button.textContent = newReadState ? "停止阅读" : "开始阅读"; if (!newReadState) { if (scrollInterval !== null) { clearInterval(scrollInterval); scrollInterval = null; } if (checkScrollTimeout !== null) { clearTimeout(checkScrollTimeout); checkScrollTimeout = null; } localStorage.removeItem("navigatingToNextTopic"); } else { // 如果是Linuxdo,就导航到我的帖子 if (BASE_URL == "https://linux.do") { window.location.href = "https://linux.do/t/topic/13716/900"; } else { window.location.href = `${BASE_URL}/t/topic/1`; } checkScroll(); } }; //自动点赞按钮 // 在页面上添加一个控制自动点赞的按钮 const toggleAutoLikeButton = document.createElement("button"); toggleAutoLikeButton.textContent = isAutoLikeEnabled() ? "禁用自动点赞" : "启用自动点赞"; toggleAutoLikeButton.style.position = "fixed"; toggleAutoLikeButton.style.bottom = "50px"; // 之前是 top,且与另一个按钮错开位置 toggleAutoLikeButton.style.left = "10px"; // 之前是 right toggleAutoLikeButton.style.zIndex = "1000"; toggleAutoLikeButton.style.backgroundColor = "#f0f0f0"; // 浅灰色背景 toggleAutoLikeButton.style.color = "#000"; // 黑色文本 toggleAutoLikeButton.style.border = "1px solid #ddd"; // 浅灰色边框 toggleAutoLikeButton.style.padding = "5px 10px"; // 内边距 toggleAutoLikeButton.style.borderRadius = "5px"; // 圆角 document.body.appendChild(toggleAutoLikeButton); // 为按钮添加点击事件处理函数 toggleAutoLikeButton.addEventListener("click", () => { const isEnabled = !isAutoLikeEnabled(); setAutoLikeEnabled(isEnabled); toggleAutoLikeButton.textContent = isEnabled ? "禁用自动点赞" : "启用自动点赞"; }); // 判断是否启用自动点赞 function isAutoLikeEnabled() { // 从localStorage获取autoLikeEnabled的值,如果未设置,默认为"true" return localStorage.getItem("autoLikeEnabled") !== "false"; } // 设置自动点赞的启用状态 function setAutoLikeEnabled(enabled) { localStorage.setItem("autoLikeEnabled", enabled ? "true" : "false"); } })(); ================================================ FILE: index_likeUser.js ================================================ // ==UserScript== // @name Auto Like Specific User // @namespace http://tampermonkey.net/ // @version 1.1.2 // @description 自动点赞特定用户,适用于discourse // @author liuweiqing // @match https://meta.discourse.org/* // @match https://linux.do/* // @match https://meta.appinn.net/* // @match https://community.openai.com/ // @grant none // @license MIT // @icon https://www.google.com/s2/favicons?domain=linux.do // @downloadURL https://update.greasyfork.org/scripts/489464/Auto%20Read.user.js // @updateURL https://update.greasyfork.org/scripts/489464/Auto%20Read.meta.js // @run-at document-end // ==/UserScript== (function () { ("use strict"); // 定义可能的基本URL const possibleBaseURLs = [ "https://meta.discourse.org", "https://linux.do", "https://meta.appinn.net", "https://community.openai.com", ]; const commentLimit = 1000; const specificUserPostListLimit = 100; const currentURL = window.location.href; let specificUser = localStorage.getItem("specificUser") || "14790897"; let likeLimit = parseInt(localStorage.getItem("likeLimit") || 200, 10); let BASE_URL = possibleBaseURLs.find((url) => currentURL.startsWith(url)); // 环境变量:阅读网址,如果没有找到匹配的URL,则默认为第一个 if (!BASE_URL) { BASE_URL = possibleBaseURLs[0]; console.log("当前BASE_URL设置为(默认): " + BASE_URL); } else { console.log("当前BASE_URL是: " + BASE_URL); } // 获取当前时间戳 const currentTime = Date.now(); // 获取存储的时间戳 const defaultTimestamp = new Date("1999-01-01T00:00:00Z").getTime(); //默认值为1999年 const storedTime = parseInt( localStorage.getItem("clickCounterTimestamp") || defaultTimestamp.toString(), 10 ); // 获取当前的点击计数,如果不存在则初始化为0 let clickCounter = parseInt(localStorage.getItem("clickCounter") || "0", 10); // 检查是否超过12小时(12小时 = 12 * 60 * 60 * 1000 毫秒) if (currentTime - storedTime > 12 * 60 * 60 * 1000) { // 超过24小时,清空点击计数器并更新时间戳 clickCounter = 0; localStorage.setItem("clickCounter", "0"); localStorage.setItem("clickCounterTimestamp", currentTime.toString()); } console.log(`Initial clickCounter: ${clickCounter}`); // 入口函数 window.addEventListener("load", () => { console.log("autoRead", localStorage.getItem("read")); checkFirstRun(); if (localStorage.getItem("read") === "true") { console.log("点赞开始"); setTimeout(() => { likeSpecificPost(); }, 2000); setTimeout(() => { openSpecificUserPost(); }, 4000); } }); function checkFirstRun() { if (localStorage.getItem("isFirstRun") === null) { console.log("脚本第一次运行,执行初始化操作..."); updateInitialData(); localStorage.setItem("isFirstRun", "false"); } else { console.log("脚本非第一次运行"); } } function updateInitialData() { localStorage.setItem("read", "false"); // 开始时自动滚动关闭 console.log("执行了初始数据更新操作"); } function getLatestTopic() { let lastOffset = Number(localStorage.getItem("lastOffset")) || 0; let specificUserPostList = []; let isDataSufficient = false; while (!isDataSufficient) { // lastOffset += 20; lastOffset += 1; //对于page来说 // 举例:https://linux.do/user_actions.json?offset=0&username=14790897&filter=5 // const url = `${BASE_URL}/user_actions.json?offset=${lastOffset}&username=${specificUser}&filter=5`; //举例:https://linux.do/search?q=%4014790897%20in%3Aunseen const url = `${BASE_URL}/search?q=%40${specificUser}%20in%3Aunseen`; //&page=${lastOffset} $.ajax({ url: url, async: false, headers: { Accept: "application/json", }, success: function (result) { // if (result && result.user_actions && result.user_actions.length > 0) { // result.user_actions.forEach((action) => { if (result && result.posts && result.posts.length > 0) { result.posts.forEach((action) => { const topicId = action.topic_id; // const postId = action.post_id; const postNumber = action.post_number; specificUserPostList.push({ topic_id: topicId, // post_id: postId, post_number: postNumber, }); }); // 检查是否已获得足够的 Posts if (specificUserPostList.length >= specificUserPostListLimit) { isDataSufficient = true; } } else { isDataSufficient = true; // 没有更多内容时停止请求 } }, error: function (XMLHttpRequest, textStatus, errorThrown) { console.error(XMLHttpRequest, textStatus, errorThrown); isDataSufficient = true; // 遇到错误时也停止请求 }, }); } // 如果列表超出限制,则截断 if (specificUserPostList.length > specificUserPostListLimit) { specificUserPostList = specificUserPostList.slice( 0, specificUserPostListLimit ); } // 存储 lastOffset 和 specificUserPostList 到 localStorage localStorage.setItem("lastOffset", lastOffset); localStorage.setItem( "specificUserPostList", JSON.stringify(specificUserPostList) ); } function openSpecificUserPost() { let specificUserPostListStr = localStorage.getItem("specificUserPostList"); let specificUserPostList = specificUserPostListStr ? JSON.parse(specificUserPostListStr) : []; // 如果列表为空,则获取最新文章 if (specificUserPostList.length === 0) { getLatestTopic(); specificUserPostListStr = localStorage.getItem("specificUserPostList"); specificUserPostList = specificUserPostListStr ? JSON.parse(specificUserPostListStr) : []; } // 如果获取到新文章,打开第一个 if (specificUserPostList.length > 0) { const post = specificUserPostList.shift(); // 获取列表中的第一个对象 localStorage.setItem( "specificUserPostList", JSON.stringify(specificUserPostList) ); window.location.href = `${BASE_URL}/t/topic/${post.topic_id}/${post.post_number}`; } else { console.error("未能获取到新的帖子数据。"); } } // 检查是否点赞 // const postId = data.post_id; // const targetId = `discourse-reactions-counter-${postId}-right`; // const element = document.getElementById(targetId); function likeSpecificPost() { const urlParts = window.location.pathname.split("/"); const lastPart = urlParts[urlParts.length - 1]; // 获取最后一部分 let buttons, reactionButton; console.log("post number:", lastPart); if (lastPart < 10000) { buttons = document.querySelectorAll( "button[aria-label]" //[class*='reply'] ); let targetButton = null; buttons.forEach((button) => { const ariaLabel = button.getAttribute("aria-label"); if (ariaLabel && ariaLabel.includes(`#${lastPart}`)) { targetButton = button; console.log("找到post number按钮:", targetButton); return; } }); if (targetButton) { // 找到按钮后,获取其父级元素 const parentElement = targetButton.parentElement; console.log("父级元素:", parentElement); reactionButton = parentElement.querySelector( ".discourse-reactions-reaction-button" ); } else { console.log(`未找到包含 #${lastPart} 的按钮`); } } else { //大于10000说明是主题帖,选择第一个 reactionButton = document.querySelectorAll( ".discourse-reactions-reaction-button" )[0]; } if (!reactionButton) { console.log("未找到点赞按钮"); return; } if ( reactionButton.title !== "点赞此帖子" && reactionButton.title !== "Like this post" ) { console.log("已经点赞过"); return "already liked"; } else if (clickCounter >= likeLimit) { console.log("已经达到点赞上限"); localStorage.setItem("read", false); return; } triggerClick(reactionButton); clickCounter++; console.log( `Clicked like button ${clickCounter},已点赞用户${specificUser}` ); localStorage.setItem("clickCounter", clickCounter.toString()); // 如果点击次数达到likeLimit次,则设置点赞变量为false if (clickCounter === likeLimit) { console.log( `Reached ${likeLimit} likes, setting the like variable to false.` ); localStorage.setItem("read", false); } else { console.log("clickCounter:", clickCounter); } } function triggerClick(button) { const event = new MouseEvent("click", { bubbles: true, cancelable: true, view: window, }); button.dispatchEvent(event); } const button = document.createElement("button"); button.textContent = localStorage.getItem("read") === "true" ? "停止阅读" : "开始阅读"; button.style.position = "fixed"; button.style.bottom = "20px"; button.style.left = "20px"; button.style.zIndex = 1000; button.style.backgroundColor = "#e0e0e0"; button.style.color = "#333"; button.style.border = "1px solid #aaa"; button.style.padding = "8px 16px"; button.style.borderRadius = "8px"; document.body.appendChild(button); button.onclick = function () { const currentlyReading = localStorage.getItem("read") === "true"; const newReadState = !currentlyReading; localStorage.setItem("read", newReadState.toString()); button.textContent = newReadState ? "停止阅读" : "开始阅读"; if (newReadState) { if (BASE_URL == "https://linux.do") { const maxPostNumber = 600; const randomPostNumber = Math.floor(Math.random() * maxPostNumber) + 1; const newUrl = `https://linux.do/t/topic/13716/${randomPostNumber}`; window.location.href = newUrl; } else { window.location.href = `${BASE_URL}/t/topic/1`; } } }; const userInput = document.createElement("input"); userInput.type = "text"; userInput.placeholder = "输入要点赞的用户ID"; userInput.style.position = "fixed"; userInput.style.bottom = "90px"; userInput.style.left = "20px"; userInput.style.zIndex = "1000"; userInput.style.padding = "6px"; userInput.style.border = "1px solid #aaa"; userInput.style.borderRadius = "8px"; userInput.style.backgroundColor = "#e0e0e0"; userInput.style.width = "100px"; userInput.value = localStorage.getItem("specificUser") || "14790897"; document.body.appendChild(userInput); const saveUserButton = document.createElement("button"); saveUserButton.textContent = "保存用户ID"; saveUserButton.style.position = "fixed"; saveUserButton.style.bottom = "60px"; saveUserButton.style.left = "20px"; saveUserButton.style.zIndex = "1000"; saveUserButton.style.backgroundColor = "#e0e0e0"; saveUserButton.style.color = "#333"; saveUserButton.style.border = "1px solid #aaa"; saveUserButton.style.padding = "8px 16px"; saveUserButton.style.borderRadius = "8px"; document.body.appendChild(saveUserButton); saveUserButton.onclick = function () { const newSpecificUser = userInput.value.trim(); if (newSpecificUser) { localStorage.setItem("specificUser", newSpecificUser); localStorage.removeItem("specificUserPostList"); localStorage.removeItem("lastOffset"); specificUser = newSpecificUser; console.log( `新的specificUser已保存: ${specificUser},specificUserPostList已重置` ); } }; const likeLimitInput = document.createElement("input"); likeLimitInput.type = "number"; likeLimitInput.placeholder = "输入点赞数量"; likeLimitInput.style.position = "fixed"; likeLimitInput.style.bottom = "180px"; likeLimitInput.style.left = "20px"; likeLimitInput.style.zIndex = "1000"; likeLimitInput.style.padding = "6px"; likeLimitInput.style.border = "1px solid #aaa"; likeLimitInput.style.borderRadius = "8px"; likeLimitInput.style.backgroundColor = "#e0e0e0"; likeLimitInput.style.width = "100px"; likeLimitInput.value = localStorage.getItem("likeLimit") || 200; document.body.appendChild(likeLimitInput); const saveLikeLimitButton = document.createElement("button"); saveLikeLimitButton.textContent = "保存点赞数量"; saveLikeLimitButton.style.position = "fixed"; saveLikeLimitButton.style.bottom = "140px"; saveLikeLimitButton.style.left = "20px"; saveLikeLimitButton.style.zIndex = "1000"; saveLikeLimitButton.style.backgroundColor = "#e0e0e0"; saveLikeLimitButton.style.color = "#333"; saveLikeLimitButton.style.border = "1px solid #aaa"; saveLikeLimitButton.style.padding = "8px 16px"; saveLikeLimitButton.style.borderRadius = "8px"; document.body.appendChild(saveLikeLimitButton); saveLikeLimitButton.onclick = function () { const newLikeLimit = parseInt(likeLimitInput.value.trim(), 10); if (newLikeLimit && newLikeLimit > 0) { localStorage.setItem("likeLimit", newLikeLimit); likeLimit = newLikeLimit; console.log(`新的likeLimit已保存: ${likeLimit}`); } }; // 增加清除数据的按钮 const clearDataButton = document.createElement("button"); clearDataButton.textContent = "清除所有数据"; clearDataButton.style.position = "fixed"; clearDataButton.style.bottom = "20px"; clearDataButton.style.left = "140px"; clearDataButton.style.zIndex = "1000"; clearDataButton.style.backgroundColor = "#ff6666"; // 红色背景,提示删除操作 clearDataButton.style.color = "#fff"; // 白色文本 clearDataButton.style.border = "1px solid #ff3333"; // 深红色边框 clearDataButton.style.padding = "8px 16px"; clearDataButton.style.borderRadius = "8px"; document.body.appendChild(clearDataButton); clearDataButton.onclick = function () { localStorage.removeItem("lastOffset"); localStorage.removeItem("clickCounter"); localStorage.removeItem("clickCounterTimestamp"); localStorage.removeItem("specificUserPostList"); console.log("所有数据已清除,除了 specificUser 和 specificUserPostList"); }; })(); ================================================ FILE: index_likeUser_activity.js ================================================ // ==UserScript== // @name Auto Like Specific User base on activity // @namespace http://tampermonkey.net/ // @version 1.1.2 // @description 自动点赞特定用户,适用于discourse // @author liuweiqing // @match https://meta.discourse.org/* // @match https://linux.do/* // @match https://meta.appinn.net/* // @match https://community.openai.com/ // @grant none // @license MIT // @icon https://www.google.com/s2/favicons?domain=linux.do // @downloadURL https://update.greasyfork.org/scripts/489464/Auto%20Read.user.js // @updateURL https://update.greasyfork.org/scripts/489464/Auto%20Read.meta.js // @run-at document-end // ==/UserScript== (function () { ("use strict"); // 定义可能的基本URL const possibleBaseURLs = [ "https://meta.discourse.org", "https://linux.do", "https://meta.appinn.net", "https://community.openai.com", ]; const commentLimit = 1000; const specificUserPostListLimit = 100; const currentURL = window.location.href; let specificUser = localStorage.getItem("specificUser") || "14790897"; let likeLimit = parseInt(localStorage.getItem("likeLimit") || 200, 10); let BASE_URL = possibleBaseURLs.find((url) => currentURL.startsWith(url)); // 环境变量:阅读网址,如果没有找到匹配的URL,则默认为第一个 if (!BASE_URL) { BASE_URL = possibleBaseURLs[0]; console.log("当前BASE_URL设置为(默认): " + BASE_URL); } else { console.log("当前BASE_URL是: " + BASE_URL); } // 获取当前时间戳 const currentTime = Date.now(); // 获取存储的时间戳 const defaultTimestamp = new Date("1999-01-01T00:00:00Z").getTime(); //默认值为1999年 const storedTime = parseInt( localStorage.getItem("clickCounterTimestamp") || defaultTimestamp.toString(), 10 ); // 获取当前的点击计数,如果不存在则初始化为0 let clickCounter = parseInt(localStorage.getItem("clickCounter") || "0", 10); // 检查是否超过12小时(12小时 = 12 * 60 * 60 * 1000 毫秒) if (currentTime - storedTime > 12 * 60 * 60 * 1000) { // 超过24小时,清空点击计数器并更新时间戳 clickCounter = 0; localStorage.setItem("clickCounter", "0"); localStorage.setItem("clickCounterTimestamp", currentTime.toString()); } console.log(`Initial clickCounter: ${clickCounter}`); // 入口函数 window.addEventListener("load", () => { console.log("autoRead", localStorage.getItem("read")); checkFirstRun(); if (localStorage.getItem("read") === "true") { console.log("点赞开始"); setTimeout(() => { likeSpecificPost(); }, 2000); setTimeout(() => { openSpecificUserPost(); }, 4000); } }); function checkFirstRun() { if (localStorage.getItem("isFirstRun") === null) { console.log("脚本第一次运行,执行初始化操作..."); updateInitialData(); localStorage.setItem("isFirstRun", "false"); } else { console.log("脚本非第一次运行"); } } function updateInitialData() { localStorage.setItem("read", "false"); // 开始时自动滚动关闭 console.log("执行了初始数据更新操作"); } function getLatestTopic() { let lastOffset = Number(localStorage.getItem("lastOffset")) || 0; let specificUserPostList = []; let isDataSufficient = false; while (!isDataSufficient) { lastOffset += 20; // lastOffset += 1; //对于page来说 // 举例:https://linux.do/user_actions.json?offset=0&username=14790897&filter=5 const url = `${BASE_URL}/user_actions.json?offset=${lastOffset}&username=${specificUser}&filter=5`; //举例:https://linux.do/search?q=%4014790897%20in%3Aunseen // const url = `${BASE_URL}/search?q=%40${specificUser}%20in%3Aunseen`; //&page=${lastOffset} $.ajax({ url: url, async: false, headers: { Accept: "application/json", }, success: function (result) { if (result && result.user_actions && result.user_actions.length > 0) { result.user_actions.forEach((action) => { // if (result && result.posts && result.posts.length > 0) { // result.posts.forEach((action) => { const topicId = action.topic_id; // const postId = action.post_id; const postNumber = action.post_number; specificUserPostList.push({ topic_id: topicId, // post_id: postId, post_number: postNumber, }); }); // 检查是否已获得足够的 Posts if (specificUserPostList.length >= specificUserPostListLimit) { isDataSufficient = true; } } else { isDataSufficient = true; // 没有更多内容时停止请求 } }, error: function (XMLHttpRequest, textStatus, errorThrown) { console.error(XMLHttpRequest, textStatus, errorThrown); isDataSufficient = true; // 遇到错误时也停止请求 }, }); } // 如果列表超出限制,则截断 if (specificUserPostList.length > specificUserPostListLimit) { specificUserPostList = specificUserPostList.slice( 0, specificUserPostListLimit ); } // 存储 lastOffset 和 specificUserPostList 到 localStorage localStorage.setItem("lastOffset", lastOffset); localStorage.setItem( "specificUserPostList", JSON.stringify(specificUserPostList) ); } function openSpecificUserPost() { let specificUserPostListStr = localStorage.getItem("specificUserPostList"); let specificUserPostList = specificUserPostListStr ? JSON.parse(specificUserPostListStr) : []; // 如果列表为空,则获取最新文章 if (specificUserPostList.length === 0) { getLatestTopic(); specificUserPostListStr = localStorage.getItem("specificUserPostList"); specificUserPostList = specificUserPostListStr ? JSON.parse(specificUserPostListStr) : []; } // 如果获取到新文章,打开第一个 if (specificUserPostList.length > 0) { const post = specificUserPostList.shift(); // 获取列表中的第一个对象 localStorage.setItem( "specificUserPostList", JSON.stringify(specificUserPostList) ); window.location.href = `${BASE_URL}/t/topic/${post.topic_id}/${post.post_number}`; } else { console.error("未能获取到新的帖子数据。"); } } // 检查是否点赞 // const postId = data.post_id; // const targetId = `discourse-reactions-counter-${postId}-right`; // const element = document.getElementById(targetId); function likeSpecificPost() { const urlParts = window.location.pathname.split("/"); const lastPart = urlParts[urlParts.length - 1]; // 获取最后一部分 let buttons, reactionButton; console.log("post number:", lastPart); if (lastPart < 10000) { buttons = document.querySelectorAll( "button[aria-label]" //[class*='reply'] ); let targetButton = null; buttons.forEach((button) => { const ariaLabel = button.getAttribute("aria-label"); if (ariaLabel && ariaLabel.includes(`#${lastPart}`)) { targetButton = button; console.log("找到post number按钮:", targetButton); return; } }); if (targetButton) { // 找到按钮后,获取其父级元素 const parentElement = targetButton.parentElement; console.log("父级元素:", parentElement); reactionButton = parentElement.querySelector( ".discourse-reactions-reaction-button" ); } else { console.log(`未找到包含 #${lastPart} 的按钮`); } } else { //大于10000说明是主题帖,选择第一个 reactionButton = document.querySelectorAll( ".discourse-reactions-reaction-button" )[0]; } if ( reactionButton.title !== "点赞此帖子" && reactionButton.title !== "Like this post" ) { console.log("已经点赞过"); return "already liked"; } else if (clickCounter >= likeLimit) { console.log("已经达到点赞上限"); localStorage.setItem("read", false); return; } triggerClick(reactionButton); clickCounter++; console.log( `Clicked like button ${clickCounter},已点赞用户${specificUser}` ); localStorage.setItem("clickCounter", clickCounter.toString()); // 如果点击次数达到likeLimit次,则设置点赞变量为false if (clickCounter === likeLimit) { console.log( `Reached ${likeLimit} likes, setting the like variable to false.` ); localStorage.setItem("read", false); } else { console.log("clickCounter:", clickCounter); } } function triggerClick(button) { const event = new MouseEvent("click", { bubbles: true, cancelable: true, view: window, }); button.dispatchEvent(event); } const button = document.createElement("button"); button.textContent = localStorage.getItem("read") === "true" ? "停止阅读" : "开始阅读"; button.style.position = "fixed"; button.style.bottom = "20px"; button.style.left = "20px"; button.style.zIndex = 1000; button.style.backgroundColor = "#e0e0e0"; button.style.color = "#333"; button.style.border = "1px solid #aaa"; button.style.padding = "8px 16px"; button.style.borderRadius = "8px"; document.body.appendChild(button); button.onclick = function () { const currentlyReading = localStorage.getItem("read") === "true"; const newReadState = !currentlyReading; localStorage.setItem("read", newReadState.toString()); button.textContent = newReadState ? "停止阅读" : "开始阅读"; if (newReadState) { if (BASE_URL == "https://linux.do") { const maxPostNumber = 600; const randomPostNumber = Math.floor(Math.random() * maxPostNumber) + 1; const newUrl = `https://linux.do/t/topic/13716/${randomPostNumber}`; window.location.href = newUrl; } else { window.location.href = `${BASE_URL}/t/topic/1`; } } }; const userInput = document.createElement("input"); userInput.type = "text"; userInput.placeholder = "输入要点赞的用户ID"; userInput.style.position = "fixed"; userInput.style.bottom = "90px"; userInput.style.left = "20px"; userInput.style.zIndex = "1000"; userInput.style.padding = "6px"; userInput.style.border = "1px solid #aaa"; userInput.style.borderRadius = "8px"; userInput.style.backgroundColor = "#e0e0e0"; userInput.style.width = "100px"; userInput.value = localStorage.getItem("specificUser") || "14790897"; document.body.appendChild(userInput); const saveUserButton = document.createElement("button"); saveUserButton.textContent = "保存用户ID"; saveUserButton.style.position = "fixed"; saveUserButton.style.bottom = "60px"; saveUserButton.style.left = "20px"; saveUserButton.style.zIndex = "1000"; saveUserButton.style.backgroundColor = "#e0e0e0"; saveUserButton.style.color = "#333"; saveUserButton.style.border = "1px solid #aaa"; saveUserButton.style.padding = "8px 16px"; saveUserButton.style.borderRadius = "8px"; document.body.appendChild(saveUserButton); saveUserButton.onclick = function () { const newSpecificUser = userInput.value.trim(); if (newSpecificUser) { localStorage.setItem("specificUser", newSpecificUser); localStorage.removeItem("specificUserPostList"); localStorage.removeItem("lastOffset"); specificUser = newSpecificUser; console.log( `新的specificUser已保存: ${specificUser},specificUserPostList已重置` ); } }; const likeLimitInput = document.createElement("input"); likeLimitInput.type = "number"; likeLimitInput.placeholder = "输入点赞数量"; likeLimitInput.style.position = "fixed"; likeLimitInput.style.bottom = "180px"; likeLimitInput.style.left = "20px"; likeLimitInput.style.zIndex = "1000"; likeLimitInput.style.padding = "6px"; likeLimitInput.style.border = "1px solid #aaa"; likeLimitInput.style.borderRadius = "8px"; likeLimitInput.style.backgroundColor = "#e0e0e0"; likeLimitInput.style.width = "100px"; likeLimitInput.value = localStorage.getItem("likeLimit") || 200; document.body.appendChild(likeLimitInput); const saveLikeLimitButton = document.createElement("button"); saveLikeLimitButton.textContent = "保存点赞数量"; saveLikeLimitButton.style.position = "fixed"; saveLikeLimitButton.style.bottom = "140px"; saveLikeLimitButton.style.left = "20px"; saveLikeLimitButton.style.zIndex = "1000"; saveLikeLimitButton.style.backgroundColor = "#e0e0e0"; saveLikeLimitButton.style.color = "#333"; saveLikeLimitButton.style.border = "1px solid #aaa"; saveLikeLimitButton.style.padding = "8px 16px"; saveLikeLimitButton.style.borderRadius = "8px"; document.body.appendChild(saveLikeLimitButton); saveLikeLimitButton.onclick = function () { const newLikeLimit = parseInt(likeLimitInput.value.trim(), 10); if (newLikeLimit && newLikeLimit > 0) { localStorage.setItem("likeLimit", newLikeLimit); likeLimit = newLikeLimit; console.log(`新的likeLimit已保存: ${likeLimit}`); } }; // 增加清除数据的按钮 const clearDataButton = document.createElement("button"); clearDataButton.textContent = "清除所有数据"; clearDataButton.style.position = "fixed"; clearDataButton.style.bottom = "20px"; clearDataButton.style.left = "140px"; clearDataButton.style.zIndex = "1000"; clearDataButton.style.backgroundColor = "#ff6666"; // 红色背景,提示删除操作 clearDataButton.style.color = "#fff"; // 白色文本 clearDataButton.style.border = "1px solid #ff3333"; // 深红色边框 clearDataButton.style.padding = "8px 16px"; clearDataButton.style.borderRadius = "8px"; document.body.appendChild(clearDataButton); clearDataButton.onclick = function () { localStorage.removeItem("lastOffset"); localStorage.removeItem("clickCounter"); localStorage.removeItem("clickCounterTimestamp"); localStorage.removeItem("specificUserPostList"); console.log("所有数据已清除,除了 specificUser 和 specificUserPostList"); }; })(); ================================================ FILE: index_likeUser_random.js ================================================ // 那个activity和普通的是不能通用的 // // ==UserScript== // // @name Auto Like Specific User base on activity // // @namespace http://tampermonkey.net/ // // @version 1.1.2 // // @description 自动点赞特定用户,适用于discourse // // @author liuweiqing // // @match https://meta.discourse.org/* // // @match https://linux.do/* // // @match https://meta.appinn.net/* // // @match https://community.openai.com/ // // @grant none // // @license MIT // // @icon https://www.google.com/s2/favicons?domain=linux.do // // @downloadURL https://update.greasyfork.org/scripts/489464/Auto%20Read.user.js // // @updateURL https://update.greasyfork.org/scripts/489464/Auto%20Read.meta.js // // @run-at document-end // // ==/UserScript== // (function () { // ("use strict"); // // 定义可能的基本URL // const possibleBaseURLs = [ // "https://linux.do", // "https://meta.discourse.org", // "https://meta.appinn.net", // "https://community.openai.com", // ]; // const commentLimit = 1000; // const specificUserPostListLimit = 100; // const currentURL = window.location.href; // let specificUser = localStorage.getItem("specificUser") || "14790897"; // let likeLimit = parseInt(localStorage.getItem("likeLimit") || 200, 10); // let BASE_URL = possibleBaseURLs.find((url) => currentURL.startsWith(url)); // // 环境变量:阅读网址,如果没有找到匹配的URL,则默认为第一个 // if (!BASE_URL) { // BASE_URL = possibleBaseURLs[0]; // console.log("当前BASE_URL设置为(默认): " + BASE_URL); // } else { // console.log("当前BASE_URL是: " + BASE_URL); // } // // 获取当前时间戳 // const currentTime = Date.now(); // // 获取存储的时间戳 // const defaultTimestamp = new Date("1999-01-01T00:00:00Z").getTime(); //默认值为1999年 // const storedTime = parseInt( // localStorage.getItem("clickCounterTimestamp") || // defaultTimestamp.toString(), // 10 // ); // // 获取当前的点击计数,如果不存在则初始化为0 // let clickCounter = parseInt(localStorage.getItem("clickCounter") || "0", 10); // // 检查是否超过12小时(12小时 = 12 * 60 * 60 * 1000 毫秒) // if (currentTime - storedTime > 12 * 60 * 60 * 1000) { // // 超过24小时,清空点击计数器并更新时间戳 // clickCounter = 0; // localStorage.setItem("clickCounter", "0"); // localStorage.setItem("clickCounterTimestamp", currentTime.toString()); // } // console.log(`Initial clickCounter: ${clickCounter}`); // // 入口函数 // window.addEventListener("load", () => { // console.log("autoRead", localStorage.getItem("read")); // checkFirstRun(); // if (localStorage.getItem("read") === "true") { // console.log("点赞开始"); // setTimeout(() => { // likeSpecificPost(); // }, 2000); // setTimeout(() => { // openSpecificUserPost(); // }, 4000); // } // }); // function checkFirstRun() { // if (localStorage.getItem("isFirstRun") === null) { // console.log("脚本第一次运行,执行初始化操作..."); // updateInitialData(); // localStorage.setItem("isFirstRun", "false"); // } else { // console.log("脚本非第一次运行"); // } // } // function updateInitialData() { // localStorage.setItem("read", "false"); // 开始时自动滚动关闭 // console.log("执行了初始数据更新操作"); // } // async function getLatestTopic() { // let lastOffset = Number(localStorage.getItem("lastOffset")) || 0; // const specificUserPostList = []; // let isDataSufficient = false; // while (!isDataSufficient) { // const randomChoice = Math.random() < 0.5; // 生成一个随机数,50% 概率为 true // let url; // if (randomChoice) { // // 使用第一个链接 // url = `${BASE_URL}/user_actions.json?offset=${lastOffset}&username=${specificUser}&filter=5`; // console.log("使用第一个链接:", url); // } else { // // 使用第二个链接 // url = `${BASE_URL}/search?q=%40${specificUser}%20in%3Aunseen`; // &page=${lastOffset} // console.log("使用第二个链接:", url); // } // try { // const response = await fetch(url, { // headers: { // Accept: "application/json", // }, // }); // if (!response.ok) { // throw new Error(`请求失败,状态码: ${response.status}`); // } // const result = await response.json(); // if (result && result.user_actions && result.user_actions.length > 0) { // result.user_actions.forEach((action) => { // const topicId = action.topic_id; // const postNumber = action.post_number; // specificUserPostList.push({ // topic_id: topicId, // post_number: postNumber, // }); // }); // // 检查是否已获得足够的 Posts // if (specificUserPostList.length >= specificUserPostListLimit) { // isDataSufficient = true; // } // } else { // isDataSufficient = true; // 没有更多内容时停止请求 // } // } catch (error) { // console.error("请求出错:", error); // isDataSufficient = true; // 遇到错误时也停止请求 // } // } // // 根据返回的数据长度调整 lastOffset // lastOffset += result.user_actions.length; // // 如果列表超出限制,则截断 // if (specificUserPostList.length > specificUserPostListLimit) { // specificUserPostList = specificUserPostList.slice( // 0, // specificUserPostListLimit // ); // } // // 存储 lastOffset 和 specificUserPostList 到 localStorage // localStorage.setItem("lastOffset", lastOffset); // localStorage.setItem( // "specificUserPostList", // JSON.stringify(specificUserPostList) // ); // } // function openSpecificUserPost() { // let specificUserPostListStr = localStorage.getItem("specificUserPostList"); // let specificUserPostList = specificUserPostListStr // ? JSON.parse(specificUserPostListStr) // : []; // // 如果列表为空,则获取最新文章 // if (specificUserPostList.length === 0) { // getLatestTopic(); // specificUserPostListStr = localStorage.getItem("specificUserPostList"); // specificUserPostList = specificUserPostListStr // ? JSON.parse(specificUserPostListStr) // : []; // } // // 如果获取到新文章,打开第一个 // if (specificUserPostList.length > 0) { // const post = specificUserPostList.shift(); // 获取列表中的第一个对象 // localStorage.setItem( // "specificUserPostList", // JSON.stringify(specificUserPostList) // ); // window.location.href = `${BASE_URL}/t/topic/${post.topic_id}/${post.post_number}`; // } else { // console.error("未能获取到新的帖子数据。"); // } // } // // 检查是否点赞 // // const postId = data.post_id; // // const targetId = `discourse-reactions-counter-${postId}-right`; // // const element = document.getElementById(targetId); // function likeSpecificPost() { // const urlParts = window.location.pathname.split("/"); // const lastPart = urlParts[urlParts.length - 1]; // 获取最后一部分 // let buttons, reactionButton; // console.log("post number:", lastPart); // if (lastPart < 10000) { // buttons = document.querySelectorAll( // "button[aria-label]" //[class*='reply'] // ); // let targetButton = null; // buttons.forEach((button) => { // const ariaLabel = button.getAttribute("aria-label"); // if (ariaLabel && ariaLabel.includes(`#${lastPart}`)) { // targetButton = button; // console.log("找到post number按钮:", targetButton); // return; // } // }); // if (targetButton) { // // 找到按钮后,获取其父级元素 // const parentElement = targetButton.parentElement; // console.log("父级元素:", parentElement); // reactionButton = parentElement.querySelector( // ".discourse-reactions-reaction-button" // ); // } else { // console.log(`未找到包含 #${lastPart} 的按钮`); // } // } else { // //大于10000说明是主题帖,选择第一个 // reactionButton = document.querySelectorAll( // ".discourse-reactions-reaction-button" // )[0]; // } // if ( // reactionButton.title !== "点赞此帖子" && // reactionButton.title !== "Like this post" // ) { // console.log("已经点赞过"); // return "already liked"; // } else if (clickCounter >= likeLimit) { // console.log("已经达到点赞上限"); // localStorage.setItem("read", false); // return; // } // triggerClick(reactionButton); // clickCounter++; // console.log( // `Clicked like button ${clickCounter},已点赞用户${specificUser}` // ); // localStorage.setItem("clickCounter", clickCounter.toString()); // // 如果点击次数达到likeLimit次,则设置点赞变量为false // if (clickCounter === likeLimit) { // console.log( // `Reached ${likeLimit} likes, setting the like variable to false.` // ); // localStorage.setItem("read", false); // } else { // console.log("clickCounter:", clickCounter); // } // } // function triggerClick(button) { // const event = new MouseEvent("click", { // bubbles: true, // cancelable: true, // view: window, // }); // button.dispatchEvent(event); // } // const button = document.createElement("button"); // button.textContent = // localStorage.getItem("read") === "true" ? "停止阅读" : "开始阅读"; // button.style.position = "fixed"; // button.style.bottom = "20px"; // button.style.left = "20px"; // button.style.zIndex = 1000; // button.style.backgroundColor = "#e0e0e0"; // button.style.color = "#333"; // button.style.border = "1px solid #aaa"; // button.style.padding = "8px 16px"; // button.style.borderRadius = "8px"; // document.body.appendChild(button); // button.onclick = function () { // const currentlyReading = localStorage.getItem("read") === "true"; // const newReadState = !currentlyReading; // localStorage.setItem("read", newReadState.toString()); // button.textContent = newReadState ? "停止阅读" : "开始阅读"; // if (newReadState) { // if (BASE_URL == "https://linux.do") { // const maxPostNumber = 600; // const randomPostNumber = Math.floor(Math.random() * maxPostNumber) + 1; // const newUrl = `https://linux.do/t/topic/13716/${randomPostNumber}`; // window.location.href = newUrl; // } else { // window.location.href = `${BASE_URL}/t/topic/1`; // } // } // }; // const userInput = document.createElement("input"); // userInput.type = "text"; // userInput.placeholder = "输入要点赞的用户ID"; // userInput.style.position = "fixed"; // userInput.style.bottom = "90px"; // userInput.style.left = "20px"; // userInput.style.zIndex = "1000"; // userInput.style.padding = "6px"; // userInput.style.border = "1px solid #aaa"; // userInput.style.borderRadius = "8px"; // userInput.style.backgroundColor = "#e0e0e0"; // userInput.style.width = "100px"; // userInput.value = localStorage.getItem("specificUser") || "14790897"; // document.body.appendChild(userInput); // const saveUserButton = document.createElement("button"); // saveUserButton.textContent = "保存用户ID"; // saveUserButton.style.position = "fixed"; // saveUserButton.style.bottom = "60px"; // saveUserButton.style.left = "20px"; // saveUserButton.style.zIndex = "1000"; // saveUserButton.style.backgroundColor = "#e0e0e0"; // saveUserButton.style.color = "#333"; // saveUserButton.style.border = "1px solid #aaa"; // saveUserButton.style.padding = "8px 16px"; // saveUserButton.style.borderRadius = "8px"; // document.body.appendChild(saveUserButton); // saveUserButton.onclick = function () { // const newSpecificUser = userInput.value.trim(); // if (newSpecificUser) { // localStorage.setItem("specificUser", newSpecificUser); // localStorage.removeItem("specificUserPostList"); // localStorage.removeItem("lastOffset"); // specificUser = newSpecificUser; // console.log( // `新的specificUser已保存: ${specificUser},specificUserPostList已重置` // ); // } // }; // const likeLimitInput = document.createElement("input"); // likeLimitInput.type = "number"; // likeLimitInput.placeholder = "输入点赞数量"; // likeLimitInput.style.position = "fixed"; // likeLimitInput.style.bottom = "180px"; // likeLimitInput.style.left = "20px"; // likeLimitInput.style.zIndex = "1000"; // likeLimitInput.style.padding = "6px"; // likeLimitInput.style.border = "1px solid #aaa"; // likeLimitInput.style.borderRadius = "8px"; // likeLimitInput.style.backgroundColor = "#e0e0e0"; // likeLimitInput.style.width = "100px"; // likeLimitInput.value = localStorage.getItem("likeLimit") || 200; // document.body.appendChild(likeLimitInput); // const saveLikeLimitButton = document.createElement("button"); // saveLikeLimitButton.textContent = "保存点赞数量"; // saveLikeLimitButton.style.position = "fixed"; // saveLikeLimitButton.style.bottom = "140px"; // saveLikeLimitButton.style.left = "20px"; // saveLikeLimitButton.style.zIndex = "1000"; // saveLikeLimitButton.style.backgroundColor = "#e0e0e0"; // saveLikeLimitButton.style.color = "#333"; // saveLikeLimitButton.style.border = "1px solid #aaa"; // saveLikeLimitButton.style.padding = "8px 16px"; // saveLikeLimitButton.style.borderRadius = "8px"; // document.body.appendChild(saveLikeLimitButton); // saveLikeLimitButton.onclick = function () { // const newLikeLimit = parseInt(likeLimitInput.value.trim(), 10); // if (newLikeLimit && newLikeLimit > 0) { // localStorage.setItem("likeLimit", newLikeLimit); // likeLimit = newLikeLimit; // console.log(`新的likeLimit已保存: ${likeLimit}`); // } // }; // // 增加清除数据的按钮 // const clearDataButton = document.createElement("button"); // clearDataButton.textContent = "清除所有数据"; // clearDataButton.style.position = "fixed"; // clearDataButton.style.bottom = "20px"; // clearDataButton.style.left = "140px"; // clearDataButton.style.zIndex = "1000"; // clearDataButton.style.backgroundColor = "#ff6666"; // 红色背景,提示删除操作 // clearDataButton.style.color = "#fff"; 白色文本 // clearDataButton.style.border = "1px solid #ff3333"; // 深红色边框 // clearDataButton.style.padding = "8px 16px"; // clearDataButton.style.borderRadius = "8px"; // document.body.appendChild(clearDataButton); // clearDataButton.onclick = function () { // localStorage.removeItem("lastOffset"); // localStorage.removeItem("clickCounter"); // localStorage.removeItem("clickCounterTimestamp"); // localStorage.removeItem("specificUserPostList"); // console.log("所有数据已清除,除了 specificUser 和 specificUserPostList"); // }; // })(); ================================================ FILE: index_passage_list_old_not_use.js ================================================ // ==UserScript== // @name Auto Read // @namespace http://tampermonkey.net/ // @version 1.3.2 // @description 自动刷linuxdo文章 // @author liuweiqing // @match https://meta.discourse.org/* // @match https://linux.do/* // @match https://meta.appinn.net/* // @match https://community.openai.com/ // @grant none // @license MIT // @icon https://www.google.com/s2/favicons?domain=linux.do // ==/UserScript== (function () { ("use strict"); // 定义可能的基本URL const possibleBaseURLs = [ "https://meta.discourse.org", "https://linux.do", "https://meta.appinn.net", "https://community.openai.com", ]; // 获取当前页面的URL const currentURL = window.location.href; // 确定当前页面对应的BASE_URL let BASE_URL = possibleBaseURLs.find((url) => currentURL.startsWith(url)); // 环境变量:阅读网址,如果没有找到匹配的URL,则默认为第一个 if (!BASE_URL) { BASE_URL = possibleBaseURLs[0]; console.log("默认BASE_URL设置为: " + BASE_URL); } else { console.log("当前BASE_URL是: " + BASE_URL); } // 以下是脚本的其余部分 console.log("脚本正在运行在: " + BASE_URL); //1.进入网页 https://linux.do/t/topic/数字(1,2,3,4) //2.使滚轮均衡的往下移动模拟刷文章 // 检查是否是第一次运行脚本 function checkFirstRun() { if (localStorage.getItem("isFirstRun") === null) { // 是第一次运行,执行初始化操作 console.log("脚本第一次运行,执行初始化操作..."); updateInitialData(); // 设置 isFirstRun 标记为 false localStorage.setItem("isFirstRun", "false"); } else { // 非第一次运行 console.log("脚本非第一次运行"); } } // 更新初始数据的函数 function updateInitialData() { localStorage.setItem("read", "false"); // 开始时自动滚动关闭 localStorage.setItem("autoLikeEnabled", "false"); //默认关闭自动点赞 console.log("执行了初始数据更新操作"); } const delay = 2000; // 滚动检查的间隔(毫秒) let scrollInterval = null; let checkScrollTimeout = null; let autoLikeInterval = null; //在主页去寻找可以进入的话题,同时可以在文章页进行浏览 function scrollToBottomSlowly( stopDistance = 9999999999, callback = undefined, distancePerStep = 20, delayPerStep = 50 ) { if (scrollInterval !== null) { clearInterval(scrollInterval); } scrollInterval = setInterval(() => { if ( window.innerHeight + window.scrollY >= document.body.offsetHeight - 100 || window.innerHeight + window.scrollY >= stopDistance ) { clearInterval(scrollInterval); scrollInterval = null; if (typeof callback === "function") { callback(); // 当滚动结束时调用回调函数 } } else { window.scrollBy(0, distancePerStep); } }, delayPerStep); } // 功能:跳转到下一个话题 function navigateToNextTopic() { // 定义包含文章列表的数组 const urls = [ `${BASE_URL}/latest`, `${BASE_URL}/top`, `${BASE_URL}/latest?ascending=false&order=posts`, // `${BASE_URL}/unread`, // 示例:如果你想将这个URL启用,只需去掉前面的注释 ]; // 生成一个随机索引 const randomIndex = Math.floor(Math.random() * urls.length); // 根据随机索引选择一个URL const nextTopicURL = urls[randomIndex]; // 在跳转之前,标记即将跳转到下一个话题 localStorage.setItem("navigatingToNextTopic", "true"); // 尝试导航到下一个话题 window.location.href = nextTopicURL; } // 检查是否已滚动到底部(不断重复执行) function checkScroll() { if (localStorage.getItem("read")) { if ( window.innerHeight + window.scrollY >= document.body.offsetHeight - 100 ) { console.log("已滚动到底部"); navigateToNextTopic(); } else { scrollToBottomSlowly(); if (checkScrollTimeout !== null) { clearTimeout(checkScrollTimeout); } checkScrollTimeout = setTimeout(checkScroll, delay); } } } // 入口函数 window.addEventListener("load", () => { checkFirstRun(); console.log( "autoRead", localStorage.getItem("read"), "autoLikeEnabled", localStorage.getItem("autoLikeEnabled") ); if (localStorage.getItem("read") === "true") { // 检查是否正在导航到下一个话题 if (localStorage.getItem("navigatingToNextTopic") === "true") { console.log("正在导航到下一个话题"); // 等待一段时间或直到页面完全加载 // 页面加载完成后,移除标记 localStorage.removeItem("navigatingToNextTopic"); // 使用setTimeout延迟执行 setTimeout(() => { // 先随机滚动一段距离然后再查找链接 scrollToBottomSlowly( Math.random() * document.body.offsetHeight * 3, searchLinkClick, 20, 20 ); }, 2000); // 延迟2000毫秒(即2秒) } else { console.log("执行正常的滚动和检查逻辑"); // 执行正常的滚动和检查逻辑 checkScroll(); if (isAutoLikeEnabled()) { //自动点赞 autoLike(); } } } }); // 创建一个控制滚动的按钮 function searchLinkClick() { // 在新页面加载后执行检查 // 使用CSS属性选择器寻找href属性符合特定格式的标签 const links = document.querySelectorAll('a[href^="/t/"]'); // 筛选出未阅读的链接 const unreadLinks = Array.from(links).filter((link) => { // 向上遍历DOM树,查找包含'visited'类的父级元素,最多查找三次 let parent = link.parentElement; let times = 0; // 查找次数计数器 while (parent && times < 3) { if (parent.classList.contains("visited")) { // 如果找到包含'visited'类的父级元素,中断循环 return false; // 父级元素包含'visited'类,排除这个链接 } parent = parent.parentElement; // 继续向上查找 times++; // 增加查找次数 } // 如果链接未被读过,且在向上查找三次内,其父级元素中没有包含'visited'类,则保留这个链接 return true; }); // 如果找到了这样的链接 if (unreadLinks.length > 0) { // 从所有匹配的链接中随机选择一个 const randomIndex = Math.floor(Math.random() * unreadLinks.length); const link = unreadLinks[randomIndex]; // 打印找到的链接(可选) console.log("Found link:", link.href); // 导航到该链接 window.location.href = link.href; } else { // 如果没有找到符合条件的链接,打印消息(可选) console.log("No link with the specified format was found."); scrollToBottomSlowly( Math.random() * document.body.offsetHeight * 3, searchLinkClick ); } } // 获取当前时间戳 const currentTime = Date.now(); // 获取存储的时间戳 const defaultTimestamp = new Date("1999-01-01T00:00:00Z").getTime(); //默认值为1999年 const storedTime = parseInt( localStorage.getItem("clickCounterTimestamp") || defaultTimestamp.toString(), 10 ); // 获取当前的点击计数,如果不存在则初始化为0 let clickCounter = parseInt(localStorage.getItem("clickCounter") || "0", 10); // 检查是否超过24小时(24小时 = 24 * 60 * 60 * 1000 毫秒) if (currentTime - storedTime > 24 * 60 * 60 * 1000) { // 超过24小时,清空点击计数器并更新时间戳 clickCounter = 0; localStorage.setItem("clickCounter", "0"); localStorage.setItem("clickCounterTimestamp", currentTime.toString()); } console.log(`Initial clickCounter: ${clickCounter}`); function triggerClick(button) { const event = new MouseEvent("click", { bubbles: true, cancelable: true, view: window, }); button.dispatchEvent(event); } function autoLike() { console.log(`Initial clickCounter: ${clickCounter}`); // 寻找所有的discourse-reactions-reaction-button const buttons = document.querySelectorAll( ".discourse-reactions-reaction-button" ); if (buttons.length === 0) { console.error( "No buttons found with the selector '.discourse-reactions-reaction-button'" ); return; } console.log(`Found ${buttons.length} buttons.`); // 调试信息 // 逐个点击找到的按钮 buttons.forEach((button, index) => { if ( (button.title !== "点赞此帖子" && button.title !== "Like this post") || clickCounter >= 50 ) { return; } // 使用setTimeout来错开每次点击的时间,避免同时触发点击 autoLikeInterval = setTimeout(() => { // 模拟点击 triggerClick(button); // 使用自定义的触发点击方法 console.log(`Clicked like button ${index + 1}`); clickCounter++; // 更新点击计数器 // 将新的点击计数存储到localStorage localStorage.setItem("clickCounter", clickCounter.toString()); // 如果点击次数达到50次,则设置点赞变量为false if (clickCounter === 50) { console.log("Reached 50 likes, setting the like variable to false."); localStorage.setItem("autoLikeEnabled", "false"); // 使用localStorage存储点赞变量状态 } else { console.log("clickCounter:", clickCounter); } }, index * 3000); // 这里的3000毫秒是两次点击之间的间隔,可以根据需要调整 }); } const button = document.createElement("button"); // 初始化按钮文本基于当前的阅读状态 button.textContent = localStorage.getItem("read") === "true" ? "停止阅读" : "开始阅读"; button.style.position = "fixed"; button.style.bottom = "10px"; // 之前是 top button.style.left = "10px"; // 之前是 right button.style.zIndex = 1000; button.style.backgroundColor = "#f0f0f0"; // 浅灰色背景 button.style.color = "#000"; // 黑色文本 button.style.border = "1px solid #ddd"; // 浅灰色边框 button.style.padding = "5px 10px"; // 内边距 button.style.borderRadius = "5px"; // 圆角 document.body.appendChild(button); button.onclick = function () { const currentlyReading = localStorage.getItem("read") === "true"; const newReadState = !currentlyReading; localStorage.setItem("read", newReadState.toString()); button.textContent = newReadState ? "停止阅读" : "开始阅读"; if (!newReadState) { if (scrollInterval !== null) { clearInterval(scrollInterval); scrollInterval = null; } if (checkScrollTimeout !== null) { clearTimeout(checkScrollTimeout); checkScrollTimeout = null; } localStorage.removeItem("navigatingToNextTopic"); } else { // 如果是Linuxdo,就导航到我的帖子 if (BASE_URL == "https://linux.do") { window.location.href = "https://linux.do/t/topic/13716/340"; } else if (BASE_URL == "https://meta.appinn.net") { window.location.href = "https://meta.appinn.net/t/topic/52006"; } else { window.location.href = `${BASE_URL}/t/topic/1`; } checkScroll(); } }; //自动点赞按钮 // 在页面上添加一个控制自动点赞的按钮 const toggleAutoLikeButton = document.createElement("button"); toggleAutoLikeButton.textContent = isAutoLikeEnabled() ? "禁用自动点赞" : "启用自动点赞"; toggleAutoLikeButton.style.position = "fixed"; toggleAutoLikeButton.style.bottom = "50px"; // 之前是 top,且与另一个按钮错开位置 toggleAutoLikeButton.style.left = "10px"; // 之前是 right toggleAutoLikeButton.style.zIndex = "1000"; toggleAutoLikeButton.style.backgroundColor = "#f0f0f0"; // 浅灰色背景 toggleAutoLikeButton.style.color = "#000"; // 黑色文本 toggleAutoLikeButton.style.border = "1px solid #ddd"; // 浅灰色边框 toggleAutoLikeButton.style.padding = "5px 10px"; // 内边距 toggleAutoLikeButton.style.borderRadius = "5px"; // 圆角 document.body.appendChild(toggleAutoLikeButton); // 为按钮添加点击事件处理函数 toggleAutoLikeButton.addEventListener("click", () => { const isEnabled = !isAutoLikeEnabled(); setAutoLikeEnabled(isEnabled); toggleAutoLikeButton.textContent = isEnabled ? "禁用自动点赞" : "启用自动点赞"; }); // 判断是否启用自动点赞 function isAutoLikeEnabled() { // 从localStorage获取autoLikeEnabled的值,如果未设置,默认为"true" return localStorage.getItem("autoLikeEnabled") !== "false"; } // 设置自动点赞的启用状态 function setAutoLikeEnabled(enabled) { localStorage.setItem("autoLikeEnabled", enabled ? "true" : "false"); } })(); ================================================ FILE: invite_codecow.js ================================================ // ==UserScript== // @name 自动上传邀请码 // @namespace https://linux.do // @version 0.0.8 // @description 直接上传邀请码! // @author codecow // @match https://linux.do/* // @icon https://www.google.com/s2/favicons?sz=64&domain=linux.do // @require https://cdn.bootcdn.net/ajax/libs/jquery/1.9.1/jquery.min.js // @grant none // @license MIT // @run-at document-end // ==/UserScript== (function () { "use strict"; const BASE_URL = "https://linux.do"; const UPLOAD_URL = "https://linuxdo-invites.speedcow.top/upload"; let username = ""; let csrfToken = document .querySelector('meta[name="csrf-token"]') .getAttribute("content"); const CHECK_INTERVAL_MS = 1000; // 检查间隔 let headers = { accept: "*/*", "accept-language": "zh-CN,zh;q=0.9,en;q=0.8", "cache-control": "no-cache", "discourse-logged-in": "true", "discourse-present": "true", pragma: "no-cache", "x-csrf-token": csrfToken, "x-requested-with": "XMLHttpRequest", "sec-ch-ua": '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"', "Discourse-Logged-In": "true", "sec-ch-ua-arch": '"x86"', "sec-ch-ua-platform-version": '"10.0.0"', "X-Requested-With": "XMLHttpRequest", "sec-ch-ua-full-version-list": '"Google Chrome";v="123.0.6312.60", "Not:A-Brand";v="8.0.0.0", "Chromium";v="123.0.6312.60"', "sec-ch-ua-bitness": '"64"', "sec-ch-ua-model": "", "sec-ch-ua-platform": '"Windows"', "Discourse-Present": "true", "sec-ch-ua-mobile": "?0", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "sec-ch-ua-full-version": '"123.0.6312.60"', Referer: `${BASE_URL}/u/${username}/invited/pending`, }; let upload_headers = { "Content-Type": "application/json", }; // 循环flag let flag = true; // 邀请链接 let inviteLinks = []; // 获取过期时间 async function getExpiresAt() { let date = new Date(); date.setDate(date.getDate() + 1); // 格式化日期和时间 const year = date.getFullYear(); const month = (date.getMonth() + 1).toString().padStart(2, "0"); // 月份是从0开始的 const day = date.getDate().toString().padStart(2, "0"); const hours = date.getHours().toString().padStart(2, "0"); const minutes = date.getMinutes().toString().padStart(2, "0"); // 时区处理,这里简化处理为+08:00,具体时区可能需要动态获取或计算 const timezone = "+08:00"; // 构建expires_at参数值 return `${year}-${month}-${day} ${hours}:${minutes}${timezone}`; } // 获取新的邀请链接 async function fetchInvite() { try { let response = await fetch(`${BASE_URL}/invites`, { headers: headers, method: "POST", mode: "cors", credentials: "include", body: `max_redemptions_allowed=2&expires_at=${encodeURIComponent( await getExpiresAt() )}`, }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } let data = await response.json(); if (data.error_type === "rate_limit") { console.log("Rate limit reached, stopping."); flag = false; } else { inviteLinks.push(data.link); await uploadInvites(inviteLinks); } } catch (error) { console.log("Error:", error); flag = false; } } // 获取已有的邀请链接列表 async function fetchInvites() { try { let response = await fetch(`${BASE_URL}/u/${username}/invited/pending`, { headers: headers, method: "GET", mode: "cors", credentials: "include", }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } let data = await response.json(); for (let invite of data.invites) { inviteLinks.push(invite.link); } await uploadInvites(inviteLinks); while (flag) { await fetchInvite(); if (!flag) { break; } } } catch (error) { console.log("Error:", error); flag = false; } } // 上传邀请链接 async function uploadInvites(inviteLinks) { try { const inviteLinksObject = inviteLinks.reduce((obj, link) => { obj[link] = link; return obj; }, {}); await fetch(`${UPLOAD_URL}?username=${username}`, { headers: upload_headers, method: "POST", mode: "no-cors", body: JSON.stringify(inviteLinksObject), }); } catch (error) { console.log("Error:", error); flag = false; } } // 获取用户名称 const checkInterval = setInterval(function () { // 查找id为current-user的li元素 const currentUserLi = document.querySelector("#current-user"); // 如果找到了元素 if (currentUserLi) { // 查找该元素下的button const button = currentUserLi.querySelector("button"); // 如果找到了button元素 if (button) { // 获取button的href属性值 const href = button.getAttribute("href"); username = href.replace("/u/", ""); // username = document.getElementsByClassName("header-dropdown-toggle current-user")[0].querySelector("button").getAttribute("href").replace("/u/", ""); clearInterval(checkInterval); // 停止检查 } } if (username !== "") { fetchInvites().then((r) => { console.log("done"); }); } }, CHECK_INTERVAL_MS); // 每隔1秒检查一次 })(); ================================================ FILE: package.json ================================================ { "type": "module", "dependencies": { "@playwright/test": "^1.56.1", "dotenv": "^16.4.5", "express": "^4.21.2", "https-proxy-agent": "^7.0.6", "mongodb": "^6.17.0", "mysql2": "^3.14.1", "node-fetch": "^3.3.2", "node-telegram-bot-api": "^0.66.0", "pg": "^8.16.0", "playwright": "^1.58.0", "playwright-extra": "^4.3.6", "playwright-extra-plugin-stealth": "^0.0.1", "puppeteer": "^22.15.0", "puppeteer-extra": "^3.3.6", "puppeteer-extra-plugin-stealth": "^2.11.2", "puppeteer-real-browser": "^1.2.31", "socks-proxy-agent": "^8.0.5", "xml2js": "^0.6.2", "xvfb": "^0.4.0" }, "version": "1.15.1" } ================================================ FILE: parsed_rss_data.json ================================================ { "channelTitle": "长期在线回收iPhone", "channelLink": "https://linux.do/t/topic/525305", "channelDescription": "各位佬有二手iPhone想出手的,可以来我这里问问价。 专业二手iPhone贩子,长期在线回收二手iPhone,只要能开机都可以回收。 但是 id机,监管机不收。\n\n所有交易都走闲鱼,安全靠谱。 \n\n闲鱼店铺名称: **东莞乐回收数码**", "items": [ { "title": "长期在线回收iPhone", "creator": "KotoriMinami", "description": "

好的ok \":ok_hand:\"

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/293", "pubDate": "Tue, 03 Jun 2025 06:38:48 +0000", "guid": "linux.do-post-525305-293", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "鲨鱼辣椒", "description": "

不到 2个月的 ip16 256G,无磕碰多少钱

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/292", "pubDate": "Tue, 03 Jun 2025 06:31:27 +0000", "guid": "linux.do-post-525305-292", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "Kubrick", "description": "

挂小红书上 高价卖给小仙女 现在低像素手机 小仙女最爱

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/291", "pubDate": "Tue, 03 Jun 2025 06:28:50 +0000", "guid": "linux.do-post-525305-291", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "shenpvip", "description": "

iphone 6s要吗

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/290", "pubDate": "Tue, 03 Jun 2025 06:26:44 +0000", "guid": "linux.do-post-525305-290", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "二手iPhone贩子", "description": "

收, 收的就是 8p, 只要是全原装, 能开机就收。

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/289", "pubDate": "Tue, 03 Jun 2025 06:22:29 +0000", "guid": "linux.do-post-525305-289", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "二手iPhone贩子", "description": "

全原装 600 。

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/288", "pubDate": "Tue, 03 Jun 2025 06:21:34 +0000", "guid": "linux.do-post-525305-288", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "沐风", "description": "

不知道iphone8还收不,俺的iphone8除了电池不行其他的都还好,不过估计也值不了几个钱了

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/287", "pubDate": "Tue, 03 Jun 2025 06:17:54 +0000", "guid": "linux.do-post-525305-287", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "sunny22", "description": "

国行xr 128的多少钱收

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/286", "pubDate": "Tue, 03 Jun 2025 06:17:12 +0000", "guid": "linux.do-post-525305-286", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "二手iPhone贩子", "description": "

我这边 1450 直接秒了。

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/285", "pubDate": "Tue, 03 Jun 2025 06:15:10 +0000", "guid": "linux.do-post-525305-285", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "二手iPhone贩子", "description": "

无磕碰, 4800 回收。

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/284", "pubDate": "Tue, 03 Jun 2025 06:14:27 +0000", "guid": "linux.do-post-525305-284", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "二手iPhone贩子", "description": "

6月初 13p 128后爆,也能收个 2k 。 还行吧。

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/283", "pubDate": "Tue, 03 Jun 2025 06:13:48 +0000", "guid": "linux.do-post-525305-283", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "二手iPhone贩子", "description": "

XS 64 无面容, 230 。
\n没有出的必要,留着当备用机吧。

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/282", "pubDate": "Tue, 03 Jun 2025 06:12:38 +0000", "guid": "linux.do-post-525305-282", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "二手iPhone贩子", "description": "

美版有锁就自己留着吧,没啥回收价值了。 美版无锁还能值个 400 左右。 自己挂闲鱼看看垃圾佬能不能出到 650

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/281", "pubDate": "Tue, 03 Jun 2025 06:11:33 +0000", "guid": "linux.do-post-525305-281", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "Those", "description": "

白色国行12pro128 无拆修,电池83,外观和屏幕完美,估价多少?

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/280", "pubDate": "Tue, 03 Jun 2025 06:05:07 +0000", "guid": "linux.do-post-525305-280", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "snow7y", "description": "

佬友,15pro 原色 256g 电池90,边框屏幕很好,无修,值多少

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/279", "pubDate": "Tue, 03 Jun 2025 05:54:53 +0000", "guid": "linux.do-post-525305-279", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "didiBird", "description": "

13pro128 背壳碎,我就卖了 1988还包了邮费,黄鱼还扣了1%手续费。 不过比转转上门多了600,转转只给1300,我没卖,黄鱼自己卖了,一天就出掉了

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/278", "pubDate": "Tue, 03 Jun 2025 05:52:35 +0000", "guid": "linux.do-post-525305-278", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "KotoriMinami", "description": "

佬,xs64g,电池80以下,人脸识别的深感摄像投进过一次水坏了,多少钱,还有没有出的必要 \":expressionless_face:\"

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/277", "pubDate": "Tue, 03 Jun 2025 05:49:52 +0000", "guid": "linux.do-post-525305-277", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "潇7", "description": "

是的,换国内屏

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/276", "pubDate": "Tue, 03 Jun 2025 05:28:09 +0000", "guid": "linux.do-post-525305-276", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "二手iPhone贩子", "description": "

3200 回收。

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/275", "pubDate": "Tue, 03 Jun 2025 05:07:15 +0000", "guid": "linux.do-post-525305-275", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "二手iPhone贩子", "description": "

应该有个 150 左右。 想卖的话 ,给你打包一起收了。

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/274", "pubDate": "Tue, 03 Jun 2025 05:06:35 +0000", "guid": "linux.do-post-525305-274", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "熊猫竹子", "description": "

国行 14 Pro 128G,电池健康 78% ,无拆无修,成色 9成 - 95 成新吧, 自用的,还有购买时候的原盒 值多少

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/273", "pubDate": "Tue, 03 Jun 2025 04:45:02 +0000", "guid": "linux.do-post-525305-273", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "害你的猪", "description": "

我的iPhone5s 16G和iPhone7 32G还能开机。不知道能卖多少

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/272", "pubDate": "Tue, 03 Jun 2025 04:37:52 +0000", "guid": "linux.do-post-525305-272", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "二手iPhone贩子", "description": "

国行 保修 280+ , 7000 走闲鱼。

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/271", "pubDate": "Tue, 03 Jun 2025 04:15:01 +0000", "guid": "linux.do-post-525305-271", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "Jiushuself", "description": "

充电27次

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/270", "pubDate": "Tue, 03 Jun 2025 04:11:58 +0000", "guid": "linux.do-post-525305-270", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" }, { "title": "长期在线回收iPhone", "creator": "二手iPhone贩子", "description": "\n

充电50次以内, 7000

\n

Read full topic

", "link": "https://linux.do/t/topic/525305/269", "pubDate": "Tue, 03 Jun 2025 04:11:21 +0000", "guid": "linux.do-post-525305-269", "guidIsPermaLink": "false", "source": "长期在线回收iPhone", "sourceUrl": "https://linux.do/t/topic/525305.rss" } ] } ================================================ FILE: pass_cf.py ================================================ # 参考:https://linux.do/t/topic/817360 #!/usr/bin/python3 from flask import Flask, request, Response, stream_with_context from curl_cffi import requests import json import time from threading import Thread, Event, Lock from queue import Queue from urllib.parse import urlparse from datetime import datetime, timedelta app = Flask(__name__) # Session 管理 class SessionManager: def __init__(self, refresh_interval=3600): # 默认每小时刷新 self.session = None self.last_refresh = None self.refresh_interval = refresh_interval self.lock = Lock() self._create_session() def _create_session(self): """创建新的 session""" self.session = requests.Session(impersonate="chrome110") self.last_refresh = datetime.now() print(f"[{self.last_refresh}] Created new session") def get_session(self): """获取 session,必要时刷新""" with self.lock: if (datetime.now() - self.last_refresh).total_seconds() > self.refresh_interval: print(f"[{datetime.now()}] Session expired, creating new one...") self._create_session() return self.session # 创建全局 session 管理器 session_manager = SessionManager(refresh_interval=3600) # 每小时刷新 # 支持的域名映射 PROXY_DOMAINS = { 'claude.ai': 'https://claude.ai', 'www.claude.ai': 'https://claude.ai', # 保留原有的Perplexity域名以保持兼容性 'www.perplexity.ai': 'https://www.perplexity.ai', 'perplexity.ai': 'https://www.perplexity.ai', 'api.cloudinary.com': 'https://api.cloudinary.com', 'ppl-ai-file-upload.s3.amazonaws.com': 'https://ppl-ai-file-upload.s3.amazonaws.com', 'pplx-res.cloudinary.com': 'https://pplx-res.cloudinary.com' } def heartbeat_generator(response_queue, stop_event): """生成心跳数据以保持连接活跃""" while not stop_event.is_set(): time.sleep(2) response_queue.put(b' ') @app.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS']) @app.route('/', methods=['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS']) def proxy(path): try: method = request.method headers = dict(request.headers) original_host = headers.get('Host', 'claude.ai') # 保留原始Cookie cookies = request.cookies # 删除可能导致问题的头部 for h in ['Host', 'host', 'X-Real-Ip', 'X-Forwarded-For']: headers.pop(h, None) if original_host in PROXY_DOMAINS: base_url = PROXY_DOMAINS[original_host] else: base_url = 'https://claude.ai' target_url = f"{base_url}/{path}" if path else base_url if request.query_string: target_url += f"?{request.query_string.decode('utf-8')}" target_host = urlparse(base_url).netloc headers['Host'] = target_host # 添加Claude特定的头部 if 'claude.ai' in target_host: headers['Origin'] = 'https://claude.ai' headers['Referer'] = 'https://claude.ai/' headers['anthropic-client-platform'] = 'web_claude_ai' print(f"Proxying {method} request from {original_host} to: {target_url}") # 更精确的SSE判断条件 is_sse = ( '/completion' in path and # 必须是completion端点 method == 'POST' # 必须是POST请求 ) or ( 'perplexity_ask' in path # 保持原有的perplexity支持 ) print(f"Is SSE request: {is_sse}") if is_sse: return handle_sse_request(method, target_url, headers, cookies) else: return handle_normal_request(method, target_url, headers, cookies) except Exception as e: print(f"Exception: {str(e)}") import traceback traceback.print_exc() return Response(f"Error: {str(e)}", status=500) def handle_normal_request(method, target_url, headers, cookies): """处理普通请求""" session = session_manager.get_session() try: # 打印请求详情以便调试 print(f"Request headers: {json.dumps(headers, indent=2)}") if method in ["POST", "PUT", "PATCH", "DELETE"]: raw_data = request.get_data() if raw_data: # 打印请求体以便调试 try: print(f"Request body: {json.dumps(json.loads(raw_data), indent=2)}") except: print(f"Request body (raw): {raw_data[:200]}...") resp = session.request( method, target_url, data=raw_data, headers=headers, cookies=cookies, timeout=30 ) else: resp = session.request( method, target_url, headers=headers, cookies=cookies, timeout=30 ) else: resp = session.request( method, target_url, headers=headers, cookies=cookies, timeout=30 ) print(f"Response status: {resp.status_code}") # 打印响应头和内容以便调试 print(f"Response headers: {json.dumps(dict(resp.headers), indent=2)}") try: print(f"Response body: {json.dumps(json.loads(resp.text), indent=2)}") except: print(f"Response body (raw): {resp.text[:200]}...") response_headers = {} for key, value in resp.headers.items(): if key.lower() not in ['content-encoding', 'transfer-encoding', 'connection']: response_headers[key] = value return Response( resp.content, status=resp.status_code, headers=response_headers ) except Exception as e: print(f"Error in normal request: {str(e)}") return Response(f"Request failed: {str(e)}", status=500) def handle_sse_request(method, target_url, headers, cookies): """处理 SSE (Server-Sent Events) 请求""" def generate(): response_queue = Queue() stop_event = Event() heartbeat_thread = Thread(target=heartbeat_generator, args=(response_queue, stop_event)) heartbeat_thread.daemon = True heartbeat_thread.start() try: session = session_manager.get_session() # 打印请求详情以便调试 print(f"SSE Request headers: {json.dumps(headers, indent=2)}") if method in ["POST", "PUT", "PATCH"]: raw_data = request.get_data() # 打印请求体以便调试 try: print(f"SSE Request body: {json.dumps(json.loads(raw_data), indent=2)}") except: print(f"SSE Request body (raw): {raw_data[:200]}...") resp = session.request( method, target_url, data=raw_data, headers=headers, cookies=cookies, stream=True, timeout=60 ) else: resp = session.request( method, target_url, headers=headers, cookies=cookies, stream=True, timeout=60 ) print(f"SSE Response status: {resp.status_code}") print(f"SSE Response headers: {json.dumps(dict(resp.headers), indent=2)}") # 如果不是200状态码,返回错误信息 if resp.status_code != 200: stop_event.set() error_content = resp.content try: error_json = json.loads(error_content) error_message = error_json.get('error', {}).get('message', str(error_content)) yield f"data: {{\"error\": \"{error_message}\"}}\n\n".encode() except: yield f"data: {{\"error\": \"Request failed with status {resp.status_code}\"}}\n\n".encode() return # 正常处理流式响应 for chunk in resp.iter_content(chunk_size=1024): if chunk: yield chunk while not response_queue.empty(): response_queue.get() except Exception as e: print(f"Error in SSE request: {str(e)}") yield f"data: {{\"error\": \"SSE request failed: {str(e)}\"}}\n\n".encode() finally: stop_event.set() heartbeat_thread.join(timeout=1) response_headers = { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no' } return Response( stream_with_context(generate()), status=200, headers=response_headers ) if __name__ == '__main__': import ssl import os if not os.path.exists('/opt/cert.pem'): print("Generating self-signed certificate...") domains = list(PROXY_DOMAINS.keys()) san_list = ','.join([f'DNS:{domain}' for domain in domains]) os.system(f'openssl req -x509 -newkey rsa:4096 -nodes -out /opt/cert.pem -keyout /opt/key.pem -days 365 -subj "/CN=proxy.local" -addext "subjectAltName={san_list}"') context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context.load_cert_chain('/opt/cert.pem', '/opt/key.pem') app.run(host='0.0.0.0', port=443, ssl_context=context, threaded=True) ================================================ FILE: postgresql/root.crt ================================================ -----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW +1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 /q4AaOeMSQ+2b1tbFfLn -----END CERTIFICATE----- ================================================ FILE: pteer.js ================================================ import fs from "fs"; import path from "path"; import puppeteer from "puppeteer-extra"; import StealthPlugin from "puppeteer-extra-plugin-stealth"; import dotenv from "dotenv"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; puppeteer.use(StealthPlugin()); // Load the default .env file dotenv.config(); if (fs.existsSync(".env.local")) { console.log("Using .env.local file to supply config environment variables"); const envConfig = dotenv.parse(fs.readFileSync(".env.local")); for (const k in envConfig) { process.env[k] = envConfig[k]; } } // 从环境变量解析用户名和密码 const usernames = process.env.USERNAMES.split(","); const passwords = process.env.PASSWORDS.split(","); const loginUrl = process.env.WEBSITE; // 每个浏览器实例之间的延迟时间(毫秒) const delayBetweenInstances = 10000; //随机等待时间 function delayClick(time) { return new Promise(function (resolve) { setTimeout(resolve, time); }); } (async () => { try { if (usernames.length !== passwords.length) { console.log(usernames.length, usernames, passwords.length, passwords); console.log("用户名和密码的数量不匹配!"); return; } // 并发启动浏览器实例进行登录 const loginPromises = usernames.map((username, index) => { const password = passwords[index]; const delay = index * delayBetweenInstances; return new Promise((resolve, reject) => { //其实直接使用await就可以了 setTimeout(() => { launchBrowserForUser(username, password).then(resolve).catch(reject); }, delay); }); }); // 等待所有登录操作完成 await Promise.all(loginPromises); } catch (error) { // 错误处理逻辑 console.error("发生错误:", error); } })(); async function launchBrowserForUser(username, password) { try { const browser = await puppeteer.launch({ headless: process.env.ENVIRONMENT !== "dev", // 当ENVIRONMENT不是'dev'时启用无头模式 args: [ "--no-sandbox", "--disable-setuid-sandbox", "--disable-infobars", "--window-position=0,0", "--ignore-certifcate-errors", "--ignore-certifcate-errors-spki-list", "--disable-web-security", "--disable-features=IsolateOrigins,site-per-process", "--allow-running-insecure-content", "--disable-blink-features=AutomationControlled", "--no-sandbox", "--mute-audio", "--no-zygote", "--no-xshm", "--window-size=1920,1080", "--no-first-run", "--no-default-browser-check", "--disable-dev-shm-usage", "--disable-gpu", "--enable-webgl", "--ignore-certificate-errors", "--lang=en-US,en;q=0.9", "--password-store=basic", "--disable-gpu-sandbox", "--disable-software-rasterizer", "--disable-background-timer-throttling", "--disable-backgrounding-occluded-windows", "--disable-renderer-backgrounding", "--disable-infobars", "--disable-breakpad", "--disable-canvas-aa", "--disable-2d-canvas-clip-aa", "--disable-gl-drawing-for-tests", "--enable-low-end-device-mode", ], //linux需要 defaultViewport: { width: 1280, height: 800, userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36", }, }); const page = await browser.newPage(); // 设置额外的 headers await page.setExtraHTTPHeaders({ "accept-language": "en-US,en;q=0.9", }); // 调用封装的反检测函数 await bypassDetection(page); // 验证 `navigator.webdriver` 属性是否为 undefined const isWebDriverUndefined = await page.evaluate(() => { return `${navigator.webdriver}`; }); console.log("navigator.webdriver is :", isWebDriverUndefined); // 输出应为 true page.on("pageerror", (error) => { console.error(`Page error: ${error.message}`); }); page.on("error", async (error) => { console.error(`Error: ${error.message}`); // 检查是否是 localStorage 的访问权限错误 if ( error.message.includes( "Failed to read the 'localStorage' property from 'Window'" ) ) { console.log("Trying to refresh the page to resolve the issue..."); await page.reload(); // 刷新页面 // 重新尝试你的操作... } }); page.on("console", async (msg) => { console.log("PAGE LOG:", msg.text()); // 使用一个标志变量来检测是否已经刷新过页面 if ( !page._isReloaded && msg.text().includes("the server responded with a status of 429") ) { // 设置标志变量为 true,表示即将刷新页面 page._isReloaded = true; //由于油候脚本它这个时候可能会导航到新的网页,会导致直接执行代码报错,所以使用这个来在每个新网页加载之前来执行 await page.evaluateOnNewDocument(() => { localStorage.setItem("autoLikeEnabled", "false"); }); // 等待一段时间,比如 3 秒 await new Promise((resolve) => setTimeout(resolve, 3000)); console.log("Retrying now..."); // 尝试刷新页面 await page.reload(); } }); //登录操作 await page.goto(loginUrl, { waitUntil: "networkidle0" }); console.log("登录操作"); // 使用XPath查询找到包含"登录"或"login"文本的按钮 await page.evaluate(() => { let loginButton = Array.from(document.querySelectorAll("button")).find( (button) => button.textContent.includes("登录") || button.textContent.includes("login") ); // 如果没有找到,尝试根据类名查找 if (!loginButton) { loginButton = document.querySelector( ".widget-button.btn.btn-primary.btn-small.login-button.btn-icon-text" ); } console.log(loginButton); if (loginButton) { loginButton.click(); console.log("Login button clicked."); } else { console.log("Login button not found."); } }); await login(page, username, password); // 查找具有类名 "avatar" 的 img 元素验证登录是否成功 const avatarImg = await page.$("img.avatar"); if (avatarImg) { console.log("找到avatarImg,登录成功"); // 可以继续对 avatarImg 进行操作,比如获取其属性等 } else { console.log("未找到avatarImg,登录失败"); } //真正执行阅读脚本 // 读取外部脚本文件的内容 const externalScriptPath = path.join( dirname(fileURLToPath(import.meta.url)), "external.js" ); const externalScript = fs.readFileSync(externalScriptPath, "utf8"); // 在每个新的文档加载时执行外部脚本 await page.evaluateOnNewDocument((...args) => { const [scriptToEval] = args; eval(scriptToEval); }, externalScript); // 添加一个监听器来监听每次页面加载完成的事件 page.on("load", async () => { // await page.evaluate(externalScript); //因为这个是在页面加载好之后执行的,而脚本是在页面加载好时刻来判断是否要执行,由于已经加载好了,脚本就不会起作用 }); await page.goto("https://linux.do/t/topic/13716/190"); } catch (err) { console.log(err); } } async function login(page, username, password) { // 等待用户名输入框加载 await page.waitForSelector("#login-account-name"); // 模拟人类在找到输入框后的短暂停顿 await delayClick(500); // 延迟500毫秒 // 清空输入框并输入用户名 await page.click("#login-account-name", { clickCount: 3 }); await page.type("#login-account-name", username, { delay: 100, }); // 输入时在每个按键之间添加额外的延迟 // 等待密码输入框加载 await page.waitForSelector("#login-account-password"); // 模拟人类在输入用户名后的短暂停顿 await delayClick(500); // 清空输入框并输入密码 await page.click("#login-account-password", { clickCount: 3 }); await page.type("#login-account-password", password, { delay: 100, }); // 模拟人类在输入完成后思考的短暂停顿 await delayClick(1000); // 假设登录按钮的ID是'login-button',点击登录按钮 await page.waitForSelector("#login-button"); await delayClick(500); // 模拟在点击登录按钮前的短暂停顿 try { await Promise.all([ page.waitForNavigation({ waitUntil: "domcontentloaded" }), // 等待 页面跳转 DOMContentLoaded 事件 page.click("#login-button"), // 点击登录按钮触发跳转 ]); //注意如果登录失败,这里会一直等待跳转,导致脚本执行失败 } catch (error) { console.error("Navigation timed out in login.:", error); throw new Error("Navigation timed out in login."); } await delayClick(1000); } async function bypassDetection(page) { // 在页面上下文中执行脚本,修改 `navigator.webdriver` 以及其他反检测属性 await page.evaluateOnNewDocument(() => { // Overwrite the `navigator.webdriver` property to return `undefined`. Object.defineProperty(navigator, "webdriver", { get: () => undefined, }); // Pass the Chrome Test. window.chrome = { runtime: {}, // Add more if needed }; // Pass the Permissions Test. const originalQuery = window.navigator.permissions.query; window.navigator.permissions.query = (parameters) => parameters.name === "notifications" ? Promise.resolve({ state: Notification.permission }) : originalQuery(parameters); // Pass the Plugins Length Test. Object.defineProperty(navigator, "plugins", { get: () => [1, 2, 3, 4, 5], // Make plugins array non-empty }); // Pass the Languages Test. Object.defineProperty(navigator, "languages", { get: () => ["en-US", "en"], }); }); } ================================================ FILE: src/browser_retry.js ================================================ ================================================ FILE: src/db.js ================================================ // 多数据库工具 (PostgreSQL + MongoDB + MySQL) import fs from "fs"; import pkg from "pg"; const { Pool } = pkg; import { MongoClient } from "mongodb"; import mysql from "mysql2/promise"; import dotenv from "dotenv"; dotenv.config(); // Utility function to format error information for better logging function formatErrorInfo(error) { if (!error) return { errorMsg: '未知错误', errorCode: '无错误代码' }; let errorMsg = error?.message || error?.toString() || '未知错误'; const errorCode = error?.code || '无错误代码'; // Handle AggregateError specially if (error instanceof AggregateError && error.errors?.length > 0) { const innerError = error.errors[0]; const innerMsg = innerError?.message || innerError?.toString() || '内部错误'; errorMsg = `${errorMsg} (${innerMsg})`; } return { errorMsg, errorCode }; } if (fs.existsSync(".env.local")) { console.log("Using .env.local file to supply config environment variables"); const envConfig = dotenv.parse(fs.readFileSync(".env.local")); for (const k in envConfig) { process.env[k] = envConfig[k]; } } else { console.log( "Using .env file to supply config environment variables, you can create a .env.local file to overwrite defaults, it doesn't upload to git" ); } // 主数据库连接池 (Aiven PostgreSQL) const pool = new Pool({ connectionString: process.env.POSTGRES_URI, max: 5, idleTimeoutMillis: 30000, connectionTimeoutMillis: 15000, // 增加到15秒,适应跨区域网络延迟 ssl: { rejectUnauthorized: false }, }); // 备用数据库连接池 (CockroachDB) const cockroachPool = new Pool({ connectionString: process.env.COCKROACH_URI, max: 3, idleTimeoutMillis: 30000, connectionTimeoutMillis: 15000, // 增加到15秒,适应跨区域网络延迟 ssl: { rejectUnauthorized: false }, }); // 备用数据库连接池 (Neon) // 备用数据库连接池 (Neon) const neonPool = new Pool({ connectionString: process.env.NEON_URI, max: 3, idleTimeoutMillis: 30000, connectionTimeoutMillis: 15000, // 增加到15秒,适应跨区域网络延迟 ssl: { rejectUnauthorized: false }, }); // MySQL 连接池 (Aiven MySQL) let mysqlPool; // 初始化 MySQL 连接池 async function initMySQL() { if (process.env.AIVEN_MYSQL_URI && !mysqlPool) { try { mysqlPool = mysql.createPool({ uri: process.env.AIVEN_MYSQL_URI, connectionLimit: 5, acquireTimeout: 60000, timeout: 60000, ssl: { rejectUnauthorized: false }, }); console.log("✅ MySQL 连接池创建成功"); } catch (error) { console.error("❌ MySQL 连接池创建失败:", error.message); mysqlPool = null; } } return mysqlPool; } // MongoDB 连接 let mongoClient; let mongoDb; // 初始化 MongoDB 连接 async function initMongoDB() { if (process.env.MONGO_URI && !mongoClient) { try { mongoClient = new MongoClient(process.env.MONGO_URI, { maxPoolSize: 5, serverSelectionTimeoutMS: 5000, socketTimeoutMS: 10000, }); // 连接到数据库 await mongoClient.connect(); mongoDb = mongoClient.db("auto_read_posts"); // 使用专门的数据库名 console.log("✅ MongoDB 连接成功"); } catch (error) { console.error("❌ MongoDB 连接失败:", error.message); mongoClient = null; mongoDb = null; } } return mongoDb; } // 所有数据库连接池数组 (PostgreSQL) const allPools = [ //注释以下开启数据库 // { name: "Aiven PostgreSQL", pool: pool }, { name: "CockroachDB", pool: cockroachPool }, // { name: "Neon", pool: neonPool }, ]; // 获取所有数据库连接数组 (包括 MongoDB 和 MySQL) async function getAllDatabases() { //注释以下开启数据库 // const mongoDb = await initMongoDB(); // const mysqlPool = await initMySQL(); return [ ...allPools, ...(mongoDb ? [{ name: "MongoDB", db: mongoDb, type: "mongo" }] : []), ...(mysqlPool ? [{ name: "Aiven MySQL", pool: mysqlPool, type: "mysql" }] : []), ]; } export async function savePosts(posts) { if (!Array.isArray(posts) || posts.length === 0) { console.warn("无效的帖子数据或空数组,跳过保存"); return; } // 验证帖子数据 const validPosts = posts.filter(post => { if (!post || !post.guid || typeof post.guid !== 'string' || post.guid.trim() === '') { console.warn(`跳过无效帖子数据: ${JSON.stringify({title: post?.title, guid: post?.guid})}`); return false; } return true; }); if (validPosts.length === 0) { console.warn("没有有效的帖子数据,跳过保存"); return; } const allDatabases = await getAllDatabases(); if (allDatabases.length === 0) { console.warn("没有可用的数据库连接,跳过保存"); return; } const createTableQuery = ` CREATE TABLE IF NOT EXISTS posts ( id SERIAL PRIMARY KEY, title TEXT, creator TEXT, description TEXT, link TEXT, pubDate TEXT, guid TEXT UNIQUE, guidIsPermaLink TEXT, source TEXT, sourceUrl TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `; const insertQuery = ` INSERT INTO posts (title, creator, description, link, pubDate, guid, guidIsPermaLink, source, sourceUrl) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT (guid) DO NOTHING `; // 并行操作所有数据库 const savePromises = allDatabases.map(async ({ name, pool, db, type }) => { try { console.log(`正在保存到 ${name}...`); if (type === "mongo" && db) { // MongoDB 操作 const collection = db.collection("posts"); // 准备 MongoDB 文档 const mongoDocuments = validPosts.map((post) => ({ title: post.title, creator: post.creator, description: post.description, link: post.link, pubDate: post.pubDate, guid: post.guid, guidIsPermaLink: post.guidIsPermaLink, source: post.source, sourceUrl: post.sourceUrl, created_at: new Date(), })); // 使用 upsert 操作避免重复 const bulkOps = mongoDocuments.map((doc) => ({ updateOne: { filter: { guid: doc.guid }, update: { $set: doc }, upsert: true, }, })); if (bulkOps.length > 0) { await collection.bulkWrite(bulkOps); } } else if (type === "mysql" && pool) { // MySQL 操作 const mysqlCreateTableQuery = ` CREATE TABLE IF NOT EXISTS posts ( id INT AUTO_INCREMENT PRIMARY KEY, title TEXT, creator TEXT, description TEXT, link TEXT, pubDate TEXT, guid VARCHAR(500) UNIQUE, guidIsPermaLink TEXT, source TEXT, sourceUrl TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `; const mysqlInsertQuery = ` INSERT INTO posts (title, creator, description, link, pubDate, guid, guidIsPermaLink, source, sourceUrl) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE title = VALUES(title), creator = VALUES(creator), description = VALUES(description), link = VALUES(link), pubDate = VALUES(pubDate), guidIsPermaLink = VALUES(guidIsPermaLink), source = VALUES(source), sourceUrl = VALUES(sourceUrl) `; // 建表 await pool.execute(mysqlCreateTableQuery); // 插入数据 for (const post of validPosts) { await pool.execute(mysqlInsertQuery, [ post.title, post.creator, post.description, post.link, post.pubDate, post.guid, post.guidIsPermaLink, post.source, post.sourceUrl, ]); } } else if (pool) { // PostgreSQL 操作 // 建表 await pool.query(createTableQuery); // 插入数据 for (const post of validPosts) { await pool.query(insertQuery, [ post.title, post.creator, post.description, post.link, post.pubDate, post.guid, post.guidIsPermaLink, post.source, post.sourceUrl, ]); } } console.log(`✅ ${name} 保存成功 (${validPosts.length} 条记录)`); return { name, success: true }; } catch (error) { const { errorMsg, errorCode } = formatErrorInfo(error); console.error(`❌ ${name} 保存失败 [${errorCode}]:`, errorMsg); return { name, success: false, error: errorMsg }; } }); // 等待所有数据库操作完成 const results = await Promise.allSettled(savePromises); // 统计结果 const successCount = results.filter( (result) => result.status === "fulfilled" && result.value.success ).length; console.log( `数据库保存结果: ${successCount}/${allDatabases.length} 个数据库保存成功` ); // 如果至少有一个数据库保存成功,就认为操作成功 if (successCount === 0) { throw new Error("所有数据库保存都失败了"); } } export async function isGuidExists(guid) { // 验证输入参数 if (!guid || typeof guid !== 'string' || guid.trim() === '') { console.warn(`无效的GUID参数: ${JSON.stringify(guid)}`); return false; } // 优先查询主数据库 (CockroachDB) try { const res = await cockroachPool.query( "SELECT 1 FROM posts WHERE guid = $1 LIMIT 1", [guid] ); // console.log("isGuidExists查询结果:", res.rows); 存在的返回[ { '?column?': 1 } ] if (res.rowCount > 0) { return true; } } catch (error) { const { errorMsg, errorCode } = formatErrorInfo(error); console.warn(`主数据库查询GUID失败 [${errorCode}]: ${errorMsg}`); } // 如果主数据库查询失败或未找到,尝试查询备用数据库 const allDatabases = await getAllDatabases(); for (const { name, pool, db, type } of allDatabases.slice(1)) { // 跳过主数据库 try { if (type === "mongo" && db) { // MongoDB 查询 const collection = db.collection("posts"); const count = await collection.countDocuments( { guid: guid }, { limit: 1 } ); if (count > 0) { console.log(`在备用数据库 ${name} 中找到GUID: ${guid}`); return true; } } else if (type === "mysql" && pool) { // MySQL 查询 const [rows] = await pool.execute( "SELECT 1 FROM posts WHERE guid = ? LIMIT 1", [guid] ); if (rows.length > 0) { console.log(`在备用数据库 ${name} 中找到GUID: ${guid}`); return true; } } else if (pool) { // PostgreSQL 查询 const res = await pool.query( "SELECT 1 FROM posts WHERE guid = $1 LIMIT 1", [guid] ); if (res.rowCount > 0) { console.log(`在备用数据库 ${name} 中找到GUID: ${guid}`); return true; } } } catch (error) { const { errorMsg, errorCode } = formatErrorInfo(error); console.warn(`备用数据库 ${name} 查询GUID失败 [${errorCode}]: ${errorMsg}`); } } return false; } // 测试所有数据库连接 export async function testAllConnections() { console.log("正在测试所有数据库连接..."); const allDatabases = await getAllDatabases(); const testPromises = allDatabases.map(async ({ name, pool, db, type }) => { try { if (type === "mongo" && db) { // 测试 MongoDB 连接 await db.admin().ping(); } else if (type === "mysql" && pool) { // 测试 MySQL 连接 await pool.execute("SELECT 1"); } else if (pool) { // 测试 PostgreSQL 连接 await pool.query("SELECT 1"); } console.log(`✅ ${name} 连接正常`); return { name, connected: true }; } catch (error) { const { errorMsg, errorCode } = formatErrorInfo(error); console.error(`❌ ${name} 连接失败 [${errorCode}]:`, errorMsg); return { name, connected: false, error: errorMsg }; } }); const results = await Promise.allSettled(testPromises); const connectedCount = results.filter( (result) => result.status === "fulfilled" && result.value.connected ).length; console.log( `数据库连接测试结果: ${connectedCount}/${allDatabases.length} 个数据库连接正常` ); return results; } // 获取所有数据库的统计信息 export async function getAllDatabaseStats() { console.log("正在获取所有数据库统计信息..."); const allDatabases = await getAllDatabases(); const statsPromises = allDatabases.map(async ({ name, pool, db, type }) => { try { let stats; if (type === "mongo" && db) { // MongoDB 统计 const collection = db.collection("posts"); const totalPosts = await collection.countDocuments(); const latestPost = await collection.findOne( {}, { sort: { created_at: -1 } } ); stats = { name, totalPosts, latestPost: latestPost?.created_at || null, status: "healthy", }; } else if (type === "mysql" && pool) { // MySQL 统计 const [countResult] = await pool.execute( "SELECT COUNT(*) as count FROM posts" ); const [latestResult] = await pool.execute( "SELECT created_at FROM posts ORDER BY created_at DESC LIMIT 1" ); stats = { name, totalPosts: parseInt(countResult[0].count), latestPost: latestResult[0]?.created_at || null, status: "healthy", }; } else if (pool) { // PostgreSQL 统计 const countResult = await pool.query( "SELECT COUNT(*) as count FROM posts" ); const latestResult = await pool.query( "SELECT created_at FROM posts ORDER BY created_at DESC LIMIT 1" ); stats = { name, totalPosts: parseInt(countResult.rows[0].count), latestPost: latestResult.rows[0]?.created_at || null, status: "healthy", }; } console.log(`📊 ${name}: ${stats.totalPosts} 条记录`); return stats; } catch (error) { const { errorMsg, errorCode } = formatErrorInfo(error); console.error(`❌ ${name} 统计信息获取失败 [${errorCode}]:`, errorMsg); return { name, totalPosts: -1, latestPost: null, status: "error", error: errorMsg, }; } }); const results = await Promise.allSettled(statsPromises); return results.map((result) => result.status === "fulfilled" ? result.value : result.reason ); } // 关闭所有数据库连接 export async function closeAllConnections() { console.log("正在关闭所有数据库连接..."); const allDatabases = await getAllDatabases(); const closePromises = allDatabases.map(async ({ name, pool, type }) => { try { if (type === "mongo") { // 关闭 MongoDB 连接 if (mongoClient) { await mongoClient.close(); mongoClient = null; mongoDb = null; } } else if (type === "mysql" && pool) { // 关闭 MySQL 连接 await pool.end(); mysqlPool = null; } else if (pool) { // 关闭 PostgreSQL 连接 await pool.end(); } console.log(`✅ ${name} 连接已关闭`); } catch (error) { const { errorMsg, errorCode } = formatErrorInfo(error); console.error(`❌ ${name} 连接关闭失败 [${errorCode}]:`, errorMsg); } }); await Promise.allSettled(closePromises); console.log("所有数据库连接关闭完成"); } // 保存话题 JSON 数据的函数 export async function saveTopicData(topicData) { if (!topicData || !topicData.id) { console.warn("无效的话题数据,跳过保存"); return; } const allDatabases = await getAllDatabases(); const createTopicsTableQuery = ` CREATE TABLE IF NOT EXISTS topics ( id SERIAL PRIMARY KEY, topic_id INTEGER UNIQUE, title TEXT, slug TEXT, posts_count INTEGER, created_at TIMESTAMP, last_posted_at TIMESTAMP, views INTEGER, like_count INTEGER, category_id INTEGER, tags TEXT[], raw_data JSONB, saved_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `; const insertTopicQuery = ` INSERT INTO topics (topic_id, title, slug, posts_count, created_at, last_posted_at, views, like_count, category_id, tags, raw_data) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) ON CONFLICT (topic_id) DO UPDATE SET title = EXCLUDED.title, slug = EXCLUDED.slug, posts_count = EXCLUDED.posts_count, last_posted_at = EXCLUDED.last_posted_at, views = EXCLUDED.views, like_count = EXCLUDED.like_count, tags = EXCLUDED.tags, raw_data = EXCLUDED.raw_data, saved_at = CURRENT_TIMESTAMP `; const savePromises = allDatabases.map(async ({ name, pool, db, type }) => { try { console.log(`正在保存话题数据到 ${name}...`); if (type === "mongo" && db) { // MongoDB 操作 const collection = db.collection("topics"); const mongoDocument = { topic_id: topicData.id, title: topicData.title, slug: topicData.slug, posts_count: topicData.posts_count, created_at: new Date(topicData.created_at), last_posted_at: topicData.last_posted_at ? new Date(topicData.last_posted_at) : null, views: topicData.views, like_count: topicData.like_count, category_id: topicData.category_id, tags: topicData.tags || [], raw_data: topicData, saved_at: new Date(), }; await collection.updateOne( { topic_id: topicData.id }, { $set: mongoDocument }, { upsert: true } ); } else if (type === "mysql" && pool) { // MySQL 操作 const mysqlCreateTableQuery = ` CREATE TABLE IF NOT EXISTS topics ( id INT AUTO_INCREMENT PRIMARY KEY, topic_id INT UNIQUE, title TEXT, slug TEXT, posts_count INT, created_at DATETIME, last_posted_at DATETIME NULL, views INT, like_count INT, category_id INT, tags JSON, raw_data JSON, saved_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `; const mysqlInsertQuery = ` INSERT INTO topics (topic_id, title, slug, posts_count, created_at, last_posted_at, views, like_count, category_id, tags, raw_data) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE title = VALUES(title), slug = VALUES(slug), posts_count = VALUES(posts_count), last_posted_at = VALUES(last_posted_at), views = VALUES(views), like_count = VALUES(like_count), tags = VALUES(tags), raw_data = VALUES(raw_data), saved_at = CURRENT_TIMESTAMP `; await pool.execute(mysqlCreateTableQuery); // 转换日期格式为 MySQL 兼容格式 (YYYY-MM-DD HH:MM:SS) const formatDateForMySQL = (dateString) => { if (!dateString) return null; const date = new Date(dateString); return date.toISOString().slice(0, 19).replace('T', ' '); }; await pool.execute(mysqlInsertQuery, [ topicData.id, topicData.title, topicData.slug, topicData.posts_count, formatDateForMySQL(topicData.created_at), formatDateForMySQL(topicData.last_posted_at), topicData.views, topicData.like_count, topicData.category_id, JSON.stringify(topicData.tags || []), JSON.stringify(topicData), ]); } else if (pool) { // PostgreSQL 操作 await pool.query(createTopicsTableQuery); await pool.query(insertTopicQuery, [ topicData.id, topicData.title, topicData.slug, topicData.posts_count, topicData.created_at, topicData.last_posted_at, topicData.views, topicData.like_count, topicData.category_id, topicData.tags || [], topicData, ]); } console.log(`✅ ${name} 话题数据保存成功 (话题ID: ${topicData.id})`); return { name, success: true }; } catch (error) { const { errorMsg, errorCode } = formatErrorInfo(error); console.error(`❌ ${name} 话题数据保存失败 [${errorCode}]:`, errorMsg); return { name, success: false, error: errorMsg }; } }); const results = await Promise.allSettled(savePromises); const successCount = results.filter( (result) => result.status === "fulfilled" && result.value.success ).length; console.log( `话题数据保存结果: ${successCount}/${allDatabases.length} 个数据库保存成功` ); if (successCount === 0) { throw new Error("所有数据库话题数据保存都失败了"); } } ================================================ FILE: src/db.test.js ================================================ import { savePosts, isGuidExists } from "./db.js"; async function runDbTest() { const testGuid = "test-guid-" + Date.now(); const testPost = { title: "GitHub Action 测试标题" + Date.now(), creator: "测试作者", description: "测试内容", link: "https://example.com", pubDate: new Date().toISOString(), guid: testGuid, guidIsPermaLink: "false", source: "test-source", sourceUrl: "https://source.com" }; // 1. 保存帖子 await savePosts([testPost]); console.log("savePosts 测试通过"); // 2. 检查 guid 是否存在 const exists = await isGuidExists(testGuid); if (exists) { console.log("isGuidExists 测试通过"); process.exit(0); } else { console.error("isGuidExists 测试失败"); process.exit(1); } } runDbTest().catch((e) => { console.error(e); process.exit(1); }); ================================================ FILE: src/format_for_telegram.js ================================================ import fs from 'fs'; const inputFilePath = 'parsed_rss_data.json'; const outputFilePath = 'telegram_message.txt'; // Function to strip HTML tags function stripHtml(html) { if (!html) return ''; // First, try to extract content from the first

tag const pMatch = html.match(/

(.*?)<\/p>/i); if (pMatch && pMatch[1]) { // Remove any nested tags within the first

return pMatch[1].replace(/<[^>]+>/g, '').trim(); } // Fallback: remove all HTML tags if no

or if it's empty return html.replace(/<[^>]+>/g, '').trim(); } fs.readFile(inputFilePath, 'utf8', (err, data) => { if (err) { console.error(`Failed to read ${inputFilePath}:`, err); return; } try { const jsonData = JSON.parse(data); let telegramMessage = `*New Posts from "${jsonData.channelTitle}"*\n`; telegramMessage += `${jsonData.channelLink}\n\n`; telegramMessage += `Channel Description:\n${jsonData.channelDescription.replace(/\*\*(.*?)\*\*/g, '*$1*')}\n\n`; // Keep markdown bold telegramMessage += "---\n\n"; if (jsonData.items && Array.isArray(jsonData.items)) { jsonData.items.forEach(item => { const descriptionSnippet = stripHtml(item.description); telegramMessage += `*${item.title}*\n`; telegramMessage += `By: ${item.creator}\n`; if (descriptionSnippet) { telegramMessage += `Content: ${descriptionSnippet}\n`; } telegramMessage += `Read more: ${item.link}\n`; telegramMessage += "---\n\n"; }); } fs.writeFile(outputFilePath, telegramMessage, (writeErr) => { if (writeErr) { console.error(`Failed to write ${outputFilePath}:`, writeErr); return; } console.log(`Successfully formatted message and wrote to ${outputFilePath}`); }); } catch (parseErr) { console.error(`Failed to parse JSON from ${inputFilePath}:`, parseErr); } }); ================================================ FILE: src/parse_rss.js ================================================ import xml2js from "xml2js"; import { savePosts, isGuidExists } from "./db.js"; // 生成适合Telegram的文本内容,去掉“阅读完整话题”和“Read full topic”内容 function cleanDescription(desc) { if (!desc) return "无内容"; // 去掉HTML标签 let text = desc.replace(/<[^>]+>/g, ""); // 去掉特定内容 text = text.replace(/阅读完整话题/g, ""); text = text.replace(/Read full topic/gi, ""); // 去掉多余的空白字符 text = text.replace(/\s+/g, " "); return text.trim() || "无内容"; } /** * 解析RSS XML并返回适合Telegram的文本内容 * @param {string} xmlData - RSS XML字符串 * @returns {Promise} 适合Telegram的文本内容 */ export async function parseRss(xmlData) { const parser = new xml2js.Parser({ explicitArray: false, trim: true }); const result = await parser.parseStringPromise(xmlData); const channel = result.rss.channel; const items = channel.item; const extractedItems = []; if (items && Array.isArray(items)) { for (const item of items) { extractedItems.push({ title: item.title, creator: item["dc:creator"], description: item.description, link: item.link, pubDate: item.pubDate, guid: item.guid?._, guidIsPermaLink: item.guid?.$?.isPermaLink, source: item.source?._, sourceUrl: item.source?.$?.url, }); } } else if (items) { extractedItems.push({ title: items.title, creator: items["dc:creator"], description: items.description, link: items.link, pubDate: items.pubDate, guid: items.guid?._, guidIsPermaLink: items.guid?.$?.isPermaLink, source: items.source?._, sourceUrl: items.source?.$?.url, }); } // items反转,最新的内容排在最前面 const reversedItems = extractedItems.reverse(); // 先过滤出新的帖子(不存在的) const newItems = []; for (const item of reversedItems) { if (item.guid) { const exists = await isGuidExists(item.guid); if (exists) { console.warn(`GUID ${item.guid} 已存在,跳过。`); continue; } } newItems.push(item); } // 如果没有新帖子,直接返回空内容 if (newItems.length === 0) { console.log("没有新帖子需要处理"); return ""; } // 先保存新帖子到数据库 try { await savePosts(newItems); console.log(`成功保存 ${newItems.length} 个新帖子到数据库`); } catch (e) { console.error("保存帖子到数据库失败:", e); // 如果保存失败,不推送任何内容 return ""; } // 保存成功后,构建推送内容 const textContentArr = []; for (let idx = 0; idx < newItems.length; idx++) { const item = newItems[idx]; const isFirst = idx === 0; const isLast = idx === newItems.length - 1; const msg = [ isFirst ? `标题: ${item.title}` : "", `作者: ${item.creator}`, isFirst ? `内容: ${cleanDescription(item.description)}` : `回复: ${cleanDescription(item.description)}`, `时间: ${item.pubDate}`, isLast ? `链接: ${item.link}` : "", "", ] .filter(Boolean) .join("\n") .trim(); if (msg) { textContentArr.push(msg); } } const textContent = textContentArr.filter(Boolean).join("\n"); return textContent; } ================================================ FILE: src/proxy_config.js ================================================ /** * 代理配置模块 * 支持HTTP、HTTPS、SOCKS代理 */ // 代理配置类型枚举 export const ProxyTypes = { HTTP: 'http', HTTPS: 'https', SOCKS4: 'socks4', SOCKS5: 'socks5' }; /** * 解析代理URL * @param {string} proxyUrl - 代理URL (http://user:pass@host:port 或 socks5://user:pass@host:port) * @returns {Object} 解析后的代理配置 */ export function parseProxyUrl(proxyUrl) { if (!proxyUrl) return null; try { const url = new URL(proxyUrl); const config = { type: url.protocol.replace(':', ''), host: url.hostname, port: parseInt(url.port), }; if (url.username) { config.username = url.username; } if (url.password) { config.password = url.password; } return config; } catch (error) { console.error('代理URL解析错误:', error.message); return null; } } /** * 获取代理配置 * @returns {Object|null} 代理配置对象 */ export function getProxyConfig() { const proxyUrl = process.env.PROXY_URL; const proxyHost = process.env.PROXY_HOST; const proxyPort = process.env.PROXY_PORT; const proxyType = process.env.PROXY_TYPE || 'http'; const proxyUsername = process.env.PROXY_USERNAME; const proxyPassword = process.env.PROXY_PASSWORD; // 优先使用PROXY_URL if (proxyUrl) { return parseProxyUrl(proxyUrl); } // 使用分离的配置参数 if (proxyHost && proxyPort) { const config = { type: proxyType, host: proxyHost, port: parseInt(proxyPort), }; if (proxyUsername) { config.username = proxyUsername; } if (proxyPassword) { config.password = proxyPassword; } return config; } return null; } /** * 为Puppeteer生成代理配置 * @param {Object} proxyConfig - 代理配置 * @returns {Object} Puppeteer代理配置 */ export function getPuppeteerProxyArgs(proxyConfig) { if (!proxyConfig) return []; const args = []; if (proxyConfig.type === 'http' || proxyConfig.type === 'https') { args.push(`--proxy-server=${proxyConfig.type}://${proxyConfig.host}:${proxyConfig.port}`); } else if (proxyConfig.type === 'socks4' || proxyConfig.type === 'socks5') { args.push(`--proxy-server=${proxyConfig.type}://${proxyConfig.host}:${proxyConfig.port}`); } return args; } /** * 为Playwright生成代理配置 * @param {Object} proxyConfig - 代理配置 * @returns {Object} Playwright代理配置 */ export function getPlaywrightProxyConfig(proxyConfig) { if (!proxyConfig) return null; const config = { server: `${proxyConfig.type}://${proxyConfig.host}:${proxyConfig.port}`, }; if (proxyConfig.username && proxyConfig.password) { config.username = proxyConfig.username; config.password = proxyConfig.password; } return config; } /** * 测试代理连接 * @param {Object} proxyConfig - 代理配置 * @returns {Promise} 是否连接成功 */ export async function testProxyConnection(proxyConfig) { if (!proxyConfig) return false; try { const { default: fetch } = await import('node-fetch'); const { HttpsProxyAgent } = await import('https-proxy-agent'); const { SocksProxyAgent } = await import('socks-proxy-agent'); let agent; if (proxyConfig.type === 'http' || proxyConfig.type === 'https') { const proxyUrl = proxyConfig.username && proxyConfig.password ? `${proxyConfig.type}://${proxyConfig.username}:${proxyConfig.password}@${proxyConfig.host}:${proxyConfig.port}` : `${proxyConfig.type}://${proxyConfig.host}:${proxyConfig.port}`; agent = new HttpsProxyAgent(proxyUrl); } else if (proxyConfig.type === 'socks4' || proxyConfig.type === 'socks5') { const proxyUrl = proxyConfig.username && proxyConfig.password ? `${proxyConfig.type}://${proxyConfig.username}:${proxyConfig.password}@${proxyConfig.host}:${proxyConfig.port}` : `${proxyConfig.type}://${proxyConfig.host}:${proxyConfig.port}`; agent = new SocksProxyAgent(proxyUrl); } const response = await fetch('https://httpbin.org/ip', { agent, timeout: 10000 }); if (response.ok) { const data = await response.json(); console.log('代理测试成功,IP地址:', data.origin); return true; } return false; } catch (error) { console.error('代理测试失败:', error.message); return false; } } /** * 获取当前IP地址(不使用代理) */ export async function getCurrentIP() { try { const { default: fetch } = await import('node-fetch'); const response = await fetch('https://httpbin.org/ip', { timeout: 10000 }); if (response.ok) { const data = await response.json(); return data.origin; } } catch (error) { console.error('获取IP地址失败:', error.message); } return null; } export default { ProxyTypes, parseProxyUrl, getProxyConfig, getPuppeteerProxyArgs, getPlaywrightProxyConfig, testProxyConnection, getCurrentIP }; ================================================ FILE: src/topic_data.js ================================================ // 话题数据获取和保存模块 import { saveTopicData } from './db.js'; // 从话题页面提取话题ID export function extractTopicIdFromUrl(url) { const match = url.match(/\/t\/topic\/(\d+)/); return match ? parseInt(match[1]) : null; } // 获取话题JSON数据 export async function fetchTopicJson(page, topicId) { try { const jsonUrl = `https://linux.do/t/${topicId}.json`; console.log(`正在获取话题JSON数据: ${jsonUrl}`); // 在新页面中获取JSON数据 const jsonPage = await page.browser().newPage(); await jsonPage.goto(jsonUrl, { waitUntil: "domcontentloaded", timeout: 15000, }); // 等待页面加载完成 await new Promise((r) => setTimeout(r, 1000)); // 获取JSON内容 const jsonText = await jsonPage.evaluate(() => { // 尝试从pre标签获取内容(某些浏览器显示JSON的方式) const pre = document.querySelector('pre'); if (pre) { return pre.textContent; } // 否则获取body的文本内容 return document.body.textContent; }); await jsonPage.close(); if (!jsonText) { console.warn(`无法获取话题 ${topicId} 的JSON数据`); return null; } // 解析JSON const topicData = JSON.parse(jsonText); console.log(`✅ 成功获取话题 ${topicId} 的JSON数据: ${topicData.title}`); return topicData; } catch (error) { console.error(`获取话题 ${topicId} JSON数据失败:`, error.message); return null; } } // 处理话题数据并保存到数据库 export async function processAndSaveTopicData(page, url) { try { const topicId = extractTopicIdFromUrl(url); if (!topicId) { console.warn(`无法从URL提取话题ID: ${url}`); return; } // 获取JSON数据 const topicData = await fetchTopicJson(page, topicId); if (!topicData) { return; } // 保存到数据库 await saveTopicData(topicData); console.log(`✅ 话题 ${topicId} 数据处理完成`); } catch (error) { console.error(`处理话题数据失败:`, error.message); } } ================================================ FILE: telegram_message.txt ================================================ *New Posts from "长期在线回收iPhone"* https://linux.do/t/topic/525305 Channel Description: 各位佬有二手iPhone想出手的,可以来我这里问问价。 专业二手iPhone贩子,长期在线回收二手iPhone,只要能开机都可以回收。 但是 id机,监管机不收。 所有交易都走闲鱼,安全靠谱。 闲鱼店铺名称: *东莞乐回收数码* --- *长期在线回收iPhone* By: KotoriMinami Content: 好的ok Read more: https://linux.do/t/topic/525305/293 --- *长期在线回收iPhone* By: 鲨鱼辣椒 Content: 不到 2个月的 ip16 256G,无磕碰多少钱 Read more: https://linux.do/t/topic/525305/292 --- *长期在线回收iPhone* By: Kubrick Content: 挂小红书上 高价卖给小仙女 现在低像素手机 小仙女最爱 Read more: https://linux.do/t/topic/525305/291 --- *长期在线回收iPhone* By: shenpvip Content: iphone 6s要吗 Read more: https://linux.do/t/topic/525305/290 --- *长期在线回收iPhone* By: 二手iPhone贩子 Content: 收, 收的就是 8p, 只要是全原装, 能开机就收。 Read more: https://linux.do/t/topic/525305/289 --- *长期在线回收iPhone* By: 二手iPhone贩子 Content: 全原装 600 。 Read more: https://linux.do/t/topic/525305/288 --- *长期在线回收iPhone* By: 沐风 Content: 不知道iphone8还收不,俺的iphone8除了电池不行其他的都还好,不过估计也值不了几个钱了 Read more: https://linux.do/t/topic/525305/287 --- *长期在线回收iPhone* By: sunny22 Content: 国行xr 128的多少钱收 Read more: https://linux.do/t/topic/525305/286 --- *长期在线回收iPhone* By: 二手iPhone贩子 Content: 我这边 1450 直接秒了。 Read more: https://linux.do/t/topic/525305/285 --- *长期在线回收iPhone* By: 二手iPhone贩子 Content: 无磕碰, 4800 回收。 Read more: https://linux.do/t/topic/525305/284 --- *长期在线回收iPhone* By: 二手iPhone贩子 Content: 6月初 13p 128后爆,也能收个 2k 。 还行吧。 Read more: https://linux.do/t/topic/525305/283 --- *长期在线回收iPhone* By: 二手iPhone贩子 Content: Read full topic Read more: https://linux.do/t/topic/525305/282 --- *长期在线回收iPhone* By: 二手iPhone贩子 Content: 美版有锁就自己留着吧,没啥回收价值了。 美版无锁还能值个 400 左右。 自己挂闲鱼看看垃圾佬能不能出到 650 Read more: https://linux.do/t/topic/525305/281 --- *长期在线回收iPhone* By: Those Content: 白色国行12pro128 无拆修,电池83,外观和屏幕完美,估价多少? Read more: https://linux.do/t/topic/525305/280 --- *长期在线回收iPhone* By: snow7y Content: 佬友,15pro 原色 256g 电池90,边框屏幕很好,无修,值多少 Read more: https://linux.do/t/topic/525305/279 --- *长期在线回收iPhone* By: didiBird Content: 13pro128 背壳碎,我就卖了 1988还包了邮费,黄鱼还扣了1%手续费。 不过比转转上门多了600,转转只给1300,我没卖,黄鱼自己卖了,一天就出掉了 Read more: https://linux.do/t/topic/525305/278 --- *长期在线回收iPhone* By: KotoriMinami Content: 佬,xs64g,电池80以下,人脸识别的深感摄像投进过一次水坏了,多少钱,还有没有出的必要 Read more: https://linux.do/t/topic/525305/277 --- *长期在线回收iPhone* By: 潇7 Content: 是的,换国内屏 Read more: https://linux.do/t/topic/525305/276 --- *长期在线回收iPhone* By: 二手iPhone贩子 Content: 3200 回收。 Read more: https://linux.do/t/topic/525305/275 --- *长期在线回收iPhone* By: 二手iPhone贩子 Content: 应该有个 150 左右。 想卖的话 ,给你打包一起收了。 Read more: https://linux.do/t/topic/525305/274 --- *长期在线回收iPhone* By: 熊猫竹子 Content: 国行 14 Pro 128G,电池健康 78% ,无拆无修,成色 9成 - 95 成新吧, 自用的,还有购买时候的原盒 值多少 Read more: https://linux.do/t/topic/525305/273 --- *长期在线回收iPhone* By: 害你的猪 Content: 我的iPhone5s 16G和iPhone7 32G还能开机。不知道能卖多少 Read more: https://linux.do/t/topic/525305/272 --- *长期在线回收iPhone* By: 二手iPhone贩子 Content: 国行 保修 280+ , 7000 走闲鱼。 Read more: https://linux.do/t/topic/525305/271 --- *长期在线回收iPhone* By: Jiushuself Content: 充电27次 Read more: https://linux.do/t/topic/525305/270 --- *长期在线回收iPhone* By: 二手iPhone贩子 Content: hone16promax 256G多少米,使用一个月 Read more: https://linux.do/t/topic/525305/269 --- ================================================ FILE: test_linuxdo.js ================================================ import { connect } from "puppeteer-real-browser"; while (true) { console.log("Start of test.js"); const { page, browser } = await connect({ headless: 'auto', args: [], customConfig: {}, skipTarget: [], fingerprint: false, turnstile: true, connectOption: {}, }); var cl = setInterval(() => { page .screenshot({ path: "example.png", fullPage: true }) .catch((e) => console.log(e.message)); }, 1000); console.log("Connected to browser"); // https://nopecha.com/demo/cloudflare https://linux.do await page.goto("https://linux.do", { waitUntil: "domcontentloaded", }); console.log("Navigated to page"); await page.waitForSelector(".link_row", { timeout: 60000, }); // await page.screenshot({ path: 'example.png' }); clearInterval(cl); await browser.close(); console.log("End of test.js"); } ================================================ FILE: test_multi_db.js ================================================ // 多数据库功能测试脚本 (PostgreSQL + MongoDB + MySQL) import { testAllConnections, getAllDatabaseStats, savePosts, isGuidExists, closeAllConnections, } from "./src/db.js"; async function testMultiDatabase() { console.log("🚀 开始多数据库功能测试 (PostgreSQL + MongoDB + MySQL)...\n"); try { // 1. 测试所有数据库连接 console.log("=== 1. 测试数据库连接 ==="); await testAllConnections(); console.log(""); // 2. 获取当前统计信息 console.log("=== 2. 获取当前统计信息 ==="); const statsBefore = await getAllDatabaseStats(); console.log(""); // 3. 测试保存数据 console.log("=== 3. 测试保存数据 ==="); const testPosts = [ { title: "测试帖子标题 - 多数据库测试 (含MongoDB+MySQL)", creator: "test_user", description: "这是一个多数据库功能测试帖子,包括 PostgreSQL、MongoDB 和 MySQL", link: "https://linux.do/t/topic/test-mysql-123", pubDate: new Date().toISOString(), guid: `test-multi-db-mysql-${Date.now()}`, guidIsPermaLink: "false", source: "Linux.do", sourceUrl: "https://linux.do", }, ]; await savePosts(testPosts); console.log(""); // 4. 测试GUID存在性检查 console.log("=== 4. 测试GUID存在性检查 ==="); const testGuid = testPosts[0].guid; const exists = await isGuidExists(testGuid); console.log(`GUID ${testGuid} 存在性: ${exists ? "✅ 存在" : "❌ 不存在"}`); console.log(""); // 5. 获取更新后的统计信息 console.log("=== 5. 获取更新后的统计信息 ==="); const statsAfter = await getAllDatabaseStats(); console.log(""); // 6. 比较统计信息 console.log("=== 6. 统计信息对比 ==="); statsBefore.forEach((beforeStat, index) => { const afterStat = statsAfter[index]; if (beforeStat.status === "healthy" && afterStat.status === "healthy") { const increase = afterStat.totalPosts - beforeStat.totalPosts; console.log( `${beforeStat.name}: ${beforeStat.totalPosts} → ${afterStat.totalPosts} (+${increase})` ); } }); console.log("\n✅ 多数据库功能测试完成 (PostgreSQL + MongoDB + MySQL)"); } catch (error) { console.error("❌ 测试过程中发生错误:", error); } finally { // 关闭所有连接 console.log("\n=== 关闭数据库连接 ==="); await closeAllConnections(); } } // 运行测试 testMultiDatabase().catch(console.error); ================================================ FILE: test_proxy.js ================================================ #!/usr/bin/env node /** * 代理测试工具 * 用于测试代理服务器的连通性和配置正确性 */ import dotenv from "dotenv"; import { getProxyConfig, testProxyConnection, getCurrentIP, parseProxyUrl, } from "./src/proxy_config.js"; // 加载环境变量 dotenv.config(); async function main() { console.log("🔍 代理配置测试工具"); console.log("=================="); // 获取当前IP console.log("\n📍 当前网络状态:"); const currentIP = await getCurrentIP(); if (currentIP) { console.log(`✅ 当前IP地址: ${currentIP}`); } else { console.log("❌ 无法获取当前IP地址"); } // 检查代理配置 console.log("\n🔧 代理配置检查:"); const proxyConfig = getProxyConfig(); if (!proxyConfig) { console.log("❌ 未配置代理服务器"); console.log("\n💡 配置方法:"); console.log("1. 设置环境变量 PROXY_URL,例如:"); console.log(" PROXY_URL=http://username:password@proxy.example.com:8080"); console.log( " PROXY_URL=socks5://username:password@proxy.example.com:1080" ); console.log("\n2. 或者分别设置:"); console.log(" PROXY_TYPE=http"); console.log(" PROXY_HOST=proxy.example.com"); console.log(" PROXY_PORT=8080"); console.log(" PROXY_USERNAME=your_username"); console.log(" PROXY_PASSWORD=your_password"); return; } console.log(`✅ 代理类型: ${proxyConfig.type}`); console.log(`✅ 代理地址: ${proxyConfig.host}:${proxyConfig.port}`); if (proxyConfig.username) { console.log(`✅ 认证用户: ${proxyConfig.username}`); console.log( `✅ 密码设置: ${"*".repeat(proxyConfig.password?.length || 0)}` ); } else { console.log("ℹ️ 无需认证"); } // 测试代理连接 console.log("\n🚀 代理连接测试:"); console.log("正在测试代理连接..."); const startTime = Date.now(); const isWorking = await testProxyConnection(proxyConfig); const endTime = Date.now(); if (isWorking) { console.log(`✅ 代理连接成功! (耗时: ${endTime - startTime}ms)`); console.log("🎉 代理服务器工作正常,可以使用"); } else { console.log(`❌ 代理连接失败! (耗时: ${endTime - startTime}ms)`); console.log("\n🔧 故障排查建议:"); console.log("1. 检查代理服务器地址和端口是否正确"); console.log("2. 检查用户名和密码是否正确"); console.log("3. 检查代理服务器是否在线"); console.log("4. 检查网络防火墙设置"); console.log("5. 尝试其他代理服务器"); } // 环境变量检查 console.log("\n📋 环境变量检查:"); const envVars = [ "PROXY_URL", "PROXY_TYPE", "PROXY_HOST", "PROXY_PORT", "PROXY_USERNAME", "PROXY_PASSWORD", ]; envVars.forEach((varName) => { const value = process.env[varName]; if (value) { if (varName.includes("PASSWORD")) { console.log(`✅ ${varName}: ${"*".repeat(value.length)}`); } else { console.log(`✅ ${varName}: ${value}`); } } else { console.log(`❌ ${varName}: 未设置`); } }); } // 如果是直接运行此脚本 if (import.meta.url === `file://${process.argv[1]}`) { main().catch(console.error); } export default main; ================================================ FILE: test_topic_data.js ================================================ import { extractTopicIdFromUrl, processAndSaveTopicData } from './src/topic_data.js'; import fs from 'fs'; import dotenv from 'dotenv'; // Load environment variables dotenv.config(); if (fs.existsSync(".env.local")) { const envConfig = dotenv.parse(fs.readFileSync(".env.local")); for (const k in envConfig) { process.env[k] = envConfig[k]; } } async function testTopicData() { try { // Test URL extraction const testUrls = [ 'https://linux.do/t/topic/710103', 'https://linux.do/t/topic/525305/100', 'https://linux.do/t/topic/13716/790' ]; console.log('测试话题ID提取:'); for (const url of testUrls) { const topicId = extractTopicIdFromUrl(url); console.log(`URL: ${url} -> Topic ID: ${topicId}`); } // Test with an invalid URL const invalidUrl = 'https://linux.do/latest'; const invalidTopicId = extractTopicIdFromUrl(invalidUrl); console.log(`Invalid URL: ${invalidUrl} -> Topic ID: ${invalidTopicId}`); console.log('\n✅ 话题ID提取测试完成'); // Note: We're not testing the full processAndSaveTopicData function here // because it requires a Puppeteer page instance which is complex to set up console.log('\n📝 完整的话题数据抓取功能需要在实际运行环境中测试'); } catch (error) { console.error('测试失败:', error); } } testTopicData(); ================================================ FILE: 随笔.md ================================================ # TimeoutError: Navigation timeout of 30000 ms exceeded 为什么puppeteer经常出现这个错误 在使用 Puppeteer 进行网页自动化或爬虫开发时,`TimeoutError: Navigation timeout of 30000 ms exceeded` 错误是一个常见问题。这个错误意味着 Puppeteer 在尝试加载页面或执行某个操作时超出了预设的最大等待时间(默认为 30,000 毫秒,即 30 秒)。这种情况通常由以下几个原因引起: ### 1. 网页加载慢 网页可能因为多种原因加载缓慢: - 网络连接慢。 - 服务器响应时间长。 - 页面资源(如大型图片、视频或复杂的 JavaScript)多且复杂,加载时间长。 ### 2. 重定向 有些网页在加载过程中可能会进行多次重定向,这些重定向可能会导致总加载时间超过设定的超时限制。 ### 3. JavaScript 执行延迟 如果网页依赖于复杂的 JavaScript 或外部脚本执行完成才能呈现,且这些脚本执行缓慢,可能会触发超时错误。 ### 4. 服务器问题 服务器可能会由于各种原因(例如高负载、维护或配置问题)响应缓慢,导致页面加载延迟。 ### 解决策略 要解决或规避这个问题,可以采取以下几种方法: #### a. 增加超时时间 如果你认为页面需要更长时间加载,可以增加超时设置: ```javascript await page.goto('http://example.com', {waitUntil: 'networkidle0', timeout: 60000}); // 设置超时时间为60秒 ``` #### b. 修改等待条件 Puppeteer 在等待 `networkidle0` 或 `networkidle2` 事件时可能会超时。这些事件分别代表没有网络连接或只有两个网络连接在指定时间内处于空闲状态。你可以改变等待策略以更适应页面的加载方式: ```javascript await page.goto('http://example.com', {waitUntil: 'domcontentloaded'}); ``` 这会让 Puppeteer 等待到 `DOM` 内容加载完成即继续执行,而不是等待所有外部资源加载完成。 #### c. 检查和优化页面加载项 如果你对服务器或页面有控制权,尝试优化网页性能。这可能包括减少页面资源大小、优化图片和使用更快的服务器。 #### d. 错误处理 增加错误处理逻辑,当加载超时时采取其他操作,例如重试或记录错误: ```javascript try { await page.goto('http://example.com', {waitUntil: 'networkidle0', timeout: 30000}); } catch (error) { console.error('Navigation timeout exceeded: ' + error.message); // 可以在此处添加重试逻辑或其他错误处理代码 } ``` #### e. 使用 headless:false 调试 在开发过程中,设置 `headless:false` 来观察 Puppeteer 在浏览器中实际执行的情况可能有助于发现问题原因: ```javascript const browser = await puppeteer.launch({headless: false}); ``` 通过上述方法,你可以有效地解决或减少遇到 `TimeoutError: Navigation timeout of 30000 ms exceeded` 错误的情况,从而提高 Puppeteer 脚本的稳定性和效率。