DeepFloodbeta

【原创脚本】DeepFlood帖子内容AI总结(支持NodeSeek)

太长不看?AI时代当然是让AI来解决阅读困难

都什么年代了还自己看帖,现在都是让AI帮忙总结了。
其实是解决两个痛点:

  1. 主帖内容太长看的头疼,又怕抓不住重点,尤其是公告帖光看就看半天
  2. 有的热门讨论虽然有意思,但是几十上百楼的讨论太多太杂,懒得一条条翻
  3. 我TM发个帖子前面都给你总结好了,还是一直有人回复太长不看。这么玩是吧,AI启动!

使用方法

  1. 安装Tampermonkey插件,将本帖末尾的脚本代码添加进插件
  2. 添加自定义API,直接修改代码开头的CONFIG,必填项包括:
  1. 保存脚本并刷新帖子页面,帖子标题下方会增加总结按钮,enjoy!

功能说明

  • ✅ 单独总结主帖或主帖+回帖
  • ✅ 支持重新生成总结,不满意就让AI重来
  • ✅ 兼容 NodeSeek 等同矩阵网站
  • ✅ 兼容OpenAI格式的自定义API
  • ✅ 根据个性化需求自定义提示词
  • ✅ 流式输出,拒绝长时间等待
  • ✅ 增强上下文关联,如楼层间的回复关系和同一个人的多条回复
  • ✅ 配合NodeSeek X等脚本,可以对页面上所有加载出的楼层做总结,同时不会对论坛服务器做额外请求

已知问题

1. 提示词优化

显然提示词还有优化空间,这个我不太擅长,缝缝补补就这样了,欢迎大佬给出更好的提示词

2. 不支持HTTP的API

暂不支持HTTP协议的 API(因浏览器混合内容限制),请使用HTTPS。
因为目前请求用的是fetch方法,支持流式输出但是没法避免混合内容问题,而GM_xmlhttpRequest可以绕过,也就是支持HTTP请求,但是反复想办法变通也无法实现流式输出,我认为这会极大影响体验。
不过因为大部分人用的应该都是HTTPS协议,所以暂时先发布了。如果有大佬能解决这个问题,欢迎帮忙修复。

功能截图

1. 按钮功能展示

按钮位于帖子标题下方,三个按钮分别对应总结主帖、总结主帖+回复和关闭弹出的总结框

2. 总结主帖+回复

以这个帖子为例(通过NodeSeek X脚本加载所有回帖,图中可以看到包括 #103都在总结范围,另外同一个人的不同楼层以及回复前后关系也有体现):
今天的元老状态更新了哈
https://www.deepflood.com/post-25303-1

3. 总结主帖

以这个帖子为例:
DeepFlood社区的定位和创立原因
https://www.deepflood.com/post-1410-1

脚本代码

// ==UserScript==
// @name         DeepFlood帖子内容AI总结
// @namespace    https://www.deepflood.com
// @version      0.2.1
// @description  帖子内容AI总结
// @author       yacc@DeepFlood
// @match        https://www.deepflood.com/post-*
// @match        https://www.nodeseek.com/post-*
// @icon         https://icons.duckduckgo.com/ip2/deepflood.com.ico
// @require      https://cdn.jsdelivr.net/npm/marked/marked.min.js
// @require      https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js
// @grant        none
// @license      GPL-3.0 License
// @supportURL   https://www.deepflood.com/post-27610-1
// @homepageURL  https://www.deepflood.com/post-27610-1
// ==/UserScript==

(function() {
    'use strict';

    // ========== 配置区域 - 请在此处修改你的API配置 ==========
    const CONFIG = {
        // 完整的 API 地址,如 https://api.openai.com/v1/chat/completions
        API_ENDPOINT: 'YOUR_API_ENDPOINT',

        // API 密钥,一般是 sk 开头
        API_KEY: 'YOUR_API_KEY',

        // 调用的模型,如 deepseek-ai/DeepSeek-V3.1-Terminus
        MODEL: 'deepseek-ai/DeepSeek-V3.1-Terminus',

        // 用于总结主帖的提示词
        PROMPT_MAIN: `
作为论坛内容分析助手,请对帖子进行简要总结。
【总结要求】
1. 只做**客观总结**,不做主观评价
2. 根据帖子类型提炼**读者最需要了解**的内容:
   - 技术帖: 技术要点、方案、问题
   - 新闻帖: 简短摘要、关键信息
   - 提问帖: 问题描述、背景、期望
   - 讨论帖: 话题、主要观点
3. 总结长度: 50-200 字
4. 关键字重点标注
5. 禁止"好的,我将为您总结"等客套话
【输出格式】
一句话概括主题
## 核心内容
- [要点 1]
- [要点 2]
- [要点 3](如有)
## 关键信息
[链接/资源/数据等,如无可省略此部分]
帖子内容如下:\n\n`,

        // 用于总结主帖和回帖全部内容的提示词
        PROMPT_ALL: `
作为论坛内容分析助手,请对**主帖**及所有**回复**进行总结。
【输入格式】
- 楼层 0 代表主帖,其他为回复
- 每条内容包含: 楼层编号、作者、正文
- 回复关系用 @用户名 (楼层#X) 标注
【总结要求】
1. 只做**客观总结**,区分**事实**和**观点**
2. 首先总结**主帖核心内容**
3. 根据帖子类型针对性总结回复:
   - 提问帖: 关注问题讨论和解决方案
   - 辩论帖: 梳理正反方论点
   - 技术帖: 关注有实质内容的技术回复
   - 新闻帖: 关注用户反应和补充信息
4. **关联处理**:
   - 针对性回复应联系上下文
   - 同一用户的多次回复应整合
   - 明显的讨论串应作为连续内容分析
5. **重点关注**:
   - 楼主的回复
   - 高质量/代表性回复
   - 关键信息(链接、资源、方案)
   - 用户间的矛盾点或争议
6. **过滤原则**:
   - 纯水帖/无实质内容的回复可跳过
   - 高度重复的观点应合并总结
   - 主帖内容在关键观点章节不要重复归纳
   - 回复数>50 时优先总结关键内容
7. 整体内容限制在500字以内
8. 关键字重点标注
9. 禁止"好的,我将为您总结"等客套话
【输出格式】(若没有实质性回复可适当跳过部分环节)
# 主帖概要
[核心内容,2-3 句话]
## 讨论焦点
[主要讨论方向,2-3 点]
## 关键观点(或代表性回复,适当合并条目压缩篇幅)
- **用户A** (楼层#X): [简述]
- **用户B** (楼层#Y): [简述]
## 实用信息
[解决方案/链接/资源等,如无可省略]
## 讨论状态
- 讨论氛围:是否友善,讨论是否积极,或者没有什么实质性回复等
- 问题状态: [已解决/讨论中/无共识](根据帖子类型选择)
帖子内容如下:\n\n`
    };

    // ========== 样式集中管理 ==========
    const styles = {
        buttonContainer: `
            display: flex;
            gap: 10px;
            margin-top: 12px;
            margin-bottom: 12px;
            flex-wrap: wrap;
        `,
        button: `
            padding: 6px 12px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 13px;
            font-weight: 500;
            transition: all 0.3s ease;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        `,
        buttonHover: `
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
        `,
        resultBox: `
            margin: 20px 0;
            padding: 20px;
            background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
            border-radius: 12px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
            animation: slideDown 0.3s ease;
        `,
        resultTitle: `
            font-weight: bold;
            font-size: 16px;
            color: #333;
            margin-bottom: 12px;
            display: flex;
            align-items: center;
            justify-content: space-between;
        `,
        resultContent: `
            background: white;
            padding: 15px;
            border-radius: 8px;
            line-height: 1.8;
            color: #333;
            min-height: 100px;
            max-height: 1000px;
            overflow-y: auto;
        `,
        closeButton: `
            background: none;
            border: none;
            cursor: pointer;
            font-size: 20px;
            color: #666;
            padding: 0;
            width: 24px;
            height: 24px;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: color 0.2s ease;
        `,
        closeButtonHover: `
            color: #d32f2f;
        `,
        loadingText: `
            color: #667eea;
            font-style: italic;
        `,
        globalStyles: `
            @keyframes slideDown {
                from {
                    opacity: 0;
                    transform: translateY(-10px);
                }
                to {
                    opacity: 1;
                    transform: translateY(0);
                }
            }

            /* Markdown 样式 */
            #ai-summary-content {
                font-family: 'PingFang SC', 'Microsoft YaHei', 'Noto Sans CJK SC', sans-serif;
            }

            #ai-summary-content h1 {
                font-size: 1.8em;
                font-weight: 600;
                margin: 0.8em 0 0.5em 0;
                padding-bottom: 0.3em;
                border-bottom: 2px solid #eaecef;
                color: #2c3e50;
            }

            #ai-summary-content h2 {
                font-size: 1.5em;
                font-weight: 600;
                margin: 0.7em 0 0.4em 0;
                padding-bottom: 0.2em;
                border-bottom: 1px solid #eaecef;
                color: #34495e;
            }

            #ai-summary-content h3 {
                font-size: 1.25em;
                font-weight: 600;
                margin: 0.6em 0 0.3em 0;
                color: #34495e;
            }

            #ai-summary-content h4, #ai-summary-content h5, #ai-summary-content h6 {
                font-size: 1.1em;
                font-weight: 600;
                margin: 0.5em 0 0.3em 0;
                color: #34495e;
            }

            #ai-summary-content p {
                margin: 0.8em 0;
                line-height: 1.8;
            }

            #ai-summary-content ul, #ai-summary-content ol {
                margin: 0.8em 0;
                padding-left: 2em;
            }

            #ai-summary-content li {
                margin: 0.3em 0;
                line-height: 1.6;
            }

            #ai-summary-content code {
                background-color: #f6f8fa;
                padding: 0.2em 0.4em;
                border-radius: 3px;
                font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
                font-size: 0.9em;
                color: #e83e8c;
            }

            #ai-summary-content pre {
                background-color: #f6f8fa;
                padding: 1em;
                border-radius: 6px;
                overflow-x: auto;
                margin: 1em 0;
                border: 1px solid #e1e4e8;
            }

            #ai-summary-content pre code {
                background-color: transparent;
                padding: 0;
                color: #333;
                font-size: 0.95em;
            }

            #ai-summary-content blockquote {
                border-left: 4px solid #667eea;
                padding-left: 1em;
                margin: 1em 0;
                color: #666;
                background-color: #f8f9fa;
                padding: 0.8em 1em;
                border-radius: 0 4px 4px 0;
            }

            #ai-summary-content a {
                color: #667eea;
                text-decoration: none;
                transition: color 0.2s ease;
            }

            #ai-summary-content a:hover {
                color: #764ba2;
                text-decoration: underline;
            }

            #ai-summary-content table {
                border-collapse: collapse;
                width: 100%;
                margin: 1em 0;
            }

            #ai-summary-content table th,
            #ai-summary-content table td {
                border: 1px solid #dfe2e5;
                padding: 0.6em 1em;
                text-align: left;
            }

            #ai-summary-content table th {
                background-color: #f6f8fa;
                font-weight: 600;
            }

            #ai-summary-content table tr:nth-child(even) {
                background-color: #f8f9fa;
            }

            #ai-summary-content hr {
                border: none;
                border-top: 2px solid #eaecef;
                margin: 1.5em 0;
            }

            #ai-summary-content strong {
                font-weight: 600;
                color: #2c3e50;
            }

            #ai-summary-content em {
                font-style: italic;
            }

            #ai-summary-content img {
                max-width: 100%;
                height: auto;
                border-radius: 4px;
                margin: 0.5em 0;
            }
        `
    };

    // ========== Markdown 渲染配置 ==========

    // 配置 marked
    if (typeof marked !== 'undefined') {
        marked.setOptions({
            breaks: true,       // 支持 GitHub 风格的换行
            gfm: true,          // 启用 GitHub 风格的 Markdown
            headerIds: false,   // 不为标题生成 ID
            mangle: false       // 不混淆邮箱地址
        });
    }

    /**
     * 将 Markdown 文本渲染为 HTML,并进行 XSS 防护
     * @param {string} markdown - Markdown 文本
     * @returns {string} - 安全的 HTML
     */
    function renderMarkdown(markdown) {
        if (typeof marked === 'undefined' || typeof DOMPurify === 'undefined') {
            console.warn('Markdown 渲染库未加载,使用纯文本显示');
            return markdown;
        }

        try {
            // 使用 marked 解析 Markdown
            const rawHtml = marked.parse(markdown);

            // 使用 DOMPurify 清理 HTML,防止 XSS 攻击
            const cleanHtml = DOMPurify.sanitize(rawHtml, {
                ALLOWED_TAGS: [
                    'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
                    'p', 'br', 'hr',
                    'strong', 'em', 'b', 'i', 'u', 's', 'del',
                    'a', 'img',
                    'ul', 'ol', 'li',
                    'blockquote', 'pre', 'code',
                    'table', 'thead', 'tbody', 'tr', 'th', 'td'
                ],
                ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'class']
            });

            return cleanHtml;
        } catch (error) {
            console.error('Markdown 渲染失败:', error);
            return markdown;
        }
    }

    // ========== 内容提取和处理函数 ==========

    /**
     * 预处理帖子内容,清洗和格式化HTML,使其更适合AI理解和总结。
     * @param {HTMLElement | string} input - 帖子内容的HTML元素或HTML字符串。
     * @returns {string} - 处理后的纯文本或Markdown格式的字符串。
     */
    function preprocessPostContent(input) {
        let tempDiv;

        // 将输入转成临时DOM来操作,DOM直接克隆,字符串新建,否则返回空字符串
        if (input instanceof HTMLElement) {
            tempDiv = input.cloneNode(true);
        } else if (typeof input === 'string') {
            tempDiv = document.createElement('div');
            tempDiv.innerHTML = input;
        } else {
            return '';
        }

        // 处理回复内容,查找 /member 和 /post 链接对
        // <p><a href="/member?t=user">@user</a> <a href="/post-114-5#14">#14</a> 回复内容</p>
        tempDiv.querySelectorAll('a[href*="/member?t="]').forEach(memberLink => {
            const nextElement = memberLink.nextElementSibling;
            // 检查紧跟其后的是否是指向楼层的链接
            // <a>@user</a> <a>#14</a> 模式
            if (nextElement && nextElement.tagName === 'A' && nextElement.getAttribute('href').includes('/post-')) {
                const user = memberLink.textContent.trim(); // 如 @user
                const floor = nextElement.textContent.trim(); // 如 #14

                // 创建一个新的文本节点来替换这两个链接
                const replyText = document.createTextNode(`回复 ${user} (楼层${floor}): `);
                memberLink.parentNode.insertBefore(replyText, memberLink);
                memberLink.remove();
                nextElement.remove();
            }
        });

        // 处理表情贴纸 (Stickers)
        // <p>帖子内容 <img class="sticker" src="/static/image/sticker/xhj/001.gif" loading="lazy" alt="xhj001"></p>
        tempDiv.querySelectorAll('img.sticker').forEach(sticker => {
            sticker.replaceWith(document.createTextNode('[sticker]'));
        });

        // 处理普通图片,转换为Markdown格式
        tempDiv.querySelectorAll('img:not(.sticker)').forEach(img => {
            const src = img.getAttribute('src') || '';
            const alt = img.getAttribute('alt') || 'image';
            const markdownImage = document.createTextNode(`!${alt} <sup>1</sup> [<sup>1</sup>](${src})`);
            img.replaceWith(markdownImage);
        });

        // 提取处理后的文本内容
        return tempDiv.innerText.trim();
    }

    /**
     * 获取帖子内容
     * @param {string} type - 'main' 只获取主帖,'all' 获取主帖和所有回帖
     * @returns {string} - 格式化后的帖子内容
     */
    function getPostContent(type) {
        // 获取帖子标题
        const titleElement = document.querySelector('.post-title-link');
        const title = titleElement?.textContent.trim() || '未知标题';

        // 创建一个数组来存储所有楼层(包括主帖)的数据
        const allPostsData = [];

        // 主帖内容
        const mainPostContainer = document.getElementById('0');
        const mainAuthor = mainPostContainer?.querySelector('.author-name')?.textContent.trim();
        const mainPostContentElement = mainPostContainer?.querySelector('.post-content');

        if (mainPostContentElement) {
            allPostsData.push({
                floor: 0,
                author: mainAuthor || '未知作者',
                content: preprocessPostContent(mainPostContentElement)
            });
        }

        // 如果是总结全部,获取所有回帖
        if (type === 'all') {
            const commentElements = document.querySelectorAll('.comments .content-item');
            commentElements.forEach(commentElement => {
                // 从元素的 id 中获取楼层号
                const floor = parseInt(commentElement.id, 10);

                // 元素内部查找作者和内容并添加数组
                const author = commentElement.querySelector('.author-name')?.textContent.trim();
                const postContentElement = commentElement.querySelector('.post-content');

                if (postContentElement) {
                    allPostsData.push({
                        floor: floor,
                        author: author || '未知作者',
                        content: preprocessPostContent(postContentElement)
                    });
                }
            });
        }

        // 格式化输出
        const formattedPosts = allPostsData.map(post => {
            return `楼层 ${post.floor} (作者: ${post.author}):\n${post.content}`;
        }).join('\n\n---\n\n');

        return `标题: ${title}\n\n${formattedPosts}`;
    }

    // ========== UI 相关函数 ==========

    // 注入全局样式
    function injectGlobalStyles() {
        if (document.getElementById('ai-summary-styles')) {
            return;
        }

        const styleTag = document.createElement('style');
        styleTag.id = 'ai-summary-styles';
        styleTag.textContent = styles.globalStyles;
        document.head.appendChild(styleTag);
    }

    // 创建按钮容器
    function createButtonContainer() {
        const container = document.createElement('div');
        container.className = 'ai-summary-buttons';
        container.style.cssText = styles.buttonContainer;

        const btn1 = createButton('AI总结主帖', () => handleSummary('main'));
        const btn2 = createButton('AI总结全部', () => handleSummary('all'));
        const btn3 = createButton('关闭总结', () => closeSummary());

        container.appendChild(btn1);
        container.appendChild(btn2);
        container.appendChild(btn3);

        return container;
    }

    // 创建按钮
    function createButton(text, onClick) {
        const button = document.createElement('button');
        button.textContent = text;
        button.style.cssText = styles.button;

        button.addEventListener('mouseenter', () => {
            button.style.cssText = styles.button + '; ' + styles.buttonHover;
        });

        button.addEventListener('mouseleave', () => {
            button.style.cssText = styles.button;
        });

        button.addEventListener('click', onClick);
        return button;
    }

    // 创建结果展示框
    function createResultBox() {
        const box = document.createElement('div');
        box.id = 'ai-summary-result';
        box.style.cssText = styles.resultBox;

        // 标题和关闭按钮
        const titleBar = document.createElement('div');
        titleBar.style.cssText = styles.resultTitle;

        const titleText = document.createElement('span');
        titleText.innerHTML = '🤖 AI总结结果';

        const closeBtn = document.createElement('button');
        closeBtn.innerHTML = '✕';
        closeBtn.style.cssText = styles.closeButton;
        closeBtn.addEventListener('mouseenter', () => {
            closeBtn.style.cssText = styles.closeButton + '; ' + styles.closeButtonHover;
        });
        closeBtn.addEventListener('mouseleave', () => {
            closeBtn.style.cssText = styles.closeButton;
        });
        closeBtn.addEventListener('click', closeSummary);

        titleBar.appendChild(titleText);
        titleBar.appendChild(closeBtn);

        // 内容区域
        const content = document.createElement('div');
        content.id = 'ai-summary-content';
        content.style.cssText = styles.resultContent;

        // 显示加载提示
        content.innerHTML = `<div style="${styles.loadingText}">AI总结中...</div>`;

        box.appendChild(titleBar);
        box.appendChild(content);

        return box;
    }

    // 处理总结请求
    async function handleSummary(type) {
        // 移除之前的结果框(如果存在)
        removeSummaryBox();

        // 创建结果展示框
        const resultBox = createResultBox();

        // 插入到标题下方
        const postTitle = document.querySelector('.post-title');
        if (!postTitle) {
            console.error('未找到标题元素');
            return;
        }
        postTitle.parentNode.insertBefore(resultBox, postTitle.nextSibling);

        // 获取帖子内容
        const content = getPostContent(type);
        console.log(content);

        // 选择对应的 prompt
        const prompt = type === 'main' ? CONFIG.PROMPT_MAIN : CONFIG.PROMPT_ALL;

        // 调用 AI API
        await callAIAPI(resultBox, prompt + content);
    }

    // 调用 AI API
    async function callAIAPI(resultBox, fullPrompt) {
        const contentDiv = document.getElementById('ai-summary-content');
        let accumulatedText = '';

        try {
            const response = await fetch(CONFIG.API_ENDPOINT, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${CONFIG.API_KEY}`
                },
                body: JSON.stringify({
                    model: CONFIG.MODEL,
                    messages: [{
                        role: 'user',
                        content: fullPrompt
                    }],
                    stream: true
                })
            });

            if (!response.ok) {
                throw new Error(`API 请求失败: ${response.status} ${response.statusText}`);
            }

            // 清空加载提示
            contentDiv.innerHTML = '';

            const reader = response.body.getReader();
            const decoder = new TextDecoder();

            while (true) {
                const {done, value} = await reader.read();
                if (done) break;

                const chunk = decoder.decode(value);

                // 处理 SSE 格式的数据
                const lines = chunk.split('\n');
                for (const line of lines) {
                    if (line.startsWith('data: ')) {
                        try {
                            const data = JSON.parse(line.slice(6));
                            const text = data.choices?.[0]?.delta?.content || '';
                            if (text) {
                                accumulatedText += text;
                                // 实时渲染 Markdown
                                contentDiv.innerHTML = renderMarkdown(accumulatedText);
                                contentDiv.scrollTop = contentDiv.scrollHeight;
                            }
                        } catch (e) {
                            // 忽略解析错误
                        }
                    }
                }
            }

            // 最终渲染一次,确保完整
            if (accumulatedText.trim()) {
                contentDiv.innerHTML = renderMarkdown(accumulatedText);
            } else {
                contentDiv.textContent = '未获取到AI返回内容';
            }

        } catch (error) {
            contentDiv.innerHTML = `<div style="color: #d32f2f;">❌ 获取AI总结失败:<br>${error.message}</div>`;
            console.error('AI API 调用错误:', error);
        }
    }

    // 关闭总结框
    function closeSummary() {
        removeSummaryBox();
    }

    // 移除总结框
    function removeSummaryBox() {
        const existingResult = document.getElementById('ai-summary-result');
        if (existingResult) {
            existingResult.remove();
        }
    }

    // ========== 初始化 ==========
    function init() {
        const postTitle = document.querySelector('.post-title');
        if (!postTitle) {
            console.log('未找到标题元素');
            return;
        }

        // 检查是否已经添加过按钮
        if (postTitle.querySelector('.ai-summary-buttons')) {
            return;
        }

        // 注入全局样式
        injectGlobalStyles();

        // 创建按钮容器
        const buttonContainer = createButtonContainer();

        // 将按钮容器插入到标题下一行
        const h1 = postTitle.querySelector('h1');
        if (h1 && h1.parentNode) {
            h1.parentNode.insertBefore(buttonContainer, h1.nextSibling);
        }
    }

    // 页面加载完成后初始化
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();

12

  • @酒神 求加分,我也要升三级口牙!

  • 哈哈,之前我本来想自己写一个的,实在抽不出时间来

  • @yacc #1 已经帮友友加好了 emote
    非常好的技术贴

    image

  • 不错的脚本,免费鸡腿附上

  • 不错

  • 嚯嚯嚯

  • 支持大佬

  • 支持

  • 好东西

12

你好啊,陌生人!

我的朋友,看起来你是新来的,如果想参与到讨论中,点击下面的按钮!

📈用户数目📈

目前论坛共有13522位用户

🎉欢迎新用户🎉