在前面几篇里,我们已经搭过 YouTube、Reddit 等「选题情报」自动化流程(系列文章如下)。
【链接】
对应《一图说透「内容创作全流程」》里的「挖选题」:每多一条稳定渠道,就多一个可持续观察的信息入口。
本篇是系列第四弹,对象换成 B 站(bilibili):按关键词抓取近一周视频,拉取热门评论,再写入飞书多维表格。

其实这条工作流的方式和之前几期工作流逻辑都是类似,用n8n流程,对接tikhub的接口,获取到对应的数据写到多维表格里,如果你之前手把手搭建过,这期对你来说就很简单了。没有的话也没关系,这期我们来讲讲这个工作流实现的逻辑。
在开始之前,我们先说说为什么把B站也接进我们的选题池。
①中文语境强:因为很多工具教程或内容、比如飞书 / n8n / AI 工作流类内容,在 B 站更新密度更高,而且内容的方向和匹配中文用户。
②评论即需求:热门评论里常有「怎么用」「踩坑」「求配置」——和 Reddit 一样,都是真实声音,并且更贴近中文用户。
③和前几弹互补:YouTube、Reddit 偏全球或英文语境;B 站补中文场景。数据都进多维表格后,仪表盘上可以多一块「B 站」视图;若你读过「仪表盘 + 龙虾」那篇,后续接龙虾做周报也顺。
一、这个工作流的整体架构
先说说工作流的整体的框架。
我们可以把它理解成一条按周运行的流水线:

1.定时触发:例如每周固定时间跑一轮。
2.Init 配置:统一把 TikHub Key、飞书应用信息、多维表格 app_token / table_id、监控关键词列表、以及「最多处理多少条视频」上限的数据,都放在这个节点来进行配置。
3.时间计算:因为我们想获取最近一周发布的视频,所以需要做一个时间的计算。我们把本次流程运行的时间作为结束时间,然后往前推7天作为开始时间,交给搜索接口做「近一周」过滤。
4.飞书鉴权:拿 tenant_access_token,给后面写表用。
5.核心 Code 节点(TikHub Search + Comments):在一个节点里串完,用代码的方式完成多关键词搜索 → 按 视频BV号去重 → 逐条调评论接口 → 拼成飞书 batch_create 需要的 records。
6.IF:有记录才写表,无记录就不需要写
7.HTTP 节点写多维表格:一次性将获取到的数据写入到多维表格中。
二、多维表格如何配置
我们可以在之前的多维表格中,新建一张数据表,和之前的reddit、youtube表一样。 这次新建的数据表就只放我们从B站获取的视频数据。

字段可以放以下几个:
| 字段 | 字段类型 |
|---|---|
| 视频标题 | 文本 |
| 作者 | 文本 |
| bvid | 文本 |
| 视频url | 文本 |
| 视频时长 | 文本 |
| 视频描述 | 文本 |
| 视频标签 | 文本 |
| 视频发布时间 | 文本 |
| 视频播放次数 | 数字 |
| 视频收藏次数 | 数字 |
| 搜索关键词 | 文本(本条视频是由哪个关键词搜到的) |
| 视频评论 | 文本:多条评论合并为 1.xxx、2.xxx;若没有评论则固定为 暂无评论 |
三、流程节点配置说明
这一章节详细说明每个节点的配置和作用

1、Schedule Trigger
定时触发节点,每周触发一次流程,如果你希望按需调整触发的频率或时间,直接在这个节点修改就好
2、Init (Config + 7d range)(Code)
代码节点
节点作用:
①将所有的配置信息,如TikHub apiKey、feishu.appId / appSecret、多维表格 bitableAppToken、bitableTableId、keywords 数组、maxVideos等需要配置的信息都放在这个节点里,方便随时修改。
②时间计算,计算 range.pubtime_end_s(当前秒)和 range.pubtime_begin_s(减 7 天)。
详细配置:
/**
* Bilibili 关键词(近 7 天)+ 热门评论 → 飞书多维表格
*
* 请务必填写:config.tikhub.apiKey、config.feishu.appId / appSecret
* 已在对话中暴露的密钥请尽快在 TikHub 轮换。
*
* 多维表格字段名需与本流程「fields」一致,或在下方 mergeFields 中改名:
* search_keyword, author, bvid, arcurl, duration, title, description, tag, pubdate, play, favorites, hot_comments
*/
const config = {
timezone: 'Asia/Shanghai',
tikhub: {
baseUrl: 'https://api.tikhub.io',
apiKey: 'j6ohNv49g3eUw3KhEZzMkEyILSeOd8RAvxbltW6Rdf/jjEy8AH2IKjnELw==',
},
feishu: {
appId: 'cli_a6e243e0f9d9100c',
appSecret: '3GwtYgK5jOGmVA6yGyeR7gFoeBVCu3bg',
bitableAppToken: 'RTnbbVoapaEDOEsaseCcVWxDnyg',
bitableTableId: 'tbl49sp2cdLxpiN5',
},
// 与你「内容创作 / 自动化 / 飞书 / n8n 选题」定位相近的检索词,可按需增删
keywords: [
'n8n工作流',
'飞书 多维表格',
'AI工作流',
'内容运营自动化',
'内容运营 选题',
'AI agent',
],
// 综合搜索:order=click,每词 1 页 20 条;去重后最多拉取评论的视频数(防超时)
maxVideos: 40,
};
const nowSec = Math.floor(Date.now() / 1000);
const beginSec = nowSec - 7 * 24 * 60 * 60;
return [{ json: { config, range: { pubtime_begin_s: beginSec, pubtime_end_s: nowSec } } }];
3、Feishu – tenant_access_token(HTTP Request)
获取飞书的tenant_access_token,给后面写表用。
详细配置:
①Method:POST
②URL:https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/
③Headers:name:Content-Type,value:application/json; charset=utf-8
④Body(JSON):app_id、app_secret 。用表达式从 \$json.config.feishu 读取。
{{ JSON.stringify({ app_id: $json.config.feishu.appId, app_secret: $json.config.feishu.appSecret }) }}

4、Pack (config + token)(Code)
code节点,这个节点的主要目的是把 Init 里的 config + range 和 上一步拿到的 token 合并成一条下游数据,供下一个核心的Code使用。
const token = $json.tenant_access_token;
const init = $('Init (Config + 7d range)').first().json;
if (!init?.config?.tikhub?.apiKey) {
throw new Error('请在 Init 节点填写 config.tikhub.apiKey(TikHub Bearer Token)');
}
if (!token) {
throw new Error('飞书 tenant_access_token 为空,请检查 appId/appSecret');
}
return [{ json: { ...init, tenant_access_token: token } }];

5.TikHub Search + Comments(code)
这个节点是流程中最核心的节点,目的主要是调用tikhub里B站的搜索接口和获取评论接口,来获取到我们所需要的数据。
之前的流程我们会用http节点以及循环的逻辑来处理,节点看起来就比较多。这次我们可以换一种思路,直接用代码节点一次性搞定(不懂代码没关系,用AI啊哈哈)
我先讲讲这个节点的逻辑,最后把代码贴出来,直接复制就行。我们不用懂详细的代码,只需要大概清楚它的逻辑和在做什么事情就好,下面的介绍我也按照这个思路来。
(1)节点逻辑
主要处理三个事情
①搜:对每个关键词,调 TikHub「B 站综合搜索」接口,限定近 7 天、按播放量排序、只要第一页(例如 20 条),减轻费用和超时风险。
②收:把所有关键词的结果汇总,按 bvid 去重(同一个视频被多个词搜到只保留第一次),并限制总视频数不超过 maxVideos。
③评 + 写:对每个保留下的视频再调「视频评论」接口(热门排序、第一页),把评论正文抽出来,编成编号列表,和标题、链接等一起塞进 records[].fields,交给飞书写入表格。
(2)节点里的几个处理
有几个函数简单说说
①getRepliesFromCommentResp:TikHub 返回体是「套娃」结构:评论列表一般在 data.data.replies 这一层。这个函数专门从复杂 JSON 里捞出数组,拿不到就返回空数组,后面逻辑不用写一堆 if。
②commentMessage:单条评论里,正文可能在 content.message,也可能个别场景是字符串。这里做兼容,统一成一段字符串,避免某条评论结构略变就整段崩掉。
③buildNumberedComments:把多条评论合成一段多行文本,比如1.第一条评论……2.第二条评论……
(3)代码逻辑里的第一段:关键词循环 + 搜索 + 去重
①从上游读出 config、range、tenant_access_token。
②用 Map(键为 bvid)做去重:同一个视频只保留第一次出现时绑定的「搜索关键词」,方便我们反查「是哪个词带来的」。
③每个关键词拼好 fetch_general_search 的 query:
- order = click(最多播放)
- page = 1、page_size = 20、duration = 0(不限时长)
- pubtime_begin_s / pubtime_end_s 用 Init 算好的一周窗口
④用 this.helpers.httpRequest 发 GET(带 Authorization: Bearer)。某个词请求失败就 continue,不拖死整次执行。
⑤从返回里取 data.data.result,只保留 type === ‘video’ 且有 bvid 的项,写入 Map;达到 maxVideos 就提前结束外层的词循环。
(4)代码逻辑里的第二段:逐视频拉评论 + 组装 fields
①把 Map 转成数组,逐个视频请求 fetch_video_comments:mode=3 热门,next_offset=1 先取第一页(可按需以后扩展翻页)。
②评论列表走 buildNumberedComments;若结果为空或只有空白,把「视频评论」字段设为「暂无评论」——这样表里不会出现空单元格含义不清的情况。
③fields 里的键名用中文,与多维表格列名一字不差,包括 视频url、视频评论 等,避免飞书报「字段不存在」。
④播放、收藏若在接口里缺失,用 0 填入数字列,减少「数字字段为空」的写入错误。
(5)代码逻辑里的最后: return
返回一个 item,json 里包含:
①feishu:bitableAppToken、bitableTableId、tenant_access_token(给下一个 HTTP 节点用)
②records:飞书 batch_create 要的数组
③stats.videoCount:方便你在 n8n 里一眼看到本次写入条数
整段代码如下:
/**
* 对每个关键词调用综合搜索(近 7 天、最多播放排序)→ 去重 BV → 拉取热门评论 → 组装飞书记录
* 多维表格字段名与表头一致(中文)
*/
function stripHtml(s) {
return String(s || '').replace(/<[^>]+>/g, '');
}
function getRepliesFromCommentResp(resp) {
const r = resp?.data?.data?.replies;
return Array.isArray(r) ? r : [];
}
function commentMessage(reply) {
const c = reply?.content;
if (c == null) return '';
if (typeof c === 'string') return c;
return c.message || c.text || '';
}
function buildNumberedComments(replies) {
const lines = [];
for (const reply of replies) {
let t = commentMessage(reply).trim().replace(/\r?\n/g, ' ').replace(/\s+/g, ' ');
if (!t) continue;
lines.push(`${lines.length + 1}.${t}`);
}
return lines.join('\n');
}
function buildQuery(params) {
const parts = [];
for (const key in params) {
if (!Object.prototype.hasOwnProperty.call(params, key)) continue;
const value = params[key];
if (value === undefined || value === null || value === '') continue;
parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(value)));
}
return parts.join('&');
}
const input = $input.first().json;
const cfg = input.config;
const range = input.range;
const token = input.tenant_access_token;
const apiKey = cfg.tikhub.apiKey;
const base = (cfg.tikhub.baseUrl || 'https://api.tikhub.io').replace(/\/$/, '');
const headers = { Authorization: `Bearer ${apiKey}` };
const maxVideos = Number(cfg.maxVideos) || 40;
const byBvid = new Map();
for (const keyword of cfg.keywords || []) {
const kw = String(keyword || '').trim();
if (!kw) continue;
const qs = buildQuery({
keyword: kw,
order: 'click',
page: '1',
page_size: '20',
duration: '0',
pubtime_begin_s: String(range.pubtime_begin_s),
pubtime_end_s: String(range.pubtime_end_s),
});
const url = `${base}/api/v1/bilibili/web/fetch_general_search?${qs}`;
let searchResp;
try {
searchResp = await this.helpers.httpRequest({
method: 'GET',
url,
headers,
json: true,
timeout: 120000,
});
} catch (e) {
continue;
}
const list = searchResp?.data?.data?.result || [];
for (const it of list) {
if (!it || it.type !== 'video' || !it.bvid) continue;
if (byBvid.has(it.bvid)) continue;
byBvid.set(it.bvid, {
search_keyword: kw,
author: it.author || '',
bvid: it.bvid,
arcurl: it.arcurl || `https://www.bilibili.com/video/${it.bvid}`,
duration: it.duration != null ? String(it.duration) : '',
title: stripHtml(it.title || ''),
description: String(it.description || ''),
tag: String(it.tag || ''),
pubdate:
typeof it.pubdate === 'number'
? new Date(it.pubdate * 1000).toISOString()
: '',
play: it.play != null ? Number(it.play) : 0,
favorites: it.favorites != null ? Number(it.favorites) : 0,
raw: it,
});
if (byBvid.size >= maxVideos) break;
}
if (byBvid.size >= maxVideos) break;
}
const videos = Array.from(byBvid.values());
const records = [];
for (const v of videos) {
const qsC = buildQuery({
bv_id: v.bvid,
mode: '3',
next_offset: '1',
});
const cUrl = `${base}/api/v1/bilibili/app/fetch_video_comments?${qsC}`;
let cResp;
try {
cResp = await this.helpers.httpRequest({
method: 'GET',
url: cUrl,
headers,
json: true,
timeout: 120000,
});
} catch (e) {
cResp = null;
}
const replies = cResp ? getRepliesFromCommentResp(cResp) : [];
let hot_comments = buildNumberedComments(replies);
if (!hot_comments || !hot_comments.trim()) {
hot_comments = '暂无评论';
}
records.push({
fields: {
视频标题: v.title,
作者: v.author,
bvid: v.bvid,
视频url: v.arcurl,
视频时长: v.duration,
视频描述: v.description,
视频标签: v.tag,
视频发布时间: v.pubdate,
视频播放次数: v.play,
视频收藏次数: v.favorites,
搜索关键词: v.search_keyword,
视频评论: hot_comments,
},
});
}
return [
{
json: {
feishu: {
bitableAppToken: cfg.feishu.bitableAppToken,
bitableTableId: cfg.feishu.bitableTableId,
tenant_access_token: token,
},
records,
stats: { videoCount: records.length },
},
},
];
执行后,每条视频记录就按标准的数据结构输出出来了,我们可以直接把这些内容插入到多维表格里即可

6、IF records > 0
IF节点,主要作用是如果接口可以获取到数据,那么才会调用多维表格的接口进行写入,如果没有,就走 No Operation,减少无意义请求。

7、Feishu – batch_create records(HTTP Request)
http节点,这个节点的作用主要是调用飞书多维表格的写入节点,把我们获取到的B站数据一次性写入到数据表格里
详细配置:
①Method:POST
②URL:https://open.feishu.cn/open-apis/bitable/v1/apps/{{ \$json.feishu.bitableAppToken }}/tables/{{ \$json.feishu.bitableTableId }}/records/batch_create
③Header:Authorization: Bearer {{ \$json.feishu.tenant_access_token }}
④Body:{{ JSON.stringify({ records: \$json.records }) }} ,这里的body值和飞书接口文档中要求的一致即可

四、最后
这一篇内容,我们是把 B 站接进了同一套「选题情报 → 多维表格」的拼图里:和 YouTube、Reddit、频道监控放在一起,我们就有了多源、可对比、可复盘的数据层。这样数据来源更广,我们就不容易把某一渠道的「偶然爆款」当成全局结论。
AI 洞察我们可以像第二弹那样单独做,也可以交给飞书龙虾按表再总结。
【飞书龙虾的那一章节】

*(让龙虾帮我们分析表格数据)*
为什么我一直坚持把数据写进多维表格,而不是只把 Agent 当成唯一入口,因为我认为这种方式对小白和团队都更友好,表里的每一行对应一条可追溯的记录——标题、链接、时间、播放量、评论摘录都在,任何人打开表就能「看见产出了什么」,理解和纠错的成本都低。相比之下,如果只依赖对话式 Agent,中间经历了哪些检索、筛掉了什么、有没有幻觉,往往像黑盒:用起来快,出了问题却难定位,也不利于和同事对齐口径。
多维表格在这里更像人机之间的「可观测层」:自动化负责搬运与结构化,人负责筛选与判断;需要再上 AI 时,也可以让模型基于同一份表内事实去做归纳(例如飞书龙虾、多维表格插件里的 AI),而不是从零开始「口述需求—等一个不可复查的答案」。这样既保留自动化的效率,又保留可迭代的空间。
📋 这套B站关键词相关模板,我整理成模板了
跟着上面步骤搭要花点时间。想直接拿现成的改,加我微信备注「B站关键词」,我发你。
🤔 卡在某一步了?
搭建过程里如果哪一步卡住了,如果你照着做没跑通,或者你们公司情况不太一样,可以找我聊聊,我免费帮你看看怎么调。不合适我也会直说。
扫码加我,备注「B站关键词」领模板
想看更多这类方案,我都整理在 吨师傅工具箱 里了:客户管理、进销存、工单报修……按场景分好类,可以直接抄。


