Hola, ¿puedes hacer que esta actualización funcione también para Azure?
La corrección de Falco debería funcionar tanto para los endpoints de Azure como para los de OpenAI. Pasan por el mismo código. Para que lo sepas, necesitas habilitar enable_responses_api.
Lamentablemente, esta configuración no aparece después de seleccionar Azure como proveedor.
Disculpe la espera. He añadido las opciones específicas del proveedor aquí:
¿Es compatible con Vertex AI en GCP?
¿Lo probaste usando OpenAI compatibility | Generative AI on Vertex AI | Google Cloud?
Gracias por informar. Se perdió durante una edición en junio. Algunas partes de la primera publicación están ocultas en comentarios HTML, y creo que noté que el editor WYSIWYG rompió eso durante una edición anterior. Pero hubo muchas actualizaciones desde que ocurrió la edición, así que ya podría estar arreglado.
No puedo encontrar nada sobre la función de IA de Discord
¿Cómo funciona? o ¿qué puede hacer? Me refiero a la búsqueda de Discord.
¡Oh, sí, sí, sí, ME ENCANTARÍA esto!
Estamos pensando en añadir suscripciones solo para tener acceso a funciones de IA. ¡Pero tener acceso escalonado a la IA sería aún mejor! Gracias por la sugerencia.
He encontrado un error (y he hecho un PR para solucionarlo) al intentar usar el Bot de Persona de Discord
Job exception: context must be an instance of BotContext
/var/www/discourse/plugins/discourse-ai/lib/personas/bot.rb:55:in `reply'
/var/www/discourse/plugins/discourse-ai/lib/discord/bot/persona_replier.rb:25:in `handle_interaction!'
/var/www/discourse/plugins/discourse-ai/app/jobs/regular/stream_discord_reply.rb:13:in `execute'
¿Cuándo será compatible DeepSeek? Es un LLM desarrollado por una empresa de tecnología china y es relativamente económico.
Gracias. ![]()
Ya es compatible, ¿ha probado alguna función en él que no haya funcionado?
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:
Se ha dividido una publicación en un nuevo tema: Solicitud de un selector de fecha en la configuración de Traducción con IA
2 publicaciones se dividieron en un nuevo tema: Por qué los temas se traducen más rápido que las publicaciones


