diff --git a/CHANGELOG.md b/CHANGELOG.md
index 86d1ba7..3d77ff9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,11 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
-## [3.3.0] - 2025-12-20
+## [3.3.0] - 2025-12-21
### ✨ Added
- **新增适配器**
- 支持 Gemini 网页版文本生成
+ - 支持 ChatGPT 图片生成
+- **支持视频生成**
+ - 支持在 Gemini 网页版和 Gemini Enterprise Business 图片生成适配器中生成视频
+
+### 🔄 Changed
+- **优化图片下载方式**
+ - 让文件下载步骤直接继承浏览器上下文减少特征
## [3.3.0] - 2025-12-20
diff --git a/README.md b/README.md
index 9f1984c..34e4390 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,9 @@
# WebAI2API
-
+
+
+
+
## 📑 目录
@@ -22,7 +25,7 @@
- 🔄 **接口兼容**: 提供标准 OpenAI 格式接口,支持流式响应与心跳保活
- 🚀 **并发隔离**: 支持多窗口并发执行,可配置独立代理,实现多账号浏览器实例级数据隔离
- 🛡️ **稳定防护**: 内置任务队列、负载均衡、故障转移、错误重试等基础功能
-- 🎨 **Web 管理界面**: 提供可视化管理界面,支持实时日志查看、VNC 连接、适配器管理等
+- 🎨 **Web 管理界面**: 提供可视化管理界面,支持实时日志查看、VNC 连接、适配器管理等
### 📋 支持列表
@@ -34,6 +37,7 @@
| [**zAI**](https://zai.is/) | ❌ | ✅ | 🚫 |
| [**Google Gemini**](https://gemini.google.com/) | ✅ | ✅ | ✅ |
| [**ZenMux**](https://zenmux.ai/) | ✅ | ❌ | 🚫 |
+| [**ChatGPT**](https://chatgpt.com/) | ❌ | ✅ | 🚫 |
| 待续... | - | - | - |
> [!NOTE]
diff --git a/src/backend/adapter/chatgpt.js b/src/backend/adapter/chatgpt.js
new file mode 100644
index 0000000..fdcd468
--- /dev/null
+++ b/src/backend/adapter/chatgpt.js
@@ -0,0 +1,207 @@
+/**
+ * @fileoverview ChatGPT 图片生成适配器
+ */
+
+import {
+ sleep,
+ safeClick,
+ uploadFilesViaChooser
+} from '../engine/utils.js';
+import {
+ fillPrompt,
+ normalizePageError,
+ moveMouseAway,
+ waitForInput,
+ gotoWithCheck,
+ waitApiResponse,
+ useContextDownload
+} from '../utils/index.js';
+import { logger } from '../../utils/logger.js';
+
+// --- 配置常量 ---
+const TARGET_URL = 'https://chatgpt.com/images/';
+const INPUT_SELECTOR = '.ProseMirror';
+
+/**
+ * 执行生图任务
+ * @param {object} context - 浏览器上下文 { page, config }
+ * @param {string} prompt - 提示词
+ * @param {string[]} imgPaths - 图片路径数组
+ * @param {string} [modelId] - 模型 ID (此适配器未使用)
+ * @param {object} [meta={}] - 日志元数据
+ * @returns {Promise<{image?: string, error?: string}>}
+ */
+async function generate(context, prompt, imgPaths, modelId, meta = {}) {
+ const { page } = context;
+ const sendBtnLocator = page.getByRole('button', { name: 'Send prompt' });
+
+ try {
+ logger.info('适配器', '开启新会话...', meta);
+ await gotoWithCheck(page, TARGET_URL);
+
+ // 1. 等待输入框加载
+ await waitForInput(page, INPUT_SELECTOR, { click: false });
+ await sleep(1500, 2500);
+
+ // 2. 上传图片
+ if (imgPaths && imgPaths.length > 0) {
+ const expectedUploads = imgPaths.length;
+ let uploadedCount = 0;
+ let processedCount = 0;
+
+ logger.debug('适配器', '点击添加文件按钮...', meta);
+ const addFilesBtn = page.getByRole('button', { name: 'Add files and more' });
+
+ await uploadFilesViaChooser(page, addFilesBtn, imgPaths, {
+ uploadValidator: (response) => {
+ const url = response.url();
+ if (response.status() === 200) {
+ // 上传请求
+ if (url.includes('backend-api/files') && !url.includes('process_upload_stream')) {
+ uploadedCount++;
+ logger.debug('适配器', `图片上传进度: ${uploadedCount}/${expectedUploads}`, meta);
+ return false;
+ }
+ // 处理完成请求
+ if (url.includes('backend-api/files/process_upload_stream')) {
+ processedCount++;
+ logger.info('适配器', `图片处理进度: ${processedCount}/${expectedUploads}`, meta);
+
+ if (processedCount >= expectedUploads) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ });
+
+ await sleep(1000, 2000);
+ }
+
+ // 3. 填写提示词
+ await safeClick(page, INPUT_SELECTOR, { bias: 'input' });
+ await fillPrompt(page, INPUT_SELECTOR, prompt, meta);
+ await sleep(500, 1000);
+
+ // 4. 点击发送
+ logger.debug('适配器', '点击发送...', meta);
+ await safeClick(page, sendBtnLocator, { bias: 'button' });
+
+ logger.info('适配器', '等待生成结果...', meta);
+
+ // 5. 等待 conversation API 返回
+ let conversationResponse;
+ try {
+ conversationResponse = await waitApiResponse(page, {
+ urlMatch: 'backend-api/f/conversation',
+ method: 'POST',
+ timeout: 180000, // 图片生成可能较慢
+ meta
+ });
+ } catch (e) {
+ const pageError = normalizePageError(e, meta);
+ if (pageError) return pageError;
+ throw e;
+ }
+
+ // 检查响应状态
+ if (conversationResponse.status() !== 200) {
+ logger.error('适配器', `API 返回错误: HTTP ${conversationResponse.status()}`, meta);
+ return { error: `API 返回错误: HTTP ${conversationResponse.status()}` };
+ }
+
+ logger.info('适配器', '生成中,等待图片就绪...', meta);
+
+ // 6. 监听文件状态接口,等待图片生成完成
+ // 通过 file_name 是否包含 .part 判断是否生成完成
+ let downloadUrl = null;
+ let fileName = null;
+
+ try {
+ await page.waitForResponse(async (response) => {
+ const url = response.url();
+ if (!url.includes('backend-api/files/download/file_')) return false;
+ if (response.status() !== 200) return false;
+
+ try {
+ const json = await response.json();
+ const fn = json.file_name;
+ const dl = json.download_url;
+
+ // 检查是否生成完成:
+ // 1. 必须有 file_name
+ // 2. file_name 不能包含 .part(表示中间状态)
+ // 3. 必须有 download_url
+ if (fn && !fn.includes('.part') && dl) {
+ fileName = fn;
+ downloadUrl = dl;
+ logger.info('适配器', `图片生成完成: ${fn}`, meta);
+ return true;
+ } else {
+ logger.debug('适配器', `图片生成中: ${fn || '无文件名'}`, meta);
+ return false;
+ }
+ } catch {
+ return false;
+ }
+ }, { timeout: 120000 });
+ } catch (e) {
+ const pageError = normalizePageError(e, meta);
+ if (pageError) return pageError;
+ throw e;
+ }
+
+ if (!downloadUrl) {
+ logger.error('适配器', '未获取到图片下载链接', meta);
+ return { error: '未获取到图片下载链接' };
+ }
+
+ logger.info('适配器', '正在下载图片...', meta);
+
+ // 7. 使用 useContextDownload 下载图片
+ const result = await useContextDownload(downloadUrl, page);
+ if (result.error) {
+ logger.error('适配器', result.error, meta);
+ return result;
+ }
+
+ logger.info('适配器', '已获取图片,任务完成', meta);
+ return result;
+
+ } catch (err) {
+ // 顶层错误处理
+ const pageError = normalizePageError(err, meta);
+ if (pageError) return pageError;
+
+ logger.error('适配器', '生成任务失败', { ...meta, error: err.message });
+ return { error: `生成任务失败: ${err.message}` };
+ } finally {
+ // 任务结束,将鼠标移至安全区域
+ await moveMouseAway(page);
+ }
+}
+
+/**
+ * 适配器 manifest
+ */
+export const manifest = {
+ id: 'chatgpt',
+ displayName: 'ChatGPT (图片生成)',
+
+ // 入口 URL
+ getTargetUrl(config, workerConfig) {
+ return TARGET_URL;
+ },
+
+ // 模型列表
+ models: [
+ { id: 'gpt-image-1', imagePolicy: 'optional' }
+ ],
+
+ // 无需导航处理器
+ navigationHandlers: [],
+
+ // 核心生图方法
+ generate
+};
diff --git a/src/backend/adapter/gemini.js b/src/backend/adapter/gemini.js
index a93839e..ae4454c 100644
--- a/src/backend/adapter/gemini.js
+++ b/src/backend/adapter/gemini.js
@@ -1,5 +1,5 @@
/**
- * @fileoverview Gemini(消费者版)适配器
+ * @fileoverview Google Gemini 图片、视频生成适配器
*/
import {
diff --git a/src/backend/adapter/gemini_biz.js b/src/backend/adapter/gemini_biz.js
index dfc69bd..137249c 100644
--- a/src/backend/adapter/gemini_biz.js
+++ b/src/backend/adapter/gemini_biz.js
@@ -1,5 +1,5 @@
/**
- * @fileoverview Gemini Business 适配器
+ * @fileoverview Gemini Business 文本生成适配器
*/
import {
diff --git a/src/backend/adapter/gemini_biz_text.js b/src/backend/adapter/gemini_biz_text.js
index 28dd4fd..ba781f8 100644
--- a/src/backend/adapter/gemini_biz_text.js
+++ b/src/backend/adapter/gemini_biz_text.js
@@ -1,5 +1,5 @@
/**
- * @fileoverview Gemini Business 适配器
+ * @fileoverview Gemini Business 图片、视频生成适配器
*/
import {
diff --git a/src/backend/adapter/gemini_text.js b/src/backend/adapter/gemini_text.js
index 4672603..98badd1 100644
--- a/src/backend/adapter/gemini_text.js
+++ b/src/backend/adapter/gemini_text.js
@@ -1,5 +1,5 @@
/**
- * @fileoverview Gemini(消费者版)文本适配器
+ * @fileoverview Google Gemini 文本生成适配器
*/
import {
diff --git a/src/backend/adapter/lmarena.js b/src/backend/adapter/lmarena.js
index 4a5886c..0a9682e 100644
--- a/src/backend/adapter/lmarena.js
+++ b/src/backend/adapter/lmarena.js
@@ -1,5 +1,5 @@
/**
- * @fileoverview LMArena 适配器
+ * @fileoverview LMArena 图片生成适配器
*/
import {
@@ -13,10 +13,10 @@ import {
waitApiResponse,
normalizePageError,
normalizeHttpError,
- downloadImage,
moveMouseAway,
waitForInput,
- gotoWithCheck
+ gotoWithCheck,
+ useContextDownload
} from '../utils/index.js';
import { logger } from '../../utils/logger.js';
@@ -148,7 +148,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
}
logger.info('适配器', '已获取结果,正在下载图片...', meta);
- const result = await downloadImage(img, context);
+ const result = await useContextDownload(img, page);
if (result.image) {
logger.info('适配器', '已下载图片,任务完成', meta);
}
diff --git a/src/backend/adapter/lmarena_text.js b/src/backend/adapter/lmarena_text.js
index 4f09578..44bfd3e 100644
--- a/src/backend/adapter/lmarena_text.js
+++ b/src/backend/adapter/lmarena_text.js
@@ -1,5 +1,5 @@
/**
- * @fileoverview LMArena 适配器
+ * @fileoverview LMArena 文本生成适配器
*/
import {
diff --git a/src/backend/adapter/nanobananafree_ai.js b/src/backend/adapter/nanobananafree_ai.js
index ab5e11a..c3751da 100644
--- a/src/backend/adapter/nanobananafree_ai.js
+++ b/src/backend/adapter/nanobananafree_ai.js
@@ -1,5 +1,5 @@
/**
- * @fileoverview NanoBananaFree AI 适配器
+ * @fileoverview NanoBananaFree 图片生成适配器
*/
import {
diff --git a/src/backend/adapter/zai_is.js b/src/backend/adapter/zai_is.js
index 125ad8a..df3eb2a 100644
--- a/src/backend/adapter/zai_is.js
+++ b/src/backend/adapter/zai_is.js
@@ -1,5 +1,5 @@
/**
- * @fileoverview zAI(zai.is)适配器
+ * @fileoverview zAI 图片生成适配器
*/
import {
@@ -14,7 +14,7 @@ import {
normalizeHttpError,
waitApiResponse,
moveMouseAway,
- downloadImage,
+ useContextDownload,
waitForPageAuth,
lockPageAuth,
unlockPageAuth,
@@ -320,7 +320,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
logger.info('适配器', `已提取图片链接: ${imageUrl}`, meta);
// 下载图片
- const downloadResult = await downloadImage(imageUrl, context);
+ const downloadResult = await useContextDownload(imageUrl, page);
if (downloadResult.error) {
return downloadResult;
}
diff --git a/src/backend/adapter/zenmux_ai_text.js b/src/backend/adapter/zenmux_ai_text.js
index 285aed7..00af4da 100644
--- a/src/backend/adapter/zenmux_ai_text.js
+++ b/src/backend/adapter/zenmux_ai_text.js
@@ -1,3 +1,7 @@
+/**
+ * @fileoverview ZenMux 文本生成适配器
+ */
+
import {
sleep,
safeClick,
diff --git a/src/backend/utils/download.js b/src/backend/utils/download.js
index f3944c6..ced95cd 100644
--- a/src/backend/utils/download.js
+++ b/src/backend/utils/download.js
@@ -4,76 +4,29 @@
*/
/**
- * 下载图片并转换为 Base64
+ * 使用页面上下文下载图片并转换为 Base64
+ * 自动继承页面的 Cookie 和 Session,解决鉴权问题
* @param {string} url - 图片 URL
- * @param {object} context - 上下文对象,包含 proxyConfig 和 userDataDir
+ * @param {import('playwright-core').Page} page - Playwright 页面对象
+ * @param {object} [options] - 可选配置
+ * @param {number} [options.timeout=60000] - 超时时间(毫秒)
* @returns {Promise<{ image?: string, error?: string }>} 下载结果
*/
-export async function downloadImage(url, context = {}) {
- // 动态导入依赖
- const { gotScraping } = await import('got-scraping');
- const fs = await import('fs');
- const path = await import('path');
- const { getHttpProxy } = await import('../../utils/proxy.js');
-
- const { proxyConfig = null, userDataDir } = context;
+export async function useContextDownload(url, page, options = {}) {
+ const { timeout = 60000 } = options;
try {
- // 读取指纹文件获取浏览器信息
- let fingerprintPath = userDataDir
- ? path.join(userDataDir, 'fingerprint.json')
- : path.join(process.cwd(), 'data', 'camoufoxUserData', 'fingerprint.json');
+ const response = await page.request.get(url, { timeout });
- let browserName = 'firefox';
- let browserMinVersion = 100;
- let os = 'windows';
- let locale = 'en-US';
-
- if (fs.existsSync(fingerprintPath)) {
- try {
- const fingerprint = JSON.parse(fs.readFileSync(fingerprintPath, 'utf8'));
- if (fingerprint.navigator?.userAgent) {
- const versionMatch = fingerprint.navigator.userAgent.match(/Firefox\/(\d+)/i);
- if (versionMatch) {
- browserMinVersion = parseInt(versionMatch[1], 10);
- }
- }
- if (fingerprint.navigator?.platform) {
- const platform = fingerprint.navigator.platform.toLowerCase();
- if (platform.includes('win')) os = 'windows';
- else if (platform.includes('mac')) os = 'macos';
- else if (platform.includes('linux')) os = 'linux';
- }
- if (fingerprint.navigator?.language) {
- locale = fingerprint.navigator.language;
- }
- } catch (e) {
- // 解析失败使用默认值
- }
+ if (!response.ok()) {
+ return { error: `下载失败: HTTP ${response.status()}` };
}
- const proxyUrl = await getHttpProxy(proxyConfig);
-
- const options = {
- url,
- responseType: 'buffer',
- http2: true,
- headerGeneratorOptions: {
- browsers: [{ name: browserName, minVersion: browserMinVersion }],
- devices: ['desktop'],
- locales: [locale],
- operatingSystems: [os],
- }
- };
-
- if (proxyUrl) {
- options.proxyUrl = proxyUrl;
- }
-
- const response = await gotScraping(options);
- const base64 = response.body.toString('base64');
- const contentType = response.headers['content-type'] || 'image/png';
+ const buffer = await response.body();
+ const base64 = buffer.toString('base64');
+ const contentType = response.headers()['content-type'] || 'image/png';
const mimeType = contentType.split(';')[0].trim();
+
return { image: `data:${mimeType};base64,${base64}` };
} catch (e) {
return { error: `已获取结果,但图片下载时遇到错误: ${e.message}` };
diff --git a/src/backend/utils/index.js b/src/backend/utils/index.js
index 09d15c2..85954f8 100644
--- a/src/backend/utils/index.js
+++ b/src/backend/utils/index.js
@@ -19,7 +19,7 @@
* - normalizeError: 通用错误归一化
*
* - 资源下载 (download.js):
- * - downloadImage: 下载图片并转换为 Base64
+ * - useContextDownload: 使用页面上下文下载图片并转换为 Base64
*/
// 页面交互
@@ -46,4 +46,4 @@ export {
} from './error.js';
// 资源下载
-export { downloadImage } from './download.js';
+export { useContextDownload } from './download.js';