feat: 添加Bilibili视频信息客户端
This commit is contained in:
parent
87522a4326
commit
2e98c430d4
@ -0,0 +1,69 @@
|
||||
import type {
|
||||
BilibiliCardPluginHost,
|
||||
BilibiliCardRuntimeConfig,
|
||||
} from '../domain/bilibili-card.types';
|
||||
|
||||
const CONFIG_RULES = {
|
||||
dedupeTtlMs: {
|
||||
defaultValue: 600000,
|
||||
key: 'QQBOT_BILIBILI_CARD_DEDUPE_TTL_MS',
|
||||
max: 3600000,
|
||||
min: 0,
|
||||
},
|
||||
descMaxLength: {
|
||||
defaultValue: 80,
|
||||
key: 'QQBOT_BILIBILI_CARD_DESC_MAX_LENGTH',
|
||||
max: 300,
|
||||
min: 0,
|
||||
},
|
||||
httpTimeoutMs: {
|
||||
defaultValue: 6000,
|
||||
key: 'QQBOT_BILIBILI_CARD_HTTP_TIMEOUT_MS',
|
||||
max: 15000,
|
||||
min: 1000,
|
||||
},
|
||||
maxRedirects: {
|
||||
defaultValue: 5,
|
||||
key: 'QQBOT_BILIBILI_CARD_MAX_REDIRECTS',
|
||||
max: 10,
|
||||
min: 0,
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Reads Bilibili card runtime config from the package host and clamps unsafe values.
|
||||
* @param host - Package-local host facade backed by the worker config snapshot.
|
||||
* @returns Runtime config used by redirect resolution, API requests, dedupe and reply text.
|
||||
*/
|
||||
export function readBilibiliCardRuntimeConfig(
|
||||
host: BilibiliCardPluginHost,
|
||||
): BilibiliCardRuntimeConfig {
|
||||
return {
|
||||
dedupeTtlMs: readClampedInteger(host, CONFIG_RULES.dedupeTtlMs),
|
||||
descMaxLength: readClampedInteger(host, CONFIG_RULES.descMaxLength),
|
||||
httpTimeoutMs: readClampedInteger(host, CONFIG_RULES.httpTimeoutMs),
|
||||
maxRedirects: readClampedInteger(host, CONFIG_RULES.maxRedirects),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads one numeric config value and constrains it to the allowed runtime range.
|
||||
* @param host - Package-local host facade that exposes manifest config values.
|
||||
* @param rule - Config key, fallback and inclusive bounds for one Bilibili setting.
|
||||
* @returns Integer value clamped between the rule's minimum and maximum.
|
||||
*/
|
||||
function readClampedInteger(
|
||||
host: BilibiliCardPluginHost,
|
||||
rule: {
|
||||
defaultValue: number;
|
||||
key: string;
|
||||
max: number;
|
||||
min: number;
|
||||
},
|
||||
) {
|
||||
const value = Number(host.getConfig(rule.key));
|
||||
const normalized = Number.isFinite(value)
|
||||
? Math.trunc(value)
|
||||
: rule.defaultValue;
|
||||
return Math.min(rule.max, Math.max(rule.min, normalized));
|
||||
}
|
||||
@ -17,3 +17,68 @@ export type BilibiliUrlExtractionInput = {
|
||||
rawEvent?: Record<string, unknown>;
|
||||
rawMessage?: string;
|
||||
};
|
||||
|
||||
export type BilibiliCardRuntimeConfig = {
|
||||
dedupeTtlMs: number;
|
||||
descMaxLength: number;
|
||||
httpTimeoutMs: number;
|
||||
maxRedirects: number;
|
||||
};
|
||||
|
||||
export type BilibiliCardHostJsonRequest = {
|
||||
context: string;
|
||||
failureMessage: (statusCode: number) => string;
|
||||
invalidJsonMessage: string;
|
||||
method?: string;
|
||||
timeoutMessage: string;
|
||||
timeoutMs: number;
|
||||
url: URL;
|
||||
};
|
||||
|
||||
export type BilibiliCardRedirectRequest = {
|
||||
context?: string;
|
||||
maxRedirects: number;
|
||||
timeoutMessage?: string;
|
||||
timeoutMs: number;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type BilibiliCardRedirectResult = {
|
||||
finalUrl: string;
|
||||
redirects: string[];
|
||||
};
|
||||
|
||||
export type BilibiliCardPluginHost = {
|
||||
getBoundEventPluginKeys: (selfId: string) => Promise<string[]>;
|
||||
getConfig: <T = string>(key: string) => T | undefined;
|
||||
requestJson: <T = unknown>(
|
||||
request: BilibiliCardHostJsonRequest,
|
||||
) => Promise<T>;
|
||||
resolveRedirect: (
|
||||
request: BilibiliCardRedirectRequest,
|
||||
) => Promise<BilibiliCardRedirectResult>;
|
||||
sendText: (input: {
|
||||
channelId?: string;
|
||||
guildId?: string;
|
||||
message: string;
|
||||
selfId: string;
|
||||
targetId: string;
|
||||
targetType: string;
|
||||
}) => Promise<unknown>;
|
||||
warn?: (message: string) => void;
|
||||
};
|
||||
|
||||
export type BilibiliVideoInfo = {
|
||||
aid: number;
|
||||
bvid: string;
|
||||
desc: string;
|
||||
duration: number;
|
||||
ownerName: string;
|
||||
pic: string;
|
||||
stat: {
|
||||
danmaku: number;
|
||||
like: number;
|
||||
view: number;
|
||||
};
|
||||
title: string;
|
||||
};
|
||||
|
||||
@ -0,0 +1,85 @@
|
||||
import type {
|
||||
BilibiliCardRuntimeConfig,
|
||||
BilibiliVideoInfo,
|
||||
} from './bilibili-card.types';
|
||||
|
||||
/**
|
||||
* Formats a Bilibili video summary as plain text for QQBot replies.
|
||||
* @param video - Normalized video info returned by the package-local Bilibili client.
|
||||
* @param config - Runtime config that controls the maximum displayed description length.
|
||||
* @returns Concise plain text reply with a canonical Bilibili video URL.
|
||||
*/
|
||||
export function formatBilibiliVideoReply(
|
||||
video: BilibiliVideoInfo,
|
||||
config: BilibiliCardRuntimeConfig,
|
||||
) {
|
||||
const lines = [
|
||||
'Bilibili 视频解析',
|
||||
`标题:${video.title || '未知标题'}`,
|
||||
`UP:${video.ownerName || '未知UP主'}`,
|
||||
`时长:${formatBilibiliDuration(video.duration)}`,
|
||||
`播放:${formatBilibiliStat(video.stat.view)} 弹幕:${formatBilibiliStat(
|
||||
video.stat.danmaku,
|
||||
)} 点赞:${formatBilibiliStat(video.stat.like)}`,
|
||||
`链接:${buildCanonicalBilibiliVideoUrl(video)}`,
|
||||
];
|
||||
const desc = truncateBilibiliDescription(video.desc, config.descMaxLength);
|
||||
if (desc) lines.push(`简介:${desc}`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the canonical public Bilibili video URL used in replies.
|
||||
* @param video - Normalized video info containing either a BV id or an av id fallback.
|
||||
* @returns Public Bilibili video URL that does not echo short links.
|
||||
*/
|
||||
function buildCanonicalBilibiliVideoUrl(video: BilibiliVideoInfo) {
|
||||
const videoId = video.bvid || `av${video.aid}`;
|
||||
return `https://www.bilibili.com/video/${videoId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats Bilibili stat counters using compact Chinese units.
|
||||
* @param value - Raw counter from the Bilibili video API response.
|
||||
* @returns Counter text such as `7890` or `12.3万`.
|
||||
*/
|
||||
function formatBilibiliStat(value: number) {
|
||||
const normalized = Math.max(0, Math.floor(value || 0));
|
||||
if (normalized < 10000) return `${normalized}`;
|
||||
const wan = normalized / 10000;
|
||||
const formatted = wan >= 100 ? `${Math.round(wan)}` : wan.toFixed(1);
|
||||
return `${formatted.replace(/\.0$/, '')}万`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a duration in seconds as `mm:ss` or `hh:mm:ss`.
|
||||
* @param seconds - Duration seconds from the Bilibili video API response.
|
||||
* @returns Padded duration suitable for compact message replies.
|
||||
*/
|
||||
function formatBilibiliDuration(seconds: number) {
|
||||
const normalized = Math.max(0, Math.floor(seconds || 0));
|
||||
const hours = Math.floor(normalized / 3600);
|
||||
const minutes = Math.floor((normalized % 3600) / 60);
|
||||
const restSeconds = normalized % 60;
|
||||
if (hours > 0) {
|
||||
return [hours, minutes, restSeconds]
|
||||
.map((item) => `${item}`.padStart(2, '0'))
|
||||
.join(':');
|
||||
}
|
||||
return [minutes, restSeconds]
|
||||
.map((item) => `${item}`.padStart(2, '0'))
|
||||
.join(':');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts multi-line descriptions into one compact line and applies the configured limit.
|
||||
* @param desc - Raw Bilibili description text from the video API response.
|
||||
* @param maxLength - Maximum number of visible characters configured for this plugin.
|
||||
* @returns Trimmed description, with an ellipsis when content was shortened.
|
||||
*/
|
||||
function truncateBilibiliDescription(desc: string, maxLength: number) {
|
||||
const normalized = desc.replace(/\s+/gu, ' ').trim();
|
||||
if (!normalized || maxLength <= 0) return '';
|
||||
if (normalized.length <= maxLength) return normalized;
|
||||
return `${normalized.slice(0, maxLength)}…`;
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
import type {
|
||||
BilibiliCardHostJsonRequest,
|
||||
BilibiliCardPluginHost,
|
||||
} from '../../domain/bilibili-card.types';
|
||||
|
||||
export type BilibiliCardGenericHost = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Creates the package-local host adapter over the generic worker host facade.
|
||||
* @param host - Generic worker host object supplied by the plugin platform runtime.
|
||||
* @param configSnapshot - Startup config values captured by the worker runtime.
|
||||
* @returns Package-local host contract used by Bilibili card domain and integration code.
|
||||
*/
|
||||
export function createBilibiliCardGenericHostAdapter(
|
||||
host: BilibiliCardGenericHost,
|
||||
configSnapshot: Record<string, string | undefined>,
|
||||
): BilibiliCardPluginHost {
|
||||
return {
|
||||
/**
|
||||
* Reads event plugin bindings through the generic host bridge.
|
||||
* @param selfId - QQBot account id from a normalized message event.
|
||||
* @returns Bound event plugin keys for the account.
|
||||
*/
|
||||
getBoundEventPluginKeys: async (selfId) =>
|
||||
await callBilibiliCardGenericHost(host, 'getBoundEventPluginKeys', selfId),
|
||||
/**
|
||||
* Reads Bilibili card config from the worker startup snapshot.
|
||||
* @param key - Runtime config key declared by the Bilibili card manifest.
|
||||
* @returns Snapshot value cast to the requested config type.
|
||||
*/
|
||||
getConfig: <T = string>(key: string) =>
|
||||
configSnapshot[key] as T | undefined,
|
||||
/**
|
||||
* Performs JSON HTTP requests through the generic host bridge.
|
||||
* @param request - Package-local HTTP request options from `BilibiliVideoClient`.
|
||||
* @returns Parsed JSON payload returned by the host HTTP client.
|
||||
*/
|
||||
requestJson: async <T = unknown>(request) =>
|
||||
await callBilibiliCardGenericHost<T>(
|
||||
host,
|
||||
'requestJson',
|
||||
serializeBilibiliCardJsonRequest(request),
|
||||
),
|
||||
/**
|
||||
* Resolves short-link redirects through the generic host bridge.
|
||||
* @param request - Redirect request containing URL, timeout and max redirect budget.
|
||||
* @returns Final URL and redirect chain returned by the host HTTP client.
|
||||
*/
|
||||
resolveRedirect: async (request) =>
|
||||
await callBilibiliCardGenericHost(host, 'resolveRedirect', request),
|
||||
/**
|
||||
* Sends a plain text QQBot reply through the generic host bridge.
|
||||
* @param input - Target conversation and message text produced by the plugin.
|
||||
* @returns Host send result.
|
||||
*/
|
||||
sendText: async (input) =>
|
||||
await callBilibiliCardGenericHost(host, 'sendText', input),
|
||||
/**
|
||||
* Emits a package warning through the generic host when available.
|
||||
* @param message - Warning message safe for platform logs.
|
||||
*/
|
||||
warn: (message) => {
|
||||
const warn = host.warn;
|
||||
if (typeof warn === 'function') warn(message);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls one generic host method and fails with a package-owned error when absent.
|
||||
* @param host - Generic worker host facade supplied by the platform runtime.
|
||||
* @param method - Host capability required by the Bilibili card adapter.
|
||||
* @param args - Positional arguments accepted by the host facade method.
|
||||
* @returns Host method result cast to the requested package-local type.
|
||||
*/
|
||||
export async function callBilibiliCardGenericHost<TResult = unknown>(
|
||||
host: BilibiliCardGenericHost,
|
||||
method: string,
|
||||
...args: unknown[]
|
||||
): Promise<TResult> {
|
||||
const fn = host[method];
|
||||
if (typeof fn !== 'function') {
|
||||
throw new Error(`Bilibili Card generic host 缺少 ${method}`);
|
||||
}
|
||||
return (await fn(...args)) as TResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts package-local JSON request options to worker-safe generic host request data.
|
||||
* @param request - HTTP request containing URL and function-based failure message.
|
||||
* @returns Serializable request data accepted by the generic host bridge.
|
||||
*/
|
||||
export function serializeBilibiliCardJsonRequest(
|
||||
request: BilibiliCardHostJsonRequest,
|
||||
) {
|
||||
const statusPlaceholder = 599;
|
||||
return {
|
||||
context: request.context,
|
||||
failureMessageTemplate: request
|
||||
.failureMessage(statusPlaceholder)
|
||||
.replaceAll(`${statusPlaceholder}`, '{statusCode}'),
|
||||
invalidJsonMessage: request.invalidJsonMessage,
|
||||
method: request.method,
|
||||
timeoutMessage: request.timeoutMessage,
|
||||
timeoutMs: request.timeoutMs,
|
||||
url: request.url.toString(),
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,142 @@
|
||||
import type {
|
||||
BilibiliCardPluginHost,
|
||||
BilibiliCardRuntimeConfig,
|
||||
BilibiliVideoInfo,
|
||||
BilibiliVideoReference,
|
||||
} from '../../domain/bilibili-card.types';
|
||||
|
||||
type BilibiliViewResponse = {
|
||||
code?: number;
|
||||
data?: {
|
||||
aid?: unknown;
|
||||
bvid?: unknown;
|
||||
desc?: unknown;
|
||||
duration?: unknown;
|
||||
owner?: {
|
||||
name?: unknown;
|
||||
};
|
||||
pic?: unknown;
|
||||
stat?: {
|
||||
danmaku?: unknown;
|
||||
like?: unknown;
|
||||
view?: unknown;
|
||||
};
|
||||
title?: unknown;
|
||||
};
|
||||
message?: unknown;
|
||||
};
|
||||
|
||||
export class BilibiliVideoClient {
|
||||
/**
|
||||
* Initializes the host-mediated Bilibili video client.
|
||||
* @param host - Package-local host facade that owns all external HTTP access.
|
||||
*/
|
||||
constructor(private readonly host: BilibiliCardPluginHost) {}
|
||||
|
||||
/**
|
||||
* Fetches and normalizes one Bilibili video through the official view endpoint.
|
||||
* @param reference - Parsed BV or av reference produced by the URL parser or redirect resolver.
|
||||
* @param config - Runtime config containing the host request timeout budget.
|
||||
* @returns Normalized video info safe for reply formatting.
|
||||
*/
|
||||
async fetchVideo(
|
||||
reference: BilibiliVideoReference,
|
||||
config: Pick<BilibiliCardRuntimeConfig, 'httpTimeoutMs'>,
|
||||
): Promise<BilibiliVideoInfo> {
|
||||
const response = await this.host.requestJson<BilibiliViewResponse>({
|
||||
context: 'Bilibili 视频信息获取',
|
||||
/**
|
||||
* Builds a readable HTTP failure message for host-mediated Bilibili API requests.
|
||||
* @param statusCode - HTTP status code reported by the generic host HTTP client.
|
||||
* @returns Chinese error text with the status code preserved.
|
||||
*/
|
||||
failureMessage: (statusCode) =>
|
||||
`Bilibili 视频信息获取失败:HTTP ${statusCode}`,
|
||||
invalidJsonMessage: 'Bilibili 视频信息返回不是合法 JSON',
|
||||
method: 'GET',
|
||||
timeoutMessage: 'Bilibili 视频信息获取超时',
|
||||
timeoutMs: config.httpTimeoutMs,
|
||||
url: buildBilibiliViewUrl(reference),
|
||||
});
|
||||
|
||||
if (response.code !== 0) {
|
||||
throw new Error(
|
||||
`Bilibili 视频信息获取失败:${normalizeBilibiliMessage(
|
||||
response.message,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return normalizeBilibiliVideoInfo(response.data, reference);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the official Bilibili web-interface view endpoint URL for a video reference.
|
||||
* @param reference - Parsed BV or av reference produced by URL parsing.
|
||||
* @returns Official Bilibili API URL with either `bvid` or `aid` query parameter.
|
||||
*/
|
||||
function buildBilibiliViewUrl(reference: BilibiliVideoReference) {
|
||||
const url = new URL('https://api.bilibili.com/x/web-interface/view');
|
||||
url.searchParams.set(reference.kind === 'bvid' ? 'bvid' : 'aid', reference.value);
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes the Bilibili API payload into the package-local video info contract.
|
||||
* @param data - Raw `data` object returned by Bilibili's view endpoint.
|
||||
* @param reference - Original parsed reference used as a fallback for missing identifiers.
|
||||
* @returns Video info with safe fallback strings and numbers.
|
||||
*/
|
||||
function normalizeBilibiliVideoInfo(
|
||||
data: BilibiliViewResponse['data'],
|
||||
reference: BilibiliVideoReference,
|
||||
): BilibiliVideoInfo {
|
||||
return {
|
||||
aid: readNumber(data?.aid, reference.kind === 'aid' ? Number(reference.value) : 0),
|
||||
bvid: readText(data?.bvid, reference.kind === 'bvid' ? reference.value : ''),
|
||||
desc: readText(data?.desc),
|
||||
duration: readNumber(data?.duration),
|
||||
ownerName: readText(data?.owner?.name, '未知UP主'),
|
||||
pic: readText(data?.pic),
|
||||
stat: {
|
||||
danmaku: readNumber(data?.stat?.danmaku),
|
||||
like: readNumber(data?.stat?.like),
|
||||
view: readNumber(data?.stat?.view),
|
||||
},
|
||||
title: readText(data?.title, '未知标题'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts arbitrary API text values to stable strings.
|
||||
* @param value - Unknown text-like field from the Bilibili API response.
|
||||
* @param fallback - Domain fallback used when the API field is absent or blank.
|
||||
* @returns Trimmed string or the supplied fallback.
|
||||
*/
|
||||
function readText(value: unknown, fallback = '') {
|
||||
const text = typeof value === 'string' ? value.trim() : '';
|
||||
return text || fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts arbitrary API number values to non-negative integers.
|
||||
* @param value - Unknown numeric field from the Bilibili API response.
|
||||
* @param fallback - Domain fallback used when the API field is missing or invalid.
|
||||
* @returns Non-negative integer suitable for duration, id or stat counters.
|
||||
*/
|
||||
function readNumber(value: unknown, fallback = 0) {
|
||||
const numberValue = typeof value === 'number' ? value : Number(value);
|
||||
return Number.isFinite(numberValue) && numberValue > 0
|
||||
? Math.floor(numberValue)
|
||||
: fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts Bilibili API error payloads to readable Chinese messages.
|
||||
* @param message - Raw `message` field returned by the Bilibili API.
|
||||
* @returns Trimmed message or a stable fallback.
|
||||
*/
|
||||
function normalizeBilibiliMessage(message: unknown) {
|
||||
return readText(message, '未知错误');
|
||||
}
|
||||
@ -0,0 +1,169 @@
|
||||
import { readBilibiliCardRuntimeConfig } from '../../../../../src/modules/qqbot/plugins/bilibili-card/src/config/bilibili-card-config';
|
||||
import type { BilibiliCardPluginHost } from '../../../../../src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-card.types';
|
||||
import { formatBilibiliVideoReply } from '../../../../../src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-reply-formatter';
|
||||
import { BilibiliVideoClient } from '../../../../../src/modules/qqbot/plugins/bilibili-card/src/infrastructure/integration/bilibili-video-client';
|
||||
|
||||
describe('Bilibili video client', () => {
|
||||
it('fetches a BV video through the plugin host and normalizes the response', async () => {
|
||||
const host = createHost({
|
||||
requestJson: jest.fn().mockResolvedValue({
|
||||
code: 0,
|
||||
data: {
|
||||
aid: 170001,
|
||||
bvid: 'BV1xx411c7mD',
|
||||
desc: '第一行\n第二行',
|
||||
duration: 125,
|
||||
owner: { name: 'UP主' },
|
||||
pic: 'https://i0.hdslb.com/bfs/archive/demo.jpg',
|
||||
stat: {
|
||||
danmaku: 456,
|
||||
like: 7890,
|
||||
view: 123456,
|
||||
},
|
||||
title: '夏祭',
|
||||
},
|
||||
}),
|
||||
});
|
||||
const client = new BilibiliVideoClient(host);
|
||||
|
||||
await expect(
|
||||
client.fetchVideo(
|
||||
{
|
||||
canonicalVideoId: 'BV1xx411c7mD',
|
||||
kind: 'bvid',
|
||||
sourceUrl: 'https://www.bilibili.com/video/BV1xx411c7mD',
|
||||
value: 'BV1xx411c7mD',
|
||||
},
|
||||
readBilibiliCardRuntimeConfig(host),
|
||||
),
|
||||
).resolves.toMatchObject({
|
||||
aid: 170001,
|
||||
bvid: 'BV1xx411c7mD',
|
||||
duration: 125,
|
||||
ownerName: 'UP主',
|
||||
title: '夏祭',
|
||||
});
|
||||
|
||||
expect(host.requestJson).toHaveBeenCalledTimes(1);
|
||||
const request = (host.requestJson as jest.Mock).mock.calls[0][0];
|
||||
expect(request.url.toString()).toBe(
|
||||
'https://api.bilibili.com/x/web-interface/view?bvid=BV1xx411c7mD',
|
||||
);
|
||||
expect(request.timeoutMs).toBe(6000);
|
||||
expect(request.context).toBe('Bilibili 视频信息获取');
|
||||
expect(request.failureMessage(502)).toBe(
|
||||
'Bilibili 视频信息获取失败:HTTP 502',
|
||||
);
|
||||
expect(request.invalidJsonMessage).toBe(
|
||||
'Bilibili 视频信息返回不是合法 JSON',
|
||||
);
|
||||
});
|
||||
|
||||
it('throws a readable error when Bilibili returns an error code', async () => {
|
||||
const host = createHost({
|
||||
requestJson: jest.fn().mockResolvedValue({
|
||||
code: -404,
|
||||
message: '啥都木有',
|
||||
}),
|
||||
});
|
||||
const client = new BilibiliVideoClient(host);
|
||||
|
||||
await expect(
|
||||
client.fetchVideo(
|
||||
{
|
||||
canonicalVideoId: 'av170001',
|
||||
kind: 'aid',
|
||||
sourceUrl: 'https://m.bilibili.com/video/av170001',
|
||||
value: '170001',
|
||||
},
|
||||
readBilibiliCardRuntimeConfig(host),
|
||||
),
|
||||
).rejects.toThrow('Bilibili 视频信息获取失败:啥都木有');
|
||||
|
||||
const request = (host.requestJson as jest.Mock).mock.calls[0][0];
|
||||
expect(request.url.toString()).toBe(
|
||||
'https://api.bilibili.com/x/web-interface/view?aid=170001',
|
||||
);
|
||||
});
|
||||
|
||||
it('formats a concise text reply without echoing short links', () => {
|
||||
expect(
|
||||
formatBilibiliVideoReply(
|
||||
{
|
||||
aid: 170001,
|
||||
bvid: 'BV1xx411c7mD',
|
||||
desc: '第一行\n第二行',
|
||||
duration: 125,
|
||||
ownerName: 'UP主',
|
||||
pic: 'https://i0.hdslb.com/bfs/archive/demo.jpg',
|
||||
stat: {
|
||||
danmaku: 456,
|
||||
like: 7890,
|
||||
view: 123456,
|
||||
},
|
||||
title: '夏祭',
|
||||
},
|
||||
{
|
||||
dedupeTtlMs: 600000,
|
||||
descMaxLength: 6,
|
||||
httpTimeoutMs: 6000,
|
||||
maxRedirects: 5,
|
||||
},
|
||||
),
|
||||
).toBe(
|
||||
[
|
||||
'Bilibili 视频解析',
|
||||
'标题:夏祭',
|
||||
'UP:UP主',
|
||||
'时长:02:05',
|
||||
'播放:12.3万 弹幕:456 点赞:7890',
|
||||
'链接:https://www.bilibili.com/video/BV1xx411c7mD',
|
||||
'简介:第一行 第二…',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('clamps runtime config values from the plugin host', () => {
|
||||
const host = createHost({
|
||||
/**
|
||||
* Reads test config values by Bilibili card runtime key.
|
||||
* @param key - Runtime config key requested by `readBilibiliCardRuntimeConfig`.
|
||||
* @returns String value supplied by the current clamp scenario.
|
||||
*/
|
||||
getConfig: (<T = string>(key: string) => {
|
||||
return {
|
||||
QQBOT_BILIBILI_CARD_DEDUPE_TTL_MS: '9999999',
|
||||
QQBOT_BILIBILI_CARD_DESC_MAX_LENGTH: '-1',
|
||||
QQBOT_BILIBILI_CARD_HTTP_TIMEOUT_MS: '10',
|
||||
QQBOT_BILIBILI_CARD_MAX_REDIRECTS: '99',
|
||||
}[key] as T | undefined;
|
||||
}) as BilibiliCardPluginHost['getConfig'],
|
||||
});
|
||||
|
||||
expect(readBilibiliCardRuntimeConfig(host)).toEqual({
|
||||
dedupeTtlMs: 3600000,
|
||||
descMaxLength: 0,
|
||||
httpTimeoutMs: 1000,
|
||||
maxRedirects: 10,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Builds the package-local Bilibili host contract used by client and config tests.
|
||||
* @param overrides - Test doubles for host methods involved in the current assertion.
|
||||
* @returns Host object with harmless defaults for unused plugin capabilities.
|
||||
*/
|
||||
function createHost(
|
||||
overrides: Partial<BilibiliCardPluginHost> = {},
|
||||
): BilibiliCardPluginHost {
|
||||
return {
|
||||
getBoundEventPluginKeys: jest.fn().mockResolvedValue([]),
|
||||
getConfig: jest.fn(),
|
||||
requestJson: jest.fn(),
|
||||
resolveRedirect: jest.fn(),
|
||||
sendText: jest.fn().mockResolvedValue(undefined),
|
||||
warn: jest.fn(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user