From da48c90d431d599739b0195ca1c00082e9bcaad7 Mon Sep 17 00:00:00 2001 From: dahetao <2732988078@qq.com> Date: Thu, 11 Dec 2025 23:13:11 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=9C=A8-high=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E4=B8=8B=EF=BC=8C=E7=9C=81=E7=95=A5=E5=A4=A7=E9=83=A8?= =?UTF-8?q?=E5=88=86base64=E6=98=BE=E7=A4=BA=EF=BC=8C=E7=A1=AE=E4=BF=9Dlog?= =?UTF-8?q?=E7=AE=80=E6=B4=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/logger.js | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/src/utils/logger.js b/src/utils/logger.js index 7a034a8d..f664f826 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -91,6 +91,43 @@ function logDetail(data) { console.log('----------------------------------------------------'); } +/** + * 截断 base64 数据,只保留前 maxLength 个字符 + * @param {any} obj - 要处理的对象 + * @param {number} maxLength - 最大保留长度,默认 100 + * @returns {any} - 处理后的对象 + */ +function truncateBase64(obj, maxLength = 100) { + if (obj === null || obj === undefined) { + return obj; + } + + if (typeof obj === 'string') { + // 检测是否为 base64 数据(长度较长且符合 base64 格式) + // base64 通常只包含 A-Za-z0-9+/= 字符 + const base64Regex = /^[A-Za-z0-9+/=]{200,}$/; + if (base64Regex.test(obj)) { + const totalLength = obj.length; + return obj.substring(0, maxLength) + `...[已截断, 共 ${totalLength} 字符]`; + } + return obj; + } + + if (Array.isArray(obj)) { + return obj.map(item => truncateBase64(item, maxLength)); + } + + if (typeof obj === 'object') { + const result = {}; + for (const key of Object.keys(obj)) { + result[key] = truncateBase64(obj[key], maxLength); + } + return result; + } + + return obj; +} + /** * 记录后端 API 的请求和响应(仅 debug=high 时生效) * @param {Object} data - 日志数据 @@ -124,7 +161,9 @@ function logBackend(data) { } if (body) { console.log(`${colors.yellow}Body:${colors.reset}`); - const bodyStr = typeof body === 'string' ? body : JSON.stringify(body, null, 2); + // 截断 base64 数据 + const truncatedBody = truncateBase64(body); + const bodyStr = typeof truncatedBody === 'string' ? truncatedBody : JSON.stringify(truncatedBody, null, 2); console.log(bodyStr); } } else if (type === 'response') { @@ -132,7 +171,9 @@ function logBackend(data) { console.log(`${colors.green}[Backend Response]${colors.reset} ${statusColor}${status}${colors.reset} ${colors.gray}${durationMs}ms${colors.reset}`); if (body) { console.log(`${colors.green}Body:${colors.reset}`); - const bodyStr = typeof body === 'string' ? body : JSON.stringify(body, null, 2); + // 截断 base64 数据 + const truncatedBody = truncateBase64(body); + const bodyStr = typeof truncatedBody === 'string' ? truncatedBody : JSON.stringify(truncatedBody, null, 2); console.log(bodyStr); } } From a1c633eadb09bc58ebb464f18e9a245f1f54afc8 Mon Sep 17 00:00:00 2001 From: dahetao <2732988078@qq.com> Date: Fri, 12 Dec 2025 00:36:42 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Gemini-3.0-Pro-Image?= =?UTF-8?q?=20=E6=80=9D=E7=BB=B4=E9=93=BE=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/client.js | 19 +++++++++++++------ src/server/index.js | 38 ++++++++++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/api/client.js b/src/api/client.js index 9058d796..185203e6 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -403,8 +403,11 @@ function parseAndEmitStreamChunk(line, state, callback) { if (parts) { for (const part of parts) { if (part.thought === true) { - // 思维链内容 - 不添加标签,直接发送 - callback({ type: 'thinking', content: part.text || '' }); + // 思维链内容 - 直接发送 + if (part.text) { + callback({ type: 'thinking', content: part.text }); + } + // 思维阶段的中间图片,跳过(只发送最终图片) } else if (part.text !== undefined) { if (part.thoughtSignature) { registerTextThoughtSignature(part.text, part.thoughtSignature); @@ -415,6 +418,10 @@ function parseAndEmitStreamChunk(line, state, callback) { } else if (part.functionCall) { // 工具调用 state.toolCalls.push(convertToToolCallWithSignature(part.functionCall, part.thoughtSignature)); + } else if (part.inlineData) { + // 图片数据 + const imageUrl = saveBase64Image(part.inlineData.data, part.inlineData.mimeType); + callback({ type: 'image', url: imageUrl }); } } } @@ -682,19 +689,19 @@ export async function generateAssistantResponseNoStream(requestBody, token) { } } - // 拼接思维链标签 - if (thinkingContent) { + // 拼接思维链标签(用于非图像模型的普通响应) + if (thinkingContent && imageUrls.length === 0) { content = `\n${thinkingContent}\n\n${content}`; } if (aggregatedText && aggregatedTextSignature) { registerTextThoughtSignature(aggregatedText, aggregatedTextSignature); } - // 生图模型:转换为 markdown 格式 + // 生图模型:转换为 markdown 格式,并返回独立的 thinking 字段 if (imageUrls.length > 0) { let markdown = content ? content + '\n\n' : ''; markdown += imageUrls.map(url => `![image](${url})`).join('\n\n'); - return { content: markdown, toolCalls }; + return { content: markdown, toolCalls, thinking: thinkingContent || null }; } return { content, toolCalls, usage }; diff --git a/src/server/index.js b/src/server/index.js index b5ca7cd2..7ff6c623 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -1605,9 +1605,14 @@ const createChatCompletionHandler = (resolveToken, options = {}) => async (req, const requestBody = generateRequestBody(messages, model, params, tools, token); if (isImageModel) { + // 为图像模型配置思维链和响应模态,使 gemini-3-pro-image 能返回思维内容 requestBody.request.generationConfig = { - candidateCount: 1 - // imageConfig: { aspectRatio: '1:1' } + candidateCount: 1, + responseModalities: ["TEXT", "IMAGE"], + thinkingConfig: { + includeThoughts: true, + thinkingBudget: 1024 + } }; requestBody.requestType = 'image_gen'; requestBody.request.systemInstruction.parts[0].text += @@ -1622,11 +1627,32 @@ const createChatCompletionHandler = (resolveToken, options = {}) => async (req, setStreamHeaders(res); if (isImageModel) { - const { content, usage } = await generateAssistantResponseNoStream(requestBody, token); - writeStreamData(res, createStreamChunk(id, created, model, { content })); + // 图像模型使用流式API,实现思维链实时传输 + const imageUrls = []; + const { usage } = await generateAssistantResponse(requestBody, token, data => { + streamEventsForLog.push(data); + + if (data.type === 'thinking') { + // 思维链内容实时发送 + writeStreamData(res, createStreamChunk(id, created, model, { reasoning_content: data.content })); + } else if (data.type === 'image') { + // 收集图片URL,最后统一发送 + imageUrls.push(data.url); + } else if (data.type === 'text') { + // 文本内容 + writeStreamData(res, createStreamChunk(id, created, model, { content: data.content })); + } + }); + + // 发送所有图片 + if (imageUrls.length > 0) { + const markdown = imageUrls.map(url => `![image](${url})`).join('\n\n'); + writeStreamData(res, createStreamChunk(id, created, model, { content: markdown })); + } + endStream(res, id, created, model, 'stop', usage); - responseBodyForLog = { stream: true, image: true, usage, content }; - responseSummaryForLog = { text: content }; + responseBodyForLog = { stream: true, image: true, usage, events: streamEventsForLog }; + responseSummaryForLog = summarizeStreamEvents(streamEventsForLog); } else { let hasToolCall = false; const { usage } = await generateAssistantResponse(requestBody, token, data => {