mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 完善 LMArena 文本模型的列表
This commit is contained in:
@@ -281,7 +281,19 @@ 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: 'gemini-3-pro-grounding', codeName: '019abdb7-6957-71c1-96a2-bfa79e8a094f', imagePolicy: 'optional', type: 'text', search: true }
|
||||
{ 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 },
|
||||
{ id: 'gemini-2.5-pro-grounding', codeName: 'b222be23-bd55-4b20-930b-a30cc84d3afd', imagePolicy: 'forbidden', type: 'text', search: true },
|
||||
{ id: 'o3-search', codeName: 'fbe08e9a-3805-4f9f-a085-7bc38e4b51d1', imagePolicy: 'forbidden', type: 'text', search: true },
|
||||
{ id: 'grok-4-search', codeName: '86d767b0-2574-4e47-a256-a22bcace9f56', imagePolicy: 'forbidden', type: 'text', search: true },
|
||||
{ id: 'ppl-sonar-reasoning-pro-high', codeName: '24145149-86c9-4690-b7c9-79c7db216e5c', imagePolicy: 'forbidden', type: 'text', search: true },
|
||||
{ id: 'claude-opus-4-1-search', codeName: 'd942b564-191c-41c5-ae22-400a930a2cfe', imagePolicy: 'forbidden', type: 'text', search: true },
|
||||
{ id: 'gpt-5-search', codeName: 'd14d9b23-1e46-4659-b157-a3804ba7e2ef', imagePolicy: 'forbidden', type: 'text', search: true },
|
||||
{ id: 'claude-opus-4-search', codeName: '25bcb878-749e-49f4-ac05-de84d964bcee', imagePolicy: 'forbidden', type: 'text', search: true },
|
||||
{ id: 'diffbot-small-xl', codeName: '0862885e-ef53-4d0d-b9c4-4c8f68f453ce', imagePolicy: 'forbidden', type: 'text', search: true },
|
||||
{ id: 'grok-4-1-fast-search', codeName: '019af19c-0658-7566-9c60-112ae5bdb8db', imagePolicy: 'forbidden', type: 'text', search: true },
|
||||
{ id: 'gpt-5.2-search', codeName: '019b1448-f74a-72de-b25d-8666618f8c5a', imagePolicy: 'forbidden', type: 'text', search: true }
|
||||
],
|
||||
|
||||
// 模型 ID 解析
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
这是一个非常好的架构升级思路。为了支持文本模型(Chat Model)并构建“虚拟上下文”,我们需要重构 `src/server/parseChat.js`,让它具备根据模型类型(图片 vs 文本)采用不同解析策略的能力。
|
||||
|
||||
以下是具体的修改方案:
|
||||
|
||||
### 修改思路
|
||||
|
||||
1. **区分模型类型**:我们需要知道当前请求的模型是“生图模型”还是“文本模型”。
|
||||
* *方案*:在 `registry.js` 中为模型定义增加 `type` 字段(如 `type: 'text'`),或者在 `parseChat.js` 中简单地通过判断模型 ID 是否包含特定关键字(如 `gpt-4`, `claude`)来区分。为了演示方便,我将在代码中添加一个简单的判断逻辑。
|
||||
2. **文本模型解析逻辑**:
|
||||
* 不再只取最后一条消息。
|
||||
* 遍历 `messages` 数组。
|
||||
* 提取 `system` 消息作为“系统指令”。
|
||||
* 提取之前的 `user` 和 `assistant` 消息作为“历史对话”。
|
||||
* 提取最后一条 `user` 消息作为“当前输入”。
|
||||
* **图片处理**:在遍历过程中,遇到图片不仅要保存文件,还要在文本中插入 `[图片1]` 占位符,并将图片路径收集到 `imagePaths` 数组中。
|
||||
|
||||
### 修改代码
|
||||
|
||||
你需要修改 `src/server/parseChat.js`。
|
||||
|
||||
#### 1\. 新增辅助函数 `buildTextContext`
|
||||
|
||||
这个函数负责将 OpenAI 格式的 `messages` 数组转换为你要求的“虚拟上下文”字符串格式。
|
||||
|
||||
#### 2\. 改造 `parseRequest` 主函数
|
||||
|
||||
增加分支逻辑:如果是文本模型走新逻辑,如果是生图模型走原有逻辑(只取最后一条)。
|
||||
|
||||
以下是完整的修改后的 `src/server/parseChat.js` 代码:
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* @fileoverview 请求解析模块
|
||||
* @description 负责解析聊天请求、提取提示词和处理图片
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import sharp from 'sharp';
|
||||
import { IMAGE_POLICY } from '../backend/registry.js';
|
||||
import { ERROR_CODES, getErrorMessage } from './errors.js';
|
||||
|
||||
/**
|
||||
* 构造解析错误结果
|
||||
*/
|
||||
function parseError(code, customMessage) {
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code,
|
||||
error: customMessage || getErrorMessage(code)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为文本模型 (根据模型ID特征简单判断,你可以根据 registry 里的配置来判断)
|
||||
* @param {string} modelId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isTextModel(modelId) {
|
||||
if (!modelId) return false;
|
||||
// 这里可以根据实际情况修改,比如检查 registry.getModelType(modelId)
|
||||
// 示例:如果 ID 包含 gpt, claude, llama, qwen 且不是 image 模型,则认为是文本模型
|
||||
// 现有的图片模型通常有 image 关键字,或者在 registry 中明确定义。
|
||||
// 为了简单起见,假设除了已知的生图模型外,其他都是文本模型,或者你可以维护一个列表。
|
||||
const textKeywords = ['gpt-4', 'claude', 'llama', 'qwen', 'deepseek', 'gemini-pro'];
|
||||
// 注意:gemini-3-pro-image-preview 是生图的,需排除
|
||||
if (modelId.includes('image') || modelId.includes('flux') || modelId.includes('seedream')) return false;
|
||||
|
||||
return textKeywords.some(k => modelId.includes(k));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析聊天请求
|
||||
*/
|
||||
export async function parseRequest(data, options) {
|
||||
const {
|
||||
tempDir,
|
||||
imageLimit,
|
||||
backendName,
|
||||
resolveModelId,
|
||||
getImagePolicy,
|
||||
requestId,
|
||||
logger
|
||||
} = options;
|
||||
|
||||
const messages = data.messages;
|
||||
const isStreaming = data.stream === true;
|
||||
|
||||
// 验证 messages
|
||||
if (!messages || messages.length === 0) {
|
||||
return parseError(ERROR_CODES.NO_MESSAGES);
|
||||
}
|
||||
|
||||
// 1. 解析模型参数
|
||||
let modelKey = null;
|
||||
let isTextMode = false;
|
||||
|
||||
if (data.model) {
|
||||
const resolved = resolveModelId(data.model);
|
||||
if (resolved) {
|
||||
modelKey = data.model;
|
||||
logger.info('服务器', `触发模型: ${data.model}`, { id: requestId });
|
||||
|
||||
// 判定是否为文本模式 (你需要根据实际情况实现 isTextModel 或传入 getModelType)
|
||||
// 这里假设如果不包含特定生图关键词就是文本模型,或者你可以扩展 options 传入判断函数
|
||||
isTextMode = isTextModel(data.model);
|
||||
if (isTextMode) {
|
||||
logger.info('服务器', '解析模式: 文本对话 (虚拟上下文构建)', { id: requestId });
|
||||
} else {
|
||||
logger.info('服务器', '解析模式: 图像生成 (仅取最后一条)', { id: requestId });
|
||||
}
|
||||
|
||||
} else {
|
||||
return parseError(ERROR_CODES.INVALID_MODEL, `模型无效/后端 ${backendName} 不支持: ${data.model}`);
|
||||
}
|
||||
} else {
|
||||
logger.info('服务器', '未指定模型,使用网页默认', { id: requestId });
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 分支 A: 文本模型解析 (构建虚拟上下文)
|
||||
// ============================================================
|
||||
if (isTextMode) {
|
||||
return await parseTextRequest(messages, tempDir, imageLimit, modelKey, isStreaming);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 分支 B: 生图模型解析 (原有逻辑,只取最后一条 User 消息)
|
||||
// ============================================================
|
||||
return await parseImageRequest(messages, tempDir, imageLimit, modelKey, isStreaming, getImagePolicy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文本请求 (构建虚拟上下文)
|
||||
*/
|
||||
async function parseTextRequest(messages, tempDir, imageLimit, modelId, isStreaming) {
|
||||
let systemPrompt = '';
|
||||
let historyPrompt = '';
|
||||
let currentPrompt = '';
|
||||
|
||||
const imagePaths = [];
|
||||
let globalImageCount = 0;
|
||||
|
||||
// 辅助函数:处理单条消息内容
|
||||
async function processContent(content) {
|
||||
let textBuffer = '';
|
||||
if (typeof content === 'string') {
|
||||
textBuffer += content;
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const item of content) {
|
||||
if (item.type === 'text') {
|
||||
textBuffer += item.text;
|
||||
} else if (item.type === 'image_url' && item.image_url?.url) {
|
||||
globalImageCount++;
|
||||
|
||||
// 图片数量限制检查
|
||||
if (imageLimit > 0 && globalImageCount > imageLimit) {
|
||||
textBuffer += `[图片${globalImageCount} (已忽略:超过限制)]`;
|
||||
continue;
|
||||
}
|
||||
|
||||
const url = item.image_url.url;
|
||||
if (url.startsWith('data:image')) {
|
||||
const imagePath = await saveBase64Image(url, tempDir);
|
||||
if (imagePath) {
|
||||
imagePaths.push(imagePath);
|
||||
// 插入占位符
|
||||
textBuffer += `[图片${globalImageCount}]`;
|
||||
} else {
|
||||
textBuffer += `[图片${globalImageCount} (上传失败)]`;
|
||||
}
|
||||
} else {
|
||||
textBuffer += `[图片${globalImageCount} (无效链接)]`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return textBuffer;
|
||||
}
|
||||
|
||||
// 1. 提取 System Prompt
|
||||
const systemMsg = messages.find(m => m.role === 'system');
|
||||
if (systemMsg) {
|
||||
const content = await processContent(systemMsg.content);
|
||||
if (content) {
|
||||
systemPrompt = `=== 系统指令 (永远置顶) ===\n${content}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 区分历史和当前消息
|
||||
// 找到最后一条 user 消息的索引
|
||||
let lastUserIndex = -1;
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
if (messages[i].role === 'user') {
|
||||
lastUserIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (lastUserIndex === -1) {
|
||||
return parseError(ERROR_CODES.NO_USER_MESSAGES);
|
||||
}
|
||||
|
||||
// 3. 构建历史对话 (不包含 system 和 最后一条 user)
|
||||
const historyMessages = messages.filter((m, index) => {
|
||||
return m.role !== 'system' && index < lastUserIndex;
|
||||
});
|
||||
|
||||
if (historyMessages.length > 0) {
|
||||
historyPrompt += `=== 历史对话 (滑动窗口或摘要) ===\n`;
|
||||
for (const msg of historyMessages) {
|
||||
const roleName = msg.role === 'user' ? 'User' : 'AI';
|
||||
const content = await processContent(msg.content);
|
||||
historyPrompt += `${roleName}: ${content}\n`;
|
||||
}
|
||||
historyPrompt += `\n`;
|
||||
}
|
||||
|
||||
// 4. 构建当前输入
|
||||
const lastUserMsg = messages[lastUserIndex];
|
||||
const currentContent = await processContent(lastUserMsg.content);
|
||||
currentPrompt = `=== 当前输入 ===\nUser: ${currentContent}`;
|
||||
|
||||
// 5. 合并最终 Prompt
|
||||
// 注意:这里我们告诉浏览器上传了 imagePaths 里的所有图片
|
||||
// 并且在 prompt 文本中已经用 [图片N] 标记了它们的位置
|
||||
const finalPrompt = systemPrompt + historyPrompt + currentPrompt;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
prompt: finalPrompt,
|
||||
imagePaths, // 包含历史和当前的图片路径,按顺序排列
|
||||
modelId,
|
||||
modelName: modelId,
|
||||
isStreaming
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析生图请求 (原有逻辑保持不变)
|
||||
*/
|
||||
async function parseImageRequest(messages, tempDir, imageLimit, modelId, isStreaming, getImagePolicy) {
|
||||
// 筛选用户消息
|
||||
const userMessages = messages.filter(m => m.role === 'user');
|
||||
if (userMessages.length === 0) {
|
||||
return parseError(ERROR_CODES.NO_USER_MESSAGES);
|
||||
}
|
||||
|
||||
const lastMessage = userMessages[userMessages.length - 1];
|
||||
|
||||
let prompt = '';
|
||||
const imagePaths = [];
|
||||
let imageCount = 0;
|
||||
|
||||
// 解析内容
|
||||
if (Array.isArray(lastMessage.content)) {
|
||||
for (const item of lastMessage.content) {
|
||||
if (item.type === 'text') {
|
||||
prompt += item.text + ' ';
|
||||
} else if (item.type === 'image_url' && item.image_url?.url) {
|
||||
imageCount++;
|
||||
|
||||
// 图片数量检查
|
||||
if (imageLimit <= 10) {
|
||||
if (imageCount > imageLimit) {
|
||||
return parseError(ERROR_CODES.TOO_MANY_IMAGES, `图片数量超过限制(最大 ${imageLimit} 张)`);
|
||||
}
|
||||
} else {
|
||||
if (imageCount > 10) continue;
|
||||
}
|
||||
|
||||
// 处理 data URL
|
||||
const url = item.image_url.url;
|
||||
if (url.startsWith('data:image')) {
|
||||
const imagePath = await saveBase64Image(url, tempDir);
|
||||
if (imagePath) {
|
||||
imagePaths.push(imagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
prompt = lastMessage.content;
|
||||
}
|
||||
|
||||
prompt = prompt.trim();
|
||||
|
||||
// 图片策略校验
|
||||
const hasImage = imagePaths.length > 0;
|
||||
const policy = modelId ? getImagePolicy(modelId) : IMAGE_POLICY.OPTIONAL;
|
||||
|
||||
if (policy === IMAGE_POLICY.REQUIRED && !hasImage) {
|
||||
return parseError(ERROR_CODES.IMAGE_REQUIRED, `模型 ${modelId} 需要参考图`);
|
||||
}
|
||||
|
||||
if (policy === IMAGE_POLICY.FORBIDDEN && hasImage) {
|
||||
return parseError(ERROR_CODES.IMAGE_FORBIDDEN, `模型 ${modelId} 不支持图片输入`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
prompt,
|
||||
imagePaths,
|
||||
modelId,
|
||||
modelName: modelId,
|
||||
isStreaming
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存 Base64 图片到临时文件
|
||||
*/
|
||||
async function saveBase64Image(dataUrl, tempDir) {
|
||||
const matches = dataUrl.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
|
||||
if (!matches || matches.length !== 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = Buffer.from(matches[2], 'base64');
|
||||
// 压缩图片
|
||||
const processedBuffer = await sharp(buffer)
|
||||
.jpeg({ quality: 90 })
|
||||
.toBuffer();
|
||||
|
||||
const filename = `img_${Date.now()}_${Math.random().toString(36).substring(7)}.jpg`;
|
||||
const filePath = path.join(tempDir, filename);
|
||||
fs.writeFileSync(filePath, processedBuffer);
|
||||
return filePath;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 配套修改建议
|
||||
|
||||
1. **修改 `src/backend/registry.js` (可选,推荐)**:
|
||||
在 `models` 数组中添加 `type: 'text'` 或 `type: 'image'` 字段,然后在 `getModelsForAdapter` 中暴露出来。这样 `parseChat.js` 就可以通过 `registry.getModelType(id)` 来精准判断,而不是靠猜。
|
||||
|
||||
2. **修改适配器代码**:
|
||||
目前的适配器(如 `gemini_biz.js`, `zai_is.js`)的 `generateImage` 函数接收的是:
|
||||
|
||||
```javascript
|
||||
async function generateImage(context, prompt, imgPaths, modelId, meta = {})
|
||||
```
|
||||
|
||||
对于文本模型,`prompt` 现在将是一个包含了系统指令、历史对话和当前输入的**长字符串**。`imgPaths` 将包含**所有**涉及到的图片。
|
||||
|
||||
* 适配器的逻辑通常是:先上传所有 `imgPaths`,然后在输入框填入 `prompt`,最后发送。
|
||||
* **注意**:你现有的 `gemini_biz.js` 和 `zai_is.js` 适配器逻辑是先 `pasteImages` 然后 `fillPrompt`。
|
||||
* 这对于新的文本逻辑**正好适用**!因为图片占位符 `[图片1]` 已经在 `prompt` 文本里了,而实际的图片文件通过 `imgPaths` 传给适配器上传,适配器上传后,输入框里会有图片缩略图,加上我们输入的带有 `[图片1]` 的长文本,AI 就能理解这个对应关系(或者至少作为上下文)。
|
||||
|
||||
### 效果预期
|
||||
|
||||
当客户端发送如下请求时:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gpt-4-text",
|
||||
"messages": [
|
||||
{"role": "system", "content": "你是一个猫娘"},
|
||||
{"role": "user", "content": "你好"},
|
||||
{"role": "assistant", "content": "喵~"},
|
||||
{"role": "user", "content": ["看看这个", {"type": "image_url", "url": "..."}]}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
服务器会解析为:
|
||||
|
||||
**prompt (传递给适配器输入框的文本):**
|
||||
|
||||
```text
|
||||
=== 系统指令 (永远置顶) ===
|
||||
你是一个猫娘
|
||||
|
||||
=== 历史对话 (滑动窗口或摘要) ===
|
||||
User: 你好
|
||||
AI: 喵~
|
||||
|
||||
=== 当前输入 ===
|
||||
User: 看看这个[图片1]
|
||||
```
|
||||
|
||||
**imagePaths (传递给适配器上传的文件):**
|
||||
`['/tmp/img_xxx.jpg']`
|
||||
|
||||
这样你就成功地在一个单轮对话的网页接口上,模拟出了带上下文和多模态理解的多轮对话体验。
|
||||
Reference in New Issue
Block a user