diff --git a/README.md b/README.md index 89518d6..a5efc2c 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ | [**Nano Banana Free**](https://nanobananafree.ai/) | ❌ | ✅ | | [**zAI**](https://zai.is/) | ❌ | ✅ | `gemini-exp-1206` 等 | | [**Google Gemini**](https://gemini.google.com/) | ❌ | ✅ | +| [**ZenMux**](https://zenmux.ai/) | ✅ | ❌ | > [!NOTE] > **获取完整模型列表**: 通过 `GET /v1/models` 接口查看当前配置下所有可用模型及其详细信息。 diff --git a/src/backend/adapter/gemini.js b/src/backend/adapter/gemini.js index 54c4375..8938233 100644 --- a/src/backend/adapter/gemini.js +++ b/src/backend/adapter/gemini.js @@ -182,12 +182,6 @@ export const manifest = { { id: 'gemini-3-pro-image-preview', imagePolicy: 'optional' } ], - // 模型 ID 解析(直通) - resolveModelId(modelKey) { - const model = this.models.find(m => m.id === modelKey); - return model ? model.id : null; - }, - // 无需导航处理器 navigationHandlers: [], diff --git a/src/backend/adapter/gemini_biz.js b/src/backend/adapter/gemini_biz.js index 72fe271..a8ebb17 100644 --- a/src/backend/adapter/gemini_biz.js +++ b/src/backend/adapter/gemini_biz.js @@ -287,12 +287,6 @@ export const manifest = { { id: 'gemini-3-pro-image-preview', imagePolicy: 'optional' } ], - // 模型 ID 解析(直通) - resolveModelId(modelKey) { - const model = this.models.find(m => m.id === modelKey); - return model ? model.id : null; - }, - // 导航处理器 navigationHandlers: [handleAccountChooser], diff --git a/src/backend/adapter/gemini_biz_text.js b/src/backend/adapter/gemini_biz_text.js index 4232cff..08cf716 100644 --- a/src/backend/adapter/gemini_biz_text.js +++ b/src/backend/adapter/gemini_biz_text.js @@ -330,18 +330,14 @@ export const manifest = { models: [ { id: 'gemini-3-pro', imagePolicy: 'optional', type: 'text' }, { id: 'gemini-2.5-pro', imagePolicy: 'optional', type: 'text' }, + { id: 'gemini-3-flash-preview', imagePolicy: 'optional', type: 'text' }, { id: 'gemini-2.5-flash', imagePolicy: 'optional', type: 'text' }, { id: 'gemini-3-pro-grounding', imagePolicy: 'optional', type: 'text' }, { id: 'gemini-2.5-pro-grounding', imagePolicy: 'optional', type: 'text' }, - { id: 'gemini-2.5-flash-grounding', imagePolicy: 'optional', type: 'text' } + { id: 'gemini-2.5-flash-grounding', imagePolicy: 'optional', type: 'text' }, + { id: 'gemini-3-flash-preview-grounding', imagePolicy: 'optional', type: 'text' }, ], - // 模型 ID 解析(直通) - resolveModelId(modelKey) { - const model = this.models.find(m => m.id === modelKey); - return model ? model.id : null; - }, - // 导航处理器 navigationHandlers: [handleAccountChooser], diff --git a/src/backend/adapter/lmarena.js b/src/backend/adapter/lmarena.js index f3ca5e1..3c8c386 100644 --- a/src/backend/adapter/lmarena.js +++ b/src/backend/adapter/lmarena.js @@ -56,6 +56,10 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { const { page, config } = context; const textareaSelector = 'textarea'; + // Worker 已验证,直接解析模型配置 + const modelConfig = manifest.models.find(m => m.id === modelId); + const codeName = modelConfig?.codeName; + try { logger.info('适配器', '开启新会话...', meta); await gotoWithCheck(page, TARGET_URL); @@ -73,10 +77,10 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { await safeClick(page, textareaSelector, { bias: 'input' }); await fillPrompt(page, textareaSelector, prompt, meta); - // 4. 配置请求拦截 (用于修改模型 ID) + // 4. 配置请求拦截 (用于修改模型 ID 为 codeName) await page.unroute('**/*').catch(() => { }); - if (modelId) { + if (codeName) { logger.debug('适配器', `准备拦截请求`, meta); await page.route(url => url.href.includes('/nextjs-api/stream'), async (route) => { const request = route.request(); @@ -85,8 +89,8 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { try { const postData = request.postDataJSON(); if (postData && postData.modelAId) { - logger.info('适配器', `已拦截请求并修改模型: ${postData.modelAId} -> ${modelId}`, meta); - postData.modelAId = modelId; + logger.info('适配器', `已拦截请求并修改模型: ${postData.modelAId} -> ${codeName}`, meta); + postData.modelAId = codeName; await route.continue({ postData: JSON.stringify(postData) }); return; } @@ -163,7 +167,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { return { error: `生成任务失败: ${err.message}` }; } finally { // 清理拦截器 - if (modelId) await page.unroute('**/*').catch(() => { }); + if (codeName) await page.unroute('**/*').catch(() => { }); // 任务结束,将鼠标移至安全区域 await moveMouseAway(page); @@ -230,12 +234,6 @@ export const manifest = { { id: 'reve-fast-edit', codeName: '019a5675-0a56-7835-abdd-1cb9e7870afa', imagePolicy: 'required' } ], - // 模型 ID 解析 - resolveModelId(modelKey) { - const model = this.models.find(m => m.id === modelKey); - return model ? model.codeName : null; - }, - // 无需导航处理器 navigationHandlers: [], diff --git a/src/backend/adapter/lmarena_text.js b/src/backend/adapter/lmarena_text.js index c4eb331..9294822 100644 --- a/src/backend/adapter/lmarena_text.js +++ b/src/backend/adapter/lmarena_text.js @@ -13,7 +13,6 @@ import { waitApiResponse, normalizePageError, normalizeHttpError, - moveMouseAway, waitForInput, gotoWithCheck @@ -37,12 +36,13 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { const { page, config } = context; const textareaSelector = 'textarea'; - // 根据 modelId (codeName) 反查模型配置,判断是否为搜索模式 - const modelConfig = manifest.models.find(m => m.codeName === modelId) || {}; - const targetUrl = modelConfig.search ? TARGET_URL_SEARCH : TARGET_URL; + // Worker 已验证,直接解析模型配置 + const modelConfig = manifest.models.find(m => m.id === modelId); + const { codeName, search } = modelConfig || {}; + const targetUrl = search ? TARGET_URL_SEARCH : TARGET_URL; try { - logger.info('适配器', `开启新会话... (搜索模式: ${!!modelConfig.search})`, meta); + logger.info('适配器', `开启新会话... (搜索模式: ${!!search})`, meta); await gotoWithCheck(page, targetUrl); // 1. 等待输入框加载 @@ -58,10 +58,10 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { await safeClick(page, textareaSelector, { bias: 'input' }); await fillPrompt(page, textareaSelector, prompt, meta); - // 4. 配置请求拦截 (用于修改模型 ID) + // 4. 配置请求拦截 (用于修改模型 ID 为 codeName) await page.unroute('**/*').catch(() => { }); - if (modelId) { + if (codeName) { logger.debug('适配器', `准备拦截请求`, meta); await page.route(url => url.href.includes('/nextjs-api/stream'), async (route) => { const request = route.request(); @@ -70,8 +70,8 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { try { const postData = request.postDataJSON(); if (postData && postData.modelAId) { - logger.info('适配器', `已拦截请求并修改模型: ${postData.modelAId} -> ${modelId}`, meta); - postData.modelAId = modelId; + logger.info('适配器', `已拦截请求并修改模型: ${postData.modelAId} -> ${codeName}`, meta); + postData.modelAId = codeName; await route.continue({ postData: JSON.stringify(postData) }); return; } @@ -158,7 +158,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { return { error: `生成任务失败: ${err.message}` }; } finally { // 清理拦截器 - if (modelId) await page.unroute('**/*').catch(() => { }); + if (codeName) await page.unroute('**/*').catch(() => { }); // 任务结束,将鼠标移至安全区域 await moveMouseAway(page); @@ -280,6 +280,8 @@ export const manifest = { { id: 'gpt-5.2', codeName: '019b1448-d548-78f4-8b98-788d72cbd057', imagePolicy: 'optional', type: 'text' }, { id: 'glm-4.6v-flash', codeName: '019b1536-49c0-73b2-8d45-403b8571568d', imagePolicy: 'optional', type: 'text' }, { id: 'qwen3-omni-flash', codeName: '0199c9dc-e157-7458-bd49-5942363be215', imagePolicy: 'optional', type: 'text' }, + { id: 'mimo-vl-7b-rl-2508', codeName: '1c0259b5-dff7-48ce-bca1-b6957675463b', imagePolicy: 'optional', type: 'text' }, + { id: 'mimo-7b', codeName: 'ee3588cd-1fe1-484a-bcc9-f92065b8380c', imagePolicy: 'forbidden', type: 'text' }, { id: 'gemini-3-pro-grounding', codeName: '019abdb7-6957-71c1-96a2-bfa79e8a094f', imagePolicy: 'forbidden', type: 'text', search: true }, { id: 'gpt-5.1-search', codeName: '019abdb7-50a5-7c05-9308-4491d069578b', imagePolicy: 'forbidden', type: 'text', search: true }, { id: 'grok-4-fast-search', codeName: '9217ac2d-91bc-4391-aa07-b8f9e2cf11f2', imagePolicy: 'forbidden', type: 'text', search: true }, @@ -295,12 +297,6 @@ export const manifest = { { id: 'gpt-5.2-search', codeName: '019b1448-f74a-72de-b25d-8666618f8c5a', imagePolicy: 'forbidden', type: 'text', search: true } ], - // 模型 ID 解析 - resolveModelId(modelKey) { - const model = this.models.find(m => m.id === modelKey); - return model ? model.codeName : null; - }, - // 无需导航处理器 navigationHandlers: [], diff --git a/src/backend/adapter/nanobananafree_ai.js b/src/backend/adapter/nanobananafree_ai.js index d4e68b7..9cb2352 100644 --- a/src/backend/adapter/nanobananafree_ai.js +++ b/src/backend/adapter/nanobananafree_ai.js @@ -147,12 +147,6 @@ export const manifest = { { id: 'gemini-2.5-flash-image', imagePolicy: 'optional' } ], - // 模型 ID 解析(直通) - resolveModelId(modelKey) { - const model = this.models.find(m => m.id === modelKey); - return model ? model.id : null; - }, - // 无需导航处理器 navigationHandlers: [], diff --git a/src/backend/adapter/turnstile_test.js b/src/backend/adapter/turnstile_test.js index 7f2115c..f669087 100644 --- a/src/backend/adapter/turnstile_test.js +++ b/src/backend/adapter/turnstile_test.js @@ -206,11 +206,6 @@ export const manifest = { { id: 'cloudflare-turnstile', imagePolicy: 'forbidden', type: 'text' } ], - resolveModelId(modelKey) { - const model = this.models.find(m => m.id === modelKey); - return model ? model.id : null; - }, - navigationHandlers: [], generate }; diff --git a/src/backend/adapter/zai_is.js b/src/backend/adapter/zai_is.js index 0fa09a3..6ec602d 100644 --- a/src/backend/adapter/zai_is.js +++ b/src/backend/adapter/zai_is.js @@ -361,12 +361,6 @@ export const manifest = { { id: 'gemini-2.5-flash-image', imagePolicy: 'optional' } ], - // 模型 ID 解析(直通) - resolveModelId(modelKey) { - const model = this.models.find(m => m.id === modelKey); - return model ? model.id : null; - }, - // 导航处理器 navigationHandlers: [handleDiscordAuth], diff --git a/src/backend/adapter/zenmux_ai.js b/src/backend/adapter/zenmux_ai.js new file mode 100644 index 0000000..3ebc219 --- /dev/null +++ b/src/backend/adapter/zenmux_ai.js @@ -0,0 +1,297 @@ +import { + sleep, + safeClick, + pasteImages +} from '../engine/utils.js'; +import { + fillPrompt, + submit, + normalizePageError, + normalizeHttpError, + waitApiResponse, + moveMouseAway, + waitForInput, + gotoWithCheck +} from '../utils/index.js'; +import { logger } from '../../utils/logger.js'; + +// Zenmux AI 输入框选择器 +const INPUT_SELECTOR = '.chat-input-container textarea'; +const SEND_BUTTON_SELECTOR = '.input-actions-send button'; + +/** + * 生成文本 + * @param {object} context - 浏览器上下文 { page, client, config } + * @param {string} prompt - 提示词 + * @param {string[]} imgPaths - 参考图片路径数组 + * @param {string} modelId - 模型 ID + * @returns {Promise<{text?: string, error?: string}>} 生成结果 + */ +async function generate(context, prompt, imgPaths, modelId, meta = {}) { + const { page } = context; + + try { + const targetUrl = 'https://zenmux.ai/settings/chat'; + + // 解析模型ID + const modelConfig = manifest.models.find(m => m.id === modelId); + const { codeName, providers } = modelConfig; + + // 导航到目标页面 + logger.info('适配器', '开启新会话', meta); + await gotoWithCheck(page, targetUrl); + + // 点击 New Chat 按钮开启新对话 + try { + const newChatBtn = page.locator('span').filter({ hasText: /New Chat/ }).locator('..').first(); + // 等待按钮出现(最多等待 5 秒) + await newChatBtn.waitFor({ state: 'visible', timeout: 5000 }); + await safeClick(page, newChatBtn, { bias: 'button' }); + logger.debug('适配器', '已点击 New Chat 按钮', meta); + await sleep(500, 1000); + } catch (e) { + logger.debug('适配器', `New Chat 按钮未找到或已在新会话中: ${e.message}`, meta); + } + + // 1. 等待输入框加载 + logger.debug('适配器', '正在寻找输入框...', meta); + await waitForInput(page, INPUT_SELECTOR, { click: false }); + await sleep(1000, 1500); + + // 2. 上传图片 (如果有) + if (imgPaths && imgPaths.length > 0) { + const expectedUploads = imgPaths.length; + let uploadedCount = 0; + + logger.info('适配器', `准备上传 ${expectedUploads} 张图片`, meta); + + await pasteImages(page, INPUT_SELECTOR, imgPaths, { + uploadValidator: (response) => { + const url = response.url(); + // 监听 oss/upload POST 请求 + if (response.request().method() === 'POST' && url.includes('oss/upload')) { + if (response.status() === 200) { + uploadedCount++; + logger.info('适配器', `图片上传进度: ${uploadedCount}/${expectedUploads}`, meta); + + // 所有图片上传完成 + if (uploadedCount >= expectedUploads) { + return true; + } + } + } + return false; + } + }); + + await sleep(1000, 2000); + logger.info('适配器', '图片上传完成', meta); + } + + // 3. 填写提示词 + await safeClick(page, INPUT_SELECTOR, { bias: 'input' }); + await fillPrompt(page, INPUT_SELECTOR, prompt, meta); + await sleep(500, 1000); + + // 4. 设置请求拦截器(修改模型ID和providers) + logger.debug('适配器', '已启用请求拦截', meta); + await page.unroute('**/*').catch(() => { }); + + await page.route(url => url.href.includes('v1/chat/completions'), async (route) => { + const request = route.request(); + if (request.method() !== 'POST') return route.continue(); + + try { + const postData = request.postDataJSON(); + if (postData) { + let modified = false; + + // 修改模型 ID(使用 codeName) + if (postData.model) { + logger.info('适配器', `已拦截请求,修改模型 ID: ${postData.model} -> ${codeName}`, meta); + postData.model = codeName; + modified = true; + } + + // 修改 providers(如果模型配置中有 providers) + if (providers && providers.length > 0) { + if (!postData.provider) postData.provider = {}; + if (!postData.provider.routing) postData.provider.routing = {}; + + logger.info('适配器', `已拦截请求,修改 providers: ${JSON.stringify(postData.provider.routing.providers)} -> ${JSON.stringify(providers)}`, meta); + postData.provider.routing.providers = providers; + modified = true; + } + + if (modified) { + await route.continue({ postData: JSON.stringify(postData) }); + return; + } + } + } catch (e) { + logger.error('适配器', '请求拦截处理失败', { ...meta, error: e.message }); + } + await route.continue(); + }); + + // 5. 提交 + logger.debug('适配器', '点击发送...', meta); + await submit(page, { + btnSelector: SEND_BUTTON_SELECTOR, + inputTarget: INPUT_SELECTOR, + meta + }); + + logger.info('适配器', '等待生成结果中...', meta); + + // 5. 等待 API 响应 + let apiResponse; + try { + apiResponse = await waitApiResponse(page, { + urlMatch: 'v1/chat/completions', + method: 'POST', + timeout: 120000, + meta + }); + } catch (e) { + const pageError = normalizePageError(e, meta); + if (pageError) return pageError; + throw e; + } + + // 检查 API 响应状态 + const httpError = normalizeHttpError(apiResponse); + if (httpError) { + logger.error('适配器', `请求生成时返回错误: ${httpError.error}`, meta); + return { error: `请求生成时返回错误: ${httpError.error}` }; + } + + // 6. 解析流式响应 + const content = await apiResponse.text(); + logger.debug('适配器', `收到响应,长度: ${content.length}`, meta); + + // 解析 EventStream 格式响应 + let fullText = ''; + try { + const lines = content.split('\n'); + + for (const line of lines) { + // 跳过空行和 [DONE] 标记 + if (!line.trim() || line.includes('[DONE]')) { + continue; + } + + // 解析 data: 开头的行 + if (line.startsWith('data: ')) { + const jsonStr = line.substring(6); // 去掉 "data: " 前缀 + + try { + const parsed = JSON.parse(jsonStr); + + // 提取 choices 中的 content + if (parsed.choices && Array.isArray(parsed.choices)) { + for (const choice of parsed.choices) { + const content = choice?.delta?.content; + + // 只提取有内容的文本(跳过空字符串和思考过程) + if (content && content.trim()) { + fullText += content; + } + } + } + } catch (parseErr) { + // 单个 JSON 解析失败不影响整体 + logger.debug('适配器', `解析单个数据块失败: ${parseErr.message}`, meta); + } + } + } + } catch (e) { + logger.error('适配器', '解析响应失败', { ...meta, error: e.message }); + return { error: `解析响应失败: ${e.message}` }; + } + + if (fullText) { + logger.info('适配器', `获取文本成功,长度: ${fullText.length}`, meta); + return { text: fullText }; + } else { + logger.warn('适配器', '未解析到有效文本内容', { ...meta, preview: content.substring(0, 200) }); + return { error: '未解析到有效文本内容' }; + } + + } catch (err) { + // 顶层错误处理 + const pageError = normalizePageError(err, meta); + if (pageError) return pageError; + + logger.error('适配器', '生成任务失败', { ...meta, error: err.message }); + return { error: `生成任务失败: ${err.message}` }; + } finally { + // 清理拦截器 + await page.unroute('**/*').catch(() => { }); + // 任务结束,将鼠标移至安全区域 + await moveMouseAway(page); + } +} + +/** + * 适配器 manifest + */ +export const manifest = { + id: 'zenmux_ai', + displayName: 'Zenmux AI', + + // 无需额外配置 + configSchema: [], + + // 入口 URL + getTargetUrl() { + return 'https://zenmux.ai/settings/chat'; + }, + + // 模型列表(仅支持非会员账户可用的模型) + models: [ + { id: 'gemini-3-flash-preview', codeName: 'google/gemini-3-flash-preview-free', imagePolicy: 'optional', type: 'text', providers: ["google-vertex"] }, + { id: 'mimo-v2-flash', codeName: 'xiaomi/mimo-v2-flash', imagePolicy: 'forbidden', type: 'text', providers: ["xiaomi"] }, + { id: 'glm-4.6v-flash', codeName: 'z-ai/glm-4.6v-flash', imagePolicy: 'optional', type: 'text', providers: ["z-ai"] }, + { id: 'mistral-large-2512', codeName: 'mistralai/mistral-large-2512', imagePolicy: 'optional', type: 'text', providers: ["azure"] }, + { id: 'deepseek-v3.2', codeName: 'deepseek/deepseek-chat', imagePolicy: 'forbidden', type: 'text', providers: ["deepseek"] }, + { id: 'deepseek-v3.2-thinking', codeName: 'deepseek/deepseek-reasoner', imagePolicy: 'forbidden', type: 'text', providers: ["deepseek"] }, + { id: 'grok-4.1-fast', codeName: 'x-ai/grok-4.1-fast', imagePolicy: 'optional', type: 'text', providers: ["x-ai"] }, + { id: 'grok-4.1-fast-non-reasoning', codeName: 'x-ai/grok-4.1-fast-non-reasoning', imagePolicy: 'optional', type: 'text', providers: ["x-ai"] }, + { id: 'gpt-5.1-codex-mini', codeName: 'openai/gpt-5.1-codex-mini', imagePolicy: 'optional', type: 'text', providers: ["openai"] }, + { id: 'ernie-5.0-thinking-preview', codeName: 'baidu/ernie-5.0-thinking-preview', imagePolicy: 'optional', type: 'text', providers: ["baidu"] }, + { id: 'doubao-seed-code', codeName: 'volcengine/doubao-seed-code', imagePolicy: 'optional', type: 'text', providers: ["volcengine"] }, + { id: 'kimi-k2-thinking', codeName: 'moonshotai/kimi-k2-thinking', imagePolicy: 'forbidden', type: 'text', providers: ["moonshotai"] }, + { id: 'minimax-m2', codeName: 'minimax/minimax-m2', imagePolicy: 'forbidden', type: 'text', providers: ["minimax"] }, + { id: 'kat-coder-pro-v1', codeName: 'kuaishou/kat-coder-pro-v1', imagePolicy: 'forbidden', type: 'text', providers: ["streamlake"] }, + { id: 'glm-4.6', codeName: 'z-ai/glm-4.6', imagePolicy: 'forbidden', type: 'text', providers: ["z-ai"] }, + { id: 'claude-sonnet-4.5', codeName: 'anthropic/claude-sonnet-4.5', imagePolicy: 'optional', type: 'text', providers: ["anthropic"] }, + { id: 'qwen3-max', codeName: 'qwen/qwen3-max', imagePolicy: 'forbidden', type: 'text', providers: ["alibaba"] }, + { id: 'grok-4-fast', codeName: 'x-ai/grok-4-fast', imagePolicy: 'optional', type: 'text', providers: ["x-ai"] }, + { id: 'grok-4-fast-non-reasoning', codeName: 'x-ai/grok-4-fast-non-reasoning', imagePolicy: 'optional', type: 'text', providers: ["x-ai"] }, + { id: 'grok-code-fast-1', codeName: 'x-ai/grok-code-fast-1', imagePolicy: 'forbidden', type: 'text', providers: ["x-ai"] }, + { id: 'deepseek-v3.1', codeName: 'deepseek/deepseek-chat-v3.1', imagePolicy: 'forbidden', type: 'text', providers: ["theta"] }, + { id: 'gpt-5-mini', codeName: 'openai/gpt-5-mini', imagePolicy: 'optional', type: 'text', providers: ["openai"] }, + { id: 'gpt-5-nano', codeName: 'openai/gpt-5-nano', imagePolicy: 'optional', type: 'text', providers: ["openai"] }, + { id: 'glm-4.5-air', codeName: 'z-ai/glm-4.5-air', imagePolicy: 'forbidden', type: 'text', providers: ["z-ai"] }, + { id: 'gemini-2.5-flash-lite', codeName: 'google/gemini-2.5-flash-lite', imagePolicy: 'optional', type: 'text', providers: ["google-vertex"] }, + { id: 'gemini-2.5-flash', codeName: 'google/gemini-2.5-flash', imagePolicy: 'optional', type: 'text', providers: ["google-vertex"] }, + { id: 'deepseek-r1-0528', codeName: 'deepseek/deepseek-r1-0528', imagePolicy: 'forbidden', type: 'text', providers: ["theta"] }, + { id: 'claude-sonnet-4', codeName: 'anthropic/claude-sonnet-4', imagePolicy: 'optional', type: 'text', providers: ["anthropic"] }, + { id: 'qwen3-14b', codeName: 'qwen/qwen3-14b', imagePolicy: 'optional', type: 'text', providers: ["theta"] }, + { id: 'o4-mini', codeName: 'openai/o4-mini', imagePolicy: 'optional', type: 'text', providers: ["openai"] }, + { id: 'gpt-4.1-mini', codeName: 'openai/gpt-4.1-mini', imagePolicy: 'optional', type: 'text', providers: ["openai"] }, + { id: 'gpt-4.1-nano', codeName: 'openai/gpt-4.1-nano', imagePolicy: 'optional', type: 'text', providers: ["openai"] }, + { id: 'gemini-2.0-flash-lite', codeName: 'google/gemini-2.0-flash-lite-001', imagePolicy: 'optional', type: 'text', providers: ["google-vertex"] }, + { id: 'claude-3.7-sonnet', codeName: 'anthropic/claude-3.7-sonnet', imagePolicy: 'optional', type: 'text', providers: ["anthropic"] }, + { id: 'gemini-2.0-flash', codeName: 'google/gemini-2.0-flash', imagePolicy: 'optional', type: 'text', providers: ["google-vertex"] }, + { id: 'claude-3.5-sonnet', codeName: 'anthropic/claude-3.5-sonnet', imagePolicy: 'optional', type: 'text', providers: ["anthropic"] }, + ], + + + // 无需导航处理器 + navigationHandlers: [], + + // 核心生成方法 + generate +}; diff --git a/src/backend/index.js b/src/backend/index.js index 69ed87d..59c5811 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -5,7 +5,7 @@ * 对外统一能力: * - `initBrowser(cfg)` → 初始化 Pool * - `generate(ctx, prompt, imagePaths, modelId, meta)` - * - `resolveModelId(modelKey)` / `getModels()` / `getImagePolicy(modelKey)` + * - `getModels()` / `getImagePolicy(modelKey)` / `getModelType(modelKey)` * - `getCookies(workerName, domain)` - 获取指定 Worker 的 Cookies */ @@ -74,19 +74,6 @@ export function getBackend() { return await poolManager.generate(ctx, prompt, paths, modelId, meta); }, - /** - * 解析模型 ID - * @param {string} modelKey - 模型 key - * @returns {string|null} - */ - resolveModelId: (modelKey) => { - if (!poolManager) { - logger.warn('适配器', 'resolveModelId 调用时 Pool 未初始化'); - return null; - } - return poolManager.resolveModelId(modelKey); - }, - /** * 获取模型列表 * @returns {object} diff --git a/src/backend/pool/PoolManager.js b/src/backend/pool/PoolManager.js index 4176e35..683e60e 100644 --- a/src/backend/pool/PoolManager.js +++ b/src/backend/pool/PoolManager.js @@ -220,19 +220,6 @@ export class PoolManager { } } - /** - * 解析模型 ID - */ - resolveModelId(modelKey) { - for (const worker of this.workers) { - const resolved = worker.resolveModelId(modelKey); - if (resolved) { - return `${worker.name}|${resolved.type}|${resolved.realId}`; - } - } - return null; - } - /** * 获取所有模型列表 */ diff --git a/src/backend/pool/Worker.js b/src/backend/pool/Worker.js index 5de1662..4e4aee3 100644 --- a/src/backend/pool/Worker.js +++ b/src/backend/pool/Worker.js @@ -184,58 +184,48 @@ export class Worker { */ supports(modelId) { if (this.type === 'merge') { + // 检查任一适配器是否支持该模型 for (const type of this.mergeTypes) { - const resolved = registry.resolveModelId(type, modelId); - if (resolved) return true; + if (registry.supportsModel(type, modelId)) return true; } + // 支持 type/model 格式 if (modelId.includes('/')) { - const [specifiedType] = modelId.split('/', 2); - return this.mergeTypes.includes(specifiedType); + const [specifiedType, actualModel] = modelId.split('/', 2); + if (this.mergeTypes.includes(specifiedType)) { + return registry.supportsModel(specifiedType, actualModel); + } } return false; } else { + // 支持 type/model 格式 if (modelId.includes('/')) { const [specifiedType, actualModel] = modelId.split('/', 2); if (specifiedType === this.type) { - const resolved = registry.resolveModelId(this.type, actualModel); - return !!resolved; + return registry.supportsModel(this.type, actualModel); } return false; } - return !!registry.resolveModelId(this.type, modelId); + return registry.supportsModel(this.type, modelId); } } /** - * 解析模型 ID + * 确定模型对应的适配器类型(内部辅助方法) + * @private */ - resolveModelId(modelKey) { + _getAdapterType(modelKey) { if (this.type === 'merge') { if (modelKey.includes('/')) { - const [specifiedType, actualModel] = modelKey.split('/', 2); - if (this.mergeTypes.includes(specifiedType)) { - const realId = registry.resolveModelId(specifiedType, actualModel); - if (realId) return { type: specifiedType, realId }; - } - return null; + const [specifiedType] = modelKey.split('/', 2); + return this.mergeTypes.includes(specifiedType) ? specifiedType : this.mergeTypes[0]; } + // 找到第一个支持该模型的适配器 for (const type of this.mergeTypes) { - const realId = registry.resolveModelId(type, modelKey); - if (realId) return { type, realId }; + if (registry.supportsModel(type, modelKey)) return type; } - return null; - } else { - if (modelKey.includes('/')) { - const [specifiedType, actualModel] = modelKey.split('/', 2); - if (specifiedType === this.type) { - const realId = registry.resolveModelId(this.type, actualModel); - return realId ? { type: this.type, realId } : null; - } - return null; - } - const realId = registry.resolveModelId(this.type, modelKey); - return realId ? { type: this.type, realId } : null; + return this.mergeTypes[0]; } + return this.type; } /** @@ -249,13 +239,23 @@ export class Worker { return this._generateWithFailover(ctx, prompt, paths, modelId, meta, failoverConfig); } - const resolved = this.resolveModelId(modelId); - if (!resolved) { + // 验证是否支持该模型 + if (!this.supports(modelId)) { return { error: `Worker [${this.name}] 不支持模型: ${modelId}` }; } - const { type, realId } = resolved; - return this._executeAdapter(ctx, type, realId, prompt, paths, meta); + // 确定适配器类型 + const type = this._getAdapterType(modelId); + + // 处理 type/model 格式,提取实际 modelId + let actualModelId = modelId; + if (modelId.includes('/')) { + const parts = modelId.split('/', 2); + actualModelId = parts[1]; + } + + // 传递原始 modelId 给适配器,由适配器自己解析 + return this._executeAdapter(ctx, type, actualModelId, prompt, paths, meta); } /** @@ -274,8 +274,8 @@ export class Worker { let lastError = null; for (let i = 0; i < maxAttempts; i++) { - const { type, realId } = candidateTypes[i]; - const result = await this._executeAdapter(ctx, type, realId, prompt, paths, meta); + const { type, modelId: actualModelId } = candidateTypes[i]; + const result = await this._executeAdapter(ctx, type, actualModelId, prompt, paths, meta); if (!result.error) { return result; @@ -299,19 +299,16 @@ export class Worker { if (modelKey.includes('/')) { const [specifiedType, actualModel] = modelKey.split('/', 2); - if (this.mergeTypes.includes(specifiedType)) { - const realId = registry.resolveModelId(specifiedType, actualModel); - if (realId) { - candidates.push({ type: specifiedType, realId }); - } + if (this.mergeTypes.includes(specifiedType) && registry.supportsModel(specifiedType, actualModel)) { + candidates.push({ type: specifiedType, modelId: actualModel }); } return candidates; } + // 收集所有支持该模型的适配器 for (const type of this.mergeTypes) { - const realId = registry.resolveModelId(type, modelKey); - if (realId) { - candidates.push({ type, realId }); + if (registry.supportsModel(type, modelKey)) { + candidates.push({ type, modelId: modelKey }); } } @@ -322,13 +319,13 @@ export class Worker { * 执行单个适配器 * @private */ - async _executeAdapter(ctx, type, realId, prompt, paths, meta) { + async _executeAdapter(ctx, type, modelId, prompt, paths, meta) { const adapter = registry.getAdapter(type); if (!adapter) { return { error: `适配器不存在: ${type}` }; } - logger.info('工作池', `[${this.name}] 执行任务 -> ${type}/${realId}`, meta); + logger.info('工作池', `[${this.name}] 执行任务 -> ${type}/${modelId}`, meta); const subContext = { ...ctx, @@ -340,7 +337,8 @@ export class Worker { this.busyCount++; try { - return await adapter.generate(subContext, prompt, paths, realId, meta); + // 传递原始 modelId,由适配器自己解析 + return await adapter.generate(subContext, prompt, paths, modelId, meta); } finally { this.busyCount--; } @@ -416,8 +414,7 @@ export class Worker { } // 收集所有支持该模型的适配器的 imagePolicy for (const type of this.mergeTypes) { - const realId = registry.resolveModelId(type, modelKey); - if (realId) { + if (registry.supportsModel(type, modelKey)) { policies.add(registry.getImagePolicy(type, modelKey)); } } @@ -444,8 +441,9 @@ export class Worker { } } for (const type of this.mergeTypes) { - const realId = registry.resolveModelId(type, modelKey); - if (realId) return registry.getModelType(type, modelKey); + if (registry.supportsModel(type, modelKey)) { + return registry.getModelType(type, modelKey); + } } return 'image'; } else { diff --git a/src/backend/registry.js b/src/backend/registry.js index 738bd4d..d3f50f9 100644 --- a/src/backend/registry.js +++ b/src/backend/registry.js @@ -205,21 +205,34 @@ class AdapterRegistry { } /** - * 解析模型 ID + * 检查适配器是否支持指定模型 + * @param {string} adapterId - 适配器 ID + * @param {string} modelId - 模型 ID + * @returns {boolean} + */ + supportsModel(adapterId, modelId) { + const adapter = this.getAdapter(adapterId); + if (!adapter?.models) return false; + return adapter.models.some(m => m.id === modelId); + } + + /** + * 解析模型 ID(保留用于向后兼容) * @param {string} adapterId - 适配器 ID * @param {string} modelKey - 模型 key - * @returns {string|null} 内部 ID,或 null + * @returns {string|null} codeName,或 null + * @deprecated 新架构下适配器自己解析,此方法主要用于向后兼容 */ resolveModelId(adapterId, modelKey) { const adapter = this.getAdapter(adapterId); if (!adapter) return null; - // 如果适配器提供了自定义解析函数 + // 如果适配器还提供了 resolveModelId 函数,调用它 if (typeof adapter.resolveModelId === 'function') { return adapter.resolveModelId(modelKey); } - // 默认行为:检查 modelKey 是否在 models 中 + // 默认行为:查找模型并返回 codeName const model = adapter.models.find(m => m.id === modelKey); if (model) { return model.codeName || model.id; diff --git a/src/server/api/openai/parse.js b/src/server/api/openai/parse.js index 4bd6355..f40333c 100644 --- a/src/server/api/openai/parse.js +++ b/src/server/api/openai/parse.js @@ -54,7 +54,7 @@ function parseError(code, customMessage) { * @param {string} options.tempDir - 临时目录路径 * @param {number} options.imageLimit - 图片数量限制 * @param {string} options.backendName - 后端名称 - * @param {Function} options.resolveModelId - 模型 ID 解析函数 + * @param {Function} options.getSupportedModels - 获取支持的模型列表函数 * @param {Function} options.getImagePolicy - 获取图片策略函数 * @param {Function} options.getModelType - 获取模型类型函数 * @param {string} options.requestId - 请求 ID @@ -66,7 +66,7 @@ export async function parseRequest(data, options) { tempDir, imageLimit, backendName, - resolveModelId, + getSupportedModels, getImagePolicy, getModelType, requestId, @@ -86,8 +86,11 @@ export async function parseRequest(data, options) { let isTextMode = false; if (data.model) { - const resolved = resolveModelId(data.model); - if (resolved) { + // 检查模型是否在支持列表中 + const supportedModels = getSupportedModels(); + const isSupported = supportedModels.data.some(m => m.id === data.model); + + if (isSupported) { modelKey = data.model; logger.info('服务器', `触发模型: ${data.model}`, { id: requestId }); diff --git a/src/server/api/openai/routes.js b/src/server/api/openai/routes.js index 26287ad..ff71b58 100644 --- a/src/server/api/openai/routes.js +++ b/src/server/api/openai/routes.js @@ -18,7 +18,6 @@ export function createOpenAIRouter(context) { const { backendName, getModels, - resolveModelId, getImagePolicy, getModelType, tempDir, @@ -107,7 +106,7 @@ export function createOpenAIRouter(context) { tempDir, imageLimit, backendName, - resolveModelId, + getSupportedModels: getModels, getImagePolicy, getModelType, requestId, diff --git a/src/server/server.js b/src/server/server.js index 28e03fd..3d81478 100644 --- a/src/server/server.js +++ b/src/server/server.js @@ -46,7 +46,6 @@ const { initBrowser, generate, TEMP_DIR, - resolveModelId, getModels, getImagePolicy, getModelType @@ -115,7 +114,6 @@ const handleRequest = createGlobalRouter({ authToken: AUTH_TOKEN, backendName, getModels, - resolveModelId, getImagePolicy, getModelType, tempDir: TEMP_DIR,