V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
apanlin
V2EX  ›  分享创造

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

  •  
  •   apanlin · 2 天前 · 873 次点击

    看到有大佬分享 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 条回复    2025-10-17 23:10:31 +08:00
    korvin
        1
    korvin  
       2 天前
    哈哈,和我之前写的差不多 /t/1127520
    saimax
        2
    saimax  
       2 天前
    想法是好的,但实际情况兑换了回复的不足 1 成。所以没啥用
    HMYang33
        3
    HMYang33  
       2 天前
    基数不够大,如果是腾讯或谷歌做的,估计有点用
    apanlin
        4
    apanlin  
    OP
       2 天前
    @saimax 总有正义的大佬会把兑换失败的全都贴到评论区的, 所以还是能提高点效率
    acluxo
        5
    acluxo  
       2 天前
    直接复制全文丢到 AI 里
    callv
        6
    callv  
       2 天前
    我写的这个社区可以直接记录兑换,你们看看效果是不是更好一些。https://2libra.com/post/festival-things/IpsWhjF
    apanlin
        7
    apanlin  
    OP
       2 天前
    @korvin 哈哈 原来大佬做过了, 失敬失敬
    apanlin
        8
    apanlin  
    OP
       2 天前
    @acluxo 这倒是好主意, 直接丢链接过去 让 AI 自己解析识别应该也可以
    deplives
        9
    deplives  
       2 天前
    实际上没啥用,用码后回复的我觉得不到 1/10
    apanlin
        10
    apanlin  
    OP
       1 天前
    @callv 直接做到平台上确实好用
    apanlin
        11
    apanlin  
    OP
       1 天前
    @deplives 靠大家一起维护, 会有很多正义大佬把兑换失败的码贴到评论里的
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   2763 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 34ms · UTC 13:04 · PVG 21:04 · LAX 06:04 · JFK 09:04
    ♥ Do have faith in what you're doing.