diff --git a/src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-url-parser.ts b/src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-url-parser.ts index ce21357..04c7439 100644 --- a/src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-url-parser.ts +++ b/src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-url-parser.ts @@ -2,10 +2,10 @@ import type { BilibiliVideoReference } from './bilibili-card.types'; const BILIBILI_HOST = 'bilibili.com'; const B23_HOST = 'b23.tv'; -const TRAILING_WRAPPERS = /[\s"'<>,。!?、;:))\]}]+$/u; -const LEADING_WRAPPERS = /^[\s"'<>(([{]+/u; -const BVID_PATTERN = /(?:^|\/)(BV[0-9A-Za-z]{10,})/; -const AID_PATTERN = /(?:^|\/)(?:av|AV)(\d+)(?:$|[/?#])/; +const TRAILING_WRAPPERS = /[\s"'<>,。!?、;:))\]}】]+$/u; +const LEADING_WRAPPERS = /^[\s"'<>(([{【]+/u; +const BVID_PATTERN = /^BV[0-9A-Za-z]{10}$/; +const AID_PATTERN = /^(?:av|AV)(\d+)$/; /** * Removes QQ card wrappers and punctuation that often stick to copied URLs. @@ -17,6 +17,10 @@ export function cleanBilibiliUrlCandidate(candidate: string) { .replaceAll('&', '&') .replaceAll('"', '"') .replaceAll('"', '"') + .replaceAll('<', '<') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('>', '>') .replace(LEADING_WRAPPERS, '') .replace(TRAILING_WRAPPERS, '') .trim(); @@ -54,18 +58,23 @@ export function parseBilibiliVideoReference( if (!isAllowedBilibiliUrl(cleaned)) return null; const url = new URL(cleaned); - const probe = `${url.pathname}${url.search}${url.hash}`; - const bvid = probe.match(BVID_PATTERN)?.[1]; - if (bvid) { + const pathSegments = url.pathname.split('/').filter(Boolean); + const videoSegmentIndex = pathSegments.findIndex( + (segment) => segment.toLowerCase() === 'video', + ); + if (videoSegmentIndex < 0) return null; + + const videoIdSegment = pathSegments[videoSegmentIndex + 1]; + if (videoIdSegment && BVID_PATTERN.test(videoIdSegment)) { return { - canonicalVideoId: bvid, + canonicalVideoId: videoIdSegment, kind: 'bvid', sourceUrl: cleaned, - value: bvid, + value: videoIdSegment, }; } - const aid = `${url.pathname}/`.match(AID_PATTERN)?.[1]; + const aid = videoIdSegment?.match(AID_PATTERN)?.[1]; if (aid) { return { canonicalVideoId: `av${aid}`, diff --git a/test/modules/qqbot/plugins/bilibili-card/bilibili-url-parser.spec.ts b/test/modules/qqbot/plugins/bilibili-card/bilibili-url-parser.spec.ts index 03a7445..39479c1 100644 --- a/test/modules/qqbot/plugins/bilibili-card/bilibili-url-parser.spec.ts +++ b/test/modules/qqbot/plugins/bilibili-card/bilibili-url-parser.spec.ts @@ -44,4 +44,33 @@ describe('Bilibili URL parser', () => { ), ).toBe('https://www.bilibili.com/video/BV1xx411c7mD?p=1'); }); + + it('does not parse video ids from query or hash on non-video Bilibili pages', () => { + expect( + parseBilibiliVideoReference( + 'https://space.bilibili.com/1?from=/video/BV1xx411c7mD', + ), + ).toBeNull(); + expect( + parseBilibiliVideoReference( + 'https://www.bilibili.com/search?keyword=BV1xx411c7mD', + ), + ).toBeNull(); + }); + + it('rejects malformed BV path segments with extra characters', () => { + expect( + parseBilibiliVideoReference( + 'https://www.bilibili.com/video/BV1xx411c7mDextra', + ), + ).toBeNull(); + }); + + it('cleans xml html entities and full-width brackets', () => { + expect( + cleanBilibiliUrlCandidate( + '<【https://www.bilibili.com/video/BV1xx411c7mD】>', + ), + ).toBe('https://www.bilibili.com/video/BV1xx411c7mD'); + }); });