diff --git a/CHANGELOG.md b/CHANGELOG.md index 3151b5ca9c5..d963c56766b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- QQBot/media-tags: support HTML entity-encoded angle brackets (`<`/`>`) in media-tag regexes so entity-escaped `` tags from upstream are correctly parsed and normalized. (#60493) Thanks @ylc0919. - Slack/media: preserve bearer auth across same-origin `files.slack.com` redirects while still stripping it on cross-origin Slack CDN hops, so `url_private_download` image attachments load again. (#62960) Thanks @vincentkoc. - Control UI: guard stale session-history reloads during fast session switches so the selected session and rendered transcript stay in sync. (#62975) Thanks @scoootscooob. diff --git a/extensions/qqbot/src/utils/media-tags.test.ts b/extensions/qqbot/src/utils/media-tags.test.ts new file mode 100644 index 00000000000..47f1f67c43f --- /dev/null +++ b/extensions/qqbot/src/utils/media-tags.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest'; +import { FUZZY_MEDIA_TAG_REGEX, SELF_CLOSING_TAG_REGEX } from './media-tags.js'; + +describe('media-tags with HTML entities', () => { + it('extracts URL from entity-encoded fuzzy tag', () => { + const input = '<qqimg>https://example.com/a.png</qqimg>'; + FUZZY_MEDIA_TAG_REGEX.lastIndex = 0; + const match = FUZZY_MEDIA_TAG_REGEX.exec(input); + expect(match?.[2]).toBe('https://example.com/a.png'); + }); + + it('extracts URL from mixed entity+plain tag', () => { + const input = '<qqimg>https://example.com/b.png'; + FUZZY_MEDIA_TAG_REGEX.lastIndex = 0; + const match = FUZZY_MEDIA_TAG_REGEX.exec(input); + expect(match?.[2]).toBe('https://example.com/b.png'); + }); + + it('extracts file from entity-encoded self-closing tag', () => { + const input = '<qqmedia file="https://example.com/c.zip" />'; + SELF_CLOSING_TAG_REGEX.lastIndex = 0; + const match = SELF_CLOSING_TAG_REGEX.exec(input); + expect(match?.[2]).toBe('https://example.com/c.zip'); + }); + + it('does not match invalid input', () => { + const input = 'no tag here'; + FUZZY_MEDIA_TAG_REGEX.lastIndex = 0; + const match = FUZZY_MEDIA_TAG_REGEX.exec(input); + expect(match).toBeNull(); + }); +}); diff --git a/extensions/qqbot/src/utils/media-tags.ts b/extensions/qqbot/src/utils/media-tags.ts index 1940f504ff2..071f6618feb 100644 --- a/extensions/qqbot/src/utils/media-tags.ts +++ b/extensions/qqbot/src/utils/media-tags.ts @@ -49,10 +49,12 @@ ALL_TAG_NAMES.sort((a, b) => b.length - a.length); const TAG_NAME_PATTERN = ALL_TAG_NAMES.join("|"); +const LEFT_BRACKET = "(?:[<<<]|<)"; +const RIGHT_BRACKET = "(?:[>>>]|>)"; /** Match self-closing media-tag syntax with file/src/path/url attributes. */ -const SELF_CLOSING_TAG_REGEX = new RegExp( +export const SELF_CLOSING_TAG_REGEX = new RegExp( "`?" + - "[<<<]\\s*(" + + LEFT_BRACKET + "\\s*(" + TAG_NAME_PATTERN + ")" + "(?:\\s+(?!file|src|path|url)[a-z_-]+\\s*=\\s*[\"']?[^\"'/>>>]*?[\"']?)*" + @@ -62,23 +64,23 @@ const SELF_CLOSING_TAG_REGEX = new RegExp( "[\"']?" + "(?:\\s+[a-z_-]+\\s*=\\s*[\"']?[^\"'/>>>]*?[\"']?)*" + "\\s*/?" + - "\\s*[>>>]" + + "\\s*" + RIGHT_BRACKET + "`?", "gi", ); /** Match malformed wrapped media tags that should be normalized. */ -const FUZZY_MEDIA_TAG_REGEX = new RegExp( +export const FUZZY_MEDIA_TAG_REGEX = new RegExp( "`?" + - "[<<<]\\s*(" + + LEFT_BRACKET + "\\s*(" + TAG_NAME_PATTERN + - ")\\s*[>>>]" + + ")\\s*" + RIGHT_BRACKET + "[\"']?\\s*" + "([^<<<>>\"'`]+?)" + "\\s*[\"']?" + - "[<<<]\\s*/?\\s*(?:" + + LEFT_BRACKET + "\\s*/?\\s*(?:" + TAG_NAME_PATTERN + - ")\\s*[>>>]" + + ")\\s*" + RIGHT_BRACKET + "`?", "gi", ); @@ -94,13 +96,13 @@ function resolveTagName(raw: string): (typeof VALID_TAGS)[number] { /** Match wrapped tags whose bodies need newline and tab cleanup. */ const MULTILINE_TAG_CLEANUP = new RegExp( - "([<<<]\\s*(?:" + + "(" + LEFT_BRACKET + "\\s*(?:" + TAG_NAME_PATTERN + - ")\\s*[>>>])" + + ")\\s*" + RIGHT_BRACKET + ")" + "([\\s\\S]*?)" + - "([<<<]\\s*/?\\s*(?:" + + "(" + LEFT_BRACKET + "\\s*/?\\s*(?:" + TAG_NAME_PATTERN + - ")\\s*[>>>])", + ")\\s*" + RIGHT_BRACKET + ")", "gi", );