├── .github └── FUNDING.yml ├── zbpack.json ├── .gitignore ├── pictures ├── url.png ├── 域名.png ├── BotGem.png ├── wechat.jpg ├── worker.png ├── Lobehub.png ├── NextChat.png ├── Opencat.png ├── Opencat2.png └── Opencatnews.png ├── vercel.json ├── package.json ├── .env.template ├── LICENSE ├── units ├── crawler.js ├── news.js └── search.js ├── api ├── index.js └── search2ai.js ├── README.md ├── README-EN.md ├── search2gemini.js ├── search2openai.js ├── search2groq.js └── search2moonshot.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: fatwang2 -------------------------------------------------------------------------------- /zbpack.json: -------------------------------------------------------------------------------- 1 | { 2 | "cache_dependencies": false 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vercel 3 | .env 4 | wrangler.toml 5 | .wrangler 6 | .vscode -------------------------------------------------------------------------------- /pictures/url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatwang2/search2ai/HEAD/pictures/url.png -------------------------------------------------------------------------------- /pictures/域名.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatwang2/search2ai/HEAD/pictures/域名.png -------------------------------------------------------------------------------- /pictures/BotGem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatwang2/search2ai/HEAD/pictures/BotGem.png -------------------------------------------------------------------------------- /pictures/wechat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatwang2/search2ai/HEAD/pictures/wechat.jpg -------------------------------------------------------------------------------- /pictures/worker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatwang2/search2ai/HEAD/pictures/worker.png -------------------------------------------------------------------------------- /pictures/Lobehub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatwang2/search2ai/HEAD/pictures/Lobehub.png -------------------------------------------------------------------------------- /pictures/NextChat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatwang2/search2ai/HEAD/pictures/NextChat.png -------------------------------------------------------------------------------- /pictures/Opencat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatwang2/search2ai/HEAD/pictures/Opencat.png -------------------------------------------------------------------------------- /pictures/Opencat2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatwang2/search2ai/HEAD/pictures/Opencat2.png -------------------------------------------------------------------------------- /pictures/Opencatnews.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatwang2/search2ai/HEAD/pictures/Opencatnews.png -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { "source": "/(.*)", "destination": "/api/index.js" } 4 | ] 5 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "search2ai", 3 | "version": "0.1.0", 4 | "description": "联网大模型", 5 | "main": "api/index.js", 6 | "scripts": { 7 | "start": "node api/index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "fatwang2", 11 | "license": "MIT", 12 | "dependencies": { 13 | "ai": "^2.2.31", 14 | "cors": "^2.8.5", 15 | "dotenv": "^16.3.1", 16 | "node-fetch": "^2.7.0", 17 | "pump": "^3.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | SEARCH1API_KEY=your_search1api_key 2 | GOOGLE_CX=your_google_cx 3 | GOOGLE_KEY=your_google_key 4 | SERPAPI_KEY=your_serpapi_key 5 | SERPER_KEY=your_serper_key 6 | BING_KEY=your_bing_key 7 | SEARXNG_BASE_URL=your_searxng_base_url 8 | SEARCH_SERVICE=your_search_service 9 | MAX_RESULTS=the results of search 10 | CRAWL_RESULTS=the reults of search you want to crawl 11 | APIBASE=https://api.openai.com 12 | OPENAI_TYPE="openai" 13 | AUTH_KEYS="1111,2222" 14 | RESOURCE_NAME="" 15 | DEPLOY_NAME="gpt-35-turbo" 16 | API_VERSION="2024-02-15-preview" 17 | AZURE_API_KEY="" 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2023] [fatwang2] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /units/crawler.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | // 爬取函数,调用你的爬取服务 4 | async function crawler(url) { 5 | console.log(`正在使用 URL 进行自定义爬取:${JSON.stringify(url)}`); 6 | try { 7 | const response = await fetch('https://crawl.search1api.com', { 8 | method: 'POST', 9 | headers: { 10 | "Content-Type": "application/json" 11 | }, 12 | body: JSON.stringify({ 13 | url: url 14 | }) 15 | }); 16 | 17 | if (!response.ok) { 18 | console.error(`API 请求失败, 状态码: ${response.status}`); 19 | return `API 请求失败, 状态码: ${response.status}`; 20 | } 21 | 22 | const contentType = response.headers.get("content-type"); 23 | if (!contentType || !contentType.includes("application/json")) { 24 | console.error("收到的响应不是有效的 JSON 格式"); 25 | return "收到的响应不是有效的 JSON 格式"; 26 | } 27 | 28 | const data = await response.json(); 29 | console.log('自定义爬取服务调用完成'); 30 | return JSON.stringify(data); 31 | } catch (error) { 32 | console.error(`在 crawler 函数中捕获到错误: ${error}`); 33 | return `在 crawler 函数中捕获到错误: ${error}`; 34 | } 35 | } 36 | module.exports = crawler; -------------------------------------------------------------------------------- /units/news.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const process = require('process'); 3 | const { config } = require('dotenv'); 4 | config({ path: __dirname + '/../.env' }); 5 | 6 | async function news(query) { 7 | console.log(`正在使用查询进行新闻搜索: ${JSON.stringify(query)}`); 8 | 9 | try { 10 | let results; 11 | 12 | switch (process.env.SEARCH_SERVICE) { 13 | case "search1api": 14 | const search1apiResponse = await fetch('https://api.search1api.com/news', { 15 | method: 'POST', 16 | headers: { 17 | "Content-Type": "application/json", 18 | "Authorization": process.env.SEARCH1API_KEY ? `Bearer ${process.env.SEARCH1API_KEY}` : '' 19 | }, 20 | body: JSON.stringify({ 21 | query: query, 22 | max_results: process.env.MAX_RESULTS || "10", 23 | crawl_results: process.env.CRAWL_RESULTS || "0" 24 | }) 25 | }); 26 | results = await search1apiResponse.json(); 27 | break; 28 | 29 | case "google": 30 | const googleApiUrl = `https://www.googleapis.com/customsearch/v1?cx=${process.env.GOOGLE_CX}&key=${process.env.GOOGLE_KEY}&q=${encodeURIComponent(query)}&tbm=nws`; 31 | const googleResponse = await fetch(googleApiUrl); 32 | const googleData = await googleResponse.json(); 33 | results = googleData.items.slice(0, process.env.MAX_RESULTS).map((item) => ({ 34 | title: item.title, 35 | link: item.link, 36 | snippet: item.snippet 37 | })); 38 | break; 39 | 40 | case "bing": 41 | const bingApiUrl = `https://api.bing.microsoft.com/v7.0/news/search?q=${encodeURIComponent(query)}`; 42 | const bingResponse = await fetch(bingApiUrl, { 43 | headers: { "Ocp-Apim-Subscription-Key": process.env.BING_KEY } 44 | }); 45 | const bingData = await bingResponse.json(); 46 | results = bingData.value.slice(0, process.env.MAX_RESULTS).map((item) => ({ 47 | title: item.name, 48 | link: item.url, 49 | snippet: item.description 50 | })); 51 | break; 52 | 53 | case "serpapi": 54 | const serpApiUrl = `https://serpapi.com/search?api_key=${process.env.SERPAPI_KEY}&engine=google_news&q=${encodeURIComponent(query)}&google_domain=google.com`; 55 | const serpApiResponse = await fetch(serpApiUrl); 56 | const serpApiData = await serpApiResponse.json(); 57 | results = serpApiData.news_results.slice(0, process.env.MAX_RESULTS).map((item) => ({ 58 | title: item.title, 59 | link: item.link, 60 | snippet: item.snippet 61 | })); 62 | break; 63 | 64 | case "serper": 65 | const gl = process.env.GL || "us"; 66 | const hl = process.env.HL || "en"; 67 | const serperApiUrl = "https://google.serper.dev/news"; 68 | const serperResponse = await fetch(serperApiUrl, { 69 | method: "POST", 70 | headers: { 71 | "X-API-KEY": process.env.SERPER_KEY, 72 | "Content-Type": "application/json" 73 | }, 74 | body: JSON.stringify({ q: query, gl: gl, hl: hl }) 75 | }); 76 | const serperData = await serperResponse.json(); 77 | results = serperData.news.slice(0, process.env.MAX_RESULTS).map((item) => ({ 78 | title: item.title, 79 | link: item.link, 80 | snippet: item.snippet 81 | })); 82 | break; 83 | 84 | case "duckduckgo": 85 | const duckDuckGoApiUrl = "https://ddg.search2ai.online/searchNews"; 86 | const body = { 87 | q: query, 88 | max_results: process.env.MAX_RESULTS || "10" 89 | }; 90 | const duckDuckGoResponse = await fetch(duckDuckGoApiUrl, { 91 | method: "POST", 92 | headers: { 93 | "Content-Type": "application/json" 94 | }, 95 | body: JSON.stringify(body) 96 | }); 97 | const duckDuckGoData = await duckDuckGoResponse.json(); 98 | results = duckDuckGoData.results.map((item) => ({ 99 | title: item.title, 100 | link: item.url, 101 | snippet: item.body 102 | })); 103 | break; 104 | 105 | case "searxng": 106 | const searXNGUrl = `${process.env.SEARXNG_BASE_URL}/search?q=${encodeURIComponent( 107 | query 108 | )}&category=news&format=json`; 109 | const searXNGResponse = await fetch(searXNGUrl); 110 | const searXNGData = await searXNGResponse.json(); 111 | results = searXNGData.results.slice(0, MAX_RESULTS).map((item) => ({ 112 | title: item.title, 113 | link: item.url, 114 | snippet: item.content 115 | })); 116 | break; 117 | 118 | default: 119 | console.error(`不支持的搜索服务: ${process.env.SEARCH_SERVICE}`); 120 | return `不支持的搜索服务: ${process.env.SEARCH_SERVICE}`; 121 | } 122 | 123 | const data = { 124 | results: results 125 | }; 126 | 127 | console.log('新闻搜索服务调用完成'); 128 | return JSON.stringify(data); 129 | 130 | } catch (error) { 131 | console.error(`在 news 函数中捕获到错误: ${error}`); 132 | return `在 news 函数中捕获到错误: ${error}`; 133 | } 134 | } 135 | 136 | module.exports = news; -------------------------------------------------------------------------------- /units/search.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const process = require('process'); 3 | const { config } = require('dotenv'); 4 | config({ path: __dirname + '/../.env' }); 5 | 6 | async function search(query) { 7 | console.log(`正在使用查询进行自定义搜索: ${JSON.stringify(query)}`); 8 | 9 | try { 10 | let results; 11 | 12 | switch (process.env.SEARCH_SERVICE) { 13 | case "search1api": 14 | const search1apiResponse = await fetch('https://api.search1api.com/search/', { 15 | method: 'POST', 16 | headers: { 17 | "Content-Type": "application/json", 18 | "Authorization": process.env.SEARCH1API_KEY ? `Bearer ${process.env.SEARCH1API_KEY}` : '' 19 | }, 20 | body: JSON.stringify({ 21 | query: query, 22 | max_results: process.env.MAX_RESULTS || "10", 23 | crawl_results: process.env.CRAWL_RESULTS || "0" 24 | }) 25 | }); 26 | results = await search1apiResponse.json(); 27 | break; 28 | 29 | case "google": 30 | const googleApiUrl = `https://www.googleapis.com/customsearch/v1?cx=${process.env.GOOGLE_CX}&key=${process.env.GOOGLE_KEY}&q=${encodeURIComponent(query)}`; 31 | const googleResponse = await fetch(googleApiUrl); 32 | const googleData = await googleResponse.json(); 33 | results = googleData.items.slice(0, process.env.MAX_RESULTS).map((item) => ({ 34 | title: item.title, 35 | link: item.link, 36 | snippet: item.snippet 37 | })); 38 | break; 39 | 40 | case "bing": 41 | const bingApiUrl = `https://api.bing.microsoft.com/v7.0/search?q=${encodeURIComponent(query)}`; 42 | const bingResponse = await fetch(bingApiUrl, { 43 | headers: { "Ocp-Apim-Subscription-Key": process.env.BING_KEY } 44 | }); 45 | const bingData = await bingResponse.json(); 46 | results = bingData.webPages.value.slice(0, process.env.MAX_RESULTS).map((item) => ({ 47 | title: item.name, 48 | link: item.url, 49 | snippet: item.snippet 50 | })); 51 | break; 52 | 53 | case "serpapi": 54 | const serpApiUrl = `https://serpapi.com/search?api_key=${process.env.SERPAPI_KEY}&engine=google&q=${encodeURIComponent(query)}&google_domain=google.com`; 55 | const serpApiResponse = await fetch(serpApiUrl); 56 | const serpApiData = await serpApiResponse.json(); 57 | results = serpApiData.organic_results.slice(0, process.env.MAX_RESULTS).map((item) => ({ 58 | title: item.title, 59 | link: item.link, 60 | snippet: item.snippet 61 | })); 62 | break; 63 | 64 | case "serper": 65 | const gl = process.env.GL || "us"; 66 | const hl = process.env.HL || "en"; 67 | const serperApiUrl = "https://google.serper.dev/search"; 68 | const serperResponse = await fetch(serperApiUrl, { 69 | method: "POST", 70 | headers: { 71 | "X-API-KEY": process.env.SERPER_KEY, 72 | "Content-Type": "application/json" 73 | }, 74 | body: JSON.stringify({ q: query, gl: gl, hl: hl }) 75 | }); 76 | const serperData = await serperResponse.json(); 77 | results = serperData.organic.slice(0, process.env.MAX_RESULTS).map((item) => ({ 78 | title: item.title, 79 | link: item.link, 80 | snippet: item.snippet 81 | })); 82 | break; 83 | 84 | case "duckduckgo": 85 | const duckDuckGoApiUrl = "https://ddg.search2ai.online/search"; 86 | const body = { 87 | q: query, 88 | max_results: process.env.MAX_RESULTS || "10" 89 | }; 90 | const duckDuckGoResponse = await fetch(duckDuckGoApiUrl, { 91 | method: "POST", 92 | headers: { 93 | "Content-Type": "application/json" 94 | }, 95 | body: JSON.stringify(body) 96 | }); 97 | const duckDuckGoData = await duckDuckGoResponse.json(); 98 | results = duckDuckGoData.results.map((item) => ({ 99 | title: item.title, 100 | link: item.href, 101 | snippet: item.body 102 | })); 103 | break; 104 | 105 | case "searxng": 106 | const searXNGUrl = `${process.env.SEARXNG_BASE_URL}/search?q=${encodeURIComponent( 107 | query 108 | )}&category=general&format=json`; 109 | const searXNGResponse = await fetch(searXNGUrl); 110 | const searXNGData = await searXNGResponse.json(); 111 | results = searXNGData.results.slice(0, MAX_RESULTS).map((item) => ({ 112 | title: item.title, 113 | link: item.url, 114 | snippet: item.content 115 | })); 116 | break; 117 | 118 | default: 119 | console.error(`不支持的搜索服务: ${process.env.SEARCH_SERVICE}`); 120 | return `不支持的搜索服务: ${process.env.SEARCH_SERVICE}`; 121 | } 122 | 123 | const data = { 124 | results: results 125 | }; 126 | 127 | console.log('自定义搜索服务调用完成'); 128 | return JSON.stringify(data); 129 | 130 | } catch (error) { 131 | console.error(`在 search 函数中捕获到错误: ${error}`); 132 | return `在 search 函数中捕获到错误: ${error}`; 133 | } 134 | } 135 | 136 | module.exports = search; -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | // index.js 示例 2 | const fetch = require('node-fetch'); 3 | const handleRequest = require('./search2ai.js'); 4 | const process = require('process'); 5 | const Stream = require('stream'); 6 | const http = require('http'); 7 | 8 | const corsHeaders = { 9 | 'Access-Control-Allow-Origin': '*', 10 | 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', // 允许的HTTP方法 11 | 'Access-Control-Allow-Headers': 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization', 12 | 'Access-Control-Max-Age': '86400', // 预检请求结果的缓存时间 13 | }; 14 | 15 | function handleOptions() { 16 | return { 17 | status: 204, 18 | headers: corsHeaders 19 | }; 20 | } 21 | 22 | async function handleOtherRequest(apiBase, apiKey, req, pathname) { 23 | // 创建一个新的 Headers 对象,复制原始请求的所有头部,但不包括 Host 头部 24 | const headers = {...req.headers}; 25 | delete headers['host']; 26 | headers['authorization'] = `Bearer ${apiKey}`; 27 | 28 | // 对所有请求,直接转发 29 | const response = await fetch(`${apiBase}${pathname}`, { 30 | method: req.method, 31 | headers: headers, 32 | body: req.body 33 | }); 34 | 35 | let data; 36 | if (pathname.startsWith('/v1/audio/')) { 37 | // 如果路径以 '/v1/audio/' 开头,处理音频文件 38 | const arrayBuffer = await response.arrayBuffer(); 39 | data = Buffer.from(arrayBuffer); 40 | return { 41 | status: response.status, 42 | headers: { ...response.headers, 'Content-Type': 'audio/mpeg', ...corsHeaders }, 43 | body: data 44 | }; 45 | } else { 46 | // 对于其他路径,处理 JSON 数据 47 | data = await response.json(); 48 | return { 49 | status: response.status, 50 | headers: corsHeaders, 51 | body: JSON.stringify(data) 52 | }; 53 | } 54 | } 55 | 56 | module.exports = async (req, res) => { 57 | console.log(`收到请求: ${req.method} ${req.url}`); 58 | if (req.url === '/') { 59 | res.statusCode = 200; 60 | res.setHeader('Content-Type', 'text/html'); 61 | res.end('

欢迎体验search2ai,让你的大模型自由联网!

'); 62 | return; 63 | } 64 | const apiBase = process.env.APIBASE || 'https://api.openai.com'; 65 | const authHeader = req.headers['authorization']; // 从请求的 headers 中获取 Authorization 66 | if (req.method === 'OPTIONS') { 67 | const optionsResponse = handleOptions(); 68 | res.statusCode = optionsResponse.status; 69 | Object.entries(optionsResponse.headers).forEach(([key, value]) => { 70 | res.setHeader(key, value); 71 | }); 72 | res.end(); 73 | return; 74 | } 75 | let apiKey = ''; 76 | if (authHeader) { 77 | apiKey = authHeader.split(' ')[1]; // 从 Authorization 中获取 API key 78 | } else { 79 | res.statusCode = 400; 80 | res.end('Authorization header is missing'); 81 | return; 82 | } 83 | let response; 84 | try { 85 | if (req.url === '/v1/chat/completions') { 86 | console.log('接收到 fetch 事件'); 87 | response = await handleRequest(req, res, apiBase, apiKey); 88 | } else { 89 | response = await handleOtherRequest(apiBase, apiKey, req, req.url); 90 | } 91 | } catch (error) { 92 | console.error('请求处理时发生错误:', error); 93 | response = { status: 500, body: 'Internal Server Error' }; 94 | } 95 | if (!res.headersSent) { 96 | res.statusCode = response.status; 97 | Object.entries({...response.headers, ...corsHeaders}).forEach(([key, value]) => { 98 | res.setHeader(key, value); 99 | }); 100 | if (response.body instanceof Stream) { 101 | console.log('Sending response as a stream'); // 添加的日志 102 | response.body.pipe(res); 103 | } else { 104 | console.log('Sending response as a string or Buffer'); // 添加的日志 105 | res.end(response.body); 106 | } 107 | } 108 | } 109 | 110 | // 创建服务器 111 | const server = http.createServer((req, res) => { 112 | if (req.method === "POST") { 113 | let body = []; // 使用数组来收集数据块 114 | req.on("data", chunk => { 115 | body.push(chunk); // 收集数据块 116 | }); 117 | req.on("end", () => { 118 | // 将数据块组合成完整的数据 119 | const combinedData = Buffer.concat(body); 120 | // 如果请求是音频,直接使用二进制数据 121 | if (!req.url.startsWith("/v1/audio/")) { 122 | try { 123 | // 尝试解析JSON 124 | req.body = JSON.parse(combinedData.toString()); 125 | } catch (error) { 126 | res.statusCode = 400; 127 | console.error("Invalid JSON:", error); 128 | res.end("Invalid JSON"); 129 | return; 130 | } 131 | } else { 132 | // 对于音频请求,直接使用二进制数据 133 | req.body = combinedData; 134 | } 135 | processRequest(req, res); 136 | }); 137 | } else { 138 | // GET 和其他类型的请求直接处理 139 | processRequest(req, res); 140 | } 141 | }); 142 | function processRequest(req, res) { 143 | (async () => { 144 | try { 145 | await module.exports(req, res); 146 | } catch (err) { 147 | console.error('处理请求时发生错误:', err); 148 | res.statusCode = 500; 149 | res.end('Internal Server Error'); 150 | } 151 | })(); 152 | } 153 | 154 | // 在指定的端口上监听请求 155 | const PORT = process.env.PORT || 3014; 156 | server.listen(PORT, () => { 157 | console.log(`Server is listening on port ${PORT}`); 158 | }); 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **简体中文** · [English](README-EN.md) 2 | 3 | ## 用户交流 4 | 5 | [discord 频道](https://discord.gg/AKXYq32Bxc) 6 | 7 | ## 友情赞助 8 | 9 | Buy Me A Coffee 10 | 11 | # 版本更新 12 | - V0.2.6,20240425,支持 SearXNG 免费搜索服务,有损支持 Moonshot 流式模式 13 | - V0.2.5,20240425,为了解决隐私担忧,开源搜索接口部分的代码 14 | - V0.2.4,20240424,支持 Groq 的llama-3、mistral等模型,速度起飞 15 | - V0.2.3,20240423,Cloudflare Worker版本支持Azure OpenAI;支持授权码,可自定义用户的请求key 16 | - V0.2.2,20240420,支持 Moonshot 的非流式模式 17 | - V0.2.1,20240310,支持Google、Bing、Duckduckgo、Search1API新闻类搜索;支持通过环境变量MAX_RESULTS调整搜索结果数量;支持通过环境变量CRAWL_RESULTS调整希望深度搜索的数量 18 | - V0.2.0,20240310,优化openai.js,cloudflare worker版本,这次速度真的更快了! 19 | 20 | 更多历史更新,请参见 [版本记录](https://github.com/fatwang2/search2ai/releases) 21 | 22 | # S2A 23 | 24 | 让你的 大模型 API 支持联网,搜索、新闻、网页总结,已支持OpenAI、Gemini、Moonshot(非流式),大模型会根据你的输入判断是否联网,不是每次都联网搜索,不需要安装任何插件,也不需要更换key,直接在你常用的三方客户端替换自定义地址即可,也支持自行部署,不会影响使用的其他功能,如画图、语音等 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
效果示例效果示例
效果示例效果示例
36 | 37 | # 功能 38 | 39 | | 模型 | 功能 | 流式输出 | 部署方式 | 40 | | ---------------- | -------------------- | ------------ | ------------------------------------------- | 41 | | `OpenAI` | 联网、新闻、内容爬取 | 流式、非流式 | Zeabur、本地部署、Cloudflare Worker、Vercel | 42 | | `Azure OpenAI` | 联网、新闻、内容爬取 | 流式、非流式 | Cloudflare Worker | 43 | | `Groq` | 联网、新闻、内容爬取 | 流式、非流式 | Cloudflare Worker | 44 | | `Gemini` | 联网 | 流式、非流式 | Cloudflare Worker | 45 | | `Moonshot` | 联网、新闻、内容爬取 | 部分流式、非流式 | Zeabur、本地部署、Cloudflare Worker(流式)、Vercel | 46 | 47 | # 使用 48 | 49 | **替换客户端自定义域名为你部署后的地址** 50 | 51 | 52 | 53 | 54 | 55 |
效果示例
56 | 57 | # 部署 58 | 59 | **Zeabur一键部署** 60 | 61 | 点击按钮即可一键部署,修改环境变量 62 | 63 | [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/A4HGYF?referralCode=fatwang2) 64 | 65 | 如需保持项目更新,建议先fork本仓库,再通过Zeabur部署你的分支 66 | 67 | [![Deployed on Zeabur](https://zeabur.com/deployed-on-zeabur-dark.svg)](https://zeabur.com?referralCode=fatwang2&utm_source=fatwang2&utm_campaign=oss) 68 | 69 | **本地部署** 70 | 71 | 1. 克隆仓库到本地 72 | 73 | ``` 74 | git clone https://github.com/fatwang2/search2ai 75 | ``` 76 | 77 | 2. 复制.env.template为.env,配置环境变量 78 | 3. 进入api目录,运行程序,实时显示日志 79 | 80 | ``` 81 | cd api && nohup node index.js > output.log 2>&1 & tail -f output.log 82 | ``` 83 | 84 | 4. 端口3014,拼接后的完整地址如下,可根据客户端的需求配置apibase地址使用(如需https,需用nginx进行反代,网上教程很多) 85 | 86 | ``` 87 | http://localhost:3014/v1/chat/completions 88 | ``` 89 | 90 | **Cloudflare Worker部署** 91 | 92 | 1. 复制[search2openai.js](search2openai.js)或者[search2gemini.js](search2gemini.js)或者[search2groq.js](search2groq.js)的代码,不需要任何修改!在cloudflare的worker里部署,上线后的worker的地址可作为你接口调用时的自定义域名地址,注意拼接,worker地址仅代表v1前的部分 93 | 2. 在worker中配置环境变量 94 | ![效果示例](pictures/worker.png) 95 | 3. worker里配置触发器-自定义域名,国内直接访问worker的地址可能会出问题,需要替换为自定义域名 96 | ![Alt text](pictures/域名.png) 97 | 98 | **Vercel部署** 99 | 100 | 特别说明:vercel项目暂不支持流式输出,且有10s响应限制,实际使用体验不佳,放出来主要是想等大神给我pull request 101 | 102 | 一键部署 103 | 104 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Ffatwang2%2Fsearch2ai&env=SEARCH_SERVICE&envDescription=%E6%9A%82%E6%97%B6%E6%94%AF%E6%8C%81google%E3%80%81bing%E3%80%81serpapi%E3%80%81serper%E3%80%81duckduckgo%EF%BC%8C%E5%BF%85%E5%A1%AB) 105 | 106 | 为保证更新,也可以先fork本项目后自己在vercel上部署 107 | 108 | # 环境变量 109 | 110 | 该项目提供了一些额外的配置项,通过环境变量设置: 111 | 112 | | 环境变量 | 是否必须 | 描述 | 例子 | 113 | | -------------------- | -------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | 114 | | `SEARCH_SERVICE` | Yes | 你的搜索服务,选择什么服务,就需要配置什么服务的key | `search1api, google, bing, serpapi, serper, duckduckgo, searxng` | 115 | | `APIBASE` | No | 三方代理地址 | `https://api.openai.com, https://api.moonshot.cn, https://api.groq.com/openai` | 116 | | `MAX_RESULTS` | Yes | 搜索结果条数 | `10` | 117 | | `CRAWL_RESULTS` | No | 要进行深度搜索(搜索后获取网页正文)的数量,目前仅支持 search1api,深度速度会慢 | `1` | 118 | | `SEARCH1API_KEY` | No | 如选search1api必填,我自己的搜索服务,注册免费领取 100 积分,点击[链接](https://www.search1api.com/?utm_source=search2ai) | `xxx` | 119 | | `BING_KEY` | No | 如选bing搜索必填,请自行搜索教程,点击[链接](https://www.microsoft.com/en-us/bing/apis/bing-web-search-api) 创建 | `xxx` | 120 | | `GOOGLE_CX` | No | 如选Google搜索必填,Search engine ID,请自行搜索教程,点击[链接](https://programmablesearchengine.google.com/controlpanel/create) 创建 | `xxx` | 121 | | `GOOGLE_KEY` | No | 如选Google搜索必填,API key,点击[链接](https://console.cloud.google.com/apis/credentials) 创建 | `xxx` | 122 | | `SERPAPI_KEY` | No | 如选serpapi必填,免费100次/月,点击[链接](https://serpapi.com/) 注册 | `xxx` | 123 | | `SERPER_KEY` | No | 如选serper必填,6个月免费额度2500次,点击[链接](https://serper.dev/) 注册 | `xxx` | 124 | | `SEARXNG_BASE_URL` | No | 如选searxng必填,填写自建searXNG服务域名,需打开 json 模式,教程参考[链接](https://github.com/searxng/searxng) | `https://search.xxx.xxx` | 125 | | `OPENAI_TYPE` | No | openai供给来源,默认为openai | `openai, azure` | 126 | | `RESOURCE_NAME` | No | 如选azure必填 | `xxxx` | 127 | | `DEPLOY_NAME` | No | 如选azure必填 | `gpt-35-turbo` | 128 | | `API_VERSION` | No | 如选azure必填 | `2024-02-15-preview` | 129 | | `AZURE_API_KEY` | No | 如选azure必填 | `xxxx` | 130 | | `AUTH_KEYS` | No | 如果希望用户请求的时候单独定义授权码作为key,则需要填写,如选azure则必填 | `000,1111,2222` | 131 | | `OPENAI_API_KEY` | No | 如果希望用户请求openai的时候也单独定义授权码作为key,则需要填写 | `sk-xxx` | 132 | 133 | # 后续迭代 134 | 135 | - 修复Vercel项目流式输出问题 136 | - 提升流式输出的速度 137 | - 支持更多垂类搜索 138 | -------------------------------------------------------------------------------- /api/search2ai.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const search = require('../units/search.js'); 3 | const crawler = require('../units/crawler.js'); 4 | const news = require('../units/news.js'); 5 | const { config } = require('dotenv'); 6 | const Stream = require('stream'); 7 | 8 | config(); 9 | 10 | const corsHeaders = { 11 | 'Access-Control-Allow-Origin': '*', 12 | 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', // 允许的HTTP方法 13 | 'Access-Control-Allow-Headers': 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization', 14 | 'Access-Control-Max-Age': '86400', // 预检请求结果的缓存时间 15 | }; 16 | async function handleRequest(req, res, apiBase, apiKey) { 17 | let responseSent = false; 18 | if (req.method !== 'POST') { 19 | console.log(`不支持的请求方法: ${req.method}`); 20 | res.statusCode = 405; 21 | res.end('Method Not Allowed'); 22 | responseSent = true; 23 | return; 24 | } 25 | const requestData = req.body; 26 | console.log('请求数据:', requestData); 27 | console.log('API base:', apiBase); 28 | const stream = requestData.stream || false; 29 | const userMessages = requestData.messages.filter(message => message.role === 'user'); 30 | const latestUserMessage = userMessages[userMessages.length - 1]; 31 | const model = requestData.model 32 | const isContentArray = Array.isArray(latestUserMessage.content); 33 | const defaultMaxTokens = 3000; 34 | const maxTokens = requestData.max_tokens || defaultMaxTokens; // 使用默认 max_tokens 如果未提供 35 | 36 | const body = JSON.stringify({ 37 | model: model, 38 | messages: requestData.messages, 39 | max_tokens: maxTokens, 40 | ...(isContentArray ? {} : { 41 | tools: [ 42 | { 43 | type: "function", 44 | function: { 45 | name: "search", 46 | description: "search for factors", 47 | parameters: { 48 | type: "object", 49 | properties: { 50 | query: { type: "string","description": "The query to search."} 51 | }, 52 | required: ["query"] 53 | } 54 | } 55 | }, 56 | { 57 | type: "function", 58 | function: { 59 | name: "news", 60 | description: "Search for news", 61 | parameters: { 62 | type: "object", 63 | properties: { 64 | query: { type: "string", description: "The query to search for news." } 65 | }, 66 | required: ["query"] 67 | } 68 | } 69 | }, 70 | { 71 | type: "function", 72 | function: { 73 | name: "crawler", 74 | description: "Get the content of a specified url", 75 | parameters: { 76 | type: "object", 77 | properties: { 78 | url: { 79 | type: "string", 80 | description: "The URL of the webpage"}, 81 | }, 82 | required: ["url"], 83 | } 84 | } 85 | } 86 | ], 87 | tool_choice: "auto" 88 | }) 89 | }); 90 | let openAIResponse; 91 | try { 92 | openAIResponse = await fetch(`${apiBase}/v1/chat/completions`, { 93 | method: 'POST', 94 | headers: { 95 | 'Content-Type': 'application/json', 96 | 'Authorization': `Bearer ${apiKey}` 97 | }, 98 | body: body 99 | }); 100 | } catch (error) { 101 | console.error('请求 OpenAI API 时发生错误:', error); 102 | res.statusCode = 500; 103 | res.end('OpenAI API 请求失败'); 104 | return { status: 500 }; 105 | } 106 | if (!openAIResponse.ok) { 107 | throw new Error('OpenAI API 请求失败'); 108 | } 109 | 110 | let data = await openAIResponse.json(); 111 | console.log('确认解析后的 data 对象:', data); 112 | if (!data) { 113 | console.error('OpenAI 响应没有数据'); 114 | res.statusCode = 500; 115 | res.end('OpenAI 响应没有数据'); 116 | return { status: 500 }; 117 | } 118 | console.log('OpenAI API 响应接收完成,检查是否需要调用自定义函数'); 119 | let messages = requestData.messages; 120 | if (!data.choices || data.choices.length === 0 || !data.choices[0].message) { 121 | console.error('OpenAI 响应数据格式不正确'); 122 | res.statusCode = 500; 123 | res.end('OpenAI 响应数据格式不正确'); 124 | return { status: 500 }; 125 | } 126 | 127 | messages.push(data.choices[0].message); 128 | console.log('更新后的 messages 数组:', messages); 129 | // 检查是否有函数调用 130 | console.log('开始检查是否有函数调用'); 131 | 132 | let calledCustomFunction = false; 133 | if (data.choices[0].message.tool_calls) { 134 | const toolCalls = data.choices[0].message.tool_calls; 135 | const availableFunctions = { 136 | "search": search, 137 | "news": news, 138 | "crawler": crawler 139 | }; 140 | for (const toolCall of toolCalls) { 141 | const functionName = toolCall.function.name; 142 | const functionToCall = availableFunctions[functionName]; 143 | const functionArgs = JSON.parse(toolCall.function.arguments); 144 | let functionResponse; 145 | if (functionName === 'search') { 146 | functionResponse = await functionToCall(functionArgs.query); 147 | } else if (functionName === 'crawler') { 148 | functionResponse = await functionToCall(functionArgs.url); 149 | } else if (functionName === 'news') { 150 | functionResponse = await functionToCall(functionArgs.query); 151 | } 152 | messages.push({ 153 | tool_call_id: toolCall.id, 154 | role: "tool", 155 | name: functionName, 156 | content: functionResponse, 157 | }); 158 | calledCustomFunction = true; 159 | } 160 | } else { 161 | console.log('没有发现函数调用'); 162 | } 163 | console.log('结束检查是否有函数调用'); 164 | 165 | // 如果调用了自定义函数,再次向 OpenAI API 发送请求 166 | if (calledCustomFunction) { 167 | let requestBody = { 168 | model: model, 169 | messages: messages, 170 | stream: stream 171 | }; 172 | try { 173 | let secondResponse = await fetch(`${apiBase}/v1/chat/completions`, { 174 | method: 'POST', 175 | headers: { 176 | 'Content-Type': 'application/json', 177 | 'Accept': 'application/json', 178 | 'Authorization': `Bearer ${apiKey}` 179 | }, 180 | body: JSON.stringify(requestBody) 181 | }); 182 | if (stream) { 183 | console.log('返回流'); 184 | return { 185 | status: secondResponse.status, 186 | headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache',...corsHeaders }, 187 | body: secondResponse.body 188 | }; 189 | }else { 190 | // 使用普通 JSON 格式 191 | const data = await secondResponse.json(); 192 | res.statusCode = secondResponse.status; 193 | res.setHeader('Content-Type', 'application/json'); 194 | res.end(JSON.stringify(data)); 195 | } 196 | } catch (error) { 197 | console.error('请求处理时发生错误:', error); 198 | if (!responseSent) { 199 | res.statusCode = 500; 200 | res.end('Internal Server Error'); 201 | responseSent = true; 202 | } return; 203 | } 204 | } else { 205 | // 没有调用自定义函数,直接返回原始回复 206 | console.log('没有调用自定义函数,返回原始回复'); 207 | if (stream) { 208 | // 使用 SSE 格式 209 | console.log('Using SSE format'); 210 | const sseStream = jsonToStream(data); 211 | res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 212 | ...corsHeaders }); 213 | 214 | sseStream.on('data', (chunk) => { 215 | res.write(chunk); 216 | }); 217 | 218 | sseStream.on('end', () => { 219 | res.end(); 220 | }); 221 | } else { 222 | // 使用普通 JSON 格式 223 | console.log('Using JSON format'); 224 | res.writeHead(200, { 'Content-Type': 'application/json', ...corsHeaders }); 225 | res.end(JSON.stringify(data)); 226 | } 227 | 228 | console.log('Response sent'); 229 | return { status: 200 }; 230 | } 231 | function jsonToStream(jsonData) { 232 | const characters = Array.from(jsonData.choices[0].message.content); 233 | let currentIndex = 0; 234 | 235 | return new Stream.Readable({ 236 | read() { 237 | const pushData = () => { 238 | if (currentIndex < characters.length) { 239 | const character = characters[currentIndex]; 240 | const newJsonData = { 241 | id: jsonData.id, 242 | object: 'chat.completion.chunk', 243 | created: jsonData.created, 244 | model: jsonData.model, 245 | choices: [ 246 | { 247 | index: 0, 248 | delta: { 249 | content: character 250 | }, 251 | logprobs: null, 252 | finish_reason: currentIndex === characters.length - 1 ? 'stop' : null 253 | } 254 | ], 255 | system_fingerprint: jsonData.system_fingerprint 256 | }; 257 | 258 | const data = `data: ${JSON.stringify(newJsonData)}\n\n`; 259 | this.push(data, 'utf8'); 260 | currentIndex++; 261 | } else { 262 | this.push('data: [DONE]\n\n', 'utf8'); 263 | this.push(null); // 结束流 264 | } 265 | }; 266 | 267 | setTimeout(pushData, 10); // 延迟 0.01 秒 268 | } 269 | }); 270 | }} 271 | module.exports = handleRequest; -------------------------------------------------------------------------------- /README-EN.md: -------------------------------------------------------------------------------- 1 | [简体中文](README.md) · **English** 2 | 3 | ## User Communication 4 | 5 | [discord](https://discord.gg/AKXYq32Bxc) 6 | 7 | ## Buy me a coffee 8 | 9 | Buy Me A Coffee 10 | 11 | # Version Updates 12 | - V0.2.6, 20240425, support the searxng search service, support the moonshot API in stream mode 13 | - V0.2.5, 20240425, open source the code for the search api 14 | - V0.2.4, 20240424, support for Groq in Cloudflare Worker 15 | - V0.2.3, 20240423, support for Azure OpenAI in Cloudflare Worker. It also introduces the ability to use an authorization code and customize the user's request key. 16 | - V0.2.2, 20240420, support Moonshot API on unstream mode 17 | - V0.2.1, 20240310, supports Google, Bing, Duckduckgo, Search1API for news-type searches; supports adjusting the number of search results via the MAX_RESULTS environment variable; supports adjusting the number of in-depth searches desired via the CRAWL_RESULTS environment variable. 18 | - V0.2.0,20240310,Optimized openai.js, cloudflare worker version, really faster this time! 19 | 20 | For more historical updates, please see [Version History](https://github.com/fatwang2/search2ai/releases) 21 | 22 | # S2A 23 | 24 | Help your LLM API support networking, search, news, web page summarization, has supported OpenAI, Gemini, Moonshot, the big model will be based on your input to determine whether the network, not every time the network search, do not need to install any plug-ins, do not need to replace the key, directly in your commonly used OpenAI/Gemini three-way client replacement of custom You can directly replace the customized address in your usual OpenAI/Gemini three-way client, and also support self-deployment, which will not affect the use of other functions, such as drawing, voice, etc. 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
效果示例效果示例
效果示例效果示例
36 | 37 | # Features 38 | 39 | | Model | Features | Stream | Deployments | 40 | | ---------------- | --------------------- | ---------------- | --------------------------------------------------- | 41 | | `OpenAI` | search, news, crawler | stream, unstream | Zeabur, Local deployment, Cloudflare Worker, Vercel | 42 | | `Azure OpenAI` | search, news, crawler | stream, unstream | Cloudflare Worker | 43 | | `Groq` | search, news, crawler | stream, unstream | Cloudflare Worker | 44 | | `Gemini` | search | stream, unstream | Cloudflare Worker | 45 | | `Moonshot` | search, news, crawler | stream(only on cf), unstream | Zeabur, Local deployment, Cloudflare Worker(stream), Vercel | 46 | 47 | # Usage 48 | 49 | **Replace the custom domain in any client with the following address** 50 | 51 | ![image](https://github.com/user-attachments/assets/ac321325-2253-4e94-bec8-8e84f8301108) 52 | 53 | 54 | 55 | # Deployment 56 | 57 | **Zeabur** 58 | 59 | Click the button for one-click deployment, switched on your own environment variables 60 | 61 | [![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/A4HGYF?referralCode=fatwang2) 62 | 63 | To keep the project updated, it is recommended to fork this repository first, then deploy your branch through Zeabur 64 | 65 | [![Deployed on Zeabur](https://zeabur.com/deployed-on-zeabur-dark.svg)](https://zeabur.com?referralCode=fatwang2&utm_source=fatwang2&utm_campaign=oss) 66 | 67 | **Local Deployment** 68 | 69 | 1. Clone the repository locally 70 | 71 | ``` 72 | git clone https://github.com/fatwang2/search2ai 73 | ``` 74 | 75 | 2. Copy .env.template as .env, configure environment variables 76 | 3. Enter the api directory, run the program, and display the log in real-time 77 | 78 | ``` 79 | cd api && nohup node index.js > output.log 2>&1 & tail -f output.log 80 | ``` 81 | 82 | 4. Port 3014, the complete address after concatenation is as follows, can be configured according to the client's requirements for the apibase address (if https is required, need to use nginx for reverse proxy, many tutorials online) 83 | 84 | ``` 85 | http://localhost:3014/v1/chat/completions 86 | ``` 87 | 88 | **Cloudflare worker** 89 | 90 | 1. Copy the code of [search2openai.js](search2openai.js), or [search2gemini.js](search2gemini.js), or [search2groq.js](search2groq.js), no modifications needed! Deploy in cloudflare's worker, after going online, the worker's address can be used as your interface call's custom domain address, note the concatenation, worker address only represents the part before v1 91 | 2. Configure variables in the worker(only openai) 92 | ![Effect Example](https://github.com/user-attachments/assets/05746a9d-0772-4b60-a228-63396fa1614a) 93 | 3. Configure triggers - custom domain in the worker, direct access to the worker's address in China might have issues, need to replace with custom domain 94 | ![Alt text](https://github.com/user-attachments/assets/01f5b013-e758-438e-ab53-2065892b0a24) 95 | 96 | 97 | **Vercel** 98 | 99 | Special note: Vercel project does not support streaming output and has a 10s response limit, actual user experience is poor, released mainly for experts to pull request 100 | 101 | One-click deployment 102 | 103 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Ffatwang2%2Fsearch2ai&env=SEARCH_SERVICE&envDescription=%E6%9A%82%E6%97%B6%E6%94%AF%E6%8C%81google%E3%80%81bing%E3%80%81serpapi%E3%80%81serper%E3%80%81duckduckgo%EF%BC%8C%E5%BF%85%E5%A1%AB) 104 | 105 | To ensure updates, you can also first fork this project and then deploy it on Vercel yourself 106 | 107 | # Environment Variables 108 | 109 | This project provides some additional configuration options, which can be set through environment variables: 110 | 111 | | Environment Variable | Required | Description | Example | 112 | | -------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | 113 | | `SEARCH_SERVICE` | Yes | Your search service. The key of the service you choose needs to be configured. | `search1api, google, bing, serpapi, serper, duckduckgo, searxng` | 114 | | `APIBASE` | No | Third-party proxy address. | `https://api.openai.com, https://api.moonshot.cn, https://api.groq.com/openai` | 115 | | `MAX_RESULTS` | Yes | Number of search results. | `10` | 116 | | `CRAWL_RESULTS` | No | The number of deep searches (retrieve the main text of the webpage after searching). Currently only supports search1api, deep search will be slow. | `1` | 117 | | `SEARCH1API_KEY` | No | Required if using search1api. Free 100 credits now. Click [here](https://www.search1api.com/?utm_source=search2ai) to register | `xxx` | 118 | | `BING_KEY` | No | Required if using Bing search. Please search for tutorials. Click [here](https://www.microsoft.com/en-us/bing/apis/bing-web-search-api) to create | `xxx` | 119 | | `GOOGLE_CX` | No | Required if using Google search. Search engine ID. Please search for tutorials. Click [here](https://programmablesearchengine.google.com/controlpanel/create) to create | `xxx` | 120 | | `GOOGLE_KEY` | No | Required if using Google search. API key. Click [here](https://console.cloud.google.com/apis/credentials) to create | `xxx` | 121 | | `SERPAPI_KEY` | No | Required if using serpapi. Free 100 requests/month. Click [here](https://serpapi.com/) to register | `xxx` | 122 | | `SERPER_KEY` | No | Required if using serper. Free 2500 requests for 6 months. Click [here](https://serper.dev/) to register | `xxx` | 123 | | `SEARXNG_BASE_URL` | No | Required if using searxng. Fill in your self-hosted searXNG service domain. JSON mode must be enabled. Tutorial: [link](https://github.com/searxng/searxng) | `https://search.xxx.xxx` 124 | | `OPENAI_TYPE` | No | OpenAI provider source, default is openai | `openai, azure` | 125 | | `RESOURCE_NAME` | Conditional | Required if azure is selected | `xxxx` | 126 | | `DEPLOY_NAME` | Conditional | Required if azure is selected | `gpt-35-turbo` | 127 | | `API_VERSION` | Conditional | Required if azure is selected | `2024-02-15-preview` | 128 | | `AZURE_API_KEY` | Conditional | Required if azure is selected | `xxxx` | 129 | | `AUTH_KEYS` | No | If you want users to define a separate authorization code as a key when making requests, you need to fill this in. Required if azure is selected | `000,1111,2222` | 130 | | `OPENAI_API_KEY` | No | If you want users to define a separate authorization code as a key when requesting openai, you need to fill this in | `sk-xxx` | 131 | 132 | # Future Iterations 133 | 134 | - Fix streaming output issues in Vercel project 135 | - Improve the speed of streaming output 136 | - Support more vertical searches 137 | -------------------------------------------------------------------------------- /search2gemini.js: -------------------------------------------------------------------------------- 1 | addEventListener("fetch", (event) => { 2 | event.respondWith(handleRequest(event.request)); 3 | }); 4 | 5 | const corsHeaders = { 6 | "Access-Control-Allow-Origin": "*", 7 | "Access-Control-Allow-Methods": "GET, POST, OPTIONS", // 允许的HTTP方法 8 | "Access-Control-Allow-Headers": 9 | "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,x-goog-api-client,x-goog-api-key", 10 | "Access-Control-Max-Age": "86400", // 预检请求结果的缓存时间 11 | }; 12 | async function search(query) { 13 | console.log( 14 | `正在使用 ${SEARCH_SERVICE} 进行自定义搜索: ${JSON.stringify(query)}` 15 | ); 16 | try { 17 | let results; 18 | 19 | switch (SEARCH_SERVICE) { 20 | case "search1api": 21 | const search1apiResponse = await fetch( 22 | "https://api.search1api.com/search/", 23 | { 24 | method: "POST", 25 | headers: { 26 | "Content-Type": "application/json", 27 | Authorization: 28 | typeof SEARCH1API_KEY !== "undefined" 29 | ? `Bearer ${SEARCH1API_KEY}` 30 | : "", 31 | }, 32 | body: JSON.stringify({ 33 | query, 34 | search_service: "google", 35 | max_results: 36 | typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "5", 37 | crawl_results: 38 | typeof CRAWL_RESULTS !== "undefined" ? CRAWL_RESULTS : "0", 39 | }), 40 | } 41 | ); 42 | results = await search1apiResponse.json(); 43 | break; 44 | 45 | case "google": 46 | const googleApiUrl = `https://www.googleapis.com/customsearch/v1?cx=${GOOGLE_CX}&key=${GOOGLE_KEY}&q=${encodeURIComponent( 47 | query 48 | )}`; 49 | const googleResponse = await fetch(googleApiUrl); 50 | const googleData = await googleResponse.json(); 51 | results = googleData.items.slice(0, MAX_RESULTS).map((item) => ({ 52 | title: item.title, 53 | link: item.link, 54 | snippet: item.snippet, 55 | })); 56 | break; 57 | 58 | case "bing": 59 | const bingApiUrl = `https://api.bing.microsoft.com/v7.0/search?q=${encodeURIComponent( 60 | query 61 | )}`; 62 | const bingResponse = await fetch(bingApiUrl, { 63 | headers: { "Ocp-Apim-Subscription-Key": BING_KEY }, 64 | }); 65 | const bingData = await bingResponse.json(); 66 | results = bingData.webPages.value.slice(0, MAX_RESULTS).map((item) => ({ 67 | title: item.name, 68 | link: item.url, 69 | snippet: item.snippet, 70 | })); 71 | break; 72 | 73 | case "serpapi": 74 | const serpApiUrl = `https://serpapi.com/search?api_key=${SERPAPI_KEY}&engine=google&q=${encodeURIComponent( 75 | query 76 | )}&google_domain=google.com`; 77 | const serpApiResponse = await fetch(serpApiUrl); 78 | const serpApiData = await serpApiResponse.json(); 79 | results = serpApiData.organic_results 80 | .slice(0, MAX_RESULTS) 81 | .map((item) => ({ 82 | title: item.title, 83 | link: item.link, 84 | snippet: item.snippet, 85 | })); 86 | break; 87 | 88 | case "serper": 89 | const gl = typeof GL !== "undefined" ? GL : "us"; 90 | const hl = typeof HL !== "undefined" ? HL : "en"; 91 | const serperApiUrl = "https://google.serper.dev/search"; 92 | const serperResponse = await fetch(serperApiUrl, { 93 | method: "POST", 94 | headers: { 95 | "X-API-KEY": SERPER_KEY, 96 | "Content-Type": "application/json", 97 | }, 98 | body: JSON.stringify({ q: query, gl: gl, hl: hl }), 99 | }); 100 | const serperData = await serperResponse.json(); 101 | results = serperData.organic.slice(0, MAX_RESULTS).map((item) => ({ 102 | title: item.title, 103 | link: item.link, 104 | snippet: item.snippet, 105 | })); 106 | break; 107 | 108 | case "duckduckgo": 109 | const duckDuckGoApiUrl = "https://ddg.search2ai.online/search"; 110 | const body = { 111 | q: query, 112 | max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "5", 113 | }; 114 | const duckDuckGoResponse = await fetch(duckDuckGoApiUrl, { 115 | method: "POST", 116 | headers: { 117 | "Content-Type": "application/json", 118 | }, 119 | body: JSON.stringify(body), 120 | }); 121 | const duckDuckGoData = await duckDuckGoResponse.json(); 122 | results = duckDuckGoData.results.map((item) => ({ 123 | title: item.title, 124 | link: item.href, 125 | snippet: item.body, 126 | })); 127 | break; 128 | 129 | case "searxng": 130 | const searXNGUrl = `${SEARXNG_BASE_URL}/search?q=${encodeURIComponent( 131 | query 132 | )}&category=general&format=json`; 133 | const searXNGResponse = await fetch(searXNGUrl); 134 | const searXNGData = await searXNGResponse.json(); 135 | results = searXNGData.results.slice(0, MAX_RESULTS).map((item) => ({ 136 | title: item.title, 137 | link: item.url, 138 | snippet: item.content, 139 | })); 140 | break; 141 | 142 | default: 143 | console.error(`不支持的搜索服务: ${SEARCH_SERVICE}`); 144 | return `不支持的搜索服务: ${SEARCH_SERVICE}`; 145 | } 146 | 147 | const data = { 148 | results: results, 149 | }; 150 | 151 | console.log("自定义搜索服务调用完成"); 152 | return JSON.stringify(data); 153 | } catch (error) { 154 | console.error(`在 search 函数中捕获到错误: ${error}`); 155 | return `在 search 函数中捕获到错误: ${error}`; 156 | } 157 | } 158 | 159 | async function parse_function_response(message) { 160 | const function_call = message[0]["functionCall"]; 161 | const function_name = function_call["name"]; 162 | 163 | console.log("Gemini: Called function " + function_name); 164 | 165 | let function_response; 166 | try { 167 | const arguments = function_call["args"]; 168 | 169 | if (function_name === "search") { 170 | // 检查 args 参数是否包含 query 属性 171 | if (!arguments.hasOwnProperty("query")) { 172 | function_response = "ERROR: Missing query parameter"; 173 | console.log("Missing query parameter"); 174 | return { function_name, function_response }; 175 | } 176 | 177 | // 获取 query 参数的值 178 | const query = arguments.query; 179 | 180 | // 调用 search 函数并获取结果 181 | function_response = await search(query); 182 | return { function_name, function_response }; // 直接返回 183 | } else { 184 | function_response = "ERROR: Called unknown function"; 185 | console.log("Called unknown function:", function_name); 186 | } 187 | } catch (error) { 188 | function_response = "ERROR: Invalid arguments"; 189 | console.log("Invalid arguments:", error.message); 190 | } 191 | console.log("Function response:", function_response); 192 | 193 | return { function_name, function_response }; 194 | } 195 | 196 | async function fetchWithRetry(url, options, maxRetries = 3) { 197 | for (let i = 0; i < maxRetries; i++) { 198 | try { 199 | const response = await fetch(url, options); 200 | if (response.ok) { 201 | return response; 202 | } 203 | } catch (error) { 204 | console.error(`Attempt ${i + 1} failed. Retrying...`); 205 | } 206 | } 207 | throw new Error(`Failed to fetch after ${maxRetries} attempts`); 208 | } 209 | 210 | async function run_conversation(api_key, message, isStream, isSSE) { 211 | const date = new Date(); 212 | const timeZone = "Asia/Shanghai"; 213 | const formatter = new Intl.DateTimeFormat("en-US", { 214 | dateStyle: "full", 215 | timeZone, 216 | }); 217 | const currentDate = formatter.format(date); 218 | if (!message) { 219 | console.log("Invalid message:", message); 220 | return errorResponse("Invalid message", 400); 221 | } 222 | const customMessage = [ 223 | { 224 | role: "user", 225 | parts: [ 226 | { 227 | text: `Today is ${currentDate}.You are a friendly intelligent assistant with the ability to search online, and you can use emoji to make your conversations more interesting!`, 228 | }, 229 | ], 230 | }, 231 | { 232 | role: "model", 233 | parts: [ 234 | { 235 | text: "okay", 236 | }, 237 | ], 238 | }, 239 | ]; 240 | message = [...customMessage, ...message]; 241 | console.log("Running conversation with message:", message); 242 | const originalMessage = [...message]; 243 | 244 | const definitions = [ 245 | { 246 | name: "search", 247 | description: 248 | "search on the Interent when the users want something new to know", 249 | parameters: { 250 | type: "object", 251 | properties: { 252 | query: { 253 | type: "string", 254 | description: "The query to search", 255 | }, 256 | }, 257 | }, 258 | }, 259 | ]; 260 | 261 | const data = { 262 | contents: message, 263 | tools: [ 264 | { 265 | functionDeclarations: definitions, 266 | }, 267 | ], 268 | }; 269 | const api_url = isStream 270 | ? "https://gemini.sum4all.site/v1beta/models/gemini-pro:streamGenerateContent?key=" + 271 | api_key 272 | : "https://gemini.sum4all.site/v1beta/models/gemini-pro:generateContent?key=" + 273 | api_key; 274 | 275 | const response = await fetchWithRetry(api_url, { 276 | method: "POST", 277 | body: JSON.stringify(data), 278 | }); 279 | // 打印响应的状态 280 | console.log("Response status:", response.status); 281 | console.log("Response ok:", response.ok); 282 | console.log("Response status text:", response.statusText); 283 | if (!response.ok) { 284 | console.log("Received error response from run_conversation"); 285 | // 修改: 返回表示出错的 Response 对象 286 | return new Response( 287 | JSON.stringify({ error: "Error fetching from Google Language API" }), 288 | { 289 | headers: { ...corsHeaders, "content-type": "application/json" }, 290 | status: 500, // 代表出现 Internal Server Error 的错误码 291 | } 292 | ); 293 | } 294 | console.log("Received successful response from run_conversation"); 295 | let responseJson = await response.json(); 296 | console.log("Response body:", responseJson); 297 | 298 | let responseContent; 299 | if (isStream) { 300 | // 流式情况下,candidates 是数组 301 | if ( 302 | !responseJson?.[0]?.["candidates"] || 303 | responseJson[0]["candidates"].length === 0 304 | ) { 305 | console.log("ERROR: No candidates in response"); 306 | return new Response( 307 | JSON.stringify({ error: "No candidates in response" }), 308 | { 309 | headers: { ...corsHeaders, "content-type": "application/json" }, 310 | status: 500, // 代表出现 Internal Server Error 的错误码 311 | } 312 | ); 313 | } 314 | responseContent = responseJson[0]["candidates"][0]["content"]; 315 | message = responseContent["parts"]; 316 | if (!message[0] || !message[0]["functionCall"]) { 317 | console.log("No functionCall in message, returning initial content"); 318 | const encoder = new TextEncoder(); 319 | const stream = new ReadableStream({ 320 | async start(controller) { 321 | for (let i = 0; i < responseJson.length; i++) { 322 | if (isSSE) { 323 | // SSE 格式 324 | controller.enqueue( 325 | encoder.encode(`data: ${JSON.stringify(responseJson[i])}\n\n`) 326 | ); 327 | } else { 328 | // JSON 格式 329 | controller.enqueue(encoder.encode(i === 0 ? "[" : ",")); 330 | controller.enqueue( 331 | encoder.encode(JSON.stringify(responseJson[i])) 332 | ); 333 | } 334 | await new Promise((resolve) => setTimeout(resolve, 500)); 335 | } 336 | if (!isSSE) { 337 | controller.enqueue(encoder.encode("]")); 338 | } 339 | controller.close(); 340 | }, 341 | }); 342 | return new Response(stream, { 343 | headers: { 344 | ...corsHeaders, 345 | "content-type": isSSE ? "text/event-stream" : "application/json", 346 | }, 347 | status: response.status, 348 | }); 349 | } 350 | } else { 351 | // 非流式情况下,candidates 是对象 352 | if (!responseJson["candidates"]) { 353 | console.log("ERROR: No candidates in response"); 354 | return new Response( 355 | JSON.stringify({ error: "No candidates in response" }), 356 | { 357 | headers: { ...corsHeaders, "content-type": "application/json" }, 358 | status: 500, // 代表出现 Internal Server Error 的错误码 359 | } 360 | ); 361 | } 362 | responseContent = responseJson["candidates"][0]["content"]; 363 | message = responseContent["parts"]; 364 | if (!message[0] || !message[0]["functionCall"]) { 365 | console.log("No functionCall in message, returning initial content"); 366 | return new Response(JSON.stringify(responseJson), { 367 | headers: { ...corsHeaders, "content-type": "application/json" }, 368 | status: response.status, 369 | }); 370 | } 371 | } 372 | 373 | if (message[0]["functionCall"]) { 374 | const { function_name, function_response } = await parse_function_response( 375 | message 376 | ); 377 | 378 | const functionResponseData = { 379 | contents: [ 380 | ...originalMessage, 381 | { 382 | role: "model", 383 | parts: [...message], 384 | }, 385 | { 386 | role: "function", 387 | parts: [ 388 | { 389 | functionResponse: { 390 | name: function_name, 391 | response: { 392 | name: function_name, 393 | content: function_response, 394 | }, 395 | }, 396 | }, 397 | ], 398 | }, 399 | ], 400 | tools: [ 401 | { 402 | functionDeclarations: definitions, 403 | }, 404 | ], 405 | }; 406 | console.log("functionResponseData:", functionResponseData); 407 | const functionResponse = await fetchWithRetry( 408 | `${api_url}${api_url.includes("?") ? "&" : "?"}${isSSE ? "alt=sse" : ""}`, 409 | { 410 | method: "POST", 411 | body: JSON.stringify(functionResponseData), 412 | } 413 | ); 414 | 415 | if (!functionResponse.ok) { 416 | console.log("Received error response from run_conversation"); 417 | return new Response( 418 | JSON.stringify({ error: "Error fetching from Gemini API" }), 419 | { 420 | headers: { ...corsHeaders, "content-type": "application/json" }, 421 | status: functionResponse.status, 422 | } 423 | ); 424 | } 425 | 426 | // 直接转发Gemini的流式响应 427 | return new Response(functionResponse.body, { 428 | status: functionResponse.status, 429 | headers: { 430 | ...corsHeaders, 431 | "Content-Type": isSSE ? "text/event-stream" : "application/json", 432 | }, 433 | }); 434 | } 435 | } 436 | 437 | // HTTP请求处理主函数 438 | async function handleRequest(request) { 439 | // 预检请求的处理 440 | if (request.method === "OPTIONS") { 441 | return handleCorsPreflight(); 442 | } 443 | 444 | // 解析请求路径 445 | const url = new URL(request.url); 446 | const path = url.pathname; 447 | // 如果请求路径包含 '/models/gemini-pro-vision',则直接转发请求 448 | if (path.includes("/models/gemini-pro-vision")) { 449 | // 创建一个新的请求对象,复制原始请求的所有信息 450 | const index = path.indexOf("/models"); 451 | const newRequest = new Request( 452 | "https://gemini.sum4all.site/v1beta" + path.substring(index) + url.search, 453 | { 454 | method: request.method, 455 | headers: request.headers, 456 | body: request.body, 457 | redirect: request.redirect, 458 | } 459 | ); 460 | 461 | // 使用 fetch API 发送新的请求 462 | const response = await fetch(newRequest); 463 | // 检查是否是一个 SSE 响应 464 | if (response.headers.get("Content-Type") === "text/event-stream") { 465 | // 如果是 SSE 响应,返回一个新的响应对象,使用原始响应的 body,但设置 headers 为 'text/event-stream' 466 | return new Response(response.body, { 467 | status: response.status, 468 | statusText: response.statusText, 469 | headers: { ...corsHeaders, "Content-Type": "text/event-stream" }, 470 | }); 471 | } 472 | // 直接返回响应 473 | return response; 474 | } 475 | if (!path.includes("/models/gemini-pro")) { 476 | return jsonResponse({ error: "Not found" }, 404); 477 | } 478 | // 检查路径是否符合预期 479 | let api_key = request.headers.get("x-goog-api-key"); 480 | 481 | // Check if 'Authorization' header exists and starts with 'Bearer ' 482 | let authHeader = request.headers.get("Authorization"); 483 | if (authHeader && authHeader.startsWith("Bearer ")) { 484 | // Extract the api key from the 'Authorization' header 485 | api_key = authHeader.slice(7); 486 | } 487 | 488 | try { 489 | // 解析请求体 490 | const requestBody = await request.json(); // 使用 request.json() 解析 JSON 请求体 491 | const isStream = path.includes("streamGenerateContent"); 492 | const isSSE = url.searchParams.get("alt") === "sse"; 493 | 494 | // 调用 run_conversation 并直接返回其响应 495 | return await run_conversation( 496 | api_key, 497 | requestBody.contents, 498 | isStream, 499 | isSSE 500 | ); 501 | } catch (error) { 502 | // 解析请求体失败或 run_conversation 抛出错误 503 | console.error("[handleRequest] Error:", error.message); 504 | return errorResponse(error.message, 500); 505 | } 506 | } 507 | function handleCorsPreflight() { 508 | // 处理 CORS 预检请求 509 | return new Response(null, { 510 | status: 204, 511 | headers: corsHeaders, 512 | }); 513 | } 514 | 515 | function jsonResponse(body, status = 200) { 516 | // 辅助函数创建 JSON 响应 517 | return new Response(JSON.stringify(body), { 518 | headers: { ...corsHeaders, "Content-Type": "application/json" }, 519 | status, 520 | }); 521 | } 522 | function errorResponse(message, statusCode = 400) { 523 | return new Response(JSON.stringify({ error: message }), { 524 | status: statusCode, 525 | headers: { "Content-Type": "application/json" }, 526 | }); 527 | } 528 | -------------------------------------------------------------------------------- /search2openai.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | // openai.js 3 | var corsHeaders = { 4 | "Access-Control-Allow-Origin": "*", 5 | "Access-Control-Allow-Methods": "GET, POST, OPTIONS", 6 | // 允许的HTTP方法 7 | "Access-Control-Allow-Headers": "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization", 8 | "Access-Control-Max-Age": "86400" 9 | // 预检请求结果的缓存时间 10 | }; 11 | 12 | var header_auth="Authorization"; //azure use "api-key" 13 | var header_auth_val="Bearer "; 14 | 15 | // get variables from env 16 | const api_type = typeof OPENAI_TYPE !== "undefined" ? OPENAI_TYPE : "openai"; 17 | const apiBase = typeof APIBASE !== "undefined" ? APIBASE : "https://api.openai.com"; 18 | const resource_name = typeof RESOURCE_NAME !== "undefined" ? RESOURCE_NAME : "xxxxx"; 19 | const deployName = typeof DEPLOY_NAME !== "undefined" ? DEPLOY_NAME : "gpt-35-turbo"; 20 | const api_ver = typeof API_VERSION !== "undefined" ? API_VERSION : "2024-03-01-preview"; 21 | let openai_key = typeof OPENAI_API_KEY !== "undefined" ? OPENAI_API_KEY : ""; 22 | const azure_key = typeof AZURE_API_KEY !== "undefined" ? AZURE_API_KEY : ""; 23 | const auth_keys = typeof AUTH_KEYS !== "undefined" ? AUTH_KEYS : [""]; 24 | 25 | let fetchAPI = ""; 26 | let request_header = new Headers({ 27 | "Content-Type": "application/json", 28 | "Authorization": "", 29 | "api-key": "" 30 | }); 31 | 32 | addEventListener("fetch", (event) => { 33 | console.log(`\u6536\u5230\u8BF7\u6C42: ${event.request.method} ${event.request.url}`); 34 | const url = new URL(event.request.url); 35 | if (event.request.method === "OPTIONS") { 36 | return event.respondWith(handleOptions()); 37 | } 38 | 39 | const authHeader = event.request.headers.get("Authorization"); 40 | let apiKey = ""; 41 | if (authHeader) { 42 | apiKey = authHeader.split(" ")[1]; 43 | if (!auth_keys.includes(apiKey) || !openai_key) { 44 | openai_key = apiKey; 45 | } 46 | } else { 47 | return event.respondWith(new Response("Authorization header is missing", { status: 400, headers: corsHeaders })); 48 | } 49 | 50 | if ( api_type === "azure" ){ 51 | fetchAPI = `https://${resource_name}.openai.azure.com/openai/deployments/${deployName}/chat/completions?api-version=${api_ver}`; 52 | header_auth = "api-key"; 53 | header_auth_val = ""; 54 | apiKey = azure_key; 55 | }else{ //openai 56 | fetchAPI = `${apiBase}/v1/chat/completions`; 57 | header_auth = "Authorization"; 58 | header_auth_val = "Bearer "; 59 | apiKey = openai_key; 60 | } 61 | 62 | if (url.pathname === '/v1/chat/completions') { //openai-style request 63 | console.log('接收到 fetch 事件'); 64 | event.respondWith(handleRequest(event.request, fetchAPI, apiKey)); 65 | } else { //other request 66 | event.respondWith(handleOtherRequest(apiBase, apiKey, event.request, url.pathname).then((response) => { 67 | return new Response(response.body, { 68 | status: response.status, 69 | headers: { ...response.headers, ...corsHeaders } 70 | }); 71 | })); 72 | } 73 | }); 74 | function handleOptions() { 75 | return new Response(null, { 76 | status: 204, 77 | headers: corsHeaders 78 | }); 79 | } 80 | async function handleOtherRequest(apiBase, apiKey, request, pathname) { 81 | const headers = new Headers(request.headers); 82 | headers.delete("Host"); 83 | if ( api_type === "azure"){ 84 | headers.set("api-key", `${apiKey}`); 85 | }else{ 86 | headers.set("Authorization", `Bearer ${apiKey}`); 87 | } 88 | 89 | const response = await fetch(`${apiBase}${pathname}`, { 90 | method: request.method, 91 | headers, 92 | body: request.body 93 | }); 94 | let data; 95 | if (pathname.startsWith("/v1/audio/")) { 96 | data = await response.arrayBuffer(); 97 | return new Response(data, { 98 | status: response.status, 99 | headers: { "Content-Type": "audio/mpeg", ...corsHeaders } 100 | }); 101 | } else { 102 | data = await response.json(); 103 | return new Response(JSON.stringify(data), { 104 | status: response.status, 105 | headers: corsHeaders 106 | }); 107 | } 108 | } 109 | async function search(query) { 110 | console.log(`正在使用 ${SEARCH_SERVICE} 进行自定义搜索: ${JSON.stringify(query)}`); 111 | try { 112 | let results; 113 | 114 | switch (SEARCH_SERVICE) { 115 | case "search1api": 116 | const search1apiResponse = await fetch("https://api.search1api.com/search/", { 117 | method: "POST", 118 | headers: { 119 | "Content-Type": "application/json", 120 | Authorization: typeof SEARCH1API_KEY !== "undefined" ? `Bearer ${SEARCH1API_KEY}` : "", 121 | }, 122 | body: JSON.stringify({ 123 | query, 124 | search_service: "google", 125 | max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "5", 126 | crawl_results: typeof CRAWL_RESULTS !== "undefined" ? CRAWL_RESULTS : "0", 127 | }), 128 | }); 129 | 130 | results = await search1apiResponse.json(); 131 | break; 132 | case "google": 133 | const googleApiUrl = `https://www.googleapis.com/customsearch/v1?cx=${GOOGLE_CX}&key=${GOOGLE_KEY}&q=${encodeURIComponent(query)}`; 134 | const googleResponse = await fetch(googleApiUrl); 135 | const googleData = await googleResponse.json(); 136 | results = googleData.items.slice(0, MAX_RESULTS).map((item) => ({ 137 | title: item.title, 138 | link: item.link, 139 | snippet: item.snippet 140 | })); 141 | break; 142 | 143 | case "bing": 144 | const bingApiUrl = `https://api.bing.microsoft.com/v7.0/search?q=${encodeURIComponent(query)}`; 145 | const bingResponse = await fetch(bingApiUrl, { 146 | headers: { "Ocp-Apim-Subscription-Key": BING_KEY } 147 | }); 148 | const bingData = await bingResponse.json(); 149 | results = bingData.webPages.value.slice(0, MAX_RESULTS).map((item) => ({ 150 | title: item.name, 151 | link: item.url, 152 | snippet: item.snippet 153 | })); 154 | break; 155 | 156 | case "serpapi": 157 | const serpApiUrl = `https://serpapi.com/search?api_key=${SERPAPI_KEY}&engine=google&q=${encodeURIComponent(query)}&google_domain=google.com`; 158 | const serpApiResponse = await fetch(serpApiUrl); 159 | const serpApiData = await serpApiResponse.json(); 160 | results = serpApiData.organic_results.slice(0, MAX_RESULTS).map((item) => ({ 161 | title: item.title, 162 | link: item.link, 163 | snippet: item.snippet 164 | })); 165 | break; 166 | 167 | case "serper": 168 | const gl = typeof GL !== "undefined" ? GL : "us"; 169 | const hl = typeof HL !== "undefined" ? HL : "en"; 170 | const serperApiUrl = "https://google.serper.dev/search"; 171 | const serperResponse = await fetch(serperApiUrl, { 172 | method: "POST", 173 | headers: { 174 | "X-API-KEY": SERPER_KEY, 175 | "Content-Type": "application/json" 176 | }, 177 | body: JSON.stringify({ q: query, gl: gl, hl: hl }) 178 | }); 179 | const serperData = await serperResponse.json(); 180 | results = serperData.organic.slice(0, MAX_RESULTS).map((item) => ({ 181 | title: item.title, 182 | link: item.link, 183 | snippet: item.snippet 184 | })); 185 | break; 186 | 187 | case "duckduckgo": 188 | const duckDuckGoApiUrl = "https://ddg.search2ai.online/search"; 189 | const body = { 190 | q: query, 191 | max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "5" 192 | }; 193 | const duckDuckGoResponse = await fetch(duckDuckGoApiUrl, { 194 | method: "POST", 195 | headers: { 196 | "Content-Type": "application/json" 197 | }, 198 | body: JSON.stringify(body) 199 | }); 200 | const duckDuckGoData = await duckDuckGoResponse.json(); 201 | results = duckDuckGoData.results.map((item) => ({ 202 | title: item.title, 203 | link: item.href, 204 | snippet: item.body 205 | })); 206 | break; 207 | 208 | case "searxng": 209 | const searXNGUrl = `${SEARXNG_BASE_URL}/search?q=${encodeURIComponent( 210 | query 211 | )}&category=general&format=json`; 212 | const searXNGResponse = await fetch(searXNGUrl); 213 | const searXNGData = await searXNGResponse.json(); 214 | results = searXNGData.results.slice(0, MAX_RESULTS).map((item) => ({ 215 | title: item.title, 216 | link: item.url, 217 | snippet: item.content 218 | })); 219 | break; 220 | 221 | default: 222 | console.error(`不支持的搜索服务: ${SEARCH_SERVICE}`); 223 | return `不支持的搜索服务: ${SEARCH_SERVICE}`; 224 | } 225 | 226 | const data = { 227 | results: results 228 | }; 229 | 230 | return JSON.stringify(data); 231 | 232 | } catch (error) { 233 | console.error(`在 search 函数中捕获到错误: ${error}`); 234 | return `在 search 函数中捕获到错误: ${error}`; 235 | } 236 | } 237 | async function news(query) { 238 | console.log(`正在使用 ${SEARCH_SERVICE} 进行新闻搜索: ${JSON.stringify(query)}`); 239 | 240 | try { 241 | let results; 242 | 243 | switch (SEARCH_SERVICE) { 244 | case "search1api": 245 | const search1apiResponse = await fetch("https://api.search1api.com/news", { 246 | method: "POST", 247 | headers: { 248 | "Content-Type": "application/json", 249 | Authorization: typeof SEARCH1API_KEY !== "undefined" ? `Bearer ${SEARCH1API_KEY}` : "", 250 | }, 251 | body: JSON.stringify({ 252 | query, 253 | max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "10", 254 | crawl_results: typeof CRAWL_RESULTS !== "undefined" ? CRAWL_RESULTS : "0", 255 | }), 256 | }); 257 | results = await search1apiResponse.json(); 258 | break; 259 | 260 | case "google": 261 | const googleApiUrl = `https://www.googleapis.com/customsearch/v1?cx=${GOOGLE_CX}&key=${GOOGLE_KEY}&q=${encodeURIComponent(query)}&tbm=nws`; 262 | const googleResponse = await fetch(googleApiUrl); 263 | const googleData = await googleResponse.json(); 264 | results = googleData.items.slice(0, MAX_RESULTS).map((item) => ({ 265 | title: item.title, 266 | link: item.link, 267 | snippet: item.snippet 268 | })); 269 | break; 270 | 271 | case "bing": 272 | const bingApiUrl = `https://api.bing.microsoft.com/v7.0/news/search?q=${encodeURIComponent(query)}`; 273 | const bingResponse = await fetch(bingApiUrl, { 274 | headers: { "Ocp-Apim-Subscription-Key": BING_KEY } 275 | }); 276 | const bingData = await bingResponse.json(); 277 | results = bingData.value.slice(0, MAX_RESULTS).map((item) => ({ 278 | title: item.name, 279 | link: item.url, 280 | snippet: item.description 281 | })); 282 | break; 283 | 284 | case "serpapi": 285 | const serpApiUrl = `https://serpapi.com/search?api_key=${SERPAPI_KEY}&engine=google_news&q=${encodeURIComponent(query)}&google_domain=google.com`; 286 | const serpApiResponse = await fetch(serpApiUrl); 287 | const serpApiData = await serpApiResponse.json(); 288 | results = serpApiData.news_results.slice(0, MAX_RESULTS).map((item) => ({ 289 | title: item.title, 290 | link: item.link, 291 | snippet: item.snippet 292 | })); 293 | break; 294 | 295 | case "serper": 296 | const gl = typeof GL !== "undefined" ? GL : "us"; 297 | const hl = typeof HL !== "undefined" ? HL : "en"; 298 | const serperApiUrl = "https://google.serper.dev/news"; 299 | const serperResponse = await fetch(serperApiUrl, { 300 | method: "POST", 301 | headers: { 302 | "X-API-KEY": SERPER_KEY, 303 | "Content-Type": "application/json" 304 | }, 305 | body: JSON.stringify({ q: query, gl: gl, hl: hl }) 306 | }); 307 | const serperData = await serperResponse.json(); 308 | results = serperData.news.slice(0, MAX_RESULTS).map((item) => ({ 309 | title: item.title, 310 | link: item.link, 311 | snippet: item.snippet 312 | })); 313 | break; 314 | 315 | case "duckduckgo": 316 | const duckDuckGoApiUrl = "https://ddg.search2ai.online/searchNews"; 317 | const body = { 318 | q: query, 319 | max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "10" 320 | }; 321 | const duckDuckGoResponse = await fetch(duckDuckGoApiUrl, { 322 | method: "POST", 323 | headers: { 324 | "Content-Type": "application/json" 325 | }, 326 | body: JSON.stringify(body) 327 | }); 328 | const duckDuckGoData = await duckDuckGoResponse.json(); 329 | results = duckDuckGoData.results.map((item) => ({ 330 | title: item.title, 331 | link: item.url, 332 | snippet: item.body 333 | })); 334 | break; 335 | 336 | case "searxng": 337 | const searXNGUrl = `${SEARXNG_BASE_URL}/search?q=${encodeURIComponent( 338 | query 339 | )}&category=news&format=json`; 340 | const searXNGResponse = await fetch(searXNGUrl); 341 | const searXNGData = await searXNGResponse.json(); 342 | results = searXNGData.results.slice(0, MAX_RESULTS).map((item) => ({ 343 | title: item.title, 344 | link: item.url, 345 | snippet: item.content 346 | })); 347 | break; 348 | 349 | default: 350 | console.error(`不支持的搜索服务: ${SEARCH_SERVICE}`); 351 | return `不支持的搜索服务: ${SEARCH_SERVICE}`; 352 | } 353 | 354 | const data = { 355 | results: results 356 | }; 357 | 358 | console.log('新闻搜索服务调用完成'); 359 | return JSON.stringify(data); 360 | 361 | } catch (error) { 362 | console.error(`在 news 函数中捕获到错误: ${error}`); 363 | return `在 news 函数中捕获到错误: ${error}`; 364 | } 365 | } 366 | async function crawler(url) { 367 | console.log(`\u6B63\u5728\u4F7F\u7528 URL \u8FDB\u884C\u81EA\u5B9A\u4E49\u722C\u53D6:${JSON.stringify(url)}`); 368 | try { 369 | const response = await fetch("https://crawl.search1api.com", { 370 | method: "POST", 371 | headers: { 372 | "Content-Type": "application/json" 373 | }, 374 | body: JSON.stringify({ 375 | url 376 | }) 377 | }); 378 | if (!response.ok) { 379 | console.error(`API \u8BF7\u6C42\u5931\u8D25, \u72B6\u6001\u7801: ${response.status}`); 380 | return `API \u8BF7\u6C42\u5931\u8D25, \u72B6\u6001\u7801: ${response.status}`; 381 | } 382 | const contentType = response.headers.get("content-type"); 383 | if (!contentType || !contentType.includes("application/json")) { 384 | console.error("\u6536\u5230\u7684\u54CD\u5E94\u4E0D\u662F\u6709\u6548\u7684 JSON \u683C\u5F0F"); 385 | return "\u6536\u5230\u7684\u54CD\u5E94\u4E0D\u662F\u6709\u6548\u7684 JSON \u683C\u5F0F"; 386 | } 387 | const data = await response.json(); 388 | console.log("\u81EA\u5B9A\u4E49\u722C\u53D6\u670D\u52A1\u8C03\u7528\u5B8C\u6210"); 389 | return JSON.stringify(data); 390 | } catch (error) { 391 | console.error(`\u5728 crawl \u51FD\u6570\u4E2D\u6355\u83B7\u5230\u9519\u8BEF: ${error}`); 392 | return `\u5728 crawler \u51FD\u6570\u4E2D\u6355\u83B7\u5230\u9519\u8BEF: ${error}`; 393 | } 394 | } 395 | async function handleRequest(request, fetchAPI, apiKey) { 396 | 397 | console.log(`\u5F00\u59CB\u5904\u7406\u8BF7\u6C42: ${request.method} ${request.url}`); 398 | if (request.method !== "POST") { 399 | console.log(`\u4E0D\u652F\u6301\u7684\u8BF7\u6C42\u65B9\u6CD5: ${request.method}`); 400 | return new Response("Method Not Allowed", { status: 405, headers: corsHeaders }); 401 | } 402 | 403 | const requestData = await request.json(); 404 | console.log("\u8BF7\u6C42\u6570\u636E:", requestData); 405 | const stream = requestData.stream || false; 406 | const userMessages = requestData.messages.filter((message) => message.role === "user"); 407 | const latestUserMessage = userMessages[userMessages.length - 1]; 408 | const model = requestData.model; 409 | const isContentArray = Array.isArray(latestUserMessage.content); 410 | const defaultMaxTokens = 3e3; 411 | const maxTokens = requestData.max_tokens || defaultMaxTokens; 412 | const body = JSON.stringify({ 413 | model, 414 | messages: requestData.messages, 415 | stream, 416 | max_tokens: maxTokens, 417 | ...isContentArray ? {} : { 418 | tools: [ 419 | { 420 | type: "function", 421 | function: { 422 | name: "search", 423 | description: "search for factors and weathers", 424 | parameters: { 425 | type: "object", 426 | properties: { 427 | query: { 428 | type: "string", 429 | "description": "The query to search." 430 | } 431 | }, 432 | required: ["query"] 433 | } 434 | } 435 | }, 436 | { 437 | type: "function", 438 | function: { 439 | name: "news", 440 | description: "Search for news", 441 | parameters: { 442 | type: "object", 443 | properties: { 444 | query: { 445 | type: "string", 446 | description: "The query to search for news." 447 | } 448 | }, 449 | required: ["query"] 450 | } 451 | } 452 | }, 453 | { 454 | type: "function", 455 | function: { 456 | name: "crawler", 457 | description: "Get the content of a specified url", 458 | parameters: { 459 | type: "object", 460 | properties: { 461 | url: { 462 | type: "string", 463 | description: "The URL of the webpage" 464 | } 465 | }, 466 | required: ["url"] 467 | } 468 | } 469 | } 470 | ], 471 | tool_choice: "auto" 472 | } 473 | }); 474 | 475 | request_header.set(`${header_auth}`,`${header_auth_val}${apiKey}`); 476 | 477 | if (stream) { 478 | const openAIResponse = await fetch(fetchAPI, { 479 | method: "POST", 480 | headers: request_header, 481 | body 482 | }); 483 | 484 | let messages = requestData.messages; 485 | let toolCalls = []; 486 | let currentToolCall = null; 487 | let currentArguments = ""; 488 | let responseContent = ""; 489 | 490 | // 这里不再直接检查 shouldDirectlyReturn,而是读取流来做判断 491 | const reader = openAIResponse.body.getReader(); 492 | const decoder = new TextDecoder(); 493 | let buffer = ""; 494 | let shouldDirectlyReturn = true; // 默认直接返回 495 | 496 | async function processFirstBlock() { 497 | const promptFilterResultsRegex = /"prompt_filter_results"/; 498 | const functionRegex = /"function"/; 499 | 500 | let { value, done } = await reader.read(); 501 | if (done) { 502 | return; 503 | } 504 | let block = decoder.decode(value, { stream: true }); 505 | buffer += block; 506 | 507 | if (promptFilterResultsRegex.test(buffer)) { 508 | const blocks = [buffer]; 509 | 510 | // 读取第二个块 511 | while (!done) { 512 | ({ value, done } = await reader.read({ size: 65536 })); // 使用更大的缓冲区大小 513 | block = decoder.decode(value, { stream: true }); 514 | blocks.push(block); 515 | 516 | // 检查是否读取到第二个块的结束位置 517 | if (block.includes('}')) { 518 | break; 519 | } 520 | } 521 | 522 | const secondBlock = blocks.join('').slice(0, blocks.join('').lastIndexOf('}') + 1); 523 | 524 | // 检查第二个块是否包含 "function" 525 | if (functionRegex.test(secondBlock)) { 526 | shouldDirectlyReturn = false; 527 | console.log("发现 function,将进行特定处理"); 528 | } 529 | 530 | // 更新缓冲区,包括第一个块和第二个块 531 | buffer = blocks.join(''); 532 | } else if (functionRegex.test(buffer)) { 533 | shouldDirectlyReturn = false; 534 | console.log("发现 function,将进行特定处理"); 535 | } else { 536 | return; 537 | } 538 | } 539 | 540 | await processFirstBlock(); 541 | 542 | if (shouldDirectlyReturn) { 543 | console.log("直接返回流,因为没有发现 function"); 544 | const remainingStream = new ReadableStream({ 545 | start(controller) { 546 | controller.enqueue(new TextEncoder().encode(buffer)); 547 | function push() { 548 | reader.read().then(({ done, value }) => { 549 | if (done) { 550 | console.log("流结束"); 551 | controller.close(); 552 | return; 553 | } 554 | controller.enqueue(value); 555 | push(); 556 | }); 557 | } 558 | push(); 559 | }, 560 | }); 561 | return new Response(remainingStream, { 562 | headers: { 563 | "Content-Type": "text/event-stream", 564 | ...corsHeaders, 565 | }, 566 | }); 567 | } else { 568 | console.log("开始处理流数据,因为发现了 function"); 569 | let functionArguments = ""; 570 | const customStream = new ReadableStream({ 571 | start(controller) { 572 | controller.enqueue(new TextEncoder().encode(buffer)); 573 | function push() { 574 | reader.read().then(({ done, value }) => { 575 | if (done) { 576 | controller.close(); 577 | return; 578 | } 579 | const chunk = decoder.decode(value, { stream: true }); 580 | functionArguments += chunk; 581 | 582 | // 检查是否获得完整的 JSON 字符串 583 | if (functionArguments.startsWith('{"') && functionArguments.endsWith('"}')) { 584 | console.log("Custom function called, processing tool calls"); 585 | } else { 586 | } 587 | 588 | controller.enqueue(value); 589 | push(); 590 | }); 591 | } 592 | push(); 593 | }, 594 | }); 595 | 596 | const customReader = customStream.getReader(); 597 | 598 | while (true) { 599 | const { done, value } = await customReader.read(); 600 | if (done) { 601 | break; 602 | } 603 | 604 | const chunk = decoder.decode(value); 605 | buffer += chunk; 606 | responseContent += chunk; 607 | 608 | let position; 609 | while ((position = buffer.indexOf('\n')) !== -1) { 610 | const line = buffer.slice(0, position); 611 | buffer = buffer.slice(position + 1); 612 | 613 | if (line.startsWith('data:')) { 614 | const data = line.slice(5).trim(); 615 | if (data === '[DONE]') { 616 | break; 617 | } 618 | 619 | try { 620 | const parsed = JSON.parse(data); 621 | if (parsed.choices && parsed.choices[0].delta.content) { 622 | // 将新接收到的内容追加到助手回复的最后一条消息中 623 | messages[messages.length - 1].content += parsed.choices[0].delta.content; 624 | } 625 | 626 | if (parsed.choices && parsed.choices[0].delta.tool_calls) { 627 | for (const toolCall of parsed.choices[0].delta.tool_calls) { 628 | if (toolCall.function.arguments) { 629 | // 累积工具调用的参数 630 | currentArguments += toolCall.function.arguments; 631 | } else { 632 | // 参数累积完成,将当前工具调用添加到工具调用数组中 633 | if (currentToolCall) { 634 | currentToolCall.function.arguments = currentArguments; 635 | toolCalls.push(currentToolCall); 636 | } 637 | currentToolCall = toolCall; 638 | currentArguments = ''; 639 | } 640 | } 641 | } 642 | } catch (err) { 643 | } 644 | } 645 | } 646 | } 647 | } 648 | 649 | // 有自定义函数调用,处理工具调用 650 | console.log('Custom function called, processing tool calls'); 651 | currentToolCall.function.arguments = currentArguments; 652 | toolCalls.push(currentToolCall); 653 | 654 | // 执行自定义函数调用的逻辑 655 | const availableFunctions = { 656 | search: search, 657 | news: news, 658 | crawler: crawler, 659 | }; 660 | 661 | let assistantMessage = { 662 | role: 'assistant', 663 | content: null, 664 | tool_calls: [], 665 | }; 666 | 667 | messages.push(assistantMessage); 668 | 669 | for (const toolCall of toolCalls) { 670 | const functionName = toolCall.function.name; 671 | const functionToCall = availableFunctions[functionName]; 672 | let functionArgs; 673 | try { 674 | // 检查 function.arguments 是否为空 675 | if (!toolCall.function.arguments) { 676 | console.error("Function arguments are empty."); 677 | continue; // 跳过当前工具调用,继续处理下一个 678 | } 679 | functionArgs = JSON.parse(toolCall.function.arguments); 680 | } catch (err) { 681 | continue; 682 | } 683 | let functionResponse; 684 | if (functionName === "search" && typeof functionArgs.query === "string") { 685 | functionResponse = await functionToCall(functionArgs.query); 686 | } else if (functionName === "crawler" && typeof functionArgs.url === "string") { 687 | functionResponse = await functionToCall(functionArgs.url); 688 | } else if (functionName === "news" && typeof functionArgs.query === "string") { 689 | functionResponse = await functionToCall(functionArgs.query); 690 | } else { 691 | console.error("Invalid function arguments for", functionName); 692 | continue; 693 | } 694 | messages.push({ 695 | tool_call_id: toolCall.id, 696 | role: "tool", 697 | name: functionName, 698 | content: functionResponse 699 | }); 700 | assistantMessage.tool_calls.push({ 701 | id: toolCall.id, 702 | type: "function", 703 | function: { 704 | name: functionName, 705 | arguments: toolCall.function.arguments 706 | } 707 | }); 708 | } 709 | const secondResponse = await fetch(fetchAPI, { 710 | method: "POST", 711 | headers: request_header, 712 | body: JSON.stringify({ 713 | model, 714 | messages, 715 | stream: true 716 | }) 717 | }); 718 | return new Response(secondResponse.body, { 719 | status: secondResponse.status, 720 | headers: { 721 | "Content-Type": "text/event-stream", 722 | ...corsHeaders 723 | } 724 | }); 725 | } else { 726 | const openAIResponse = await fetch(fetchAPI, { 727 | method: "POST", 728 | headers: request_header, 729 | body 730 | }); 731 | if (openAIResponse.status !== 200) { 732 | console.error(`OpenAI API \u8BF7\u6C42\u5931\u8D25,\u72B6\u6001\u7801: ${openAIResponse.status}`); 733 | return new Response(`OpenAI API \u8BF7\u6C42\u5931\u8D25,\u72B6\u6001\u7801: ${openAIResponse.status}`, { 734 | status: 500, 735 | headers: corsHeaders 736 | }); 737 | } 738 | const data = await openAIResponse.json(); 739 | console.log("OpenAI API \u54CD\u5E94\u72B6\u6001\u7801:", openAIResponse.status); 740 | if (!data.choices || data.choices.length === 0) { 741 | console.log("\u6570\u636E\u4E2D\u6CA1\u6709\u9009\u62E9\u9879"); 742 | return new Response("\u6570\u636E\u4E2D\u6CA1\u6709\u9009\u62E9\u9879", { status: 500 }); 743 | } 744 | console.log("OpenAI API \u54CD\u5E94\u63A5\u6536\u5B8C\u6210\uFF0C\u68C0\u67E5\u662F\u5426\u9700\u8981\u8C03\u7528\u81EA\u5B9A\u4E49\u51FD\u6570"); 745 | let messages = requestData.messages; 746 | messages.push(data.choices[0].message); 747 | let calledCustomFunction = false; 748 | if (data.choices[0].message.tool_calls) { 749 | const toolCalls = data.choices[0].message.tool_calls; 750 | const availableFunctions = { 751 | "search": search, 752 | "news": news, 753 | "crawler": crawler 754 | }; 755 | for (const toolCall of toolCalls) { 756 | const functionName = toolCall.function.name; 757 | const functionToCall = availableFunctions[functionName]; 758 | const functionArgs = JSON.parse(toolCall.function.arguments); 759 | let functionResponse; 760 | if (functionName === "search") { 761 | functionResponse = await functionToCall(functionArgs.query); 762 | } else if (functionName === "crawler") { 763 | functionResponse = await functionToCall(functionArgs.url); 764 | } else if (functionName === "news") { 765 | functionResponse = await functionToCall(functionArgs.query); 766 | } 767 | messages.push({ 768 | tool_call_id: toolCall.id, 769 | role: "tool", 770 | name: functionName, 771 | content: functionResponse 772 | }); 773 | if (functionName === "search" || functionName === "crawler" || functionName === "news") { 774 | calledCustomFunction = true; 775 | } 776 | } 777 | } 778 | if (calledCustomFunction) { 779 | console.log("\u51C6\u5907\u53D1\u9001\u7B2C\u4E8C\u6B21 OpenAI API \u8BF7\u6C42"); 780 | const requestBody = { 781 | model, 782 | messages 783 | }; 784 | const secondResponse = await fetch(fetchAPI, { 785 | method: "POST", 786 | headers: request_header, 787 | body: JSON.stringify(requestBody) 788 | }); 789 | console.log("\u54CD\u5E94\u72B6\u6001\u7801: 200"); 790 | const data2 = await secondResponse.json(); 791 | return new Response(JSON.stringify(data2), { 792 | status: 200, 793 | headers: { 794 | "Content-Type": "application/json", 795 | ...corsHeaders 796 | } 797 | }); 798 | } else { 799 | console.log("\u54CD\u5E94\u72B6\u6001\u7801: 200"); 800 | return new Response(JSON.stringify(data), { 801 | status: 200, 802 | headers: { 803 | "Content-Type": "application/json", 804 | ...corsHeaders 805 | } 806 | }); 807 | } 808 | } 809 | } 810 | })(); 811 | //# sourceMappingURL=openai.js.map 812 | -------------------------------------------------------------------------------- /search2groq.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | // openai.js 3 | var corsHeaders = { 4 | "Access-Control-Allow-Origin": "*", 5 | "Access-Control-Allow-Methods": "GET, POST, OPTIONS", 6 | // 允许的HTTP方法 7 | "Access-Control-Allow-Headers": 8 | "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization", 9 | "Access-Control-Max-Age": "86400", 10 | // 预检请求结果的缓存时间 11 | }; 12 | 13 | var header_auth = "Authorization"; //azure use "api-key" 14 | var header_auth_val = "Bearer "; 15 | 16 | // get variables from env 17 | const api_type = typeof OPENAI_TYPE !== "undefined" ? OPENAI_TYPE : "openai"; 18 | const apiBase = 19 | typeof APIBASE !== "undefined" ? APIBASE : "https://api.openai.com"; 20 | const resource_name = 21 | typeof RESOURCE_NAME !== "undefined" ? RESOURCE_NAME : "xxxxx"; 22 | const deployName = 23 | typeof DEPLOY_NAME !== "undefined" ? DEPLOY_NAME : "gpt-35-turbo"; 24 | const api_ver = 25 | typeof API_VERSION !== "undefined" ? API_VERSION : "2024-03-01-preview"; 26 | let openai_key = typeof OPENAI_API_KEY !== "undefined" ? OPENAI_API_KEY : ""; 27 | const azure_key = typeof AZURE_API_KEY !== "undefined" ? AZURE_API_KEY : ""; 28 | const auth_keys = typeof AUTH_KEYS !== "undefined" ? AUTH_KEYS : [""]; 29 | 30 | let fetchAPI = ""; 31 | let request_header = new Headers({ 32 | "Content-Type": "application/json", 33 | Authorization: "", 34 | "api-key": "", 35 | }); 36 | 37 | addEventListener("fetch", (event) => { 38 | console.log( 39 | `\u6536\u5230\u8BF7\u6C42: ${event.request.method} ${event.request.url}` 40 | ); 41 | const url = new URL(event.request.url); 42 | if (event.request.method === "OPTIONS") { 43 | return event.respondWith(handleOptions()); 44 | } 45 | 46 | const authHeader = event.request.headers.get("Authorization"); 47 | let apiKey = ""; 48 | if (authHeader) { 49 | apiKey = authHeader.split(" ")[1]; 50 | if (!auth_keys.includes(apiKey) || !openai_key) { 51 | openai_key = apiKey; 52 | } 53 | } else { 54 | return event.respondWith( 55 | new Response("Authorization header is missing", { 56 | status: 400, 57 | headers: corsHeaders, 58 | }) 59 | ); 60 | } 61 | 62 | if (api_type === "azure") { 63 | fetchAPI = `https://${resource_name}.openai.azure.com/openai/deployments/${deployName}/chat/completions?api-version=${api_ver}`; 64 | header_auth = "api-key"; 65 | header_auth_val = ""; 66 | apiKey = azure_key; 67 | } else { 68 | //openai 69 | fetchAPI = `${apiBase}/v1/chat/completions`; 70 | header_auth = "Authorization"; 71 | header_auth_val = "Bearer "; 72 | apiKey = openai_key; 73 | } 74 | 75 | if (url.pathname === "/v1/chat/completions") { 76 | //openai-style request 77 | console.log("接收到 fetch 事件"); 78 | event.respondWith(handleRequest(event.request, fetchAPI, apiKey)); 79 | } else { 80 | //other request 81 | event.respondWith( 82 | handleOtherRequest(apiBase, apiKey, event.request, url.pathname).then( 83 | (response) => { 84 | return new Response(response.body, { 85 | status: response.status, 86 | headers: { ...response.headers, ...corsHeaders }, 87 | }); 88 | } 89 | ) 90 | ); 91 | } 92 | }); 93 | function handleOptions() { 94 | return new Response(null, { 95 | status: 204, 96 | headers: corsHeaders, 97 | }); 98 | } 99 | async function handleOtherRequest(apiBase, apiKey, request, pathname) { 100 | const headers = new Headers(request.headers); 101 | headers.delete("Host"); 102 | if (api_type === "azure") { 103 | headers.set("api-key", `${apiKey}`); 104 | } else { 105 | headers.set("Authorization", `Bearer ${apiKey}`); 106 | } 107 | 108 | const response = await fetch(`${apiBase}${pathname}`, { 109 | method: request.method, 110 | headers, 111 | body: request.body, 112 | }); 113 | let data; 114 | if (pathname.startsWith("/v1/audio/")) { 115 | data = await response.arrayBuffer(); 116 | return new Response(data, { 117 | status: response.status, 118 | headers: { "Content-Type": "audio/mpeg", ...corsHeaders }, 119 | }); 120 | } else { 121 | data = await response.json(); 122 | return new Response(JSON.stringify(data), { 123 | status: response.status, 124 | headers: corsHeaders, 125 | }); 126 | } 127 | } 128 | async function search(query) { 129 | console.log(`正在使用 ${SEARCH_SERVICE} 进行自定义搜索: ${JSON.stringify(query)}`); 130 | try { 131 | let results; 132 | 133 | switch (SEARCH_SERVICE) { 134 | case "search1api": 135 | const search1apiResponse = await fetch("https://api.search1api.com/search/", { 136 | method: "POST", 137 | headers: { 138 | "Content-Type": "application/json", 139 | Authorization: typeof SEARCH1API_KEY !== "undefined" ? `Bearer ${SEARCH1API_KEY}` : "", 140 | }, 141 | body: JSON.stringify({ 142 | query, 143 | max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "5", 144 | crawl_results: typeof CRAWL_RESULTS !== "undefined" ? CRAWL_RESULTS : "0", 145 | }), 146 | }); 147 | results = await search1apiResponse.json(); 148 | break; 149 | 150 | case "google": 151 | const googleApiUrl = `https://www.googleapis.com/customsearch/v1?cx=${GOOGLE_CX}&key=${GOOGLE_KEY}&q=${encodeURIComponent(query)}`; 152 | const googleResponse = await fetch(googleApiUrl); 153 | const googleData = await googleResponse.json(); 154 | results = googleData.items.slice(0, MAX_RESULTS).map((item) => ({ 155 | title: item.title, 156 | link: item.link, 157 | snippet: item.snippet 158 | })); 159 | break; 160 | 161 | case "bing": 162 | const bingApiUrl = `https://api.bing.microsoft.com/v7.0/search?q=${encodeURIComponent(query)}`; 163 | const bingResponse = await fetch(bingApiUrl, { 164 | headers: { "Ocp-Apim-Subscription-Key": BING_KEY } 165 | }); 166 | const bingData = await bingResponse.json(); 167 | results = bingData.webPages.value.slice(0, MAX_RESULTS).map((item) => ({ 168 | title: item.name, 169 | link: item.url, 170 | snippet: item.snippet 171 | })); 172 | break; 173 | 174 | case "serpapi": 175 | const serpApiUrl = `https://serpapi.com/search?api_key=${SERPAPI_KEY}&engine=google&q=${encodeURIComponent(query)}&google_domain=google.com`; 176 | const serpApiResponse = await fetch(serpApiUrl); 177 | const serpApiData = await serpApiResponse.json(); 178 | results = serpApiData.organic_results.slice(0, MAX_RESULTS).map((item) => ({ 179 | title: item.title, 180 | link: item.link, 181 | snippet: item.snippet 182 | })); 183 | break; 184 | 185 | case "serper": 186 | const gl = typeof GL !== "undefined" ? GL : "us"; 187 | const hl = typeof HL !== "undefined" ? HL : "en"; 188 | const serperApiUrl = "https://google.serper.dev/search"; 189 | const serperResponse = await fetch(serperApiUrl, { 190 | method: "POST", 191 | headers: { 192 | "X-API-KEY": SERPER_KEY, 193 | "Content-Type": "application/json" 194 | }, 195 | body: JSON.stringify({ q: query, gl: gl, hl: hl }) 196 | }); 197 | const serperData = await serperResponse.json(); 198 | results = serperData.organic.slice(0, MAX_RESULTS).map((item) => ({ 199 | title: item.title, 200 | link: item.link, 201 | snippet: item.snippet 202 | })); 203 | break; 204 | 205 | case "duckduckgo": 206 | const duckDuckGoApiUrl = "https://ddg.search2ai.online/search"; 207 | const body = { 208 | q: query, 209 | max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "5" 210 | }; 211 | const duckDuckGoResponse = await fetch(duckDuckGoApiUrl, { 212 | method: "POST", 213 | headers: { 214 | "Content-Type": "application/json" 215 | }, 216 | body: JSON.stringify(body) 217 | }); 218 | const duckDuckGoData = await duckDuckGoResponse.json(); 219 | results = duckDuckGoData.results.map((item) => ({ 220 | title: item.title, 221 | link: item.href, 222 | snippet: item.body 223 | })); 224 | break; 225 | 226 | case "searxng": 227 | const searXNGUrl = `${SEARXNG_BASE_URL}/search?q=${encodeURIComponent( 228 | query 229 | )}&category=general&format=json`; 230 | const searXNGResponse = await fetch(searXNGUrl); 231 | const searXNGData = await searXNGResponse.json(); 232 | results = searXNGData.results.slice(0, MAX_RESULTS).map((item) => ({ 233 | title: item.title, 234 | link: item.url, 235 | snippet: item.content 236 | })); 237 | break; 238 | 239 | default: 240 | console.error(`不支持的搜索服务: ${SEARCH_SERVICE}`); 241 | return `不支持的搜索服务: ${SEARCH_SERVICE}`; 242 | } 243 | 244 | const data = { 245 | results: results 246 | }; 247 | 248 | console.log('自定义搜索服务调用完成'); 249 | return JSON.stringify(data); 250 | 251 | } catch (error) { 252 | console.error(`在 search 函数中捕获到错误: ${error}`); 253 | return `在 search 函数中捕获到错误: ${error}`; 254 | } 255 | } 256 | async function news(query) { 257 | console.log(`正在使用 ${SEARCH_SERVICE} 进行新闻搜索: ${JSON.stringify(query)}`); 258 | 259 | try { 260 | let results; 261 | 262 | switch (SEARCH_SERVICE) { 263 | case "search1api": 264 | const search1apiResponse = await fetch("https://api.search1api.com/news/", { 265 | method: "POST", 266 | headers: { 267 | "Content-Type": "application/json", 268 | Authorization: typeof SEARCH1API_KEY !== "undefined" ? `Bearer ${SEARCH1API_KEY}` : "", 269 | }, 270 | body: JSON.stringify({ 271 | query, 272 | max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "10", 273 | crawl_results: typeof CRAWL_RESULTS !== "undefined" ? CRAWL_RESULTS : "0", 274 | }), 275 | }); 276 | results = await search1apiResponse.json(); 277 | break; 278 | 279 | case "google": 280 | const googleApiUrl = `https://www.googleapis.com/customsearch/v1?cx=${GOOGLE_CX}&key=${GOOGLE_KEY}&q=${encodeURIComponent(query)}&tbm=nws`; 281 | const googleResponse = await fetch(googleApiUrl); 282 | const googleData = await googleResponse.json(); 283 | results = googleData.items.slice(0, MAX_RESULTS).map((item) => ({ 284 | title: item.title, 285 | link: item.link, 286 | snippet: item.snippet 287 | })); 288 | break; 289 | 290 | case "bing": 291 | const bingApiUrl = `https://api.bing.microsoft.com/v7.0/news/search?q=${encodeURIComponent(query)}`; 292 | const bingResponse = await fetch(bingApiUrl, { 293 | headers: { "Ocp-Apim-Subscription-Key": BING_KEY } 294 | }); 295 | const bingData = await bingResponse.json(); 296 | results = bingData.value.slice(0, MAX_RESULTS).map((item) => ({ 297 | title: item.name, 298 | link: item.url, 299 | snippet: item.description 300 | })); 301 | break; 302 | 303 | case "serpapi": 304 | const serpApiUrl = `https://serpapi.com/search?api_key=${SERPAPI_KEY}&engine=google_news&q=${encodeURIComponent(query)}&google_domain=google.com`; 305 | const serpApiResponse = await fetch(serpApiUrl); 306 | const serpApiData = await serpApiResponse.json(); 307 | results = serpApiData.news_results.slice(0, MAX_RESULTS).map((item) => ({ 308 | title: item.title, 309 | link: item.link, 310 | snippet: item.snippet 311 | })); 312 | break; 313 | 314 | case "serper": 315 | const gl = typeof GL !== "undefined" ? GL : "us"; 316 | const hl = typeof HL !== "undefined" ? HL : "en"; 317 | const serperApiUrl = "https://google.serper.dev/news"; 318 | const serperResponse = await fetch(serperApiUrl, { 319 | method: "POST", 320 | headers: { 321 | "X-API-KEY": SERPER_KEY, 322 | "Content-Type": "application/json" 323 | }, 324 | body: JSON.stringify({ q: query, gl: gl, hl: hl }) 325 | }); 326 | const serperData = await serperResponse.json(); 327 | results = serperData.news.slice(0, MAX_RESULTS).map((item) => ({ 328 | title: item.title, 329 | link: item.link, 330 | snippet: item.snippet 331 | })); 332 | break; 333 | 334 | case "duckduckgo": 335 | const duckDuckGoApiUrl = "https://ddg.search2ai.online/searchNews"; 336 | const body = { 337 | q: query, 338 | max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "10" 339 | }; 340 | const duckDuckGoResponse = await fetch(duckDuckGoApiUrl, { 341 | method: "POST", 342 | headers: { 343 | "Content-Type": "application/json" 344 | }, 345 | body: JSON.stringify(body) 346 | }); 347 | const duckDuckGoData = await duckDuckGoResponse.json(); 348 | results = duckDuckGoData.results.map((item) => ({ 349 | title: item.title, 350 | link: item.url, 351 | snippet: item.body 352 | })); 353 | break; 354 | 355 | case "searxng": 356 | const searXNGUrl = `${SEARXNG_BASE_URL}/search?q=${encodeURIComponent( 357 | query 358 | )}&category=news&format=json`; 359 | const searXNGResponse = await fetch(searXNGUrl); 360 | const searXNGData = await searXNGResponse.json(); 361 | results = searXNGData.results.slice(0, MAX_RESULTS).map((item) => ({ 362 | title: item.title, 363 | link: item.url, 364 | snippet: item.content 365 | })); 366 | break; 367 | 368 | default: 369 | console.error(`不支持的搜索服务: ${SEARCH_SERVICE}`); 370 | return `不支持的搜索服务: ${SEARCH_SERVICE}`; 371 | } 372 | 373 | const data = { 374 | results: results 375 | }; 376 | 377 | console.log('新闻搜索服务调用完成'); 378 | return JSON.stringify(data); 379 | 380 | } catch (error) { 381 | console.error(`在 news 函数中捕获到错误: ${error}`); 382 | return `在 news 函数中捕获到错误: ${error}`; 383 | } 384 | } 385 | async function crawler(url) { 386 | console.log( 387 | `\u6B63\u5728\u4F7F\u7528 URL \u8FDB\u884C\u81EA\u5B9A\u4E49\u722C\u53D6:${JSON.stringify( 388 | url 389 | )}` 390 | ); 391 | try { 392 | const response = await fetch("https://crawl.search1api.com", { 393 | method: "POST", 394 | headers: { 395 | "Content-Type": "application/json", 396 | }, 397 | body: JSON.stringify({ 398 | url, 399 | }), 400 | }); 401 | if (!response.ok) { 402 | console.error( 403 | `API \u8BF7\u6C42\u5931\u8D25, \u72B6\u6001\u7801: ${response.status}` 404 | ); 405 | return `API \u8BF7\u6C42\u5931\u8D25, \u72B6\u6001\u7801: ${response.status}`; 406 | } 407 | const contentType = response.headers.get("content-type"); 408 | if (!contentType || !contentType.includes("application/json")) { 409 | console.error( 410 | "\u6536\u5230\u7684\u54CD\u5E94\u4E0D\u662F\u6709\u6548\u7684 JSON \u683C\u5F0F" 411 | ); 412 | return "\u6536\u5230\u7684\u54CD\u5E94\u4E0D\u662F\u6709\u6548\u7684 JSON \u683C\u5F0F"; 413 | } 414 | const data = await response.json(); 415 | console.log( 416 | "\u81EA\u5B9A\u4E49\u722C\u53D6\u670D\u52A1\u8C03\u7528\u5B8C\u6210" 417 | ); 418 | return JSON.stringify(data); 419 | } catch (error) { 420 | console.error( 421 | `\u5728 crawl \u51FD\u6570\u4E2D\u6355\u83B7\u5230\u9519\u8BEF: ${error}` 422 | ); 423 | return `\u5728 crawler \u51FD\u6570\u4E2D\u6355\u83B7\u5230\u9519\u8BEF: ${error}`; 424 | } 425 | } 426 | async function handleRequest(request, fetchAPI, apiKey) { 427 | console.log( 428 | `\u5F00\u59CB\u5904\u7406\u8BF7\u6C42: ${request.method} ${request.url}` 429 | ); 430 | if (request.method !== "POST") { 431 | console.log( 432 | `\u4E0D\u652F\u6301\u7684\u8BF7\u6C42\u65B9\u6CD5: ${request.method}` 433 | ); 434 | return new Response("Method Not Allowed", { 435 | status: 405, 436 | headers: corsHeaders, 437 | }); 438 | } 439 | 440 | const requestData = await request.json(); 441 | console.log("\u8BF7\u6C42\u6570\u636E:", requestData); 442 | const stream = requestData.stream || false; 443 | const userMessages = requestData.messages.filter( 444 | (message) => message.role === "user" 445 | ); 446 | const latestUserMessage = userMessages[userMessages.length - 1]; 447 | const model = requestData.model; 448 | const isContentArray = Array.isArray(latestUserMessage.content); 449 | const defaultMaxTokens = 3e3; 450 | const maxTokens = requestData.max_tokens || defaultMaxTokens; 451 | const body = JSON.stringify({ 452 | model, 453 | messages: requestData.messages, 454 | max_tokens: maxTokens, 455 | ...(isContentArray 456 | ? {} 457 | : { 458 | tools: [ 459 | { 460 | type: "function", 461 | function: { 462 | name: "search", 463 | description: "search for factors and weathers", 464 | parameters: { 465 | type: "object", 466 | properties: { 467 | query: { 468 | type: "string", 469 | description: "The query to search.", 470 | }, 471 | }, 472 | required: ["query"], 473 | }, 474 | }, 475 | }, 476 | { 477 | type: "function", 478 | function: { 479 | name: "news", 480 | description: "Search for news", 481 | parameters: { 482 | type: "object", 483 | properties: { 484 | query: { 485 | type: "string", 486 | description: "The query to search for news.", 487 | }, 488 | }, 489 | required: ["query"], 490 | }, 491 | }, 492 | }, 493 | { 494 | type: "function", 495 | function: { 496 | name: "crawler", 497 | description: "Get the content of a specified url", 498 | parameters: { 499 | type: "object", 500 | properties: { 501 | url: { 502 | type: "string", 503 | description: "The URL of the webpage", 504 | }, 505 | }, 506 | required: ["url"], 507 | }, 508 | }, 509 | }, 510 | ], 511 | tool_choice: "auto", 512 | }), 513 | }); 514 | 515 | request_header.set(`${header_auth}`, `${header_auth_val}${apiKey}`); 516 | 517 | if (stream) { 518 | const openAIResponse = await fetch(fetchAPI, { 519 | method: "POST", 520 | headers: request_header, 521 | body, 522 | }); 523 | if (openAIResponse.status !== 200) { 524 | console.error( 525 | `OpenAI API \u8BF7\u6C42\u5931\u8D25,\u72B6\u6001\u7801: ${openAIResponse.status}` 526 | ); 527 | return new Response( 528 | `OpenAI API \u8BF7\u6C42\u5931\u8D25,\u72B6\u6001\u7801: ${openAIResponse.status}`, 529 | { 530 | status: 500, 531 | headers: corsHeaders, 532 | } 533 | ); 534 | } 535 | const data = await openAIResponse.json(); 536 | console.log( 537 | "OpenAI API \u54CD\u5E94\u72B6\u6001\u7801:", 538 | openAIResponse.status 539 | ); 540 | if (!data.choices || data.choices.length === 0) { 541 | console.log("\u6570\u636E\u4E2D\u6CA1\u6709\u9009\u62E9\u9879"); 542 | return new Response( 543 | "\u6570\u636E\u4E2D\u6CA1\u6709\u9009\u62E9\u9879", 544 | { status: 500 } 545 | ); 546 | } 547 | console.log( 548 | "OpenAI API \u54CD\u5E94\u63A5\u6536\u5B8C\u6210\uFF0C\u68C0\u67E5\u662F\u5426\u9700\u8981\u8C03\u7528\u81EA\u5B9A\u4E49\u51FD\u6570" 549 | ); 550 | let messages = requestData.messages; 551 | messages.push(data.choices[0].message); 552 | let calledCustomFunction = false; 553 | if (data.choices[0].message.tool_calls) { 554 | const toolCalls = data.choices[0].message.tool_calls; 555 | const availableFunctions = { 556 | search: search, 557 | news: news, 558 | crawler: crawler, 559 | }; 560 | for (const toolCall of toolCalls) { 561 | const functionName = toolCall.function.name; 562 | const functionToCall = availableFunctions[functionName]; 563 | const functionArgs = JSON.parse(toolCall.function.arguments); 564 | let functionResponse; 565 | if (functionName === "search") { 566 | functionResponse = await functionToCall(functionArgs.query); 567 | } else if (functionName === "crawler") { 568 | functionResponse = await functionToCall(functionArgs.url); 569 | } else if (functionName === "news") { 570 | functionResponse = await functionToCall(functionArgs.query); 571 | } 572 | messages.push({ 573 | tool_call_id: toolCall.id, 574 | role: "tool", 575 | name: functionName, 576 | content: functionResponse, 577 | }); 578 | if ( 579 | functionName === "search" || 580 | functionName === "crawler" || 581 | functionName === "news" 582 | ) { 583 | calledCustomFunction = true; 584 | } 585 | } 586 | } 587 | if (calledCustomFunction) { 588 | console.log( 589 | "\u51C6\u5907\u53D1\u9001\u7B2C\u4E8C\u6B21 OpenAI API \u8BF7\u6C42" 590 | ); 591 | 592 | const secondRequestBody = JSON.stringify({ 593 | model, 594 | messages, 595 | }); 596 | 597 | const secondResponse = await fetch(fetchAPI, { 598 | method: "POST", 599 | headers: request_header, 600 | body: secondRequestBody, 601 | }); 602 | 603 | console.log("Second response status:", secondResponse.status); 604 | console.log("Second response headers:", secondResponse.headers); 605 | if (secondResponse.status !== 200) { 606 | throw new Error( 607 | `OpenAI API 第二次请求失败,状态码: ${secondResponse.status}` 608 | ); 609 | } 610 | 611 | const data = await secondResponse.json(); 612 | const content = data.choices[0].message.content; 613 | const words = content.split(/(\s+)/); 614 | 615 | const stream = new ReadableStream({ 616 | async start(controller) { 617 | const baseData = { 618 | id: data.id, 619 | object: "chat.completion.chunk", 620 | created: data.created, 621 | model: data.model, 622 | system_fingerprint: data.system_fingerprint, 623 | choices: [ 624 | { 625 | index: 0, 626 | delta: {}, 627 | logprobs: null, 628 | finish_reason: null, 629 | }, 630 | ], 631 | x_groq: { 632 | id: data.x_groq ? data.x_groq.id : null, 633 | }, 634 | }; 635 | 636 | for (const word of words) { 637 | const chunkData = { 638 | ...baseData, 639 | choices: [ 640 | { 641 | ...baseData.choices[0], 642 | delta: { content: word.includes("\n") ? word : word + " " }, 643 | }, 644 | ], 645 | }; 646 | const sseMessage = `data: ${JSON.stringify(chunkData)}\n\n`; 647 | controller.enqueue(new TextEncoder().encode(sseMessage)); 648 | await new Promise((resolve) => setTimeout(resolve, 5)); 649 | } 650 | 651 | const finalChunkData = { 652 | ...baseData, 653 | choices: [ 654 | { 655 | ...baseData.choices[0], 656 | finish_reason: data.choices[0].finish_reason, 657 | }, 658 | ], 659 | x_groq: { 660 | ...baseData.x_groq, 661 | usage: data.usage, 662 | }, 663 | }; 664 | const finalSseMessage = `data: ${JSON.stringify( 665 | finalChunkData 666 | )}\n\ndata: [DONE]\n\n`; 667 | controller.enqueue(new TextEncoder().encode(finalSseMessage)); 668 | controller.close(); 669 | }, 670 | }); 671 | 672 | return new Response(stream, { 673 | status: 200, 674 | headers: { 675 | "Content-Type": "text/event-stream", 676 | ...corsHeaders, 677 | }, 678 | }); 679 | } else { 680 | const content = data.choices[0].message.content; 681 | const words = content.split(/(\s+)/); 682 | 683 | const stream = new ReadableStream({ 684 | async start(controller) { 685 | const baseData = { 686 | id: data.id, 687 | object: "chat.completion.chunk", 688 | created: data.created, 689 | model: data.model, 690 | system_fingerprint: data.system_fingerprint, 691 | choices: [ 692 | { 693 | index: 0, 694 | delta: {}, 695 | logprobs: null, 696 | finish_reason: null, 697 | }, 698 | ], 699 | x_groq: { 700 | id: data.x_groq ? data.x_groq.id : null, 701 | }, 702 | }; 703 | 704 | for (const word of words) { 705 | const chunkData = { 706 | ...baseData, 707 | choices: [ 708 | { 709 | ...baseData.choices[0], 710 | delta: { content: word.includes("\n") ? word : word + " " }, 711 | }, 712 | ], 713 | }; 714 | const sseMessage = `data: ${JSON.stringify(chunkData)}\n\n`; 715 | controller.enqueue(new TextEncoder().encode(sseMessage)); 716 | await new Promise((resolve) => setTimeout(resolve, 5)); 717 | } 718 | 719 | const finalChunkData = { 720 | ...baseData, 721 | choices: [ 722 | { 723 | ...baseData.choices[0], 724 | finish_reason: data.choices[0].finish_reason, 725 | }, 726 | ], 727 | x_groq: { 728 | ...baseData.x_groq, 729 | usage: data.usage, 730 | }, 731 | }; 732 | const finalSseMessage = `data: ${JSON.stringify( 733 | finalChunkData 734 | )}\n\ndata: [DONE]\n\n`; 735 | controller.enqueue(new TextEncoder().encode(finalSseMessage)); 736 | controller.close(); 737 | }, 738 | }); 739 | 740 | return new Response(stream, { 741 | status: 200, 742 | headers: { 743 | "Content-Type": "text/event-stream", 744 | ...corsHeaders, 745 | }, 746 | }); 747 | } 748 | } else { 749 | const openAIResponse = await fetch(fetchAPI, { 750 | method: "POST", 751 | headers: request_header, 752 | body, 753 | }); 754 | if (openAIResponse.status !== 200) { 755 | console.error( 756 | `OpenAI API \u8BF7\u6C42\u5931\u8D25,\u72B6\u6001\u7801: ${openAIResponse.status}` 757 | ); 758 | return new Response( 759 | `OpenAI API \u8BF7\u6C42\u5931\u8D25,\u72B6\u6001\u7801: ${openAIResponse.status}`, 760 | { 761 | status: 500, 762 | headers: corsHeaders, 763 | } 764 | ); 765 | } 766 | const data = await openAIResponse.json(); 767 | console.log( 768 | "OpenAI API \u54CD\u5E94\u72B6\u6001\u7801:", 769 | openAIResponse.status 770 | ); 771 | if (!data.choices || data.choices.length === 0) { 772 | console.log("\u6570\u636E\u4E2D\u6CA1\u6709\u9009\u62E9\u9879"); 773 | return new Response( 774 | "\u6570\u636E\u4E2D\u6CA1\u6709\u9009\u62E9\u9879", 775 | { status: 500 } 776 | ); 777 | } 778 | console.log( 779 | "OpenAI API \u54CD\u5E94\u63A5\u6536\u5B8C\u6210\uFF0C\u68C0\u67E5\u662F\u5426\u9700\u8981\u8C03\u7528\u81EA\u5B9A\u4E49\u51FD\u6570" 780 | ); 781 | let messages = requestData.messages; 782 | messages.push(data.choices[0].message); 783 | let calledCustomFunction = false; 784 | if (data.choices[0].message.tool_calls) { 785 | const toolCalls = data.choices[0].message.tool_calls; 786 | const availableFunctions = { 787 | search: search, 788 | news: news, 789 | crawler: crawler, 790 | }; 791 | for (const toolCall of toolCalls) { 792 | const functionName = toolCall.function.name; 793 | const functionToCall = availableFunctions[functionName]; 794 | const functionArgs = JSON.parse(toolCall.function.arguments); 795 | let functionResponse; 796 | if (functionName === "search") { 797 | functionResponse = await functionToCall(functionArgs.query); 798 | } else if (functionName === "crawler") { 799 | functionResponse = await functionToCall(functionArgs.url); 800 | } else if (functionName === "news") { 801 | functionResponse = await functionToCall(functionArgs.query); 802 | } 803 | messages.push({ 804 | tool_call_id: toolCall.id, 805 | role: "tool", 806 | name: functionName, 807 | content: functionResponse, 808 | }); 809 | if ( 810 | functionName === "search" || 811 | functionName === "crawler" || 812 | functionName === "news" 813 | ) { 814 | calledCustomFunction = true; 815 | } 816 | } 817 | } 818 | if (calledCustomFunction) { 819 | console.log( 820 | "\u51C6\u5907\u53D1\u9001\u7B2C\u4E8C\u6B21 OpenAI API \u8BF7\u6C42" 821 | ); 822 | 823 | const requestBody = { 824 | model, 825 | messages, 826 | }; 827 | const secondResponse = await fetch(fetchAPI, { 828 | method: "POST", 829 | headers: request_header, 830 | body: JSON.stringify(requestBody), 831 | }); 832 | console.log("\u54CD\u5E94\u72B6\u6001\u7801: 200"); 833 | const data2 = await secondResponse.json(); 834 | return new Response(JSON.stringify(data2), { 835 | status: 200, 836 | headers: { 837 | "Content-Type": "application/json", 838 | ...corsHeaders, 839 | }, 840 | }); 841 | } else { 842 | console.log("\u54CD\u5E94\u72B6\u6001\u7801: 200"); 843 | return new Response(JSON.stringify(data), { 844 | status: 200, 845 | headers: { 846 | "Content-Type": "application/json", 847 | ...corsHeaders, 848 | }, 849 | }); 850 | } 851 | } 852 | } 853 | })(); 854 | //# sourceMappingURL=openai.js.map 855 | -------------------------------------------------------------------------------- /search2moonshot.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | // openai.js 3 | var corsHeaders = { 4 | "Access-Control-Allow-Origin": "*", 5 | "Access-Control-Allow-Methods": "GET, POST, OPTIONS", 6 | // 允许的HTTP方法 7 | "Access-Control-Allow-Headers": 8 | "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization", 9 | "Access-Control-Max-Age": "86400", 10 | // 预检请求结果的缓存时间 11 | }; 12 | 13 | var header_auth = "Authorization"; //azure use "api-key" 14 | var header_auth_val = "Bearer "; 15 | 16 | // get variables from env 17 | const api_type = typeof OPENAI_TYPE !== "undefined" ? OPENAI_TYPE : "openai"; 18 | const apiBase = 19 | typeof APIBASE !== "undefined" ? APIBASE : "https://api.openai.com"; 20 | const resource_name = 21 | typeof RESOURCE_NAME !== "undefined" ? RESOURCE_NAME : "xxxxx"; 22 | const deployName = 23 | typeof DEPLOY_NAME !== "undefined" ? DEPLOY_NAME : "gpt-35-turbo"; 24 | const api_ver = 25 | typeof API_VERSION !== "undefined" ? API_VERSION : "2024-03-01-preview"; 26 | let openai_key = typeof OPENAI_API_KEY !== "undefined" ? OPENAI_API_KEY : ""; 27 | const azure_key = typeof AZURE_API_KEY !== "undefined" ? AZURE_API_KEY : ""; 28 | const auth_keys = typeof AUTH_KEYS !== "undefined" ? AUTH_KEYS : [""]; 29 | 30 | let fetchAPI = ""; 31 | let request_header = new Headers({ 32 | "Content-Type": "application/json", 33 | Authorization: "", 34 | "api-key": "", 35 | }); 36 | 37 | addEventListener("fetch", (event) => { 38 | console.log( 39 | `\u6536\u5230\u8BF7\u6C42: ${event.request.method} ${event.request.url}` 40 | ); 41 | const url = new URL(event.request.url); 42 | if (event.request.method === "OPTIONS") { 43 | return event.respondWith(handleOptions()); 44 | } 45 | 46 | const authHeader = event.request.headers.get("Authorization"); 47 | let apiKey = ""; 48 | if (authHeader) { 49 | apiKey = authHeader.split(" ")[1]; 50 | if (!auth_keys.includes(apiKey) || !openai_key) { 51 | openai_key = apiKey; 52 | } 53 | } else { 54 | return event.respondWith( 55 | new Response("Authorization header is missing", { 56 | status: 400, 57 | headers: corsHeaders, 58 | }) 59 | ); 60 | } 61 | 62 | if (api_type === "azure") { 63 | fetchAPI = `https://${resource_name}.openai.azure.com/openai/deployments/${deployName}/chat/completions?api-version=${api_ver}`; 64 | header_auth = "api-key"; 65 | header_auth_val = ""; 66 | apiKey = azure_key; 67 | } else { 68 | //openai 69 | fetchAPI = `${apiBase}/v1/chat/completions`; 70 | header_auth = "Authorization"; 71 | header_auth_val = "Bearer "; 72 | apiKey = openai_key; 73 | } 74 | 75 | if (url.pathname === "/v1/chat/completions") { 76 | //openai-style request 77 | console.log("接收到 fetch 事件"); 78 | event.respondWith(handleRequest(event.request, fetchAPI, apiKey)); 79 | } else { 80 | //other request 81 | event.respondWith( 82 | handleOtherRequest(apiBase, apiKey, event.request, url.pathname).then( 83 | (response) => { 84 | return new Response(response.body, { 85 | status: response.status, 86 | headers: { ...response.headers, ...corsHeaders }, 87 | }); 88 | } 89 | ) 90 | ); 91 | } 92 | }); 93 | function handleOptions() { 94 | return new Response(null, { 95 | status: 204, 96 | headers: corsHeaders, 97 | }); 98 | } 99 | async function handleOtherRequest(apiBase, apiKey, request, pathname) { 100 | const headers = new Headers(request.headers); 101 | headers.delete("Host"); 102 | if (api_type === "azure") { 103 | headers.set("api-key", `${apiKey}`); 104 | } else { 105 | headers.set("Authorization", `Bearer ${apiKey}`); 106 | } 107 | 108 | const response = await fetch(`${apiBase}${pathname}`, { 109 | method: request.method, 110 | headers, 111 | body: request.body, 112 | }); 113 | let data; 114 | if (pathname.startsWith("/v1/audio/")) { 115 | data = await response.arrayBuffer(); 116 | return new Response(data, { 117 | status: response.status, 118 | headers: { "Content-Type": "audio/mpeg", ...corsHeaders }, 119 | }); 120 | } else { 121 | data = await response.json(); 122 | return new Response(JSON.stringify(data), { 123 | status: response.status, 124 | headers: corsHeaders, 125 | }); 126 | } 127 | } 128 | async function search(query) { 129 | console.log(`正在使用 ${SEARCH_SERVICE} 进行自定义搜索: ${JSON.stringify(query)}`); 130 | try { 131 | let results; 132 | 133 | switch (SEARCH_SERVICE) { 134 | case "search1api": 135 | const search1apiResponse = await fetch("https://api.search1api.com/search/", { 136 | method: "POST", 137 | headers: { 138 | "Content-Type": "application/json", 139 | Authorization: typeof SEARCH1API_KEY !== "undefined" ? `Bearer ${SEARCH1API_KEY}` : "", 140 | }, 141 | body: JSON.stringify({ 142 | query, 143 | max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "5", 144 | crawl_results: typeof CRAWL_RESULTS !== "undefined" ? CRAWL_RESULTS : "0", 145 | }), 146 | }); 147 | results = await search1apiResponse.json(); 148 | break; 149 | 150 | case "google": 151 | const googleApiUrl = `https://www.googleapis.com/customsearch/v1?cx=${GOOGLE_CX}&key=${GOOGLE_KEY}&q=${encodeURIComponent(query)}`; 152 | const googleResponse = await fetch(googleApiUrl); 153 | const googleData = await googleResponse.json(); 154 | results = googleData.items.slice(0, MAX_RESULTS).map((item) => ({ 155 | title: item.title, 156 | link: item.link, 157 | snippet: item.snippet 158 | })); 159 | break; 160 | 161 | case "bing": 162 | const bingApiUrl = `https://api.bing.microsoft.com/v7.0/search?q=${encodeURIComponent(query)}`; 163 | const bingResponse = await fetch(bingApiUrl, { 164 | headers: { "Ocp-Apim-Subscription-Key": BING_KEY } 165 | }); 166 | const bingData = await bingResponse.json(); 167 | results = bingData.webPages.value.slice(0, MAX_RESULTS).map((item) => ({ 168 | title: item.name, 169 | link: item.url, 170 | snippet: item.snippet 171 | })); 172 | break; 173 | 174 | case "serpapi": 175 | const serpApiUrl = `https://serpapi.com/search?api_key=${SERPAPI_KEY}&engine=google&q=${encodeURIComponent(query)}&google_domain=google.com`; 176 | const serpApiResponse = await fetch(serpApiUrl); 177 | const serpApiData = await serpApiResponse.json(); 178 | results = serpApiData.organic_results.slice(0, MAX_RESULTS).map((item) => ({ 179 | title: item.title, 180 | link: item.link, 181 | snippet: item.snippet 182 | })); 183 | break; 184 | 185 | case "serper": 186 | const gl = typeof GL !== "undefined" ? GL : "us"; 187 | const hl = typeof HL !== "undefined" ? HL : "en"; 188 | const serperApiUrl = "https://google.serper.dev/search"; 189 | const serperResponse = await fetch(serperApiUrl, { 190 | method: "POST", 191 | headers: { 192 | "X-API-KEY": SERPER_KEY, 193 | "Content-Type": "application/json" 194 | }, 195 | body: JSON.stringify({ q: query, gl: gl, hl: hl }) 196 | }); 197 | const serperData = await serperResponse.json(); 198 | results = serperData.organic.slice(0, MAX_RESULTS).map((item) => ({ 199 | title: item.title, 200 | link: item.link, 201 | snippet: item.snippet 202 | })); 203 | break; 204 | 205 | case "duckduckgo": 206 | const duckDuckGoApiUrl = "https://ddg.search2ai.online/search"; 207 | const body = { 208 | q: query, 209 | max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "5" 210 | }; 211 | const duckDuckGoResponse = await fetch(duckDuckGoApiUrl, { 212 | method: "POST", 213 | headers: { 214 | "Content-Type": "application/json" 215 | }, 216 | body: JSON.stringify(body) 217 | }); 218 | const duckDuckGoData = await duckDuckGoResponse.json(); 219 | results = duckDuckGoData.results.map((item) => ({ 220 | title: item.title, 221 | link: item.href, 222 | snippet: item.body 223 | })); 224 | break; 225 | 226 | case "searxng": 227 | const searXNGUrl = `${SEARXNG_BASE_URL}/search?q=${encodeURIComponent( 228 | query 229 | )}&category=general&format=json`; 230 | const searXNGResponse = await fetch(searXNGUrl); 231 | const searXNGData = await searXNGResponse.json(); 232 | results = searXNGData.results.slice(0, MAX_RESULTS).map((item) => ({ 233 | title: item.title, 234 | link: item.url, 235 | snippet: item.content 236 | })); 237 | break; 238 | 239 | default: 240 | console.error(`不支持的搜索服务: ${SEARCH_SERVICE}`); 241 | return `不支持的搜索服务: ${SEARCH_SERVICE}`; 242 | } 243 | 244 | const data = { 245 | results: results 246 | }; 247 | 248 | console.log('自定义搜索服务调用完成'); 249 | return JSON.stringify(data); 250 | 251 | } catch (error) { 252 | console.error(`在 search 函数中捕获到错误: ${error}`); 253 | return `在 search 函数中捕获到错误: ${error}`; 254 | } 255 | } 256 | async function news(query) { 257 | console.log(`正在使用 ${SEARCH_SERVICE} 进行新闻搜索: ${JSON.stringify(query)}`); 258 | 259 | try { 260 | let results; 261 | 262 | switch (SEARCH_SERVICE) { 263 | case "search1api": 264 | const search1apiResponse = await fetch("https://api.search1api.com/news", { 265 | method: "POST", 266 | headers: { 267 | "Content-Type": "application/json", 268 | Authorization: typeof SEARCH1API_KEY !== "undefined" ? `Bearer ${SEARCH1API_KEY}` : "", 269 | }, 270 | body: JSON.stringify({ 271 | query, 272 | max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "10", 273 | crawl_results: typeof CRAWL_RESULTS !== "undefined" ? CRAWL_RESULTS : "0", 274 | }), 275 | }); 276 | results = await search1apiResponse.json(); 277 | break; 278 | 279 | case "google": 280 | const googleApiUrl = `https://www.googleapis.com/customsearch/v1?cx=${GOOGLE_CX}&key=${GOOGLE_KEY}&q=${encodeURIComponent(query)}&tbm=nws`; 281 | const googleResponse = await fetch(googleApiUrl); 282 | const googleData = await googleResponse.json(); 283 | results = googleData.items.slice(0, MAX_RESULTS).map((item) => ({ 284 | title: item.title, 285 | link: item.link, 286 | snippet: item.snippet 287 | })); 288 | break; 289 | 290 | case "bing": 291 | const bingApiUrl = `https://api.bing.microsoft.com/v7.0/news/search?q=${encodeURIComponent(query)}`; 292 | const bingResponse = await fetch(bingApiUrl, { 293 | headers: { "Ocp-Apim-Subscription-Key": BING_KEY } 294 | }); 295 | const bingData = await bingResponse.json(); 296 | results = bingData.value.slice(0, MAX_RESULTS).map((item) => ({ 297 | title: item.name, 298 | link: item.url, 299 | snippet: item.description 300 | })); 301 | break; 302 | 303 | case "serpapi": 304 | const serpApiUrl = `https://serpapi.com/search?api_key=${SERPAPI_KEY}&engine=google_news&q=${encodeURIComponent(query)}&google_domain=google.com`; 305 | const serpApiResponse = await fetch(serpApiUrl); 306 | const serpApiData = await serpApiResponse.json(); 307 | results = serpApiData.news_results.slice(0, MAX_RESULTS).map((item) => ({ 308 | title: item.title, 309 | link: item.link, 310 | snippet: item.snippet 311 | })); 312 | break; 313 | 314 | case "serper": 315 | const gl = typeof GL !== "undefined" ? GL : "us"; 316 | const hl = typeof HL !== "undefined" ? HL : "en"; 317 | const serperApiUrl = "https://google.serper.dev/news"; 318 | const serperResponse = await fetch(serperApiUrl, { 319 | method: "POST", 320 | headers: { 321 | "X-API-KEY": SERPER_KEY, 322 | "Content-Type": "application/json" 323 | }, 324 | body: JSON.stringify({ q: query, gl: gl, hl: hl }) 325 | }); 326 | const serperData = await serperResponse.json(); 327 | results = serperData.news.slice(0, MAX_RESULTS).map((item) => ({ 328 | title: item.title, 329 | link: item.link, 330 | snippet: item.snippet 331 | })); 332 | break; 333 | 334 | case "duckduckgo": 335 | const duckDuckGoApiUrl = "https://ddg.search2ai.online/searchNews"; 336 | const body = { 337 | q: query, 338 | max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "10" 339 | }; 340 | const duckDuckGoResponse = await fetch(duckDuckGoApiUrl, { 341 | method: "POST", 342 | headers: { 343 | "Content-Type": "application/json" 344 | }, 345 | body: JSON.stringify(body) 346 | }); 347 | const duckDuckGoData = await duckDuckGoResponse.json(); 348 | results = duckDuckGoData.results.map((item) => ({ 349 | title: item.title, 350 | link: item.url, 351 | snippet: item.body 352 | })); 353 | break; 354 | 355 | case "searxng": 356 | const searXNGUrl = `${SEARXNG_BASE_URL}/search?q=${encodeURIComponent( 357 | query 358 | )}&category=news&format=json`; 359 | const searXNGResponse = await fetch(searXNGUrl); 360 | const searXNGData = await searXNGResponse.json(); 361 | results = searXNGData.results.slice(0, MAX_RESULTS).map((item) => ({ 362 | title: item.title, 363 | link: item.url, 364 | snippet: item.content 365 | })); 366 | break; 367 | 368 | default: 369 | console.error(`不支持的搜索服务: ${SEARCH_SERVICE}`); 370 | return `不支持的搜索服务: ${SEARCH_SERVICE}`; 371 | } 372 | 373 | const data = { 374 | results: results 375 | }; 376 | 377 | console.log('新闻搜索服务调用完成'); 378 | return JSON.stringify(data); 379 | 380 | } catch (error) { 381 | console.error(`在 news 函数中捕获到错误: ${error}`); 382 | return `在 news 函数中捕获到错误: ${error}`; 383 | } 384 | } 385 | async function crawler(url) { 386 | console.log( 387 | `\u6B63\u5728\u4F7F\u7528 URL \u8FDB\u884C\u81EA\u5B9A\u4E49\u722C\u53D6:${JSON.stringify( 388 | url 389 | )}` 390 | ); 391 | try { 392 | const response = await fetch("https://crawl.search1api.com", { 393 | method: "POST", 394 | headers: { 395 | "Content-Type": "application/json", 396 | }, 397 | body: JSON.stringify({ 398 | url, 399 | }), 400 | }); 401 | if (!response.ok) { 402 | console.error( 403 | `API \u8BF7\u6C42\u5931\u8D25, \u72B6\u6001\u7801: ${response.status}` 404 | ); 405 | return `API \u8BF7\u6C42\u5931\u8D25, \u72B6\u6001\u7801: ${response.status}`; 406 | } 407 | const contentType = response.headers.get("content-type"); 408 | if (!contentType || !contentType.includes("application/json")) { 409 | console.error( 410 | "\u6536\u5230\u7684\u54CD\u5E94\u4E0D\u662F\u6709\u6548\u7684 JSON \u683C\u5F0F" 411 | ); 412 | return "\u6536\u5230\u7684\u54CD\u5E94\u4E0D\u662F\u6709\u6548\u7684 JSON \u683C\u5F0F"; 413 | } 414 | const data = await response.json(); 415 | console.log( 416 | "\u81EA\u5B9A\u4E49\u722C\u53D6\u670D\u52A1\u8C03\u7528\u5B8C\u6210" 417 | ); 418 | return JSON.stringify(data); 419 | } catch (error) { 420 | console.error( 421 | `\u5728 crawl \u51FD\u6570\u4E2D\u6355\u83B7\u5230\u9519\u8BEF: ${error}` 422 | ); 423 | return `\u5728 crawler \u51FD\u6570\u4E2D\u6355\u83B7\u5230\u9519\u8BEF: ${error}`; 424 | } 425 | } 426 | async function handleRequest(request, fetchAPI, apiKey) { 427 | console.log( 428 | `\u5F00\u59CB\u5904\u7406\u8BF7\u6C42: ${request.method} ${request.url}` 429 | ); 430 | if (request.method !== "POST") { 431 | console.log( 432 | `\u4E0D\u652F\u6301\u7684\u8BF7\u6C42\u65B9\u6CD5: ${request.method}` 433 | ); 434 | return new Response("Method Not Allowed", { 435 | status: 405, 436 | headers: corsHeaders, 437 | }); 438 | } 439 | 440 | const requestData = await request.json(); 441 | console.log("\u8BF7\u6C42\u6570\u636E:", requestData); 442 | const stream = requestData.stream || false; 443 | const userMessages = requestData.messages.filter( 444 | (message) => message.role === "user" 445 | ); 446 | const latestUserMessage = userMessages[userMessages.length - 1]; 447 | const model = requestData.model; 448 | const isContentArray = Array.isArray(latestUserMessage.content); 449 | const defaultMaxTokens = 3e3; 450 | const maxTokens = requestData.max_tokens || defaultMaxTokens; 451 | const body = JSON.stringify({ 452 | model, 453 | messages: requestData.messages, 454 | max_tokens: maxTokens, 455 | ...(isContentArray 456 | ? {} 457 | : { 458 | tools: [ 459 | { 460 | type: "function", 461 | function: { 462 | name: "search", 463 | description: "search for factors and weathers", 464 | parameters: { 465 | type: "object", 466 | properties: { 467 | query: { 468 | type: "string", 469 | description: "The query to search.", 470 | }, 471 | }, 472 | required: ["query"], 473 | }, 474 | }, 475 | }, 476 | { 477 | type: "function", 478 | function: { 479 | name: "news", 480 | description: "Search for news", 481 | parameters: { 482 | type: "object", 483 | properties: { 484 | query: { 485 | type: "string", 486 | description: "The query to search for news.", 487 | }, 488 | }, 489 | required: ["query"], 490 | }, 491 | }, 492 | }, 493 | { 494 | type: "function", 495 | function: { 496 | name: "crawler", 497 | description: "Get the content of a specified url", 498 | parameters: { 499 | type: "object", 500 | properties: { 501 | url: { 502 | type: "string", 503 | description: "The URL of the webpage", 504 | }, 505 | }, 506 | required: ["url"], 507 | }, 508 | }, 509 | }, 510 | ], 511 | tool_choice: "auto", 512 | }), 513 | }); 514 | 515 | request_header.set(`${header_auth}`, `${header_auth_val}${apiKey}`); 516 | 517 | if (stream) { 518 | const openAIResponse = await fetch(fetchAPI, { 519 | method: "POST", 520 | headers: request_header, 521 | body, 522 | }); 523 | if (openAIResponse.status !== 200) { 524 | console.error( 525 | `OpenAI API \u8BF7\u6C42\u5931\u8D25,\u72B6\u6001\u7801: ${openAIResponse.status}` 526 | ); 527 | return new Response( 528 | `OpenAI API \u8BF7\u6C42\u5931\u8D25,\u72B6\u6001\u7801: ${openAIResponse.status}`, 529 | { 530 | status: 500, 531 | headers: corsHeaders, 532 | } 533 | ); 534 | } 535 | const data = await openAIResponse.json(); 536 | console.log( 537 | "OpenAI API \u54CD\u5E94\u72B6\u6001\u7801:", 538 | openAIResponse.status 539 | ); 540 | if (!data.choices || data.choices.length === 0) { 541 | console.log("\u6570\u636E\u4E2D\u6CA1\u6709\u9009\u62E9\u9879"); 542 | return new Response( 543 | "\u6570\u636E\u4E2D\u6CA1\u6709\u9009\u62E9\u9879", 544 | { status: 500 } 545 | ); 546 | } 547 | console.log( 548 | "OpenAI API \u54CD\u5E94\u63A5\u6536\u5B8C\u6210\uFF0C\u68C0\u67E5\u662F\u5426\u9700\u8981\u8C03\u7528\u81EA\u5B9A\u4E49\u51FD\u6570" 549 | ); 550 | let messages = requestData.messages; 551 | messages.push(data.choices[0].message); 552 | let calledCustomFunction = false; 553 | if (data.choices[0].message.tool_calls) { 554 | const toolCalls = data.choices[0].message.tool_calls; 555 | const availableFunctions = { 556 | search: search, 557 | news: news, 558 | crawler: crawler, 559 | }; 560 | for (const toolCall of toolCalls) { 561 | const functionName = toolCall.function.name; 562 | const functionToCall = availableFunctions[functionName]; 563 | const functionArgs = JSON.parse(toolCall.function.arguments); 564 | let functionResponse; 565 | if (functionName === "search") { 566 | functionResponse = await functionToCall(functionArgs.query); 567 | } else if (functionName === "crawler") { 568 | functionResponse = await functionToCall(functionArgs.url); 569 | } else if (functionName === "news") { 570 | functionResponse = await functionToCall(functionArgs.query); 571 | } 572 | messages.push({ 573 | tool_call_id: toolCall.id, 574 | role: "tool", 575 | name: functionName, 576 | content: functionResponse, 577 | }); 578 | if ( 579 | functionName === "search" || 580 | functionName === "crawler" || 581 | functionName === "news" 582 | ) { 583 | calledCustomFunction = true; 584 | } 585 | } 586 | } 587 | if (calledCustomFunction) { 588 | console.log( 589 | "\u51C6\u5907\u53D1\u9001\u7B2C\u4E8C\u6B21 OpenAI API \u8BF7\u6C42" 590 | ); 591 | 592 | const secondRequestBody = JSON.stringify({ 593 | model, 594 | messages, 595 | }); 596 | 597 | const secondResponse = await fetch(fetchAPI, { 598 | method: "POST", 599 | headers: request_header, 600 | body: secondRequestBody, 601 | }); 602 | 603 | console.log("Second response status:", secondResponse.status); 604 | console.log("Second response headers:", secondResponse.headers); 605 | if (secondResponse.status !== 200) { 606 | throw new Error( 607 | `OpenAI API 第二次请求失败,状态码: ${secondResponse.status}` 608 | ); 609 | } 610 | 611 | const data = await secondResponse.json(); 612 | const content = data.choices[0].message.content; 613 | const words = content.split(/(\s+)/); 614 | 615 | const stream = new ReadableStream({ 616 | async start(controller) { 617 | const baseData = { 618 | id: data.id, 619 | object: "chat.completion.chunk", 620 | created: data.created, 621 | model: data.model, 622 | system_fingerprint: data.system_fingerprint, 623 | choices: [ 624 | { 625 | index: 0, 626 | delta: {}, 627 | logprobs: null, 628 | finish_reason: null, 629 | }, 630 | ], 631 | x_groq: { 632 | id: data.x_groq ? data.x_groq.id : null, 633 | }, 634 | }; 635 | 636 | for (const word of words) { 637 | const chunkData = { 638 | ...baseData, 639 | choices: [ 640 | { 641 | ...baseData.choices[0], 642 | delta: { content: word.includes("\n") ? word : word + " " }, 643 | }, 644 | ], 645 | }; 646 | const sseMessage = `data: ${JSON.stringify(chunkData)}\n\n`; 647 | controller.enqueue(new TextEncoder().encode(sseMessage)); 648 | await new Promise((resolve) => setTimeout(resolve, 5)); 649 | } 650 | 651 | const finalChunkData = { 652 | ...baseData, 653 | choices: [ 654 | { 655 | ...baseData.choices[0], 656 | finish_reason: data.choices[0].finish_reason, 657 | }, 658 | ], 659 | x_groq: { 660 | ...baseData.x_groq, 661 | usage: data.usage, 662 | }, 663 | }; 664 | const finalSseMessage = `data: ${JSON.stringify( 665 | finalChunkData 666 | )}\n\ndata: [DONE]\n\n`; 667 | controller.enqueue(new TextEncoder().encode(finalSseMessage)); 668 | controller.close(); 669 | }, 670 | }); 671 | 672 | return new Response(stream, { 673 | status: 200, 674 | headers: { 675 | "Content-Type": "text/event-stream", 676 | ...corsHeaders, 677 | }, 678 | }); 679 | } else { 680 | const content = data.choices[0].message.content; 681 | const words = content.split(/(\s+)/); 682 | 683 | const stream = new ReadableStream({ 684 | async start(controller) { 685 | const baseData = { 686 | id: data.id, 687 | object: "chat.completion.chunk", 688 | created: data.created, 689 | model: data.model, 690 | system_fingerprint: data.system_fingerprint, 691 | choices: [ 692 | { 693 | index: 0, 694 | delta: {}, 695 | logprobs: null, 696 | finish_reason: null, 697 | }, 698 | ], 699 | x_groq: { 700 | id: data.x_groq ? data.x_groq.id : null, 701 | }, 702 | }; 703 | 704 | for (const word of words) { 705 | const chunkData = { 706 | ...baseData, 707 | choices: [ 708 | { 709 | ...baseData.choices[0], 710 | delta: { content: word.includes("\n") ? word : word + " " }, 711 | }, 712 | ], 713 | }; 714 | const sseMessage = `data: ${JSON.stringify(chunkData)}\n\n`; 715 | controller.enqueue(new TextEncoder().encode(sseMessage)); 716 | await new Promise((resolve) => setTimeout(resolve, 5)); 717 | } 718 | 719 | const finalChunkData = { 720 | ...baseData, 721 | choices: [ 722 | { 723 | ...baseData.choices[0], 724 | finish_reason: data.choices[0].finish_reason, 725 | }, 726 | ], 727 | x_groq: { 728 | ...baseData.x_groq, 729 | usage: data.usage, 730 | }, 731 | }; 732 | const finalSseMessage = `data: ${JSON.stringify( 733 | finalChunkData 734 | )}\n\ndata: [DONE]\n\n`; 735 | controller.enqueue(new TextEncoder().encode(finalSseMessage)); 736 | controller.close(); 737 | }, 738 | }); 739 | 740 | return new Response(stream, { 741 | status: 200, 742 | headers: { 743 | "Content-Type": "text/event-stream", 744 | ...corsHeaders, 745 | }, 746 | }); 747 | } 748 | } else { 749 | const openAIResponse = await fetch(fetchAPI, { 750 | method: "POST", 751 | headers: request_header, 752 | body, 753 | }); 754 | if (openAIResponse.status !== 200) { 755 | console.error( 756 | `OpenAI API \u8BF7\u6C42\u5931\u8D25,\u72B6\u6001\u7801: ${openAIResponse.status}` 757 | ); 758 | return new Response( 759 | `OpenAI API \u8BF7\u6C42\u5931\u8D25,\u72B6\u6001\u7801: ${openAIResponse.status}`, 760 | { 761 | status: 500, 762 | headers: corsHeaders, 763 | } 764 | ); 765 | } 766 | const data = await openAIResponse.json(); 767 | console.log( 768 | "OpenAI API \u54CD\u5E94\u72B6\u6001\u7801:", 769 | openAIResponse.status 770 | ); 771 | if (!data.choices || data.choices.length === 0) { 772 | console.log("\u6570\u636E\u4E2D\u6CA1\u6709\u9009\u62E9\u9879"); 773 | return new Response( 774 | "\u6570\u636E\u4E2D\u6CA1\u6709\u9009\u62E9\u9879", 775 | { status: 500 } 776 | ); 777 | } 778 | console.log( 779 | "OpenAI API \u54CD\u5E94\u63A5\u6536\u5B8C\u6210\uFF0C\u68C0\u67E5\u662F\u5426\u9700\u8981\u8C03\u7528\u81EA\u5B9A\u4E49\u51FD\u6570" 780 | ); 781 | let messages = requestData.messages; 782 | messages.push(data.choices[0].message); 783 | let calledCustomFunction = false; 784 | if (data.choices[0].message.tool_calls) { 785 | const toolCalls = data.choices[0].message.tool_calls; 786 | const availableFunctions = { 787 | search: search, 788 | news: news, 789 | crawler: crawler, 790 | }; 791 | for (const toolCall of toolCalls) { 792 | const functionName = toolCall.function.name; 793 | const functionToCall = availableFunctions[functionName]; 794 | const functionArgs = JSON.parse(toolCall.function.arguments); 795 | let functionResponse; 796 | if (functionName === "search") { 797 | functionResponse = await functionToCall(functionArgs.query); 798 | } else if (functionName === "crawler") { 799 | functionResponse = await functionToCall(functionArgs.url); 800 | } else if (functionName === "news") { 801 | functionResponse = await functionToCall(functionArgs.query); 802 | } 803 | messages.push({ 804 | tool_call_id: toolCall.id, 805 | role: "tool", 806 | name: functionName, 807 | content: functionResponse, 808 | }); 809 | if ( 810 | functionName === "search" || 811 | functionName === "crawler" || 812 | functionName === "news" 813 | ) { 814 | calledCustomFunction = true; 815 | } 816 | } 817 | } 818 | if (calledCustomFunction) { 819 | console.log( 820 | "\u51C6\u5907\u53D1\u9001\u7B2C\u4E8C\u6B21 OpenAI API \u8BF7\u6C42" 821 | ); 822 | 823 | const requestBody = { 824 | model, 825 | messages, 826 | }; 827 | const secondResponse = await fetch(fetchAPI, { 828 | method: "POST", 829 | headers: request_header, 830 | body: JSON.stringify(requestBody), 831 | }); 832 | console.log("\u54CD\u5E94\u72B6\u6001\u7801: 200"); 833 | const data2 = await secondResponse.json(); 834 | return new Response(JSON.stringify(data2), { 835 | status: 200, 836 | headers: { 837 | "Content-Type": "application/json", 838 | ...corsHeaders, 839 | }, 840 | }); 841 | } else { 842 | console.log("\u54CD\u5E94\u72B6\u6001\u7801: 200"); 843 | return new Response(JSON.stringify(data), { 844 | status: 200, 845 | headers: { 846 | "Content-Type": "application/json", 847 | ...corsHeaders, 848 | }, 849 | }); 850 | } 851 | } 852 | } 853 | })(); 854 | //# sourceMappingURL=openai.js.map 855 | --------------------------------------------------------------------------------