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 @@
# 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 @@
# 版本更新
-
+- 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`;