吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 483|回复: 7
上一主题 下一主题
收起左侧

[其他原创] 浏览器书签管理

[复制链接]
跳转到指定楼层
楼主
Su007 发表于 2026-9-22 16:43 回帖奖励

一个浏览器扩展:跨文件夹任意勾选书签,聚合成一个文件导出
支持 HTML(浏览器可直接导入)、JSON、Markdown、CSV 四种格式。纯本地运行,不联网、不收集任何数据。

shot-01-main.png

它能做什么

  • 跨层级任意勾选:书签、文件夹、跨文件夹混着选,三态复选框(全选 / 部分选中 / 未选)一目了然
  • Shift 区间连选:点第一项,按住 Shift 点另一项,中间整段一起选中,跟文件管理器一样;对已选中区间 Shift 点击则整段取消
  • 四种导出格式
格式 用途
HTML(Netscape 格式) 浏览器「导入书签」直接还原,层级完整保留
JSON 备份 / 程序处理
Markdown 发帖、笔记、分享
CSV(带 BOM) Excel 直接打开,含 folder_path
  • 复制按钮:不下载文件,直接把「标题 + 链接」的 Markdown 列表复制进剪贴板,粘贴到聊天 / 文档即用
  • 搜索过滤:按标题 / URL 实时搜索,在结果里直接勾选
  • 网址去重:跨文件夹勾选时同一网址只保留一条(忽略末尾斜杠与大小写)
  • 自动清理:空文件夹不导出、无标题书签用网址兜底、选项自动记忆
  • 直接整理书签:勾选书签后(可跨文件夹、跨层级各自勾选),鼠标移到目标文件夹行上的 ⊕ 移到此 即可整批移入;文件夹行 +新建 / 顶部 +新建 可就地建子文件夹或顶层文件夹。把"剪切到临时文件夹再导出"也省了
  • 删除:勾选后点工具栏红色「删除」(带确认条,不可恢复)。删文件夹会连同子项一起删;书签栏 / 其他书签等根容器受保护不会被删

浏览器兼容性

  • Chrome / Edge / QQ 浏览器 / Firefox 等标准内核:读、导出、移动、新建、删除全部正常
  • 360 极速浏览器 X:读 / 导出正常,但新建 / 移动 / 删除不可用——这是 360 自身的书签写入接口缺陷(chrome.bookmarkscreate / move / remove / update 全部返回 Bookmark id is invalid,360 社区多人反馈、暂无官方修复,非本工具 bug)。本工具通过运行时实测探测(建一个临时文件夹再删,若 create 报错即判定写入接口不可用,不靠浏览器 UA 嗅探)识别此类缺陷,命中后页面显示提示条、并直接置灰「新建 / 删除」按钮、隐藏文件夹行的「移到此 / 新建」,写入报错也会给出明确原因。整理书签请改用 Chrome / Edge。

    两种导出结构,按需选择

shot-04-compare.png

  • 保留层级(默认):勾了文件夹就带文件夹名,导入后分组完整。适合备份和迁移。
  • 扁平导出:只输出链接,不带任何文件夹。适合临时凑一批链接分享、导入到别处。

另有「去掉根容器外壳」选项(默认开):导入后不会多出「书签栏 / 其他书签」这一层无意义的嵌套。每个选项都有悬浮说明,鼠标放上去就能看到解释。

shot-03-tip.png

Shift 区间连选

几百条书签里挑连续的一段?点第一个,按住 Shift 点最后一个,中间整段选中。折叠的分支不参与(只按当前可见顺序),搜索结果列表里同样可用。
shot-02-shift.png

隐私说明(重点)

  • 仅申请 bookmarks 一个权限,用于读取你勾选的内容
  • 不申请网络、下载、存储等任何其他权限——导出动作就是浏览器原生的文件保存
  • 所有逻辑跑在你本地,代码全部开源可审查,总共几百行,无构建、无混淆

    相关代码

1) 三态勾选的状态模型(取消某个子项时,把"整选的父文件夹"展开成"兄弟节点逐个勾选",这样导出时不会有空文件夹、也不会父子重复):

// 取消某节点时:把它的"已被整选的祖先"拆成兄弟节点的显式勾选,使该节点不再被祖先覆盖
function materializeAncestors(id) {
  let cur = id;
  const exclude = new Set([id]);
  const guard = new Set();
  while (cur != null && !guard.has(cur)) {
    guard.add(cur);
    const p = parentOf.get(cur);
    if (p == null) break;
    if (selected.has(p)) {
      selected.delete(p);
      const pn = index.get(p);
      if (pn && pn.children) {
        for (const c of pn.children) if (!exclude.has(c.id)) selected.add(c.id);
      }
    }
    exclude.add(p);
    cur = p;
  }
}

function setChecked(id, checked) {
  const n = index.get(id);
  if (!n) return;
  clearCache();
  if (checked) {
    if (ancestorSelected(id)) return; // 祖先已整选时跳过,避免父子重复入列
    selected.add(id);
    walk(n, (d) => { if (d.id !== id) selected.delete(d.id); });
  } else {
    materializeAncestors(id);
    selected.delete(id);
  }
  clearCache();
}

2) 生成可被任意浏览器导入的 Netscape HTML(这套格式 Chrome / Edge / Firefox / Safari 都能直接「导入书签」还原层级):

function toNetscapeHtml(nodes) {
  const out = ['<!DOCTYPE NETSCAPE-Bookmark-file-1>',
    '<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">',
    '<TITLE>Bookmarks</TITLE>', '<H1>Bookmarks</H1>', '<DL><p>'];
  const walk = (list, depth) => {
    const pad = '    '.repeat(depth + 1);
    for (const n of list) {
      if (isFolder(n)) {                       // 文件夹 → <H3> 包一层 <DL>
        out.push(`${pad}<DT><H3 ADD_DATE="${toSec(n.dateAdded)}">${escapeHtml(n.title)}</H3>`);
        out.push(`${pad}<DL><p>`);
        if (n.children) walk(n.children, depth + 1);
        out.push(`${pad}</DL><p>`);
      } else {                                  // 书签 → <A HREF>
        out.push(`${pad}<DT><A HREF="${escapeHtml(n.url)}" ADD_DATE="${toSec(n.dateAdded)}">${escapeHtml(n.title)}</A>`);
      }
    }
  };
  walk(nodes, 0);
  out.push('</DL><p>');
  return out.join('\n') + '\n';
}

3) 移动书签 + 拦截"移进自己子文件夹"成环(新功能:跨文件夹勾选书签后整批移入目标文件夹;hasDescendant 防止把文件夹丢进自己的子树里导致书签丢失;移动后用 parentOf 核对是否真的落位,避免"API 报成功却没动"的假成功):

// 判断 maybeChild 是否为 ancestor 的子孙(拦截成环移动)
function hasDescendant(ancestorId, maybeChildId) {
  const a = index.get(ancestorId);
  if (!a || !a.children) return false;
  let found = false;
  walk(a, (n) => { if (n.id === maybeChildId) found = true; });
  return found;
}

// 把当前勾选的全部移入 targetId 下;移进自身或自身子树则跳过
async function moveSelectedTo(targetId) {
  if (targetId == null) return { moved: 0, skipped: 0 };
  const ids = Array.from(selected);
  let moved = 0, skipped = 0;
  for (const id of ids) {
    if (id === targetId) { skipped++; continue; }
    if (hasDescendant(id, targetId)) { skipped++; continue; }
    await call(api.move, id, { parentId: targetId });
    moved++;
  }
  selected.clear();
  clearCache();
  return { moved, skipped };
}

4) 删除书签 / 文件夹(勾选后工具栏「删除」:书签用 remove,文件夹用 removeTree 连子项一起删;顶层根容器受保护不删,且沿用上面的 call 会在出错时抛真实错误而不是假成功):

async function removeSelected() {
  const ids = Array.from(selected);
  if (!ids.length) return { removed: 0, skipped: 0 };
  let removed = 0, skipped = 0, lastErr = null;
  for (const id of ids) {
    const n = index.get(id);
    if (!n) { skipped++; continue; }
    if (parentOf.get(id) == null) { skipped++; continue; } // 根容器(书签栏/其他书签)不可删
    try {
      if (isFolder(n)) await call(api.removeTree, id);
      else await call(api.remove, id);
      removed++;
    } catch (e) { skipped++; lastErr = e; }
  }
  selected.clear();
  clearCache();
  if (removed === 0 && lastErr) throw lastErr; // 一条都没删成 → 抛出真实错误
  return { removed, skipped };
}

完整代码

蓝奏 https://wwbfw.lanzoue.com/b00mqnko2j 密码:52pj
百度网盘 https://pan.baidu.com/s/1ROGpsfvngLSbuJ6RVb_K0w?pwd=52pj 提取码: 52pj

免费评分

参与人数 2吾爱币 +2 收起 理由
wa1111 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
LFNT2010 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
sholincong 发表于 2026-9-22 16:58
我来试试好用不
3#
afti 发表于 2026-9-22 18:30
4#
fuvenusck 发表于 2026-9-22 19:28
5#
wentao520 发表于 2026-9-22 19:58
这个工具好,很实用
6#
cxwapj66 发表于 2026-9-22 21:22
这个好,很不错
7#
WWsrg7790 发表于 2026-9-22 21:25
我来试试,标签确实是个头疼的东西。得整理一下
8#
wa1111 发表于 2026-9-22 22:57
这个好,很不错,一直想要这样的功能呢,感谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-9-23 02:59

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表