From 658ec9c6a4e843b08729960b722e75dd3a123dd8 Mon Sep 17 00:00:00 2001 From: kapelame Date: Sun, 2 Aug 2026 15:49:00 +0800 Subject: [PATCH] Add MiniMax provider configuration --- projects/xiaoyue-web/.env.example | 7 +- projects/xiaoyue-web/lib/minimax-chat.js | 153 +++++++++++++++++++ projects/xiaoyue-web/public/index.html | 3 +- projects/xiaoyue-web/server-openclaw.js | 47 +++--- projects/xiaoyue-web/server-with-openclaw.js | 105 ++++++------- projects/xiaoyue-web/server.js | 63 ++++---- 6 files changed, 257 insertions(+), 121 deletions(-) create mode 100644 projects/xiaoyue-web/lib/minimax-chat.js diff --git a/projects/xiaoyue-web/.env.example b/projects/xiaoyue-web/.env.example index 02da515..7187226 100644 --- a/projects/xiaoyue-web/.env.example +++ b/projects/xiaoyue-web/.env.example @@ -2,9 +2,10 @@ # 小易伴侣 - 环境变量配置示例 # ========================================== -# 智谱 AI API Key (必需) -# 获取地址: https://open.bigmodel.cn/ -ZHIPU_API_KEY=your-zhipu-api-key-here +# MiniMax API Key (required) +# Get one at https://platform.minimax.io/ or https://platform.minimaxi.com/ +MINIMAX_API_KEY=your-minimax-api-key-here +# MINIMAX_REGION=global_en # 服务器端口 (可选,默认 3000) PORT=3000 diff --git a/projects/xiaoyue-web/lib/minimax-chat.js b/projects/xiaoyue-web/lib/minimax-chat.js new file mode 100644 index 0000000..ced37ab --- /dev/null +++ b/projects/xiaoyue-web/lib/minimax-chat.js @@ -0,0 +1,153 @@ +const axios = require('axios'); + +const MINIMAX_PROVIDER_NAME = 'MiniMax'; + +const MINIMAX_TEXT_MODEL_CONFIG = { + reason_codes: { + provider_add: 'provider-add', + model_add: 'model-add', + parameter_refresh: 'parameter-refresh', + input_capability: 'input-capability', + }, + model_id: 'MiniMax-M3', + model_ids: ['MiniMax-M3', 'MiniMax-M2.7'], + models: [ + { + model_id: 'MiniMax-M3', + context_window: 1000000, + pricing_usd_per_million_tokens: { + input: 0.6, + output: 2.4, + cache_read: 0.12, + cache_write: null, + }, + input_modalities: ['text', 'image', 'video'], + thinking: ['adaptive', 'disabled'], + }, + { + model_id: 'MiniMax-M2.7', + context_window: 204800, + pricing_usd_per_million_tokens: { + input: 0.3, + output: 1.2, + cache_read: 0.06, + cache_write: 0.375, + }, + input_modalities: ['text'], + thinking: ['always_on'], + }, + ], + anthropic_base_url: 'https://api.minimax.io/anthropic', + openai_base_url: 'https://api.minimax.io/v1', + context_window: 1000000, + pricing_usd_per_million_tokens: { + input: 0.6, + output: 2.4, + cache_read: 0.12, + cache_write: null, + }, + thinking: ['adaptive', 'disabled'], +}; + +const MINIMAX_REGIONAL_ENDPOINTS = [ + { + region: 'global_en', + openai_base_url: 'https://api.minimax.io/v1', + anthropic_base_url: 'https://api.minimax.io/anthropic', + docs_root: 'https://platform.minimax.io/docs', + }, + { + region: 'cn_zh', + openai_base_url: 'https://api.minimaxi.com/v1', + anthropic_base_url: 'https://api.minimaxi.com/anthropic', + docs_root: 'https://platform.minimaxi.com/docs', + }, +]; + +const MINIMAX_REGION_MAP = Object.fromEntries( + MINIMAX_REGIONAL_ENDPOINTS.map((entry) => [entry.region, entry]), +); + +const MINIMAX_MODEL_MAP = Object.fromEntries( + MINIMAX_TEXT_MODEL_CONFIG.models.map((model) => [model.model_id, model]), +); + +function normalizeMiniMaxRegion(region) { + return region === 'cn_zh' ? 'cn_zh' : 'global_en'; +} + +function resolveMiniMaxConfig(overrides = {}) { + const region = normalizeMiniMaxRegion( + overrides.region || process.env.MINIMAX_REGION || MINIMAX_REGIONAL_ENDPOINTS[0].region, + ); + const regionConfig = MINIMAX_REGION_MAP[region] || MINIMAX_REGIONAL_ENDPOINTS[0]; + const requestedModelId = overrides.modelId || overrides.model || process.env.MINIMAX_MODEL || MINIMAX_TEXT_MODEL_CONFIG.model_id; + const modelId = MINIMAX_MODEL_MAP[requestedModelId] ? requestedModelId : MINIMAX_TEXT_MODEL_CONFIG.model_id; + const modelSpec = MINIMAX_MODEL_MAP[modelId]; + const apiKey = overrides.apiKey || process.env.MINIMAX_API_KEY || process.env.ZHIPU_API_KEY || ''; + const apiBaseUrl = overrides.apiBaseUrl || process.env.MINIMAX_API_BASE || regionConfig.openai_base_url; + const anthropicBaseUrl = overrides.anthropicBaseUrl || process.env.MINIMAX_ANTHROPIC_BASE_URL || regionConfig.anthropic_base_url; + + return { + providerName: MINIMAX_PROVIDER_NAME, + apiKey, + apiBaseUrl, + anthropicBaseUrl, + region, + modelId, + modelSpec, + modelIds: MINIMAX_TEXT_MODEL_CONFIG.model_ids.slice(), + textModelConfig: MINIMAX_TEXT_MODEL_CONFIG, + regionalEndpoints: MINIMAX_REGIONAL_ENDPOINTS, + }; +} + +async function postMiniMaxChatCompletion(options = {}) { + const config = resolveMiniMaxConfig(options); + + if (!config.apiKey) { + const error = new Error('MiniMax API key is not configured'); + error.code = 'MINIMAX_API_KEY_MISSING'; + throw error; + } + + const payload = { + model: config.modelId, + messages: options.messages || [], + }; + + if (options.temperature !== undefined) { + payload.temperature = options.temperature; + } + if (options.top_p !== undefined) { + payload.top_p = options.top_p; + } + if (options.max_tokens !== undefined) { + payload.max_tokens = options.max_tokens; + } + if (options.stream !== undefined) { + payload.stream = options.stream; + } + + const response = await axios.post( + `${config.apiBaseUrl}/chat/completions`, + payload, + { + headers: { + Authorization: `Bearer ${config.apiKey}`, + 'Content-Type': 'application/json', + }, + timeout: options.timeout ?? 30000, + }, + ); + + return { config, response }; +} + +module.exports = { + MINIMAX_PROVIDER_NAME, + MINIMAX_TEXT_MODEL_CONFIG, + MINIMAX_REGIONAL_ENDPOINTS, + resolveMiniMaxConfig, + postMiniMaxChatCompletion, +}; diff --git a/projects/xiaoyue-web/public/index.html b/projects/xiaoyue-web/public/index.html index 122abb5..7914d51 100644 --- a/projects/xiaoyue-web/public/index.html +++ b/projects/xiaoyue-web/public/index.html @@ -361,7 +361,7 @@

小易伴侣

statusText.textContent = '在线'; } else { statusText.textContent = '需要配置'; - addSystemMessage('⚠️ 请在服务器配置 ZHIPU_API_KEY 环境变量才能使用'); + addSystemMessage(`⚠️ Please configure the ${data.provider || 'MiniMax'} API key on the server before using this service.`); } } } catch (error) { @@ -549,4 +549,3 @@

小易伴侣

- diff --git a/projects/xiaoyue-web/server-openclaw.js b/projects/xiaoyue-web/server-openclaw.js index a5f44e9..e7577ac 100644 --- a/projects/xiaoyue-web/server-openclaw.js +++ b/projects/xiaoyue-web/server-openclaw.js @@ -5,6 +5,10 @@ const path = require('path'); const fs = require('fs'); const { spawn } = require('child_process'); require('dotenv').config(); +const { + resolveMiniMaxConfig, + postMiniMaxChatCompletion, +} = require('./lib/minimax-chat'); const app = express(); const PORT = process.env.PORT || 3000; @@ -16,9 +20,8 @@ app.use(express.json()); const publicPath = path.join(__dirname, 'public'); app.use(express.static(publicPath)); -// 智谱 AI 配置 -const ZHIPU_API_KEY = process.env.ZHIPU_API_KEY || process.env.OPENCLAW_TOKEN; -const ZHIPU_API_BASE = 'https://open.bigmodel.cn/api/paas/v4'; +// MiniMax configuration +const MINIMAX_DEFAULT_CONFIG = resolveMiniMaxConfig(); // OpenClaw 配置 - 硬编码配置 const OPENCLAW_ENABLED = true; // 强制启用 @@ -147,15 +150,13 @@ const XIAOYI_PERSONA = `你是小易(知易),一个融合中国传统文 // API 路由 app.post('/api/chat', async (req, res) => { try { - const { message, sessionId = 'default', apiKey } = req.body; + const { message, sessionId = 'default', apiKey, model, region } = req.body; + const chatConfig = resolveMiniMaxConfig({ apiKey, modelId: model, region }); - // 优先使用请求中的 API Key,其次是环境变量 - const useApiKey = apiKey || ZHIPU_API_KEY; - - if (!useApiKey) { + if (!chatConfig.apiKey) { return res.json({ success: false, - error: '请配置 ZHIPU_API_KEY 环境变量或在请求中提供 apiKey' + error: 'Please configure the MiniMax API key or provide apiKey in the request' }); } @@ -192,22 +193,13 @@ app.post('/api/chat', async (req, res) => { messages.push({ role: 'user', content: message }); - // 调用智谱 AI - const response = await axios.post( - `${ZHIPU_API_BASE}/chat/completions`, - { - model: 'glm-4.7-flash', - messages: messages, - temperature: 0.9, - max_tokens: 200 - }, - { - headers: { - 'Authorization': `Bearer ${useApiKey}`, - 'Content-Type': 'application/json' - } - } - ); + // Call MiniMax chat completions + const { response } = await postMiniMaxChatCompletion({ + ...chatConfig, + messages: messages, + temperature: 0.9, + max_tokens: 200 + }); const reply = response.data.choices?.[0]?.message?.content || '抱歉,我没有收到有效的回复'; @@ -259,7 +251,10 @@ app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), - apiKeyConfigured: !!ZHIPU_API_KEY, + provider: MINIMAX_DEFAULT_CONFIG.providerName, + model: MINIMAX_DEFAULT_CONFIG.modelId, + region: MINIMAX_DEFAULT_CONFIG.region, + apiKeyConfigured: !!MINIMAX_DEFAULT_CONFIG.apiKey, openclawEnabled: OPENCLAW_ENABLED, openclawApi: OPENCLAW_API }); diff --git a/projects/xiaoyue-web/server-with-openclaw.js b/projects/xiaoyue-web/server-with-openclaw.js index d61f314..453cea0 100644 --- a/projects/xiaoyue-web/server-with-openclaw.js +++ b/projects/xiaoyue-web/server-with-openclaw.js @@ -16,6 +16,10 @@ const axios = require('axios'); const path = require('path'); const fs = require('fs'); require('dotenv').config(); +const { + resolveMiniMaxConfig, + postMiniMaxChatCompletion, +} = require('./lib/minimax-chat'); const app = express(); const PORT = process.env.PORT || 3000; @@ -30,9 +34,8 @@ app.use(express.static(publicPath)); // ==================== 配置 ==================== -// 智谱 AI 配置 -const ZHIPU_API_KEY = process.env.ZHIPU_API_KEY; -const ZHIPU_API_BASE = 'https://open.bigmodel.cn/api/paas/v4'; +// MiniMax configuration +const MINIMAX_DEFAULT_CONFIG = resolveMiniMaxConfig(); // 多模态微服务配置 const TTS_SERVER = process.env.TTS_SERVER || 'http://127.0.0.1:5050'; @@ -113,12 +116,10 @@ async function handleFeishuMessage(messageId, text, chatId) { // 保留最近 20 条 if (history.length > 20) history.splice(0, history.length - 20); - // 获取当前使用的 API Key(优先 .env,其次前端保存的) - const apiKey = ZHIPU_API_KEY && ZHIPU_API_KEY !== 'your-zhipu-api-key-here' - ? ZHIPU_API_KEY : null; + const chatConfig = MINIMAX_DEFAULT_CONFIG; - if (!apiKey) { - await replyFeishuMessage(messageId, '智谱 API Key 未配置,请先在小易页面或 .env 中配置。'); + if (!chatConfig.apiKey) { + await replyFeishuMessage(messageId, 'MiniMax API key is not configured. Please set it in the server environment.'); return; } @@ -151,13 +152,11 @@ async function handleFeishuMessage(messageId, text, chatId) { messages = [{ role: 'system', content: systemPrompt }, ...history]; } - const res = await axios.post(`${ZHIPU_API_BASE}/chat/completions`, { - model: 'glm-4-flash', + const { response: res } = await postMiniMaxChatCompletion({ + ...chatConfig, messages: messages, temperature: 0.8, - max_tokens: 500 - }, { - headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, + max_tokens: 500, timeout: 15000 }); @@ -464,15 +463,13 @@ function loadAgentSoul(agentId) { */ app.post('/api/chat', async (req, res) => { try { - const { message, sessionId = 'default', apiKey } = req.body; - - // 优先使用请求中的 API Key,其次是环境变量 - const useApiKey = apiKey || ZHIPU_API_KEY; + let { message, sessionId = 'default', apiKey, model, region } = req.body; + const chatConfig = resolveMiniMaxConfig({ apiKey, modelId: model, region }); - if (!useApiKey) { + if (!chatConfig.apiKey) { return res.json({ success: false, - error: '请配置 ZHIPU_API_KEY 环境变量或在请求中提供 apiKey' + error: 'Please configure the MiniMax API key or provide apiKey in the request' }); } @@ -528,46 +525,36 @@ app.post('/api/chat', async (req, res) => { messages.push({ role: 'user', content: message }); - // 调用智谱 AI + // Call MiniMax chat completions let reply; try { - const response = await axios.post( - `${ZHIPU_API_BASE}/chat/completions`, - { - model: 'glm-4-flash', - messages: messages, - temperature: 0.9, - max_tokens: 200 - }, - { - headers: { - 'Authorization': `Bearer ${useApiKey}`, - 'Content-Type': 'application/json' - }, - timeout: 30000 - } - ); + const { response } = await postMiniMaxChatCompletion({ + ...chatConfig, + messages: messages, + temperature: 0.9, + max_tokens: 200, + timeout: 30000 + }); - console.log('[Zhipu] Response:', JSON.stringify(response.data, null, 2)); + console.log('[MiniMax] Response:', JSON.stringify(response.data, null, 2)); if (response.data.choices && response.data.choices[0] && response.data.choices[0].message) { reply = response.data.choices[0].message.content; } else { - reply = '抱歉,AI 没有返回有效回复'; + reply = 'Sorry, the AI did not return a valid reply'; } - } catch (zhipuError) { - console.error('[Zhipu] API Error:', zhipuError.response?.data || zhipuError.message); + } catch (miniMaxError) { + console.error('[MiniMax] API Error:', miniMaxError.response?.data || miniMaxError.message); - // 检查是否是 API Key 错误 - if (zhipuError.response?.data?.error?.code === '401') { + if (miniMaxError.code === 'MINIMAX_API_KEY_MISSING') { return res.json({ success: false, - error: 'API Key 无效或已过期', - message: '请检查您的智谱 API Key 是否正确' + error: 'MiniMax API key is not configured', + message: 'Please check the MiniMax API key in your server environment' }); } - reply = '抱歉,AI 服务暂时不可用,请稍后再试'; + reply = 'Sorry, the AI service is temporarily unavailable. Please try again later'; } // 更新对话历史 @@ -640,11 +627,11 @@ app.get('/api/openclaw/status', async (req, res) => { */ app.post('/api/starclaw/chat', async (req, res) => { try { - const { message, agentId, agentName, sessionId = 'default', apiKey } = req.body; - const useApiKey = apiKey || ZHIPU_API_KEY; + const { message, agentId, agentName, sessionId = 'default', apiKey, model, region } = req.body; + const chatConfig = resolveMiniMaxConfig({ apiKey, modelId: model, region }); - if (!useApiKey) { - return res.json({ success: false, error: 'API Key 未配置' }); + if (!chatConfig.apiKey) { + return res.json({ success: false, error: 'MiniMax API key is not configured' }); } // 加载 Agent 的 SOUL.md @@ -666,11 +653,13 @@ app.post('/api/starclaw/chat', async (req, res) => { ...history ]; - const response = await axios.post( - `${ZHIPU_API_BASE}/chat/completions`, - { model: 'glm-4-flash', messages, temperature: 0.85, max_tokens: 500 }, - { headers: { 'Authorization': `Bearer ${useApiKey}`, 'Content-Type': 'application/json' }, timeout: 30000 } - ); + const { response } = await postMiniMaxChatCompletion({ + ...chatConfig, + messages, + temperature: 0.85, + max_tokens: 500, + timeout: 30000 + }); const reply = response.data.choices[0].message.content; history.push({ role: 'assistant', content: reply }); @@ -799,11 +788,15 @@ app.post('/api/voice/clone', async (req, res) => { */ app.get('/api/health', async (req, res) => { const openclawRunning = await checkOpenClawHealth(); + const healthConfig = MINIMAX_DEFAULT_CONFIG; res.json({ status: 'ok', timestamp: new Date().toISOString(), - hasApiKey: !!ZHIPU_API_KEY, - apiKeyConfigured: !!ZHIPU_API_KEY, + provider: healthConfig.providerName, + model: healthConfig.modelId, + region: healthConfig.region, + hasApiKey: !!healthConfig.apiKey, + apiKeyConfigured: !!healthConfig.apiKey, openclaw: { enabled: OPENCLAW_ENABLED, envEnabled: OPENCLAW_ENABLED, diff --git a/projects/xiaoyue-web/server.js b/projects/xiaoyue-web/server.js index 0ed40ee..ad32e6c 100644 --- a/projects/xiaoyue-web/server.js +++ b/projects/xiaoyue-web/server.js @@ -1,12 +1,15 @@ const express = require('express'); const cors = require('cors'); -const axios = require('axios'); const path = require('path'); const fs = require('fs'); const { exec } = require('child_process'); const { promisify } = require('util'); const execPromise = promisify(exec); require('dotenv').config(); +const { + resolveMiniMaxConfig, + postMiniMaxChatCompletion, +} = require('./lib/minimax-chat'); const app = express(); const PORT = process.env.PORT || 3000; @@ -19,9 +22,8 @@ const publicPath = path.join(__dirname, 'public'); console.log('Public directory:', publicPath); app.use(express.static(publicPath)); -// 智谱 AI 配置 -const ZHIPU_API_KEY = process.env.ZHIPU_API_KEY; -const ZHIPU_API_BASE = 'https://open.bigmodel.cn/api/paas/v4'; +// MiniMax configuration +const MINIMAX_DEFAULT_CONFIG = resolveMiniMaxConfig(); // OpenClaw/OpenCode 配置 const OPENCLAW_CLI = process.env.OPENCLAW_CLI || process.env.OPENCODE_CLI || 'C:\\D\\opencode\\opencode-cli.exe'; @@ -270,15 +272,13 @@ ${openclawHint} // 生成对话 app.post('/api/chat', async (req, res) => { try { - const { message, sessionId = 'default', apiKey } = req.body; + const { message, sessionId = 'default', apiKey, model, region } = req.body; + const chatConfig = resolveMiniMaxConfig({ apiKey, modelId: model, region }); - // 优先使用请求中的 API Key,其次是环境变量 - const useApiKey = apiKey || ZHIPU_API_KEY; - - if (!useApiKey) { + if (!chatConfig.apiKey) { return res.json({ success: false, - error: '请配置 ZHIPU_API_KEY 环境变量或在请求中提供 apiKey' + error: 'Please configure the MiniMax API key or provide apiKey in the request' }); } @@ -309,28 +309,19 @@ app.post('/api/chat', async (req, res) => { content: message }); - // 调用智谱 AI - const response = await axios.post( - `${ZHIPU_API_BASE}/chat/completions`, - { - model: 'glm-4.7-flash', - messages: [ - { - role: 'system', - content: generateSystemPrompt() - }, - ...history.slice(-10) // 保留最近10轮对话 - ], - temperature: 0.8, - top_p: 0.7 - }, - { - headers: { - 'Authorization': `Bearer ${useApiKey}`, - 'Content-Type': 'application/json' - } - } - ); + // Call MiniMax chat completions + const { response } = await postMiniMaxChatCompletion({ + ...chatConfig, + messages: [ + { + role: 'system', + content: generateSystemPrompt() + }, + ...history.slice(-10) + ], + temperature: 0.8, + top_p: 0.7 + }); const aiMessage = response.data.choices[0].message.content; @@ -446,9 +437,13 @@ app.post('/api/tts', async (req, res) => { // 健康检查 app.get('/api/health', (req, res) => { + const healthConfig = resolveMiniMaxConfig(); res.json({ status: 'ok', - hasApiKey: !!ZHIPU_API_KEY, + provider: healthConfig.providerName, + model: healthConfig.modelId, + region: healthConfig.region, + hasApiKey: !!healthConfig.apiKey, openclaw: OPENCLAW_STATUS }); }); @@ -463,7 +458,7 @@ app.get('/api/openclaw-status', (req, res) => { app.listen(PORT, () => { console.log(`\n🚀 小易伴侣服务器运行在 http://localhost:${PORT}`); - console.log(`📝 API Key 状态: ${ZHIPU_API_KEY ? '✅ 已配置' : '❌ 未配置'}`); + console.log(`📝 MiniMax API key status: ${MINIMAX_DEFAULT_CONFIG.apiKey ? 'configured' : 'missing'}`); console.log(`\n🤖 OpenClaw 集成状态:`); console.log(` 启用状态: ${OPENCLAW_STATUS.enabled ? '✅ 已启用' : '❌ 未启用'}`);