├── .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 |
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 | [](https://zeabur.com/templates/A4HGYF?referralCode=fatwang2)
64 |
65 | 如需保持项目更新,建议先fork本仓库,再通过Zeabur部署你的分支
66 |
67 | [](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 | 
95 | 3. worker里配置触发器-自定义域名,国内直接访问worker的地址可能会出问题,需要替换为自定义域名
96 | 
97 |
98 | **Vercel部署**
99 |
100 | 特别说明:vercel项目暂不支持流式输出,且有10s响应限制,实际使用体验不佳,放出来主要是想等大神给我pull request
101 |
102 | 一键部署
103 |
104 | [](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 |
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 | 
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 | [](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 | [](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 | 
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 | 
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 | [](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 |
--------------------------------------------------------------------------------