diff --git a/src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-url-extractor.ts b/src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-url-extractor.ts index aaf358e..ced520c 100644 --- a/src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-url-extractor.ts +++ b/src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-url-extractor.ts @@ -4,7 +4,8 @@ import { isAllowedBilibiliUrl, } from './bilibili-url-parser'; -const URL_PATTERN = /https?:\/\/[^\s<>"',;:!,。!?;、\]\)}]+/giu; +const URL_PATTERN = /https?:\/\/[^\s<>"',:!,。!?;、\]\)}]+/giu; +const HTML_ENTITY_BEFORE_SEMICOLON = /&(?:amp|quot|lt|gt|#34|#60|#62)$/iu; const MAX_DEPTH = 7; const MAX_NODES = 200; const MAX_STRINGS = 200; @@ -31,7 +32,9 @@ export function extractBilibiliUrls(input: BilibiliUrlExtractionInput) { for (const text of candidates) { for (const rawUrl of text.match(URL_PATTERN) || []) { - const cleaned = cleanBilibiliUrlCandidate(rawUrl); + const cleaned = cleanBilibiliUrlCandidate( + trimNonEntitySemicolonTail(rawUrl), + ); if (!isAllowedBilibiliUrl(cleaned) || seen.has(cleaned)) continue; seen.add(cleaned); output.push(cleaned); @@ -42,6 +45,20 @@ export function extractBilibiliUrls(input: BilibiliUrlExtractionInput) { return output; } +/** + * Removes semicolon-delimited CQ tail text while keeping common HTML entities intact. + * @param rawUrl - Raw URL token collected by the extractor regex. + * @returns URL token that can still be decoded by `cleanBilibiliUrlCandidate`. + */ +function trimNonEntitySemicolonTail(rawUrl: string) { + for (let index = 0; index < rawUrl.length; index += 1) { + if (rawUrl[index] !== ';') continue; + if (HTML_ENTITY_BEFORE_SEMICOLON.test(rawUrl.slice(0, index))) continue; + return rawUrl.slice(0, index); + } + return rawUrl; +} + /** * Collects string values that may contain links from text fields and nested QQ card objects. * @param input - Extraction input built from normalized message state. diff --git a/test/modules/qqbot/plugins/bilibili-card/bilibili-url-extractor.spec.ts b/test/modules/qqbot/plugins/bilibili-card/bilibili-url-extractor.spec.ts index 8859507..c5e595f 100644 --- a/test/modules/qqbot/plugins/bilibili-card/bilibili-url-extractor.spec.ts +++ b/test/modules/qqbot/plugins/bilibili-card/bilibili-url-extractor.spec.ts @@ -135,4 +135,36 @@ describe('Bilibili URL extractor', () => { }), ).toEqual(['https://b23.tv/abc123']); }); + + it('keeps html entity wrappers intact until URL cleanup runs', () => { + expect( + extractBilibiliUrls({ + messageText: '', + rawEvent: { + message: [ + { + data: { + data: '<【https://www.bilibili.com/video/BV1xx411c7mD】>', + }, + type: 'xml', + }, + ], + }, + rawMessage: '', + }), + ).toEqual(['https://www.bilibili.com/video/BV1xx411c7mD']); + }); + + it('preserves ampersand query entities while removing CQ separators', () => { + expect( + extractBilibiliUrls({ + messageText: + '[CQ:share,url=https://www.bilibili.com/video/BV1xx411c7mD?foo=1&bar=2,title=视频]', + rawEvent: {}, + rawMessage: '', + }), + ).toEqual([ + 'https://www.bilibili.com/video/BV1xx411c7mD?foo=1&bar=2', + ]); + }); });