Add plugin: Bilibili Video Searching

This commit is contained in:
Sheng Fan 2024-04-06 20:19:37 +08:00
parent 5a6e386440
commit c7f1358709
8 changed files with 17311 additions and 1334 deletions

View File

@ -0,0 +1,85 @@
import md5 from "md5";
const mixinKeyEncTab: number[] = [
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61,
26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36,
20, 34, 44, 52,
];
// 对 imgKey 和 subKey 进行字符顺序打乱编码
const getMixinKey = (orig: string): string =>
mixinKeyEncTab
.map((n) => orig[n])
.join("")
.slice(0, 32);
// 为请求参数进行 wbi 签名
export function encWbi(
params: Record<string, string | number>,
img_key: string,
sub_key: string,
): string {
const mixin_key: string = getMixinKey(img_key + sub_key);
const curr_time: number = Math.round(Date.now() / 1000);
const chr_filter: RegExp = /[!'()*]/g;
Object.assign(params, { wts: curr_time }); // 添加 wts 字段
// 按照 key 重排参数
const query: string = Object.keys(params)
.sort()
.map((key) => {
// 过滤 value 中的 "!'()*" 字符
const value: string = params[key].toString().replace(chr_filter, "");
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
})
.join("&");
const wbi_sign: string = md5(query + mixin_key); // 计算 w_rid
return query + "&w_rid=" + wbi_sign;
}
// 获取最新的 img_key 和 sub_key
export async function getWbiKeys(): Promise<{
img_key: string;
sub_key: string;
}> {
const res: Response = await fetch(
"https://api.bilibili.com/x/web-interface/nav",
{
headers: {
// SESSDATA 字段
Cookie: "SESSDATA=xxxxxx",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
Referer: "https://www.bilibili.com/", //对于直接浏览器调用可能不适用
},
},
);
const {
data: {
wbi_img: { img_url, sub_url },
},
} = await res.json();
return {
img_key: img_url.slice(
img_url.lastIndexOf("/") + 1,
img_url.lastIndexOf("."),
),
sub_key: sub_url.slice(
sub_url.lastIndexOf("/") + 1,
sub_url.lastIndexOf("."),
),
};
}
// async function main(): Promise<void> {
// const web_keys: { img_key: string, sub_key: string } = await getWbiKeys()
// const params: Record<string, string | number> = { foo: '114', bar: '514', baz: 1919810 }
// const img_key: string = web_keys.img_key
// const sub_key: string = web_keys.sub_key
// const query: string = encWbi(params, img_key, sub_key)
// console.log(query)
// }

View File

@ -11,93 +11,6 @@ export interface RequestTool {
timeout: number;
}
async function fetchVideoInfo(prompt: string) {
const headers = new Headers();
headers.append("User-Agent", getRandomUserAgent());
let video_param = "";
if (prompt.toLowerCase().startsWith("av")) {
video_param = `aid=${prompt.slice(2)}`;
} else if (prompt.toLowerCase().startsWith("bv")) {
video_param = `bvid=${prompt}`;
} else {
return "FAIL: Invalid video ID or URL.";
}
const resp = await fetch(
`https://api.bilibili.com/x/web-interface/view?${video_param}`,
{
headers: headers,
},
);
let rawData: { [key: string]: any } = await resp.json();
let data: { [key: string]: string } = {};
// Keep those: bvid, aid, videos, copyright, tname, title, pubdate, desc, state(values see below), owner, argue_info
// state:
// 1 橙色通过
// 0 开放浏览
// -1 待审
// -2 被打回
// -3 网警锁定
// -4 被锁定 视频撞车了
// -5 管理员锁定
// -6 修复待审
// -7 暂缓审核
// -8 补档待审
// -9 等待转码
// -10 延迟审核
// -11 视频源待修
// -12 转储失败
// -13 允许评论待审
// -14 临时回收站
// -15 分发中
// -16 转码失败
// -20 创建未提交
// -30 创建已提交
// -40 定时发布
// -100 用户删除
// convert state to string
const stateConvertDict: { [key: string]: string } = {
"1": "橙色通过",
"0": "开放浏览",
"-1": "待审",
"-2": "被打回",
"-3": "网警锁定",
"-4": "被锁定",
"-5": "管理员锁定",
"-6": "修复待审",
"-7": "暂缓审核",
"-8": "补档待审",
"-9": "等待转码",
"-10": "延迟审核",
"-11": "视频源待修",
"-12": "转储失败",
"-13": "允许评论待审",
"-14": "临时回收站",
"-15": "分发中",
"-16": "转码失败",
"-20": "创建未提交",
"-30": "创建已提交",
"-40": "定时发布",
"-100": "用户删除",
};
data["state"] = stateConvertDict[rawData.data.state.toString()];
data["bvid"] = rawData.data.bvid;
data["aid"] = rawData.data.aid;
data["videos"] = rawData.data.videos;
data["copyright"] = rawData.data.copyright;
data["tname"] = rawData.data.tname;
data["title"] = rawData.data.title;
data["pubdate"] = rawData.data.pubdate;
data["desc"] = rawData.data.desc;
// data["state"] = rawData.data.state.toString();
data["owner"] = rawData.data.owner.name;
data["argue_info"] = rawData.data.argue_info;
return "SUCCESS: Video data should be in this JSON: " + JSON.stringify(data);
}
export class BilibiliVideoInfoTool extends Tool implements RequestTool {
name = "bilibili_video_info";
@ -119,7 +32,7 @@ export class BilibiliVideoInfoTool extends Tool implements RequestTool {
/** @ignore */
async _call(input: string) {
try {
let result = await fetchVideoInfo(input);
let result = await this.fetchVideoInfo(input);
// console.log(result)
return result;
} catch (error) {
@ -128,6 +41,95 @@ export class BilibiliVideoInfoTool extends Tool implements RequestTool {
}
}
async fetchVideoInfo(prompt: string) {
const headers = new Headers();
headers.append("User-Agent", getRandomUserAgent());
let video_param = "";
if (prompt.toLowerCase().startsWith("av")) {
video_param = `aid=${prompt.slice(2)}`;
} else if (prompt.toLowerCase().startsWith("bv")) {
video_param = `bvid=${prompt}`;
} else {
return "FAIL: Invalid video ID or URL.";
}
const resp = await this.fetchWithTimeout(
`https://api.bilibili.com/x/web-interface/view?${video_param}`,
{
headers: headers,
},
);
let rawData: { [key: string]: any } = await resp.json();
let data: { [key: string]: string } = {};
// Keep those: bvid, aid, videos, copyright, tname, title, pubdate, desc, state(values see below), owner, argue_info
// state:
// 1 橙色通过
// 0 开放浏览
// -1 待审
// -2 被打回
// -3 网警锁定
// -4 被锁定 视频撞车了
// -5 管理员锁定
// -6 修复待审
// -7 暂缓审核
// -8 补档待审
// -9 等待转码
// -10 延迟审核
// -11 视频源待修
// -12 转储失败
// -13 允许评论待审
// -14 临时回收站
// -15 分发中
// -16 转码失败
// -20 创建未提交
// -30 创建已提交
// -40 定时发布
// -100 用户删除
// convert state to string
const stateConvertDict: { [key: string]: string } = {
"1": "橙色通过",
"0": "开放浏览",
"-1": "待审",
"-2": "被打回",
"-3": "网警锁定",
"-4": "被锁定",
"-5": "管理员锁定",
"-6": "修复待审",
"-7": "暂缓审核",
"-8": "补档待审",
"-9": "等待转码",
"-10": "延迟审核",
"-11": "视频源待修",
"-12": "转储失败",
"-13": "允许评论待审",
"-14": "临时回收站",
"-15": "分发中",
"-16": "转码失败",
"-20": "创建未提交",
"-30": "创建已提交",
"-40": "定时发布",
"-100": "用户删除",
};
data["state"] = stateConvertDict[rawData.data.state.toString()];
data["bvid"] = rawData.data.bvid;
data["aid"] = rawData.data.aid;
data["subVideoCount"] = rawData.data.videos;
data["copyrightData"] = rawData.data.copyright;
data["videoTypeName"] = rawData.data.tname;
data["title"] = rawData.data.title;
data["publishDate"] = rawData.data.pubdate;
data["descriptions"] = rawData.data.desc;
// data["state"] = rawData.data.state.toString();
data["ownerName"] = rawData.data.owner.name;
data["argueInfo"] = rawData.data.argue_info;
return (
"SUCCESS: Video data should be in this JSON: " + JSON.stringify(data)
);
}
async fetchWithTimeout(
resource: RequestInfo | URL,
options = {},

View File

@ -0,0 +1,122 @@
import { Tool } from "@langchain/core/tools";
import { getRandomUserAgent } from "./ua_tools";
import { encWbi, getWbiKeys } from "./bili_wbi_tools";
export interface Headers {
[key: string]: string;
}
export interface RequestTool {
headers: Headers;
maxOutputLength?: number;
timeout: number;
}
export class BilibiliVideoSearchTool extends Tool implements RequestTool {
name = "bilibili_video_search";
maxOutputLength = Infinity;
timeout = 10000;
constructor(
public headers: Headers = {},
{ maxOutputLength }: { maxOutputLength?: number } = {},
{ timeout }: { timeout?: number } = {},
) {
super(...arguments);
this.maxOutputLength = maxOutputLength ?? this.maxOutputLength;
this.timeout = timeout ?? this.timeout;
}
/** @ignore */
async _call(searchQuery: string) {
try {
let result = await this.searchFromBilibili(searchQuery);
// console.log(result)
return JSON.stringify(result);
} catch (error) {
console.error(error);
return (error as Error).toString();
}
}
async searchFromBilibili(searchQuery: string, searchType: string = "video") {
const headers = new Headers();
headers.append("User-Agent", getRandomUserAgent());
headers.append(
"Referer",
"https://search.bilibili.com/all?keyword=" +
encodeURIComponent(searchQuery),
);
headers.append("Origin", "https://search.bilibili.com");
headers.append("Cookie", process.env.BILIBILI_COOKIES || "");
const { img_key, sub_key } = await getWbiKeys();
const queryString = encWbi(
{
keyword: searchQuery.trim(),
search_type: searchType,
},
img_key,
sub_key,
);
const url = `https://api.bilibili.com/x/web-interface/wbi/search/type?${queryString}`;
const resp = await this.fetchWithTimeout(url, {
headers: headers,
});
let rawData: { [key: string]: any } = await resp.json();
// console.log(rawData);
let data: Array<{
title: string;
url: string;
authorName: string;
authorMid: number;
viewCount: number;
durationSeconds: number;
}> = [];
rawData.data.result.forEach((element: { [key: string]: any }) => {
const rawTitle = element.title;
const title = rawTitle.replace(/<[^>]+>/g, ""); // remove HTML tags
const url = `https://www.bilibili.com/video/${element.bvid}`;
const authorName = element.author;
const authorMid = element.mid;
const viewCount = element.play;
const durationHHMM = element.duration;
var durationSeconds = 0;
durationHHMM.split(":").forEach((timetag: string) => {
durationSeconds = durationSeconds * 60 + parseInt(timetag);
});
data.push({
title,
url,
authorName,
authorMid,
viewCount,
durationSeconds,
});
});
return data;
}
async fetchWithTimeout(
resource: RequestInfo | URL,
options = {},
timeout: number = 30000,
) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const response = await fetch(resource, {
...options,
signal: controller.signal,
});
clearTimeout(id);
return response;
}
description = `A tool that searches for videos on Bilibili. Input string is the search query (use Chinese characters for better results in most cases). Output is a list of video titles and URLs.`;
}

View File

@ -8,6 +8,7 @@ import { Calculator } from "langchain/tools/calculator";
import { WebBrowser } from "langchain/tools/webbrowser";
import { WolframAlphaTool } from "@/app/api/langchain-tools/wolframalpha";
import { BilibiliVideoInfoTool } from "./bilibili_vid_info";
import { BilibiliVideoSearchTool } from "./bilibili_vid_search";
export class NodeJSTool {
private apiKey: string | undefined;
@ -50,6 +51,7 @@ export class NodeJSTool {
const wolframAlphaTool = new WolframAlphaTool();
const pdfBrowserTool = new PDFBrowser(this.model, this.embeddings);
const bilibiliVideoInfoTool = new BilibiliVideoInfoTool();
const bilibiliVideoSearchTool = new BilibiliVideoSearchTool();
let tools = [
calculatorTool,
webBrowserTool,
@ -59,6 +61,7 @@ export class NodeJSTool {
wolframAlphaTool,
pdfBrowserTool,
bilibiliVideoInfoTool,
bilibiliVideoSearchTool,
];
return tools;
}

View File

@ -32,6 +32,26 @@ export const CN_PLUGINS: BuiltinPlugin[] = [
enable: true,
onlyNodeRuntime: false,
},
{
name: "Bilibili视频信息获取",
toolName: "bilibili_video_info",
lang: "cn",
description: "通过Bilibili视频ID获取视频信息如标题、简介等。",
builtin: true,
createdAt: 1712394126000,
enable: true,
onlyNodeRuntime: true,
},
{
name: "Bilibili视频搜索",
toolName: "bilibili_video_search",
lang: "cn",
description: "通过关键词搜索Bilibili视频并获取视频信息。",
builtin: true,
createdAt: 1712394126000,
enable: true,
onlyNodeRuntime: true,
},
{
name: "维基百科",
toolName: "WikipediaQueryRun",
@ -95,14 +115,4 @@ export const CN_PLUGINS: BuiltinPlugin[] = [
enable: false,
onlyNodeRuntime: false,
},
{
name: "Bilibili视频信息获取",
toolName: "bilibili_video_info",
lang: "cn",
description: "通过Bilibili视频ID获取视频信息如标题、简介等。",
builtin: true,
createdAt: 1712394126000,
enable: true,
onlyNodeRuntime: true,
},
];

15701
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -38,6 +38,7 @@
"html-to-text": "^9.0.5",
"https-proxy-agent": "^7.0.2",
"langchain": "0.1.20",
"md5": "^2.3.0",
"mermaid": "^10.6.1",
"nanoid": "^5.0.3",
"next": "^13.4.9",
@ -54,6 +55,7 @@
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"sass": "^1.59.2",
"sharp": "^0.33.3",
"spark-md5": "^3.0.2",
"use-debounce": "^9.0.4",
"zustand": "^4.3.8"
@ -61,6 +63,7 @@
"devDependencies": {
"@tauri-apps/cli": "1.5.11",
"@types/html-to-text": "^9.0.1",
"@types/md5": "^2.3.5",
"@types/node": "^20.11.30",
"@types/react": "^18.2.70",
"@types/react-dom": "^18.2.7",
@ -83,4 +86,4 @@
"openai": "4.28.4"
},
"packageManager": "yarn@1.22.19"
}
}

2521
yarn.lock

File diff suppressed because it is too large Load Diff