Discourse AI

Olá, você pode fazer esta atualização funcionar para o Azure também?

O conserto do Falco deve funcionar para os endpoints do Azure e da OpenAI. Eles passam pelo mesmo código. Para sua informação, você precisa habilitar o enable_responses_api.

Infelizmente, esta configuração não aparece após selecionar o Azure como provedor.

2 curtidas

Desculpe pela demora. Adicionei as opções específicas do provedor aqui:

3 curtidas

Ele suporta Vertex AI no GCP?

3 curtidas

Você tentou usando OpenAI compatibility  |  Generative AI on Vertex AI  |  Google Cloud?

Espero que este seja o lugar certo para relatar isso. Há um pequeno erro de ortografia.

1 curtida

Obrigado por relatar. Foi perdido durante uma edição em junho. Algumas partes da primeira postagem estão ocultas em comentários HTML, e acho que notei o editor WYSIWYG quebrando isso durante uma edição anterior. Mas houve muitas atualizações desde que a edição aconteceu, então pode já ter sido corrigido.

2 curtidas

Estou tentando descobrir qualquer coisa sobre o recurso de IA do Discord, mas sou completamente incapaz de encontrar alguma coisa :frowning: Como funciona? ou o que ele pode fazer? Estou falando da busca do Discord.

sim sim sim, eu ADORARIA isso.

Estamos pensando em adicionar assinaturas apenas para ter acesso a recursos de IA. Mas ter acesso escalonado à IA seria ainda melhor! Obrigado pela sugestão.

1 curtida

Encontrei um bug (e criei um PR para corrigi-lo) ao tentar usar o Discord Persona Bot

Job exception: context must be an instance of BotContext

/var/www/discourse/plugins/discourse-ai/lib/personas/bot.rb:55:in `reply'
/var/www/discourse/plugins/discourse-ai/lib/discord/bot/persona_replier.rb:25:in `handle_interaction!'
/var/www/discourse/plugins/discourse-ai/app/jobs/regular/stream_discord_reply.rb:13:in `execute'
2 curtidas

O DeepSeek terá suporte quando? É um LLM desenvolvido por uma empresa de tecnologia chinesa e é relativamente barato.

Obrigado.:laughing:

Já é suportado, você tentou alguma funcionalidade nele que não funcionou?

最近马斯克开源了推特推荐算法,能否在这个插件实现?
以下是某个论坛用户写的油猴脚本实现推特推荐算法,理论上是用于任何discourse论坛:

// ==UserScript==
// @name         *** Algorithm
// @namespace    http://tampermonkey.net/
// @version      0.9
// @description  融合 X 算法和 DeepSeek AI 的智能推荐系统,提供个性化主题推荐
// @author       ***
// @match        ****
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @connect      2c2ch1u11-share-api-0.hf.space
// ==/UserScript==

(function() {
    'use strict';

    const API_KEY = '***';
    const API_URL = '***/v1/chat/completions';
    const MODEL = 'deepseek-chat';

    // ========== 算法配置参数(可调整优化) ==========
    const CONFIG = {
        // X 算法权重参数
        WEIGHT_LIKES: 0.5,
        WEIGHT_REPLIES: 13.5,
        WEIGHT_VIEWS: 0.015,
        PINNED_BOOST: 2.0,
        TIME_DECAY_FACTOR: 1.5,
        TIME_DECAY_OFFSET: 2,

        // 评分最大值参数
        X_SCORE_MAX: 45,
        AI_SCORE_MAX: 55,

        // 评分阈值参数
        MAX_SCORE: 100,
        MIN_DISPLAY_SCORE: 20,

        // 缓存参数
        PROFILE_CACHE_TTL: 86400000,      // 用户画像缓存:24小时
        TOPIC_SCORE_CACHE_TTL: 3600000,   // 单个主题AI评分缓存:1小时

        // API 参数
        MAX_TOPICS_PER_BATCH: 30,
        MAX_LIKED_TITLES: 15,       // 点赞主题数量
        MAX_REPLIED_TITLES: 10,     // 回复主题数量
        MAX_CREATED_TITLES: 5,      // 创建主题数量
        API_RETRY_COUNT: 2,
        API_RETRY_DELAY: 1000,

        // 用户画像分析维度
        PROFILE_CATEGORIES: ['技术兴趣', '内容偏好', '互动习惯', '专业领域', '阅读深度'],
    };

    let isRecommendMode = false;
    let scoreMap = {};
    let allTopicsData = {};
    let sortTimeout = null;
    let userProfile = "";
    let isLoading = false;
    let isProcessingNewTopics = false;

    // ========== CSS 样式 ==========
    GM_addStyle(`
        .nav-pills > li > a,
        .nav-pills > li.ember-view > a,
        .navigation-container .nav-pills > li > a {
            border-bottom: 3px solid transparent !important;
            transition: all 0.2s ease;
        }

        body.recommend-mode-active .nav-pills > li:not(#nav-item-recommend) > a,
        body.recommend-mode-active .nav-pills > li.ember-view:not(#nav-item-recommend) > a,
        body.recommend-mode-active .navigation-container .nav-pills > li:not(#nav-item-recommend) > a {
            color: var(--primary-medium) !important;
            border-bottom-color: transparent !important;
        }

        .nav-pills li#nav-item-recommend.active > a,
        body.recommend-mode-active .nav-pills li#nav-item-recommend > a {
            color: var(--tertiary) !important;
            border-bottom: 3px solid var(--tertiary) !important;
            font-weight: 600;
        }

        .nav-pills li#nav-item-recommend > a:hover {
            color: var(--tertiary) !important;
            opacity: 0.8;
        }

        .recommend-loading-container {
            width: 100%;
            padding: 60px 20px;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            min-height: 300px;
        }

        .recommend-loading-text {
            color: var(--primary-medium);
            font-size: 15px;
            margin-bottom: 24px;
            text-align: center;
        }

        .recommend-spinner {
            width: 36px;
            height: 36px;
            border: 3px solid var(--primary-low);
            border-top: 3px solid var(--primary-medium);
            border-radius: 50%;
            animation: spin 0.8s linear infinite;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        .manus-score-badge {
            font-size: 0.75em;
            color: var(--tertiary);
            margin-left: 8px;
            padding: 2px 6px;
            background: var(--tertiary-very-low);
            border-radius: 4px;
            font-weight: 500;
            transition: all 0.3s ease;
        }

        .manus-score-badge.calculating {
            color: var(--primary-medium);
            background: var(--primary-very-low);
            animation: pulse 1.5s ease-in-out infinite;
        }

        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.5; }
        }

        .manus-score-loading {
            font-size: 0.75em;
            color: var(--primary-medium);
            margin-left: 8px;
        }

        .recommend-full-overlay {
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: var(--secondary);
            z-index: 100;
            min-height: 500px;
        }

        .recommend-loading-row {
            position: relative;
            z-index: 101;
        }
    `);

    // ========== 主题AI评分缓存函数 ==========
    function getTopicScoreCache() {
        return GM_getValue('topic_ai_scores_cache', {});
    }

    function setTopicScoreCache(topicId, aiScore) {
        const cache = getTopicScoreCache();
        cache[topicId] = { score: aiScore, time: Date.now() };
        GM_setValue('topic_ai_scores_cache', cache);
    }

    function getCachedTopicScore(topicId) {
        const cache = getTopicScoreCache();
        const entry = cache[topicId];
        if (entry && (Date.now() - entry.time < CONFIG.TOPIC_SCORE_CACHE_TTL)) {
            return entry.score;
        }
        return null;
    }

    function cleanExpiredCache() {
        const cache = getTopicScoreCache();
        const now = Date.now();
        let cleaned = false;
        for (const [id, entry] of Object.entries(cache)) {
            if (now - entry.time > CONFIG.TOPIC_SCORE_CACHE_TTL) {
                delete cache[id];
                cleaned = true;
            }
        }
        if (cleaned) {
            GM_setValue('topic_ai_scores_cache', cache);
        }
    }

    // ========== 注入推荐标签页 ==========
    function injectRecommendTab() {
        if (document.querySelector('#nav-item-recommend')) return;
        const navPills = document.querySelector('.nav-pills');
        if (!navPills) return;

        const recommendTab = document.createElement('li');
        recommendTab.id = 'nav-item-recommend';
        recommendTab.className = 'ember-view nav-item_recommend';
        recommendTab.innerHTML = `<a href="javascript:void(0)">推荐</a>`;

        const latestTab = navPills.querySelector('.nav-item_latest') ||
                          navPills.querySelector('[data-filter-type="latest"]')?.parentElement ||
                          navPills.firstChild;
        navPills.insertBefore(recommendTab, latestTab);

        recommendTab.addEventListener('click', async (e) => {
            e.preventDefault();
            e.stopPropagation();

            if (isLoading) return;

            isRecommendMode = true;
            document.body.classList.add('recommend-mode-active');

            navPills.querySelectorAll('li').forEach(li => {
                li.classList.remove('active');
                li.querySelector('a')?.classList.remove('active');
            });
            recommendTab.classList.add('active');

            cleanExpiredCache();

            showLoading(true);
            await startRecommendation();
            showLoading(false);

            sortAndDisplayRows();
        });

        navPills.querySelectorAll('li:not(#nav-item-recommend)').forEach(tab => {
            tab.addEventListener('click', () => {
                isRecommendMode = false;
                document.body.classList.remove('recommend-mode-active');
                recommendTab.classList.remove('active');
                document.querySelectorAll('.manus-score-badge').forEach(b => b.remove());
                document.querySelectorAll('.manus-score-loading').forEach(b => b.remove());
            });
        });

        window.addEventListener('popstate', () => {
            if (isRecommendMode) {
                isRecommendMode = false;
                document.body.classList.remove('recommend-mode-active');
                recommendTab.classList.remove('active');
            }
        });
    }

    // ========== 加载提示 ==========
    let originalTableContent = null;

    function showLoading(show, text = '正在融合 X 算法与 DeepSeek AI推荐内容...') {
        isLoading = show;

        const topicListContainer = document.querySelector('.topic-list-container') ||
                                   document.querySelector('.topic-list')?.parentElement ||
                                   document.querySelector('.topic-list');
        const tbody = document.querySelector('.topic-list tbody');

        if (!topicListContainer) return;

        if (show) {
            if (!originalTableContent && tbody) {
                originalTableContent = tbody.innerHTML;
            }

            if (tbody) {
                tbody.innerHTML = `
                    <tr class="recommend-loading-row">
                        <td colspan="100%">
                            <div class="recommend-loading-container">
                                <div class="recommend-loading-text">${text}</div>
                                <div class="recommend-spinner"></div>
                            </div>
                        </td>
                    </tr>
                `;
            }

            topicListContainer.style.position = 'relative';

            let overlay = topicListContainer.querySelector('.recommend-full-overlay');
            if (!overlay) {
                overlay = document.createElement('div');
                overlay.className = 'recommend-full-overlay';
                topicListContainer.appendChild(overlay);
            }
        } else {
            topicListContainer.querySelector('.recommend-full-overlay')?.remove();

            if (originalTableContent && tbody) {
                tbody.innerHTML = originalTableContent;
                originalTableContent = null;
            }
        }
    }

    // ========== 提取标题的辅助函数 ==========
    function extractTitles(data, maxCount) {
        if (!data?.user_actions) return '';

        const titles = [...new Set(
            data.user_actions
                .filter(a => a.title)
                .map(a => a.title)
        )].slice(0, maxCount);

        return titles.length > 0 ? titles.join('\n') : '';
    }

    // ========== 优化:更详细的用户画像获取 ==========
    async function fetchUserProfile(username) {
        if (!username) {
            console.log('[推荐] 未登录,使用默认画像');
            return "默认";
        }

        const cacheKey = `user_profile_v9_${username}`;
        const cached = GM_getValue(cacheKey);
        if (cached && (Date.now() - cached.time < CONFIG.PROFILE_CACHE_TTL)) {
            console.log(`[推荐] 用户画像(缓存):\n${cached.profile}`);
            return cached.profile;
        }

        try {
            // 获取多种用户行为数据
            const [likedData, repliedData, createdData] = await Promise.all([
                fetch(`/user_actions.json?username=${username}&filter=1`).then(r => r.json()).catch(() => null), // 点赞
                fetch(`/user_actions.json?username=${username}&filter=5`).then(r => r.json()).catch(() => null), // 回复
                fetch(`/user_actions.json?username=${username}&filter=4`).then(r => r.json()).catch(() => null), // 创建
            ]);

            // 提取不同类型的标题
            const likedTitles = extractTitles(likedData, CONFIG.MAX_LIKED_TITLES);
            const repliedTitles = extractTitles(repliedData, CONFIG.MAX_REPLIED_TITLES);
            const createdTitles = extractTitles(createdData, CONFIG.MAX_CREATED_TITLES);

            if (!likedTitles && !repliedTitles && !createdTitles) return "默认";

            // 构建更详细的分析提示词
            const prompt = `你是一个专业的用户行为分析师。请基于以下用户行为数据,生成一个详细的用户画像:

**用户点赞的主题**(最能反映兴趣):
${likedTitles || '无数据'}

**用户回复过的主题**(反映参与度和专业领域):
${repliedTitles || '无数据'}

**用户创建的主题**(反映主动关注点):
${createdTitles || '无数据'}

请从以下维度分析用户画像,每个维度用一句话概括:
1. **技术兴趣**:用户关注哪些技术栈、工具或平台
2. **内容偏好**:偏好教程、讨论、资讯还是问答类内容
3. **互动习惯**:是深度参与者还是浅层浏览者
4. **专业领域**:主要专注的技术领域或行业方向
5. **阅读深度**:偏好入门级、进阶级还是专家级内容

输出格式(不要编号,直接输出5行):
技术兴趣:[分析结果]
内容偏好:[分析结果]
互动习惯:[分析结果]
专业领域:[分析结果]
阅读深度:[分析结果]`;

            const profile = await callDeepSeek(prompt, false);

            if (profile && profile.length > 0) {
                GM_setValue(cacheKey, { time: Date.now(), profile: profile });
                console.log(`[推荐] 用户画像:\n${profile}`);
                return profile;
            }
            return "默认";
        } catch (e) {
            console.error('[推荐] 获取用户画像失败:', e);
            return "默认";
        }
    }

    // ========== 启动推荐流程 ==========
    async function startRecommendation() {
        try {
            const currentUser = getCurrentUserInfo();

            if (!userProfile) {
                userProfile = await fetchUserProfile(currentUser?.username);
            }

            const response = await fetch('/latest.json');
            const data = await response.json();
            const topics = data.topic_list.topics;

            topics.forEach(t => {
                allTopicsData[t.id] = t;
            });

            const uncachedTopics = [];
            const xScoresRaw = {};
            const aiScoresMap = {};

            topics.forEach(topic => {
                xScoresRaw[topic.id] = calculateXScore(topic);

                const cachedScore = getCachedTopicScore(topic.id);
                if (cachedScore !== null) {
                    aiScoresMap[topic.id] = cachedScore;
                } else {
                    uncachedTopics.push(topic);
                }
            });

            if (uncachedTopics.length > 0) {
                await batchScoreTopics(uncachedTopics, aiScoresMap);
            }

            calculateFinalScores(topics, xScoresRaw, aiScoresMap);

        } catch (e) {
            console.error('[推荐] 推荐流程失败:', e);
        }
    }

    // ========== 批量评分主题 ==========
    async function batchScoreTopics(topics, aiScoresMap) {
        for (let i = 0; i < topics.length; i += CONFIG.MAX_TOPICS_PER_BATCH) {
            const batch = topics.slice(i, i + CONFIG.MAX_TOPICS_PER_BATCH);
            const aiScores = await getAIScoresForBatch(batch);

            batch.forEach(topic => {
                const aiScore = aiScores[topic.id] || 5;
                setTopicScoreCache(topic.id, aiScore);
                aiScoresMap[topic.id] = aiScore;
            });
        }
    }

    // ========== 优化:更精准的批量主题评分 ==========
    async function getAIScoresForBatch(topics) {
        const topicList = topics.map((t, idx) =>
            `${idx + 1}. [ID:${t.id}] ${t.title}`
        ).join('\n');

        const prompt = `你是一个内容推荐专家。请基于用户画像,为以下主题打分。

**用户画像**:
${userProfile}

**待评分主题**:
${topicList}

**评分标准**(0-10分):
- **9-10分**:与用户画像高度匹配,用户极可能感兴趣
- **7-8分**:与用户画像较匹配,用户很可能感兴趣
- **5-6分**:与用户画像部分匹配,用户可能感兴趣
- **3-4分**:与用户画像关联较弱,用户不太可能感兴趣
- **0-2分**:与用户画像无关或相反,用户不感兴趣

**评分考虑因素**:
1. 主题内容与用户技术兴趣的匹配度
2. 主题类型与用户内容偏好的一致性
3. 主题深度与用户阅读深度的适配性
4. 主题领域与用户专业领域的相关性

请返回JSON格式,键为主题ID(纯数字),值为评分(0-10的数字)。
示例:{"123": 8.5, "456": 5.0}

只返回JSON,不要其他文字。`;

        const scores = await callDeepSeek(prompt, true);

        // 清洗和验证评分
        const cleanedScores = {};
        for (const [id, score] of Object.entries(scores)) {
            const numId = parseInt(id, 10);
            const numScore = parseFloat(score);
            if (!isNaN(numId) && !isNaN(numScore) && numScore >= 0 && numScore <= 10) {
                cleanedScores[numId] = Math.round(numScore * 10) / 10;
            }
        }

        return cleanedScores;
    }

    // ========== 优化:更稳健的最终评分计算 ==========
    function calculateFinalScores(topics, xScoresRaw, aiScoresMap) {
        const xScores = Object.values(xScoresRaw);
        if (xScores.length === 0) return;

        // 使用分位数归一化,避免极端值影响
        const sortedX = [...xScores].sort((a, b) => a - b);
        const p5 = sortedX[Math.floor(sortedX.length * 0.05)];  // 5%分位数
        const p95 = sortedX[Math.floor(sortedX.length * 0.95)]; // 95%分位数
        const rangeX = p95 - p5;

        topics.forEach(topic => {
            const id = topic.id;

            let xScoreNormalized;
            if (rangeX === 0) {
                xScoreNormalized = CONFIG.X_SCORE_MAX / 2;
            } else {
                // 限制在 p5 到 p95 范围内
                const clampedX = Math.max(p5, Math.min(p95, xScoresRaw[id]));
                xScoreNormalized = ((clampedX - p5) / rangeX) * CONFIG.X_SCORE_MAX;
            }

            const aiScore = aiScoresMap[id] || 5;
            const aiScoreNormalized = (aiScore / 10) * CONFIG.AI_SCORE_MAX;

            // 最终评分:X算法(45%) + AI评分(55%)
            const finalScore = xScoreNormalized + aiScoreNormalized;
            scoreMap[id] = Math.round(finalScore * 10) / 10;
        });
    }

    // ========== 计算新主题的最终评分 ==========
    function calculateFinalScoresForNew(topicIds, xScoresRaw, aiScoresMap) {
        const allXScores = Object.values(scoreMap).length > 0
            ? [...Object.values(xScoresRaw), ...Object.keys(allTopicsData).map(id => calculateXScore(allTopicsData[id]))]
            : Object.values(xScoresRaw);

        if (allXScores.length === 0) return;

        const sortedX = [...allXScores].sort((a, b) => a - b);
        const p5 = sortedX[Math.floor(sortedX.length * 0.05)];
        const p95 = sortedX[Math.floor(sortedX.length * 0.95)];
        const rangeX = p95 - p5;

        topicIds.forEach(id => {
            if (xScoresRaw[id] === undefined) return;

            let xScoreNormalized;
            if (rangeX === 0) {
                xScoreNormalized = CONFIG.X_SCORE_MAX / 2;
            } else {
                const clampedX = Math.max(p5, Math.min(p95, xScoresRaw[id]));
                xScoreNormalized = ((clampedX - p5) / rangeX) * CONFIG.X_SCORE_MAX;
            }

            const aiScore = aiScoresMap[id] || 5;
            const aiScoreNormalized = (aiScore / 10) * CONFIG.AI_SCORE_MAX;

            const finalScore = xScoreNormalized + aiScoreNormalized;
            scoreMap[id] = Math.round(finalScore * 10) / 10;
        });
    }

    // ========== 处理新加载的主题(滚动加载) ==========
    async function processNewTopics(newRows) {
        if (!isRecommendMode || isProcessingNewTopics) return;
        isProcessingNewTopics = true;

        try {
            const newTopicIds = [];
            newRows.forEach(row => {
                const id = row.getAttribute('data-topic-id');
                if (id && !scoreMap[id]) {
                    newTopicIds.push(id);
                }
            });

            if (newTopicIds.length === 0) {
                isProcessingNewTopics = false;
                return;
            }

            newTopicIds.forEach(id => {
                const row = document.querySelector(`tr[data-topic-id="${id}"]`);
                if (row) {
                    addCalculatingBadge(row);
                }
            });

            const uncachedTopics = [];
            const xScoresRaw = {};
            const aiScoresMap = {};

            for (const id of newTopicIds) {
                const topicData = allTopicsData[id] || await fetchTopicData(id);
                if (!topicData) continue;

                allTopicsData[id] = topicData;
                xScoresRaw[id] = calculateXScore(topicData);

                const cachedScore = getCachedTopicScore(id);
                if (cachedScore !== null) {
                    aiScoresMap[id] = cachedScore;
                } else {
                    uncachedTopics.push(topicData);
                }
            }

            if (uncachedTopics.length > 0) {
                await batchScoreTopics(uncachedTopics, aiScoresMap);
            }

            calculateFinalScoresForNew(newTopicIds, xScoresRaw, aiScoresMap);

            updateRowBadges();

        } catch (e) {
            console.error('[推荐] 处理新主题失败:', e);
        }

        isProcessingNewTopics = false;
    }

    // ========== 获取单个主题数据 ==========
    async function fetchTopicData(topicId) {
        try {
            const row = document.querySelector(`tr[data-topic-id="${topicId}"]`);
            if (row) {
                const title = row.querySelector('.title')?.textContent?.trim() || '';
                const replies = parseInt(row.querySelector('.posts')?.textContent) || 0;
                const views = parseInt(row.querySelector('.views')?.textContent?.replace(/[k,]/gi, '')) || 0;
                const likes = parseInt(row.querySelector('.likes')?.textContent) || 0;

                return {
                    id: topicId,
                    title: title,
                    posts_count: replies,
                    views: views,
                    like_count: likes,
                    created_at: new Date().toISOString(),
                    pinned: row.classList.contains('pinned')
                };
            }
            return null;
        } catch (e) {
            return null;
        }
    }

    // ========== 排序并显示 ==========
    function sortAndDisplayRows() {
        if (!isRecommendMode) return;
        const tbody = document.querySelector('.topic-list tbody');
        if (!tbody) return;

        const rows = Array.from(tbody.querySelectorAll('tr.topic-list-item'));

        rows.sort((a, b) => {
            const idA = a.getAttribute('data-topic-id');
            const idB = b.getAttribute('data-topic-id');
            const scoreA = scoreMap[idA] || 0;
            const scoreB = scoreMap[idB] || 0;
            return scoreB - scoreA;
        });

        let hiddenCount = 0;
        rows.forEach(row => {
            const id = row.getAttribute('data-topic-id');
            const score = scoreMap[id];

            if (score === undefined) {
                row.style.display = '';
                addCalculatingBadge(row);
            } else if (score < CONFIG.MIN_DISPLAY_SCORE) {
                row.style.display = 'none';
                hiddenCount++;
            } else {
                row.style.display = '';
                addScoreBadge(row);
            }
            tbody.appendChild(row);
        });
    }

    // ========== 更新所有行的徽章 ==========
    function updateRowBadges() {
        if (!isRecommendMode) return;
        const rows = document.querySelectorAll('tr.topic-list-item');
        rows.forEach(row => {
            const id = row.getAttribute('data-topic-id');
            const score = scoreMap[id];

            if (score === undefined) {
                addCalculatingBadge(row);
            } else if (score < CONFIG.MIN_DISPLAY_SCORE) {
                row.style.display = 'none';
            } else {
                row.style.display = '';
                addScoreBadge(row);
            }
        });
    }

    // ========== 添加评分徽章 ==========
    function addScoreBadge(row) {
        const id = row.getAttribute('data-topic-id');
        const score = scoreMap[id];

        row.querySelector('.manus-score-loading')?.remove();

        if (score !== undefined) {
            let badge = row.querySelector('.manus-score-badge');
            if (!badge) {
                badge = document.createElement('span');
                badge.className = 'manus-score-badge';
                const titleContainer = row.querySelector('.link-top-line') ||
                                      row.querySelector('.title')?.parentElement ||
                                      row.querySelector('.main-link') ||
                                      row.querySelector('td:first-child');
                if (titleContainer) {
                    titleContainer.appendChild(badge);
                }
            }
            if (badge) {
                badge.textContent = `🔥 ${score.toFixed(1)}`;
                badge.classList.remove('calculating');
            }
        }
    }

    // ========== 添加"计算中"徽章 ==========
    function addCalculatingBadge(row) {
        const id = row.getAttribute('data-topic-id');

        if (scoreMap[id] !== undefined) return;

        let badge = row.querySelector('.manus-score-badge');
        if (!badge) {
            badge = document.createElement('span');
            badge.className = 'manus-score-badge calculating';
            const titleContainer = row.querySelector('.link-top-line') ||
                                  row.querySelector('.title')?.parentElement ||
                                  row.querySelector('.main-link') ||
                                  row.querySelector('td:first-child');
            if (titleContainer) {
                titleContainer.appendChild(badge);
            }
        }
        if (badge) {
            badge.textContent = '⏳ 计算中...';
            badge.classList.add('calculating');
        }
    }

    // ========== 优化:改进 JSON 清洗逻辑 ==========
    function cleanJsonResponse(content) {
        if (!content) return content;

        let cleaned = content.trim();

        // 移除 Markdown 代码块标记
        cleaned = cleaned.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '');

        // 移除可能的文字说明
        const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
        if (jsonMatch) {
            cleaned = jsonMatch[0];
        }

        return cleaned.trim();
    }

    // ========== API 调用 ==========
    async function callDeepSeek(prompt, isJson = true, retryCount = 0) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "POST",
                url: API_URL,
                headers: {
                    "Content-Type": "application/json",
                    "Authorization": `Bearer ${API_KEY}`
                },
                data: JSON.stringify({
                    model: MODEL,
                    messages: [{ role: "user", content: prompt }],
                    temperature: 0.3,
                    max_tokens: 1000
                }),
                timeout: 30000,
                onload: (res) => {
                    try {
                        if (res.status >= 200 && res.status < 300) {
                            let content = JSON.parse(res.responseText).choices[0].message.content;

                            if (isJson) {
                                content = cleanJsonResponse(content);
                                resolve(JSON.parse(content));
                            } else {
                                resolve(content);
                            }
                        } else {
                            throw new Error(`HTTP ${res.status}`);
                        }
                    } catch(e) {
                        console.error('[推荐] API 响应解析失败:', e);
                        if (retryCount < CONFIG.API_RETRY_COUNT) {
                            setTimeout(() => {
                                callDeepSeek(prompt, isJson, retryCount + 1).then(resolve);
                            }, CONFIG.API_RETRY_DELAY);
                        } else {
                            resolve(isJson ? {} : "");
                        }
                    }
                },
                onerror: (err) => {
                    console.error('[推荐] API 调用失败:', err);
                    if (retryCount < CONFIG.API_RETRY_COUNT) {
                        setTimeout(() => {
                            callDeepSeek(prompt, isJson, retryCount + 1).then(resolve);
                        }, CONFIG.API_RETRY_DELAY);
                    } else {
                        resolve(isJson ? {} : "");
                    }
                },
                ontimeout: () => {
                    console.error('[推荐] API 调用超时');
                    if (retryCount < CONFIG.API_RETRY_COUNT) {
                        setTimeout(() => {
                            callDeepSeek(prompt, isJson, retryCount + 1).then(resolve);
                        }, CONFIG.API_RETRY_DELAY);
                    } else {
                        resolve(isJson ? {} : "");
                    }
                }
            });
        });
    }

    // ========== 优化:更智能的 X 算法评分 ==========
    function calculateXScore(topic) {
        const now = new Date();
        const created = new Date(topic.created_at);
        const hoursOld = Math.max(0, (now - created) / (1000 * 60 * 60));

        // 基础互动评分
        const likeScore = (topic.like_count || 0) * CONFIG.WEIGHT_LIKES;
        const replyScore = (topic.posts_count || 0) * CONFIG.WEIGHT_REPLIES;
        const viewScore = (topic.views || 0) * CONFIG.WEIGHT_VIEWS;

        // 计算互动率加成(高互动率的内容质量更高)
        const views = topic.views || 1;
        const engagementRate = ((topic.like_count || 0) + (topic.posts_count || 0)) / views;
        const engagementBoost = 1 + Math.min(engagementRate * 10, 2); // 最多2倍加成

        let score = (likeScore + replyScore + viewScore) * engagementBoost;

        // 置顶加成
        if (topic.pinned) {
            score *= CONFIG.PINNED_BOOST;
        }

        // 时间衰减(新鲜度)
        const timeFactor = Math.pow(hoursOld + CONFIG.TIME_DECAY_OFFSET, CONFIG.TIME_DECAY_FACTOR);

        return score / timeFactor;
    }

    // ========== 获取当前用户信息 ==========
    function getCurrentUserInfo() {
        try {
            // 方法 1: Discourse 容器
            const container = window.Discourse?.__container__;
            if (container) {
                const currentUser = container.lookup('service:current-user');
                if (currentUser?.username) {
                    return {
                        username: currentUser.username,
                        id: currentUser.id,
                        name: currentUser.name,
                        trust_level: currentUser.trust_level
                    };
                }
            }

            // 方法 2: User.current()
            if (window.User?.current?.()?.username) {
                const user = window.User.current();
                return { username: user.username, id: user.id };
            }

            // 方法 3: #current-user
            const userLink = document.querySelector('#current-user a[data-user-card]');
            if (userLink) {
                return { username: userLink.getAttribute('data-user-card') };
            }

            // 方法 4: header
            const headerUser = document.querySelector('.header-dropdown-toggle.current-user button');
            if (headerUser) {
                const img = headerUser.querySelector('img');
                if (img?.alt) {
                    return { username: img.alt };
                }
            }

            // 方法 5: d-header
            const anyUserCard = document.querySelector('.d-header [data-user-card]');
            if (anyUserCard) {
                return { username: anyUserCard.getAttribute('data-user-card') };
            }

            // 方法 6: preload 数据
            const preloadData = document.querySelector('#data-preloaded');
            if (preloadData) {
                try {
                    const data = JSON.parse(preloadData.dataset.preloaded || '{}');
                    const currentUser = JSON.parse(data.currentUser || '{}');
                    if (currentUser.username) {
                        return { username: currentUser.username };
                    }
                } catch (e) {}
            }

            return null;
        } catch (e) {
            return null;
        }
    }

    // ========== 监听DOM变化 ==========
    const listObserver = new MutationObserver((mutations) => {
        if (!isRecommendMode || isLoading) return;

        const newRows = [];
        mutations.forEach(m => {
            m.addedNodes.forEach(node => {
                if (node.nodeType === 1) {
                    if (node.classList?.contains('topic-list-item')) {
                        newRows.push(node);
                    } else if (node.querySelectorAll) {
                        node.querySelectorAll('.topic-list-item').forEach(row => newRows.push(row));
                    }
                }
            });
        });

        if (newRows.length > 0) {
            newRows.forEach(row => {
                const id = row.getAttribute('data-topic-id');
                if (id && scoreMap[id] === undefined) {
                    addCalculatingBadge(row);
                } else if (scoreMap[id] !== undefined) {
                    if (scoreMap[id] >= CONFIG.MIN_DISPLAY_SCORE) {
                        addScoreBadge(row);
                    } else {
                        row.style.display = 'none';
                    }
                }
            });

            clearTimeout(sortTimeout);
            sortTimeout = setTimeout(() => processNewTopics(newRows), 500);
        }
    });

    const navObserver = new MutationObserver(() => {
        injectRecommendTab();
        const tbody = document.querySelector('.topic-list tbody');
        if (tbody && !tbody.dataset.observing) {
            tbody.dataset.observing = 'true';
            listObserver.observe(tbody, { childList: true });
        }
    });

    // ========== 启动脚本 ==========
    navObserver.observe(document.body, { childList: true, subtree: true });
    injectRecommendTab();

    console.log('[推荐系统] 已加载 - 融合 X 算法与 DeepSeek AI');

})();

以下是在discourse论坛的效果:


1 curtida