入口初始化 + 样式 + XHR/fetch网络拦截 — JM Shelf 推荐脚本的模块库,通过 @require 被主脚本引用。
Detta skript bör inte installeras direkt. Det är ett bibliotek för andra skript att inkludera med meta-direktivet // @require https://update.sleazyfork.org/scripts/581113/1842615/JM%20Shelf%20-%20Init.js
// ==UserScript==
// @name JM Shelf - Init
// @namespace jmshelf-lib
// @version 1.0.0
// @author Kesdi
// @description 入口初始化 + 样式 + XHR/fetch网络拦截 — JM Shelf 推荐脚本的模块库,通过 @require 被主脚本引用。
// @license MIT
// ==/UserScript==
//
// 此文件是 GreasyFork 库(library),不直接安装。
// 请安装主脚本: JM Shelf 给杂鱼的个性化推荐
//
// ═══ [15] INIT ═══
// ============================================================
// ═══ 网络拦截: 监控收藏/爱心 XHR ═══
(function() {
const origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(body) {
const url = this._url || '';
if (url.includes('/ajax/favorite_album') || url.includes('/ajax/delete_favorite_album') ||
url.includes('/ajax/love_album') || url.includes('/ajax/unlove_album')) {
try {
const formData = new URLSearchParams(body);
const albumId = formData.get('album_id');
if (albumId) {
if (url.includes('delete_favorite_album')) {
LOG.info(`📌 网络监测: 取消收藏 album ${albumId}`);
const favs = State.getFavorites();
const idx = favs.findIndex(f => String(f.id) === String(albumId));
if (idx >= 0) { favs.splice(idx, 1); State.saveFavorites(favs); showNotification('📌 已取消收藏'); }
} else if (url.includes('favorite_album')) {
LOG.info(`📌 网络监测: 收藏 album ${albumId}`);
const favs = State.getFavorites();
if (!favs.find(f => String(f.id) === String(albumId))) {
const albumData = State.getAlbumCache()[String(albumId)] || { id: albumId, title: '', tags: [], authors: [], typeTags: [] };
favs.push({ ...albumData, id: String(albumId) });
State.saveFavorites(favs);
showNotification('📌 已收藏!(network)');
}
try { _insertTitleToTrie(_initTrie(), albumId, albumData.title || '', 'fav'); _titleTrie.rebuildAfterInsert(); _titleTrie.save(); } catch(e) {}
} else if (url.includes('unlove_album')) {
LOG.info(`❤ 网络监测: 取消喜欢 album ${albumId}`);
const liked = State.getLikedAlbums();
const idx = liked.findIndex(f => String(f.id) === String(albumId));
if (idx >= 0) { liked.splice(idx, 1); State.saveLikedAlbums(liked); showNotification('💔 已取消喜欢'); }
} else if (url.includes('love_album')) {
LOG.info(`❤ 网络监测: 喜欢 album ${albumId}`);
State.addLikedAlbum(albumId);
showNotification('❤️ 已记录喜欢!');
}
}
} catch(e) {}
}
return origSend.apply(this, arguments);
};
const origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url) {
this._url = url;
return origOpen.apply(this, arguments);
};
})();
// ═══ fetch拦截: 兜底监控 ═══
(function() {
const origFetch = window.fetch;
window.fetch = function(url, options) {
const urlStr = typeof url === 'string' ? url : (url.url || '');
const method = (options && options.method) || 'GET';
if (method === 'POST' && (urlStr.includes('/ajax/favorite_album') || urlStr.includes('/ajax/delete_favorite_album') ||
urlStr.includes('/ajax/love_album') || urlStr.includes('/ajax/unlove_album'))) {
const body = (options && options.body) ? new URLSearchParams(options.body) : new URLSearchParams();
const albumId = body.get('album_id');
if (albumId) {
if (urlStr.includes('delete_favorite_album')) {
const favs = State.getFavorites();
const idx = favs.findIndex(f => String(f.id) === String(albumId));
if (idx >= 0) { favs.splice(idx, 1); State.saveFavorites(favs); showNotification('📌 已取消收藏'); }
} else if (urlStr.includes('favorite_album')) {
const favs = State.getFavorites();
if (!favs.find(f => String(f.id) === String(albumId))) {
const albumData = State.getAlbumCache()[String(albumId)] || { id: albumId, title: '', tags: [], authors: [], typeTags: [] };
favs.push({ ...albumData, id: String(albumId) });
State.saveFavorites(favs);
showNotification('📌 已收藏!(network)');
}
try { const ad = State.getAlbumCache()[String(albumId)]; if (ad && ad.title) { _insertTitleToTrie(_initTrie(), albumId, ad.title, 'fav'); _titleTrie.rebuildAfterInsert(); _titleTrie.save(); } } catch(e) {}
} else if (urlStr.includes('unlove_album')) {
const liked = State.getLikedAlbums();
const idx = liked.findIndex(f => String(f.id) === String(albumId));
if (idx >= 0) { liked.splice(idx, 1); State.saveLikedAlbums(liked); showNotification('💔 已取消喜欢'); }
} else if (urlStr.includes('love_album')) {
State.addLikedAlbum(albumId);
showNotification('❤️ 已记录喜欢!');
}
}
}
return origFetch.apply(this, arguments);
};
})();
// 全局click监听: 喜欢按钮
let _currentAlbumData = null;
document.addEventListener('click', (e) => {
const btn = e.target.closest('[id^="love_likes_"], [id^="love_heart_"], [id^="favorite_album_"]');
if (!btn) return;
const btnId = btn.id || '';
const isLike = btnId.startsWith('love_likes_') || btnId.startsWith('love_heart_');
const isFav = btnId.startsWith('favorite_album_');
const idMatch = btnId.match(/\d+$/);
const albumId = idMatch ? idMatch[0] : null;
if (!albumId) return;
if (!_currentAlbumData) { _currentAlbumData = { id: albumId, title: '', tags: [], authors: [] }; }
if (isLike) {
LOG.info(`❤ 检测到喜欢: album ${albumId}`);
const oldProfile = State.getProfile();
const liked = State.getLikedAlbums();
const likedEntry = liked.find(f => String(f.id) === String(albumId));
const daysSinceLiked = likedEntry ? (Date.now() - (likedEntry.likedAt||0)) / 86400000 : 0;
const likeDecay = 0.1 + 0.9 * Math.exp(-daysSinceLiked / 30);
const effectiveWeight = CONFIG.LIKE_WEIGHT * likeDecay;
const newProfile = ProfileManager.update({ ...oldProfile }, _currentAlbumData, effectiveWeight);
State.saveProfile(newProfile);
State.addLikedAlbum(albumId);
if (ProfileManager.changeRatio(oldProfile, newProfile) > CONFIG.PROFILE_CHANGE_RECALC) {
recalcRecommendations();
}
showNotification('❤️ 已记录喜欢!');
}
if (isFav) return; // 收藏由XHR网络监控处理
});
// ═══ STYLES ═══
function injectStyles() {
GM_addStyle(`
#jms-panel {
position: fixed; bottom: 20px; right: 20px; width: 380px;
background: #1a1a2e; color: #e0e0e0; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.5);
z-index: 99999; font-size: 13px; overflow: hidden;
display: none;
}
#jms-panel-header {
background: #16213e; padding: 10px 16px; font-weight: bold; font-size: 14px;
display: flex; justify-content: space-between; align-items: center;
}
#jms-panel-header span { opacity: 0.9; }
#jms-panel-close-x {
background: none; border: none; color: #888; cursor: pointer; font-size: 20px; padding: 0 6px; line-height: 1;
}
#jms-panel-close-x:hover { color: #fff; }
#jms-panel-body { padding: 16px; max-height: 65vh; overflow-y: auto; }
#jms-status { color: #0f9; margin-bottom: 8px; font-size: 12px; }
#jms-progress-container { margin: 10px 0; }
#jms-progress-bar { height: 6px; background: #333; border-radius: 3px; overflow: hidden; }
#jms-progress-fill { height: 100%; background: linear-gradient(90deg, #0f9, #0cf); width: 0; transition: width 0.3s; }
#jms-progress-text { font-size: 11px; color: #aaa; margin-top: 4px; }
#jms-stats { font-size: 12px; color: #aaa; margin: 8px 0; line-height: 1.6; }
#jms-actions { display: flex; gap: 8px; margin: 10px 0; }
#jms-settings-details { margin-top: 12px; font-size: 12px; }
#jms-settings-details summary { cursor: pointer; color: #aaa; }
#jms-history-details { margin-top: 12px; font-size: 12px; }
#jms-history-details summary { cursor: pointer; color: #aaa; }
#jms-log-details { margin-top: 12px; font-size: 12px; }
#jms-log-details summary { cursor: pointer; color: #aaa; }
#jms-settings { padding: 8px 0; }
#jms-float-btn {
position: fixed; bottom: 30px; right: 30px; width: 48px; height: 48px;
border-radius: 50%; display: flex; align-items: center; justify-content: center;
cursor: pointer; box-shadow: 0 4px 16px rgba(0,0,0,0.4);
z-index: 99998; transition: transform 0.2s; overflow: hidden;
animation: jms-float-rgb 20s ease-in-out infinite;
}
@keyframes jms-float-rgb {
0% { background: #ff4d6a; }
16% { background: #ff8c42; }
33% { background: #ffd166; }
50% { background: #4ecdc4; }
66% { background: #6c5ce7; }
83% { background: #e056a0; }
100% { background: #ff4d6a; }
}
#jms-float-btn:hover { transform: scale(1.1); }
#jms-notification {
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
background: #1a1a2e; color: #fff; padding: 12px 24px; border-radius: 8px;
z-index: 100000; font-size: 14px; box-shadow: 0 4px 16px rgba(0,0,0,0.4);
animation: jms-fadeIn 0.3s ease;
}
@keyframes jms-fadeIn {
from { opacity: 0; transform: translateX(-50%) translateY(-10px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
`);
}
// ═══ 数据迁移 ═══
function migrateData(from) {
if (from < 1) {
try {
const p = State.getProfile();
if (p && !p.live) { p.live = { tags: {}, authors: {}, types: {}, lastUpdate: 0 }; State.saveProfile(p); }
} catch(e) {}
try {
const v = State.getViewedAlbums();
if (v.length > 0 && typeof v[0] === 'number') State.saveViewedAlbums(v.map(String));
} catch(e) {}
try { GMStore.set('scan_state', null); } catch(e) {}
}
}
// ═══ 页面类型检测 ═══
function detectPageType() {
const path = location.pathname;
if (path === '/' || path === '') return 'homepage';
if (/\/album\/\d+/.test(path)) return 'album';
if (/\/photo\/\d+/.test(path)) return 'photo';
if (/\/albums/.test(path)) return 'listing';
if (/\/search\/photos/.test(path)) return 'listing';
if (/\/user\//.test(path)) return 'user';
if (/\/theme\//.test(path)) return 'theme';
return 'other';
}
// ═══ INIT ═══
async function init() {
LOG._restore();
LOG.info('──────────────');
const savedTemp = Storage.get('temperature');
if (savedTemp != null) CONFIG.TEMPERATURE = savedTemp;
const storedVersion = GMStore.get('version', 0);
if (storedVersion < CONFIG.VERSION) {
LOG.info(`版本升级 ${storedVersion} → ${CONFIG.VERSION}, 保留现有数据`);
GMStore.set('version', CONFIG.VERSION);
}
const storedDataVer = GMStore.get('dataVersion', 0);
if (storedDataVer < CONFIG.DATA_VERSION) {
LOG.info(`数据格式迁移 ${storedDataVer} → ${CONFIG.DATA_VERSION}`);
migrateData(storedDataVer);
GMStore.set('dataVersion', CONFIG.DATA_VERSION);
}
injectStyles();
const pageType = detectPageType();
LOG.info(`当前页面类型: ${pageType}`);
if (!isLoggedIn()) {
LOG.info('未登录,仅注入UI框架');
createPanel();
return;
}
let username = detectUsername();
if (!username) username = State.getUsername();
if (username) {
const lastUser = GMStore.get('lastUser', '');
if (username !== lastUser) {
GMStore.set('lastUser', username);
State.saveUsername(username);
LOG.info(`检测到用户: ${username}${lastUser ? ' (切换自 ' + lastUser + ')' : ''}`);
}
setAccount(username);
}
createPanel();
if (!document.getElementById('jms-float-btn')) {
const fb = document.createElement('div');
fb.id = 'jms-float-btn';
fb.title = 'JM Shelf';
fb.innerHTML = '<img src="https://i.postimg.cc/BQ8vkqZv/JM-メスガキ.png" style="width:36px;height:36px;border-radius:50%">';
fb.addEventListener('click', () => {
const p = document.getElementById('jms-panel') || createPanel();
p.style.display = p.style.display === 'block' ? 'none' : 'block';
});
document.body.appendChild(fb);
}
switch (pageType) {
case 'homepage': handleHomepage(); break;
case 'album': handleAlbumDetail(); break;
case 'photo': handlePhotoPage(); break;
case 'listing': handleListingPage(); break;
case 'user': handleUserPage(); break;
}
LOG.info(`📌 已收藏/喜欢${State.getFavorites().length}本, 推荐${State.getRecommendations().length}条, 已看${State.getViewedAlbums().length}`);
LOG.info('初始化完成');
}
// ═══ 调试工具 ═══
try {
unsafeWindow.jmsDebug = {
score(id) {
const cache = State.getAlbumCache();
const album = cache[String(id)];
if (!album) return `Album ${id} not in cache`;
const profile = ProfileManager.getEffective(State.getProfile()||{});
const favs = State.getFavorites();
const hist = State.getHistory();
const s = Recommender.score(album, profile, favs, hist, ProfileManager.deriveWeights(profile.popularityFingerprint||0.7));
const maxTagW = Math.max(...Object.values(profile.tags), 1);
const tf = State.getTagFreq();
const fv = Object.values(tf).filter(v=>v>0);
const mf = Math.max(...fv,1);
const detail = (album.tags||[]).map(t => {
const nt = normalizeTag(t);
const w = profile.tags[nt]||0;
if(w<=0) return null;
const gc = tf[nt]||0;
let idf = gc>0 ? clamp(1.0+Math.log10(Math.max(mf/gc,1))*0.6,0.6,2.0) : 1.0;
const raw = w/maxTagW;
return `${nt}(w${w} idf${idf.toFixed(2)} raw${raw.toFixed(2)}→${(raw*idf).toFixed(3)})`;
}).filter(Boolean);
return { id, title:album.title, score:s.score.toFixed(4), detail };
},
top(n) {
const recs = State.getRecommendations().slice(0,n||10);
const cache = State.getAlbumCache();
return recs.map((r,i)=>{const al=cache[r.id];return `${i+1}. [${r.id}] ${(r._wm||'def')} s${r.score.toFixed(2)} ${(al?.title||'').slice(0,30)}`});
}
};
LOG.info('调试工具已就绪: jmsDebug.score(albumId) / jmsDebug.top(N)');
} catch (e) {}
// ═══ START ═══
init().catch(e => LOG.error('初始化失败', e));