From 2c1072c48e9150be7f62a122cd227a660e63a073 Mon Sep 17 00:00:00 2001 From: fatwang2 Date: Thu, 25 Apr 2024 23:05:45 +0800 Subject: [PATCH] support searxng And moonshot stream --- .env.template | 10 +- README-EN.md | 10 +- README.md | 12 +- search2gemini.js | 2 +- search2groq.js | 4 +- search2moonshot.js | 854 +++++++++++++++++++++++++++++++++++++++++++++ search2openai.js | 4 +- units/news.js | 2 +- units/search.js | 2 +- 9 files changed, 881 insertions(+), 19 deletions(-) create mode 100644 search2moonshot.js diff --git a/.env.template b/.env.template index c9a2d9d..5ad71ab 100644 --- a/.env.template +++ b/.env.template @@ -1,9 +1,17 @@ +SEARCH1API_KEY=your_search1api_key GOOGLE_CX=your_google_cx GOOGLE_KEY=your_google_key SERPAPI_KEY=your_serpapi_key SERPER_KEY=your_serper_key BING_KEY=your_bing_key +SEARXNG_BASE_URL=your_searxng_base_url SEARCH_SERVICE=your_search_service MAX_RESULTS=the results of search CRAWL_RESULTS=the reults of search you want to crawl -APIBASE=https://api.openai.com \ No newline at end of file +APIBASE=https://api.openai.com +OPENAI_TYPE="openai" +AUTH_KEYS="1111,2222" +RESOURCE_NAME="" +DEPLOY_NAME="gpt-35-turbo" +API_VERSION="2024-02-15-preview" +AZURE_API_KEY="" diff --git a/README-EN.md b/README-EN.md index 7b5e26c..bf6ea80 100644 --- a/README-EN.md +++ b/README-EN.md @@ -9,11 +9,11 @@ Buy Me A Coffee # Version Updates - +- V0.2.6, 20240425, support the searxng search service, support the moonshot API in stream mode - V0.2.5, 20240425, open source the code for the search api - V0.2.4, 20240424, support for Groq in Cloudflare Worker - 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. -- V0.2.2, 20240420, support Moonshot API +- V0.2.2, 20240420, support Moonshot API on unstream mode - 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. - V0.2.0,20240310,Optimized openai.js, cloudflare worker version, really faster this time! @@ -42,7 +42,7 @@ Help your LLM API support networking, search, news, web page summarization, has | `Azure OpenAI` | search, news, crawler | stream, unstream | Cloudflare Worker | | `Groq` | search, news, crawler | stream, unstream | Cloudflare Worker | | `Gemini` | search | stream, unstream | Cloudflare Worker | -| `Moonshot` | search, news, crawler | unstream | Zeabur, Local deployment, Cloudflare Worker, Vercel | +| `Moonshot` | search, news, crawler | stream(only on cf), unstream | Zeabur, Local deployment, Cloudflare Worker(stream), Vercel | # Usage @@ -111,7 +111,7 @@ This project provides some additional configuration options, which can be set th | Environment Variable | Required | Description | Example | | -------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -| `SEARCH_SERVICE` | Yes | Your search service. The key of the service you choose needs to be configured. Supports search1api, google, bing, serpapi, serper, duckduckgo. | `search1api, google, bing, serpapi, serper, duckduckgo` | +| `SEARCH_SERVICE` | Yes | Your search service. The key of the service you choose needs to be configured. | `search1api, google, bing, serpapi, serper, duckduckgo, searxng` | | `APIBASE` | No | Third-party proxy address. | `https://api.openai.com, https://api.moonshot.cn, https://api.groq.com/openai` | | `MAX_RESULTS` | No | Number of search results. | `10` | | `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` | @@ -121,7 +121,7 @@ This project provides some additional configuration options, which can be set th | `GOOGLE_KEY` | Conditional | Required if Google search is selected. API key. Apply at https://search2ai.online/googlekey. | `xxx` | | `SERPAPI_KEY` | Conditional | Required if serpapi is selected. Free 100 times/month. Register at https://search2ai.online/serpapi. | `xxx` | | `SERPER_KEY` | Conditional | Required if serper is selected. Free 2500 times for 6 months. Register at https://search2ai.online/serper. | `xxx` | -| `SEARXNG_BASE_URL` | Conditional | Required if searXNG is selected. Fill in the domain name of the self-built searXNG service, e.g. https://search.xxx.xxx. (Must contain https/http without a / at the end.) | `xxx` | +| `SEARXNG_BASE_URL` | Conditional | Required if searxng is selected. Fill in the domain name of the self-built searXNG service, refer to this repo https://github.com/searxng/searxng, plz open the json mode | `https://search.xxx.xxx` | | `OPENAI_TYPE` | No | OpenAI provider source, default is openai | `openai, azure` | | `RESOURCE_NAME` | Conditional | Required if azure is selected | `xxxx` | | `DEPLOY_NAME` | Conditional | Required if azure is selected | `gpt-35-turbo` | diff --git a/README.md b/README.md index d2dac72..8b97dd7 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ Buy Me A Coffee # 版本更新 - +- V0.2.6,20240425,支持 SearXNG 免费搜索服务,有损支持 Moonshot 流式模式 - V0.2.5,20240425,为了解决隐私担忧,开源搜索接口部分的代码 - V0.2.4,20240424,支持 Groq 的llama-3、mistral等模型,速度起飞 - V0.2.3,20240423,Cloudflare Worker版本支持Azure OpenAI;支持授权码,可自定义用户的请求key -- V0.2.2,20240420,支持Moonshot的非流式模式 +- V0.2.2,20240420,支持 Moonshot 的非流式模式 - V0.2.1,20240310,支持Google、Bing、Duckduckgo、Search1API新闻类搜索;支持通过环境变量MAX_RESULTS调整搜索结果数量;支持通过环境变量CRAWL_RESULTS调整希望深度搜索的数量 - V0.2.0,20240310,优化openai.js,cloudflare worker版本,这次速度真的更快了! @@ -42,7 +42,7 @@ | `Azure OpenAI` | 联网、新闻、内容爬取 | 流式、非流式 | Cloudflare Worker | | `Groq` | 联网、新闻、内容爬取 | 流式、非流式 | Cloudflare Worker | | `Gemini` | 联网 | 流式、非流式 | Cloudflare Worker | -| `Moonshot` | 联网、新闻、内容爬取 | 非流式 | Zeabur、本地部署、Cloudflare Worker、Vercel | +| `Moonshot` | 联网、新闻、内容爬取 | 部分流式、非流式 | Zeabur、本地部署、Cloudflare Worker(流式)、Vercel | # 使用 @@ -111,8 +111,8 @@ http://localhost:3014/v1/chat/completions | 环境变量 | 是否必须 | 描述 | 例子 | | -------------------- | -------- | --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | -| `SEARCH_SERVICE` | Yes | 你的搜索服务,选择什么服务,就需要配置什么服务的key支持search1api、google、bing、serpapi、serper、duckduckgo、searXNG | `search1api, google, bing, serpapi, serper, duckduckgo, searXNG` | -| `APIBASE` | No | 三方代理地址` | `https://api.openai.com, https://api.moonshot.cn, https://api.groq.com/openai` | +| `SEARCH_SERVICE` | Yes | 你的搜索服务,选择什么服务,就需要配置什么服务的key | `search1api, google, bing, serpapi, serper, duckduckgo, searxng` | +| `APIBASE` | No | 三方代理地址 | `https://api.openai.com, https://api.moonshot.cn, https://api.groq.com/openai` | | `MAX_RESULTS` | No | 搜索结果条数 | `10` | | `CRAWL_RESULTS` | No | 要进行深度搜索(搜索后获取网页正文)的数量,目前仅支持 search1api,深度速度会慢 | `1` | | `SEARCH1API_KEY` | No | 如选search1api必填,我自己搭建的搜索服务,又快又便宜,申请地址 https://search21api.com | `xxx` | @@ -121,7 +121,7 @@ http://localhost:3014/v1/chat/completions | `GOOGLE_KEY` | No | 如选Google搜索必填,API key,申请地址 https://search2ai.online/googlekey | `xxx` | | `SERPAPI_KEY` | No | 如选serpapi必填,免费100次/月,注册地址 https://search2ai.online/serpapi | `xxx` | | `SERPER_KEY` | No | 如选serper必填,6个月免费额度2500次,注册地址 https://search2ai.online/serper | `xxx` | -| `SEARXNG_BASE_URL` | No | 如选searXNG必填,填写自建searXNG服务域名,例如:https://search.xxx.xxx。(须包含https/http且末尾不含/) | `xxxx` | +| `SEARXNG_BASE_URL` | No | 如选searxng必填,填写自建searXNG服务域名,教程 https://github.com/searxng/searxng,需打开 json 模式 | `https://search.xxx.xxx` | | `OPENAI_TYPE` | No | openai供给来源,默认为openai | `openai, azure` | | `RESOURCE_NAME` | No | 如选azure必填 | `xxxx` | | `DEPLOY_NAME` | No | 如选azure必填 | `gpt-35-turbo` | diff --git a/search2gemini.js b/search2gemini.js index df5e613..cfb7b3a 100644 --- a/search2gemini.js +++ b/search2gemini.js @@ -126,7 +126,7 @@ async function search(query) { })); break; - case "searXNG": + case "searxng": const searXNGUrl = `${SEARXNG_BASE_URL}/search?q=${encodeURIComponent( query )}&category=general&format=json`; diff --git a/search2groq.js b/search2groq.js index ff9e69f..dd950f3 100644 --- a/search2groq.js +++ b/search2groq.js @@ -223,7 +223,7 @@ })); break; - case "searXNG": + case "searxng": const searXNGUrl = `${SEARXNG_BASE_URL}/search?q=${encodeURIComponent( query )}&category=general&format=json`; @@ -352,7 +352,7 @@ })); break; - case "searXNG": + case "searxng": const searXNGUrl = `${SEARXNG_BASE_URL}/search?q=${encodeURIComponent( query )}&category=news&format=json`; diff --git a/search2moonshot.js b/search2moonshot.js new file mode 100644 index 0000000..dd950f3 --- /dev/null +++ b/search2moonshot.js @@ -0,0 +1,854 @@ +(() => { + // openai.js + var corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + // 允许的HTTP方法 + "Access-Control-Allow-Headers": + "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization", + "Access-Control-Max-Age": "86400", + // 预检请求结果的缓存时间 + }; + + var header_auth = "Authorization"; //azure use "api-key" + var header_auth_val = "Bearer "; + + // get variables from env + const api_type = typeof OPENAI_TYPE !== "undefined" ? OPENAI_TYPE : "openai"; + const apiBase = + typeof APIBASE !== "undefined" ? APIBASE : "https://api.openai.com"; + const resource_name = + typeof RESOURCE_NAME !== "undefined" ? RESOURCE_NAME : "xxxxx"; + const deployName = + typeof DEPLOY_NAME !== "undefined" ? DEPLOY_NAME : "gpt-35-turbo"; + const api_ver = + typeof API_VERSION !== "undefined" ? API_VERSION : "2024-03-01-preview"; + let openai_key = typeof OPENAI_API_KEY !== "undefined" ? OPENAI_API_KEY : ""; + const azure_key = typeof AZURE_API_KEY !== "undefined" ? AZURE_API_KEY : ""; + const auth_keys = typeof AUTH_KEYS !== "undefined" ? AUTH_KEYS : [""]; + + let fetchAPI = ""; + let request_header = new Headers({ + "Content-Type": "application/json", + Authorization: "", + "api-key": "", + }); + + addEventListener("fetch", (event) => { + console.log( + `\u6536\u5230\u8BF7\u6C42: ${event.request.method} ${event.request.url}` + ); + const url = new URL(event.request.url); + if (event.request.method === "OPTIONS") { + return event.respondWith(handleOptions()); + } + + const authHeader = event.request.headers.get("Authorization"); + let apiKey = ""; + if (authHeader) { + apiKey = authHeader.split(" ")[1]; + if (!auth_keys.includes(apiKey) || !openai_key) { + openai_key = apiKey; + } + } else { + return event.respondWith( + new Response("Authorization header is missing", { + status: 400, + headers: corsHeaders, + }) + ); + } + + if (api_type === "azure") { + fetchAPI = `https://${resource_name}.openai.azure.com/openai/deployments/${deployName}/chat/completions?api-version=${api_ver}`; + header_auth = "api-key"; + header_auth_val = ""; + apiKey = azure_key; + } else { + //openai + fetchAPI = `${apiBase}/v1/chat/completions`; + header_auth = "Authorization"; + header_auth_val = "Bearer "; + apiKey = openai_key; + } + + if (url.pathname === "/v1/chat/completions") { + //openai-style request + console.log("接收到 fetch 事件"); + event.respondWith(handleRequest(event.request, fetchAPI, apiKey)); + } else { + //other request + event.respondWith( + handleOtherRequest(apiBase, apiKey, event.request, url.pathname).then( + (response) => { + return new Response(response.body, { + status: response.status, + headers: { ...response.headers, ...corsHeaders }, + }); + } + ) + ); + } + }); + function handleOptions() { + return new Response(null, { + status: 204, + headers: corsHeaders, + }); + } + async function handleOtherRequest(apiBase, apiKey, request, pathname) { + const headers = new Headers(request.headers); + headers.delete("Host"); + if (api_type === "azure") { + headers.set("api-key", `${apiKey}`); + } else { + headers.set("Authorization", `Bearer ${apiKey}`); + } + + const response = await fetch(`${apiBase}${pathname}`, { + method: request.method, + headers, + body: request.body, + }); + let data; + if (pathname.startsWith("/v1/audio/")) { + data = await response.arrayBuffer(); + return new Response(data, { + status: response.status, + headers: { "Content-Type": "audio/mpeg", ...corsHeaders }, + }); + } else { + data = await response.json(); + return new Response(JSON.stringify(data), { + status: response.status, + headers: corsHeaders, + }); + } + } + async function search(query) { + console.log(`正在使用 ${SEARCH_SERVICE} 进行自定义搜索: ${JSON.stringify(query)}`); + try { + let results; + + switch (SEARCH_SERVICE) { + case "search1api": + const search1apiResponse = await fetch("https://search.search2ai.one", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: typeof SEARCH1API_KEY !== "undefined" ? `Bearer ${SEARCH1API_KEY}` : "", + }, + body: JSON.stringify({ + query, + max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "5", + crawl_results: typeof CRAWL_RESULTS !== "undefined" ? MAX_RESULTS : "0", + }), + }); + results = await search1apiResponse.json(); + break; + + case "google": + const googleApiUrl = `https://www.googleapis.com/customsearch/v1?cx=${GOOGLE_CX}&key=${GOOGLE_KEY}&q=${encodeURIComponent(query)}`; + const googleResponse = await fetch(googleApiUrl); + const googleData = await googleResponse.json(); + results = googleData.items.slice(0, MAX_RESULTS).map((item) => ({ + title: item.title, + link: item.link, + snippet: item.snippet + })); + break; + + case "bing": + const bingApiUrl = `https://api.bing.microsoft.com/v7.0/search?q=${encodeURIComponent(query)}`; + const bingResponse = await fetch(bingApiUrl, { + headers: { "Ocp-Apim-Subscription-Key": BING_KEY } + }); + const bingData = await bingResponse.json(); + results = bingData.webPages.value.slice(0, MAX_RESULTS).map((item) => ({ + title: item.name, + link: item.url, + snippet: item.snippet + })); + break; + + case "serpapi": + const serpApiUrl = `https://serpapi.com/search?api_key=${SERPAPI_KEY}&engine=google&q=${encodeURIComponent(query)}&google_domain=google.com`; + const serpApiResponse = await fetch(serpApiUrl); + const serpApiData = await serpApiResponse.json(); + results = serpApiData.organic_results.slice(0, MAX_RESULTS).map((item) => ({ + title: item.title, + link: item.link, + snippet: item.snippet + })); + break; + + case "serper": + const gl = typeof GL !== "undefined" ? GL : "us"; + const hl = typeof HL !== "undefined" ? HL : "en"; + const serperApiUrl = "https://google.serper.dev/search"; + const serperResponse = await fetch(serperApiUrl, { + method: "POST", + headers: { + "X-API-KEY": SERPER_KEY, + "Content-Type": "application/json" + }, + body: JSON.stringify({ q: query, gl: gl, hl: hl }) + }); + const serperData = await serperResponse.json(); + results = serperData.organic.slice(0, MAX_RESULTS).map((item) => ({ + title: item.title, + link: item.link, + snippet: item.snippet + })); + break; + + case "duckduckgo": + const duckDuckGoApiUrl = "https://ddg.search2ai.online/search"; + const body = { + q: query, + max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "5" + }; + const duckDuckGoResponse = await fetch(duckDuckGoApiUrl, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(body) + }); + const duckDuckGoData = await duckDuckGoResponse.json(); + results = duckDuckGoData.results.map((item) => ({ + title: item.title, + link: item.href, + snippet: item.body + })); + break; + + case "searxng": + const searXNGUrl = `${SEARXNG_BASE_URL}/search?q=${encodeURIComponent( + query + )}&category=general&format=json`; + const searXNGResponse = await fetch(searXNGUrl); + const searXNGData = await searXNGResponse.json(); + results = searXNGData.results.slice(0, MAX_RESULTS).map((item) => ({ + title: item.title, + link: item.url, + snippet: item.content + })); + break; + + default: + console.error(`不支持的搜索服务: ${SEARCH_SERVICE}`); + return `不支持的搜索服务: ${SEARCH_SERVICE}`; + } + + const data = { + results: results + }; + + console.log('自定义搜索服务调用完成'); + return JSON.stringify(data); + + } catch (error) { + console.error(`在 search 函数中捕获到错误: ${error}`); + return `在 search 函数中捕获到错误: ${error}`; + } + } + async function news(query) { + console.log(`正在使用 ${SEARCH_SERVICE} 进行新闻搜索: ${JSON.stringify(query)}`); + + try { + let results; + + switch (SEARCH_SERVICE) { + case "search1api": + const search1apiResponse = await fetch("https://search.search2ai.one/news", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: typeof SEARCH1API_KEY !== "undefined" ? `Bearer ${SEARCH1API_KEY}` : "", + }, + body: JSON.stringify({ + query, + max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "10", + crawl_results: typeof CRAWL_RESULTS !== "undefined" ? MAX_RESULTS : "0", + }), + }); + results = await search1apiResponse.json(); + break; + + case "google": + const googleApiUrl = `https://www.googleapis.com/customsearch/v1?cx=${GOOGLE_CX}&key=${GOOGLE_KEY}&q=${encodeURIComponent(query)}&tbm=nws`; + const googleResponse = await fetch(googleApiUrl); + const googleData = await googleResponse.json(); + results = googleData.items.slice(0, MAX_RESULTS).map((item) => ({ + title: item.title, + link: item.link, + snippet: item.snippet + })); + break; + + case "bing": + const bingApiUrl = `https://api.bing.microsoft.com/v7.0/news/search?q=${encodeURIComponent(query)}`; + const bingResponse = await fetch(bingApiUrl, { + headers: { "Ocp-Apim-Subscription-Key": BING_KEY } + }); + const bingData = await bingResponse.json(); + results = bingData.value.slice(0, MAX_RESULTS).map((item) => ({ + title: item.name, + link: item.url, + snippet: item.description + })); + break; + + case "serpapi": + const serpApiUrl = `https://serpapi.com/search?api_key=${SERPAPI_KEY}&engine=google_news&q=${encodeURIComponent(query)}&google_domain=google.com`; + const serpApiResponse = await fetch(serpApiUrl); + const serpApiData = await serpApiResponse.json(); + results = serpApiData.news_results.slice(0, MAX_RESULTS).map((item) => ({ + title: item.title, + link: item.link, + snippet: item.snippet + })); + break; + + case "serper": + const gl = typeof GL !== "undefined" ? GL : "us"; + const hl = typeof HL !== "undefined" ? HL : "en"; + const serperApiUrl = "https://google.serper.dev/news"; + const serperResponse = await fetch(serperApiUrl, { + method: "POST", + headers: { + "X-API-KEY": SERPER_KEY, + "Content-Type": "application/json" + }, + body: JSON.stringify({ q: query, gl: gl, hl: hl }) + }); + const serperData = await serperResponse.json(); + results = serperData.news.slice(0, MAX_RESULTS).map((item) => ({ + title: item.title, + link: item.link, + snippet: item.snippet + })); + break; + + case "duckduckgo": + const duckDuckGoApiUrl = "https://ddg.search2ai.online/searchNews"; + const body = { + q: query, + max_results: typeof MAX_RESULTS !== "undefined" ? MAX_RESULTS : "10" + }; + const duckDuckGoResponse = await fetch(duckDuckGoApiUrl, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(body) + }); + const duckDuckGoData = await duckDuckGoResponse.json(); + results = duckDuckGoData.results.map((item) => ({ + title: item.title, + link: item.url, + snippet: item.body + })); + break; + + case "searxng": + const searXNGUrl = `${SEARXNG_BASE_URL}/search?q=${encodeURIComponent( + query + )}&category=news&format=json`; + const searXNGResponse = await fetch(searXNGUrl); + const searXNGData = await searXNGResponse.json(); + results = searXNGData.results.slice(0, MAX_RESULTS).map((item) => ({ + title: item.title, + link: item.url, + snippet: item.content + })); + break; + + default: + console.error(`不支持的搜索服务: ${SEARCH_SERVICE}`); + return `不支持的搜索服务: ${SEARCH_SERVICE}`; + } + + const data = { + results: results + }; + + console.log('新闻搜索服务调用完成'); + return JSON.stringify(data); + + } catch (error) { + console.error(`在 news 函数中捕获到错误: ${error}`); + return `在 news 函数中捕获到错误: ${error}`; + } + } + async function crawler(url) { + console.log( + `\u6B63\u5728\u4F7F\u7528 URL \u8FDB\u884C\u81EA\u5B9A\u4E49\u722C\u53D6:${JSON.stringify( + url + )}` + ); + try { + const response = await fetch("https://crawler.search2ai.one", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + url, + }), + }); + if (!response.ok) { + console.error( + `API \u8BF7\u6C42\u5931\u8D25, \u72B6\u6001\u7801: ${response.status}` + ); + return `API \u8BF7\u6C42\u5931\u8D25, \u72B6\u6001\u7801: ${response.status}`; + } + const contentType = response.headers.get("content-type"); + if (!contentType || !contentType.includes("application/json")) { + console.error( + "\u6536\u5230\u7684\u54CD\u5E94\u4E0D\u662F\u6709\u6548\u7684 JSON \u683C\u5F0F" + ); + return "\u6536\u5230\u7684\u54CD\u5E94\u4E0D\u662F\u6709\u6548\u7684 JSON \u683C\u5F0F"; + } + const data = await response.json(); + console.log( + "\u81EA\u5B9A\u4E49\u722C\u53D6\u670D\u52A1\u8C03\u7528\u5B8C\u6210" + ); + return JSON.stringify(data); + } catch (error) { + console.error( + `\u5728 crawl \u51FD\u6570\u4E2D\u6355\u83B7\u5230\u9519\u8BEF: ${error}` + ); + return `\u5728 crawler \u51FD\u6570\u4E2D\u6355\u83B7\u5230\u9519\u8BEF: ${error}`; + } + } + async function handleRequest(request, fetchAPI, apiKey) { + console.log( + `\u5F00\u59CB\u5904\u7406\u8BF7\u6C42: ${request.method} ${request.url}` + ); + if (request.method !== "POST") { + console.log( + `\u4E0D\u652F\u6301\u7684\u8BF7\u6C42\u65B9\u6CD5: ${request.method}` + ); + return new Response("Method Not Allowed", { + status: 405, + headers: corsHeaders, + }); + } + + const requestData = await request.json(); + console.log("\u8BF7\u6C42\u6570\u636E:", requestData); + const stream = requestData.stream || false; + const userMessages = requestData.messages.filter( + (message) => message.role === "user" + ); + const latestUserMessage = userMessages[userMessages.length - 1]; + const model = requestData.model; + const isContentArray = Array.isArray(latestUserMessage.content); + const defaultMaxTokens = 3e3; + const maxTokens = requestData.max_tokens || defaultMaxTokens; + const body = JSON.stringify({ + model, + messages: requestData.messages, + max_tokens: maxTokens, + ...(isContentArray + ? {} + : { + tools: [ + { + type: "function", + function: { + name: "search", + description: "search for factors and weathers", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: "The query to search.", + }, + }, + required: ["query"], + }, + }, + }, + { + type: "function", + function: { + name: "news", + description: "Search for news", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: "The query to search for news.", + }, + }, + required: ["query"], + }, + }, + }, + { + type: "function", + function: { + name: "crawler", + description: "Get the content of a specified url", + parameters: { + type: "object", + properties: { + url: { + type: "string", + description: "The URL of the webpage", + }, + }, + required: ["url"], + }, + }, + }, + ], + tool_choice: "auto", + }), + }); + + request_header.set(`${header_auth}`, `${header_auth_val}${apiKey}`); + + if (stream) { + const openAIResponse = await fetch(fetchAPI, { + method: "POST", + headers: request_header, + body, + }); + if (openAIResponse.status !== 200) { + console.error( + `OpenAI API \u8BF7\u6C42\u5931\u8D25,\u72B6\u6001\u7801: ${openAIResponse.status}` + ); + return new Response( + `OpenAI API \u8BF7\u6C42\u5931\u8D25,\u72B6\u6001\u7801: ${openAIResponse.status}`, + { + status: 500, + headers: corsHeaders, + } + ); + } + const data = await openAIResponse.json(); + console.log( + "OpenAI API \u54CD\u5E94\u72B6\u6001\u7801:", + openAIResponse.status + ); + if (!data.choices || data.choices.length === 0) { + console.log("\u6570\u636E\u4E2D\u6CA1\u6709\u9009\u62E9\u9879"); + return new Response( + "\u6570\u636E\u4E2D\u6CA1\u6709\u9009\u62E9\u9879", + { status: 500 } + ); + } + console.log( + "OpenAI API \u54CD\u5E94\u63A5\u6536\u5B8C\u6210\uFF0C\u68C0\u67E5\u662F\u5426\u9700\u8981\u8C03\u7528\u81EA\u5B9A\u4E49\u51FD\u6570" + ); + let messages = requestData.messages; + messages.push(data.choices[0].message); + let calledCustomFunction = false; + if (data.choices[0].message.tool_calls) { + const toolCalls = data.choices[0].message.tool_calls; + const availableFunctions = { + search: search, + news: news, + crawler: crawler, + }; + for (const toolCall of toolCalls) { + const functionName = toolCall.function.name; + const functionToCall = availableFunctions[functionName]; + const functionArgs = JSON.parse(toolCall.function.arguments); + let functionResponse; + if (functionName === "search") { + functionResponse = await functionToCall(functionArgs.query); + } else if (functionName === "crawler") { + functionResponse = await functionToCall(functionArgs.url); + } else if (functionName === "news") { + functionResponse = await functionToCall(functionArgs.query); + } + messages.push({ + tool_call_id: toolCall.id, + role: "tool", + name: functionName, + content: functionResponse, + }); + if ( + functionName === "search" || + functionName === "crawler" || + functionName === "news" + ) { + calledCustomFunction = true; + } + } + } + if (calledCustomFunction) { + console.log( + "\u51C6\u5907\u53D1\u9001\u7B2C\u4E8C\u6B21 OpenAI API \u8BF7\u6C42" + ); + + const secondRequestBody = JSON.stringify({ + model, + messages, + }); + + const secondResponse = await fetch(fetchAPI, { + method: "POST", + headers: request_header, + body: secondRequestBody, + }); + + console.log("Second response status:", secondResponse.status); + console.log("Second response headers:", secondResponse.headers); + if (secondResponse.status !== 200) { + throw new Error( + `OpenAI API 第二次请求失败,状态码: ${secondResponse.status}` + ); + } + + const data = await secondResponse.json(); + const content = data.choices[0].message.content; + const words = content.split(/(\s+)/); + + const stream = new ReadableStream({ + async start(controller) { + const baseData = { + id: data.id, + object: "chat.completion.chunk", + created: data.created, + model: data.model, + system_fingerprint: data.system_fingerprint, + choices: [ + { + index: 0, + delta: {}, + logprobs: null, + finish_reason: null, + }, + ], + x_groq: { + id: data.x_groq ? data.x_groq.id : null, + }, + }; + + for (const word of words) { + const chunkData = { + ...baseData, + choices: [ + { + ...baseData.choices[0], + delta: { content: word.includes("\n") ? word : word + " " }, + }, + ], + }; + const sseMessage = `data: ${JSON.stringify(chunkData)}\n\n`; + controller.enqueue(new TextEncoder().encode(sseMessage)); + await new Promise((resolve) => setTimeout(resolve, 5)); + } + + const finalChunkData = { + ...baseData, + choices: [ + { + ...baseData.choices[0], + finish_reason: data.choices[0].finish_reason, + }, + ], + x_groq: { + ...baseData.x_groq, + usage: data.usage, + }, + }; + const finalSseMessage = `data: ${JSON.stringify( + finalChunkData + )}\n\ndata: [DONE]\n\n`; + controller.enqueue(new TextEncoder().encode(finalSseMessage)); + controller.close(); + }, + }); + + return new Response(stream, { + status: 200, + headers: { + "Content-Type": "text/event-stream", + ...corsHeaders, + }, + }); + } else { + const content = data.choices[0].message.content; + const words = content.split(/(\s+)/); + + const stream = new ReadableStream({ + async start(controller) { + const baseData = { + id: data.id, + object: "chat.completion.chunk", + created: data.created, + model: data.model, + system_fingerprint: data.system_fingerprint, + choices: [ + { + index: 0, + delta: {}, + logprobs: null, + finish_reason: null, + }, + ], + x_groq: { + id: data.x_groq ? data.x_groq.id : null, + }, + }; + + for (const word of words) { + const chunkData = { + ...baseData, + choices: [ + { + ...baseData.choices[0], + delta: { content: word.includes("\n") ? word : word + " " }, + }, + ], + }; + const sseMessage = `data: ${JSON.stringify(chunkData)}\n\n`; + controller.enqueue(new TextEncoder().encode(sseMessage)); + await new Promise((resolve) => setTimeout(resolve, 5)); + } + + const finalChunkData = { + ...baseData, + choices: [ + { + ...baseData.choices[0], + finish_reason: data.choices[0].finish_reason, + }, + ], + x_groq: { + ...baseData.x_groq, + usage: data.usage, + }, + }; + const finalSseMessage = `data: ${JSON.stringify( + finalChunkData + )}\n\ndata: [DONE]\n\n`; + controller.enqueue(new TextEncoder().encode(finalSseMessage)); + controller.close(); + }, + }); + + return new Response(stream, { + status: 200, + headers: { + "Content-Type": "text/event-stream", + ...corsHeaders, + }, + }); + } + } else { + const openAIResponse = await fetch(fetchAPI, { + method: "POST", + headers: request_header, + body, + }); + if (openAIResponse.status !== 200) { + console.error( + `OpenAI API \u8BF7\u6C42\u5931\u8D25,\u72B6\u6001\u7801: ${openAIResponse.status}` + ); + return new Response( + `OpenAI API \u8BF7\u6C42\u5931\u8D25,\u72B6\u6001\u7801: ${openAIResponse.status}`, + { + status: 500, + headers: corsHeaders, + } + ); + } + const data = await openAIResponse.json(); + console.log( + "OpenAI API \u54CD\u5E94\u72B6\u6001\u7801:", + openAIResponse.status + ); + if (!data.choices || data.choices.length === 0) { + console.log("\u6570\u636E\u4E2D\u6CA1\u6709\u9009\u62E9\u9879"); + return new Response( + "\u6570\u636E\u4E2D\u6CA1\u6709\u9009\u62E9\u9879", + { status: 500 } + ); + } + console.log( + "OpenAI API \u54CD\u5E94\u63A5\u6536\u5B8C\u6210\uFF0C\u68C0\u67E5\u662F\u5426\u9700\u8981\u8C03\u7528\u81EA\u5B9A\u4E49\u51FD\u6570" + ); + let messages = requestData.messages; + messages.push(data.choices[0].message); + let calledCustomFunction = false; + if (data.choices[0].message.tool_calls) { + const toolCalls = data.choices[0].message.tool_calls; + const availableFunctions = { + search: search, + news: news, + crawler: crawler, + }; + for (const toolCall of toolCalls) { + const functionName = toolCall.function.name; + const functionToCall = availableFunctions[functionName]; + const functionArgs = JSON.parse(toolCall.function.arguments); + let functionResponse; + if (functionName === "search") { + functionResponse = await functionToCall(functionArgs.query); + } else if (functionName === "crawler") { + functionResponse = await functionToCall(functionArgs.url); + } else if (functionName === "news") { + functionResponse = await functionToCall(functionArgs.query); + } + messages.push({ + tool_call_id: toolCall.id, + role: "tool", + name: functionName, + content: functionResponse, + }); + if ( + functionName === "search" || + functionName === "crawler" || + functionName === "news" + ) { + calledCustomFunction = true; + } + } + } + if (calledCustomFunction) { + console.log( + "\u51C6\u5907\u53D1\u9001\u7B2C\u4E8C\u6B21 OpenAI API \u8BF7\u6C42" + ); + + const requestBody = { + model, + messages, + }; + const secondResponse = await fetch(fetchAPI, { + method: "POST", + headers: request_header, + body: JSON.stringify(requestBody), + }); + console.log("\u54CD\u5E94\u72B6\u6001\u7801: 200"); + const data2 = await secondResponse.json(); + return new Response(JSON.stringify(data2), { + status: 200, + headers: { + "Content-Type": "application/json", + ...corsHeaders, + }, + }); + } else { + console.log("\u54CD\u5E94\u72B6\u6001\u7801: 200"); + return new Response(JSON.stringify(data), { + status: 200, + headers: { + "Content-Type": "application/json", + ...corsHeaders, + }, + }); + } + } + } +})(); +//# sourceMappingURL=openai.js.map diff --git a/search2openai.js b/search2openai.js index fafcb7d..e2ec68b 100644 --- a/search2openai.js +++ b/search2openai.js @@ -205,7 +205,7 @@ })); break; - case "searXNG": + case "searxng": const searXNGUrl = `${SEARXNG_BASE_URL}/search?q=${encodeURIComponent( query )}&category=general&format=json`; @@ -334,7 +334,7 @@ console.log(`搜索结果: ${JSON.stringify(results)}`); })); break; - case "searXNG": + case "searxng": const searXNGUrl = `${SEARXNG_BASE_URL}/search?q=${encodeURIComponent( query )}&category=news&format=json`; diff --git a/units/news.js b/units/news.js index e158db8..decdbf3 100644 --- a/units/news.js +++ b/units/news.js @@ -102,7 +102,7 @@ async function news(query) { })); break; - case "searXNG": + case "searxng": const searXNGUrl = `${process.env.SEARXNG_BASE_URL}/search?q=${encodeURIComponent( query )}&category=news&format=json`; diff --git a/units/search.js b/units/search.js index c2a54c7..d7e3347 100644 --- a/units/search.js +++ b/units/search.js @@ -102,7 +102,7 @@ async function search(query) { })); break; - case "searXNG": + case "searxng": const searXNGUrl = `${process.env.SEARXNG_BASE_URL}/search?q=${encodeURIComponent( query )}&category=general&format=json`;