// ==UserScript==
// @name COLG 阅帖助手
// @namespace colg-assistant
// @version 0.3.2
// @description 在 colg(bbs.colg.cn)页面右侧叠加看板:分流高价值(收藏≥5)与日常帖,作者黑名单标红/好友标黄。默认收起不挡页面、可全屏、排序切换。⚠ 仅供个人学习交流,请低频使用(建议间隔≥5分钟),风控风险自担。
// @match https://bbs.colg.cn/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @require https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js
// @license MIT
// ==/UserScript==
(function () {
'use strict';
if (window.top !== window) return;
var FID = 171, LIMIT = 150, MIN_COLLECT = 5;
var AUTO_REFRESH = true; // 打开/刷新页面自动抓一次
var CACHE_TTL = 5 * 60 * 1000; // 5 分钟内用缓存,不重复抓(防频繁进帖触发风控)
var CACHE_KEY = 'colg-us-data', CACHE_TIME_KEY = 'colg-us-time';
var DEFAULT_COLLAPSED = true; // 默认收起
var CSS = [
'#cusPanel{position:fixed;top:0;right:0;width:380px;height:100%;background:#12141d;color:#e6e8ee;box-shadow:-8px 0 26px rgba(0,0,0,.28);z-index:2147483000;display:flex;flex-direction:column;transition:transform .25s ease;}',
'#cusPanel.full{width:100%;box-shadow:none;}',
'#cusPanel.collapsed{transform:translateX(100%);box-shadow:none;}',
'#cusPanel .cus-head{padding:12px 16px 10px;border-bottom:1px solid #2c3140;}',
'#cusPanel .cus-htop{display:flex;justify-content:space-between;align-items:center;gap:6px;}',
'#cusPanel .cus-title{font-size:15px;font-weight:700;color:#e6e8ee;flex:1;}',
'#cusPanel .cus-sub{font-size:11px;color:#8b91a3;margin-top:3px;}',
'#cusPanel .cus-hdbtn{background:#21242f;color:#e6e8ee;border:1px solid #2c3140;border-radius:7px;width:30px;height:30px;cursor:pointer;font-size:15px;line-height:1;}',
'#cusPanel .cus-body{flex:1;overflow:hidden;padding:12px;display:flex;flex-direction:column;gap:12px;}',
'#cusPanel.full .cus-body{flex-direction:row;align-items:stretch;padding:22px 26px;}',
'#cusPanel .cus-phone{background:#1a1d27;border:1px solid #2c3140;border-radius:12px;padding:10px;display:flex;flex-direction:column;min-height:0;}',
'#cusPanel:not(.full) .cus-phone{flex:1 1 0;}',
'#cusPanel.full .cus-phone{flex:1 1 0;}',
'#cusPanel .cus-pt{display:flex;justify-content:space-between;font-size:13px;font-weight:700;color:#e6e8ee;flex:none;}',
'#cusPanel .cus-pt .c{font-size:11px;color:#8b91a3;font-weight:400;background:#21242f;border:1px solid #2c3140;padding:1px 8px;border-radius:10px;}',
'#cusPanel .cus-cards{flex:1;overflow:auto;margin-top:2px;}',
'#cusPanel .cus-card{background:#21242f;border:1px solid #2c3140;border-radius:8px;padding:8px 10px;margin-top:8px;cursor:pointer;}',
'#cusPanel .cus-card:hover{border-color:#3C9D40;}',
'#cusPanel .cus-card .t{font-weight:600;font-size:12.5px;line-height:1.35;color:#e6e8ee;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;}',
'#cusPanel .cus-card .b{display:flex;justify-content:space-between;color:#8b91a3;font-size:11px;margin-top:7px;}',
'#cusPanel .cus-btns{padding:9px 12px;display:flex;gap:8px;border-top:1px solid #2c3140;background:#0e1017;justify-content:flex-end;}',
'#cusPanel .cus-btns button{background:#21242f;color:#e6e8ee;border:1px solid #2c3140;border-radius:7px;padding:5px 10px;font-size:12px;cursor:pointer;}',
'#cusPanel .cus-btns button.active{background:#3C9D40;color:#fff;border-color:#3C9D40;}',
'#cusPanel .cus-progress{height:3px;background:#21242f;overflow:hidden;display:none;}',
'#cusPanel .cus-progress.on{display:block;}',
'#cusPanel .cus-progress i{display:block;height:100%;width:40%;background:#3C9D40;animation:cusp 1s linear infinite;}',
'@keyframes cusp{from{transform:translateX(-100%)}to{transform:translateX(350%)}}',
'.cus-loading{display:flex;align-items:center;justify-content:center;gap:6px;padding:34px 0;color:#8b91a3;font-size:12px;}',
'.cus-loading i{width:12px;height:12px;border:2px solid #3C9D40;border-top-color:transparent;border-radius:50%;animation:cuspin .7s linear infinite;}',
'@keyframes cuspin{to{transform:rotate(360deg)}}',
'#cusTab{position:fixed;right:0;top:42%;width:30px;height:60px;background:#12141d;color:#e6e8ee;border:1px solid #2c3140;border-right:none;border-radius:8px 0 0 8px;cursor:pointer;z-index:2147483001;font-size:16px;}',
'.cus-empty{color:#8b91a3;text-align:center;padding:26px 8px;font-size:12px;line-height:1.6;}',
'.cus-empty.err{color:#e05f5f;}',
'/* 深色滚动条 */',
'#cusPanel ::-webkit-scrollbar{width:8px;height:8px;}',
'#cusPanel ::-webkit-scrollbar-thumb{background:#2c3140;border-radius:4px;}',
'#cusPanel ::-webkit-scrollbar-thumb:hover{background:#3C9D40;}',
'#cusPanel ::-webkit-scrollbar-track{background:transparent;}',
'/* 板块标题图标 */',
'#cusPanel .cus-pt{font-size:14px;}',
'#cusPanel .cus-pt .ic{margin-right:5px;}',
'/* 帮助气泡 */',
'#cusPanel .cus-help{position:relative;}',
'#cusPanel .cus-tip{display:none;position:absolute;top:38px;right:0;width:252px;background:#1a1d27;border:1px solid #2c3140;border-radius:10px;padding:12px;font-size:12px;color:#c8ccd8;line-height:1.7;z-index:30;box-shadow:0 8px 24px rgba(0,0,0,.4);}',
'#cusPanel .cus-help:hover .cus-tip{display:block;}',
'#cusPanel .cus-note{padding:7px 16px;font-size:11px;color:#e0a94a;border-top:1px solid #2c3140;background:#151723;}'
].join('\n');
GM_addStyle(CSS);
function injectPanel() {
var el = document.createElement('div');
el.id = 'cusPanel';
el.innerHTML =
'
COLG 阅帖助手
' +
'
· 《 全屏 / 》 还原:两块放大看板
· × 收起:隐藏看板(右侧 ◀ 展开)
· ↻ 刷新:重新抓取,自动分流高价值/日常
· 按发布 / 按最后回复:切换排序
· 作者红色(拉黑)= 对方拉黑了你;黄色(好友)= 好友
· 收藏≥5 归入「高价值」
⚠ 请低频使用(间隔≥5分钟),避免被风控
' +
'
' +
'
' +
'' +
'⚠ 仅供个人学习 · 请低频使用(间隔≥5分钟),避免被风控
' +
'';
document.body.appendChild(el);
var tab = document.createElement('button');
tab.id = 'cusTab'; tab.textContent = '◀'; tab.style.display = 'block';
tab.onclick = function () { el.classList.remove('collapsed'); tab.style.display = 'none'; };
document.body.appendChild(tab);
function toggleFull() {
var full = el.classList.toggle('full');
document.getElementById('cusFullBtn').textContent = full ? '》' : '《';
}
document.getElementById('cusFullBtn').onclick = toggleFull;
document.getElementById('cusCollapseBtn').onclick = function () { el.classList.add('collapsed'); tab.style.display = 'block'; };
document.getElementById('cusRefresh').onclick = refresh;
function setActive(id) { var p = document.getElementById('cusSortP'), r = document.getElementById('cusSortR'); p.classList.toggle('active', id === 'cusSortP'); r.classList.toggle('active', id === 'cusSortR'); }
document.getElementById('cusSortP').onclick = function () { sort = 'published'; setActive('cusSortP'); render(); };
document.getElementById('cusSortR').onclick = function () { sort = 'lastreply'; setActive('cusSortR'); render(); };
setActive('cusSortP');
if (DEFAULT_COLLAPSED) { el.classList.add('collapsed'); tab.style.display = 'block'; }
return el;
}
async function fetchText(url) { var r = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }); return await r.text(); }
function parseList(html) {
var doc = new DOMParser().parseFromString(html, 'text/html');
var $$ = jQuery(doc), posts = [], seen = {};
$$.find('tbody[id^="normalthread_"]').each(function () {
var t = jQuery(this), title = t.find('a.xst');
if (!title.length) return;
var tid = (t.attr('id') || '').replace('normalthread_', '');
if (!tid || seen[tid]) return; seen[tid] = 1;
var href = title.attr('href') || '', uid = (href.match(/uid=(\d+)/) || [])[1] || '';
posts.push({ tid: tid, title: title.text().trim(), author: t.find('td.by > cite > a').first().text().trim(), authorUid: uid, publish: parseInt(t.find('a[id$="_new_show"]').attr('dbdateline'), 10) || 0, lastReply: parseInt(t.find('a[id$="_new_show"]').attr('dblastpost'), 10) || 0, replies: parseInt(t.find('td.num a.xi2').text(), 10) || 0, url: 'https://bbs.colg.cn/thread-' + tid + '-1-1.html', collect: 0, black: null });
});
return posts;
}
async function fetchCollect(tid) {
try { var j = await (await fetch('https://bbs.colg.cn/forum.php?mod=ajax&action=async_get_thread_info&tid=' + tid, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })).json(); return +((j.data && j.data.thread_collect_count) || 0); } catch (e) { return 0; }
}
async function fetchBlack(uid) {
if (!uid) return 'other';
try {
var r = await fetch('https://bbs.colg.cn/home.php?mod=spacecp&ac=friend&op=add&uid=' + uid, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
var doc = new DOMParser().parseFromString(await r.text(), 'text/html');
if (jQuery(doc).find('#return_').length) return 'other';
var msg = jQuery(doc).find('#messagetext p:first').text();
if (msg.indexOf('好友') > -1) return 'friend';
if (msg.indexOf('隐私设置') > -1) return 'black';
return 'other';
} catch (e) { return 'other'; }
}
function setProg(on) { var p = document.getElementById('cusProg'); if (p) p.classList.toggle('on', !!on); }
function loading(idx) { jQuery('#cusPanel .cus-phone:eq(' + idx + ') .cus-cards').html('加载中…
'); }
async function refresh() {
setProg(true);
loading(0); loading(1);
try {
var all = {}, order = [];
for (var p = 1; p <= 4; p++) {
try { var html = await fetchText('https://bbs.colg.cn/forum-' + FID + '-' + p + '.html'); var arr = parseList(html); arr.forEach(function (x) { if (!all[x.tid]) { all[x.tid] = x; order.push(x); } }); } catch (e) { console.error('列表页 ' + p + ' 抓取失败:', e); }
if (order.length >= LIMIT) break;
}
if (!order.length) throw new Error('列表页解析到 0 条(可能被风控或需登录)');
var posts = order.slice(0, LIMIT);
// 并行补收藏
var i = 0;
async function wC() { while (i < posts.length) { var k = i++; posts[k].collect = await fetchCollect(posts[k].tid); } }
await Promise.all([wC(), wC(), wC(), wC(), wC(), wC(), wC(), wC()]);
// 并行黑名单(作者去重)
var uids = {}; posts.forEach(function (x) { if (x.authorUid) uids[x.authorUid] = 1; });
var uidArr = Object.keys(uids), j = 0;
async function wB() { while (j < uidArr.length) { var u = uidArr[j++], b = await fetchBlack(u); if (b !== 'other') console.log('黑名单探测 uid=' + u + ' -> ' + b); posts.forEach(function (x) { if (x.authorUid === u) x.black = b; }); } }
await Promise.all([wB(), wB(), wB(), wB()]);
state.posts = posts; // 全部抓完+分流数据就绪,才一次性显示
saveCache(); // 存缓存,5 分钟内刷新页直接用,不再发请求
console.log('分流完成'); render();
} catch (e) { console.error('刷新失败:', e); jQuery('#cusPanel .cus-phone:eq(0) .cus-cards').html('刷新失败:' + e.message + '
'); }
finally { setProg(false); }
}
function fmtTime(ts) {
if (!ts) return '—';
var d = new Date(+ts * 1000), now = Math.floor(Date.now() / 1000), diff = now - +ts;
if (diff < 60) return '刚刚';
if (diff < 3600) return Math.floor(diff / 60) + ' 分钟前';
if (diff < 86400) return Math.floor(diff / 3600) + ' 小时前';
if (diff < 86400 * 7) return Math.floor(diff / 86400) + ' 天前';
return (d.getMonth() + 1) + '/' + d.getDate();
}
function esc(s) { return String(s || '').replace(/[&<>"']/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; }); }
function card(p) {
var flag = p.black === 'black' ? '(拉黑) ' : (p.black === 'friend' ? '(好友) ' : '');
return '' + flag + esc(p.title) + '
' + esc(p.author) + ' · ' + fmtTime(p.publish) + '♥' + p.collect + ' · 💬' + p.replies + '
';
}
function xtime(p) { return sort === 'lastreply' ? (+p.lastReply || 0) : (+p.publish || 0); }
var state = { posts: [] }, sort = 'published';
function render() {
var list = state.posts.slice().sort(function (a, b) { return xtime(b) - xtime(a); });
var s1 = list.filter(function (p) { return p.collect >= MIN_COLLECT; });
var s2 = list.filter(function (p) { return p.collect < MIN_COLLECT; });
jQuery('#cusPanel .cus-phone:eq(0) .cus-cards').html(s1.length ? s1.map(card).join('') : '暂无
');
jQuery('#cusPanel .cus-phone:eq(0) .cus-pt .c').text(s1.length + ' 条');
jQuery('#cusPanel .cus-phone:eq(1) .cus-cards').html(s2.length ? s2.map(card).join('') : '暂无
');
jQuery('#cusPanel .cus-phone:eq(1) .cus-pt .c').text(s2.length + ' 条');
}
function saveCache() { try { GM_setValue(CACHE_KEY, state.posts); GM_setValue(CACHE_TIME_KEY, Date.now()); } catch (e) {} }
function loadCache() { try { var t = GM_getValue(CACHE_TIME_KEY, 0); if (Date.now() - t < CACHE_TTL) { var p = GM_getValue(CACHE_KEY, null); if (p && p.length) { state.posts = p; return true; } } return false; } catch (e) { return false; } }
if (!document.getElementById('cusPanel')) injectPanel();
if (loadCache()) { render(); } // 5 分钟内有缓存 -> 直接用,不重新抓(防频繁进帖触发风控)
// 首次/缓存过期 -> 延迟抓一次;手动点「刷新」则强制抓
refresh = refresh.bind ? refresh : refresh;
setTimeout(function () { if (AUTO_REFRESH && !state.posts.length) refresh(); }, 1500);
document.getElementById('cusRefresh').onclick = function () { loadCache(); refresh(); };
})();