mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 支持 ZenMux,删除多余的历史遗留设计
This commit is contained in:
@@ -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` 接口查看当前配置下所有可用模型及其详细信息。
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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],
|
||||
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user