[基于jev的L站推荐系统] LINUX DO Algorithm + Jev
tibbar
2026-09-22 15:02
1
// ==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">
<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">
<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'
: 'https://openrouter.ai/api/v1/systemone';
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、手工画像、自定义权重与列表自动评分');
})();
最新回复 (4)
-
linuschen 09-22 15:581楼Jev是什么 有没有Jav推荐系统 ^-^
-
林语尘 09-22 15:582楼我相信很快就会有佬友做出来了

等着被喂饭

-
Ray Li 09-22 16:063楼牛逼,体验一下看看
-
GodKing 09-23 01:544楼佬的算法使我的gpt旋转 ^-^
* 帖子来源Linux.do
附近帖子
- ↑CLAUDE封号的一起讨论看看。究竟哪里出了问题
- ↑Dario 笑话之踩踩刹与老马啊老马
- ↑参考一下大家的本地+移动端coding方案
- ↑Opus 4.6的活人感回来了吗?
- ↑想办一张信用卡
- 📍 [基于jev的L站推荐系统] LINUX DO Algorithm + Jev
- ↓你的Muse还在排队?Muse AI 的一种注册方式,不挑IP
- ↓Opus5.5 鹈鹕感觉非常丝滑,比Fable 5.1 和 GPT 6 Astra都强?价格不到一半
- ↓cursor性价比好低
- ↓:fire:【大模型百科52】关于Opus 5.5, 你想知道的一切【AA反超Astra/Fable5分,挤爆牙膏】
- ↓据目前办理页面信息,中国银行哔哩哔哩2233联名借记卡另有横版,需线下申请。
飞读
tibbar
|
主题数 1 |
帖子数 1 |
注册排名 3 |