From d09750fc7b55174abbe583f8e8ec80d7740f4cab Mon Sep 17 00:00:00 2001 From: foxhui Date: Thu, 16 Apr 2026 02:28:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20ChatGPT=20=E5=9C=A8?= =?UTF-8?q?=20LaTeX=E4=B8=8B=E8=A2=AB=E6=88=AA=E6=96=AD=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E5=B9=B6=E6=94=B9=E7=94=A8=E4=B8=B4=E6=97=B6?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=A8=A1=E5=9D=97=20(ref=20#64,=20closes=20#?= =?UTF-8?q?60)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 10 ++ src/backend/adapter/chatgpt_text.js | 160 ++++++++++++++-------------- src/backend/adapter/gemini.js | 8 ++ src/backend/adapter/gemini_text.js | 8 ++ 4 files changed, 106 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea954f1..c709d0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [3.6.5] - 2026-04-09 +### ✨ Added +- **适配器** + - ChatGPT 和 Gemini 改用临时对话模式防止污染历史列表 + +### 🐛 Fixed +- **适配器** + - 修复 ChatGPT 在 LaTeX 输出下可能会导致截断的问题 + +## [3.6.5] - 2026-04-09 + ### ✨ Added - **适配器** - DeepSeek 支持专业模式 diff --git a/src/backend/adapter/chatgpt_text.js b/src/backend/adapter/chatgpt_text.js index 9651188..940c72a 100644 --- a/src/backend/adapter/chatgpt_text.js +++ b/src/backend/adapter/chatgpt_text.js @@ -16,7 +16,7 @@ import { import { logger } from '../../utils/logger.js'; // --- 配置常量 --- -const TARGET_URL = 'https://chatgpt.com/'; +const TARGET_URL = 'https://chatgpt.com/?temporary-chat=true'; // 感谢 @zhongjianhua163 提供方案 const INPUT_SELECTOR = '.ProseMirror'; /** @@ -139,92 +139,92 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { await safeClick(page, INPUT_SELECTOR, { bias: 'input' }); await humanType(page, INPUT_SELECTOR, prompt); + // 4. 先启动 SSE 监听,再发送提示词(避免竞态) + logger.info('适配器', '监听 SSE 流获取文本...', meta); + + let textContent = ''; + let isComplete = false; + let targetMessageId = null; // 只追踪 channel: "final" 的消息 + + const responsePromise = page.waitForResponse(async (response) => { + const url = response.url(); + if (!url.includes('backend-api/f/conversation')) return false; + if (response.request().method() !== 'POST') return false; + if (response.status() !== 200) return false; + + try { + const body = await response.text(); + const lines = body.split('\n'); + + for (const line of lines) { + // 跳过空行和事件行 + if (!line.startsWith('data: ')) continue; + + const dataStr = line.slice(6).trim(); + if (dataStr === '[DONE]') { + isComplete = true; + continue; + } + + try { + const data = JSON.parse(dataStr); + + // 检测目标消息 (assistant 角色, channel: "final", content_type: "text") + if (data.v?.message?.author?.role === 'assistant' && + data.v?.message?.channel === 'final' && + data.v?.message?.content?.content_type === 'text') { + targetMessageId = data.v.message.id; + // 重置内容(即使 parts[0] 为空也要重置,清除之前 commentary 的文本) + const parts = data.v.message.content.parts; + textContent = (parts && parts[0]) || ''; + } + + // 以下所有内容累积都必须在 targetMessageId 设置之后才执行 + // 避免误收 commentary / thinking 频道的内容 + if (!targetMessageId) continue; + + // 累积 delta 内容 (append 操作,顶层 path) + if (data.o === 'append' && data.p === '/message/content/parts/0' && data.v) { + textContent += data.v; + } + + // patch 操作中的 append (数组格式) + if (Array.isArray(data.v)) { + for (const patch of data.v) { + if (patch.o === 'append' && patch.p === '/message/content/parts/0' && patch.v) { + textContent += patch.v; + } + // 仅在 targetMessageId 存在时检查完成 + if (patch.p === '/message/status' && patch.v === 'finished_successfully') { + isComplete = true; + } + } + } + + // message_stream_complete 表示完成 + if (data.type === 'message_stream_complete') { + isComplete = true; + } + } catch { + // 忽略解析错误 + } + } + + return isComplete; + } catch { + return false; + } + }, { timeout: waitTimeout }); + // 5. 发送提示词 logger.debug('适配器', '发送提示词...', meta); await page.keyboard.press('Enter'); logger.info('适配器', '等待生成结果...', meta); - // 6. 监听 conversation API 的 SSE 流,解析文本内容 - logger.info('适配器', '监听 SSE 流获取文本...', meta); - - let textContent = ''; - let isComplete = false; - let targetMessageId = null; // 追踪目标消息 ID - + // 6. 等待 SSE 响应完成 try { - await page.waitForResponse(async (response) => { - const url = response.url(); - if (!url.includes('backend-api/f/conversation')) return false; - if (response.request().method() !== 'POST') return false; - if (response.status() !== 200) return false; - - try { - const body = await response.text(); - const lines = body.split('\n'); - - for (const line of lines) { - // 跳过空行和事件行 - if (!line.startsWith('data: ')) continue; - - const dataStr = line.slice(6).trim(); - if (dataStr === '[DONE]') { - isComplete = true; - continue; - } - - try { - const data = JSON.parse(dataStr); - - // 检测目标消息 (assistant 角色, channel: "final", content_type: "text") - if (data.v?.message?.author?.role === 'assistant' && - data.v?.message?.channel === 'final' && - data.v?.message?.content?.content_type === 'text') { - targetMessageId = data.v.message.id; - // 初始内容 - const parts = data.v.message.content.parts; - if (parts && parts[0]) { - textContent = parts[0]; - } - } - - // 累积 delta 内容 (append 操作) - if (data.o === 'append' && data.p === '/message/content/parts/0' && data.v) { - textContent += data.v; - } - - // 简单的 delta 追加 (没有 p/o,只有 v) - if (data.v && typeof data.v === 'string' && !data.o && !data.p && targetMessageId) { - textContent += data.v; - } - - // patch 操作中的 append - if (Array.isArray(data.v)) { - for (const patch of data.v) { - if (patch.o === 'append' && patch.p === '/message/content/parts/0' && patch.v) { - textContent += patch.v; - } - // 检查是否完成 - if (patch.p === '/message/status' && patch.v === 'finished_successfully') { - isComplete = true; - } - } - } - - // message_stream_complete 表示完成 - if (data.type === 'message_stream_complete') { - isComplete = true; - } - } catch { - // 忽略解析错误 - } - } - - return isComplete; - } catch { - return false; - } - }, { timeout: waitTimeout }); + await responsePromise; } catch (e) { const pageError = normalizePageError(e, meta); if (pageError) return pageError; diff --git a/src/backend/adapter/gemini.js b/src/backend/adapter/gemini.js index 113b9ce..aad8f10 100644 --- a/src/backend/adapter/gemini.js +++ b/src/backend/adapter/gemini.js @@ -41,6 +41,14 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { logger.info('适配器', '开启新会话...', meta); await gotoWithCheck(page, TARGET_URL); + try { + logger.debug('适配器', '尝试点击 Temporary chat...', meta); + const tempChatBtn = page.getByRole('button', { name: 'Temporary chat' }); + await safeClick(page, tempChatBtn, { bias: 'button', timeout: 3000 }); + } catch (e) { + logger.debug('适配器', '未找到 Temporary chat 按钮或点击失败,忽略', meta); + } + // 1. 等待输入框加载 await waitForInput(page, inputLocator, { click: false }); await sleep(300, 500); diff --git a/src/backend/adapter/gemini_text.js b/src/backend/adapter/gemini_text.js index 65acab0..6c26dd3 100644 --- a/src/backend/adapter/gemini_text.js +++ b/src/backend/adapter/gemini_text.js @@ -39,6 +39,14 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { logger.info('适配器', '开启新会话...', meta); await gotoWithCheck(page, TARGET_URL); + try { + logger.debug('适配器', '尝试点击 Temporary chat...', meta); + const tempChatBtn = page.getByRole('button', { name: 'Temporary chat' }); + await safeClick(page, tempChatBtn, { bias: 'button', timeout: 3000 }); + } catch (e) { + logger.debug('适配器', '未找到 Temporary chat 按钮或点击失败,忽略', meta); + } + // 1. 等待输入框加载 await waitForInput(page, inputLocator, { click: false });