mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 支持 ChatGPT、zAI、DeepSeek 文本生成
This commit is contained in:
@@ -5,6 +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.4.0] - 2025-12-23
|
||||
|
||||
### ✨ Added
|
||||
- **新增适配器**
|
||||
- 支持 ChatGPT 文本生成适配器
|
||||
- 支持 zAI 文本生成适配器
|
||||
- 支持 DeepSeek 文本生成适配器
|
||||
|
||||
### 🔄 Changed
|
||||
- **适配器实现更改**
|
||||
- zAI 图片生成适配器不再使用拦截请求修改响应体的方式,改为UI选择模型列表,并且Nano Banana Pro 支持选择1K、2K、4K
|
||||
|
||||
## [3.3.2] - 2025-12-22
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
@@ -38,10 +38,11 @@
|
||||
| [**LMArena**](https://lmarena.ai/) | ✅ | ✅ | 🚫 |
|
||||
| [**Gemini Enterprise Business**](https://business.gemini.google/) | ✅ | ✅ | ✅ |
|
||||
| [**Nano Banana Free**](https://nanobananafree.ai/) | 🚫 | ✅ | 🚫 |
|
||||
| [**zAI**](https://zai.is/) | ❌ | ✅ | 🚫 |
|
||||
| [**zAI**](https://zai.is/) | ✅ | ✅ | 🚫 |
|
||||
| [**Google Gemini**](https://gemini.google.com/) | ✅ | ✅ | ✅ |
|
||||
| [**ZenMux**](https://zenmux.ai/) | ✅ | ❌ | 🚫 |
|
||||
| [**ChatGPT**](https://chatgpt.com/) | ❌ | ✅ | 🚫 |
|
||||
| [**ChatGPT**](https://chatgpt.com/) | ✅ | ✅ | 🚫 |
|
||||
| [**DeepSeek**](https://chat.deepseek.com/) | ✅ | 🚫 | 🚫 |
|
||||
| 待续... | - | - | - |
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
/**
|
||||
* @fileoverview ChatGPT 文本生成适配器
|
||||
*/
|
||||
|
||||
import {
|
||||
sleep,
|
||||
safeClick,
|
||||
uploadFilesViaChooser
|
||||
} from '../engine/utils.js';
|
||||
import {
|
||||
fillPrompt,
|
||||
normalizePageError,
|
||||
moveMouseAway,
|
||||
waitForInput,
|
||||
gotoWithCheck,
|
||||
waitApiResponse
|
||||
} from '../utils/index.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
// --- 配置常量 ---
|
||||
const TARGET_URL = 'https://chatgpt.com/';
|
||||
const INPUT_SELECTOR = '.ProseMirror';
|
||||
|
||||
/**
|
||||
* 通过 UI 选择模型
|
||||
* @param {import('playwright-core').Page} page - 页面对象
|
||||
* @param {string} codeName - 模型 codeName
|
||||
* @param {object} meta - 日志元数据
|
||||
* @returns {Promise<boolean>} 是否成功选择了模型
|
||||
*/
|
||||
async function selectModel(page, codeName, meta = {}) {
|
||||
try {
|
||||
// 1. 点击 Model selector 按钮
|
||||
const modelSelectorBtn = page.getByRole('button', { name: /^Model selector/ });
|
||||
const btnExists = await modelSelectorBtn.count();
|
||||
if (btnExists === 0) {
|
||||
logger.debug('适配器', '未找到模型选择器按钮,跳过选择模型', meta);
|
||||
return false;
|
||||
}
|
||||
|
||||
await modelSelectorBtn.waitFor({ timeout: 5000 });
|
||||
await sleep(300, 500);
|
||||
await safeClick(page, modelSelectorBtn, { bias: 'button' });
|
||||
await sleep(500, 800);
|
||||
|
||||
// 2. 检查是否有 Legacy models 选项
|
||||
const legacyMenuItem = page.getByRole('menuitem', { name: /^Legacy models/ });
|
||||
const legacyExists = await legacyMenuItem.count();
|
||||
if (legacyExists > 0) {
|
||||
logger.debug('适配器', '发现 Legacy models 选项,正在点击...', meta);
|
||||
await safeClick(page, legacyMenuItem, { bias: 'button' });
|
||||
await sleep(500, 800);
|
||||
}
|
||||
|
||||
// 3. 查找匹配 codeName 开头的 menuitem
|
||||
const targetMenuItem = page.getByRole('menuitem', { name: new RegExp(`^${codeName}`) });
|
||||
const targetExists = await targetMenuItem.count();
|
||||
if (targetExists > 0) {
|
||||
logger.info('适配器', `正在选择模型: ${codeName}`, meta);
|
||||
await safeClick(page, targetMenuItem, { bias: 'button' });
|
||||
await sleep(500, 1000);
|
||||
return true;
|
||||
} else {
|
||||
logger.debug('适配器', `未找到模型 ${codeName},使用默认模型`, meta);
|
||||
// 点击空白区域关闭菜单
|
||||
await page.keyboard.press('Escape');
|
||||
await sleep(300, 500);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('适配器', `选择模型失败: ${e.message}`, meta);
|
||||
// 尝试关闭菜单
|
||||
await page.keyboard.press('Escape').catch(() => { });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行文本生成任务
|
||||
* @param {object} context - 浏览器上下文 { page, config }
|
||||
* @param {string} prompt - 提示词
|
||||
* @param {string[]} imgPaths - 图片路径数组
|
||||
* @param {string} [modelId] - 模型 ID
|
||||
* @param {object} [meta={}] - 日志元数据
|
||||
* @returns {Promise<{text?: 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. 选择模型
|
||||
const modelConfig = manifest.models.find(m => m.id === modelId);
|
||||
const targetModel = modelConfig?.codeName || modelId;
|
||||
if (targetModel) {
|
||||
await selectModel(page, targetModel, meta);
|
||||
}
|
||||
|
||||
// 3. 上传图片 (双击 Add files and more 按钮)
|
||||
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, {
|
||||
clickAction: 'dblclick', // 使用双击
|
||||
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);
|
||||
}
|
||||
|
||||
// 4. 填写提示词
|
||||
await safeClick(page, INPUT_SELECTOR, { bias: 'input' });
|
||||
await fillPrompt(page, INPUT_SELECTOR, prompt, meta);
|
||||
await sleep(500, 1000);
|
||||
|
||||
// 5. 点击发送
|
||||
logger.debug('适配器', '点击发送...', meta);
|
||||
await safeClick(page, sendBtnLocator, { bias: 'button' });
|
||||
|
||||
logger.info('适配器', '等待生成结果...', meta);
|
||||
|
||||
// 6. 监听 conversation API 的 SSE 流,解析文本内容
|
||||
logger.info('适配器', '监听 SSE 流获取文本...', meta);
|
||||
|
||||
let textContent = '';
|
||||
let isComplete = false;
|
||||
let targetMessageId = null; // 追踪目标消息 ID
|
||||
|
||||
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 (data.o === 'patch' && 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: 180000 });
|
||||
} catch (e) {
|
||||
const pageError = normalizePageError(e, meta);
|
||||
if (pageError) return pageError;
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (!textContent || textContent.trim() === '') {
|
||||
logger.warn('适配器', '回复内容为空', meta);
|
||||
return { error: '回复内容为空' };
|
||||
}
|
||||
|
||||
logger.info('适配器', `已获取文本内容 (${textContent.length} 字符)`, meta);
|
||||
logger.info('适配器', '文本生成完成,任务完成', meta);
|
||||
return { text: textContent.trim() };
|
||||
|
||||
} 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_text',
|
||||
displayName: 'ChatGPT (文本生成)',
|
||||
|
||||
// 入口 URL
|
||||
getTargetUrl(config, workerConfig) {
|
||||
return TARGET_URL;
|
||||
},
|
||||
|
||||
// 模型列表
|
||||
models: [
|
||||
{ id: 'gpt-5.2', codeName: 'GPT-5.2 Instant', imagePolicy: 'optional' },
|
||||
{ id: 'gpt-5.2-thinking', codeName: 'GPT-5.2 Thinking', imagePolicy: 'optional' },
|
||||
{ id: 'gpt-5.1', codeName: 'GPT-5.1 Instant', imagePolicy: 'optional' },
|
||||
{ id: 'gpt-5.1-thinking', codeName: 'GPT-5.1 Thinking', imagePolicy: 'optional' },
|
||||
{ id: 'gpt-5', codeName: 'GPT-5 Instant', imagePolicy: 'optional' },
|
||||
{ id: 'gpt-5-thinking', codeName: 'GPT-5 Thinking', imagePolicy: 'optional' },
|
||||
],
|
||||
|
||||
// 无需导航处理器
|
||||
navigationHandlers: [],
|
||||
|
||||
// 核心文本生成方法
|
||||
generate
|
||||
};
|
||||
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* @fileoverview DeepSeek 文本生成适配器
|
||||
*/
|
||||
|
||||
import {
|
||||
sleep,
|
||||
safeClick
|
||||
} from '../engine/utils.js';
|
||||
import {
|
||||
fillPrompt,
|
||||
normalizePageError,
|
||||
moveMouseAway,
|
||||
waitForInput,
|
||||
gotoWithCheck
|
||||
} from '../utils/index.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
// --- 配置常量 ---
|
||||
const TARGET_URL = 'https://chat.deepseek.com/';
|
||||
const INPUT_SELECTOR = 'textarea';
|
||||
|
||||
/**
|
||||
* 切换功能按钮状态
|
||||
* @param {import('playwright-core').Page} page - 页面对象
|
||||
* @param {string} buttonName - 按钮名称 (DeepThink / Search)
|
||||
* @param {boolean} targetState - 目标状态 (true=开启, false=关闭)
|
||||
* @param {object} meta - 日志元数据
|
||||
* @returns {Promise<boolean>} 是否成功切换
|
||||
*/
|
||||
async function toggleButton(page, buttonName, targetState, meta = {}) {
|
||||
try {
|
||||
const btn = page.getByRole('button', { name: buttonName });
|
||||
const btnCount = await btn.count();
|
||||
if (btnCount === 0) {
|
||||
logger.debug('适配器', `未找到 ${buttonName} 按钮`, meta);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取当前状态 (检查 class 是否包含 ds-toggle-button--selected)
|
||||
const isSelected = await btn.evaluate(el => el.classList.contains('ds-toggle-button--selected'));
|
||||
|
||||
if (isSelected !== targetState) {
|
||||
logger.info('适配器', `切换 ${buttonName}: ${isSelected} -> ${targetState}`, meta);
|
||||
await safeClick(page, btn, { bias: 'button' });
|
||||
await sleep(300, 500);
|
||||
return true;
|
||||
} else {
|
||||
logger.debug('适配器', `${buttonName} 已是目标状态: ${targetState}`, meta);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('适配器', `切换 ${buttonName} 失败: ${e.message}`, meta);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置模型功能 (thinking / search)
|
||||
* @param {import('playwright-core').Page} page - 页面对象
|
||||
* @param {object} modelConfig - 模型配置
|
||||
* @param {object} meta - 日志元数据
|
||||
*/
|
||||
async function configureModel(page, modelConfig, meta = {}) {
|
||||
const thinking = modelConfig?.thinking || false;
|
||||
const search = modelConfig?.search || false;
|
||||
|
||||
// 切换 DeepThink 状态
|
||||
await toggleButton(page, 'DeepThink', thinking, meta);
|
||||
await sleep(200, 400);
|
||||
|
||||
// 切换 Search 状态
|
||||
await toggleButton(page, 'Search', search, meta);
|
||||
await sleep(200, 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行文本生成任务
|
||||
* @param {object} context - 浏览器上下文 { page, config }
|
||||
* @param {string} prompt - 提示词
|
||||
* @param {string[]} imgPaths - 图片路径数组 (此适配器不支持)
|
||||
* @param {string} [modelId] - 模型 ID
|
||||
* @param {object} [meta={}] - 日志元数据
|
||||
* @returns {Promise<{text?: string, error?: string}>}
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page } = context;
|
||||
|
||||
try {
|
||||
logger.info('适配器', '开启新会话...', meta);
|
||||
await gotoWithCheck(page, TARGET_URL);
|
||||
|
||||
// 1. 等待输入框加载
|
||||
await waitForInput(page, INPUT_SELECTOR, { click: false });
|
||||
await sleep(1500, 2500);
|
||||
|
||||
// 2. 配置模型功能 (thinking / search)
|
||||
const modelConfig = manifest.models.find(m => m.id === modelId);
|
||||
if (modelConfig) {
|
||||
await configureModel(page, modelConfig, meta);
|
||||
}
|
||||
|
||||
// 3. 填写提示词
|
||||
await safeClick(page, INPUT_SELECTOR, { bias: 'input' });
|
||||
await fillPrompt(page, INPUT_SELECTOR, prompt, meta);
|
||||
await sleep(500, 1000);
|
||||
|
||||
// 4. 按回车发送
|
||||
logger.debug('适配器', '按回车发送...', meta);
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
logger.info('适配器', '等待生成结果...', meta);
|
||||
|
||||
// 5. 监听 chat/completion SSE 流,解析文本内容
|
||||
logger.info('适配器', '监听 SSE 流获取文本...', meta);
|
||||
|
||||
let textContent = '';
|
||||
let isComplete = false;
|
||||
let responseFragmentIndex = -1; // RESPONSE 类型 fragment 的数组索引
|
||||
let currentFragmentIndex = -1; // 当前正在追加内容的 fragment 数组索引
|
||||
let fragmentCount = 0; // fragments 数组的当前长度
|
||||
|
||||
try {
|
||||
await page.waitForResponse(async (response) => {
|
||||
const url = response.url();
|
||||
if (!url.includes('chat/completion')) 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('event:') || !line.startsWith('data:')) continue;
|
||||
|
||||
const dataStr = line.slice(5).trim();
|
||||
if (!dataStr || dataStr === '{}') continue;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(dataStr);
|
||||
|
||||
// 初始响应中可能已有 fragments (如 SEARCH)
|
||||
if (data.v?.response?.fragments && Array.isArray(data.v.response.fragments)) {
|
||||
for (const fragment of data.v.response.fragments) {
|
||||
const idx = fragmentCount++;
|
||||
if (fragment.type === 'RESPONSE') {
|
||||
responseFragmentIndex = idx;
|
||||
currentFragmentIndex = idx;
|
||||
if (fragment.content) {
|
||||
textContent += fragment.content;
|
||||
}
|
||||
} else {
|
||||
currentFragmentIndex = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 简单的文本追加 (只有 v 字符串,没有 p 和 o)
|
||||
// 只有当前活跃的 fragment 是 RESPONSE 类型时才收集
|
||||
if (data.v && typeof data.v === 'string' && !data.p && !data.o) {
|
||||
if (currentFragmentIndex === responseFragmentIndex && responseFragmentIndex >= 0) {
|
||||
textContent += data.v;
|
||||
}
|
||||
}
|
||||
|
||||
// 带路径的 APPEND 操作 (如 response/fragments/1/content)
|
||||
if (data.o === 'APPEND' && data.p && typeof data.v === 'string') {
|
||||
const match = data.p.match(/response\/fragments\/(\d+)\/content/);
|
||||
if (match) {
|
||||
const fragIdx = parseInt(match[1], 10);
|
||||
currentFragmentIndex = fragIdx;
|
||||
if (fragIdx === responseFragmentIndex) {
|
||||
textContent += data.v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 不带操作符的路径设置 (如 {"v": "xxx", "p": "response/fragments/1/content"})
|
||||
if (data.p && typeof data.v === 'string' && !data.o) {
|
||||
const match = data.p.match(/response\/fragments\/(\d+)\/content/);
|
||||
if (match) {
|
||||
const fragIdx = parseInt(match[1], 10);
|
||||
currentFragmentIndex = fragIdx;
|
||||
if (fragIdx === responseFragmentIndex) {
|
||||
textContent += data.v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fragments APPEND - 新增 fragment (非 BATCH)
|
||||
if (data.p === 'response/fragments' && data.o === 'APPEND' && Array.isArray(data.v)) {
|
||||
for (const fragment of data.v) {
|
||||
const idx = fragmentCount++;
|
||||
if (fragment.type === 'RESPONSE') {
|
||||
responseFragmentIndex = idx;
|
||||
currentFragmentIndex = idx;
|
||||
if (fragment.content) {
|
||||
textContent += fragment.content;
|
||||
}
|
||||
} else {
|
||||
// THINK 或 SEARCH
|
||||
currentFragmentIndex = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BATCH 操作中的 fragments
|
||||
if (data.o === 'BATCH' && data.p === 'response' && Array.isArray(data.v)) {
|
||||
for (const item of data.v) {
|
||||
// fragments 追加
|
||||
if (item.p === 'fragments' && item.o === 'APPEND' && Array.isArray(item.v)) {
|
||||
for (const fragment of item.v) {
|
||||
const idx = fragmentCount++;
|
||||
if (fragment.type === 'RESPONSE') {
|
||||
responseFragmentIndex = idx;
|
||||
currentFragmentIndex = idx;
|
||||
if (fragment.content) {
|
||||
textContent += fragment.content;
|
||||
}
|
||||
} else {
|
||||
// THINK 或 SEARCH
|
||||
currentFragmentIndex = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 检查是否完成
|
||||
if (item.p === 'status' && item.v === 'FINISHED') {
|
||||
isComplete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 忽略解析错误
|
||||
}
|
||||
}
|
||||
|
||||
return isComplete;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}, { timeout: 180000 });
|
||||
} catch (e) {
|
||||
const pageError = normalizePageError(e, meta);
|
||||
if (pageError) return pageError;
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (!textContent || textContent.trim() === '') {
|
||||
logger.warn('适配器', '回复内容为空', meta);
|
||||
return { error: '回复内容为空' };
|
||||
}
|
||||
|
||||
logger.info('适配器', `已获取文本内容 (${textContent.length} 字符)`, meta);
|
||||
logger.info('适配器', '文本生成完成,任务完成', meta);
|
||||
return { text: textContent.trim() };
|
||||
|
||||
} 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: 'deepseek_text',
|
||||
displayName: 'DeepSeek (文本生成)',
|
||||
|
||||
// 入口 URL
|
||||
getTargetUrl(config, workerConfig) {
|
||||
return TARGET_URL;
|
||||
},
|
||||
|
||||
// 模型列表
|
||||
models: [
|
||||
{ id: 'deepseek-v3.2', imagePolicy: 'forbidden' },
|
||||
{ id: 'deepseek-v3.2-thinking', imagePolicy: 'forbidden', thinking: true },
|
||||
{ id: 'deepseek-v3.2-search', imagePolicy: 'forbidden', search: true },
|
||||
{ id: 'deepseek-v3.2-thinking-search', imagePolicy: 'forbidden', thinking: true, search: true },
|
||||
],
|
||||
|
||||
// 无需导航处理器
|
||||
navigationHandlers: [],
|
||||
|
||||
// 核心文本生成方法
|
||||
generate
|
||||
};
|
||||
@@ -164,30 +164,79 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
await fillPrompt(page, INPUT_SELECTOR, prompt, meta);
|
||||
await sleep(500, 1000);
|
||||
|
||||
// 4. 设置请求拦截器 - 修改 chat/completions 请求中的 model 参数
|
||||
logger.debug('适配器', '已启用请求拦截', meta);
|
||||
await page.unroute('**/*').catch(() => { });
|
||||
// 4. 通过 UI 交互选择模型
|
||||
const modelConfig = manifest.models.find(m => m.id === modelId);
|
||||
const targetModel = modelConfig?.codeName || modelId;
|
||||
|
||||
await page.route(url => url.href.includes('chat/completions'), async (route) => {
|
||||
const request = route.request();
|
||||
if (request.method() !== 'POST') return route.continue();
|
||||
logger.debug('适配器', `正在选择模型: ${targetModel}`, meta);
|
||||
|
||||
try {
|
||||
const postData = request.postDataJSON();
|
||||
if (postData) {
|
||||
logger.debug('适配器', '已拦截 chat/completions 请求,正在修改 model...', meta);
|
||||
postData.model = modelId;
|
||||
logger.info('适配器', `已拦截请求,使用模型: ${modelId}`, meta);
|
||||
await route.continue({ postData: JSON.stringify(postData) });
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('适配器', '请求拦截处理失败', { ...meta, error: e.message });
|
||||
// 点击 "Select a models" 按钮
|
||||
const selectModelBtn = page.getByRole('button', { name: 'Select a model' });
|
||||
await selectModelBtn.waitFor({ timeout: 5000 });
|
||||
await sleep(300, 500);
|
||||
await safeClick(page, selectModelBtn, { bias: 'button' });
|
||||
await sleep(500, 800);
|
||||
|
||||
// 在 "Search In Models" 文本框中输入模型名称
|
||||
const searchInput = page.getByRole('textbox', { name: 'Search In Models' });
|
||||
await searchInput.waitFor({ timeout: 5000 });
|
||||
await searchInput.fill(targetModel);
|
||||
await sleep(300, 500);
|
||||
|
||||
// 按回车确认选择
|
||||
await searchInput.press('Enter');
|
||||
await sleep(500, 1000);
|
||||
|
||||
logger.info('适配器', `已选择模型: ${targetModel}`, meta);
|
||||
|
||||
// 5. 检查 GIF Generation 按钮状态,确保为 OFF
|
||||
const gifBtn = page.getByRole('button', { name: /^GIF Generation/ });
|
||||
const gifBtnExists = await gifBtn.count();
|
||||
if (gifBtnExists > 0) {
|
||||
const gifState = await gifBtn.evaluate(el => {
|
||||
const generic = el.querySelector('[role="generic"]') || el;
|
||||
return generic.textContent?.trim() || '';
|
||||
});
|
||||
|
||||
if (!gifState.includes('OFF')) {
|
||||
logger.debug('适配器', `GIF Generation 当前为 ${gifState},正在切换为 OFF`, meta);
|
||||
await safeClick(page, gifBtn, { bias: 'button' });
|
||||
await sleep(300, 500);
|
||||
}
|
||||
await route.continue();
|
||||
});
|
||||
}
|
||||
|
||||
// 5. 提交
|
||||
// 6. 设置图片大小 (如果模型配置了 imageSize)
|
||||
if (modelConfig?.imageSize) {
|
||||
const targetSize = modelConfig.imageSize; // 例如 "1K", "2K", "4K"
|
||||
logger.debug('适配器', `正在设置图片大小: ${targetSize}`, meta);
|
||||
|
||||
const imageSizeBtn = page.getByRole('button', { name: /^Image Size/ });
|
||||
const btnExists = await imageSizeBtn.count();
|
||||
|
||||
if (btnExists > 0) {
|
||||
// 最多点击 4 次切换
|
||||
for (let i = 0; i < 4; i++) {
|
||||
// 获取当前图片大小 (从按钮下的 generic 元素中的 text leaf 获取)
|
||||
const currentSize = await imageSizeBtn.evaluate(el => {
|
||||
const generic = el.querySelector('[role="generic"]') || el;
|
||||
return generic.textContent?.trim() || '';
|
||||
});
|
||||
|
||||
if (currentSize.includes(targetSize)) {
|
||||
logger.info('适配器', `图片大小已设置为: ${targetSize}`, meta);
|
||||
break;
|
||||
}
|
||||
|
||||
// 点击切换
|
||||
await safeClick(page, imageSizeBtn, { bias: 'button' });
|
||||
await sleep(300, 500);
|
||||
}
|
||||
} else {
|
||||
logger.debug('适配器', '未找到 Image Size 按钮', meta);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 提交
|
||||
logger.debug('适配器', '点击发送...', meta);
|
||||
await submit(page, {
|
||||
btnSelector: 'button[type="submit"]',
|
||||
@@ -197,7 +246,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
|
||||
logger.info('适配器', '等待生成结果中...', meta);
|
||||
|
||||
// 6. 等待 v1/chats/new 响应 (状态码 200 且响应体中有 id)
|
||||
// 8. 等待 v1/chats/new 响应 (状态码 200 且响应体中有 id)
|
||||
let chatsNewResponse;
|
||||
try {
|
||||
chatsNewResponse = await waitApiResponse(page, {
|
||||
@@ -231,7 +280,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
return { error: '解析对话响应失败' };
|
||||
}
|
||||
|
||||
// 7. 等待 chat/completions 响应 (状态码 200 且 status: true)
|
||||
// 9. 等待 chat/completions 响应 (状态码 200 且 status: true)
|
||||
let completionsResponse;
|
||||
try {
|
||||
completionsResponse = await waitApiResponse(page, {
|
||||
@@ -264,7 +313,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
return { error: '解析生成响应失败' };
|
||||
}
|
||||
|
||||
// 8. 等待 chat/completed 响应,从中提取图片链接
|
||||
// 10. 等待 chat/completed 响应,从中提取图片链接
|
||||
logger.debug('适配器', '正在等待完成响应...', meta);
|
||||
|
||||
let completedResponse;
|
||||
@@ -309,8 +358,8 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
return { error: '回复内容为空可能触发违规/限流' };
|
||||
}
|
||||
|
||||
// 从 content 中提取图片链接 (格式: )
|
||||
const imageUrlMatch = content.match(/!\[.*?\]\((https:\/\/zai\.is\/media\/[^)]+)\)/);
|
||||
// 从 content 中提取图片链接 (格式: )
|
||||
const imageUrlMatch = content.match(/!\[.*?\]\((https:\/\/zai\.is\/[^)]+)\)/);
|
||||
if (!imageUrlMatch || !imageUrlMatch[1]) {
|
||||
logger.warn('适配器', '回复中未找到图片链接', meta);
|
||||
return { error: '回复中未找到图片链接' };
|
||||
@@ -336,8 +385,6 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
logger.error('适配器', '生成任务失败', { ...meta, error: err.message });
|
||||
return { error: `生成任务失败: ${err.message}` };
|
||||
} finally {
|
||||
// 清理拦截器
|
||||
await page.unroute('**/*').catch(() => { });
|
||||
// 任务结束,将鼠标移至安全区域
|
||||
await moveMouseAway(page);
|
||||
}
|
||||
@@ -357,8 +404,10 @@ export const manifest = {
|
||||
|
||||
// 模型列表
|
||||
models: [
|
||||
{ id: 'gemini-3-pro-image-preview', imagePolicy: 'optional' },
|
||||
{ id: 'gemini-2.5-flash-image', imagePolicy: 'optional' }
|
||||
{ id: 'gemini-3-pro-image-preview', codeName: 'Nano Banana Pro', imagePolicy: 'optional', imageSize: '1K' },
|
||||
{ id: 'gemini-3-pro-image-preview-2k', codeName: 'Nano Banana Pro', imagePolicy: 'optional', imageSize: '2K' },
|
||||
{ id: 'gemini-3-pro-image-preview-4k', codeName: 'Nano Banana Pro', imagePolicy: 'optional', imageSize: '4K' },
|
||||
{ id: 'gemini-2.5-flash-image', codeName: 'Nano Banana', imagePolicy: 'optional' }
|
||||
],
|
||||
|
||||
// 导航处理器
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
/**
|
||||
* @fileoverview zAI 文本生成适配器
|
||||
*/
|
||||
|
||||
import {
|
||||
sleep,
|
||||
safeClick,
|
||||
pasteImages
|
||||
} from '../engine/utils.js';
|
||||
import {
|
||||
fillPrompt,
|
||||
submit,
|
||||
normalizePageError,
|
||||
normalizeHttpError,
|
||||
waitApiResponse,
|
||||
moveMouseAway,
|
||||
waitForPageAuth,
|
||||
lockPageAuth,
|
||||
unlockPageAuth,
|
||||
isPageAuthLocked,
|
||||
waitForInput,
|
||||
gotoWithCheck
|
||||
} from '../utils/index.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
// zai.is 输入框选择器
|
||||
const INPUT_SELECTOR = '.tiptap.ProseMirror';
|
||||
|
||||
// 入口 URL
|
||||
const TARGET_URL = 'https://zai.is/';
|
||||
|
||||
/**
|
||||
* 处理 Discord OAuth2 登录流程
|
||||
* @param {import('playwright-core').Page} page
|
||||
* @returns {Promise<boolean>} 是否处理了登录
|
||||
*/
|
||||
async function handleDiscordAuth(page) {
|
||||
// 防止重复处理
|
||||
if (isPageAuthLocked(page)) return false;
|
||||
|
||||
const currentUrl = page.url();
|
||||
|
||||
// 1. 检查是否在 zai.is/auth 页面
|
||||
if (currentUrl.includes('zai.is/auth')) {
|
||||
lockPageAuth(page);
|
||||
logger.info('适配器', '[登录器(zai)] 检测到登录页面,正在处理 Discord 登录...');
|
||||
|
||||
try {
|
||||
// 等待页面加载完成,点击唯一的 button 标签
|
||||
await page.waitForSelector('button', { timeout: 30000 });
|
||||
await sleep(1000, 1500);
|
||||
await safeClick(page, 'button', { bias: 'button' });
|
||||
logger.info('适配器', '[登录器(zai)] 已点击登录按钮,等待跳转到 Discord...');
|
||||
|
||||
// 2. 等待跳转到 Discord OAuth2 授权页面
|
||||
await page.waitForURL(url => url.href.includes('discord.com/oauth2/authorize'), { timeout: 60000 });
|
||||
logger.info('适配器', '[登录器(zai)] 已到达 Discord 授权页面');
|
||||
await sleep(2000, 3000);
|
||||
|
||||
// 3. 使用鼠标滚轮滚动 main 元素,直到授权按钮可用
|
||||
// 授权按钮选择器: data-align="stretch" 的 div 中的最后一个按钮 (授权按钮在右边)
|
||||
const authorizeBtnSelector = 'div[data-align="stretch"] button:last-child';
|
||||
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const authorizeBtn = await page.$(authorizeBtnSelector);
|
||||
if (authorizeBtn) {
|
||||
const isDisabled = await authorizeBtn.evaluate(el => el.disabled).catch(() => true);
|
||||
if (!isDisabled) {
|
||||
logger.info('适配器', '[登录器(zai)] 授权按钮已可用,正在点击...');
|
||||
await sleep(500, 1000);
|
||||
await safeClick(page, authorizeBtn, { bias: 'button' });
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 使用鼠标滚轮在 main 元素中滚动
|
||||
const mainElement = await page.$('main');
|
||||
if (mainElement) {
|
||||
const box = await mainElement.boundingBox();
|
||||
if (box) {
|
||||
// 将鼠标移动到 main 元素中心并滚动
|
||||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||||
await page.mouse.wheel(0, 200);
|
||||
}
|
||||
}
|
||||
await sleep(800, 1200);
|
||||
}
|
||||
|
||||
// 4. 等待跳转回 zai.is (不包含 auth 和 discord)
|
||||
logger.info('适配器', '[登录器(zai)] 等待跳转回目标页面...');
|
||||
await page.waitForURL(url => {
|
||||
const href = url.href;
|
||||
return href.includes('zai.is') &&
|
||||
!href.includes('/auth') &&
|
||||
!href.includes('discord.com');
|
||||
}, { timeout: 60000 });
|
||||
|
||||
logger.info('适配器', '[登录器(zai)] Discord 登录完成');
|
||||
await sleep(2000, 3000);
|
||||
unlockPageAuth(page);
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.warn('适配器', `[登录器(zai)] Discord 登录处理失败: ${err.message}`);
|
||||
unlockPageAuth(page);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 content 中移除开头的 <details> 思考块
|
||||
* @param {string} content - 原始内容
|
||||
* @returns {string} 处理后的内容
|
||||
*/
|
||||
function extractTextContent(content) {
|
||||
if (!content) return '';
|
||||
|
||||
// 匹配开头的 <details type="reasoning" ...>...</details> 块
|
||||
// 使用非贪婪匹配和 dotAll 模式 (s flag)
|
||||
const detailsPattern = /^<details\s+type="reasoning"[^>]*>[\s\S]*?<\/details>\s*/;
|
||||
|
||||
// 移除开头的 details 块,返回剩余内容
|
||||
const result = content.replace(detailsPattern, '').trim();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成文本
|
||||
* @param {object} context - 浏览器上下文 { page, client, config }
|
||||
* @param {string} prompt - 提示词
|
||||
* @param {string[]} imgPaths - 参考图片路径数组
|
||||
* @param {string} modelId - 模型 ID
|
||||
* @param {object} meta - 日志元数据
|
||||
* @returns {Promise<{text?: string, error?: string}>} 生成结果
|
||||
*/
|
||||
async function generate(context, prompt, imgPaths, modelId, meta = {}) {
|
||||
const { page, config } = context;
|
||||
|
||||
try {
|
||||
// 开启新对话 - 先等待可能正在进行的登录处理完成
|
||||
await waitForPageAuth(page);
|
||||
|
||||
logger.info('适配器', '开启新会话', meta);
|
||||
await gotoWithCheck(page, TARGET_URL);
|
||||
|
||||
// 如果触发了登录跳转,等待全局处理器完成
|
||||
await waitForPageAuth(page);
|
||||
|
||||
// 1. 等待输入框加载
|
||||
logger.debug('适配器', '正在寻找输入框...', meta);
|
||||
await waitForInput(page, INPUT_SELECTOR, { click: false });
|
||||
await sleep(1500, 2500);
|
||||
|
||||
// 2. 上传图片 (如果有多张图片,会一张一张上传,每次都是 v1/files POST 请求)
|
||||
if (imgPaths && imgPaths.length > 0) {
|
||||
const expectedUploads = imgPaths.length;
|
||||
let uploadedCount = 0;
|
||||
|
||||
await pasteImages(page, INPUT_SELECTOR, imgPaths, {
|
||||
uploadValidator: (response) => {
|
||||
const url = response.url();
|
||||
if (response.status() === 200 && url.includes('v1/files')) {
|
||||
uploadedCount++;
|
||||
logger.info('适配器', `图片上传进度: ${uploadedCount}/${expectedUploads}`, meta);
|
||||
if (uploadedCount >= 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. 通过 UI 交互选择模型
|
||||
const modelConfig = manifest.models.find(m => m.id === modelId);
|
||||
const targetModel = modelConfig?.codeName || modelId;
|
||||
|
||||
logger.debug('适配器', `正在选择模型: ${targetModel}`, meta);
|
||||
|
||||
// 点击 "Select a models" 按钮
|
||||
const selectModelBtn = page.getByRole('button', { name: 'Select a model' });
|
||||
await selectModelBtn.waitFor({ timeout: 5000 });
|
||||
await sleep(300, 500);
|
||||
await safeClick(page, selectModelBtn, { bias: 'button' });
|
||||
await sleep(500, 800);
|
||||
|
||||
// 在 "Search In Models" 文本框中输入模型名称
|
||||
const searchInput = page.getByRole('textbox', { name: 'Search In Models' });
|
||||
await searchInput.waitFor({ timeout: 5000 });
|
||||
await searchInput.fill(targetModel);
|
||||
await sleep(300, 500);
|
||||
|
||||
// 按回车确认选择
|
||||
await searchInput.press('Enter');
|
||||
await sleep(500, 1000);
|
||||
|
||||
logger.info('适配器', `已选择模型: ${targetModel}`, meta);
|
||||
|
||||
// 5. 提交
|
||||
logger.debug('适配器', '点击发送...', meta);
|
||||
await submit(page, {
|
||||
btnSelector: 'button[type="submit"]',
|
||||
inputTarget: INPUT_SELECTOR,
|
||||
meta
|
||||
});
|
||||
|
||||
logger.info('适配器', '等待生成结果中...', meta);
|
||||
|
||||
// 6. 等待 v1/chats/new 响应 (状态码 200 且响应体中有 id)
|
||||
let chatsNewResponse;
|
||||
try {
|
||||
chatsNewResponse = await waitApiResponse(page, {
|
||||
urlMatch: 'v1/chats/new',
|
||||
method: 'POST',
|
||||
timeout: 60000,
|
||||
meta
|
||||
});
|
||||
} catch (e) {
|
||||
const pageError = normalizePageError(e, meta);
|
||||
if (pageError) return pageError;
|
||||
throw e;
|
||||
}
|
||||
|
||||
// 检查 chats/new 响应
|
||||
const httpError = normalizeHttpError(chatsNewResponse);
|
||||
if (httpError) {
|
||||
logger.error('适配器', `创建对话失败: ${httpError.error}`, meta);
|
||||
return { error: `创建对话失败: ${httpError.error}` };
|
||||
}
|
||||
|
||||
try {
|
||||
const chatsNewBody = await chatsNewResponse.json();
|
||||
if (!chatsNewBody.id) {
|
||||
logger.error('适配器', '创建对话响应中没有无 id', meta);
|
||||
return { error: '创建对话响应中没有 id' };
|
||||
}
|
||||
logger.debug('适配器', `对话创建成功, id: ${chatsNewBody.id}`, meta);
|
||||
} catch (e) {
|
||||
logger.error('适配器', '解析 chats/new 响应失败', { ...meta, error: e.message });
|
||||
return { error: '解析对话响应失败' };
|
||||
}
|
||||
|
||||
// 7. 等待 chat/completions 响应 (状态码 200 且 status: true)
|
||||
let completionsResponse;
|
||||
try {
|
||||
completionsResponse = await waitApiResponse(page, {
|
||||
urlMatch: 'chat/completions',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
meta
|
||||
});
|
||||
} catch (e) {
|
||||
const pageError = normalizePageError(e, meta);
|
||||
if (pageError) return pageError;
|
||||
throw e;
|
||||
}
|
||||
|
||||
const completionsHttpError = normalizeHttpError(completionsResponse);
|
||||
if (completionsHttpError) {
|
||||
logger.error('适配器', `生成请求失败: ${completionsHttpError.error}`, meta);
|
||||
return { error: `生成请求失败: ${completionsHttpError.error}` };
|
||||
}
|
||||
|
||||
try {
|
||||
const completionsBody = await completionsResponse.json();
|
||||
if (!completionsBody.status) {
|
||||
logger.error('适配器', '生成响应 status 不为 true', meta);
|
||||
return { error: '生成失败,响应状态异常' };
|
||||
}
|
||||
logger.debug('适配器', '生成请求成功', meta);
|
||||
} catch (e) {
|
||||
logger.error('适配器', '解析 completions 响应失败', { ...meta, error: e.message });
|
||||
return { error: '解析生成响应失败' };
|
||||
}
|
||||
|
||||
// 8. 等待 chat/completed 响应,从中提取文本内容
|
||||
logger.debug('适配器', '正在等待完成响应...', meta);
|
||||
|
||||
let completedResponse;
|
||||
try {
|
||||
completedResponse = await waitApiResponse(page, {
|
||||
urlMatch: 'chat/completed',
|
||||
method: 'POST',
|
||||
timeout: 120000,
|
||||
meta
|
||||
});
|
||||
} catch (e) {
|
||||
const pageError = normalizePageError(e, meta);
|
||||
if (pageError) {
|
||||
if (e.name === 'TimeoutError') {
|
||||
return { error: '等待完成响应超时 (120秒)' };
|
||||
}
|
||||
return pageError;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
// 解析 chat/completed 响应
|
||||
let completedBody;
|
||||
try {
|
||||
completedBody = await completedResponse.json();
|
||||
} catch (e) {
|
||||
logger.error('适配器', '解析 chat/completed 响应失败', { ...meta, error: e.message });
|
||||
return { error: '解析完成响应失败' };
|
||||
}
|
||||
|
||||
// 在 messages 数组中查找匹配的消息 (id 与响应体的 id 相同)
|
||||
const targetMessage = (completedBody.messages || []).find(msg => msg.id === completedBody.id);
|
||||
if (!targetMessage) {
|
||||
logger.error('适配器', `未找到匹配的消息`, meta);
|
||||
return { error: '未找到匹配的消息' };
|
||||
}
|
||||
|
||||
// 检查 content
|
||||
const content = targetMessage.content;
|
||||
if (!content || content.trim() === '') {
|
||||
logger.warn('适配器', '回复内容为空可能触发违规/限流', meta);
|
||||
return { error: '回复内容为空可能触发违规/限流' };
|
||||
}
|
||||
|
||||
// 提取文本内容 (移除开头的 <details> 思考块)
|
||||
const textContent = extractTextContent(content);
|
||||
if (!textContent) {
|
||||
logger.warn('适配器', '提取文本内容为空', meta);
|
||||
return { error: '提取文本内容为空' };
|
||||
}
|
||||
|
||||
logger.info('适配器', `已提取文本内容 (${textContent.length} 字符)`, meta);
|
||||
logger.info('适配器', '文本生成完成,任务完成', meta);
|
||||
return { text: textContent };
|
||||
|
||||
} 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: 'zai_is_text',
|
||||
displayName: 'zAI (文本生成)',
|
||||
|
||||
// 入口 URL
|
||||
getTargetUrl(config, workerConfig) {
|
||||
return TARGET_URL;
|
||||
},
|
||||
|
||||
// 模型列表 - 文本生成模型
|
||||
models: [
|
||||
{ id: 'glm-4.6', codeName: 'GLM 4.6', imagePolicy: 'optional' },
|
||||
{ id: 'gemini-3-pro-preview', codeName: 'Gemini 3 Pro Preview', imagePolicy: 'optional' },
|
||||
{ id: 'gemini-2.5-pro', codeName: 'Gemini 2.5 Pro', imagePolicy: 'optional' },
|
||||
{ id: 'gemini-3-flash-preview', codeName: 'Gemini 3 Flash Preview', imagePolicy: 'optional' },
|
||||
{ id: 'claude-sonnet-4.5', codeName: 'Claude Sonnet 4.5', imagePolicy: 'optional' },
|
||||
{ id: 'claude-sonnet-4', codeName: 'Claude Sonnet 4', imagePolicy: 'optional' },
|
||||
{ id: 'claude-haiku-4.5', codeName: 'Claude Haiku 4.5', imagePolicy: 'optional' },
|
||||
{ id: 'gpt-5.1', codeName: 'GPT-5.1', imagePolicy: 'optional' },
|
||||
{ id: 'gpt-5', codeName: 'GPT-5', imagePolicy: 'optional' },
|
||||
{ id: 'gpt-4.1', codeName: 'GPT-4.1', imagePolicy: 'optional' },
|
||||
{ id: 'gpt-5.2', codeName: 'GPT-5.2 Chat', imagePolicy: 'optional' },
|
||||
{ id: 'o3-high', codeName: 'o3-high', imagePolicy: 'optional' },
|
||||
{ id: 'o3-mini', codeName: 'o3-mini', imagePolicy: 'optional' },
|
||||
{ id: 'o4-mini', codeName: 'o4-mini', imagePolicy: 'optional' },
|
||||
{ id: 'grok-4.1-fast', codeName: 'Grok 4.1 Fast', imagePolicy: 'optional' },
|
||||
{ id: 'grok-4', codeName: 'Grok 4', imagePolicy: 'optional' },
|
||||
{ id: 'kimi-k2-thinking', codeName: 'Kimi K2 Thinking', imagePolicy: 'optional' },
|
||||
],
|
||||
|
||||
// 导航处理器
|
||||
navigationHandlers: [handleDiscordAuth],
|
||||
|
||||
// 核心文本生成方法
|
||||
generate
|
||||
};
|
||||
Reference in New Issue
Block a user