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

3 天前
 apanlin

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

安装方式

推荐使用 Tampermonkey/Violentmonkey

  1. 安装浏览器扩展 Tampermonkey
  2. 点击 “创建新脚本”,粘贴下面完整脚本
  3. 保存后访问任意 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);
})();

1075 次点击
所在节点    分享创造
11 条回复
korvin
3 天前
哈哈,和我之前写的差不多 /t/1127520
saimax
3 天前
想法是好的,但实际情况兑换了回复的不足 1 成。所以没啥用
HMYang33
3 天前
基数不够大,如果是腾讯或谷歌做的,估计有点用
apanlin
3 天前
@saimax 总有正义的大佬会把兑换失败的全都贴到评论区的, 所以还是能提高点效率
acluxo
3 天前
直接复制全文丢到 AI 里
callv
3 天前
我写的这个社区可以直接记录兑换,你们看看效果是不是更好一些。https://2libra.com/post/festival-things/IpsWhjF
apanlin
3 天前
@korvin 哈哈 原来大佬做过了, 失敬失敬
apanlin
3 天前
@acluxo 这倒是好主意, 直接丢链接过去 让 AI 自己解析识别应该也可以
deplives
3 天前
实际上没啥用,用码后回复的我觉得不到 1/10
apanlin
2 天前
@callv 直接做到平台上确实好用
apanlin
2 天前
@deplives 靠大家一起维护, 会有很多正义大佬把兑换失败的码贴到评论里的

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1166480

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX