Recientemente, Musk abrió el código del algoritmo de recomendación de Twitter. ¿Se puede implementar esto en este plugin?
A continuación, se muestra un script de Greasemonkey escrito por un usuario de un foro que implementa el algoritmo de recomendación de Twitter. En teoría, es compatible con cualquier foro Discourse:
// ==UserScript==
// @name *** Algorithm
// @namespace http://tampermonkey.net/
// @version 0.9
// @description Sistema de recomendación inteligente que fusiona el algoritmo de X y DeepSeek AI, ofreciendo recomendaciones de temas personalizadas
// @author ***
// @match ****
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @connect 2c2ch1u11-share-api-0.hf.space
// ==/UserScript==
(function() {
'use strict';
const API_KEY = '***';
const API_URL = '***/v1/chat/completions';
const MODEL = 'deepseek-chat';
// ========== Parámetros de configuración del algoritmo (ajustables para optimización) ==========
const CONFIG = {
// Parámetros de peso del algoritmo de X
WEIGHT_LIKES: 0.5,
WEIGHT_REPLIES: 13.5,
WEIGHT_VIEWS: 0.015,
PINNED_BOOST: 2.0,
TIME_DECAY_FACTOR: 1.5,
TIME_DECAY_OFFSET: 2,
// Parámetros de puntuación máxima
X_SCORE_MAX: 45,
AI_SCORE_MAX: 55,
// Parámetros de umbral de puntuación
MAX_SCORE: 100,
MIN_DISPLAY_SCORE: 20,
// Parámetros de caché
PROFILE_CACHE_TTL: 86400000, // Caché del perfil de usuario: 24 horas
TOPIC_SCORE_CACHE_TTL: 3600000, // Caché de puntuación AI de un solo tema: 1 hora
// Parámetros de API
MAX_TOPICS_PER_BATCH: 30,
MAX_LIKED_TITLES: 15, // Número de temas con me gusta
MAX_REPLIED_TITLES: 10, // Número de temas respondidos
MAX_CREATED_TITLES: 5, // Número de temas creados
API_RETRY_COUNT: 2,
API_RETRY_DELAY: 1000,
// Dimensiones de análisis del perfil de usuario
PROFILE_CATEGORIES: ['Intereses técnicos', 'Preferencias de contenido', 'Hábitos de interacción', 'Áreas de especialización', 'Profundidad de lectura'],
};
let isRecommendMode = false;
let scoreMap = {};
let allTopicsData = {};
let sortTimeout = null;
let userProfile = "";
let isLoading = false;
let isProcessingNewTopics = false;
// ========== Estilos 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;
}
`);
// ========== Funciones de caché de puntuación AI de temas ==========
function getTopicScoreCache() {
return GM_getValue('topic_ai_scores_cache', {});
}
function setTopicScoreCache(topicId, aiScore) {
const cache = getTopicScoreCache();
cache[topicId] = { score: aiScore, time: Date.now() };
GM_setValue('topic_ai_scores_cache', cache);
}
function getCachedTopicScore(topicId) {
const cache = getTopicScoreCache();
const entry = cache[topicId];
if (entry && (Date.now() - entry.time < CONFIG.TOPIC_SCORE_CACHE_TTL)) {
return entry.score;
}
return null;
}
function cleanExpiredCache() {
const cache = getTopicScoreCache();
const now = Date.now();
let cleaned = false;
for (const [id, entry] of Object.entries(cache)) {
if (now - entry.time > CONFIG.TOPIC_SCORE_CACHE_TTL) {
delete cache[id];
cleaned = true;
}
}
if (cleaned) {
GM_setValue('topic_ai_scores_cache', cache);
}
}
// ========== Inyectar pestaña de recomendaciones ==========
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)">Recomendados</a>`;
const latestTab = navPills.querySelector('.nav-item_latest') ||
navPills.querySelector('[data-filter-type="latest"]')?.parentElement ||
navPills.firstChild;
navPills.insertBefore(recommendTab, latestTab);
recommendTab.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
if (isLoading) return;
isRecommendMode = true;
document.body.classList.add('recommend-mode-active');
navPills.querySelectorAll('li').forEach(li => {
li.classList.remove('active');
li.querySelector('a')?.classList.remove('active');
});
recommendTab.classList.add('active');
cleanExpiredCache();
showLoading(true);
await startRecommendation();
showLoading(false);
sortAndDisplayRows();
});
navPills.querySelectorAll('li:not(#nav-item-recommend)').forEach(tab => {
tab.addEventListener('click', () => {
isRecommendMode = false;
document.body.classList.remove('recommend-mode-active');
recommendTab.classList.remove('active');
document.querySelectorAll('.manus-score-badge').forEach(b => b.remove());
document.querySelectorAll('.manus-score-loading').forEach(b => b.remove());
});
});
window.addEventListener('popstate', () => {
if (isRecommendMode) {
isRecommendMode = false;
document.body.classList.remove('recommend-mode-active');
recommendTab.classList.remove('active');
}
});
}
// ========== Indicador de carga ==========
let originalTableContent = null;
function showLoading(show, text = 'Fusionando el algoritmo de X y las recomendaciones de DeepSeek AI...') {
isLoading = show;
const topicListContainer = document.querySelector('.topic-list-container') ||
document.querySelector('.topic-list')?.parentElement ||
document.querySelector('.topic-list');
const tbody = document.querySelector('.topic-list tbody');
if (!topicListContainer) return;
if (show) {
if (!originalTableContent && tbody) {
originalTableContent = tbody.innerHTML;
}
if (tbody) {
tbody.innerHTML = `
<tr class="recommend-loading-row">
<td colspan="100%">
<div class="recommend-loading-container">
<div class="recommend-loading-text">${text}</div>
<div class="recommend-spinner"></div>
</div>
</td>
</tr>
`;
}
topicListContainer.style.position = 'relative';
let overlay = topicListContainer.querySelector('.recommend-full-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.className = 'recommend-full-overlay';
topicListContainer.appendChild(overlay);
}
} else {
topicListContainer.querySelector('.recommend-full-overlay')?.remove();
if (originalTableContent && tbody) {
tbody.innerHTML = originalTableContent;
originalTableContent = null;
}
}
}
// ========== Función auxiliar para extraer títulos ==========
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') : '';
}
// ========== Optimización: Obtención de perfil de usuario más detallada ==========
async function fetchUserProfile(username) {
if (!username) {
console.log('[Recomendación] No conectado, usando perfil por defecto');
return "Por defecto";
}
const cacheKey = `user_profile_v9_${username}`;
const cached = GM_getValue(cacheKey);
if (cached && (Date.now() - cached.time < CONFIG.PROFILE_CACHE_TTL)) {
console.log(`[Recomendación] Perfil de usuario (caché):\n${cached.profile}`);
return cached.profile;
}
try {
// Obtener varios tipos de datos de comportamiento de usuario
const [likedData, repliedData, createdData] = await Promise.all([
fetch(`/user_actions.json?username=${username}&filter=1`).then(r => r.json()).catch(() => null), // Me gusta
fetch(`/user_actions.json?username=${username}&filter=5`).then(r => r.json()).catch(() => null), // Respuestas
fetch(`/user_actions.json?username=${username}&filter=4`).then(r => r.json()).catch(() => null), // Creación
]);
// Extraer diferentes tipos de títulos
const likedTitles = extractTitles(likedData, CONFIG.MAX_LIKED_TITLES);
const repliedTitles = extractTitles(repliedData, CONFIG.MAX_REPLIED_TITLES);
const createdTitles = extractTitles(createdData, CONFIG.MAX_CREATED_TITLES);
if (!likedTitles && !repliedTitles && !createdTitles) return "Por defecto";
// Construir prompt de análisis más detallado
const prompt = `Eres un analista profesional de comportamiento de usuarios. Por favor, genera un perfil de usuario detallado basado en los siguientes datos de comportamiento:
**Temas que al usuario le gustaron** (reflejan mejor los intereses):
${likedTitles || 'Sin datos'}
**Temas a los que el usuario respondió** (reflejan participación y campo profesional):
${repliedTitles || 'Sin datos'}
**Temas creados por el usuario** (reflejan puntos de atención activa):
${createdTitles || 'Sin datos'}
Por favor, analiza el perfil del usuario desde las siguientes dimensiones, resumiendo cada una en una frase:
1. **Interés técnico**: Qué stacks tecnológicos, herramientas o plataformas sigue el usuario
2. **Preferencia de contenido**: Prefiere tutoriales, discusiones, noticias o contenido de preguntas y respuestas
3. **Hábitos de interacción**: Es un participante profundo o un navegador superficial
4. **Campo profesional**: Principales áreas tecnológicas o direcciones de la industria en las que se enfoca
5. **Profundidad de lectura**: Prefiere contenido de nivel introductorio, avanzado o experto
Formato de salida (sin numeración, salida directa de 5 líneas):
Interés técnico: [Resultado del análisis]
Preferencia de contenido: [Resultado del análisis]
Hábitos de interacción: [Resultado del análisis]
Campo profesional: [Resultado del análisis]
Profundidad de lectura: [Resultado del análisis]`;
const profile = await callDeepSeek(prompt, false);
if (profile && profile.length > 0) {
GM_setValue(cacheKey, { time: Date.now(), profile: profile });
console.log(`[Recomendación] Perfil de usuario:\n${profile}`);
return profile;
}
return "Por defecto";
} catch (e) {
console.error('[Recomendación] Error al obtener el perfil de usuario:', e);
return "Por defecto";
}
}
// ========== Iniciar proceso de recomendación ==========
async function startRecommendation() {
try {
const currentUser = getCurrentUserInfo();
if (!userProfile) {
userProfile = await fetchUserProfile(currentUser?.username);
}
const response = await fetch('/latest.json');
const data = await response.json();
const topics = data.topic_list.topics;
topics.forEach(t => {
allTopicsData[t.id] = t;
});
const uncachedTopics = [];
const xScoresRaw = {};
const aiScoresMap = {};
topics.forEach(topic => {
xScoresRaw[topic.id] = calculateXScore(topic);
const cachedScore = getCachedTopicScore(topic.id);
if (cachedScore !== null) {
aiScoresMap[topic.id] = cachedScore;
} else {
uncachedTopics.push(topic);
}
});
if (uncachedTopics.length > 0) {
await batchScoreTopics(uncachedTopics, aiScoresMap);
}
calculateFinalScores(topics, xScoresRaw, aiScoresMap);
} catch (e) {
console.error('[Recomendación] Fallo en el proceso de recomendación:', e);
}
}
// ========== Puntuación por lotes de temas ==========
async function batchScoreTopics(topics, aiScoresMap) {
for (let i = 0; i < topics.length; i += CONFIG.MAX_TOPICS_PER_BATCH) {
const batch = topics.slice(i, i + CONFIG.MAX_TOPICS_PER_BATCH);
const aiScores = await getAIScoresForBatch(batch);
batch.forEach(topic => {
const aiScore = aiScores[topic.id] || 5;
setTopicScoreCache(topic.id, aiScore);
aiScoresMap[topic.id] = aiScore;
});
}
}
// ========== Optimización: Puntuación por lotes de temas más precisa ==========
async function getAIScoresForBatch(topics) {
const topicList = topics.map((t, idx) =>
`${idx + 1}. [ID:${t.id}] ${t.title}`
).join('\n');
const prompt = `Eres un experto en recomendación de contenido. Por favor, puntúa los siguientes temas basándote en el perfil del usuario.
**Perfil del usuario**:
${userProfile}
**Temas a puntuar**:
${topicList}
**Criterios de puntuación** (0-10 puntos):
- **9-10 puntos**: Coincidencia alta con el perfil del usuario, es muy probable que le interese
- **7-8 puntos**: Coincidencia media con el perfil del usuario, es probable que le interese
- **5-6 puntos**: Coincidencia parcial con el perfil del usuario, podría interesarle
- **3-4 puntos**: Debil asociación con el perfil del usuario, es poco probable que le interese
- **0-2 puntos**: Sin relación o opuesto al perfil del usuario, no le interesa
**Factores a considerar para la puntuación**:
1. Grado de coincidencia entre el contenido del tema y los intereses técnicos del usuario
2. Consistencia entre el tipo de tema y las preferencias de contenido del usuario
3. Adecuación entre la profundidad del tema y la profundidad de lectura del usuario
4. Relevancia entre el campo del tema y el campo profesional del usuario
Por favor, devuelve el formato JSON, donde la clave sea el ID del tema (solo números) y el valor sea la puntuación (número de 0-10).
Ejemplo: {"123": 8.5, "456": 5.0}
Solo devuelve JSON, sin otro texto.`;
const scores = await callDeepSeek(prompt, true);
// Limpieza y validación de puntuaciones
const cleanedScores = {};
for (const [id, score] of Object.entries(scores)) {
const numId = parseInt(id, 10);
const numScore = parseFloat(score);
if (!isNaN(numId) && !isNaN(numScore) && numScore >= 0 && numScore <= 10) {
cleanedScores[numId] = Math.round(numScore * 10) / 10;
}
}
return cleanedScores;
}
// ========== Optimización: Cálculo de puntuación final más robusto ==========
function calculateFinalScores(topics, xScoresRaw, aiScoresMap) {
const xScores = Object.values(xScoresRaw);
if (xScores.length === 0) return;
// Usar normalización por cuantiles para evitar la influencia de valores extremos
const sortedX = [...xScores].sort((a, b) => a - b);
const p5 = sortedX[Math.floor(sortedX.length * 0.05)]; // Cuantil 5%
const p95 = sortedX[Math.floor(sortedX.length * 0.95)]; // Cuantil 95%
const rangeX = p95 - p5;
topics.forEach(topic => {
const id = topic.id;
let xScoreNormalized;
if (rangeX === 0) {
xScoreNormalized = CONFIG.X_SCORE_MAX / 2;
} else {
// Limitar al rango de p5 a p95
const clampedX = Math.max(p5, Math.min(p95, xScoresRaw[id]));
xScoreNormalized = ((clampedX - p5) / rangeX) * CONFIG.X_SCORE_MAX;
}
const aiScore = aiScoresMap[id] || 5;
const aiScoreNormalized = (aiScore / 10) * CONFIG.AI_SCORE_MAX;
// Puntuación final: Algoritmo X (45%) + Puntuación AI (55%)
const finalScore = xScoreNormalized + aiScoreNormalized;
scoreMap[id] = Math.round(finalScore * 10) / 10;
});
}
// ========== Calcular puntuación final de nuevos temas ==========
function calculateFinalScoresForNew(topicIds, xScoresRaw, aiScoresMap) {
const allXScores = Object.values(scoreMap).length > 0
? [...Object.values(xScoresRaw), ...Object.keys(allTopicsData).map(id => calculateXScore(allTopicsData[id]))]
: Object.values(xScoresRaw);
if (allXScores.length === 0) return;
const sortedX = [...allXScores].sort((a, b) => a - b);
const p5 = sortedX[Math.floor(sortedX.length * 0.05)];
const p95 = sortedX[Math.floor(sortedX.length * 0.95)];
const rangeX = p95 - p5;
topicIds.forEach(id => {
if (xScoresRaw[id] === undefined) return;
let xScoreNormalized;
if (rangeX === 0) {
xScoreNormalized = CONFIG.X_SCORE_MAX / 2;
} else {
const clampedX = Math.max(p5, Math.min(p95, xScoresRaw[id]));
xScoreNormalized = ((clampedX - p5) / rangeX) * CONFIG.X_SCORE_MAX;
}
const aiScore = aiScoresMap[id] || 5;
const aiScoreNormalized = (aiScore / 10) * CONFIG.AI_SCORE_MAX;
const finalScore = xScoreNormalized + aiScoreNormalized;
scoreMap[id] = Math.round(finalScore * 10) / 10;
});
}
// ========== Procesar temas cargados nuevos (carga por desplazamiento) ==========
async function processNewTopics(newRows) {
if (!isRecommendMode || isProcessingNewTopics) return;
isProcessingNewTopics = true;
try {
const newTopicIds = [];
newRows.forEach(row => {
const id = row.getAttribute('data-topic-id');
if (id && !scoreMap[id]) {
newTopicIds.push(id);
}
});
if (newTopicIds.length === 0) {
isProcessingNewTopics = false;
return;
}
newTopicIds.forEach(id => {
const row = document.querySelector(`tr[data-topic-id="${id}"]`);
if (row) {
addCalculatingBadge(row);
}
});
const uncachedTopics = [];
const xScoresRaw = {};
const aiScoresMap = {};
for (const id of newTopicIds) {
const topicData = allTopicsData[id] || await fetchTopicData(id);
if (!topicData) continue;
allTopicsData[id] = topicData;
xScoresRaw[id] = calculateXScore(topicData);
const cachedScore = getCachedTopicScore(id);
if (cachedScore !== null) {
aiScoresMap[id] = cachedScore;
} else {
uncachedTopics.push(topicData);
}
}
if (uncachedTopics.length > 0) {
await batchScoreTopics(uncachedTopics, aiScoresMap);
}
calculateFinalScoresForNew(newTopicIds, xScoresRaw, aiScoresMap);
updateRowBadges();
} catch (e) {
console.error('[Recomendación] Error al procesar nuevos temas:', e);
}
isProcessingNewTopics = false;
}
// ========== Obtener datos de un solo tema ==========
async function fetchTopicData(topicId) {
try {
const row = document.querySelector(`tr[data-topic-id="${topicId}"]`);
if (row) {
const title = row.querySelector('.title')?.textContent?.trim() || '';
const replies = parseInt(row.querySelector('.posts')?.textContent) || 0;
const views = parseInt(row.querySelector('.views')?.textContent?.replace(/[k,]/gi, '')) || 0;
const likes = parseInt(row.querySelector('.likes')?.textContent) || 0;
return {
id: topicId,
title: title,
posts_count: replies,
views: views,
like_count: likes,
created_at: new Date().toISOString(),
pinned: row.classList.contains('pinned')
};
}
return null;
} catch (e) {
return null;
}
}
// ========== Ordenar y mostrar ==========
function sortAndDisplayRows() {
if (!isRecommendMode) return;
const tbody = document.querySelector('.topic-list tbody');
if (!tbody) return;
const rows = Array.from(tbody.querySelectorAll('tr.topic-list-item'));
rows.sort((a, b) => {
const idA = a.getAttribute('data-topic-id');
const idB = b.getAttribute('data-topic-id');
const scoreA = scoreMap[idA] || 0;
const scoreB = scoreMap[idB] || 0;
return scoreB - scoreA;
});
let hiddenCount = 0;
rows.forEach(row => {
const id = row.getAttribute('data-topic-id');
const score = scoreMap[id];
if (score === undefined) {
row.style.display = '';
addCalculatingBadge(row);
} else if (score < CONFIG.MIN_DISPLAY_SCORE) {
row.style.display = 'none';
hiddenCount++;
} else {
row.style.display = '';
addScoreBadge(row);
}
tbody.appendChild(row);
});
}
// ========== Actualizar insignias de todas las filas ==========
function updateRowBadges() {
if (!isRecommendMode) return;
const rows = document.querySelectorAll('tr.topic-list-item');
rows.forEach(row => {
const id = row.getAttribute('data-topic-id');
const score = scoreMap[id];
if (score === undefined) {
addCalculatingBadge(row);
} else if (score < CONFIG.MIN_DISPLAY_SCORE) {
row.style.display = 'none';
} else {
row.style.display = '';
addScoreBadge(row);
}
});
}
// ========== Añadir insignia de puntuación ==========
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');
}
}
}
// ========== Añadir insignia "Calculando" ==========
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 = '⏳ Calculando...';
badge.classList.add('calculating');
}
}
// ========== Optimización: Mejorar lógica de limpieza JSON ==========
function cleanJsonResponse(content) {
if (!content) return content;
let cleaned = content.trim();
// Eliminar marcas de bloques de código Markdown
cleaned = cleaned.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/i, '');
// Eliminar posibles explicaciones de texto
const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
if (jsonMatch) {
cleaned = jsonMatch[0];
}
return cleaned.trim();
}
// ========== Llamada API ==========
async function callDeepSeek(prompt, isJson = true, retryCount = 0) {
return new Promise((resolve) => {
GM_xmlhttpRequest({
method: "POST",
url: API_URL,
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${API_KEY}`
},
data: JSON.stringify({
model: MODEL,
messages: [{ role: "user", content: prompt }],
temperature: 0.3,
max_tokens: 1000
}),
timeout: 30000,
onload: (res) => {
try {
if (res.status >= 200 && res.status < 300) {
let content = JSON.parse(res.responseText).choices[0].message.content;
if (isJson) {
content = cleanJsonResponse(content);
resolve(JSON.parse(content));
} else {
resolve(content);
}
} else {
throw new Error(`HTTP ${res.status}`);
}
} catch(e) {
console.error('[Recomendación] Error al analizar respuesta API:', e);
if (retryCount < CONFIG.API_RETRY_COUNT) {
setTimeout(() => {
callDeepSeek(prompt, isJson, retryCount + 1).then(resolve);
}, CONFIG.API_RETRY_DELAY);
} else {
resolve(isJson ? {} : "");
}
}
},
onerror: (err) => {
console.error('[Recomendación] Fallo en llamada API:', err);
if (retryCount < CONFIG.API_RETRY_COUNT) {
setTimeout(() => {
callDeepSeek(prompt, isJson, retryCount + 1).then(resolve);
}, CONFIG.API_RETRY_DELAY);
} else {
resolve(isJson ? {} : "");
}
},
ontimeout: () => {
console.error('[Recomendación] Tiempo de espera agotado en llamada API');
if (retryCount < CONFIG.API_RETRY_COUNT) {
setTimeout(() => {
callDeepSeek(prompt, isJson, retryCount + 1).then(resolve);
}, CONFIG.API_RETRY_DELAY);
} else {
resolve(isJson ? {} : "");
}
}
});
});
}
// ========== Optimización: Algoritmo X de puntuación más inteligente ==========
function calculateXScore(topic) {
const now = new Date();
const created = new Date(topic.created_at);
const hoursOld = Math.max(0, (now - created) / (1000 * 60 * 60));
// Puntuación básica de interacción
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;
// Calcular bonificación por tasa de interacción (contenido con alta tasa de interacción tiene mayor calidad)
const views = topic.views || 1;
const engagementRate = ((topic.like_count || 0) + (topic.posts_count || 0)) / views;
const engagementBoost = 1 + Math.min(engagementRate * 10, 2); // Bonificación máxima de 2x
let score = (likeScore + replyScore + viewScore) * engagementBoost;
// Bonificación por fijado
if (topic.pinned) {
score *= CONFIG.PINNED_BOOST;
}
// Decaimiento temporal (frescura)
const timeFactor = Math.pow(hoursOld + CONFIG.TIME_DECAY_OFFSET, CONFIG.TIME_DECAY_FACTOR);
return score / timeFactor;
}
// ========== Obtener información del usuario actual ==========
function getCurrentUserInfo() {
try {
// Método 1: Contenedor Discourse
const container = window.Discourse?.__container__;
if (container) {
const currentUser = container.lookup('service:current-user');
if (currentUser?.username) {
return {
username: currentUser.username,
id: currentUser.id,
name: currentUser.name,
trust_level: currentUser.trust_level
};
}
}
// Método 2: User.current()
if (window.User?.current?.()?.username) {
const user = window.User.current();
return { username: user.username, id: user.id };
}
// Método 3: #current-user
const userLink = document.querySelector('#current-user a[data-user-card]');
if (userLink) {
return { username: userLink.getAttribute('data-user-card') };
}
// Método 4: header
const headerUser = document.querySelector('.header-dropdown-toggle.current-user button');
if (headerUser) {
const img = headerUser.querySelector('img');
if (img?.alt) {
return { username: img.alt };
}
}
// Método 5: d-header
const anyUserCard = document.querySelector('.d-header [data-user-card]');
if (anyUserCard) {
return { username: anyUserCard.getAttribute('data-user-card') };
}
// Método 6: Datos precargados
const preloadData = document.querySelector('#data-preloaded');
if (preloadData) {
try {
const data = JSON.parse(preloadData.dataset.preloaded || '{}');
const currentUser = JSON.parse(data.currentUser || '{}');
if (currentUser.username) {
return { username: currentUser.username };
}
} catch (e) {}
}
return null;
} catch (e) {
return null;
}
}
// ========== Escuchar cambios en el DOM ==========
const listObserver = new MutationObserver((mutations) => {
if (!isRecommendMode || isLoading) return;
const newRows = [];
mutations.forEach(m => {
m.addedNodes.forEach(node => {
if (node.nodeType === 1) {
if (node.classList?.contains('topic-list-item')) {
newRows.push(node);
} else if (node.querySelectorAll) {
node.querySelectorAll('.topic-list-item').forEach(row => newRows.push(row));
}
}
});
});
if (newRows.length > 0) {
newRows.forEach(row => {
const id = row.getAttribute('data-topic-id');
if (id && scoreMap[id] === undefined) {
addCalculatingBadge(row);
} else if (scoreMap[id] !== undefined) {
if (scoreMap[id] >= CONFIG.MIN_DISPLAY_SCORE) {
addScoreBadge(row);
} else {
row.style.display = 'none';
}
}
});
clearTimeout(sortTimeout);
sortTimeout = setTimeout(() => processNewTopics(newRows), 500);
}
});
const navObserver = new MutationObserver(() => {
injectRecommendTab();
const tbody = document.querySelector('.topic-list tbody');
if (tbody && !tbody.dataset.observing) {
tbody.dataset.observing = 'true';
listObserver.observe(tbody, { childList: true });
}
});
// ========== Iniciar script ==========
navObserver.observe(document.body, { childList: true, subtree: true });
injectRecommendTab();
console.log('[Sistema de recomendación] Cargado - Fusión de Algoritmo X y DeepSeek AI');
})();
A continuación se muestra el efecto en el foro de Discourse:

