• tibbar

    // ==UserScript==
    // @name LINUX DO Algorithm
    // @namespace http://tampermonkey.net/
    // @version 0.9
    // @description 融合 X 算法和 DeepSeek AI 的智能推荐系统,提供个性化主题推荐
    // @author SMNET
    // @…

    基于神墨佬的小玩具改了一版,接入了jev打分```
    // ==UserScript==
    // @name LINUX DO Algorithm + Jev
    // @namespace http://tampermonkey.net/
    // @version 1.4.2
    // @description 热度与个性化推荐:独立配置画像模型、Jev 渠道、评分模型和权重
    // @author SMNET / Tibbar
    // @match https://linux.do/*
    // @grant GM_xmlhttpRequest
    // @grant GM_setValue
    // @grant GM_getValue
    // @grant GM_addStyle
    // @grant GM_registerMenuCommand
    // @connect openrouter.ai
    // @connect api.typesafe.ai
    // @connect *
    // ==/UserScript==

    (function() {
    'use strict';

    const SETTINGS_KEY = 'linux_do_algorithm_settings_v2';
    const LEGACY_SETTINGS_KEY = 'linux_do_algorithm_settings_v1';
    const DEFAULT_SETTINGS = {
    scoreEngine: 'jev',
    jevProvider: 'typesafe',
    jevApiKey: '',
    jevModel: 'jev-latest',
    profileMode: 'summary',
    profileApiKey: '',
    profileApiUrl: 'https://api.deepseek.com/v1/chat/completions',
    profileModel: 'deepseek-chat',
    scoreApiKey: '',
    scoreApiUrl: 'https://api.deepseek.com/v1/chat/completions',
    scoreModel: 'deepseek-chat',
    customProfile: '',
    autoScoreAllLists: false,
    heatWeight: 45,
    interestWeight: 55,
    minDisplayScore: 20
    };

    function loadSettings() {
    const saved = GM_getValue(SETTINGS_KEY, null);
    if (saved) return { ...DEFAULT_SETTINGS, ...saved };
    const legacy = GM_getValue(LEGACY_SETTINGS_KEY, null);
    if (!legacy) return { ...DEFAULT_SETTINGS };
    return {
    ...DEFAULT_SETTINGS,
    scoreEngine: legacy.provider === 'deepseek' ? 'chat' : 'jev',
    jevProvider: 'openrouter',
    jevApiKey: legacy.openRouterApiKey || '',
    jevModel: String(legacy.jevModel || 'jev-latest').replace(/^~?typesafe\//, ''),
    profileMode: legacy.chatApiKey ? 'llm' : 'summary',
    profileApiKey: legacy.chatApiKey || '',
    profileApiUrl: legacy.chatApiUrl || DEFAULT_SETTINGS.profileApiUrl,
    profileModel: legacy.chatModel || DEFAULT_SETTINGS.profileModel,
    scoreApiKey: legacy.chatApiKey || '',
    scoreApiUrl: legacy.chatApiUrl || DEFAULT_SETTINGS.scoreApiUrl,
    scoreModel: legacy.chatModel || DEFAULT_SETTINGS.scoreModel,
    customProfile: legacy.customProfile || '',
    heatWeight: legacy.heatWeight ?? 45,
    interestWeight: legacy.interestWeight ?? 55,
    minDisplayScore: legacy.minDisplayScore ?? 20
    };
    }

    let settings = loadSettings();

    // ========== 算法配置参数 ==========
    const CONFIG = {
    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,
    MAX_SCORE: 100,
    PROFILE_CACHE_TTL: 86400000,
    TOPIC_SCORE_CACHE_TTL: 3600000,
    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 isPageScoreMode = false;
    let scoreMap = {};
    let allTopicsData = {};
    let sortTimeout = null;
    let autoScoreTimeout = 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;
    }
    .lda-settings-backdrop {
    position: fixed; inset: 0; z-index: 10000;
    display: flex; align-items: center; justify-content: center;
    padding: 20px; background: rgba(0, 0, 0, 0.55);
    }
    .lda-settings-panel {
    width: min(680px, 100%); max-height: 90vh; overflow: auto;
    padding: 24px; border-radius: 12px;
    color: var(--primary); background: var(--secondary);
    box-shadow: 0 16px 48px rgba(0, 0, 0, 0.3);
    }
    .lda-settings-panel h2 { margin: 0 0 18px; }
    .lda-settings-panel label { display: block; margin: 14px 0 6px; font-weight: 600; }
    .lda-settings-panel input, .lda-settings-panel select, .lda-settings-panel textarea {
    box-sizing: border-box; width: 100%; padding: 9px 10px;
    border: 1px solid var(--primary-low-mid); border-radius: 6px;
    color: var(--primary); background: var(--secondary);
    }
    .lda-settings-panel textarea { min-height: 160px; resize: vertical; }
    .lda-settings-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
    .lda-settings-help { margin-top: 5px; color: var(--primary-medium); font-size: 12px; }
    .lda-settings-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
    .lda-settings-actions button { padding: 8px 16px; cursor: pointer; }
    .lda-settings-error { min-height: 20px; margin-top: 12px; color: var(--danger); }
    .lda-settings-panel [hidden] { display: none !important; }
    .lda-settings-status { min-height: 20px; margin-top: 8px; color: var(--primary-medium); }
    .lda-settings-status[data-state="error"] { color: var(--danger); }
    .lda-settings-status[data-state="success"] { color: var(--success, #22863a); }
    .lda-checkbox { display: flex !important; align-items: center; gap: 8px; font-weight: 600; }
    .lda-checkbox input { width: auto !important; margin: 0; }
    @media (max-width: 600px) { .lda-settings-grid { grid-template-columns: 1fr; } }
    `);

    // ========== 主题AI评分缓存函数 ==========
    function getTopicScoreCache() {
    return GM_getValue('topic_ai_scores_cache', {});
    }
    function getScoreContextKey() {
    const source = interest-v2-noul|${settings.scoreEngine}|${settings.jevProvider}|${settings.jevModel}|${settings.scoreApiUrl}|${settings.scoreModel}|${userProfile};
    let hash = 2166136261;
    for (let i = 0; i < source.length; i++) {
    hash ^= source.charCodeAt(i);
    hash = Math.imul(hash, 16777619);
    }
    return (hash >>> 0).toString(36);
    }

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

    function getCachedTopicScore(topicId) {
    const cache = getTopicScoreCache();
    const entry = cache[topicId];
    if (entry && entry.context === getScoreContextKey() && (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 clearRecommendationState() {
    userProfile = '';
    scoreMap = {};
    allTopicsData = {};
    GM_setValue('topic_ai_scores_cache', {});
    }

    function getBlendWeights() {
    const heat = Math.max(0, Number(settings.heatWeight) || 0);
    const interest = Math.max(0, Number(settings.interestWeight) || 0);
    const total = heat + interest || 1;
    return {
    heatMax: CONFIG.MAX_SCORE * heat / total,
    interestMax: CONFIG.MAX_SCORE * interest / total
    };
    }

    function openSettings() {
    document.querySelector('.lda-settings-backdrop')?.remove();
    const backdrop = document.createElement('div');
    backdrop.className = 'lda-settings-backdrop';
    backdrop.innerHTML = `
    <form class="lda-settings-panel">
    <h2>推荐算法设置</h2>
    <h3>个人画像</h3>
    <label>手工画像</label>
    <textarea name="customProfile" placeholder="填写后优先使用;留空则自动构建。"></textarea>
    <div class="lda-settings-actions" style="justify-content:flex-start;margin-top:8px">
    <button type="button" data-action="load-profile">载入当前自动画像</button>
    <button type="button" data-action="clear-profile">恢复自动画像</button>
    <button type="button" data-action="generate-profile">调用画像模型生成画像</button>
    </div>
    <label>自动画像方式</label>
    <select name="profileMode">
    <option value="summary">直接使用行为摘要(不调用模型)</option>
    <option value="llm">调用独立画像模型</option>
    </select>
    <div class="lda-settings-grid">
    <div><label>画像 API Key</label><input name="profileApiKey" type="password" autocomplete="off"></div>
    <div><label>画像模型</label><input name="profileModel" placeholder="deepseek-chat"></div>
    </div>
    <label>画像 API URL(OpenAI 兼容)</label>
    <input name="profileApiUrl" placeholder="https://api.deepseek.com/v1/chat/completions"&gt;
    <div class="lda-settings-help">画像模型只归纳行为记录,不参与主题评分。</div>
    <button type="button" data-action="test-profile">测试画像模型连接</button>
    <div class="lda-settings-status" data-status="profile" role="status" aria-live="polite"></div>

    <h3>兴趣评分</h3>
    <label>评分引擎</label>
    <select name="scoreEngine">
    <option value="jev">Jev 结构化评分</option>
    <option value="chat">OpenAI 兼容聊天模型评分</option>
    </select>
    <div data-group="jev">
    <label>Jev 渠道</label>
    <select name="jevProvider">
    <option value="typesafe">TypeSafe 官方 API</option>
    <option value="openrouter">OpenRouter System One API</option>
    </select>
    <div class="lda-settings-grid">
    <div><label>Jev API Key</label><input name="jevApiKey" type="password" autocomplete="off"></div>
    <div><label>Jev 评分模型</label><input name="jevModel" placeholder="jev-latest"></div>
    </div>
    </div>
    <div data-group="chat">
    <div class="lda-settings-grid">
    <div><label>聊天评分 API Key</label><input name="scoreApiKey" type="password" autocomplete="off"></div>
    <div><label>聊天评分模型</label><input name="scoreModel" placeholder="deepseek-chat"></div>
    </div>
    <label>聊天评分 API URL(OpenAI 兼容)</label>
    <input name="scoreApiUrl" placeholder="https://api.deepseek.com/v1/chat/completions"&gt;
    <div class="lda-settings-help">评分模型只根据画像计算兴趣分,与画像模型完全独立。</div>
    </div>
    <button type="button" data-action="test-score">测试当前评分引擎连接</button>
    <div class="lda-settings-status" data-status="score" role="status" aria-live="polite"></div>

    <h3>自动评分</h3>
    <label class="lda-checkbox">
    <input name="autoScoreAllLists" type="checkbox">
    <span>所有帖子列表自动评分</span>
    </label>
    <div class="lda-settings-help">开启后,分类、标签、最新、热门等列表会自动显示分数;不改变原顺序,也不隐藏低分帖子。</div>

    <h3>分数合成</h3>
    <div class="lda-settings-grid">
    <div><label>热度权重</label><input name="heatWeight" type="number" min="0" max="100" step="1"></div>
    <div><label>兴趣权重</label><input name="interestWeight" type="number" min="0" max="100" step="1"></div>
    </div>
    <div class="lda-settings-help">修改任一权重时,另一项自动补足到 100。</div>
    <label>最低显示分数</label>
    <input name="minDisplayScore" type="number" min="0" max="100" step="1">
    <div class="lda-settings-error" role="alert"></div>
    <div class="lda-settings-actions">
    <button type="button" data-action="cancel">取消</button>
    <button type="submit" class="btn-primary">保存并清除评分缓存</button>
    </div>
    </form>`;
    const form = backdrop.querySelector('form');

    const secretFields = ['jevApiKey', 'profileApiKey', 'scoreApiKey'];
    for (const [key, value] of Object.entries(settings)) {
    if (secretFields.includes(key)) continue;
    const field = form.elements.namedItem(key);
    if (field) field.value = value;
    }
    form.elements.namedItem('autoScoreAllLists').checked = Boolean(settings.autoScoreAllLists);
    secretFields.forEach(key => {
    if (settings[key]) form.elements.namedItem(key).placeholder = '已保存;留空则保持不变';
    });
    const updateScoreFields = () => {
    const useJev = form.elements.namedItem('scoreEngine').value === 'jev';
    form.querySelector('[data-group="jev"]').hidden = !useJev;
    form.querySelector('[data-group="chat"]').hidden = useJev;
    form.querySelector('[data-status="score"]').textContent = '';
    };
    form.elements.namedItem('scoreEngine').addEventListener('change', updateScoreFields);
    updateScoreFields();
    const heatInput = form.elements.namedItem('heatWeight');
    const interestInput = form.elements.namedItem('interestWeight');
    const previousHeat = Number(heatInput.value);
    const previousInterest = Number(interestInput.value);
    const previousTotal = previousHeat + previousInterest;
    if (Number.isFinite(previousTotal) && previousTotal > 0 && previousTotal !== 100) {
    interestInput.value = Math.round(previousInterest / previousTotal * 100);
    heatInput.value = 100 - Number(interestInput.value);
    }
    const linkWeights = (edited, other) => {
    edited.addEventListener('input', () => {
    if (edited.value === '') return;
    const value = Number(edited.value);
    if (Number.isFinite(value) && value >= 0 && value <= 100) {
    other.value = 100 - value;
    }
    });
    };
    linkWeights(heatInput, interestInput);
    linkWeights(interestInput, heatInput);

    const close = () => backdrop.remove();
    backdrop.addEventListener('click', (event) => {
    if (event.target === backdrop) close();
    });
    form.querySelector('[data-action="cancel"]').addEventListener('click', close);
    form.querySelector('[data-action="clear-profile"]').addEventListener('click', () => {
    form.elements.namedItem('customProfile').value = '';
    });
    form.querySelector('[data-action="load-profile"]').addEventListener('click', async (event) => {
    const button = event.currentTarget;
    const originalText = button.textContent;
    button.disabled = true;
    button.textContent = '正在载入...';
    try {
    const profile = userProfile || await fetchUserProfile(await resolveUsername());
    form.elements.namedItem('customProfile').value = profile;
    } catch (error) {
    form.querySelector('.lda-settings-error').textContent = 画像载入失败:${error.message || error};
    } finally {
    button.disabled = false;
    button.textContent = originalText;
    }
    });
    const fieldValue = name => String(form.elements.namedItem(name).value).trim();
    const draftOptions = () => ({
    jevProvider: fieldValue('jevProvider'),
    jevApiKey: fieldValue('jevApiKey') || settings.jevApiKey,
    jevModel: fieldValue('jevModel') || DEFAULT_SETTINGS.jevModel,
    profileApiKey: fieldValue('profileApiKey') || settings.profileApiKey,
    profileApiUrl: fieldValue('profileApiUrl') || DEFAULT_SETTINGS.profileApiUrl,
    profileModel: fieldValue('profileModel') || DEFAULT_SETTINGS.profileModel,
    scoreApiKey: fieldValue('scoreApiKey') || settings.scoreApiKey,
    scoreApiUrl: fieldValue('scoreApiUrl') || DEFAULT_SETTINGS.scoreApiUrl,
    scoreModel: fieldValue('scoreModel') || DEFAULT_SETTINGS.scoreModel
    });
    const runAction = (action, kind, callback) => {
    form.querySelector([data-action="${action}"]).addEventListener('click', async event => {
    const button = event.currentTarget;
    const status = form.querySelector([data-status="${kind}"]);
    const originalLabel = button.textContent;
    button.disabled = true;
    status.dataset.state = '';
    status.textContent = '请求中...';
    try {
    const message = await callback(draftOptions());
    if (backdrop.isConnected) {
    status.dataset.state = 'success';
    status.textContent = message;
    }
    } catch (error) {
    if (backdrop.isConnected) {
    status.dataset.state = 'error';
    status.textContent = 失败:${error.message || error};
    }
    } finally {
    button.disabled = false;
    button.textContent = originalLabel;
    }
    });
    };
    runAction('test-profile', 'profile', async options => {
    const result = await callChatModel('profile', '请只回复:连接成功', false, options);
    if (!result) throw new Error('画像模型返回空内容');
    return '画像模型连接成功(已发起一次实际请求)。';
    });
    runAction('generate-profile', 'profile', async options => {
    const textarea = form.elements.namedItem('customProfile');
    if (textarea.value.trim() && !window.confirm('生成结果将覆盖文本框中未保存的画像,继续吗?')) return '已取消,原画像未改变。';
    const summary = await getBehaviorSummary(await resolveUsername());
    const profile = await callChatModel('profile', buildProfilePrompt(summary), false, options);
    if (!profile.trim()) throw new Error('画像模型返回空内容');
    if (backdrop.isConnected) textarea.value = profile.trim();
    return '画像已生成到文本框;检查并点击保存后才会生效。';
    });
    runAction('test-score', 'score', async options => {
    if (fieldValue('scoreEngine') === 'jev') {
    const answers = await callJev({ user_profile: '喜欢 JavaScript 教程', topic: 'JavaScript 入门教程' }, {
    connection_test: { type: 'noul', instructions: '根据 user_profile,用户会对 topic 感兴趣吗?' }
    }, options);
    const probability = answers.connection_test?.noul;
    if (typeof probability !== 'number' || !Number.isFinite(probability) || probability < 0 || probability > 1) throw new Error('Jev 未返回有效概率');
    return Jev 连接成功,兴趣概率 ${(probability * 100).toFixed(1)}%。;
    }
    const scores = await callChatModel('score', '只返回 JSON:{"1": 7},不要附加说明。', true, options);
    const score = Number(scores?.['1']);
    if (!Number.isFinite(score) || score < 0 || score > 10) throw new Error('聊天评分模型未返回有效 JSON 分数');
    return 聊天评分模型连接成功,测试评分 ${score}/10。;
    });
    form.addEventListener('submit', (event) => {
    event.preventDefault();
    const formData = new FormData(form);
    const heatWeight = Number(formData.get('heatWeight'));
    const interestWeight = Number(formData.get('interestWeight'));
    const minDisplayScore = Number(formData.get('minDisplayScore'));
    const error = form.querySelector('.lda-settings-error');
    if (heatInput.value === '' || interestInput.value === '' ||
    !Number.isFinite(heatWeight) || !Number.isFinite(interestWeight) || !Number.isFinite(minDisplayScore) ||
    heatWeight < 0 || heatWeight > 100 || interestWeight < 0 || interestWeight > 100 ||
    heatWeight + interestWeight !== 100 || minDisplayScore < 0 || minDisplayScore > 100) {
    error.textContent = '热度与兴趣权重都须在 0–100 之间,且合计为 100;最低显示分数须在 0–100 之间。';
    return;
    }
    settings = {
    scoreEngine: String(formData.get('scoreEngine')),
    jevProvider: String(formData.get('jevProvider')),
    jevApiKey: String(formData.get('jevApiKey')).trim() || settings.jevApiKey,
    jevModel: String(formData.get('jevModel')).trim().replace(/^~?typesafe\//, '') || DEFAULT_SETTINGS.jevModel,
    profileMode: String(formData.get('profileMode')),
    profileApiKey: String(formData.get('profileApiKey')).trim() || settings.profileApiKey,
    profileApiUrl: String(formData.get('profileApiUrl')).trim() || DEFAULT_SETTINGS.profileApiUrl,
    profileModel: String(formData.get('profileModel')).trim() || DEFAULT_SETTINGS.profileModel,
    scoreApiKey: String(formData.get('scoreApiKey')).trim() || settings.scoreApiKey,
    scoreApiUrl: String(formData.get('scoreApiUrl')).trim() || DEFAULT_SETTINGS.scoreApiUrl,
    scoreModel: String(formData.get('scoreModel')).trim() || DEFAULT_SETTINGS.scoreModel,
    customProfile: String(formData.get('customProfile')).trim(),
    autoScoreAllLists: form.elements.namedItem('autoScoreAllLists').checked,
    heatWeight,
    interestWeight,
    minDisplayScore
    };
    GM_setValue(SETTINGS_KEY, settings);
    clearRecommendationState();
    close();
    if (isRecommendMode) startRecommendation().then(sortAndDisplayRows);
    else if (settings.autoScoreAllLists) scheduleAutoScoring();
    else {
    isPageScoreMode = false;
    document.querySelectorAll('.manus-score-badge, .manus-score-loading').forEach(badge => badge.remove());
    }
    });
    document.body.appendChild(backdrop);
    form.elements.namedItem('profileMode').focus();
    }

    function hasScoringCredentials() {
    const scoreReady = settings.scoreEngine === 'jev' ? Boolean(settings.jevApiKey) : Boolean(settings.scoreApiKey);
    const profileReady = Boolean(settings.customProfile) || settings.profileMode !== 'llm' || Boolean(settings.profileApiKey);
    return scoreReady && profileReady;
    }

    function scheduleAutoScoring(delay = 150) {
    clearTimeout(autoScoreTimeout);
    if (!settings.autoScoreAllLists || isRecommendMode || isLoading || !hasScoringCredentials()) return;
    autoScoreTimeout = setTimeout(() => {
    if (!settings.autoScoreAllLists || isRecommendMode || isLoading) return;
    scoreCurrentPage();
    }, delay);
    }

    // ========== 注入推荐标签页 ==========
    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;
    const missingScoreKey = settings.scoreEngine === 'jev'
    ? !settings.jevApiKey
    : !settings.scoreApiKey;
    const missingProfileKey = !settings.customProfile && settings.profileMode === 'llm' && !settings.profileApiKey;
    if (missingScoreKey || missingProfileKey) {
    openSettings();
    return;
    }

    isRecommendMode = true;
    isPageScoreMode = false;
    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;
    isPageScoreMode = settings.autoScoreAllLists;
    document.body.classList.remove('recommend-mode-active');
    recommendTab.classList.remove('active');
    document.querySelectorAll('.manus-score-badge, .manus-score-loading').forEach(badge => badge.remove());
    scheduleAutoScoring(300);
    });
    });

    window.addEventListener('popstate', () => {
    isRecommendMode = false;
    isPageScoreMode = settings.autoScoreAllLists;
    document.body.classList.remove('recommend-mode-active');
    recommendTab.classList.remove('active');
    document.querySelectorAll('.manus-score-badge, .manus-score-loading').forEach(badge => badge.remove());
    scheduleAutoScoring(300);
    });
    }

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

    function showLoading(show, text = 正在融合热度算法与 ${settings.scoreEngine === 'jev' ? 'Jev' : settings.scoreModel} 兴趣评分...) {
    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 getBehaviorSummary(username) {
    if (!username) throw new Error('无法从当前页面或站点会话识别登录用户,无法读取行为记录');
    const [likedData, repliedData, createdData] = await Promise.all([
    fetch(/user_actions.json?username=${encodeURIComponent(username)}&filter=1).then(r => { if (!r.ok) throw new Error('点赞记录获取失败'); return r.json(); }),
    fetch(/user_actions.json?username=${encodeURIComponent(username)}&filter=5).then(r => { if (!r.ok) throw new Error('回复记录获取失败'); return r.json(); }),
    fetch(/user_actions.json?username=${encodeURIComponent(username)}&filter=4).then(r => { if (!r.ok) throw new Error('发帖记录获取失败'); return r.json(); })
    ]);
    const liked = extractTitles(likedData, CONFIG.MAX_LIKED_TITLES);
    const replied = extractTitles(repliedData, CONFIG.MAX_REPLIED_TITLES);
    const created = extractTitles(createdData, CONFIG.MAX_CREATED_TITLES);
    if (!liked && !replied && !created) throw new Error('没有可用于生成画像的点赞、回复或发帖记录');
    return 点赞主题(兴趣信号最强):\n${liked || '无'}\n\n回复主题(参与和专业信号):\n${replied || '无'}\n\n创建主题(主动关注信号):\n${created || '无'};
    }

    function buildProfilePrompt(behaviorSummary) {
    return 你是用户行为分析师。基于以下行为数据生成用户画像:\n\n${behaviorSummary}\n\n请只输出五行:技术兴趣、内容偏好、互动习惯、专业领域、阅读深度。;
    }

    // ========== 用户画像:手工内容优先,否则从站内行为构建 ==========
    async function fetchUserProfile(username) {
    if (settings.customProfile) {
    console.log([推荐] 使用手工画像:\n${settings.customProfile});
    return settings.customProfile;
    }
    if (!username) {
    console.log('[推荐] 未登录且未填写画像,使用默认画像');
    return '对各类技术与社区内容保持一般兴趣';
    }

    const cacheKey = user_profile_v11_${settings.profileMode}_${settings.profileApiUrl}_${settings.profileModel}_${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 behaviorSummary = await getBehaviorSummary(username);
    let profile = behaviorSummary;
    if (settings.profileMode === 'llm') {
    profile = await callChatModel('profile', buildProfilePrompt(behaviorSummary), false) || behaviorSummary;
    }

    GM_setValue(cacheKey, { time: Date.now(), profile });
    console.log([推荐] 用户画像:\n${profile});
    return profile;
    } catch (error) {
    console.error('[推荐] 获取用户画像失败:', error);
    if (settings.profileMode === 'llm') throw error;
    return '对各类技术与社区内容保持一般兴趣';
    }
    }

    // ========== 启动推荐流程 ==========
    async function startRecommendation() {
    try {
    const username = await resolveUsername();
    if (!userProfile) userProfile = await fetchUserProfile(username);

    const response = await fetch('/latest.json');
    if (!response.ok) throw new Error(latest.json HTTP ${response.status});
    const data = await response.json();
    const topics = data?.topic_list?.topics || [];
    topics.forEach(topic => { allTopicsData[topic.id] = topic; });

    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 (error) {
    console.error('[推荐] 推荐流程失败:', error);
    alert(推荐计算失败:${error.message || error});
    }
    }

    // ========== 批量评分主题 ==========
    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 scores = await getAIScoresForBatch(batch);
    batch.forEach(topic => {
    const score = scores[topic.id] ?? 5;
    setTopicScoreCache(topic.id, score);
    aiScoresMap[topic.id] = score;
    });
    }
    }

    async function getAIScoresForBatch(topics) {
    if (settings.scoreEngine === 'jev') return getJevScoresForBatch(topics);

    const topicList = topics.map((topic, index) => ${index + 1}. [ID:${topic.id}] ${topic.title}).join('\n');
    const prompt = 你是内容推荐专家。根据用户画像为主题打 0-10 分。\n\n用户画像:\n${userProfile}\n\n待评分主题:\n${topicList}\n\n9-10 高度匹配;7-8 较匹配;5-6 部分匹配;3-4 关联较弱;0-2 无关或相反。只返回以主题 ID 为键、分数为值的 JSON。;
    const scores = await callChatModel('score', prompt, true);
    const cleanedScores = {};
    for (const [id, score] of Object.entries(scores || {})) {
    const numId = parseInt(id, 10);
    const numScore = parseFloat(score);
    if (Number.isFinite(numId) && Number.isFinite(numScore) && numScore >= 0 && numScore <= 10) {
    cleanedScores[numId] = Math.round(numScore * 10) / 10;
    }
    }
    return cleanedScores;
    }

    async function getJevScoresForBatch(topics) {
    const questions = {};
    topics.forEach((topic, index) => {
    questions[topic_${topic.id}] = {
    type: 'noul',
    instructions: 根据 user_profile,用户会对 topics[${index}] 感兴趣吗?只判断个人兴趣匹配,不考虑浏览量、回复数、点赞数或热度。
    };
    });
    const answers = await callJev({
    user_profile: userProfile,
    topics: topics.map(topic => ({ id: topic.id, title: topic.title }))
    }, questions);
    const scores = {};
    topics.forEach(topic => {
    const probability = Number(answers?.[topic_${topic.id}]?.noul);
    if (Number.isFinite(probability)) scores[topic.id] = Math.round(Math.max(0, Math.min(1, probability)) * 100) / 10;
    });
    return scores;
    }

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

    // 使用分位数归一化,避免极端值影响
    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 = heatMax / 2;
    } else {
    // 限制在 p5 到 p95 范围内
    const clampedX = Math.max(p5, Math.min(p95, xScoresRaw[id]));
    xScoreNormalized = ((clampedX - p5) / rangeX) * heatMax;
    }

    const aiScore = aiScoresMap[id] ?? 5;
    const aiScoreNormalized = (aiScore / 10) * interestMax;
    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 { heatMax, interestMax } = getBlendWeights();
    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 = heatMax / 2;
    } else {
    const clampedX = Math.max(p5, Math.min(p95, xScoresRaw[id]));
    xScoreNormalized = ((clampedX - p5) / rangeX) * heatMax;
    }
    const aiScore = aiScoresMap[id] ?? 5;
    const aiScoreNormalized = (aiScore / 10) * interestMax;
    const finalScore = xScoreNormalized + aiScoreNormalized;
    scoreMap[id] = Math.round(finalScore * 10) / 10;
    });
    }

    async function scoreCurrentPage() {
    if (!settings.autoScoreAllLists || isRecommendMode || isProcessingNewTopics) return;
    const rows = Array.from(document.querySelectorAll('.topic-list-item[data-topic-id]'));
    if (rows.length === 0) return;
    try {
    isPageScoreMode = true;
    const username = await resolveUsername();
    if (!userProfile) userProfile = await fetchUserProfile(username);
    scoreMap = {};
    allTopicsData = {};
    rows.forEach(row => {
    row.style.display = '';
    row.querySelector('.manus-score-badge, .manus-score-loading')?.remove();
    addCalculatingBadge(row);
    });
    await processNewTopics(rows);
    } catch (error) {
    console.error('[推荐] 自动评分失败:', error);
    }
    }

    // ========== 处理新加载的主题(滚动加载) ==========
    async function processNewTopics(newRows) {
    if ((!isRecommendMode && !isPageScoreMode) || isProcessingNewTopics) return;
    isProcessingNewTopics = true;
    try {
    const newTopicIds = [];
    newRows.forEach(row => {
    const id = row.getAttribute('data-topic-id');
    if (id && scoreMap[id] === undefined) newTopicIds.push(id);
    });
    if (newTopicIds.length === 0) return;

    newTopicIds.forEach(id => {
    const row = document.querySelector(.topic-list-item[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 (error) {
    console.error('[推荐] 处理新主题失败:', error);
    } finally {
    isProcessingNewTopics = false;
    }
    }

    function parseCompactCount(text) {
    const value = String(text || '').trim().toLowerCase().replace(/,/g, '');
    const number = parseFloat(value) || 0;
    if (value.includes('万')) return Math.round(number * 10000);
    if (value.endsWith('k')) return Math.round(number * 1000);
    if (value.endsWith('m')) return Math.round(number * 1000000);
    return Math.round(number);
    }

    // ========== 获取单个主题数据 ==========
    async function fetchTopicData(topicId) {
    try {
    const row = document.querySelector(.topic-list-item[data-topic-id="${topicId}"]);
    if (row) {
    const title = row.querySelector('.title, .topic-list-data .link-top-line a, .main-link a')?.textContent?.trim() || '';
    const replies = parseCompactCount(row.querySelector('.posts')?.textContent);
    const views = parseCompactCount(row.querySelector('.views')?.textContent);
    const likes = parseCompactCount(row.querySelector('.likes')?.textContent);

    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 < settings.minDisplayScore) {
    row.style.display = 'none';
    hiddenCount++;
    } else {
    row.style.display = '';
    addScoreBadge(row);
    }
    tbody.appendChild(row);
    });
    }

    // ========== 更新所有行的徽章 ==========
    function updateRowBadges() {
    if (!isRecommendMode && !isPageScoreMode) return;
    const rows = document.querySelectorAll('.topic-list-item[data-topic-id]');
    rows.forEach(row => {
    const id = row.getAttribute('data-topic-id');
    const score = scoreMap[id];
    if (score === undefined) {
    addCalculatingBadge(row);
    } else if (isPageScoreMode) {
    row.style.display = '';
    addScoreBadge(row);
    } else if (score < settings.minDisplayScore) {
    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 调用 ==========
    function requestJson(url, headers, body, retryCount = 0) {
    return new Promise((resolve, reject) => {
    GM_xmlhttpRequest({
    method: 'POST',
    url,
    headers,
    data: JSON.stringify(body),
    timeout: 30000,
    onload: (response) => {
    if (response.status >= 200 && response.status < 300) {
    try {
    resolve(JSON.parse(response.responseText));
    } catch (error) {
    reject(new Error(API 返回了无效 JSON:${error.message}));
    }
    return;
    }
    const retryable = [408, 429, 500, 502, 503, 524, 529].includes(response.status);
    if (retryable && retryCount < CONFIG.API_RETRY_COUNT) {
    setTimeout(() => requestJson(url, headers, body, retryCount + 1).then(resolve, reject), CONFIG.API_RETRY_DELAY * (2 ** retryCount));
    return;
    }
    reject(new Error(API HTTP ${response.status}: ${(response.responseText || '').slice(0, 200)}));
    },
    onerror: () => {
    if (retryCount < CONFIG.API_RETRY_COUNT) {
    setTimeout(() => requestJson(url, headers, body, retryCount + 1).then(resolve, reject), CONFIG.API_RETRY_DELAY * (2 ** retryCount));
    } else {
    reject(new Error('API 网络请求失败'));
    }
    },
    ontimeout: () => reject(new Error('API 请求超时'))
    });
    });
    }

    async function callChatModel(purpose, prompt, isJson = true, options = settings) {
    const isProfile = purpose === 'profile';
    const apiKey = isProfile ? options.profileApiKey : options.scoreApiKey;
    const apiUrl = isProfile ? options.profileApiUrl : options.scoreApiUrl;
    const model = isProfile ? options.profileModel : options.scoreModel;
    const label = isProfile ? '画像模型' : '聊天评分模型';
    if (!apiKey) throw new Error(请先在推荐设置中填写${label} API Key);
    const response = await requestJson(apiUrl, {
    'Content-Type': 'application/json',
    'Authorization': Bearer ${apiKey}
    }, {
    model,
    messages: [{ role: 'user', content: prompt }],
    temperature: isProfile ? 0.3 : 0.1,
    max_tokens: 1000
    });
    let content = response?.choices?.[0]?.message?.content;
    if (typeof content !== 'string') throw new Error(${label}响应缺少 choices[0].message.content);
    if (!isJson) return content.trim();
    return JSON.parse(cleanJsonResponse(content));
    }

    async function callJev(state, questions, options = settings) {
    if (!options.jevApiKey) throw new Error('请先在推荐设置中填写 Jev API Key');
    const isTypeSafe = options.jevProvider === 'typesafe';
    const endpoint = isTypeSafe
    ? 'https://api.typesafe.ai/v1/systemone&#039;
    : 'https://openrouter.ai/api/v1/systemone&#039;;
    const model = String(options.jevModel || 'jev-latest').replace(/^~?typesafe\//, '');
    const headers = {
    'Content-Type': 'application/json',
    'Authorization': Bearer ${options.jevApiKey}
    };
    if (!isTypeSafe) headers['X-Title'] = 'LINUX DO Algorithm + Jev';
    const response = await requestJson(endpoint, headers, { model, state, questions });
    if (!response?.answers) throw new Error('Jev 响应缺少 answers');
    console.log([推荐] Jev 渠道=${settings.jevProvider} 模型与用量:, response.model, response.usage);
    return response.answers;
    }

    // ========== 优化:更智能的 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);
    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;
    }

    async function resolveUsername() {
    const visibleUser = getCurrentUserInfo()?.username;
    if (visibleUser) return visibleUser;
    try {
    const response = await fetch('/session/current.json', { credentials: 'same-origin' });
    if (!response.ok) return null;
    const session = await response.json();
    return session.current_user?.username || session.currentUser?.username || session.user?.username || null;
    } catch (error) {
    console.warn('[推荐] 无法从站点会话读取用户名:', error);
    return null;
    }
    }

    // ========== 获取当前用户信息 ==========
    function getCurrentUserInfo() {
    try {
    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 };
    }
    }
    if (window.User?.current?.()?.username) {
    const user = window.User.current();
    return { username: user.username, id: user.id };
    }
    const userLink = document.querySelector('#current-user a[data-user-card]');
    if (userLink) return { username: userLink.getAttribute('data-user-card') };
    const headerUser = document.querySelector('.header-dropdown-toggle.current-user button');
    if (headerUser) {
    const img = headerUser.querySelector('img');
    if (img?.alt) return { username: img.alt };
    }
    const anyUserCard = document.querySelector('.d-header [data-user-card]');
    if (anyUserCard) return { username: anyUserCard.getAttribute('data-user-card') };
    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 (error) {}
    }
    return null;
    } catch (error) {
    return null;
    }
    }

    // ========== 监听DOM变化 ==========
    const listObserver = new MutationObserver(mutations => {
    if ((!isRecommendMode && !isPageScoreMode) || isLoading) return;
    const newRows = [];
    mutations.forEach(mutation => {
    mutation.addedNodes.forEach(node => {
    if (node.nodeType !== 1) return;
    if (node.classList?.contains('topic-list-item')) newRows.push(node);
    else node.querySelectorAll?.('.topic-list-item').forEach(row => newRows.push(row));
    });
    });
    if (newRows.length === 0) return;
    newRows.forEach(row => {
    const id = row.getAttribute('data-topic-id');
    if (id && scoreMap[id] === undefined) {
    addCalculatingBadge(row);
    } else if (scoreMap[id] !== undefined) {
    if (isPageScoreMode || scoreMap[id] >= settings.minDisplayScore) {
    row.style.display = '';
    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 });
    if (settings.autoScoreAllLists && !isRecommendMode) {
    isPageScoreMode = true;
    scheduleAutoScoring();
    }
    }
    });

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

    if (typeof GM_registerMenuCommand === 'function') {
    GM_registerMenuCommand('推荐算法设置', openSettings);
    }
    console.log('[推荐系统] 已加载 - 支持 Jev、手工画像、自定义权重与列表自动评分');

    })();

    主楼
  • linuschen-linuxdo

    Jev是什么 有没有Jav推荐系统 ^-^

    #1
  • 林语尘

    我相信很快就会有佬友做出来了

    图片

    等着被喂饭

    图片

    #2
  • Ray_Li

    牛逼,体验一下看看

    #3
  • GodKing

    佬的算法使我的gpt旋转 ^-^

    #4

发表回复

登录后回复