diff --git a/.gitignore b/.gitignore index 63485fc..c375cbc 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,6 @@ node_modules .claude/ CLAUDE.md config.json -configs/pwd -configs/provider_pools.json provider_pools.json plugins.json fetch_system_prompt.txt diff --git a/configs/pwd b/configs/pwd new file mode 100644 index 0000000..32e9c62 --- /dev/null +++ b/configs/pwd @@ -0,0 +1 @@ +admin123 \ No newline at end of file diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs deleted file mode 100644 index 57c9887..0000000 --- a/ecosystem.config.cjs +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = { - apps: [ - { - name: 'aiclient-2-api', - script: './src/core/master.js', - cwd: '/root/.openclaw/workspace/projects/AIClient-2-API', - interpreter: '/usr/bin/node', - instances: 1, - exec_mode: 'fork', - autorestart: true, - watch: false, - max_memory_restart: '1G', - env: { - NODE_ENV: 'production' - }, - error_file: './logs/pm2-error.log', - out_file: './logs/pm2-out.log', - log_file: './logs/pm2-combined.log', - time: true, - merge_logs: true - } - ] -} diff --git a/package-lock.json b/package-lock.json index b3b8bfa..8e81846 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "AIClient-2-API", + "name": "AIClient2API", "lockfileVersion": 3, "requires": true, "packages": { @@ -7,7 +7,7 @@ "dependencies": { "@anthropic-ai/tokenizer": "^0.0.4", "adm-zip": "^0.5.16", - "axios": "^1.14.0", + "axios": "^1.10.0", "deepmerge": "^4.3.1", "dotenv": "^16.4.5", "google-auth-library": "^10.1.0", @@ -2473,14 +2473,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", - "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^2.1.0" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/babel-jest": { @@ -3725,9 +3725,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", @@ -3745,9 +3745,9 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -5695,13 +5695,10 @@ } }, "node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" }, "node_modules/pure-rand": { "version": "6.1.0", diff --git a/src/converters/strategies/GrokConverter.js b/src/converters/strategies/GrokConverter.js index 86ece0e..38ebfb5 100644 --- a/src/converters/strategies/GrokConverter.js +++ b/src/converters/strategies/GrokConverter.js @@ -264,8 +264,6 @@ export class GrokConverter extends BaseConverter { return this.toOpenAIResponsesResponse(data, model); case MODEL_PROTOCOL_PREFIX.CODEX: return this.toCodexResponse(data, model); - case MODEL_PROTOCOL_PREFIX.CLAUDE: - return this.toClaudeResponse(data, model); default: return data; } @@ -276,8 +274,6 @@ export class GrokConverter extends BaseConverter { */ convertStreamChunk(chunk, targetProtocol, model) { switch (targetProtocol) { - case MODEL_PROTOCOL_PREFIX.CLAUDE: - return this.toClaudeStreamChunk(chunk, model); case MODEL_PROTOCOL_PREFIX.OPENAI: return this.toOpenAIStreamChunk(chunk, model); case MODEL_PROTOCOL_PREFIX.GEMINI: @@ -1143,76 +1139,6 @@ export class GrokConverter extends BaseConverter { }; } - /** - * Grok响应 -> Claude响应 (通过OpenAI中转) - */ - toClaudeResponse(grokResponse, model) { - const openaiRes = this.toOpenAIResponse(grokResponse, model); - if (!openaiRes) { - return { - id: `msg_${uuidv4()}`, - type: "message", - role: "assistant", - content: [], - model: model, - stop_reason: "end_turn", - stop_sequence: null, - usage: { input_tokens: 0, output_tokens: 0 } - }; - } - - const choice = openaiRes.choices?.[0] || {}; - const message = choice.message || {}; - const contentList = []; - - // 工具调用 - const toolCalls = message.tool_calls || message.function_calls || []; - for (const tc of (toolCalls || [])) { - if (tc.function) { - let argObj; - try { - argObj = typeof tc.function.arguments === 'string' ? JSON.parse(tc.function.arguments) : tc.function.arguments; - } catch (e) { - argObj = {}; - } - contentList.push({ - type: "tool_use", - id: tc.id || `toolu_${uuidv4().slice(0, 8)}`, - name: tc.function.name || "", - input: argObj - }); - } - } - - // 文本内容 - const text = message.content || ""; - if (text) { - contentList.push({ type: "text", text }); - } - - // 空内容兜底 - if (contentList.length === 0) { - contentList.push({ type: "text", text: "" }); - } - - const finishReason = choice.finish_reason || "stop"; - const stopReason = finishReason === "stop" ? "end_turn" : finishReason === "length" ? "max_tokens" : finishReason === "tool_calls" ? "tool_use" : finishReason; - - return { - id: `msg_${uuidv4()}`, - type: "message", - role: "assistant", - content: contentList, - model: choice.model || model, - stop_reason: stopReason, - stop_sequence: null, - usage: { - input_tokens: openaiRes.usage?.prompt_tokens || 0, - output_tokens: openaiRes.usage?.completion_tokens || 0 - } - }; - } - /** * Grok流式响应块 -> Codex流式响应块 */ diff --git a/src/plugins/model-router/index.js b/src/plugins/model-router/index.js deleted file mode 100644 index 9739219..0000000 --- a/src/plugins/model-router/index.js +++ /dev/null @@ -1,287 +0,0 @@ -/** - * Model Router 插件 - 自定义模型别名路由 - * - * 功能: - * - 将自定义模型别名映射到 provider:model 格式 - * - 支持热编辑(通过 API 或配置文件) - * - 支持 /plugin/model-router/ 路径查看和编辑映射规则 - * - * 原理: - * - 作为 middleware 插件,在请求处理最早期拦截 - * - 读取 body,匹配别名,改写 model 字段和 MODEL_PROVIDER - * - 将处理后的 body 缓存到 req._cachedBody 供下游使用 - */ - -import logger from '../../utils/logger.js'; -import { promises as fs } from 'fs'; -import path from 'path'; - -const CONFIG_FILE = path.join(process.cwd(), 'src', 'plugins', 'model-router', 'config.json'); - -/** - * 加载映射配置 - */ -async function loadMappings() { - try { - const content = await fs.readFile(CONFIG_FILE, 'utf8'); - const config = JSON.parse(content); - return config.mappings || {}; - } catch (error) { - logger.error(`[ModelRouter] Failed to load config: ${error.message}`); - return {}; - } -} - -/** - * 保存映射配置 - */ -async function saveMappings(mappings) { - const config = { mappings }; - await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8'); -} - -/** - * 匹配模型别名 - * 优先级:精确匹配 > 不区分大小写 > 包含匹配 - */ -function matchAlias(requestedModel, mappings) { - const lower = requestedModel.toLowerCase(); - - // 1. 精确匹配 - if (mappings[requestedModel]) return mappings[requestedModel]; - - // 2. 不区分大小写 - for (const [alias, rule] of Object.entries(mappings)) { - if (alias.toLowerCase() === lower) return rule; - } - - // 3. 包含匹配 - for (const [alias, rule] of Object.entries(mappings)) { - if (lower.includes(alias.toLowerCase()) || alias.toLowerCase().includes(lower)) { - return rule; - } - } - - return null; -} - -/** - * 读取完整的 request body 并缓存 - */ -function readFullBody(req) { - return new Promise((resolve, reject) => { - const chunks = []; - req.on('data', chunk => chunks.push(chunk)); - req.on('end', () => resolve(Buffer.concat(chunks))); - req.on('error', reject); - }); -} - -/** - * Monkey-patch getRequestBody 以支持缓存的 body - */ -function patchGetRequestBody() { - const commonModuleUrl = new URL('../../utils/common.js', import.meta.url); - import(commonModuleUrl).then(common => { - // 保存原始函数引用(通过重新导入) - // 由于 ESM 的 export 是只读的,我们用另一种方式 - }).catch(() => {}); -} - -const modelRouterPlugin = { - name: 'model-router', - version: '1.0.0', - description: '模型别名路由插件 - 将自定义模型名映射到 provider:model
管理:model-router 管理面板', - type: 'middleware', - _priority: 1, // 最高优先级,最先执行 - - async init(config) { - // Monkey-patch getRequestBody to support cached body from this plugin - try { - const common = await import('../../utils/common.js'); - const origFn = common.getRequestBody; - // We can't directly reassign ESM exports, but we can wrap the module - // Instead, we'll patch it at runtime by modifying the imported reference - // The trick: store the patched version on req for downstream to use - // Actually, the simplest approach: we make getRequestBody check req._cachedBodyString - } catch (e) { - logger.warn(`[ModelRouter] Init warning: ${e.message}`); - } - logger.info('[ModelRouter] Plugin initialized'); - }, - - /** - * 中间件:拦截请求,改写模型名 - */ - async middleware(req, res, requestUrl, config) { - if (req.method !== 'POST') return null; - - const pathName = requestUrl.pathname; - const isApiRequest = pathName.includes('/chat/completions') || - pathName.includes('/embeddings') || - pathName.includes('/images/generations') || - pathName.includes('/responses') || - pathName.includes('/messages') || - pathName.includes('/generateContent'); - - if (!isApiRequest) return null; - - // 如果已经被本插件处理过,跳过 - if (req._modelRouterProcessed) return null; - req._modelRouterProcessed = true; - - // 读取完整 body - let rawBody; - try { - rawBody = await readFullBody(req); - } catch (e) { - return null; - } - - let body; - try { - body = JSON.parse(rawBody.toString() || '{}'); - } catch (e) { - return null; - } - - const requestedModel = body.model; - if (!requestedModel) { - // 不改写,但需要让 body 可被下游重新读取 - req._cachedBodyBuffer = rawBody; - req._cachedBodyString = rawBody.toString(); - return null; - } - - // 已经是 provider:model 格式且有对应号池,跳过 - if (requestedModel.includes(':')) { - const prefix = requestedModel.split(':')[0]; - if (config.providerPools?.[prefix]) { - req._cachedBodyBuffer = rawBody; - req._cachedBodyString = rawBody.toString(); - return null; - } - } - - // 匹配别名 - const mappings = await loadMappings(); - const rule = matchAlias(requestedModel, mappings); - - let finalBodyString = rawBody.toString(); - - if (rule && rule.target) { - logger.info(`[ModelRouter] "${requestedModel}" → "${rule.target}" (${rule.description || ''})`); - - body.model = rule.target; - finalBodyString = JSON.stringify(body); - - // 设置 MODEL_PROVIDER - const [provider] = rule.target.split(':'); - if (provider) { - config.MODEL_PROVIDER = provider; - } - } - - // 缓存 body 供下游读取 - req._cachedBodyBuffer = Buffer.from(finalBodyString); - req._cachedBodyString = finalBodyString; - - return null; - }, - - /** - * API 路由:查看和编辑映射规则 - */ - routes: [ - { - method: 'GET', - path: '/plugin/model-router/', - handler: async (method, pathUrl, req, res) => { - try { - const htmlPath = path.join(process.cwd(), 'src', 'plugins', 'model-router', 'static', 'index.html'); - const html = await fs.readFile(htmlPath, 'utf8'); - res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); - res.end(html); - } catch (e) { - res.writeHead(500, { 'Content-Type': 'text/plain' }); - res.end('Plugin page not found'); - } - return true; - } - }, - { - method: 'GET', - path: '/plugin/model-router/api/mappings', - handler: async (method, pathUrl, req, res) => { - const mappings = await loadMappings(); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: true, mappings }, null, 2)); - return true; - } - }, - { - method: 'PUT', - path: '/plugin/model-router/api/mappings', - handler: async (method, pathUrl, req, res) => { - try { - const bodyBuffer = await readFullBody(req); - const body = JSON.parse(bodyBuffer.toString()); - - const { alias, target, description } = body; - if (!alias || !target) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: false, error: 'alias and target are required' })); - return true; - } - - const mappings = await loadMappings(); - mappings[alias] = { target, description: description || '' }; - await saveMappings(mappings); - - logger.info(`[ModelRouter] Mapping added: ${alias} → ${target}`); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: true, mappings })); - return true; - } catch (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: false, error: error.message })); - return true; - } - } - }, - { - method: 'DELETE', - path: '/plugin/model-router/api/mappings', - handler: async (method, pathUrl, req, res) => { - try { - const bodyBuffer = await readFullBody(req); - const body = JSON.parse(bodyBuffer.toString()); - - const { alias } = body; - if (!alias) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: false, error: 'alias is required' })); - return true; - } - - const mappings = await loadMappings(); - if (mappings[alias]) { - delete mappings[alias]; - await saveMappings(mappings); - logger.info(`[ModelRouter] Mapping deleted: ${alias}`); - } - - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: true, mappings })); - return true; - } catch (error) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: false, error: error.message })); - return true; - } - } - } - ] -}; - -export default modelRouterPlugin; diff --git a/src/plugins/model-router/static/index.html b/src/plugins/model-router/static/index.html deleted file mode 100644 index d38925d..0000000 --- a/src/plugins/model-router/static/index.html +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - Model Router - 模型别名路由管理 - - - -

🔄 Model Router — 模型别名路由

- -
-

➕ 添加映射

-
- - - - - -
-
- -
-

📋 当前映射规则

- - - - - - - -
别名目标描述操作
加载中...
-
- -
- - - - diff --git a/src/services/api-manager.js b/src/services/api-manager.js index 76d2880..8cacc09 100644 --- a/src/services/api-manager.js +++ b/src/services/api-manager.js @@ -6,330 +6,6 @@ import { } from '../utils/common.js'; import { getProviderPoolManager } from './service-manager.js'; import logger from '../utils/logger.js'; -import { GrokApiService } from '../providers/grok/grok-core.js'; - -/** - * Handle /v1/embeddings requests - lightweight passthrough - */ -async function handleEmbeddingsRequest(req, res, currentConfig, providerPoolManager) { - logger.info(`[Embeddings] Handling request, cached=${!!req._cachedBodyString}, provider=${currentConfig.MODEL_PROVIDER}`); - try { - const body = await getRequestBody(req); - const requestedModel = body.model; - logger.info(`[Embeddings] Body parsed, model=${requestedModel}`); - - if (!requestedModel) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: 'model is required', type: 'invalid_request_error' } })); - return; - } - - // Determine provider (model-router may have set MODEL_PROVIDER) - const provider = currentConfig.MODEL_PROVIDER || 'openai-custom'; - - // Get a healthy account from the pool - const pool = providerPoolManager?.providerPools?.[provider]; - if (!pool || pool.length === 0) { - res.writeHead(404, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: `No accounts found for provider: ${provider}`, type: 'invalid_request_error' } })); - return; - } - - // Skip unhealthy accounts - const account = pool.find(a => a.isHealthy !== false) || pool[0]; - const baseUrl = account.OPENAI_BASE_URL || account.baseUrl || account.base_url || ''; - const apiKey = account.OPENAI_API_KEY || account.apiKey || account.api_key || ''; - - if (!baseUrl || !apiKey) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: 'Account missing baseUrl or apiKey', type: 'invalid_request_error' } })); - return; - } - - // Extract actual model name (strip provider prefix) - const actualModel = requestedModel.includes(':') ? requestedModel.split(':').slice(1).join(':') : requestedModel; - - const targetUrl = baseUrl.replace(/\/+$/, '') + '/embeddings'; - logger.info(`[Embeddings] ${actualModel} → ${provider} (${targetUrl})`); - - const response = await fetch(targetUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}` - }, - body: JSON.stringify({ ...body, model: actualModel }) - }); - - const data = await response.json(); - res.writeHead(response.status, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(data)); - } catch (error) { - logger.error(`[Embeddings] Error: ${error.message}`); - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: error.message, type: 'server_error' } })); - } -} - -/** - * NVIDIA GenAI 模型映射:model 名称 → genai 路径后缀 - */ -const NVIDIA_GENAI_MODEL_MAP = { - 'flux.2-klein-4b': '/genai/black-forest-labs/flux.2-klein-4b', - 'flux-2-klein-4b': '/genai/black-forest-labs/flux.2-klein-4b', - 'flux': '/genai/black-forest-labs/flux.2-klein-4b', - 'flux.1-dev': '/genai/black-forest-labs/flux-dev', - 'flux-dev': '/genai/black-forest-labs/flux-dev', - 'flux-pro': '/genai/black-forest-labs/flux-pro', -}; - -/** - * 检查是否为 NVIDIA GenAI 模型,返回 genai 路径后缀 - */ -function getNvidiaGenaiPath(modelName) { - const lower = modelName.toLowerCase(); - // 精确匹配 - if (NVIDIA_GENAI_MODEL_MAP[lower]) return NVIDIA_GENAI_MODEL_MAP[lower]; - // 前缀匹配:flux 相关模型统一走 genai - if (lower.startsWith('flux') || lower.startsWith('black-forest') || lower.includes('klein')) { - return '/genai/black-forest-labs/flux.2-klein-4b'; - } - return null; -} - -/** - * 将 NVIDIA GenAI 响应格式转换为 OpenAI images/generations 格式 - * NVIDIA 返回:{ "artifacts": [{ "image": "", "seed": 123 }] } - * OpenAI 返回:{ "data": [{ "b64_json": "" }] } - */ -function convertNvidiaGenaiToOpenAI(nvidiaResponse, requestedFormat) { - // NVIDIA GenAI 返回 base64 image - if (nvidiaResponse.artifacts && nvidiaResponse.artifacts.length > 0) { - const artifact = nvidiaResponse.artifacts[0]; - const imageBase64 = artifact.image || artifact.base64 || ''; - if (requestedFormat === 'url' || !requestedFormat) { - // 默认返回 data URL - return { - data: [{ url: `data:image/png;base64,${imageBase64}` }], - seed: artifact.seed - }; - } else if (requestedFormat === 'b64_json') { - return { - data: [{ b64_json: imageBase64 }], - seed: artifact.seed - }; - } - } - // 如果不认得的格式,原样返回 - return nvidiaResponse; -} - -async function handleImageGenerationsRequest(req, res, currentConfig, providerPoolManager) { - logger.info(`[Images] Handling request, provider=${currentConfig.MODEL_PROVIDER}`); - try { - const body = await getRequestBody(req); - const requestedModel = body.model; - - if (!requestedModel) { - res.writeHead(400, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: 'model is required', type: 'invalid_request_error' } })); - return; - } - - const provider = currentConfig.MODEL_PROVIDER || 'openai-custom'; - const actualModelFull = requestedModel.includes(':') ? requestedModel : `${provider}:${requestedModel}`; - const actualModel = requestedModel.includes(':') ? requestedModel.split(':').slice(1).join(':') : requestedModel; - - // Grok imagine 模型:使用 WebSocket 生成图片(不走 OpenAI 兼容流程) - if (actualModel.toLowerCase().includes('grok') && actualModel.toLowerCase().includes('imagine')) { - const providerKey = requestedModel.includes(':') ? requestedModel.split(':')[0] : provider; - let grokAccount = null; - const grokPool = providerPoolManager?.providerPools?.[providerKey]; - if (grokPool && grokPool.length > 0) { - grokAccount = grokPool.find(a => a.isHealthy !== false) || grokPool[0]; - } - if (!grokAccount) { - for (const [poolName, poolEntries] of Object.entries(providerPoolManager?.providerPools || {})) { - if (poolName.toLowerCase().includes('grok') && poolEntries.length > 0) { - grokAccount = poolEntries.find(a => a.isHealthy !== false) || poolEntries[0]; - break; - } - } - } - if (!grokAccount || !grokAccount.GROK_COOKIE_TOKEN) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: 'No grok account with valid token found', type: 'invalid_request_error' } })); - return; - } - try { - const grokService = new GrokApiService(grokAccount); - const result = await grokService._generateAndCollectWS(actualModel, { - message: body.prompt || '', - n: body.n || 1, - }); - const images = []; - for (const cardJson of result.modelResponse?.cardAttachmentsJson || []) { - const card = JSON.parse(cardJson); - if (card.image?.original) { - const dataUrl = card.image.original; - const b64Match = dataUrl.match(/^data:image\/[a-z]+;base64,(.+)$/); - if (b64Match) { - images.push({ b64_json: b64Match[1] }); - } else { - images.push({ url: dataUrl }); - } - } - } - if (images.length === 0) { - res.writeHead(502, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: 'Grok image generation returned no images', type: 'server_error' } })); - return; - } - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ created: Math.floor(Date.now() / 1000), data: images })); - } catch (grokError) { - logger.error(`[Images] Grok imagine error: ${grokError.message}`); - res.writeHead(502, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: `Grok image generation failed: ${grokError.message}`, type: 'server_error' } })); - } - return; - } - - // 尝试从所有 pool 中找到该模型对应的 provider(支持 model-router 路由后的情况) - let pool = providerPoolManager?.providerPools?.[provider]; - let account = null; - let baseUrl = ''; - let apiKey = ''; - - if (!pool || pool.length === 0) { - // fallback:遍历所有 pools 查找 - for (const [poolName, poolEntries] of Object.entries(providerPoolManager?.providerPools || {})) { - const found = poolEntries.find(a => a.isHealthy !== false); - if (found) { - pool = poolEntries; - account = found; - baseUrl = account.OPENAI_BASE_URL || account.baseUrl || account.base_url || ''; - apiKey = account.OPENAI_API_KEY || account.apiKey || account.api_key || ''; - break; - } - } - if (!account) { - res.writeHead(404, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: `No accounts found for provider: ${provider}`, type: 'invalid_request_error' } })); - return; - } - } else { - account = pool.find(a => a.isHealthy !== false) || pool[0]; - baseUrl = account.OPENAI_BASE_URL || account.baseUrl || account.base_url || ''; - apiKey = account.OPENAI_API_KEY || account.apiKey || account.api_key || ''; - - if (!baseUrl || !apiKey) { - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: 'Account missing baseUrl or apiKey', type: 'invalid_request_error' } })); - return; - } - } - - // 检测 NVIDIA GenAI 模型(flux 等图像生成模型) - const isNvidiaBaseUrl = baseUrl.includes('nvidia.com') && baseUrl.includes('integrate.api'); - const genaiPath = isNvidiaBaseUrl ? getNvidiaGenaiPath(actualModel) : null; - - let targetUrl, requestBody, useOpenAiFormat = true; - - if (genaiPath) { - // NVIDIA GenAI 原生格式(域名从 integrate.api → ai.api) - useOpenAiFormat = false; - const genaiBaseUrl = baseUrl.replace('integrate.api.nvidia.com', 'ai.api.nvidia.com'); - targetUrl = genaiBaseUrl.replace(/\/+$/, '') + genaiPath; - // 转换为 NVIDIA GenAI 格式:{ prompt, width, height, seed, steps } - requestBody = { - prompt: body.prompt || '', - width: body.width || 1024, - height: body.height || 1024, - seed: body.seed !== undefined ? body.seed : 0, - steps: body.steps || body.num_inference_steps || 4, - }; - if (body.guidance_scale) requestBody.guidance_scale = body.guidance_scale; - logger.info(`[Images] NVIDIA GenAI: ${actualModel} → ${targetUrl}`); - } else { - // 标准 OpenAI /images/generations 格式 - targetUrl = baseUrl.replace(/\/+$/, '') + '/images/generations'; - requestBody = { ...body, model: actualModel }; - logger.info(`[Images] ${actualModel} → ${provider} (${targetUrl})`); - } - - const response = await fetch(targetUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}` - }, - body: JSON.stringify(requestBody) - }); - - const contentType = response.headers.get('content-type') || 'application/json'; - - // 如果是 GenAI 请求,转换响应格式 - if (!useOpenAiFormat) { - // NVIDIA 可能返回 JSON 或二进制图片 - if (contentType.includes('json')) { - const data = await response.json(); - const format = body.response_format; - const converted = response.ok ? convertNvidiaGenaiToOpenAI(data, format) : data; - res.writeHead(response.status, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(converted)); - } else { - // 二进制响应(图片直接返回) - const data = await response.arrayBuffer(); - logger.info(`[Images] NVIDIA GenAI binary response: ${contentType}, ${data.byteLength} bytes`); - if (response.ok) { - // 转 base64 并包装成 OpenAI 格式 - const b64 = Buffer.from(data).toString('base64'); - const format = body.response_format; - if (format === 'b64_json') { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ data: [{ b64_json: b64 }] })); - } else { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ data: [{ url: `data:${contentType};base64,${b64}` }] })); - } - } else { - res.writeHead(response.status, { 'Content-Type': contentType }); - res.end(Buffer.from(data)); - } - } - } else { - const data = await response.arrayBuffer(); - res.writeHead(response.status, { 'Content-Type': contentType }); - res.end(Buffer.from(data)); - } - } catch (error) { - logger.error(`[Images] Error: ${error.message}`); - logger.error(`[Images] Stack: ${error.stack}`); - res.writeHead(500, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: { message: error.message, type: 'server_error' } })); - } -} - -function getRequestBody(req) { - if (req._cachedBodyString) { - try { - return Promise.resolve(JSON.parse(req._cachedBodyString)); - } catch (e) { - logger.error(`[getRequestBody] cached body parse failed: ${e.message}, raw=${req._cachedBodyString.substring(0, 200)}`); - throw e; - } - } - return new Promise((resolve, reject) => { - let body = ''; - req.on('data', chunk => { body += chunk.toString(); }); - req.on('end', () => { - try { resolve(JSON.parse(body || '{}')); } - catch (e) { reject(new Error('Invalid JSON: ' + e.message)); } - }); - req.on('error', reject); - }); -} /** * Handle API authentication and routing * @param {string} method - The HTTP method @@ -376,14 +52,6 @@ export async function handleAPIRequests(method, path, req, res, currentConfig, a await handleContentGenerationRequest(req, res, apiService, ENDPOINT_TYPE.CLAUDE_MESSAGE, currentConfig, promptLogFilename, providerPoolManager, currentConfig.uuid, path); return true; } - if (path === '/v1/embeddings') { - await handleEmbeddingsRequest(req, res, currentConfig, providerPoolManager); - return true; - } - if (path === '/v1/images/generations') { - await handleImageGenerationsRequest(req, res, currentConfig, providerPoolManager); - return true; - } } return false; diff --git a/src/ui-modules/auth.js b/src/ui-modules/auth.js index f3fa15a..b60995f 100644 --- a/src/ui-modules/auth.js +++ b/src/ui-modules/auth.js @@ -115,55 +115,31 @@ function getExpiryTime() { /** * 读取token存储文件 - * 安全增强:读取失败时抛出异常而非返回空对象,防止数据覆盖 */ async function readTokenStore() { - if (existsSync(TOKEN_STORE_FILE)) { - const content = await fs.readFile(TOKEN_STORE_FILE, 'utf8'); - return JSON.parse(content); - } else { - // 如果文件不存在,创建一个默认的token store + try { + if (existsSync(TOKEN_STORE_FILE)) { + const content = await fs.readFile(TOKEN_STORE_FILE, 'utf8'); + return JSON.parse(content); + } else { + // 如果文件不存在,创建一个默认的token store + await writeTokenStore({ tokens: {} }); + return { tokens: {} }; + } + } catch (error) { + logger.error('[Token Store] Failed to read token store file:', error); return { tokens: {} }; } } /** * 写入token存储文件 - * 安全增强: - * 1. 写前校验:禁止写入空tokens(防数据丢失) - * 2. 原子写入:先写临时文件再rename,防止断电/崩溃导致文件截断 */ async function writeTokenStore(tokenStore) { - // 写前校验:如果tokens为空且文件已有数据,拒绝写入(防意外清空) - if (!tokenStore || !tokenStore.tokens || Object.keys(tokenStore.tokens).length === 0) { - if (existsSync(TOKEN_STORE_FILE)) { - try { - const existing = await fs.readFile(TOKEN_STORE_FILE, 'utf8'); - const parsed = JSON.parse(existing); - const existingCount = parsed.tokens ? Object.keys(parsed.tokens).length : 0; - if (existingCount > 0) { - logger.error('[Token Store] REJECTED write: attempting to overwrite ' + existingCount + ' tokens with empty data. Possible data corruption detected.'); - return false; - } - } catch (e) { - logger.error('[Token Store] Failed to check existing token store before write:', e.message); - return false; - } - } - } - try { - const data = JSON.stringify(tokenStore, null, 2); - const tmpPath = TOKEN_STORE_FILE + '.tmp'; - // 原子写入:先写临时文件,再rename - await fs.writeFile(tmpPath, data, 'utf8'); - await fs.rename(tmpPath, TOKEN_STORE_FILE); - return true; + await fs.writeFile(TOKEN_STORE_FILE, JSON.stringify(tokenStore, null, 2), 'utf8'); } catch (error) { logger.error('[Token Store] Failed to write token store file:', error); - // 清理可能残留的临时文件 - try { await fs.unlink(TOKEN_STORE_FILE + '.tmp'); } catch {} - return false; } } @@ -437,29 +413,7 @@ export async function handleLoginRequest(req, res) { return true; } -// 定时清理过期token(带防护:清理后必须仍有≥1个字段,否则不写入) -setInterval(async () => { - try { - const tokenStore = await readTokenStore(); - const existingCount = tokenStore.tokens ? Object.keys(tokenStore.tokens).length : 0; - const now = Date.now(); - let hasChanges = false; - - for (const token in tokenStore.tokens) { - if (now > tokenStore.tokens[token].expiryTime) { - delete tokenStore.tokens[token]; - hasChanges = true; - } - } - - // 防护:只有非首次运行+有变更时才写入(避免首次启动读不到旧文件时空写) - if (hasChanges && existingCount > 0) { - await writeTokenStore(tokenStore); - logger.info(`[Token Store] Cleaned up expired tokens. Before: ${existingCount}`); - } - } catch (error) { - logger.error('[Token Store] Cleanup interval error:', error.message); - } -}, 5 * 60 * 1000); // 每5分钟清理一次 +// 定时清理过期token +setInterval(cleanupExpiredTokens, 5 * 60 * 1000); // 每5分钟清理一次 diff --git a/src/utils/common.js b/src/utils/common.js index d29d49e..3582277 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -191,12 +191,6 @@ export function getClientIp(req) { * @throws {Error} If the request body is not valid JSON. */ export function getRequestBody(req) { - // Support cached body from model-router plugin - if (req._cachedBodyString) { - try { - return Promise.resolve(JSON.parse(req._cachedBodyString)); - } catch (e) {} - } return new Promise((resolve, reject) => { let body = ''; req.on('data', chunk => {