集思录我的关注页面优化脚本

闲来无事用AI写了个集思录首页我的关注的信息聚合优化油猴脚本。

访问集思录「我的关注」页面(https://www.jisilu.cn/home/mine/#all)后,脚本自动工作:
1. 自动Loading → 自动点击页面底部「更多」按钮,逐批加载历史数据,基本上倒推12个小时的更新内容
2. 按问题聚合 → 所有动态按问题标题分组,每个问题一张卡片,问题标题去重
3. 内容去重 → 相同回复内容只保留一条,关注/赞同汇总为「XX 人关注/赞同」
4. 时间倒序 → 卡片按最新活动排序,卡片内回复按时间倒序
完成后展示聚合视图,见下图效果。 有需要的话再把脚本贴上来吧。

整体页面效果



单图问题卡片效果(仅关注的人的回复)

发表时间 2026-06-12 18:19     来自上海

赞同来自: 奔魔 eckeels 肥铛

1

MoneyMemory - 初闻不知曲中意,再听已是曲中人。

赞同来自: binye2020

用了一段时间,感觉还凑活,需要脚本的自取。放在油猴插件里面启用,需要开启edge或chrome的开发者权限。
// ==UserScript==
// @name         集思录关注页去重聚合
// @namespace    https://www.jisilu.cn/
// @version      3.2.0
// @description  自动加载→按问题聚合→去重回复→已读追踪→仅看未读
// @author       Gu @ Software Workshop
// @match        https://www.jisilu.cn/home/mine/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
'use strict';

const CUTOFF_HOURS = 12;
const MAX_CLICKS = 40;
const LOAD_WAIT = 3000;
const CLICK_PAUSE = 800;

/* ========== 工具 ========== */

function h(str) { let v = 5381; for (let i = 0; i < str.length; i++) v = ((v << 5) + v) + str.charCodeAt(i) | 0; return Math.abs(v).toString(16); }
function qid(it) { const a = it.querySelector('h4 a[href*="/question/"]'); if (a) { const m = a.href.match(/\/question\/(\d+)/); if (m) return m[1]; } const f = it.querySelector('a[onclick*="focus_question"]'); if (f) { const m = f.getAttribute('onclick').match(/focus_question\([^,]+,\s*(\d+)/); if (m) return m[1]; } return null; }
function qtitle(it) { const a = it.querySelector('h4 a[href*="/question/"]'); return a ? a.textContent.trim() : ''; }
function qlink(it) { const a = it.querySelector('h4 a[href*="/question/"]'); return a ? a.href : ''; }
function rtext(it) { const box = it.querySelector('.markitup-box'); if (!box) return null; const t = (box.textContent || '').replace(/\s+/g, ' ').trim(); return t.length > 0 ? t : null; }
function uname(it) { const a = it.querySelector('.aw-mod-head a.aw-user-name'); return a ? a.textContent.trim() : ''; }
function act(it) { const p = it.querySelector('.aw-mod-head p.aw-text-color-999'); if (!p) return ''; const t = p.textContent || ''; if (t.includes('关注了该问题')) return 'follow'; if (t.includes('赞同了该问题')) return 'agree'; if (t.includes('回复')) return 'reply'; return 'other'; }
function ts(it) { const p = it.querySelector('.aw-mod-head p.aw-text-color-999'); if (p) { const m = p.textContent.match(/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})/); if (m) return new Date(m[1]).getTime(); } const id = it.getAttribute('data-id') || ''; const nm = id.match(/(\d+)/); return nm ? parseInt(nm[1], 10) : 0; }
function dkey(it) { const id = qid(it); if (!id) return null; const rt = rtext(it); return rt ? id + '::' + h(rt) : id + '::action'; }

/* ========== 时间范围 ========== */

function rawItems() {
return document.querySelectorAll('#main_contents div.aw-item:not(.jsl-group-header):not(.jsl-group-stats-bar):not(.jsl-group-btn-row)');
}

function allTimestamps() {
const r = [];
rawItems().forEach(it => { if (!it.closest('.jsl-group-card')) { const t = ts(it); if (t > 0) r.push(t); } });
return r;
}

function getNewestTime() { const a = allTimestamps(); return a.length ? Math.max(...a) : Date.now(); }
function getOldestTime() { const a = allTimestamps(); return a.length ? Math.min(...a) : Date.now(); }

/* ========== 点击「更多」加载 ========== */

async function autoLoad(onStatus) {
const newest = getNewestTime();
if (newest <= 0) { onStatus && onStatus(' 未检测到时间'); return; }
const cutoffMs = newest - CUTOFF_HOURS * 3600 * 1000;
let lastCount = 0;

for (let i = 0; i < MAX_CLICKS; i++) {
  const oldest = getOldestTime();
  if (oldest < cutoffMs) { onStatus && onStatus(' 已覆盖12小时'); return; }
  const cur = rawItems().length;
  if (i > 1 && cur === lastCount) { onStatus && onStatus(' 已全部加载'); return; }
  lastCount = cur;

  const moreBtn = document.querySelector('#bp_more, .aw-load-more-content');
  if (!moreBtn || moreBtn.style.display === 'none' || moreBtn.offsetParent === null) {
    onStatus && onStatus(' 无更多内容'); return;
  }

  onStatus && onStatus(' 加载 ' + cur + ' 条...');
  moreBtn.click();

  await new Promise(resolve => {
    let done = false, t = null;
    const obs = new MutationObserver(muts => {
      if (done) return;
      if (muts.some(m => Array.from(m.addedNodes).some(n => n.nodeType === 1 && n.classList && n.classList.contains('aw-item')))) {
        done = true; clearTimeout(t); obs.disconnect(); resolve();
      }
    });
    const c = document.getElementById('main_contents');
    if (c) obs.observe(c, { childList: true, subtree: true });
    t = setTimeout(() => { if (!done) { done = true; obs.disconnect(); resolve(); } }, LOAD_WAIT);
  });
  await new Promise(r => setTimeout(r, CLICK_PAUSE));
}
onStatus && onStatus(' 已达到点击上限');
}

/* ========== 已读追踪 ========== */

function getReadThreshold() {
return GM_getValue('jsl_read_latest', 0);
}

function saveReadRange(latestTs) {
GM_setValue('jsl_read_latest', latestTs);
}

/* ========== 聚合视图 ========== */

function restoreAllItems() {
const container = document.getElementById('main_contents');
// 将卡片内的条目移回 #main_contents
document.querySelectorAll('.jsl-group-card .jsl-reply-wrap .aw-item').forEach(el => {
  if (container) container.appendChild(el);
});
// 显示所有隐藏条目
rawItems().forEach(el => {
  el.style.display = '';
  el._jslHide = false;
  el._jslRead = false;
});
// 移除聚合UI
document.querySelectorAll('.jsl-group-card, .jsl-group-section').forEach(el => el.remove());
}

function buildGroupedView() {
const container = document.getElementById('main_contents');
if (!container) return;

restoreAllItems();

const allItems = Array.from(rawItems());
if (allItems.length === 0) return;

const readThreshold = getReadThreshold();

const groups = new Map();
const orphans = [];

allItems.forEach(item => {
  const id = qid(item);
  if (!id) { orphans.push(item); return; }
  if (!groups.has(id)) {
    groups.set(id, { title: qtitle(item), link: qlink(item), items: [], followers: [], latestTs: 0, total: 0, unreadCount: 0 });
  }
  const g = groups.get(id);
  const t = ts(item); if (t > g.latestTs) g.latestTs = t;
  g.total++;
  const a = act(item), dk = dkey(item), rt = rtext(item);

  if (a === 'follow' || a === 'agree') {
    const u = uname(item);
    if (u && !g.followers.includes(u)) g.followers.push(u);
    item._jslHide = true;
  } else if (rt) {
    if (!g._seen) g._seen = new Set();
    if (!g._seen.has(dk)) {
      g._seen.add(dk);
      g.items.push(item);
      if (t > readThreshold) g.unreadCount++;
    } else {
      item._jslHide = true;
    }
  } else {
    g.items.push(item);
    if (t > readThreshold) g.unreadCount++;
  }
});
groups.forEach(g => { delete g._seen; g.items.sort((a, b) => ts(b) - ts(a)); });
const sorted = Array.from(groups.values()).sort((a, b) => b.latestTs - a.latestTs);

allItems.forEach(item => { if (item._jslHide) item.style.display = 'none'; });

// 未读统计
let totalUnread = sorted.reduce((s, g) => s + g.unreadCount, 0) + orphans.length;

// ---- UI ----
const wrapper = document.createElement('div');
wrapper.className = 'jsl-group-section';

// 统计条
const stats = document.createElement('div');
stats.className = 'jsl-group-stats-bar';
let vis = sorted.reduce((s, g) => s + g.items.length, 0) + orphans.length;
let fol = sorted.reduce((s, g) => s + g.followers.length, 0);
stats.innerHTML = `<span>${sorted.length} 个话题 · ${vis} 条回复${fol > 0 ? ' · ' + fol + ' 人关注' : ''}</span>
  <span style="margin-left:12px;color:#e65100;">${readThreshold > 0 ? ' 未读 ' + totalUnread + ' 条' : ' 首次访问'}</span>`;
wrapper.appendChild(stats);

// 按钮行
const btns = document.createElement('div');
btns.className = 'jsl-group-btn-row';
btns.innerHTML = `
  <span class="jsl-btn jsl-btn-primary" id="jsl-toggle-unread">仅看未读</span>
  <span class="jsl-btn" id="jsl-raw-view">原始列表</span>
  <span class="jsl-btn" id="jsl-refresh">刷新</span>
`;
wrapper.appendChild(btns);

// 渲染卡片
sorted.forEach(g => {
  const card = document.createElement('div');
  card.className = 'jsl-group-card';
  if (g.unreadCount === 0 && readThreshold > 0) card.classList.add('jsl-card-all-read');

  const header = document.createElement('div');
  header.className = 'jsl-card-header';
  let meta = `${g.items.length} 条回复`;
  if (g.followers.length) meta += ` · ${g.followers.length} 人关注`;
  const deduped = g.total - g.items.length - g.followers.length;
  if (deduped > 0) meta += ` · 去重${deduped}条`;
  if (readThreshold > 0 && g.unreadCount > 0) meta += ` · <span style="color:#e65100;">${g.unreadCount}条未读</span>`;
  header.innerHTML = `<h4><a href="${g.link}" target="_blank">${g.title}</a></h4><span class="jsl-card-meta">${meta}</span>`;

  const body = document.createElement('div');
  body.className = 'jsl-card-body';
  let readCount = 0;

  g.items.forEach(item => {
    const wrap = document.createElement('div');
    wrap.className = 'jsl-reply-wrap';
    const itemTs = ts(item);
    if (readThreshold > 0 && itemTs <= readThreshold) {
      wrap.classList.add('jsl-reply-read');
      item._jslRead = true;
      readCount++;
    }
    wrap.appendChild(item);
    item._inCard = true;
    item.style.display = '';
    body.appendChild(wrap);
  });

  // 已读回复:默认折叠 + 底部切换条
  if (readCount > 0) {
    const readWraps = body.querySelectorAll('.jsl-reply-read');
    readWraps.forEach(w => { w.style.display = 'none'; });
    const toggle = document.createElement('div');
    toggle.className = 'jsl-read-toggle';
    toggle.innerHTML = `<span class="jsl-read-toggle-icon">▸</span> 已读 ${readCount} 条`;
    let readExpanded = false;
    toggle.addEventListener('click', () => {
      readExpanded = !readExpanded;
      toggle.querySelector('.jsl-read-toggle-icon').textContent = readExpanded ? '▾' : '▸';
      readWraps.forEach(w => { w.style.display = readExpanded ? '' : 'none'; });
    });
    body.appendChild(toggle);
  }

  if (g.followers.length > 0) {
    const fb = document.createElement('div');
    fb.className = 'jsl-follow-bar';
    fb.textContent = g.followers.join('、') + ' 关注/赞同了该问题';
    body.appendChild(fb);
  }

  let expanded = true;
  header.style.cursor = 'pointer';
  header.addEventListener('click', () => {
    expanded = !expanded;
    body.style.display = expanded ? '' : 'none';
    header.classList.toggle('jsl-card-collapsed', !expanded);
  });

  card.appendChild(header);
  card.appendChild(body);
  wrapper.appendChild(card);
});

if (orphans.length > 0) {
  const card = document.createElement('div');
  card.className = 'jsl-group-card';
  card.innerHTML = '<div class="jsl-card-header"><h4>其他动态</h4></div>';
  const body = document.createElement('div');
  body.className = 'jsl-card-body';
  orphans.forEach(item => {
    const wrap = document.createElement('div');
    wrap.className = 'jsl-reply-wrap';
    wrap.appendChild(item);
    item.style.display = '';
    body.appendChild(wrap);
  });
  card.appendChild(body);
  wrapper.appendChild(card);
}

container.insertBefore(wrapper, container.firstChild);
window.scrollTo(0, 0);

// ---- 按钮事件 ----
// 仅看未读
let showUnreadOnly = false;
document.getElementById('jsl-toggle-unread')?.addEventListener('click', function () {
  showUnreadOnly = !showUnreadOnly;
  this.textContent = showUnreadOnly ? '显示全部' : '仅看未读';
  document.querySelectorAll('.jsl-reply-wrap').forEach(el => {
    if (showUnreadOnly) {
      el._preToggle = el.style.display || '';
      if (el.classList.contains('jsl-reply-read')) el.style.display = 'none';
    } else {
      el.style.display = el._preToggle || '';
    }
  });
  // 隐藏已读切换条 + 空卡片
  document.querySelectorAll('.jsl-read-toggle').forEach(el => { el.style.display = showUnreadOnly ? 'none' : ''; });
  document.querySelectorAll('.jsl-group-card').forEach(card => {
    if (!showUnreadOnly) { card.style.display = ''; return; }
    const visible = card.querySelectorAll('.jsl-reply-wrap:not(.jsl-reply-read)').length;
    card.style.display = visible > 0 ? '' : 'none';
  });
});

document.getElementById('jsl-raw-view')?.addEventListener('click', () => {
  restoreAllItems();
  window.scrollTo(0, 0);
});
document.getElementById('jsl-refresh')?.addEventListener('click', () => buildGroupedView());
}

/* ========== 样式 ========== */

function injectStyles() {
if (document.getElementById('jsl-v3-style')) return;
const s = document.createElement('style');
s.id = 'jsl-v3-style';
s.textContent = `
  .jsl-group-stats-bar { padding:6px 12px; margin-bottom:8px; background:#e8f5e9; border:1px solid #a5d6a7; border-radius:4px; font-size:13px; color:#2e7d32; display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
  .jsl-group-btn-row { display:flex; gap:8px; margin-bottom:10px; }
  .jsl-btn { padding:3px 10px; font-size:12px; background:#f5f5f5; border:1px solid #ddd; border-radius:3px; cursor:pointer; color:#555; user-select:none; }
  .jsl-btn:hover { background:#e3f2fd; border-color:#90caf9; color:#1565c0; }
  .jsl-btn-primary { background:#24be58 !important; border-color:#24be58 !important; color:#fff !important; }
  .jsl-btn-primary:hover { background:#1fa54a !important; }
  .jsl-group-card { background:#fff; border:1px solid #e0e0e0; border-radius:6px; margin-bottom:12px; overflow:hidden; box-shadow:0 1px 3px rgba(0,0,0,.06); }
  .jsl-card-all-read { opacity:0.65; }
  .jsl-card-header { padding:10px 14px; background:#fafafa; border-bottom:1px solid #eee; }
  .jsl-card-header:hover { background:#f0f0f0; }
  .jsl-card-header h4 { margin:0 0 4px; font-size:15px; line-height:1.4; }
  .jsl-card-header h4 a { color:#333; text-decoration:none; }
  .jsl-card-header h4 a:hover { color:#24be58; }
  .jsl-card-meta { font-size:12px; color:#999; }
  .jsl-card-collapsed { border-bottom:none !important; }
  .jsl-card-body { }
  .jsl-reply-wrap { border-bottom:1px solid #f0f0f0; }
  .jsl-reply-wrap:last-child { border-bottom:none; }
  .jsl-reply-wrap .aw-item { margin:0 !important; border:none !important; padding:8px 14px !important; }
  .jsl-reply-wrap .aw-item .aw-mod-head h4 { display:none !important; }

  /* 已读回复:灰色标签 */
  .jsl-reply-read { position:relative; opacity:0.6; }
  .jsl-reply-read::after { content:"已读"; position:absolute; top:6px; right:10px; font-size:10px; color:#999; background:#f5f5f5; padding:1px 6px; border-radius:2px; }

  /* 已读折叠切换条 */
  .jsl-read-toggle { padding:5px 14px; font-size:12px; color:#999; cursor:pointer; background:#fafafa; border-top:1px dashed #e0e0e0; user-select:none; }
  .jsl-read-toggle:hover { color:#666; background:#f0f0f0; }
  .jsl-read-toggle-icon { font-size:10px; margin-right:2px; }

  .jsl-follow-bar { padding:6px 14px; font-size:12px; color:#888; background:#f9f9f9; border-top:1px solid #eee; }
  .jsl-loader { position:fixed; bottom:20px; right:20px; padding:8px 16px; background:rgba(0,0,0,.75); color:#fff; border-radius:20px; font-size:13px; z-index:10001; pointer-events:none; box-shadow:0 2px 8px rgba(0,0,0,.2); }

  .jsl-loading-overlay { position:fixed; top:0; left:0; width:100%; height:100%; z-index:10000; background:rgba(255,255,255,.92); display:flex; flex-direction:column; align-items:center; justify-content:center; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; }
  .jsl-loading-spinner { width:44px; height:44px; border:4px solid #e0e0e0; border-top-color:#24be58; border-radius:50%; animation:jsl-spin .8s linear infinite; margin-bottom:20px; }
  @keyframes jsl-spin { to { transform:rotate(360deg); } }
  .jsl-loading-title { font-size:18px; color:#333; margin-bottom:8px; font-weight:500; }
  .jsl-loading-sub { font-size:14px; color:#999; }
  .jsl-loading-dots::after { content:""; animation:jsl-dots 1.5s steps(4,end) infinite; }
  @keyframes jsl-dots { 0%{content:""} 25%{content:"."} 50%{content:".."} 75%{content:"..."} 100%{content:""} }
`;
document.head.appendChild(s);
}

/* ========== Loading ========== */

function showLoading(msg) {
let ov = document.getElementById('jsl-loading-overlay');
if (!ov) {
  ov = document.createElement('div');
  ov.id = 'jsl-loading-overlay';
  ov.className = 'jsl-loading-overlay';
  ov.innerHTML = `
    <div class="jsl-loading-spinner"></div>
    <div class="jsl-loading-title">数据自动加载处理中</div>
    <div class="jsl-loading-sub">${msg || '请等待 3-5 秒'}<span class="jsl-loading-dots"></span></div>
  `;
  document.body.appendChild(ov);
}
if (msg) {
  const sub = ov.querySelector('.jsl-loading-sub');
  if (sub) sub.innerHTML = msg + '<span class="jsl-loading-dots"></span>';
}
ov.style.display = 'flex';
}

function hideLoading() {
const ov = document.getElementById('jsl-loading-overlay');
if (ov) ov.style.display = 'none';
}

/* ========== 入口 ========== */

async function run() {
if (!document.getElementById('main_contents')) return;
injectStyles();

showLoading('正在点击「更多」加载历史数据');

const loader = document.createElement('div');
loader.className = 'jsl-loader';
document.body.appendChild(loader);
const stat = msg => { loader.textContent = msg; loader.style.display = 'block'; };
const hideLoader = () => { loader.style.display = 'none'; };

stat(' 正在加载...');
await autoLoad(stat);
hideLoader();

showLoading('数据加载完成,正在聚合去重');
await new Promise(r => setTimeout(r, 300));

buildGroupedView();

// 保存已读标记:以当前刷新时间为准
saveReadRange(Date.now());

hideLoading();

window.addEventListener('hashchange', async () => {
  await new Promise(r => setTimeout(r, 600));
  hideLoader();
  buildGroupedView();
});
}

GM_registerMenuCommand(' 刷新聚合', () => buildGroupedView());
GM_registerMenuCommand(' 重新加载+聚合', async () => {
showLoading('重新加载数据中');
const l = document.querySelector('.jsl-loader');
if (l) { l.style.display = 'block'; l.textContent = ' 重新加载...'; }
await autoLoad(msg => { if (l) l.textContent = msg; });
if (l) l.style.display = 'none';
showLoading('正在聚合去重');
await new Promise(r => setTimeout(r, 200));
buildGroupedView();
saveReadRange(Date.now());
hideLoading();
});
GM_registerMenuCommand(' 清除已读记录', () => { GM_setValue('jsl_read_latest', 0); alert('已读记录已清除,下次访问全部视为未读'); });

if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => setTimeout(run, 800));
else setTimeout(run, 800);
})();
2026-06-25 20:27 来自上海 引用
1

binye2020

赞同来自: KevinLe

发出来试试看啊
2026-06-13 14:16 来自新加坡 引用
2

MoneyMemory - 初闻不知曲中意,再听已是曲中人。

赞同来自: KevinLe 符工

增加了标记未读功能,同一个设备上,已经看过的内容会降级显示,按时间点判断的会粗糙一点。

表头部分展示多少条未读



已读的正常显示和未读的弱化显示

2026-06-12 20:58 来自上海 引用

要回复问题请先登录注册

发起人

MoneyMemory
MoneyMemory

初闻不知曲中意,再听已是曲中人。

问题状态

  • 最新活动: 2026-06-25 20:27
  • 浏览: 1009
  • 关注: 17