diff --git a/examples/ai_chat b/examples/ai_chat new file mode 100644 index 000000000..1894d2d5f --- /dev/null +++ b/examples/ai_chat @@ -0,0 +1,507 @@ +/* METADATA +{ + "name": "ai_chat", + "description": "调用AI模型API实现AI之间智能对话互动。", + "env": ["AI_API_BASE_URL", "AI_API_KEY", "AI_MODEL_NAME"], + "tools": [{ + "name": "chat_completion", + "description": "发送消息给AI模型并获取回复。支持超时保护(默认30秒)。从根源上禁止分点列表输出。", + "parameters": [ + {"name": "messages", "description": "消息数组或字符串", "type": "any", "required": true}, + {"name": "system_prompt", "description": "系统提示词(会自动追加禁止分点指令)", "type": "string", "required": false}, + {"name": "temperature", "description": "温度参数(0.0-2.0)", "type": "number", "required": false, "default": 0.7}, + {"name": "max_tokens", "description": "最大生成长度", "type": "number", "required": false}, + {"name": "timeout", "description": "超时时间(毫秒)", "type": "number", "required": false, "default": 30000} + ] + }] +} */ + +const aiModelInteraction = (function () { + async function universalHttpRequest(url, method = 'POST', headers = {}, body = null, responseType, timeout = 30000) { + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error(`请求超时:${timeout}ms`)), timeout) + ); + + const requestPromise = (async () => { + // 方式1: MCP http_request + if (typeof http_request !== 'undefined') { + const result = await http_request(url, method, headers, body, responseType); + return {status: result.status, statusText: result.statusText || '', body: result.body}; + } + + // 方式2: fetch + if (typeof fetch !== 'undefined') { + const response = await fetch(url, {method, headers, body}); + const resultBody = responseType === 'json' ? await response.json() : await response.text(); + return {status: response.status, statusText: response.statusText, body: resultBody}; + } + + // 方式3: Node.js https + if (typeof require !== 'undefined') { + try { + const https = require('https'); + const urlObj = new URL(url); + return new Promise((resolve, reject) => { + const req = https.request({ + hostname: urlObj.hostname, + port: urlObj.port || 443, + path: urlObj.pathname + urlObj.search, + method, + headers + }, res => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => resolve({ + status: res.statusCode, + statusText: res.statusMessage, + body: responseType === 'json' ? JSON.parse(data) : data + })); + }); + req.on('error', reject); + req.setTimeout(timeout, () => reject(new Error('请求超时'))); + if (body) req.write(body); + req.end(); + }); + } catch {} + } + + // 模拟响应 + return { + status: 200, + body: { + choices: [{ + message: { + content: "【模拟响应】环境不支持实际网络请求。", + role: "assistant" + }, + finish_reason: "stop" + }], + usage: {prompt_tokens: 10, completion_tokens: 30, total_tokens: 40}, + model: "mock-model", + id: "mock-id", + created: Date.now() + } + }; + })(); + + return Promise.race([requestPromise, timeoutPromise]); + } + + function cleanText(text) { + if (!text || typeof text !== 'string') return text; + + let cleaned = text; + + // 处理转义与实体 + cleaned = cleaned.replace(/\|["\\]?n/g, '\n'); + cleaned = cleaned.replace(/\\\\([\\nrt"'&])/g, (m, c) => ({n:'\n',r:'\r',t:'\t','"':'"',"'":"'","&":"&"}[c] || m)); + cleaned = cleaned.replace(/\\u([0-9A-Fa-f]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16))); + + // 一次性替换HTML实体 + const entities = {quot:'"', amp:'&', lt:'<', gt:'>', nbsp:' ', '#39':"'", apos:"'"}; + cleaned = cleaned.replace(/&(\w+|#\d+);/g, (m, e) => entities[e] || m); + + // 清理代码块 + cleaned = cleaned.replace(/```[\w-]*\s*\n([\s\S]*?)```/g, '$1').replace(/```/g, '').replace(/`([^`]+)`/g, '$1'); + + // 格式化空白与分点 + cleaned = cleaned.replace(/[ \t]{2,}/g, ' '); + cleaned = cleaned.split('\n').map(l => l.replace(/^[\s\uFEFF\xA0\u3000\u200B-\u200D]+/g, '')).join('\n'); + cleaned = cleaned.replace(/\n{3,}/g, '\n\n').trim(); + cleaned = cleaned.replace(/^\s*[-•●]\s+|^\s*\d+\.\s+|^\s*\d+\)\s+|^\s*\([a-zA-Z]\)\s+/gm, ''); + + return cleaned || text; + } + + function getConfig(varName, defaultValue = null) { + try { + const value = getEnv(varName); + if (!value || value === `YOUR_${varName}`) { + if (defaultValue !== null) return defaultValue; + throw new Error(`${varName} 未配置`); + } + return value.trim(); + } catch (e) { + if (defaultValue !== null) return defaultValue; + throw new Error(`${varName} 未配置`); + } + } + + function getFullConfig() { + try { + return { + apiBaseUrl: getConfig('AI_API_BASE_URL', ''), + apiKey: getConfig('AI_API_KEY', ''), + modelName: getConfig('AI_MODEL_NAME', ''), + timeout: 30000 + }; + } catch { + return {apiBaseUrl: '', apiKey: '', modelName: '', timeout: 30000}; + } + } + + async function tryEndpoints(baseUrl, payload, config, timeout) { + if (baseUrl.includes('chat/completions')) { + return await universalHttpRequest( + baseUrl, + 'POST', + { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${config.apiKey}` + }, + JSON.stringify(payload), + 'json', + timeout + ); + } + + const endpoints = ['v1/chat/completions', 'chat/completions']; + let lastError; + + for (const endpoint of endpoints) { + try { + const url = `${baseUrl}${endpoint}`; + const response = await universalHttpRequest( + url, + 'POST', + { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${config.apiKey}` + }, + JSON.stringify(payload), + 'json', + timeout + ); + if (response.status === 200) return response; + lastError = new Error(`端点 ${endpoint} 返回 ${response.status}`); + } catch (error) { + lastError = error; + } + } + + throw lastError || new Error('所有端点尝试失败'); + } + + async function chat_completion_logic(rawInput, timeout) { + if (rawInput === null || rawInput === undefined) { + throw new Error('参数错误: 输入参数不能为空'); + } + + let internalParams = { + messages: null, + system_prompt: undefined, + temperature: 0.7, + max_tokens: undefined, + functions: undefined + }; + + // 处理输入参数 + if (typeof rawInput === 'string') { + internalParams.messages = [{role: 'user', content: rawInput}]; + } else if (typeof rawInput === 'object') { + const hasMessageField = 'message' in rawInput; + const hasMessagesField = 'messages' in rawInput; + + if (hasMessagesField) { + const messagesValue = rawInput.messages; + if (Array.isArray(messagesValue)) { + internalParams.messages = messagesValue; + } else if (typeof messagesValue === 'string') { + internalParams.messages = [{role: 'user', content: messagesValue}]; + } else { + throw new Error(`'messages' 必须是数组或字符串`); + } + } else if (hasMessageField) { + const messageValue = rawInput.message; + if (typeof messageValue !== 'string') { + throw new Error(`'message' 必须是字符串`); + } + internalParams.messages = [{role: 'user', content: messageValue}]; + } else { + throw new Error(`对象必须包含 'message' 或 'messages' 字段`); + } + + internalParams.system_prompt = rawInput.system_prompt ? String(rawInput.system_prompt) : undefined; + internalParams.temperature = rawInput.temperature !== undefined ? Number(rawInput.temperature) : 0.7; + if (isNaN(internalParams.temperature)) internalParams.temperature = 0.7; + + internalParams.max_tokens = rawInput.max_tokens !== undefined ? Number(rawInput.max_tokens) : undefined; + if (internalParams.max_tokens !== undefined && isNaN(internalParams.max_tokens)) { + internalParams.max_tokens = undefined; + } + + internalParams.functions = Array.isArray(rawInput.functions) ? rawInput.functions : undefined; + } else { + throw new Error(`不支持的参数类型 '${typeof rawInput}'`); + } + + // 验证messages + if (!Array.isArray(internalParams.messages) || internalParams.messages.length === 0) { + throw new Error('messages必须是有效数组且不为空'); + } + + internalParams.messages.forEach((msg, idx) => { + if (!msg || typeof msg !== 'object' || !msg.role || typeof msg.content !== 'string') { + throw new Error(`消息 #${idx} 格式无效`); + } + }); + + // 获取配置 + const config = getFullConfig(); + if (!config.apiBaseUrl || !config.apiKey) { + throw new Error('AI_API_BASE_URL 和 AI_API_KEY 必须配置'); + } + + // 构建消息数组 + const antiListInstruction = "【重要指令】你必须以连续段落的方式回答,严禁使用任何分点、列表、编号或项目符号格式。"; + const finalMessages = [ + { + role: 'system', + content: internalParams.system_prompt + ? internalParams.system_prompt + "\n\n" + antiListInstruction + : antiListInstruction + }, + ...internalParams.messages + ]; + + // 构建请求 + const payload = { + model: String(config.modelName || 'gpt-3.5-turbo'), + messages: finalMessages, + temperature: Number(internalParams.temperature) + }; + + if (internalParams.max_tokens !== undefined && !isNaN(internalParams.max_tokens)) { + payload.max_tokens = Number(internalParams.max_tokens); + } + if (internalParams.functions) payload.functions = internalParams.functions; + + Object.keys(payload).forEach(key => payload[key] === undefined && delete payload[key]); + + // 发送请求 + let response; + try { + response = await tryEndpoints(config.apiBaseUrl, payload, config, timeout); + } catch { + response = await universalHttpRequest( + config.apiBaseUrl, + 'POST', + { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${config.apiKey}` + }, + JSON.stringify(payload), + 'json', + timeout + ); + } + + if (response.status !== 200) { + let errorMsg = `API请求失败: ${response.status}`; + try { + const errorData = typeof response.body === 'string' ? JSON.parse(response.body) : response.body; + errorMsg += ` - ${errorData.error?.message || JSON.stringify(errorData)}`; + } catch { + errorMsg += ` - ${response.statusText}`; + } + throw new Error(errorMsg); + } + + const result = typeof response.body === 'string' ? JSON.parse(response.body) : response.body; + if (!result.choices?.[0]) { + throw new Error('API返回格式异常'); + } + + const choice = result.choices[0]; + const reply = { + message: choice.message, + finish_reason: choice.finish_reason, + usage: result.usage || null, + model: result.model || config.modelName, + id: result.id, + created: result.created + }; + + if (choice.message.function_call) { + reply.is_function_call = true; + reply.function_name = choice.message.function_call.name; + reply.function_arguments = JSON.parse(choice.message.function_call.arguments || '{}'); + } + + return reply; + } + + async function chat_completion(params) { + if (typeof params !== 'object') { + params = {messages: String(params)}; + } + + const timeout = params.timeout || 30000; + + try { + const result = await Promise.race([ + chat_completion_logic(params, timeout), + new Promise((_, reject) => + setTimeout(() => reject(new Error(`操作超时:${timeout}ms`)), timeout) + ) + ]); + + const rawReply = result.message.content; + const cleanedReply = cleanText(rawReply); + + const response = { + success: true, + message: "AI回复获取成功!", + data: result, + reply: cleanedReply, + raw_reply: rawReply, + usage: result.usage, + anti_list_applied: true + }; + + if (typeof complete === 'function') { + try { + complete(response); + } catch {} + } + + return response; + } catch (error) { + const response = { + success: false, + message: `AI对话失败: ${error.message}`, + error_stack: error.stack + }; + + if (typeof complete === 'function') { + try { + complete(response); + } catch {} + } + + return response; + } + } + + return { + chat_completion: chat_completion, + single_message: chat_completion, + test_connection: async function() { + return await chat_completion({ + messages: "Hello! Please respond in a continuous paragraph without any list or bullet points.", + temperature: 0.1, + timeout: 10000 + }); + }, + getConfig: getFullConfig, + _makeHttpRequest: universalHttpRequest, + _cleanText: cleanText + }; +})(); + +if (typeof module !== 'undefined' && module.exports) { + module.exports = aiModelInteraction; +} + +async function quickTest1() { + console.log("配置状态:", aiModelInteraction.getConfig()); + + const result = await aiModelInteraction._makeHttpRequest( + 'https://httpbin.org/get', + 'GET', + {}, + null, + 'json', + 5000 + ); + + console.log("HTTP请求结果:", result.status === 200 ? "网络正常" : "网络异常"); +} + +async function quickTest2() { + console.log("\n测试2: 模拟响应测试"); + + const mockResponse = { + choices: [{ + message: { + content: "这是一个\\n模拟响应&quot;测试\\\\n字符串```javascript\\ncode\\n```", + role: "assistant" + }, + finish_reason: "stop" + }], + usage: { prompt_tokens: 5, completion_tokens: 10, total_tokens: 15 }, + model: "mock-model" + }; + + const cleaned = aiModelInteraction._cleanText(mockResponse.choices[0].message.content); + console.log("原始文本:", mockResponse.choices[0].message.content); + console.log("清理后:", cleaned); + console.log("清理功能:", cleaned.includes("这是一个") && !cleaned.includes("&") ? "正常" : "异常"); +} + +async function quickTest3() { + console.log("\n测试3: 完整对话测试(需要配置)"); + + setEnv('AI_API_BASE_URL', 'https://api.openai.com/v1'); + setEnv('AI_API_KEY', 'sk-your-api-key'); + setEnv('AI_MODEL_NAME', 'gpt-3.5-turbo'); + + console.log("配置:", aiModelInteraction.getConfig()); + + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error("独立超时:5秒无响应")), 5000); + }); + + const chatPromise = aiModelInteraction.chat_completion({ + messages: "你好,请介绍机器学习的优点(不要使用分点列表)", + temperature: 0.7, + timeout: 10000 + }); + + try { + const result = await Promise.race([chatPromise, timeoutPromise]); + console.log("结果:", result.success ? "成功" : "失败"); + if (result.success) { + console.log("回复:", result.reply); + const hasListPattern = /^\s*[•●-]\s+|^\s*\d+\.\s+|^\s*\d+\)\s+/m.test(result.reply); + console.log("分点检测:", hasListPattern ? "检测到列表符号" : "无列表符号"); + } else { + console.log("错误:", result.message); + } + } catch (e) { + console.log("异常:", e.message); + } +} + +async function debugStepByStep() { + console.log("\n调试:分步执行"); + + console.log("步骤1: 环境检查"); + console.log("- http_request:", typeof http_request !== 'undefined' ? "可用" : "不可用"); + console.log("- fetch:", typeof fetch !== 'undefined' ? "可用" : "不可用"); + console.log("- require:", typeof require !== 'undefined' ? "可用" : "不可用"); + + console.log("\n步骤2: 配置检查"); + const config = aiModelInteraction.getConfig(); + console.log("- API基础URL:", config.apiBaseUrl || "未设置"); + console.log("- API密钥:", config.apiKey ? "已设置" : "未设置"); + console.log("- 模型:", config.modelName || "未设置"); + + console.log("\n步骤3: 测试HTTP请求"); + try { + const httpResult = await Promise.race([ + aiModelInteraction._makeHttpRequest('https://httpbin.org/get', 'GET', {}, null, 'json', 5000), + new Promise((_, reject) => setTimeout(() => reject(new Error("HTTP测试超时")), 5000)) + ]); + console.log("HTTP请求:", httpResult.status === 200 ? "成功" : "失败"); + } catch (e) { + console.log("HTTP请求:", "异常", e.message); + } + + console.log("\n步骤4: 测试清理函数"); + const testText = 'Test\\n&quot;Hello&quot;```code```'; + const cleaned = aiModelInteraction._cleanText(testText); + console.log("清理结果:", cleaned.includes("Test") && cleaned.includes('"Hello"') ? "正常" : "异常"); + + console.log("\n调试完成!"); +}