feat: 支持 ZenMux,删除多余的历史遗留设计

This commit is contained in:
foxhui
2025-12-21 02:37:50 +08:00
Unverified
parent c366418bfb
commit 0d4b5ecd76
17 changed files with 396 additions and 152 deletions
+1
View File
@@ -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` 接口查看当前配置下所有可用模型及其详细信息。
-6
View File
@@ -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: [],
-6
View File
@@ -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],
+3 -7
View File
@@ -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],
+9 -11
View File
@@ -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: [],
+12 -16
View File
@@ -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: [],
-6
View File
@@ -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: [],
-5
View File
@@ -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
};
-6
View File
@@ -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],
+297
View File
@@ -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
};
+1 -14
View File
@@ -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}
-13
View File
@@ -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;
}
/**
* 获取所有模型列表
*/
+48 -50
View File
@@ -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 {
+17 -4
View File
@@ -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;
+7 -4
View File
@@ -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 });
+1 -2
View File
@@ -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,
-2
View File
@@ -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,