From 8371ebf2fb636d71ae983d9e54c7050bfa2fc66e Mon Sep 17 00:00:00 2001 From: baiyu-yu <135424680+baiyu-yu@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:44:34 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=E5=85=81=E8=AE=B8render=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=B8=B2=E6=9F=93url=E5=92=8Cbase64=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tool/tool_render.ts | 165 ++++++++++++++++-- .../render.js" | 18 +- 2 files changed, 165 insertions(+), 18 deletions(-) diff --git a/src/tool/tool_render.ts b/src/tool/tool_render.ts index 2c8994f..51f396b 100644 --- a/src/tool/tool_render.ts +++ b/src/tool/tool_render.ts @@ -33,14 +33,138 @@ async function postToRenderEndpoint(endpoint: string, bodyData: any): Promise { + const errors: string[] = []; + let processedContent = content; + let hasImages = false; + const match = content.match(/[<<][\|│|]img:.+?(?:[\|│|][>>]|[\|│|>>])/g); + + if (!match) return { processedContent, errors, hasImages }; + + const uniqueRefs = [...new Set(match)]; + + for (const imgRef of uniqueRefs) { + const idMatch = imgRef.match(/[<<][\|│|]img:(.+?)(?:[\|│|][>>]|[\|│|>>])/); + if (!idMatch) continue; + + const id = idMatch[1].trim(); + const image = ai.context.findImage(ctx, id); + + if (!image) { + errors.push(`未找到图片<|img:${id}|>`); + continue; + } + + if (image.type === 'local' ) { + errors.push(`图片<|img:${id}|>为本地图片,暂不支持`); + continue; + } + + let imgUrl = ''; + if (image.type === 'base64') { + const format = image.format || 'png'; + imgUrl = `data:image/${format};base64,${image.base64}`; + hasImages = true; + } else if (image.type === 'url') { + imgUrl = image.file; + hasImages = true; + } + + if (!imgUrl) continue; + + const escapedRef = imgRef.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const mdSyntaxRegex = new RegExp(`(\\[.*?\\]\\(\\s*)${escapedRef}(\\s*\\))`, 'g'); + if (mdSyntaxRegex.test(processedContent)) { + processedContent = processedContent.replace(mdSyntaxRegex, `$1${imgUrl}$2`); + } + + const htmlSrcRegex = new RegExp(`(src\\s*=\\s*['"]\\s*)${escapedRef}(\\s*['"])`, 'g'); + if (htmlSrcRegex.test(processedContent)) { + processedContent = processedContent.replace(htmlSrcRegex, `$1${imgUrl}$2`); + } + + const standaloneRegex = new RegExp(escapedRef, 'g'); + if (renderMode === 'markdown') { + processedContent = processedContent.replace(standaloneRegex, `![image](${imgUrl})`); + } else { + processedContent = processedContent.replace(standaloneRegex, ``); + } + } + + return { processedContent, errors, hasImages }; +} + +//替换内容中的头像标签 +async function replaceAvatarReferencesInContent(ctx: seal.MsgContext, ai: any, content: string, renderMode: 'markdown' | 'html'): Promise<{ processedContent: string, errors: string[], hasImages: boolean }> { + const errors: string[] = []; + let processedContent = content; + let hasImages = false; + + const avatarMatch = content.match(/[<<][\|│|]avatar:(private|group):(.+?)(?:[\|│|][>>]|[\|│|>>])/g); + + if (!avatarMatch) return { processedContent, errors, hasImages }; + + const uniqueRefs = [...new Set(avatarMatch)]; + + for (const avatarRef of uniqueRefs) { + const match = avatarRef.match(/[<<][\|│|]avatar:(private|group):(.+?)(?:[\|│|][>>]|[\|│|>>])/); + if (!match) continue; + + const avatarType = match[1]; + const name = match[2].trim(); + + let url = ''; + if (avatarType === 'private') { + const uid = await ai.context.findUserId(ctx, name, true); + if (uid === null) { + errors.push(`未找到用户<${name}>,无法获取头像`); + continue; + } + url = `https://q1.qlogo.cn/g?b=qq&nk=${uid.replace(/^.+:/, '')}&s=640`; + } else if (avatarType === 'group') { + const gid = await ai.context.findGroupId(ctx, name); + if (gid === null) { + errors.push(`未找到群聊<${name}>,无法获取头像`); + continue; + } + url = `https://p.qlogo.cn/gh/${gid.replace(/^.+:/, '')}/${gid.replace(/^.+:/, '')}/640`; + } + + if (url) { + hasImages = true; + const escapedRef = avatarRef.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + const mdSyntaxRegex = new RegExp(`(\\[.*?\\]\\(\\s*)${escapedRef}(\\s*\\))`, 'g'); + if (mdSyntaxRegex.test(processedContent)) { + processedContent = processedContent.replace(mdSyntaxRegex, `$1${url}$2`); + } + + const htmlSrcRegex = new RegExp(`(src\\s*=\\s*['"]\\s*)${escapedRef}(\\s*['"])`, 'g'); + if (htmlSrcRegex.test(processedContent)) { + processedContent = processedContent.replace(htmlSrcRegex, `$1${url}$2`); + } + + const standaloneRegex = new RegExp(escapedRef, 'g'); + if (renderMode === 'markdown') { + processedContent = processedContent.replace(standaloneRegex, `![avatar](${url})`); + } else { + processedContent = processedContent.replace(standaloneRegex, ``); + } + } + } + + return { processedContent, errors, hasImages }; +} + // Markdown 渲染 -async function renderMarkdown(markdown: string, theme: 'light' | 'dark' | 'gradient' = 'light', width = 1200) { - return postToRenderEndpoint('/render/markdown', { markdown, theme, width, quality: 90 }); +async function renderMarkdown(markdown: string, theme: 'light' | 'dark' | 'gradient' = 'light', width = 1200, hasImages = false) { + return postToRenderEndpoint('/render/markdown', { markdown, theme, width, quality: 90, hasImages }); } // HTML 渲染 -async function renderHtml(html: string, width = 1200) { - return postToRenderEndpoint('/render/html', { html, width, quality: 90 }); +async function renderHtml(html: string, width = 1200, hasImages = false) { + return postToRenderEndpoint('/render/html', { html, width, quality: 90, hasImages }); } export function registerRender() { @@ -54,7 +178,7 @@ export function registerRender() { properties: { content: { type: "string", - description: "要渲染的 Markdown 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式" + description: "要渲染的 Markdown 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式。可以使用<|img:xxxxxx|>引用图片(xxxxxx为6位图片ID,不支持本地图片)。可以使用<|avatar:private:name|>或<|avatar:group:name|>引用头像" }, name: { type: "string", @@ -87,7 +211,17 @@ export function registerRender() { const kws = ["render", "markdown", name, theme]; try { - const result = await renderMarkdown(content, theme, 1200); + const { processedContent: contentWithImages, errors: imageErrors, hasImages: hasImageRefs } = await replaceImageReferencesInContent(ctx, ai, content, 'markdown'); + const { processedContent: finalContent, errors: avatarErrors, hasImages: hasAvatarRefs } = await replaceAvatarReferencesInContent(ctx, ai, contentWithImages, 'markdown'); + + const allErrors = [...imageErrors, ...avatarErrors]; + if (allErrors.length > 0) { + return { content: allErrors.join('\n'), images: [] }; + } + + const hasImages = hasImageRefs || hasAvatarRefs; + + const result = await renderMarkdown(finalContent, theme, 1200, hasImages); if (result.status === "success" && result.base64) { const base64 = result.base64; if (!base64) { @@ -124,7 +258,7 @@ export function registerRender() { properties: { content: { type: "string", - description: "要渲染的 HTML 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式。" + description: "要渲染的 HTML 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式。可以使用<|img:xxxxxx|>引用图片(xxxxxx为图片ID,不支持本地图片)。可以使用<|avatar:private:name|>或<|avatar:group:name|>引用头像" }, name: { type: "string", @@ -151,7 +285,17 @@ export function registerRender() { const kws = ["render", "html", name]; try { - const result = await renderHtml(content, 1200); + const { processedContent: contentWithImages, errors: imageErrors, hasImages: hasImageRefs } = await replaceImageReferencesInContent(ctx, ai, content, 'html'); + const { processedContent: finalContent, errors: avatarErrors, hasImages: hasAvatarRefs } = await replaceAvatarReferencesInContent(ctx, ai, contentWithImages, 'html'); + + const allErrors = [...imageErrors, ...avatarErrors]; + if (allErrors.length > 0) { + return { content: allErrors.join('\n'), images: [] }; + } + + const hasImages = hasImageRefs || hasAvatarRefs; + + const result = await renderHtml(finalContent, 1200, hasImages); if (result.status === "success" && result.base64) { const base64 = result.base64; if (!base64) { @@ -178,7 +322,4 @@ export function registerRender() { } } -// TODO:嵌入图片…… -// 1. 嵌入本地图片 -// 2. 嵌入网络图片,包括聊天记录,用户头像,群头像,直接使用url -// 3. 嵌入base64图片 \ No newline at end of file +// TODO:嵌入本地图片 diff --git "a/\347\233\270\345\205\263\345\220\216\347\253\257\351\241\271\347\233\256/md\345\222\214html\345\233\276\347\211\207\346\270\262\346\237\223/render.js" "b/\347\233\270\345\205\263\345\220\216\347\253\257\351\241\271\347\233\256/md\345\222\214html\345\233\276\347\211\207\346\270\262\346\237\223/render.js" index 6f43c76..9eff290 100644 --- "a/\347\233\270\345\205\263\345\220\216\347\253\257\351\241\271\347\233\256/md\345\222\214html\345\233\276\347\211\207\346\270\262\346\237\223/render.js" +++ "b/\347\233\270\345\205\263\345\220\216\347\253\257\351\241\271\347\233\256/md\345\222\214html\345\233\276\347\211\207\346\270\262\346\237\223/render.js" @@ -263,7 +263,8 @@ async function renderToImage(content, options = {}) { theme = 'light', style = 'github', width = 1200, - quality = 90 + quality = 90, + hasImages = false } = options; const browser = await puppeteer.launch({ @@ -287,9 +288,12 @@ async function renderToImage(content, options = {}) { const html = generateHTML(content, contentType, theme, style); + // 如果有图片,增加超时时间(图片加载需要更长时间) + const timeout = hasImages ? 60000 : 30000; + await page.setContent(html, { waitUntil: 'networkidle0', - timeout: 30000 + timeout: timeout }); await new Promise(r => setTimeout(r, 1500)); @@ -362,7 +366,7 @@ async function renderToImage(content, options = {}) { // 渲染 Markdown app.post('/render/markdown', async (req, res) => { try { - const { markdown, theme = 'light', width = 1200, quality = 90 } = req.body; + const { markdown, theme = 'light', width = 1200, quality = 90, hasImages = false } = req.body; if (!markdown) { return res.status(400).json({ status: 'error', message: 'Field "markdown" is required' }); } @@ -371,7 +375,8 @@ app.post('/render/markdown', async (req, res) => { contentType: 'markdown', theme, width, - quality + quality, + hasImages }); res.json({ @@ -390,7 +395,7 @@ app.post('/render/markdown', async (req, res) => { // 渲染 HTML app.post('/render/html', async (req, res) => { try { - const { html, width = 1200, quality = 90 } = req.body; + const { html, width = 1200, quality = 90, hasImages = false } = req.body; if (!html) { return res.status(400).json({ status: 'error', message: 'Field "html" is required' }); } @@ -398,7 +403,8 @@ app.post('/render/html', async (req, res) => { const result = await renderToImage(html, { contentType: 'html', width, - quality + quality, + hasImages }); res.json({ From d42cb676754436121858bca292f0a6a793a23693 Mon Sep 17 00:00:00 2001 From: baiyu-yu <135424680+baiyu-yu@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:33:46 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix:=E6=B7=BB=E5=8A=A0=E5=AF=B9html?= =?UTF-8?q?=E4=B8=AD=E5=9B=BE=E7=89=87=E4=BD=9C=E4=B8=BA=E8=83=8C=E6=99=AF?= =?UTF-8?q?=E7=9A=84=E6=9B=BF=E6=8D=A2=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tool/tool_render.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/tool/tool_render.ts b/src/tool/tool_render.ts index 51f396b..8b8adee 100644 --- a/src/tool/tool_render.ts +++ b/src/tool/tool_render.ts @@ -84,6 +84,12 @@ async function replaceImageReferencesInContent(ctx: seal.MsgContext, ai: any, co processedContent = processedContent.replace(htmlSrcRegex, `$1${imgUrl}$2`); } + // 处理背景图片 + const htmlBgImageRegex = new RegExp(`(background-image:\\s*url\\(['"]\\s*)${escapedRef}(\\s*['"]\\))`, 'g'); + if (htmlBgImageRegex.test(processedContent)) { + processedContent = processedContent.replace(htmlBgImageRegex, `$1${imgUrl}$2`); + } + const standaloneRegex = new RegExp(escapedRef, 'g'); if (renderMode === 'markdown') { processedContent = processedContent.replace(standaloneRegex, `![image](${imgUrl})`); @@ -145,6 +151,12 @@ async function replaceAvatarReferencesInContent(ctx: seal.MsgContext, ai: any, c processedContent = processedContent.replace(htmlSrcRegex, `$1${url}$2`); } + // 处理背景图片 + const htmlBgImageRegex = new RegExp(`(background-image:\\s*url\\(['"]\\s*)${escapedRef}(\\s*['"]\\))`, 'g'); + if (htmlBgImageRegex.test(processedContent)) { + processedContent = processedContent.replace(htmlBgImageRegex, `$1${url}$2`); + } + const standaloneRegex = new RegExp(escapedRef, 'g'); if (renderMode === 'markdown') { processedContent = processedContent.replace(standaloneRegex, `![avatar](${url})`); From 9733c48581e6a10edda284ec00421249ae597336 Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Fri, 28 Nov 2025 01:17:34 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E6=94=B9=E5=9B=9E=E7=AC=AC=E4=B8=80?= =?UTF-8?q?=E7=89=88=EF=BC=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/AI/image.ts | 17 +++- src/tool/tool_image.ts | 7 +- src/tool/tool_meme.ts | 2 +- src/tool/tool_render.ts | 198 +++++++++----------------------------- src/utils/utils_string.ts | 2 +- 5 files changed, 66 insertions(+), 160 deletions(-) diff --git a/src/AI/image.ts b/src/AI/image.ts index 991ed40..b5e5258 100644 --- a/src/AI/image.ts +++ b/src/AI/image.ts @@ -43,6 +43,19 @@ export class Image { return `[CQ:image,file=${file}]`; } + get base64Url(): string { + let format = this.format; + if (!format || format === "unknown") format = 'png'; + return `data:image/${format};base64,${this.base64}` + } + + /** + * 获取图片的URL,若为base64则返回base64Url + */ + get url(): string { + return this.type === 'base64' ? this.base64Url : this.file; + } + async checkImageUrl(): Promise { if (this.type !== 'url') return true; let isValid = false; @@ -111,7 +124,7 @@ export class Image { role: "user", content: [{ "type": "image_url", - "image_url": { "url": this.type === 'base64' ? `data:image/${this.format};base64,${this.base64}` : this.file } + "image_url": { "url": this.url } }, { "type": "text", "text": prompt ? prompt : defaultPrompt @@ -123,7 +136,7 @@ export class Image { if (!this.content && urlToBase64 === '自动' && this.type === 'url') { logger.info(`图片${this.id}第一次识别失败,自动尝试使用转换为base64`); await this.urlToBase64(); - messages[0].content[0].image_url.url = `data:image/${this.format};base64,${this.base64}`; + messages[0].content[0].image_url.url = this.base64Url; this.content = (await sendITTRequest(messages)).slice(0, maxChars); } diff --git a/src/tool/tool_image.ts b/src/tool/tool_image.ts index 003fe5b..f0f938b 100644 --- a/src/tool/tool_image.ts +++ b/src/tool/tool_image.ts @@ -13,7 +13,7 @@ export function registerImage() { properties: { id: { type: "string", - description: `图片的id,六位字符,或user_avatar:用户名称` + (ConfigManager.message.showNumber ? '或纯数字QQ号' : '') + `,或group_avatar:群聊名称` + (ConfigManager.message.showNumber ? '或纯数字群号' : '') + description: `图片id,或user_avatar:用户名称` + (ConfigManager.message.showNumber ? '或纯数字QQ号' : '') + `,或group_avatar:群聊名称` + (ConfigManager.message.showNumber ? '或纯数字群号' : '') }, content: { type: "string", @@ -74,4 +74,7 @@ export function registerImage() { return { content: `图像生成失败:${e}`, images: [] }; } } -} \ No newline at end of file +} + +// TODO: tti改为返回图片base64 +// 注意兼容问题 \ No newline at end of file diff --git a/src/tool/tool_meme.ts b/src/tool/tool_meme.ts index 4bbf53a..2cfef4a 100644 --- a/src/tool/tool_meme.ts +++ b/src/tool/tool_meme.ts @@ -101,7 +101,7 @@ export function registerMeme() { image_ids: { type: "array", items: { type: "string" }, - description: `图片的id,六位字符,或user_avatar:用户名称` + (ConfigManager.message.showNumber ? '或纯数字QQ号' : '') + `,或group_avatar:群聊名称` + (ConfigManager.message.showNumber ? '或纯数字群号' : '') + description: `图片id,或user_avatar:用户名称` + (ConfigManager.message.showNumber ? '或纯数字QQ号' : '') + `,或group_avatar:群聊名称` + (ConfigManager.message.showNumber ? '或纯数字群号' : '') }, save: { type: "boolean", diff --git a/src/tool/tool_render.ts b/src/tool/tool_render.ts index 8b8adee..334fa8a 100644 --- a/src/tool/tool_render.ts +++ b/src/tool/tool_render.ts @@ -1,9 +1,10 @@ import { logger } from "../logger"; import { Tool } from "./tool"; import { ConfigManager } from "../config/configManager"; -import { AIManager } from "../AI/AI"; +import { AI, AIManager } from "../AI/AI"; import { Image } from "../AI/image"; import { generateId } from "../utils/utils"; +import { parseSpecialTokens } from "../utils/utils_string"; interface RenderResponse { status: string; @@ -33,140 +34,43 @@ async function postToRenderEndpoint(endpoint: string, bodyData: any): Promise { - const errors: string[] = []; - let processedContent = content; - let hasImages = false; - const match = content.match(/[<<][\|│|]img:.+?(?:[\|│|][>>]|[\|│|>>])/g); - - if (!match) return { processedContent, errors, hasImages }; - - const uniqueRefs = [...new Set(match)]; - - for (const imgRef of uniqueRefs) { - const idMatch = imgRef.match(/[<<][\|│|]img:(.+?)(?:[\|│|][>>]|[\|│|>>])/); - if (!idMatch) continue; - - const id = idMatch[1].trim(); - const image = ai.context.findImage(ctx, id); - - if (!image) { - errors.push(`未找到图片<|img:${id}|>`); - continue; - } - - if (image.type === 'local' ) { - errors.push(`图片<|img:${id}|>为本地图片,暂不支持`); - continue; - } - - let imgUrl = ''; - if (image.type === 'base64') { - const format = image.format || 'png'; - imgUrl = `data:image/${format};base64,${image.base64}`; - hasImages = true; - } else if (image.type === 'url') { - imgUrl = image.file; - hasImages = true; - } - - if (!imgUrl) continue; - - const escapedRef = imgRef.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const mdSyntaxRegex = new RegExp(`(\\[.*?\\]\\(\\s*)${escapedRef}(\\s*\\))`, 'g'); - if (mdSyntaxRegex.test(processedContent)) { - processedContent = processedContent.replace(mdSyntaxRegex, `$1${imgUrl}$2`); - } - - const htmlSrcRegex = new RegExp(`(src\\s*=\\s*['"]\\s*)${escapedRef}(\\s*['"])`, 'g'); - if (htmlSrcRegex.test(processedContent)) { - processedContent = processedContent.replace(htmlSrcRegex, `$1${imgUrl}$2`); - } - - // 处理背景图片 - const htmlBgImageRegex = new RegExp(`(background-image:\\s*url\\(['"]\\s*)${escapedRef}(\\s*['"]\\))`, 'g'); - if (htmlBgImageRegex.test(processedContent)) { - processedContent = processedContent.replace(htmlBgImageRegex, `$1${imgUrl}$2`); - } - - const standaloneRegex = new RegExp(escapedRef, 'g'); - if (renderMode === 'markdown') { - processedContent = processedContent.replace(standaloneRegex, `![image](${imgUrl})`); - } else { - processedContent = processedContent.replace(standaloneRegex, ``); - } - } - - return { processedContent, errors, hasImages }; -} - -//替换内容中的头像标签 -async function replaceAvatarReferencesInContent(ctx: seal.MsgContext, ai: any, content: string, renderMode: 'markdown' | 'html'): Promise<{ processedContent: string, errors: string[], hasImages: boolean }> { - const errors: string[] = []; - let processedContent = content; - let hasImages = false; - - const avatarMatch = content.match(/[<<][\|│|]avatar:(private|group):(.+?)(?:[\|│|][>>]|[\|│|>>])/g); - - if (!avatarMatch) return { processedContent, errors, hasImages }; - - const uniqueRefs = [...new Set(avatarMatch)]; - - for (const avatarRef of uniqueRefs) { - const match = avatarRef.match(/[<<][\|│|]avatar:(private|group):(.+?)(?:[\|│|][>>]|[\|│|>>])/); - if (!match) continue; - - const avatarType = match[1]; - const name = match[2].trim(); - - let url = ''; - if (avatarType === 'private') { - const uid = await ai.context.findUserId(ctx, name, true); - if (uid === null) { - errors.push(`未找到用户<${name}>,无法获取头像`); - continue; - } - url = `https://q1.qlogo.cn/g?b=qq&nk=${uid.replace(/^.+:/, '')}&s=640`; - } else if (avatarType === 'group') { - const gid = await ai.context.findGroupId(ctx, name); - if (gid === null) { - errors.push(`未找到群聊<${name}>,无法获取头像`); - continue; - } - url = `https://p.qlogo.cn/gh/${gid.replace(/^.+:/, '')}/${gid.replace(/^.+:/, '')}/640`; - } - - if (url) { - hasImages = true; - const escapedRef = avatarRef.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - - const mdSyntaxRegex = new RegExp(`(\\[.*?\\]\\(\\s*)${escapedRef}(\\s*\\))`, 'g'); - if (mdSyntaxRegex.test(processedContent)) { - processedContent = processedContent.replace(mdSyntaxRegex, `$1${url}$2`); - } - - const htmlSrcRegex = new RegExp(`(src\\s*=\\s*['"]\\s*)${escapedRef}(\\s*['"])`, 'g'); - if (htmlSrcRegex.test(processedContent)) { - processedContent = processedContent.replace(htmlSrcRegex, `$1${url}$2`); +async function transformContentToUrlText(ctx: seal.MsgContext, ai: AI, content: string): Promise<{ text: string, images: Image[] }> { + const segs = parseSpecialTokens(content); + let text = ''; + const images: Image[] = []; + for (const seg of segs) { + switch (seg.type) { + case 'text': { + text += seg.content; + break; } - - // 处理背景图片 - const htmlBgImageRegex = new RegExp(`(background-image:\\s*url\\(['"]\\s*)${escapedRef}(\\s*['"]\\))`, 'g'); - if (htmlBgImageRegex.test(processedContent)) { - processedContent = processedContent.replace(htmlBgImageRegex, `$1${url}$2`); + case 'at': { + const name = seg.content; + const ui = await ai.context.findUserInfo(ctx, name); + if (ui !== null) { + text += ` @${ui.name} `; + } else { + logger.warning(`无法找到用户:${name}`); + text += ` @${name} `; + } + break; } - - const standaloneRegex = new RegExp(escapedRef, 'g'); - if (renderMode === 'markdown') { - processedContent = processedContent.replace(standaloneRegex, `![avatar](${url})`); - } else { - processedContent = processedContent.replace(standaloneRegex, ``); + case 'img': { + const id = seg.content; + const image = await ai.context.findImage(ctx, id); + + if (image) { + if (image.type === 'local') throw new Error(`图片<|img:${id}|>为本地图片,暂不支持`); + images.push(image); + text += image.url; + } else { + logger.warning(`无法找到图片:${id}`); + } + break; } } } - - return { processedContent, errors, hasImages }; + return { text, images }; } // Markdown 渲染 @@ -190,7 +94,7 @@ export function registerRender() { properties: { content: { type: "string", - description: "要渲染的 Markdown 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式。可以使用<|img:xxxxxx|>引用图片(xxxxxx为6位图片ID,不支持本地图片)。可以使用<|avatar:private:name|>或<|avatar:group:name|>引用头像" + description: "要渲染的 Markdown 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式。可以使用<|img:xxxxxx|>替代图片url(注意使用markdown语法显示图片),xxxxxx为" + `图片id,或user_avatar:用户名称` + (ConfigManager.message.showNumber ? '或纯数字QQ号' : '') + `,或group_avatar:群聊名称` + (ConfigManager.message.showNumber ? '或纯数字群号' : '') }, name: { type: "string", @@ -223,17 +127,10 @@ export function registerRender() { const kws = ["render", "markdown", name, theme]; try { - const { processedContent: contentWithImages, errors: imageErrors, hasImages: hasImageRefs } = await replaceImageReferencesInContent(ctx, ai, content, 'markdown'); - const { processedContent: finalContent, errors: avatarErrors, hasImages: hasAvatarRefs } = await replaceAvatarReferencesInContent(ctx, ai, contentWithImages, 'markdown'); - - const allErrors = [...imageErrors, ...avatarErrors]; - if (allErrors.length > 0) { - return { content: allErrors.join('\n'), images: [] }; - } - - const hasImages = hasImageRefs || hasAvatarRefs; - - const result = await renderMarkdown(finalContent, theme, 1200, hasImages); + const { text, images } = await transformContentToUrlText(ctx, ai, content); + const hasImages = images.length > 0; + + const result = await renderMarkdown(text, theme, 1200, hasImages); if (result.status === "success" && result.base64) { const base64 = result.base64; if (!base64) { @@ -270,7 +167,7 @@ export function registerRender() { properties: { content: { type: "string", - description: "要渲染的 HTML 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式。可以使用<|img:xxxxxx|>引用图片(xxxxxx为图片ID,不支持本地图片)。可以使用<|avatar:private:name|>或<|avatar:group:name|>引用头像" + description: "要渲染的 HTML 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式。可以使用<|img:xxxxxx|>替代图片url(注意使用html元素显示图片),xxxxxx为" + `图片id,或user_avatar:用户名称` + (ConfigManager.message.showNumber ? '或纯数字QQ号' : '') + `,或group_avatar:群聊名称` + (ConfigManager.message.showNumber ? '或纯数字群号' : '') }, name: { type: "string", @@ -297,17 +194,10 @@ export function registerRender() { const kws = ["render", "html", name]; try { - const { processedContent: contentWithImages, errors: imageErrors, hasImages: hasImageRefs } = await replaceImageReferencesInContent(ctx, ai, content, 'html'); - const { processedContent: finalContent, errors: avatarErrors, hasImages: hasAvatarRefs } = await replaceAvatarReferencesInContent(ctx, ai, contentWithImages, 'html'); - - const allErrors = [...imageErrors, ...avatarErrors]; - if (allErrors.length > 0) { - return { content: allErrors.join('\n'), images: [] }; - } - - const hasImages = hasImageRefs || hasAvatarRefs; - - const result = await renderHtml(finalContent, 1200, hasImages); + const { text, images } = await transformContentToUrlText(ctx, ai, content); + const hasImages = images.length > 0; + + const result = await renderHtml(text, 1200, hasImages); if (result.status === "success" && result.base64) { const base64 = result.base64; if (!base64) { diff --git a/src/utils/utils_string.ts b/src/utils/utils_string.ts index bb39705..f766910 100644 --- a/src/utils/utils_string.ts +++ b/src/utils/utils_string.ts @@ -475,7 +475,7 @@ interface TokenSegment { content: string; } -function parseSpecialTokens(s: string): TokenSegment[] { +export function parseSpecialTokens(s: string): TokenSegment[] { const result: TokenSegment[] = []; const segs = s.split(/([<<][\|│|][^::]+[::]?\s?.+?(?:[\|│|][>>]|[\|│|>>]))/); segs.forEach(seg => { From eef5349bf4e38a1289bbff283eb32c800670266a Mon Sep 17 00:00:00 2001 From: error2913 <2913949387@qq.com> Date: Fri, 28 Nov 2025 01:25:55 +0800 Subject: [PATCH 4/4] update update info --- src/update.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/update.ts b/src/update.ts index c9380d4..1a26928 100644 --- a/src/update.ts +++ b/src/update.ts @@ -1,5 +1,9 @@ // 版本更新日志,格式为 "版本号": "更新内容",版本号格式为 "x.y.z",按照时间顺序从新到旧排列。 export const updateInfo = { + "4.12.1": `- 新增按时间搜索记忆 +- 新增图片头像ID发送 +- 将img命令改为ai子命令 +- 新增render嵌入图片`, "4.12.0": `- 新增通过名称选择角色设定功能 - 修复获取好友、群聊等列表时的bug - 修复了调用函数时,无需cmdArgs的函数也会报错的问题