ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI浏览器插件实战:用大模型整理B站收藏夹与关注列表

AI浏览器插件实战:用大模型整理B站收藏夹与关注列表 你有没有过这种体验B站关注列表已经涨到几百个UP主刷首页像在随机抽卡收藏夹还叫“默认收藏夹”里面塞了三年都找不回来的教程而粉丝数明明不低却完全不知道他们想让你更新什么内容。这其实是很多人忽略的一个事实**账号积累了数据不等于账号沉淀了资产。**关注、粉丝、收藏夹是B站用户最核心的三类“内容关系数据”但默认产品形态里它们几乎都处于不可管理状态。关注列表线性展示、收藏夹没有语义、粉丝列表没有标签。过去我们靠“手动整理文件夹Excel”的方式做但面对几百上千条记录时心智负担太重整理成本远大于收益。这才是我觉得“哔站理理”这类AI浏览器插件值得讨论的原因。它把B站个人数据当成一个可以被治理的“内容资产库”在浏览器里既读数据又用大模型做分类、打标签、分层然后把整理建议交还给用户确认。这篇文章不打算只介绍一个产品概念而是把它拆成开发者视角能落地的技术方案浏览器插件怎么和B站页面共处、AI在什么环节介入、如何处理关注/粉丝/收藏夹三类数据、最小闭环怎么跑通、有哪些坑必须避开。如果你恰好是B站重度用户或者对“浏览器插件 AI Agent”组合感兴趣这篇文章适合你收藏着慢慢读。1. 为什么B站关注、粉丝、收藏夹会“失控”1.1 三类数据的默认形态先看清B站三类个人数据的真实属性。关注列表本质是一张“单向订阅关系表”产品上只给你线性的时间线。UP主数量一旦超过某个阈值信息流就开始互相淹没。你关注了600个UP主每天产生的更新视频可能有几百条最后真正看完的只有十几条剩下全靠“稍后再看”自我安慰。收藏夹则更像一个“内容垃圾桶”。大多数人只会建“默认收藏夹”和“稍后再看”很少维护分类。技术类收藏夹、美食教程、数码评测、知识区课程混在一起。一年之后打开标题都认识但“当时为什么收藏”这个上下文已经消失了。粉丝列表问题更隐蔽。对个人创作者来说粉丝不是数字游戏而是内容运营的基础数据。普通创作者能看到的粉丝列表几乎不带标签更没有任何群体画像。哪些人是真正会互动的活跃粉丝哪些人来自某一条爆款视频的瞬时关注哪些人其实早已不看B站完全无从得知。这三类数据的共同点是**它们都存在而且可视但没有结构和语义。**技术上可以称之为“无Schema数据”。这也是为什么数据量越大用户越容易焦虑。1.2 “数据”不等于“资产”数据要成为资产至少要满足两个条件可以被检索可以被重新组织。B站收藏夹提供了搜索框但搜索只能解决“我记得标题关键词”的情况。更多时候你面对的问题是“我记得当时看过一个讲OpenCV调试的教程但不知道哪个UP主也忘了标题”——这种情况下搜索是无力的。关注列表更麻烦。你想找出“最近三个月更新频率明显下降但内容质量仍然很高的UP主”但B站没有提供按更新频率排序的视图。唯一可行的做法是把关注列表和每个UP主的主页信息拉出来做二次分析。这个系统能支撑前浏览器插件就是最合适的载体。当一个平台不能提供数据导出和自定义视图能力而用户又确实持有这些数据的查看权时“用户在页面上能看到的区域里额外增加一层AI分析”就成了合理路线。这就是哔站理理这类浏览器插件存在的底层逻辑。1.3 为什么现在被AI解锁几年前就有人做过B站收藏夹整理工具但普遍不好用。原因是传统规则式分类太僵硬。你想把“AI绘画”和“数字人”分开传统代码要么写死规则要么靠关键词命中根本理解不了“Stable Diffusion工程化”应该归到哪个分组。大模型的语义理解能力改变了这件事。你可以直接在Prompt里写请把这些视频标题按主题聚成5到8个语义分组你可以让模型根据UP主过往内容方向给关注列表做价值分层你也可以让模型解释“这条收藏为什么值得去看”。AI真正替代的不是“理解内容”这件事本身而是把“需要逐条打开页面去理解内容”的劳动力剥削感消解掉了。AI浏览器插件则进一步把“读取页面数据—构建Prompt—调用大模型—展示整理结果”压缩成一次点击。这是技术上完全可行的闭环而且开发和部署成本低到个人开发者就能完成。下面开始拆结构和实现。2. 哔站理理的核心架构与技术判断先给一个关键判断**想让AI整理自动化不能只考虑大模型能不能分析而要重新设计三层结构——数据怎么读、结果怎么算、动作怎么落。**这三层如果边界不清插件很容易变成“能生成一堆建议却没法用”的玩具。2.1 第一层页面数据读取浏览器插件读取B站数据通常有两种路径。数据路径原理优点风险DOM解析页面在B站页面上执行JavaScript读取当前可见的卡片、标题、UP主名等不需要额外接口签名权限可收敛到当前网站平台改版后选择器失效抓取页面XHR接口通过开发者工具观察页面自身请求模拟调用数据接口拿JSON数据结构稳定、字段全需要登录态、可能带签名参数高频调用有风控风险更稳妥的原则是**优先读取用户当前页面可见的数据。**用户已经在B站页面上了也已经登录了插件不需要保存账号密码更不需要做“扫码登录”。它只是替用户从页面上把数据“抄”下来再交给AI。“页面可见优先”还有一个隐藏好处它天然控制请求频率。你只能读到用户滚到的内容不可能一次把全站数据拖走这让插件的风控风险大大降低。2.2 第二层智能分析由LLM承担大模型在整理任务中扮演的是“结构化引擎”。B站收藏夹的原始数据是一串视频标题比如“用Python自动整理桌面文件”“iPhone 15详细评测”“机器学习入门课程推荐”。传统代码很难把这些归到“效率工具”“数码评测”“课程推荐”三个语义组大模型可以做到而且还能给你解释分组的理由。这一层的核心不是换个更强的模型而是把输出格式设计成可解析的JSON。如果你的Prompt只让大模型“自由发挥”它确实可能给你一篇散文。正确做法是让模型严格按照你定义的Schema输出只有输出可解析后续动作才有可能落地。2.3 第三层动作执行必须留人工确认整理动作包括改收藏夹名、把视频从一个收藏夹移动另一个、取消关注某个UP主、给某个UP主打上“核心内容源”标签。这一层最容易出事。过去很多自动化脚本失败不是技术问题而是产品逻辑问题用户内心并不想被AI“一键退坑”他只想要“建议被充分理解后的一个确认动作”。所以哔站理理的执行层应当做成“建议引擎 人工确认工作台”。AI给出建议用户看一眼理由再决定是否执行。“一键取消所有低活跃UP主”这种设计是灾难不是效率。原因很简单用户在关注某个UP主时可能带着一段独特记忆AI不知道也不该替用户做最终决定。读数据要克制算数据要结构化写数据要确认。这三层边界建立起来插件工程才真正成熟。3. 环境准备与项目初始化3.1 开发准备开发Chrome扩展不一定需要复杂的构建链。为了提高上手速度可以完全不用打包工具直接用原生JavaScript写然后通过“加载已解压的扩展程序”方式调试。推荐环境如下Chrome或Edge浏览器使用Chromium内核即可。Visual Studio Code作为编辑器推荐安装“HTML CSS Support”和“JavaScript (ES6) code snippets”插件。不需要额外安装Node.js但如果你希望写一些本地辅助脚本做离线分析Node.js或Python都可以。一个可以访问的OpenAI兼容接口服务。建议选择国内合法提供的AI服务只要支持/v1/chat/completions格式就可以复用同一套代码。3.2 建立工程目录项目目录建议如下名字先叫bili-organizer。bili-organizer/ ├─ manifest.json ├─ background.js ├─ content.js ├─ popup.html ├─ popup.js └─ assets/这里没有用webpack、vite等工具并不是因为它们不好而是对于一个以“读取页面DOM 发请求 展示结果”为主的MV3插件原生的manifest方案更直观调试时也少一处“构建产物和源码不一致”的困惑。3.3 大模型服务配置由于文章示例需要接入大模型这里有一个工程取舍**浏览器插件属于客户端应用直接在前端保存API Key并不是安全方案。**如果你做个人小工具可以先把Key保存在chrome.storage.local里图个方便如果做生产级产品强烈建议把大模型调用后置到自己的后端服务。为了可复制、可演示本文示例代码采用“轻量本地配置”方案用户在自己的popup页面中填写API Base、API Key、模型名插件把配置存到storage.local再通过后台Service Worker发起请求。这种方式适合个人开发和学习但不适合公开上线给大量用户使用原因后面安全章节会展开说。4. 第一个可跑通的最小闭环收藏夹智能分类不要一开始就做“关注列表全面管理”“粉丝资产分析”那种复杂功能。优先做收藏夹最小闭环**打开收藏夹页面读取十几条视频标题让AI给出分组建议然后展示给用户确认。**这个小闭环能验证架构是否跑通也能让你体会AI整理和手工整理的真实差异。4.1 页面数据收集Content Script主要负责从收藏夹页面读取数据。B站的收藏夹页面是动态渲染的浏览时你需要手动向下滚动让“懒加载”把更多视频条目加载出来再点击“整理本页收藏夹”。这一步的采集逻辑通常包含判断当前页面是否包含收藏夹内容。使用document.querySelectorAll找到视频卡片节点。从节点中提取视频标题和UP主名称。转成纯文本格式发送给后台。这里必须坦白说**B站页面改版比较频繁代码里的选择器需要自己根据真实页面DOM调整。**下面会提供一套兼容性相对宽的写法和一段排查方式但没办法保证永远不过期。4.2 构造Prompt并调用大模型收集到标题 UP主列表之后需要构造Prompt。Prompt写得好不好直接影响输出质量。经验是先给System设定角色再给User明确的数据和输出格式约束。推荐Prompt模板如下你是B站个人内容整理助手。用户会提供一组收藏夹里的视频标题和UP主名称。 请先理解这些内容的主题然后把它们聚合成5到8个语义分组。 每个分组必须包含 - 分组名称 - 一句话说明这个分组为什么成立 - 属于该分组的原始条目的序号列表 只输出JSON不要输出Markdown不要输出额外解释。 JSON格式必须是 { summary: 对整个收藏夹的一句话总体判断, groups: [ {name: 分组名, reason: 分组理由, itemIndexes: [1,2,3]} ] }在代码中调用大模型时建议使用response_format: { type: json_object }来约束输出。如果你的模型服务不支持这个参数可以在System中强调“只输出JSON”然后在解析失败时自动重试一次。4.3 解析建议结果大模型返回的文本应该是一段JSON字符串。解析后渲染到popup页面。用户在界面上看到“这个收藏夹里有Python自动化、设计素材、AI工具三个主题”如果确认插件可以把建议分组保存为一份快照或者弹窗提示“建议你去收藏夹管理页新建这几个收藏夹”。这里不要图省事直接帮用户移动视频。B站收藏夹的视频移动属于修改型操作一旦批量移动错了整理成本比不整理还高。最小闭环的最优解是“给建议让用户手动完成分组”等用户信任结果后再逐步加入自动改名、批量移动等操作。5. 核心功能二关注列表的分层设计5.1 从关注页取数关注列表的取数逻辑和收藏夹类似但页面结构不同。B站个人的“关注”页面一般可以滚动加载出所有已关注UP主。采集时需要读取的字段包括UP主昵称UP主签名或简介是否已设置特别关注是否悄悄关注如果是B站创作者最好能看到TA近期是否更新这一信息能辅助判断活跃度和收藏夹不同关注列表的采集通常需要滚动多次才能完整加载。Content Script可以配合scroll事件或者定期执行window.scrollTo(0, document.body.scrollHeight)触发懒加载。更稳妥的办法是写一句提示“请先手动滚动到页面底部待所有UP主加载完成后再开始整理”。5.2 分层建议从“全部关注”到“内容矩阵”关注列表分析的目标不是让用户清除关注而是帮用户理解自己的关注结构。大模型可以基于UP主昵称、个人简介和历史内容方向把关注列表聚合成内容主题并标注活跃度。判断时我推荐用三种层级核心内容源与用户当前兴趣或工作高度相关、更新稳定、值得每天看。这类UP主建议保持正常关注排列以免被信息流淹没。观察型内容源内容有一定价值但主题边缘、更新频率低或者近期内容方向正在变化。这类可以放入“悄悄关注”或固定每周集中浏览一次。可优化内容源长期不更新、内容主题已经偏离兴趣、同质化严重。这些不一定直接取关可以先移入“悄悄关注”观察两周再做决定。这三个层级不是“AI替你退坑”而是给用户一个信息流管理的操作框架。你可以在关注页面看到AI生成的分层结果后自己做最终确认。5.3 减少误操作不要一键取关关于关注列表最重要的工程建议是**不要写“一键取关”按钮。**至少不要默认开启。即便AI判断某个UP主“三个月没更新”也不排除用户和这位UP主有某种特定关系。批量取关的不可逆性很高虽然B站允许重新关注但看着一串“已关注UP主减少”的提示用户心理体验并不好。更稳妥的产品逻辑是先把AI标记为“可优化”的UP主加入悄悄关注给一个冷静期之后再做二次确认。执行时必须关注B站的频率限制连续高频调用取关接口非常容易被判定为异常操作。间隔至少控制在数秒以上并且每批操作后随机等待一段时间。6. 完整参考代码与拆解下面给出一个最小示例不包含全部页面选择器适配重点展示MV3插件的消息流用户点击popup按钮向content script发送采集请求content script返回数据popup再把数据组装成Prompt发给backgroundbackground调用大模型最终渲染结果。6.1 manifest.json{ manifest_version: 3, name: 哔站理理, version: 0.1.0, description: 整理B站关注、粉丝、收藏夹的AI浏览器插件示例, permissions: [storage, activeTab], host_permissions: [ https://*.bilibili.com/*, https://YOUR_LLM_DOMAIN.example.com/* ], action: { default_popup: popup.html, default_title: 哔站理理 }, background: { service_worker: background.js }, content_scripts: [ { matches: [https://*.bilibili.com/*], js: [content.js], run_at: document_idle } ] }说明host_permissions中的B站域名用于content script注入。YOUR_LLM_DOMAIN.example.com需要替换成你实际使用的大模型API域名。如果域名不在host_permissions里后台发起fetch时可能遇到CORS或权限不足的报错。MV3使用Service Worker作为后台所有监听器必须是异步安全的chrome.runtime.onMessage.addListener中如果用了sendResponse异步返回必须return true。6.2 background.js这个文件负责调用大模型。它接收popup发来的messages和config然后向OpenAI兼容API发起请求。// background.js async function askLLM(messages, config) { const apiBase (config.apiBase || ).replace(/\/$/, ); const apiKey config.apiKey || ; const model config.model || ; if (!apiBase || !apiKey || !model) { throw new Error(AI服务配置不完整请先填写API Base、API Key和模型名); } const resp await fetch(${apiBase}/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${apiKey} }, body: JSON.stringify({ model: model, messages: messages, temperature: 0.2, max_tokens: 2048, response_format: { type: json_object } }) }); if (!resp.ok) { const errText await resp.text().catch(() ); throw new Error(LLM请求失败: ${resp.status} ${errText}); } const data await resp.json(); const content data.choices?.[0]?.message?.content || ; if (!content) { throw new Error(LLM返回内容为空); } try { return JSON.parse(content); } catch (e) { // 如果模型没有严格遵守JSON输出返回一个可追踪对象 return { raw: content, error: JSON解析失败模型返回的内容不是有效JSON }; } } chrome.runtime.onMessage.addListener((msg, sender, sendResponse) { if (msg?.type ORGANIZE_FAVORITES) { askLLM(msg.messages, msg.config || {}) .then((result) sendResponse({ ok: true, result })) .catch((err) sendResponse({ ok: false, error: String(err.message || err) })); return true; // 表示会异步调用sendResponse } });这段代码中最容易踩坑的是response_format。如果你的模型服务基于某些开源模型不支持json_object类型调用会直接报400错误。此时可以把response_format去掉并在Prompt里继续强调JSON格式增加重试和字符串截取兜底。6.3 content.jsContent Script只做一件事采集当前页面的可见收藏夹条目。注意选择器需要根据真实DOM调整。// content.js function collectFavorites() { // 这里的selector只作演示B站改版后需要用开发者工具重新确认真实节点 const cards document.querySelectorAll( .fav-video-list .bili-video-card, [class*fav] [class*video-card] ); const results []; for (const card of cards) { const titleNode card.querySelector(a[title], .title, [title]); const ownerNode card.querySelector(.up-name, [class*up-name], [class*author]); let title ; if (titleNode) { title titleNode.getAttribute(title) || titleNode.textContent || ; } const owner ownerNode ? ownerNode.textContent.trim() : ; if (title) { results.push({ title: title.trim(), owner }); } // 最多采集30条避免一次传给模型太多导致超时或结果不稳定 if (results.length 30) break; } return results; } chrome.runtime.onMessage.addListener((msg, sender, sendResponse) { if (msg?.type COLLECT_FAVORITES) { const items collectFavorites(); sendResponse({ ok: true, items, pageTitle: document.title }); } });把采集数量限制在30条内是刻意为之。一是控制单次请求长度避免大模型处理过长文本后返回质量下降二是减少对页面性能影响。如果你要处理几百条收藏更合理的做法是分批处理。6.4 popup.jsPopup是用户最直接的交互界面。它负责判断当前页面、发消息给content script、组装Prompt、调后台、渲染结果。// popup.js async function getActiveBilibiliTab() { const [tab] await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab?.url?.includes(bilibili.com)) { throw new Error(请先打开B站收藏夹页面); } return tab; } async function loadAIConfig() { const cfg await chrome.storage.local.get([apiBase, apiKey, model]); return cfg; } function buildOrganizePrompt(rawText) { const system 你是B站个人内容整理助手。 你会收到一个收藏夹内的视频标题和UP主名称列表。 请把内容聚合成5到8个语义分组。 只输出JSONJSON格式为 { summary: 总体判断, groups: [ {name: 分组名称, reason: 分组理由, itemIndexes: [1,2,3]} ] } .trim(); const user 请整理以下收藏夹条目\n${rawText}; return [ { role: system, content: system }, { role: user, content: user } ]; } function renderResult(result) { const output document.getElementById(output); if (!output) return; if (result?.error) { output.textContent 模型返回内容无法解析\n (result.raw || ); return; } output.textContent JSON.stringify(result, null, 2); } document.getElementById(organizeBtn)?.addEventListener(click, async () { const status document.getElementById(status); const btn document.getElementById(organizeBtn); try { btn.disabled true; status.textContent 正在读取当前页面...; const tab await getActiveBilibiliTab(); const collectResp await chrome.tabs.sendMessage(tab.id, { type: COLLECT_FAVORITES }); if (!collectResp?.ok || !collectResp.items?.length) { throw new Error(没有读取到收藏夹内容请先滚动加载后再试); } const rawText collectResp.items .map((item, idx) ${idx 1}. ${item.title} UP主${item.owner}) .join(\n); status.textContent
返回列表