Threads V1.21

自動瀏覽 Threads 文章,模擬真實使用行為。工具欄新增【開始 🚀】與【暫停 ⛔】按鈕,並在狀態欄顯示當前狀態、倒計時與完整循環次數(每完成一次目標頁與首頁瀏覽算一輪)。請用於刷文章觀看。

// ==UserScript==
// @name         Threads V1.21
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  自動瀏覽 Threads 文章,模擬真實使用行為。工具欄新增【開始 🚀】與【暫停 ⛔】按鈕,並在狀態欄顯示當前狀態、倒計時與完整循環次數(每完成一次目標頁與首頁瀏覽算一輪)。請用於刷文章觀看。
// @author       ChatGPT
// @match        https://www.threads.net/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ========= 初始設定 =========
    // 讀取或設定目標文章與首頁 URL(若 localStorage 中無則採用預設值)
    let targetUrl = localStorage.getItem("THREADS_TARGET_URL") || "https://www.threads.net/posts/xxxxxx"; // 請替換預設目標文章 URL(請注意目標文章 URL 應包含 "/post/" 或 "/posts/")
    let homeUrl = localStorage.getItem("HOME_URL") || "https://www.threads.net"; // 預設首頁 URL
    localStorage.setItem("THREADS_TARGET_URL", targetUrl);
    localStorage.setItem("HOME_URL", homeUrl);

    // 使用 localStorage 儲存自動運行旗標與循環次數(每次進入目標文章頁視為完成一輪循環)
    const AUTO_FLAG = "THREADS_AUTOMATION_RUNNING";
    const LOOP_COUNT_KEY = "LOOP_COUNT";

    // ========= 時間參數(毫秒) =========
    const STAY_TIME = [30000, 60000];       // 目標文章頁瀏覽:30~60秒
    const BROWSE_TIME = [120000, 240000];     // 首頁瀏覽:2~4分鐘,原3~5分鐘
    const SCROLL_INTERVAL = [2000, 5000];     // 滾動間隔:2~5秒

    // ========= 工具函式 =========
    function randomDelay(min, max) {
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }
    async function wait(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    // 倒計時等待,每秒更新,並顯示當前階段描述
    async function countdown(ms, description) {
        let seconds = Math.ceil(ms / 1000);
        while (seconds > 0 && localStorage.getItem(AUTO_FLAG) === "true") {
            countdownDisplay.textContent = `${description} 倒數:${seconds}秒`;
            await wait(1000);
            seconds--;
        }
    }

    // 更新狀態顯示
    function updateStatus(text) {
        statusDisplay.textContent = "狀態:" + text;
    }
    // 寫入日誌
    function logMessage(msg) {
        const div = document.createElement('div');
        div.textContent = msg;
        logArea.appendChild(div);
        logArea.scrollTop = logArea.scrollHeight;
    }

    // 隨機滾動一次,增加隨機上下滾動效果(模擬真實使用)
    async function scrollPage() {
        logMessage("👉 開始滾動頁面...");
        const scrollStep = window.innerHeight * (Math.random() * 0.5 + 0.5);
        // 70% 機率向下滾,30% 機率向上滾
        const direction = Math.random() < 0.7 ? 1 : -1;
        window.scrollBy({ top: scrollStep * direction, behavior: 'smooth' });
        await wait(randomDelay(...SCROLL_INTERVAL));
        logMessage("✅ 完成滾動");
    }

    // ========= 模擬流程 =========

    // 模擬目標文章頁瀏覽(包含隨機滾動與倒計時等待,完成後等待一段時間再跳轉至首頁)
    async function simulateTargetPage() {
        // 判斷條件修改為:只要 URL 包含 "/post/" 或 "/posts/" 就認為是目標文章頁
        if (!(window.location.href.includes("/post/") || window.location.href.includes("/posts/"))) return;
        // 累計循環次數,每進入目標文章頁即累加
        let count = parseInt(localStorage.getItem(LOOP_COUNT_KEY) || "0") + 1;
        localStorage.setItem(LOOP_COUNT_KEY, count.toString());
        cycleCountDisplay.textContent = "已循環:" + count + " 次";
        updateStatus("瀏覽目標文章中 📄");
        logMessage("🔔 開始在目標文章頁模擬瀏覽...");
        let duration = randomDelay(...STAY_TIME);
        let startTime = Date.now();
        while (Date.now() - startTime < duration && localStorage.getItem(AUTO_FLAG) === "true") {
            await scrollPage();
            let remaining = Math.ceil((duration - (Date.now() - startTime)) / 1000);
            countdownDisplay.textContent = "目標頁倒數:" + remaining + "秒";
        }
        if (localStorage.getItem(AUTO_FLAG) === "true") {
            updateStatus("結束目標文章瀏覽,準備返回首頁 🏠");
            let waitTime = randomDelay(10000, 20000);
            await countdown(waitTime, "返回首頁等待");
            window.location.href = homeUrl;
        }
    }

    // 模擬首頁瀏覽(包含隨機滾動、倒計時等待,以及隨機點擊文章內部模擬瀏覽,再返回首頁)
    async function simulateHomePage() {
        if (window.location.href.includes("/post/") || window.location.href.includes("/posts/")) return;
        updateStatus("瀏覽首頁中 🌐");
        logMessage("🔔 開始在首頁模擬瀏覽...");
        const startTime = Date.now();
        const browseDuration = randomDelay(...BROWSE_TIME);
        let clickCount = 0; // 控制隨機點擊文章次數(最多 2 次)
        while (Date.now() - startTime < browseDuration && localStorage.getItem(AUTO_FLAG) === "true") {
            await scrollPage();
            let remaining = Math.ceil((browseDuration - (Date.now() - startTime)) / 1000);
            countdownDisplay.textContent = "首頁倒數:" + remaining + "秒";
            // 隨機觸發文章點擊(模擬使用者點進文章閱讀),機率 30%
            if (Math.random() < 0.3 && clickCount < 5) {
                logMessage("隨機點擊一篇文章...");
                let posts = document.querySelectorAll('.x1xdureb.xkbb5z.x13vxnyz');
                if (posts.length > 0) {
                    const randomPost = posts[Math.floor(Math.random() * posts.length)];
                    randomPost.click();
                    logMessage("點擊了文章,等待模擬瀏覽...");
                    // 模擬在文章內瀏覽,停留 5~15秒
                    let articleDuration = randomDelay(5000, 15000);
                    let articleStart = Date.now();
                    while (Date.now() - articleStart < articleDuration && localStorage.getItem(AUTO_FLAG) === "true") {
                        await scrollPage();
                    }
                    // 返回首頁(使用瀏覽器的 history.back() 模擬返回)
                    window.history.back();
                    logMessage("返回首頁...");
                    await wait(randomDelay(2000, 5000));  // 停留 2~5秒
                    clickCount++;
                }
            }
        }
        if (localStorage.getItem(AUTO_FLAG) === "true") {
            updateStatus("結束首頁瀏覽,準備返回目標文章 📄");
            let waitTime = randomDelay(5000, 10000);
            await countdown(waitTime, "返回目標等待");
            window.location.href = targetUrl;
        }
    }

    // 自動恢復流程:根據當前 URL 執行對應的模擬過程
    function autoContinue() {
        if (localStorage.getItem(AUTO_FLAG) === "true") {
            if (window.location.href.includes("/post/") || window.location.href.includes("/posts/")) {
                simulateTargetPage();
            } else {
                simulateHomePage();
            }
        } else {
            updateStatus("待命");
        }
    }

    // ========= 建立工具欄 =========
    // 加入 Emoji 使介面更活潑
    const controlPanelDiv = document.createElement('div');
    controlPanelDiv.style.position = 'fixed';
    controlPanelDiv.style.top = '10px';
    controlPanelDiv.style.right = '10px';
    controlPanelDiv.style.backgroundColor = '#f1f1f1';
    controlPanelDiv.style.padding = '10px';
    controlPanelDiv.style.border = '1px solid #ccc';
    controlPanelDiv.style.zIndex = '9999';
    controlPanelDiv.style.fontSize = '14px';
    controlPanelDiv.style.maxWidth = '300px';

    // 【更新目標文章 ✏️】按鈕
    const updateTargetBtn = document.createElement('button');
    updateTargetBtn.textContent = "更新目標文章 ✏️";
    updateTargetBtn.style.display = 'block';
    updateTargetBtn.style.marginBottom = '5px';
    updateTargetBtn.onclick = function() {
        const newPostUrl = prompt("請輸入新的目標文章 URL:");
        if (newPostUrl) {
            targetUrl = newPostUrl;
            localStorage.setItem("THREADS_TARGET_URL", targetUrl);
            logMessage("🔄 目標文章連結已更新:" + targetUrl);
            updateStatus("目標文章更新完畢");
        }
    };

    // 【更新首頁 URL 🌐】按鈕
    const updateHomeBtn = document.createElement('button');
    updateHomeBtn.textContent = "更新首頁 URL 🌐";
    updateHomeBtn.style.display = 'block';
    updateHomeBtn.style.marginBottom = '5px';
    updateHomeBtn.onclick = function() {
        const newHomeUrl = prompt("請輸入新的首頁 URL:");
        if (newHomeUrl) {
            homeUrl = newHomeUrl;
            localStorage.setItem("HOME_URL", homeUrl);
            logMessage("🔄 首頁連結已更新:" + homeUrl);
            updateStatus("首頁更新完畢");
        }
    };

    // 開始與暫停按鈕
    const startBtn = document.createElement('button');
    startBtn.textContent = "開始 🚀";
    startBtn.style.marginRight = '10px';
    const pauseBtn = document.createElement('button');
    pauseBtn.textContent = "暫停 ⛔";

    // 將開始與暫停按鈕放在同一行
    const btnContainer = document.createElement('div');
    btnContainer.appendChild(startBtn);
    btnContainer.appendChild(pauseBtn);

    // 狀態顯示區
    const statusDisplay = document.createElement('div');
    statusDisplay.style.backgroundColor = '#fff';
    statusDisplay.style.border = '1px solid #ccc';
    statusDisplay.style.padding = '4px';
    statusDisplay.style.margin = '4px 0';
    statusDisplay.textContent = "狀態:待命";

    // 倒計時顯示區
    const countdownDisplay = document.createElement('div');
    countdownDisplay.style.backgroundColor = '#fff';
    countdownDisplay.style.border = '1px solid #ccc';
    countdownDisplay.style.padding = '4px';
    countdownDisplay.style.margin = '4px 0';
    countdownDisplay.textContent = "倒計時:";

    // 循環次數顯示區
    const cycleCountDisplay = document.createElement('div');
    cycleCountDisplay.style.backgroundColor = '#fff';
    cycleCountDisplay.style.border = '1px solid #ccc';
    cycleCountDisplay.style.padding = '4px';
    cycleCountDisplay.style.margin = '4px 0';
    cycleCountDisplay.textContent = "已循環:0 次";

    // 日誌顯示區
    const logArea = document.createElement('div');
    logArea.style.height = '200px';
    logArea.style.overflowY = 'auto';
    logArea.style.backgroundColor = '#fff';
    logArea.style.border = '1px solid #ccc';
    logArea.style.padding = '5px';

    // 組裝控制面板
    controlPanelDiv.appendChild(updateTargetBtn);
    controlPanelDiv.appendChild(updateHomeBtn);
    controlPanelDiv.appendChild(btnContainer);
    controlPanelDiv.appendChild(statusDisplay);
    controlPanelDiv.appendChild(countdownDisplay);
    controlPanelDiv.appendChild(cycleCountDisplay);
    controlPanelDiv.appendChild(logArea);
    document.body.appendChild(controlPanelDiv);

    // --------------------
    // 開始按鈕事件:設置 AUTO_FLAG 為 "true",重置循環計數,然後開始流程
    // --------------------
    startBtn.addEventListener('click', function() {
        localStorage.setItem(AUTO_FLAG, "true");
        localStorage.setItem(LOOP_COUNT_KEY, "0");
        cycleCountDisplay.textContent = "已循環:0 次";
        logMessage("🚀 開始模擬...");
        updateStatus("開始模擬");
        // 若當前頁面不在目標文章頁(判斷條件:同時支援 "/post/" 或 "/posts/"),則跳轉
        if (!(window.location.href.includes("/post/") || window.location.href.includes("/posts/"))) {
            window.location.href = targetUrl;
        } else {
            simulateTargetPage();
        }
    });

    // 暫停按鈕事件:將 AUTO_FLAG 設為 "false",重置循環計數
    pauseBtn.addEventListener('click', function() {
        localStorage.setItem(AUTO_FLAG, "false");
        logMessage("⛔ 模擬已暫停");
        updateStatus("已暫停");
        cycleCountDisplay.textContent = "已循環:0 次";
    });

    // --------------------
    // 當頁面載入時,自動檢查是否需要恢復模擬
    // --------------------
    window.addEventListener('load', function() {
        if (localStorage.getItem(AUTO_FLAG) === "true") {
            logMessage("🔄 自動啟動檢測:恢復模擬");
            let cnt = localStorage.getItem(LOOP_COUNT_KEY) || "0";
            cycleCountDisplay.textContent = "已循環:" + cnt + " 次";
            if (window.location.href.includes("/post/") || window.location.href.includes("/posts/")) {
                simulateTargetPage();
            } else {
                simulateHomePage();
            }
        } else {
            updateStatus("待命");
            logMessage("頁面載入完成,請更新目標文章與首頁 URL,再點【開始 🚀】按鈕");
        }
    });

    // --------------------
    // 自動恢復:根據當前 URL 執行對應模擬流程
    // --------------------
    function autoContinue() {
        if (localStorage.getItem(AUTO_FLAG) === "true") {
            if (window.location.href.includes("/post/") || window.location.href.includes("/posts/")) {
                simulateTargetPage();
            } else {
                simulateHomePage();
            }
        } else {
            updateStatus("待命");
        }
    }
    // 若頁面是因跳轉而重載,延遲 3 秒執行 autoContinue
    setTimeout(autoContinue, 3000);

})();