edit | blame | history | raw

---
tags:
- tagfeed-live
cssclasses:

- tagfeed

🏷 #FuckGFW(实时聚合页)

本页由 DataviewJS 实时计算,笔记有变动时自动刷新,无需任何后台服务。

// ============================================================
//  tagfeed-live:Logseq 风格标签实时聚合(标准 DataviewJS 脚本)
//
//  用法:
//   1. 新建一个笔记(例如 00index/tagfeed-md/标签名.md)
//   2. 在笔记里插入一个 dataviewjs 代码块(三个反引号 + dataviewjs),
//      把本脚本全部粘贴进去
//   3. 只需要改下面「配置区」的两行:
//        - targetTag    换成你要聚合的标签(不带 #,中英文都可以;多个标签写成数组 ["a", "b"])
//        - excludedPaths 换成你想排除的文件夹
//   4. 完成。以后任何笔记里出现/删除这个标签,本页自动刷新,
//      不需要跑任何命令(Dataview 会在文件变动后自动重新渲染)。
// ============================================================

// ---------------- 配置区(通常只改这里) ----------------
const targetTag = "FuckGFW";        // 要聚合的标签(不带 #);多个标签用数组:["lyric", "歌词"]
const excludedPaths = ["X2.Archived", "00index", "X1.Knomo", "X.Attachment", "P3.bobo", "02DS/02copilot", "02DS/01dril-book", ".agents", ".claude", ".copilot", ".opencode", ".smart-env", ".workbuddy", ".trash"];        // 要排除的文件夹/路径前缀(vault 相对路径)
                                 // 例如:["00index", "模板", "X1.Knomo"]
// 补充说明:
//   - 所有隐藏文件/文件夹(. 开头:.agents/.claude/.copilot/.opencode/.smart-env/.workbuddy/.trash 等)与 X.Attachment 始终跳过
//   - tagfeed 系统页会自动跳过:静态聚合页(文件头含 <!-- tagfeed 标记)
//     和实时页(frontmatter 带 tagfeed / tagfeed-live 标签),
//     无需手动排除 00index
//   - 本脚本所在的当前笔记也会自动跳过
// ---------------------------------------------------------

try {
const SKIP_DIRS = [".obsidian", ".git", ".trash", "X.Attachment"];
const MARKER = "<!-- tagfeed";
const WEEKDAYS = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
const PREV_CHARS = 500;    // 预览字数上限:≤上限全文展示;>上限截断并附「查看全文」链接

const esc = s => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const TAG_LIST = Array.isArray(targetTag) ? targetTag : [targetTag];
const tagKey = s => String(s).toLowerCase();
const TAG_LIST_LOWER = new Set(TAG_LIST.map(tagKey));
const hasFmTag = (tags, wanted) => {
  const key = tagKey(wanted);
  for (const tag of tags) if (tagKey(tag) === key) return true;
  return false;
};
const hasAnyTargetFmTag = tags => {
  for (const tag of tags) if (TAG_LIST_LOWER.has(tagKey(tag))) return true;
  return false;
};
const TAG_RE  = new RegExp("(?<![\\w\\u4e00-\\u9fff#])#(?:" + TAG_LIST.map(esc).join("|") + ")(?![\\w\\u4e00-\\u9fff/\\-])", "i");
const ANY_TAG = /(?<![\w#])#([\w\u4e00-\u9fff][\w\u4e00-\u9fff/\-]*)/g;
const LIST_RE = /^(\s*)([-*+]|\d+[.)])(?:\s+(.*))?$/;
const HEAD_RE = /^(#{1,6})\s+(.*)$/;
const DATEH_RE = /\[\[(\d{4})-(\d{2})-(\d{2})\]\]/;
const TIME_RE = /(\d{1,2}):(\d{2})(?::(\d{2}))?/;
const FENCE_RE = /^\s*(`{3,}|~~~)/;
const indentOf = ln => {                    // tab 按 2 空格计(与 Python 版一致)
  const expanded = ln.replace(/\t/g, "  ");
  return expanded.length - expanded.replace(/^ +/, "").length;
};

function cleanMatch(ln) {
  let s = ln.replace(/`[^`\n]*`/g, "");
  s = s.replace(/<!--[\s\S]*?-->/g, "");
  return s;
}

const MACHINE_LINE_RE = [
  /Switch to EXCALIDRAW VIEW/i,
  /MORE OPTIONS menu of this document/i,
  /Decompress current Excalidraw file/i,
  /plugin settings under ['"]Saving['"]/i,
  /^\s*#{1,6}\s*Drawing\s*$/i,
  /^\s*```(?:compressed-json|json)?\s*$/i,
  /^\s*[\{\[]\s*"type"\s*:\s*"excalidraw"/i,
  /^\s*"elements"\s*:/i,
];
const HTML_COMMENT_RE = /<!--[\s\S]*?-->/g;
const TEMPLATE_RE = /<%[\s\S]*?%>/g;
const HTML_TAG_RE = /<\/?[A-Za-z][^>\n]*>/g;
const PASTED_IMAGE_MD_RE = /!?\[[^\]\n]*Pasted Image[^\]\n]*\]\([^)]+?\.(?:png|jpe?g|gif|webp)\)/gi;
const PASTED_IMAGE_WIKI_RE = /!?\[\[[^\]\n]*Pasted Image[^\]\n]*\.(?:png|jpe?g|gif|webp)(?:\|[^\]\n]*)?\]\]/gi;
const PASTED_IMAGE_TEXT_RE = /\bPasted Image[^\n]*?\.(?:png|jpe?g|gif|webp)\b/gi;
const SENSITIVE_TOKEN_LINE_RE = /\b(?:github_pat_[A-Za-z0-9_]+|gh[pousr]_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,})\b/i;
const SENSITIVE_TOKEN_RE = /\b(?:github_pat_[A-Za-z0-9_]+|gh[pousr]_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,})\b/g;
const LONG_HEX_RE = /\b[0-9a-fA-F]{32,}\b/g;
const EXCALIDRAW_DATA_HEADING_RE = /^\s*#{1,6}\s+Excalidraw Data\s*$/i;
const isExcalidrawDataHeading = ln => EXCALIDRAW_DATA_HEADING_RE.test(String(ln || "").trim());

const EXCALIDRAW_BLOCK_REF_RE = /\s+\^[A-Za-z0-9_-]+\s*$/;

function extractFrontmatterPrefix(text) {
  const m = String(text || "").match(/^---\s*\n[\s\S]*?\n(?:---|\.\.\.)\s*\n/);
  return m ? m[0].trimEnd() : "";
}

function extractExcalidrawTextElements(text) {
  const lines = String(text || "").split("\n");
  const start = lines.findIndex(ln => /^\s*##\s+Text Elements\s*$/i.test(ln));
  if (start < 0) return "";
  const chunks = [];
  let current = [];
  const flush = () => {
    const joined = current.map(x => x.trim()).filter(Boolean).join(" ").replace(/[ \t]{2,}/g, " ").trim();
    if (joined) chunks.push(joined);
    current = [];
  };
  for (let i = start + 1; i < lines.length; i++) {
    const raw = lines[i];
    const stripped = raw.trim();
    if (stripped === "%%" || /^\s*##\s+(?:Embedded Files|Drawing)\s*$/i.test(raw)) break;
    if (!stripped) {
      if (current.length) current.push("");
      continue;
    }
    const hadRef = EXCALIDRAW_BLOCK_REF_RE.test(raw);
    const cleaned = raw.replace(EXCALIDRAW_BLOCK_REF_RE, "").trim();
    if (cleaned) current.push(cleaned);
    if (hadRef) flush();
  }
  flush();
  return chunks.join("\n\n");
}

function readableExcalidrawMarkdown(text) {
  const frontmatter = extractFrontmatterPrefix(text);
  const body = extractExcalidrawTextElements(text);
  return [frontmatter, body].filter(Boolean).join("\n\n");
}

function sanitizeHumanLine(ln) {
  let s = String(ln || "");
  if (!s.trim()) return null;
  if (MACHINE_LINE_RE.some(re => re.test(s))) return null;
  if (SENSITIVE_TOKEN_LINE_RE.test(s)) return null;
  s = s.replace(HTML_COMMENT_RE, "");
  s = s.replace(TEMPLATE_RE, "");
  s = s.replace(PASTED_IMAGE_MD_RE, "");
  s = s.replace(PASTED_IMAGE_WIKI_RE, "");
  s = s.replace(PASTED_IMAGE_TEXT_RE, "");
  s = s.replace(SENSITIVE_TOKEN_RE, "[已隐藏敏感内容]");
  s = s.replace(LONG_HEX_RE, "[已隐藏机器ID]");
  s = s.replace(HTML_TAG_RE, "");
  s = s.replace(/[ \t]{2,}/g, " ").trim();
  const residue = s.replace(/\[已隐藏机器ID\]|\[已隐藏敏感内容\]/g, "").replace(/[^A-Za-z0-9\u4e00-\u9fff]+/g, "");
  if (!s || !residue || /^[-*+>]*$/.test(s)) return null;
  return s;
}

function humanizeBlock(block) {
  const out = [];
  for (const row of block) {
    const lvl = Array.isArray(row) ? row[0] : 0;
    const txt = sanitizeHumanLine(Array.isArray(row) ? row[1] : row);
    if (txt) out.push([lvl, txt]);
  }
  if (!out.length) return [];
  const minLevel = Math.min(...out.map(x => x[0]));
  return out.map(([lvl, txt]) => [Math.max(0, lvl - minLevel), txt]);
}
function vdate(y, m, d) {
  const dt = new Date(y, m - 1, d);
  return (dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d) ? dt : null;
}
function dateFromName(name) {           // 文件名含 YYYY-MM-DD,或日记命名 26.0805 -> 2026-08-05
  const m1 = name.match(/(\d{4})-(\d{2})-(\d{2})/);
  if (m1) {
    const d1 = vdate(+m1[1], +m1[2], +m1[3]);
    if (d1) return d1;
  }
  const m = name.match(/^(\d{2})\.(\d{2})(\d{2})(?:\D|$)/);
  return m ? vdate(2000 + +m[1], +m[2], +m[3]) : null;
}
function parseFrontmatter(text) {
  const lines = text.split("\n");
  if (!lines.length || lines[0].trim() !== "---") return { tags: new Set(), date: null, bodyStart: 0 };
  const n = Math.min(lines.length, 200);
  for (let i = 1; i < n; i++) {
    const t = lines[i].trim();
    if (t === "---" || t === "...") {
      const block = lines.slice(1, i).join("\n");
      const tags = new Set();
      const im = block.match(/^tags:\s*\[(.*?)\]/m);
      if (im) {
        for (const x0 of im[1].split(",")) {
          const x = x0.trim().replace(/^["']|["']$/g, "").replace(/^#/, "").trim();
          if (x) tags.add(x);
        }
      } else {
        const bm = block.match(/^tags:\s*\n((?:[ \t]+-[ \t]+.*\n?)+)/m);
        if (bm) for (const ln of bm[1].split("\n")) {
          const m2 = ln.match(/^\s*-\s*["']?([\w\u4e00-\u9fff/\-]+)["']?\s*$/);
          if (m2) tags.add(m2[1]);
        }
      }
      let fdate = null;
      for (const key of ["date", "created", "published"]) {
        const dm = block.match(new RegExp("^" + key + ":\\s*[\"']?(\\d{4})-(\\d{2})-(\\d{2})", "m"));
        if (dm) { fdate = vdate(+dm[1], +dm[2], +dm[3]); break; }
      }
      return { tags, date: fdate, bodyStart: i + 1 };
    }
  }
  return { tags: new Set(), date: null, bodyStart: 0 };
}
const stripTag = s => String(s).replace(TAG_RE, "").replace(/[ \t]{2,}/g, " ").trim();

function scanText(text, note, relpath, fileDate, mtimeDate) {
  const fm = parseFrontmatter(text);
  if (hasFmTag(fm.tags, "tagfeed") || hasFmTag(fm.tags, "tagfeed-live")) return [];  // 跳过 tagfeed 系统页(静态聚合页/实时页)
  const lines = text.split("\n");
  const entries = [], seen = new Set();
  let order = 0, inFence = false, inPct = false, inHtml = false;
  let curHead = null, curHeadRaw = null, curDateHead = null;

  const entryDate = () => curDateHead || fileDate || fm.date || mtimeDate;
  const anchorOf  = () => (curDateHead ? null : curHeadRaw);
  const breadcrumb = () => {
    if (curHead && !DATEH_RE.test(curHead)) {
      const t = curHead.replace(ANY_TAG, "").replace(/\s+/g, " ").trim();
      if (t) return t;
    }
    return null;
  };
  function addBlock(keyIdx, block) {
    block = humanizeBlock(block);
    const key = relpath + "|" + keyIdx;
    if (seen.has(key) || !block.length) return;
    seen.add(key);
    const d = entryDate();
    if (!d) return;
    let t = null;
    const tm = block[0][1].slice(0, 30).match(TIME_RE);
    if (tm) t = [+tm[1], +tm[2], +(tm[3] || 0)];
    order++;
    entries.push({ date: d, time: t, order, relpath, note,
                   breadcrumb: breadcrumb(), lines: block,
                   pageLevel: false, notes: [note], anchor: anchorOf() });
  }
  function collectChildren(i, parentIndent) {
    const kids = [], raw = [];
    let j = i + 1;
    while (j < lines.length) {
      const ln = lines[j];
      if (ln.trim() === "") {
        let k = j + 1;
        while (k < lines.length && lines[k].trim() === "") k++;
        if (k < lines.length && LIST_RE.test(lines[k]) && indentOf(lines[k]) > parentIndent) { j = k; continue; }
        break;
      }
      const mi = ln.match(LIST_RE);
      if (mi && indentOf(ln) > parentIndent) {
        raw.push([indentOf(ln), (mi[3] || "").replace(/\s+$/, "")]);
        j++;
      } else break;
    }
    for (const [ind, txt] of raw) kids.push([Math.floor((ind - parentIndent) / 2), txt]);
    return kids;
  }

  // A tag on an indented continuation line belongs to its nearest list item.
  function collectParentListBlock(i) {
    let parentIdx = null;
    let parentIndent = 0;
    for (let k = i - 1; k >= fm.bodyStart; k--) {
      const prev = lines[k];
      if (!prev.trim() || HEAD_RE.test(prev) || FENCE_RE.test(prev)) break;
      if (prev.match(LIST_RE)) {
        parentIdx = k;
        parentIndent = indentOf(prev);
        break;
      }
    }
    if (parentIdx === null || indentOf(lines[i]) <= parentIndent) return null;

    const block = [];
    for (let j = parentIdx; j <= i; j++) {
      const current = lines[j];
      if (!current.trim()) continue;
      const cm = current.match(LIST_RE);
      if (cm) {
        const currentIndent = indentOf(current);
        if (j !== parentIdx && currentIndent <= parentIndent) return null;
        const text = stripTag(cleanMatch(cm[3] || "")) || (cm[3] || "").trim();
        block.push([j === parentIdx ? 0 : Math.max(1, Math.floor((currentIndent - parentIndent) / 2)), text]);
      } else {
        const text = stripTag(cleanMatch(current));
        if (text) block.push([Math.max(1, Math.floor((indentOf(current) - parentIndent) / 2)), text]);
      }
    }
    return block.length ? { keyIdx: parentIdx, block } : null;
  }

  for (let i = fm.bodyStart; i < lines.length; i++) {
    const ln = lines[i];
    if (FENCE_RE.test(ln)) { inFence = !inFence; continue; }
    if (inFence) continue;
    if (isExcalidrawDataHeading(ln)) break;
    if (inPct)  { if (ln.includes("%%")) inPct = false; continue; }
    if (inHtml) { if (ln.includes("-->")) inHtml = false; continue; }
    const stripped = ln.trim();
    if (stripped.startsWith("%%") && !stripped.slice(2).includes("%%")) { inPct = true; continue; }
    if (stripped.startsWith("<!--") && !stripped.includes("-->")) { inHtml = true; continue; }

    const hm = ln.match(HEAD_RE);
    if (hm) {
      const htext = hm[2];
      const dh = htext.match(DATEH_RE);
      if (dh) { curDateHead = vdate(+dh[1], +dh[2], +dh[3]); curHead = null; curHeadRaw = null; }
      else { curHead = htext; curHeadRaw = htext.trim(); }
      if (TAG_RE.test(cleanMatch(htext))) {
        const sec = [[0, "**" + stripTag(htext) + "**"]];
        let j = i + 1;
        const level = hm[1].length;
        let count = 0;
        while (j < lines.length && count < 300) {
          const sl = lines[j];
          if (isExcalidrawDataHeading(sl)) break;
          const shm = sl.match(HEAD_RE);
          if (shm && shm[1].length <= level) break;
          if (FENCE_RE.test(sl)) break;
          if (sl.trim()) {
            const smi = sl.match(LIST_RE);
            if (smi) sec.push([1 + Math.floor(indentOf(sl) / 2), (smi[3] || "").replace(/\s+$/, "")]);
            else sec.push([1, sl.trim()]);
            count++;
          }
          j++;
        }
        addBlock(i, sec);
      }
      continue;
    }

    const clean = cleanMatch(ln);
    if (!TAG_RE.test(clean)) continue;

    const mi = ln.match(LIST_RE);
    if (mi) {
      const ind = indentOf(ln);
      const itemText = mi[3] || "";
      const cleaned = stripTag(itemText);
      const kids = collectChildren(i, ind);
      addBlock(i, cleaned ? [[0, cleaned], ...kids] : [[0, itemText.trim()], ...kids]);
      continue;
    }

    // 独立标签行:向下吸收 list items,或向上归属到紧邻的块
    if (ln.replace(ANY_TAG, "").trim() === "" || ln.replace(ANY_TAG, "").trim() === "%%") {
      let sec = [];
      let j = i + 1;
      while (j < lines.length) {
        const mj = lines[j].match(LIST_RE);
        if (mj) { sec.push([indentOf(lines[j]), (mj[3] || "").replace(/\s+$/, "")]); j++; }
        else break;
      }
      let keyIdx = i;
      if (!sec.length) {
        const up = [];
        let k = i - 1, bulletIdx = null;
        while (k >= fm.bodyStart) {
          const bk = lines[k];
          if (bk.trim() === "" || HEAD_RE.test(bk) || FENCE_RE.test(bk)) break;
          up.push(k);
          if (LIST_RE.test(bk)) { bulletIdx = k; break; }
          k--;
        }
        if (bulletIdx !== null) {
          const inds = [];
          for (let bl = bulletIdx; bl < i; bl++) {
            const mb = lines[bl].match(LIST_RE);
            if (mb) { inds.push(indentOf(lines[bl])); sec.push([indentOf(lines[bl]), (mb[3] || "").replace(/\s+$/, "")]); }
            else sec.push([(inds.length ? inds[inds.length - 1] + 2 : 0), lines[bl].trim()]);
          }
          keyIdx = bulletIdx;
        } else if (up.length) {
          sec = up.slice().reverse().map(k2 => [0, lines[k2].trim()]);
        } else {
          // 兜底:标签行紧挨在某个标题下方(中间只有空行)→ 捕获该标题下的整个小节
          let h = i - 1;
          while (h >= fm.bodyStart && lines[h].trim() === "") h--;
          const hmk = h >= fm.bodyStart ? lines[h].match(HEAD_RE) : null;
          if (hmk) {
            const level = hmk[1].length;
            const title = hmk[2].replace(ANY_TAG, "").replace(/\s+/g, " ").trim();
            const sec2 = title ? [[0, "**" + title + "**"]] : [];
            let j2 = i + 1, count2 = 0;
            while (j2 < lines.length && count2 < 300) {
              const sl = lines[j2];
              if (isExcalidrawDataHeading(sl)) break;
              const shm = sl.match(HEAD_RE);
              if (shm && shm[1].length <= level) break;
              if (FENCE_RE.test(sl)) break;
              if (sl.trim()) {
                const smi = sl.match(LIST_RE);
                if (smi) sec2.push([1 + Math.floor(indentOf(sl) / 2), (smi[3] || "").replace(/\s+$/, "")]);
                else sec2.push([1, sl.trim().replace(/^>\s*/, "")]);
                count2++;
              }
              j2++;
            }
            if (sec2.length) sec = sec2;
          }
        }
      }
      if (sec.length) {
        const mi0 = Math.min(...sec.map(x => x[0]));
        addBlock(keyIdx, sec.map(([ind, t]) => [Math.floor((ind - mi0) / 2), t]));
      }
      continue;
    }

    // 命中在列表项的缩进续行:摘要应回到所属列表项,而不是只截取续行尾部。
    const parentBlock = collectParentListBlock(i);
    if (parentBlock) {
      addBlock(parentBlock.keyIdx, parentBlock.block);
      continue;
    }

    // 普通段落带标签:吸收到空行为止
    const para = [stripTag(clean)];
    let j = i + 1;
    while (j < lines.length) {
      const nl = lines[j];
      if (isExcalidrawDataHeading(nl) || nl.trim() === "" || HEAD_RE.test(nl) || LIST_RE.test(nl) || FENCE_RE.test(nl)) break;
      para.push(nl.trim());
      j++;
    }
    addBlock(i, [[0, para.filter(p => p).join(" ")]]);
  }

  // frontmatter 标签 → 页面级条目(附正文开头预览)
  if (hasAnyTargetFmTag(fm.tags)) {
    const d = fm.date || fileDate || mtimeDate;
    if (d) {
      order++;
      // 收集正文文本至 PREV_CHARS:超过上限的长文在渲染时截断并附「查看全文」链接
      const prev = [];
      let prevTotal = 0, prevCount = 0;
      for (let k = fm.bodyStart; k < lines.length && prevCount < 200; k++) {
        const s = lines[k].trim();
        if (isExcalidrawDataHeading(s)) break;
        if (!s || s.startsWith("#") || s === "---") continue;
        const clean = sanitizeHumanLine(s.replace(ANY_TAG, ""));
        if (!clean) continue;
        prev.push(clean);
        prevTotal += clean.length;
        prevCount++;
        if (prevTotal > PREV_CHARS) break;   // 超过上限:已足够判断,停止收集
      }
      const preview = prev.join(" ");
      entries.push({ date: d, time: null, order, relpath, note, breadcrumb: null,
                     lines: preview ? [[0, preview]] : [],
                     pageLevel: true, notes: [note], anchor: null });
    }
  }
  return entries;
}

// ---------------- 扫描整个 vault ----------------
const currentPath = (dv.current() && dv.current().file) ? dv.current().file.path : null;
let all = [];
for (const f of app.vault.getMarkdownFiles()) {
  if (f.path === currentPath) continue;
  if (SKIP_DIRS.some(d => f.path.startsWith(d + "/"))) continue;
  if (f.path.split("/").some(seg => seg.startsWith("."))) continue;  // 排除所有隐藏文件/文件夹(.开头)
  if (excludedPaths.some(p => f.path === p || f.path.startsWith(p.endsWith("/") ? p : p + "/"))) continue;
  let text;
  try { text = await app.vault.cachedRead(f); } catch (e) { continue; }
  if (f.path.endsWith(".excalidraw.md")) text = readableExcalidrawMarkdown(text);
  if (text.slice(0, 1000).includes(MARKER)) continue;   // 跳过 tagfeed 生成的静态页
  all = all.concat(scanText(text, f.basename, f.path, dateFromName(f.basename), new Date(f.stat.mtime)));
}

// ---------------- 去重:同日期+同内容合并,来源并排 ----------------
const timeCmp = (a, b) => (a[0] - b[0]) || (a[1] - b[1]) || (a[2] - b[2]);
const mergedMap = new Map();
for (const e of all) {
  const fp = e.date.getTime() + "|" + e.lines.map(x => x[0] + ":" + x[1]).join("\n");
  const m = mergedMap.get(fp);
  if (m) {
    if (!m.notes.includes(e.note)) m.notes.push(e.note);
    if (e.time && (!m.time || timeCmp(e.time, m.time) < 0)) m.time = e.time;
    // 若先扫到的来源没有面包屑/锚点,而后面的有,则采用后者(与扫描顺序无关,结果确定)
    if (!m.breadcrumb && e.breadcrumb) {
      m.breadcrumb = e.breadcrumb;
      m.anchor = e.anchor;
      m.relpath = e.relpath;
      m.note = e.note;
    }
  } else mergedMap.set(fp, e);
}
const entries = [...mergedMap.values()];

// ---------------- 渲染:按日期倒序 ----------------
const byDate = new Map();
for (const e of entries) {
  const k = e.date.getTime();
  if (!byDate.has(k)) byDate.set(k, []);
  byDate.get(k).push(e);
}
const dates = [...byDate.keys()].sort((a, b) => b - a);
const noteSet = new Set();
for (const e of entries) for (const n of e.notes) noteSet.add(n);

const out = [];
out.push("> **" + entries.length + "** 个内容块 | **" + noteSet.size + "** 篇笔记 | 按日期倒序 | 随笔记变动自动刷新");

const pad2 = x => String(x).padStart(2, "0");
const fmtDate = d => d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate());
const invTime = t => t.map(x => pad2(99 - x)).join("");

for (const dk of dates) {
  const d = new Date(dk);
  out.push("## 📅 " + fmtDate(d) + " · " + WEEKDAYS[d.getDay() === 0 ? 6 : d.getDay() - 1]);
  out.push("");
  const group = byDate.get(dk);

  const sub = new Map();   // (笔记+面包屑) -> entries
  for (const e of group) {
    const key = e.relpath + "|" + (e.breadcrumb || "");
    if (!sub.has(key)) sub.set(key, []);
    sub.get(key).push(e);
  }
  const subKey = ([key, es]) => {
    const times = es.filter(e => e.time).map(e => e.time);
    const best = times.length ? times.reduce((a, b) => (timeCmp(a, b) > 0 ? a : b)) : null;
    return (best ? "0" : "1") + (best ? invTime(best) : "000000") + key;
  };

  for (const [key, es] of [...sub.entries()].sort((a, b) => (subKey(a) < subKey(b) ? -1 : subKey(a) > subKey(b) ? 1 : 0))) {
    const noteList = [];
    for (const e of es) for (const n of e.notes) if (!noteList.includes(n)) noteList.push(n);
    noteList.sort();
    const crumb = es.map(e => e.breadcrumb).find(b => b) || null;
    out.push("- 🔗 " + noteList.map(n => "[[" + n + "]]").join(" · ") + (crumb ? " · " + crumb : ""));
    const sorted = es.slice().sort((a, b) => {
      const ka = (a.time ? "0" : "1") + (a.time ? invTime(a.time) : "") + String(a.order).padStart(6, "0");
      const kb = (b.time ? "0" : "1") + (b.time ? invTime(b.time) : "") + String(b.order).padStart(6, "0");
      return ka < kb ? -1 : ka > kb ? 1 : 0;
    });
    for (const e of sorted) {
      let bl = e.lines, truncated = false;
      const totalChars = bl.reduce((s, x) => s + x[1].length, 0);
      if (totalChars > PREV_CHARS) {          // 只有超过上限才截断;上限内全文展示
        truncated = true;
        const shown = [];
        let chars = 0;
        for (const [lvl, txt] of bl) {
          if (chars + txt.length > PREV_CHARS) {
            if (PREV_CHARS - chars > 0) shown.push([lvl, txt.slice(0, PREV_CHARS - chars) + "…"]);
            break;
          }
          shown.push([lvl, txt]);
          chars += txt.length;
        }
        bl = shown;
      }
      for (const [lvl, txt] of bl) out.push("\t".repeat(lvl + 1) + "- " + txt);
      if (truncated) {
        const target = e.note + (e.anchor ? "#" + e.anchor : "");
        const label = e.pageLevel ? "页面级标签 · 查看全文" : "查看全文";
        out.push("\t- [[" + target + "|↗ " + label + "]]");
      }
    }
    out.push("");
  }
  out.push("");
}
dv.paragraph(out.join("\n"));

} catch (err) {
  dv.paragraph("> ❌ tagfeed-live 执行失败:" + err.message);
  console.error("tagfeed-live:", err);
}