V2EX = way to explore
V2EX 是一个关于分享和探索的地方
Sign Up Now
For Existing Member  Sign In
爱意满满的作品展示区。
apanlin

我让 GPT 写了个 APP 兑换码高亮助手

  •  
  •   apanlin · Oct 17, 2025 · 2521 views
    This topic created in 202 days ago, the information mentioned may be changed or developed.

    看到有大佬分享 APP 兑换码, 但是试了好多, 都是用过的, 即便很多高素质大佬把使用过的贴到了评论区,依然非常难找出一个未使用的兑换码.
    于是让 GPT 写了个油猴脚本, 把未使用的兑换码高亮出来方便查找.
    当然这个前提是需要大家主动把已经使用的兑换码贴到评论里
    高亮显示未使用(绿色)和已使用(红色)兑换码

    安装方式

    推荐使用 Tampermonkey/Violentmonkey

    1. 安装浏览器扩展 Tampermonkey
    2. 点击 “创建新脚本”,粘贴下面完整脚本
    3. 保存后访问任意 V2EX 帖子页面,自动生效

    使用方法

    • 打开 V2EX 帖子页面
    • 脚本会自动抓取作者的兑换码 + 评论
    • 高亮显示未使用(绿色)和已使用(红色)兑换码
    • 页面右下角显示统计信息

    完整脚本( v1.6 )

    // ==UserScript==
    // @name         V2EX 兑换码高亮助手 (多页评论)
    // @namespace    https://v2ex.com/
    // @version      1.5
    // @description  高亮显示作者发布的兑换码(正文 + 附言),抓取多页评论兑换码,评论中出现的默认已使用。
    // @match        https://www.v2ex.com/t/*
    // @match        https://v2ex.com/t/*
    // @grant        none
    // ==/UserScript==
    
    (function () {
        'use strict';
    
        const MIN_LEN = 10; // 兑换码最小长度
    
        function extractCodes(text) {
            const pattern = new RegExp(`\\b[A-Z0-9]{${MIN_LEN},}\\b`, 'g');
            return new Set(text.match(pattern) || []);
        }
    
        function extractCodesFromReply(replyNode) {
            const codes = new Set();
    
             console.log('[V2EX Code Highlighter] replyNode:', replyNode);
            // 遍历 replyNode 的子节点
            replyNode.childNodes.forEach(node => {
                if (node.nodeType === Node.TEXT_NODE) {
                    // 文本节点按空格分割
                    node.textContent.split(/\s+/).forEach(word => {
                        //console.log('正在解析:', word)
                        // 全局匹配所有 10 位以上大写字母或数字
                        const pattern = /\b[A-Z0-9]{10,}\b/g;
                        const matches = word.match(pattern) || [];
                        matches.forEach(c => codes.add(c));
                    });
                } else if (node.nodeName === 'BR') {
                    // <br> 就当作分隔,不需要处理
                } else {
                    // 递归抓取子节点
                    extractCodesFromReply(node).forEach(c => codes.add(c));
                }
            });
    
            //console.log('该评论最后得到:', codes)
            return codes;
        }
    
    
        function replaceTextNodes(node, callback) {
            const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);
            const nodes = [];
            let n;
            while (n = walker.nextNode()) nodes.push(n);
            for (const t of nodes) callback(t);
        }
    
        function highlightCodeSpan(code, used) {
            const span = document.createElement('span');
            span.textContent = code;
            span.style.cssText = `
                background-color: ${used ? 'red' : 'green'};
                color: white;
                font-weight: bold;
                padding: 2px 4px;
                border-radius: 4px;
                margin: 0 2px;
                font-family: monospace;
            `;
            span.title = used ? '已用' : '未用';
            return span;
        }
    
        // 异步抓取评论页内容
        async function fetchReplyCodes(url, authorName) {
            const commentCodes = new Set();
            try {
                const res = await fetch(url);
                const text = await res.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(text, 'text/html');
                const replyNodes = doc.querySelectorAll('.reply_content');
                replyNodes.forEach(r => {
                    const floorNode = r.closest('.cell');
                    const userLink = floorNode ? floorNode.querySelector('.dark, .username, a[href^="/member/"]') : null;
                    const userName = userLink ? userLink.textContent.trim() : '';
                    if (userName === authorName) return; // 跳过作者
                    extractCodesFromReply(r).forEach(c => commentCodes.add(c));
                });
            } catch (e) {
                console.error('[V2EX Code Highlighter] Fetch page error:', url, e);
            }
            return commentCodes;
        }
    
        async function run() {
            const mainPostNode = document.querySelector('#Main .topic_content');
            if (!mainPostNode) return;
    
            const authorNode = document.querySelector('#Main .header .fr a[href^="/member/"]');
            if (!authorNode) return;
            const authorName = authorNode.textContent.trim();
            console.log('[V2EX Code Highlighter] Author:', authorName);
    
            const mainCodes = new Set();
            const commentCodes = new Set();
    
            // 1️⃣ 抓取作者正文
            extractCodes(mainPostNode.innerText).forEach(c => mainCodes.add(c));
    
            // 2️⃣ 抓取作者附言
            const subNotes = document.querySelectorAll('#Main .subtle .topic_content');
            subNotes.forEach(note => {
                extractCodes(note.innerText).forEach(c => mainCodes.add(c));
            });
    
            // 输出作者兑换码日志
            console.log('[V2EX Code Highlighter] Author codes:', [...mainCodes]);
    
            // 3️⃣ 获取评论页数
            const psContainer = document.querySelector('.cell.ps_container');
            let totalPages = 1;
            if (psContainer) {
                const pageLinks = psContainer.querySelectorAll('a.page_current, a.page_normal');
                totalPages = Math.max(...Array.from(pageLinks).map(a => parseInt(a.textContent.trim())));
            }
            console.log('[V2EX Code Highlighter] totalPages:', totalPages);
    
    
            // 4️⃣ 抓取所有评论页
            const currentUrl = window.location.href.split('?')[0];
            const pageUrls = [];
            for (let p = 1; p <= totalPages; p++) {
                pageUrls.push(`${currentUrl}?p=${p}`);
            }
    
            for (const url of pageUrls) {
                const codes = await fetchReplyCodes(url, authorName);
                codes.forEach(c => commentCodes.add(c));
            }
    
            console.log('[V2EX Code Highlighter] Comment codes (all pages):', [...commentCodes]);
    
            // 5️⃣ 计算未用
            const unusedCodes = [...mainCodes].filter(c => !commentCodes.has(c));
    
            // 6️⃣ 高亮当前页面作者兑换码(正文 + 附言)
            const authorContentNodes = [mainPostNode, ...Array.from(subNotes)];
            authorContentNodes.forEach(node => {
                replaceTextNodes(node, t => {
                    const text = t.textContent;
                    const codes = extractCodes(text);
                    if (!codes.size) return;
                    const frag = document.createDocumentFragment();
                    let remaining = text;
                    codes.forEach(c => {
                        const parts = remaining.split(c);
                        frag.appendChild(document.createTextNode(parts.shift()));
                        const used = commentCodes.has(c);
                        frag.appendChild(highlightCodeSpan(c, used));
                        remaining = parts.join(c);
                    });
                    frag.appendChild(document.createTextNode(remaining));
                    t.parentNode.replaceChild(frag, t);
                });
            });
    
            // 7️⃣ 页面右下角统计
            const panel = document.createElement('div');
            panel.style.cssText = `
                position: fixed;
                bottom: 10px;
                right: 10px;
                background: #222;
                color: #fff;
                padding: 10px 14px;
                border-radius: 8px;
                box-shadow: 0 0 6px rgba(0,0,0,0.5);
                font-size: 13px;
                z-index: 9999;
                line-height: 1.5;
            `;
            panel.innerHTML = `
                <b>兑换码统计</b><br>
                总数: ${mainCodes.size}<br>
                已用: ${commentCodes.size}<br>
                可用: ${unusedCodes.length}
            `;
            document.body.appendChild(panel);
        }
    
        window.addEventListener('load', run);
    })();
    
    
    11 replies    2025-10-17 23:10:31 +08:00
    korvin
        1
    korvin  
       Oct 17, 2025
    哈哈,和我之前写的差不多 /t/1127520
    saimax
        2
    saimax  
       Oct 17, 2025
    想法是好的,但实际情况兑换了回复的不足 1 成。所以没啥用
    HMYang33
        3
    HMYang33  
       Oct 17, 2025
    基数不够大,如果是腾讯或谷歌做的,估计有点用
    apanlin
        4
    apanlin  
    OP
       Oct 17, 2025
    @saimax 总有正义的大佬会把兑换失败的全都贴到评论区的, 所以还是能提高点效率
    acluxo
        5
    acluxo  
       Oct 17, 2025
    直接复制全文丢到 AI 里
    callv
        6
    callv  
       Oct 17, 2025
    我写的这个社区可以直接记录兑换,你们看看效果是不是更好一些。https://2libra.com/post/festival-things/IpsWhjF
    apanlin
        7
    apanlin  
    OP
       Oct 17, 2025
    @korvin 哈哈 原来大佬做过了, 失敬失敬
    apanlin
        8
    apanlin  
    OP
       Oct 17, 2025
    @acluxo 这倒是好主意, 直接丢链接过去 让 AI 自己解析识别应该也可以
    deplives
        9
    deplives  
       Oct 17, 2025
    实际上没啥用,用码后回复的我觉得不到 1/10
    apanlin
        10
    apanlin  
    OP
       Oct 17, 2025
    @callv 直接做到平台上确实好用
    apanlin
        11
    apanlin  
    OP
       Oct 17, 2025
    @deplives 靠大家一起维护, 会有很多正义大佬把兑换失败的码贴到评论里的
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   5590 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 77ms · UTC 09:11 · PVG 17:11 · LAX 02:11 · JFK 05:11
    ♥ Do have faith in what you're doing.