feat(bilibili): add messages routes (#20890)

* feat(bilibili): add messages routes

* fix(bilibili/message): do not display summary when no unread messages

* fix(bilibili/message): update cache duration to use configurable routeExpire
This commit is contained in:
Pilgrim Lyieu 2026-01-16 09:34:43 +08:00 committed by GitHub
parent c21c82fc05
commit 8bb3d6f019
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1077 additions and 0 deletions

View File

@ -0,0 +1,146 @@
import { config } from '@/config';
import ConfigNotFoundError from '@/errors/types/config-not-found';
import type { DataItem, Route } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
import cache from './cache';
export const route: Route = {
path: '/message/at/:uid',
categories: ['social-media'],
example: '/bilibili/message/at/2267573',
parameters: { uid: '用户 id' },
features: {
requireConfig: [
{
name: 'BILIBILI_COOKIE_*',
description: `BILIBILI_COOKIE_{uid}: 用于用户关注动态系列路由,对应 uid 的 b 站用户登录后的 Cookie 值,\`{uid}\` 替换为 uid\`BILIBILI_COOKIE_2267573\`,获取方式:
1. [https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8](https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8)
2. Network
3. dynamic_new Cookie
4. UP \`SESSDATA\` 字段,动态需复制整段 Cookie`,
},
],
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
name: '@我的',
maintainers: ['pilgrimlyieu'],
handler,
description: `:::warning
b Cookie
:::`,
};
interface AtItem {
id: number;
user: {
mid: number;
fans: number;
nickname: string;
avatar: string;
mid_link: string;
follow: boolean;
};
item: {
subject_id: number;
root_id: number;
source_id: number;
target_id: number;
type: string;
business_id: number;
business: string;
title: string;
desc: string;
image: string;
uri: string;
native_uri: string;
detail_title: string;
source_content: string;
at_details: unknown[];
};
at_time: number;
}
interface AtResponse {
code: number;
message: string;
ttl: number;
data: {
cursor: {
is_end: boolean;
id: number;
time: number;
};
items: AtItem[];
};
}
async function handler(ctx) {
const uid = ctx.req.param('uid');
const name = await cache.getUsernameFromUID(uid);
const cookie = config.bilibili.cookies[uid];
if (cookie === undefined) {
throw new ConfigNotFoundError('缺少对应 uid 的 Bilibili 用户登录后的 Cookie 值');
}
const response = await ofetch<AtResponse>('https://api.bilibili.com/x/msgfeed/at', {
query: {
platform: 'web',
build: 0,
mobi_app: 'web',
},
headers: {
Referer: 'https://message.bilibili.com/',
Cookie: cookie,
},
});
if (response.code !== 0) {
throw new Error(response.message ?? `Error code ${response.code}`);
}
const items: DataItem[] = (response.data.items || []).map((item) => {
const atUser = item.user;
const atItem = item.item;
const sourceContent = atItem.source_content;
let description = `<p><strong>${atUser.nickname}</strong> @了你:</p>`;
description += `<blockquote>${sourceContent}</blockquote>`;
if (atItem.image) {
description += `<p><img src="${atItem.image.replace('http://', 'https://')}" /></p>`;
}
description += `<p>来自:${atItem.business} - ${atItem.title}</p>`;
// Generate link with root_id for direct navigation
let link = atItem.uri;
if (atItem.root_id && atItem.uri) {
link = `${atItem.uri}/#reply${atItem.root_id}`;
} else if (atItem.source_id && atItem.uri) {
link = `${atItem.uri}/#reply${atItem.source_id}`;
}
return {
title: `${atUser.nickname} @了你:${sourceContent}`,
description,
link,
pubDate: parseDate(item.at_time * 1000),
author: atUser.nickname,
};
});
return {
title: `${name} 的 B站消息 - @我的`,
link: 'https://message.bilibili.com/#/at',
description: `${name} 的 B站消息 - @我的`,
item: items,
allowEmpty: true,
};
}

View File

@ -0,0 +1,163 @@
import { config } from '@/config';
import ConfigNotFoundError from '@/errors/types/config-not-found';
import type { DataItem, Route } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
import cache from './cache';
export const route: Route = {
path: '/message/like/:uid',
categories: ['social-media'],
example: '/bilibili/message/like/2267573',
parameters: { uid: '用户 id' },
features: {
requireConfig: [
{
name: 'BILIBILI_COOKIE_*',
description: `BILIBILI_COOKIE_{uid}: 用于用户关注动态系列路由,对应 uid 的 b 站用户登录后的 Cookie 值,\`{uid}\` 替换为 uid\`BILIBILI_COOKIE_2267573\`,获取方式:
1. [https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8](https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8)
2. Network
3. dynamic_new Cookie
4. UP \`SESSDATA\` 字段,动态需复制整段 Cookie`,
},
],
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
name: '收到的赞',
maintainers: ['pilgrimlyieu'],
handler,
description: `:::warning
b Cookie
:::`,
};
interface LikeUser {
mid: number;
fans: number;
nickname: string;
avatar: string;
mid_link: string;
follow: boolean;
}
interface LikeItem {
id: number;
users: LikeUser[];
item: {
item_id: number;
pid: number;
type: string;
business: string;
business_id: number;
reply_business_id: number;
like_business_id: number;
title: string;
desc: string;
image: string;
uri: string;
detail_name: string;
native_uri: string;
ctime: number;
};
counts: number;
like_time: number;
notice_state: number;
}
interface LikeResponse {
code: number;
message: string;
ttl: number;
data: {
latest: {
items: LikeItem[];
last_view_at: number;
};
total: {
cursor: {
is_end: boolean;
id: number;
time: number;
};
items: LikeItem[];
};
};
}
async function handler(ctx) {
const uid = ctx.req.param('uid');
const name = await cache.getUsernameFromUID(uid);
const cookie = config.bilibili.cookies[uid];
if (cookie === undefined) {
throw new ConfigNotFoundError('缺少对应 uid 的 Bilibili 用户登录后的 Cookie 值');
}
const response = await ofetch<LikeResponse>('https://api.bilibili.com/x/msgfeed/like', {
query: {
platform: 'web',
build: 0,
mobi_app: 'web',
},
headers: {
Referer: 'https://message.bilibili.com/',
Cookie: cookie,
},
});
if (response.code !== 0) {
throw new Error(response.message ?? `Error code ${response.code}`);
}
const allItems = [...(response.data.latest?.items || []), ...(response.data.total?.items || [])];
// Deduplicate by id
const uniqueItems = allItems.filter((item, index, self) => index === self.findIndex((t) => t.id === item.id));
const items: DataItem[] = uniqueItems.map((item) => {
const likeUsers = item.users;
const likeItem = item.item;
const counts = item.counts;
const userNames = likeUsers.map((u) => u.nickname).join('、');
const displayNames = counts > likeUsers.length ? `${userNames}${counts}` : userNames;
let description = `<p><strong>${displayNames}</strong> 赞了你的${likeItem.business}</p>`;
description += `<p><strong>${likeItem.title}</strong></p>`;
if (likeItem.desc) {
description += `<blockquote>${likeItem.desc}</blockquote>`;
}
if (likeItem.image) {
description += `<p><img src="${likeItem.image.replace('http://', 'https://')}" /></p>`;
}
// Generate link based on type
let link = likeItem.uri;
if (likeItem.type === 'reply' && likeItem.item_id) {
link = `${likeItem.uri}/#reply${likeItem.item_id}`;
}
return {
title: `${displayNames} 赞了你的${likeItem.business}${likeItem.title}`,
description,
link,
pubDate: parseDate(item.like_time * 1000),
author: userNames,
};
});
return {
title: `${name} 的 B站消息 - 收到的赞`,
link: 'https://message.bilibili.com/#/love',
description: `${name} 的 B站消息 - 收到的赞`,
item: items,
allowEmpty: true,
};
}

View File

@ -0,0 +1,164 @@
import { config } from '@/config';
import ConfigNotFoundError from '@/errors/types/config-not-found';
import type { DataItem, Route } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
import cache from './cache';
export const route: Route = {
path: '/message/reply/:uid',
categories: ['social-media'],
example: '/bilibili/message/reply/2267573',
parameters: { uid: '用户 id' },
features: {
requireConfig: [
{
name: 'BILIBILI_COOKIE_*',
description: `BILIBILI_COOKIE_{uid}: 用于用户关注动态系列路由,对应 uid 的 b 站用户登录后的 Cookie 值,\`{uid}\` 替换为 uid\`BILIBILI_COOKIE_2267573\`,获取方式:
1. [https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8](https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8)
2. Network
3. dynamic_new Cookie
4. UP \`SESSDATA\` 字段,动态需复制整段 Cookie`,
},
],
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
name: '回复我的',
maintainers: ['pilgrimlyieu'],
handler,
description: `:::warning
b Cookie
:::`,
};
interface ReplyItem {
id: number;
user: {
mid: number;
fans: number;
nickname: string;
avatar: string;
mid_link: string;
follow: boolean;
};
item: {
subject_id: number;
root_id: number;
source_id: number;
target_id: number;
type: string;
business_id: number;
business: string;
title: string;
desc: string;
image: string;
uri: string;
native_uri: string;
detail_title: string;
root_reply_content: string;
source_content: string;
target_reply_content: string;
at_details: unknown[];
hide_reply_button: boolean;
hide_like_button: boolean;
like_state: number;
danmu: unknown;
message: string;
};
counts: number;
is_multi: number;
reply_time: number;
}
interface ReplyResponse {
code: number;
message: string;
ttl: number;
data: {
cursor: {
is_end: boolean;
id: number;
time: number;
};
items: ReplyItem[];
last_view_at: number;
};
}
async function handler(ctx) {
const uid = ctx.req.param('uid');
const name = await cache.getUsernameFromUID(uid);
const cookie = config.bilibili.cookies[uid];
if (cookie === undefined) {
throw new ConfigNotFoundError('缺少对应 uid 的 Bilibili 用户登录后的 Cookie 值');
}
const response = await ofetch<ReplyResponse>('https://api.bilibili.com/x/msgfeed/reply', {
query: {
platform: 'web',
build: 0,
mobi_app: 'web',
},
headers: {
Referer: 'https://message.bilibili.com/',
Cookie: cookie,
},
});
if (response.code !== 0) {
throw new Error(response.message ?? `Error code ${response.code}`);
}
const items: DataItem[] = (response.data.items || []).map((item) => {
const replyUser = item.user;
const replyItem = item.item;
const sourceContent = replyItem.source_content;
const targetContent = replyItem.target_reply_content;
const rootContent = replyItem.root_reply_content;
let description = `<p><strong>${replyUser.nickname}</strong> 回复了你:</p>`;
description += `<blockquote>${sourceContent}</blockquote>`;
if (targetContent) {
description += `<p>你的评论:</p><blockquote>${targetContent}</blockquote>`;
} else if (rootContent) {
description += `<p>你的评论:</p><blockquote>${rootContent}</blockquote>`;
}
if (replyItem.image) {
description += `<p><img src="${replyItem.image.replace('http://', 'https://')}" /></p>`;
}
description += `<p>来自:${replyItem.business} - ${replyItem.title}</p>`;
// Generate comment link with root_id for direct navigation to the comment
let link = replyItem.uri;
if (replyItem.root_id && replyItem.uri) {
link = `${replyItem.uri}/#reply${replyItem.root_id}`;
} else if (replyItem.source_id && replyItem.uri) {
link = `${replyItem.uri}/#reply${replyItem.source_id}`;
}
return {
title: `${replyUser.nickname} 回复了你:${sourceContent}`,
description,
link,
pubDate: parseDate(item.reply_time * 1000),
author: replyUser.nickname,
};
});
return {
title: `${name} 的 B站消息 - 回复我的`,
link: 'https://message.bilibili.com/#/reply',
description: `${name} 的 B站消息 - 回复我的`,
item: items,
allowEmpty: true,
};
}

View File

@ -0,0 +1,242 @@
import { config } from '@/config';
import ConfigNotFoundError from '@/errors/types/config-not-found';
import type { DataItem, Route } from '@/types';
import cache from '@/utils/cache';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
import bilibiliCache from './cache';
export const route: Route = {
path: '/message/sessions/:uid',
categories: ['social-media'],
example: '/bilibili/message/sessions/2267573',
parameters: { uid: '用户 id' },
features: {
requireConfig: [
{
name: 'BILIBILI_COOKIE_*',
description: `BILIBILI_COOKIE_{uid}: 用于用户关注动态系列路由,对应 uid 的 b 站用户登录后的 Cookie 值,\`{uid}\` 替换为 uid\`BILIBILI_COOKIE_2267573\`,获取方式:
1. [https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8](https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8)
2. Network
3. dynamic_new Cookie
4. UP \`SESSDATA\` 字段,动态需复制整段 Cookie`,
},
],
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
name: '我的消息',
maintainers: ['pilgrimlyieu'],
handler,
description: `:::warning
b Cookie
:::`,
};
interface SessionItem {
talker_id: number;
session_type: number;
at_seqno: number;
top_ts: number;
group_name: string;
group_cover: string;
is_follow: number;
is_dnd: number;
ack_seqno: number;
ack_ts: number;
session_ts: number;
unread_count: number;
last_msg: {
sender_uid: number;
receiver_type: number;
receiver_id: number;
msg_type: number;
content: string;
msg_seqno: number;
timestamp: number;
at_uids: number[] | null;
msg_key: number;
msg_status: number;
notify_code: string;
msg_source: number;
} | null;
group_type: number;
can_fold: number;
status: number;
max_seqno: number;
new_push_msg: number;
setting: number;
is_guardian: number;
is_intercept: number;
is_trust: number;
system_msg_type: number;
live_status: number;
biz_msg_unread_count: number;
user_label: unknown;
}
interface SessionResponse {
code: number;
msg: string;
message: string;
ttl: number;
data: {
session_list: SessionItem[] | null;
has_more: number;
anti_disturb_cleaning: boolean;
is_address_list_empty: number;
system_msg: Record<string, number>;
show_level: boolean;
};
}
interface UserInfo {
mid: string;
face: string;
name: string;
}
interface UserCardsResponse {
code: number;
message: string;
ttl: number;
data: Record<string, UserInfo>;
}
/**
* Parse message content based on msg_type
* msg_type 1: text, 2: image, 5: recall, etc.
*/
function parseMessageContent(content: string, msgType: number): string {
try {
const parsed = JSON.parse(content);
switch (msgType) {
case 1: // Text message
return parsed.content || content;
case 2: // Image
return `[图片] ${parsed.url || ''}`;
case 5: // Recall
return '[消息已撤回]';
case 6: // Sticker
return '[表情]';
case 7: // Share
return `[分享] ${parsed.title || ''}`;
case 10: // System notification
return parsed.content || parsed.title || content;
case 11: // Video card
return `[视频] ${parsed.title || ''}`;
case 12: // Article card
return `[专栏] ${parsed.title || ''}`;
case 14: // Bangumi card
return `[番剧] ${parsed.title || ''}`;
default:
return parsed.content || parsed.title || content;
}
} catch {
return content;
}
}
async function handler(ctx) {
const uid = ctx.req.param('uid');
const name = await bilibiliCache.getUsernameFromUID(uid);
const cookie = config.bilibili.cookies[uid];
if (cookie === undefined) {
throw new ConfigNotFoundError('缺少对应 uid 的 Bilibili 用户登录后的 Cookie 值');
}
const response = await ofetch<SessionResponse>('https://api.vc.bilibili.com/session_svr/v1/session_svr/get_sessions', {
query: {
session_type: 1,
group_fold: 1,
unfollow_fold: 0,
sort_rule: 2,
build: 0,
mobi_app: 'web',
},
headers: {
Referer: 'https://message.bilibili.com/',
Cookie: cookie,
},
});
if (response.code !== 0) {
throw new Error(response.message ?? response.msg ?? `Error code ${response.code}`);
}
const sessionList = response.data.session_list || [];
const talkerIds = sessionList.filter((s) => s.session_type === 1).map((s) => s.talker_id);
// Fetch user info for all talkers
let userCards: Record<string, UserInfo> = {};
if (talkerIds.length > 0) {
const userCardsResponse = await cache.tryGet(
`bilibili-user-cards-${talkerIds.join(',')}`,
async () => {
const res = await ofetch<UserCardsResponse>('https://api.bilibili.com/x/polymer/pc-electron/v1/user/cards', {
query: {
uids: talkerIds.join(','),
build: 0,
mobi_app: 'web',
},
headers: {
Referer: 'https://message.bilibili.com/',
Cookie: cookie,
},
});
return res.data || {};
},
config.cache.routeExpire
);
userCards = userCardsResponse as Record<string, UserInfo>;
}
const items: DataItem[] = sessionList
.filter((session) => session.last_msg)
.map((session) => {
const lastMsg = session.last_msg!;
const talkerId = session.talker_id;
const userInfo = userCards[String(talkerId)];
const talkerName = userInfo?.name || `用户${talkerId}`;
const talkerFace = userInfo?.face || '';
const msgContent = parseMessageContent(lastMsg.content, lastMsg.msg_type);
const isSentByMe = lastMsg.sender_uid === Number(uid);
let description = '';
if (talkerFace) {
description += `<p><img src="${talkerFace.replace('http://', 'https://')}" width="48" height="48" style="border-radius: 50%;" /></p>`;
}
description += isSentByMe ? `<p><strong>你</strong> 对 <strong>${talkerName}</strong> 说:</p>` : `<p><strong>${talkerName}</strong> 说:</p>`;
description += `<blockquote>${msgContent}</blockquote>`;
if (session.unread_count > 0) {
description += `<p>未读消息: ${session.unread_count} 条</p>`;
}
const title = isSentByMe ? `你对 ${talkerName} 说:${msgContent}` : `${talkerName}${msgContent}`;
return {
title,
description,
link: `https://message.bilibili.com/#/whisper/mid${talkerId}`,
pubDate: parseDate(lastMsg.timestamp * 1000),
author: isSentByMe ? name : talkerName,
guid: `bilibili-session-${talkerId}-${lastMsg.msg_key}`,
};
});
return {
title: `${name} 的 B站消息 - 我的消息`,
link: 'https://message.bilibili.com/#/whisper',
description: `${name} 的 B站消息 - 我的消息`,
item: items,
allowEmpty: true,
};
}

View File

@ -0,0 +1,143 @@
import { config } from '@/config';
import ConfigNotFoundError from '@/errors/types/config-not-found';
import type { DataItem, Route } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
import cache from './cache';
export const route: Route = {
path: '/message/system/:uid',
categories: ['social-media'],
example: '/bilibili/message/system/2267573',
parameters: { uid: '用户 id' },
features: {
requireConfig: [
{
name: 'BILIBILI_COOKIE_*',
description: `BILIBILI_COOKIE_{uid}: 用于用户关注动态系列路由,对应 uid 的 b 站用户登录后的 Cookie 值,\`{uid}\` 替换为 uid\`BILIBILI_COOKIE_2267573\`,获取方式:
1. [https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8](https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8)
2. Network
3. dynamic_new Cookie
4. UP \`SESSDATA\` 字段,动态需复制整段 Cookie`,
},
],
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
name: '系统通知',
maintainers: ['pilgrimlyieu'],
handler,
description: `:::warning
b Cookie
:::`,
};
interface SystemNotifyItem {
id: number;
cursor: number;
publisher: {
name: string;
mid: number;
face: string;
};
type: number;
title: string;
content: string;
source: {
name: string;
logo: string;
};
time_at: string;
card_type: number;
card_brief: string;
card_msg_brief: string;
card_cover: string;
card_story_title: string;
card_link: string;
mc: string;
is_station: number;
is_send: number;
notify_cursor: number;
}
interface SystemResponse {
code: number;
msg: string;
message: string;
ttl: number;
data: {
system_notify_list: SystemNotifyItem[];
};
}
/**
* Parse bilibili message content with special link format
* Format: #{text}{"url"} -> <a href="url">text</a>
*/
function parseMessageContent(content: string): string {
// Match pattern like #{text}{"url"}
const linkPattern = /#\{([^}]+)\}\{"([^"]+)"\}/g;
return content.replaceAll(linkPattern, '<a href="$2">$1</a>');
}
async function handler(ctx) {
const uid = ctx.req.param('uid');
const name = await cache.getUsernameFromUID(uid);
const cookie = config.bilibili.cookies[uid];
if (cookie === undefined) {
throw new ConfigNotFoundError('缺少对应 uid 的 Bilibili 用户登录后的 Cookie 值');
}
const response = await ofetch<SystemResponse>('https://message.bilibili.com/x/sys-msg/query_user_notify', {
query: {
page_size: 20,
build: 0,
mobi_app: 'web',
},
headers: {
Referer: 'https://message.bilibili.com/',
Cookie: cookie,
},
});
if (response.code !== 0) {
throw new Error(response.message ?? response.msg ?? `Error code ${response.code}`);
}
const items: DataItem[] = (response.data.system_notify_list || []).map((item) => {
let description = `<p><strong>${item.title}</strong></p>`;
const parsedContent = parseMessageContent(item.content);
description += `<p>${parsedContent.replaceAll('\n', '<br>')}</p>`;
if (item.source.logo) {
description += `<p><img src="${item.source.logo.replace('http://', 'https://')}" width="40" /></p>`;
}
if (item.card_cover) {
description += `<p><img src="${item.card_cover.replace('http://', 'https://')}" /></p>`;
}
const link = item.card_link || 'https://message.bilibili.com/#/system';
return {
title: item.title,
description,
link,
pubDate: parseDate(item.time_at),
guid: `bilibili-system-notify-${item.id}-${item.cursor}`,
};
});
return {
title: `${name} 的 B站消息 - 系统通知`,
link: 'https://message.bilibili.com/#/system',
description: `${name} 的 B站消息 - 系统通知`,
item: items,
allowEmpty: true,
};
}

View File

@ -0,0 +1,219 @@
import { config } from '@/config';
import ConfigNotFoundError from '@/errors/types/config-not-found';
import type { DataItem, Route } from '@/types';
import ofetch from '@/utils/ofetch';
import cache from './cache';
export const route: Route = {
path: '/message/unread/:uid',
categories: ['social-media'],
example: '/bilibili/message/unread/2267573',
parameters: { uid: '用户 id' },
features: {
requireConfig: [
{
name: 'BILIBILI_COOKIE_*',
description: `BILIBILI_COOKIE_{uid}: 用于用户关注动态系列路由,对应 uid 的 b 站用户登录后的 Cookie 值,\`{uid}\` 替换为 uid\`BILIBILI_COOKIE_2267573\`,获取方式:
1. [https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8](https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8)
2. Network
3. dynamic_new Cookie
4. UP \`SESSDATA\` 字段,动态需复制整段 Cookie`,
},
],
requirePuppeteer: false,
antiCrawler: false,
supportBT: false,
supportPodcast: false,
supportScihub: false,
},
name: '未读消息',
maintainers: ['pilgrimlyieu'],
handler,
description: `:::warning
b Cookie
:::`,
};
interface UnreadMsgResponse {
code: number;
message: string;
ttl: number;
data: {
at: number;
coin: number;
danmu: number;
favorite: number;
like: number;
recv_like: number;
recv_reply: number;
reply: number;
sys_msg: number;
sys_msg_style: number;
up: number;
};
}
interface UnreadSessionResponse {
code: number;
msg: string;
message: string;
ttl: number;
data: {
unfollow_unread: number;
follow_unread: number;
unfollow_push_msg: number;
dustbin_push_msg: number;
dustbin_unread: number;
biz_msg_unfollow_unread: number;
biz_msg_follow_unread: number;
custom_unread: number;
};
}
async function handler(ctx) {
const uid = ctx.req.param('uid');
const name = await cache.getUsernameFromUID(uid);
const cookie = config.bilibili.cookies[uid];
if (cookie === undefined) {
throw new ConfigNotFoundError('缺少对应 uid 的 Bilibili 用户登录后的 Cookie 值');
}
// Fetch message unread counts
const [msgUnread, sessionUnread] = await Promise.all([
ofetch<UnreadMsgResponse>('https://api.vc.bilibili.com/x/im/web/msgfeed/unread', {
query: {
build: 0,
mobi_app: 'web',
},
headers: {
Referer: 'https://message.bilibili.com/',
Cookie: cookie,
},
}),
ofetch<UnreadSessionResponse>('https://api.vc.bilibili.com/session_svr/v1/session_svr/single_unread', {
query: {
unread_type: 0,
show_dustbin: 1,
build: 0,
mobi_app: 'web',
},
headers: {
Referer: 'https://message.bilibili.com/',
Cookie: cookie,
},
}),
]);
if (msgUnread.code !== 0) {
throw new Error(msgUnread.message ?? `Error code ${msgUnread.code}`);
}
const msgData = msgUnread.data;
const sessionData = sessionUnread.data;
const items: DataItem[] = [];
const now = new Date();
// 回复我的
if (msgData.recv_reply > 0 || msgData.reply > 0) {
const replyCount = msgData.recv_reply || msgData.reply;
items.push({
title: `回复我的:${replyCount} 条未读`,
description: `<p>你有 <strong>${replyCount}</strong> 条未读回复消息</p><p><a href="https://message.bilibili.com/#/reply">点击查看</a></p>`,
link: 'https://message.bilibili.com/#/reply',
pubDate: now,
guid: `bilibili-unread-reply-${uid}-${replyCount}`,
});
}
// @我的
if (msgData.at > 0) {
items.push({
title: `@我的:${msgData.at} 条未读`,
description: `<p>你有 <strong>${msgData.at}</strong> 条未读@消息</p><p><a href="https://message.bilibili.com/#/at">点击查看</a></p>`,
link: 'https://message.bilibili.com/#/at',
pubDate: now,
guid: `bilibili-unread-at-${uid}-${msgData.at}`,
});
}
// 收到的赞
if (msgData.recv_like > 0 || msgData.like > 0) {
const likeCount = msgData.recv_like || msgData.like;
items.push({
title: `收到的赞:${likeCount} 条未读`,
description: `<p>你有 <strong>${likeCount}</strong> 条未读点赞消息</p><p><a href="https://message.bilibili.com/#/love">点击查看</a></p>`,
link: 'https://message.bilibili.com/#/love',
pubDate: now,
guid: `bilibili-unread-like-${uid}-${likeCount}`,
});
}
// 系统通知
if (msgData.sys_msg > 0) {
items.push({
title: `系统通知:${msgData.sys_msg} 条未读`,
description: `<p>你有 <strong>${msgData.sys_msg}</strong> 条未读系统通知</p><p><a href="https://message.bilibili.com/#/system">点击查看</a></p>`,
link: 'https://message.bilibili.com/#/system',
pubDate: now,
guid: `bilibili-unread-system-${uid}-${msgData.sys_msg}`,
});
}
// 私信
const privateUnread = (sessionData?.follow_unread || 0) + (sessionData?.unfollow_unread || 0);
if (privateUnread > 0) {
items.push({
title: `私信:${privateUnread} 条未读`,
description: `<p>你有 <strong>${privateUnread}</strong> 条未读私信(已关注: ${sessionData?.follow_unread || 0},未关注: ${sessionData?.unfollow_unread || 0}</p><p><a href="https://message.bilibili.com/#/whisper">点击查看</a></p>`,
link: 'https://message.bilibili.com/#/whisper',
pubDate: now,
guid: `bilibili-unread-session-${uid}-${privateUnread}`,
});
}
// 投币
if (msgData.coin > 0) {
items.push({
title: `收到的投币:${msgData.coin} 条未读`,
description: `<p>你有 <strong>${msgData.coin}</strong> 条未读投币消息</p>`,
link: 'https://message.bilibili.com/',
pubDate: now,
guid: `bilibili-unread-coin-${uid}-${msgData.coin}`,
});
}
// 收藏
if (msgData.favorite > 0) {
items.push({
title: `收到的收藏:${msgData.favorite} 条未读`,
description: `<p>你有 <strong>${msgData.favorite}</strong> 条未读收藏消息</p>`,
link: 'https://message.bilibili.com/',
pubDate: now,
guid: `bilibili-unread-favorite-${uid}-${msgData.favorite}`,
});
}
// UP主助手 messages
if (msgData.up > 0) {
items.push({
title: `UP主助手${msgData.up} 条未读`,
description: `<p>你有 <strong>${msgData.up}</strong> 条未读UP主助手消息</p>`,
link: 'https://message.bilibili.com/',
pubDate: now,
guid: `bilibili-unread-up-${uid}-${msgData.up}`,
});
}
return {
title: `${name} 的 B站未读消息`,
link: 'https://message.bilibili.com/',
description: `${name} 的 B站未读消息汇总`,
item: items,
allowEmpty: true,
};
}