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 2c8994f..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,14 +34,53 @@ async function postToRenderEndpoint(endpoint: string, bodyData: any): Promise { + const segs = parseSpecialTokens(content); + let text = ''; + const images: Image[] = []; + for (const seg of segs) { + switch (seg.type) { + case 'text': { + text += seg.content; + break; + } + 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; + } + 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 { text, images }; +} + // 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 +94,7 @@ export function registerRender() { properties: { content: { type: "string", - description: "要渲染的 Markdown 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式" + description: "要渲染的 Markdown 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式。可以使用<|img:xxxxxx|>替代图片url(注意使用markdown语法显示图片),xxxxxx为" + `图片id,或user_avatar:用户名称` + (ConfigManager.message.showNumber ? '或纯数字QQ号' : '') + `,或group_avatar:群聊名称` + (ConfigManager.message.showNumber ? '或纯数字群号' : '') }, name: { type: "string", @@ -87,7 +127,10 @@ export function registerRender() { const kws = ["render", "markdown", name, theme]; try { - const result = await renderMarkdown(content, theme, 1200); + 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) { @@ -124,7 +167,7 @@ export function registerRender() { properties: { content: { type: "string", - description: "要渲染的 HTML 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式。" + description: "要渲染的 HTML 内容。支持 LaTeX 数学公式,使用前后 $ 包裹行内公式,前后 $$ 包裹块级公式。可以使用<|img:xxxxxx|>替代图片url(注意使用html元素显示图片),xxxxxx为" + `图片id,或user_avatar:用户名称` + (ConfigManager.message.showNumber ? '或纯数字QQ号' : '') + `,或group_avatar:群聊名称` + (ConfigManager.message.showNumber ? '或纯数字群号' : '') }, name: { type: "string", @@ -151,7 +194,10 @@ export function registerRender() { const kws = ["render", "html", name]; try { - const result = await renderHtml(content, 1200); + 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) { @@ -178,7 +224,4 @@ export function registerRender() { } } -// TODO:嵌入图片…… -// 1. 嵌入本地图片 -// 2. 嵌入网络图片,包括聊天记录,用户头像,群头像,直接使用url -// 3. 嵌入base64图片 \ No newline at end of file +// TODO:嵌入本地图片 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的函数也会报错的问题 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 => { 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({