diff --git a/app/api/[provider]/[...path]/route.ts b/app/api/[provider]/[...path]/route.ts
index 06e3e5160..24aa5ec04 100644
--- a/app/api/[provider]/[...path]/route.ts
+++ b/app/api/[provider]/[...path]/route.ts
@@ -10,6 +10,8 @@ import { handle as alibabaHandler } from "../../alibaba";
import { handle as moonshotHandler } from "../../moonshot";
import { handle as stabilityHandler } from "../../stability";
import { handle as iflytekHandler } from "../../iflytek";
+import { handle as proxyHandler } from "../../proxy";
+
async function handle(
req: NextRequest,
{ params }: { params: { provider: string; path: string[] } },
@@ -36,8 +38,10 @@ async function handle(
return stabilityHandler(req, { params });
case ApiPath.Iflytek:
return iflytekHandler(req, { params });
- default:
+ case ApiPath.OpenAI:
return openaiHandler(req, { params });
+ default:
+ return proxyHandler(req, { params });
}
}
diff --git a/app/api/proxy.ts b/app/api/proxy.ts
new file mode 100644
index 000000000..731003aa1
--- /dev/null
+++ b/app/api/proxy.ts
@@ -0,0 +1,75 @@
+import { NextRequest, NextResponse } from "next/server";
+
+export async function handle(
+ req: NextRequest,
+ { params }: { params: { path: string[] } },
+) {
+ console.log("[Proxy Route] params ", params);
+
+ if (req.method === "OPTIONS") {
+ return NextResponse.json({ body: "OK" }, { status: 200 });
+ }
+
+ // remove path params from searchParams
+ req.nextUrl.searchParams.delete("path");
+ req.nextUrl.searchParams.delete("provider");
+
+ const subpath = params.path.join("/");
+ const fetchUrl = `${req.headers.get(
+ "x-base-url",
+ )}/${subpath}?${req.nextUrl.searchParams.toString()}`;
+ const skipHeaders = ["connection", "host", "origin", "referer", "cookie"];
+ const headers = new Headers(
+ Array.from(req.headers.entries()).filter((item) => {
+ if (
+ item[0].indexOf("x-") > -1 ||
+ item[0].indexOf("sec-") > -1 ||
+ skipHeaders.includes(item[0])
+ ) {
+ return false;
+ }
+ return true;
+ }),
+ );
+ const controller = new AbortController();
+ const fetchOptions: RequestInit = {
+ headers,
+ method: req.method,
+ body: req.body,
+ // to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
+ redirect: "manual",
+ // @ts-ignore
+ duplex: "half",
+ signal: controller.signal,
+ };
+
+ const timeoutId = setTimeout(
+ () => {
+ controller.abort();
+ },
+ 10 * 60 * 1000,
+ );
+
+ try {
+ const res = await fetch(fetchUrl, fetchOptions);
+ // to prevent browser prompt for credentials
+ const newHeaders = new Headers(res.headers);
+ newHeaders.delete("www-authenticate");
+ // to disable nginx buffering
+ newHeaders.set("X-Accel-Buffering", "no");
+
+ // The latest version of the OpenAI API forced the content-encoding to be "br" in json response
+ // So if the streaming is disabled, we need to remove the content-encoding header
+ // Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
+ // The browser will try to decode the response with brotli and fail
+ newHeaders.delete("content-encoding");
+
+ return new Response(res.body, {
+ status: res.status,
+ statusText: res.statusText,
+ headers: newHeaders,
+ });
+ } finally {
+ clearTimeout(timeoutId);
+ }
+}
diff --git a/app/components/plugin.tsx b/app/components/plugin.tsx
index 35cda9089..5aa66ce94 100644
--- a/app/components/plugin.tsx
+++ b/app/components/plugin.tsx
@@ -14,10 +14,12 @@ import CloseIcon from "../icons/close.svg";
import DeleteIcon from "../icons/delete.svg";
import EyeIcon from "../icons/eye.svg";
import CopyIcon from "../icons/copy.svg";
+import ConfirmIcon from "../icons/confirm.svg";
import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
import {
Input,
+ PasswordInput,
List,
ListItem,
Modal,
@@ -191,55 +193,102 @@ export function PluginPage() {
onClose={closePluginModal}
actions={[
}
- text={Locale.Plugin.EditModal.Download}
+ icon={}
+ text={Locale.UI.Confirm}
key="export"
bordered
- onClick={() =>
- downloadAs(
- JSON.stringify(editingPlugin),
- `${editingPlugin.title}@${editingPlugin.version}.json`,
- )
- }
+ onClick={() => setEditingPluginId("")}
/>,
]}
>
-
-
- {Locale.Plugin.EditModal.Content}
-
-
+
+
+
+ {editingPlugin.authType == "custom" && (
+
+ {
+ pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+ plugin.authHeader = e.target.value;
+ });
+ }}
+ >
+
+ )}
+ {["bearer", "basic", "custom"].includes(
+ editingPlugin.authType as string,
+ ) && (
+
+ {
+ pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+ plugin.authToken = e.currentTarget.value;
+ });
+ }}
+ >
+
+ )}
+
-
-
-
-
-
- {Locale.Plugin.EditModal.Method}
-
-
- {editingPluginTool?.tools.map((tool, index) => (
-
-
-
-
- {tool?.function?.name}
-
-
- {tool?.function?.description}
-
-
-
+
{
+ pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
+ plugin.usingProxy = e.currentTarget.checked;
+ });
+ }}
+ >
+
+
+
+
+
+
+
- ))}
-
-
+ }
+ >
+ {editingPluginTool?.tools.map((tool, index) => (
+
+ ))}
+
)}
diff --git a/app/components/ui-lib.tsx b/app/components/ui-lib.tsx
index fd78f9c47..828c9a27d 100644
--- a/app/components/ui-lib.tsx
+++ b/app/components/ui-lib.tsx
@@ -51,7 +51,7 @@ export function Card(props: { children: JSX.Element[]; className?: string }) {
export function ListItem(props: {
title: string;
- subTitle?: string;
+ subTitle?: string | JSX.Element;
children?: JSX.Element | JSX.Element[];
icon?: JSX.Element;
className?: string;
diff --git a/app/locales/cn.ts b/app/locales/cn.ts
index f0ff705c1..af600564e 100644
--- a/app/locales/cn.ts
+++ b/app/locales/cn.ts
@@ -546,9 +546,20 @@ const cn = {
Delete: "删除",
DeleteConfirm: "确认删除?",
},
+ Auth: {
+ None: "不需要授权",
+ Basic: "Basic",
+ Bearer: "Bearer",
+ Custom: "自定义",
+ CustomHeader: "自定义头",
+ Token: "Token",
+ Proxy: "使用代理",
+ ProxyDescription: "使用代理解决 CORS 错误",
+ },
EditModal: {
Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`,
Download: "下载",
+ Auth: "授权方式",
Content: "OpenAPI Schema",
Method: "方法",
Error: "格式错误",
diff --git a/app/locales/en.ts b/app/locales/en.ts
index 15db8190a..59a284006 100644
--- a/app/locales/en.ts
+++ b/app/locales/en.ts
@@ -554,10 +554,21 @@ const en: LocaleType = {
Delete: "Delete",
DeleteConfirm: "Confirm to delete?",
},
+ Auth: {
+ None: "None",
+ Basic: "Basic",
+ Bearer: "Bearer",
+ Custom: "Custom",
+ CustomHeader: "Custom Header",
+ Token: "Token",
+ Proxy: "Using Proxy",
+ ProxyDescription: "Using proxies to solve CORS error",
+ },
EditModal: {
Title: (readonly: boolean) =>
`Edit Plugin ${readonly ? "(readonly)" : ""}`,
Download: "Download",
+ Auth: "Authentication Type",
Content: "OpenAPI Schema",
Method: "Method",
Error: "OpenAPI Schema Error",
diff --git a/app/store/plugin.ts b/app/store/plugin.ts
index 031a2aaf5..d7b0de255 100644
--- a/app/store/plugin.ts
+++ b/app/store/plugin.ts
@@ -12,6 +12,10 @@ export type Plugin = {
version: string;
content: string;
builtin: boolean;
+ authType?: string;
+ authHeader?: string;
+ authToken?: string;
+ usingProxy?: boolean;
};
export type FunctionToolItem = {
@@ -34,10 +38,30 @@ export const FunctionToolService = {
tools: {} as Record,
add(plugin: Plugin, replace = false) {
if (!replace && this.tools[plugin.id]) return this.tools[plugin.id];
+ const headerName = (
+ plugin?.authType == "custom" ? plugin?.authHeader : "Authorization"
+ ) as string;
+ const tokenValue =
+ plugin?.authType == "basic"
+ ? `Basic ${plugin?.authToken}`
+ : plugin?.authType == "bearer"
+ ? ` Bearer ${plugin?.authToken}`
+ : plugin?.authToken;
+ const definition = yaml.load(plugin.content) as any;
+ const serverURL = definition.servers?.[0]?.url;
+ const baseURL = !!plugin?.usingProxy ? "/api/proxy" : serverURL;
const api = new OpenAPIClientAxios({
definition: yaml.load(plugin.content) as any,
+ axiosConfigDefaults: {
+ baseURL,
+ headers: {
+ // 'Cache-Control': 'no-cache',
+ // 'Content-Type': 'application/json', // TODO
+ [headerName]: tokenValue,
+ "X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined,
+ },
+ },
});
- console.log("add", plugin, api);
try {
api.initSync();
} catch (e) {}
@@ -79,14 +103,29 @@ export const FunctionToolService = {
type: "function",
function: {
name: o.operationId,
- description: o.description,
+ description: o.description || o.summary,
parameters: parameters,
},
} as FunctionToolItem;
}),
funcs: operations.reduce((s, o) => {
// @ts-ignore
- s[o.operationId] = api.client[o.operationId];
+ s[o.operationId] = function (args) {
+ const argument = [];
+ if (o.parameters instanceof Array) {
+ o.parameters.forEach((p) => {
+ // @ts-ignore
+ argument.push(args[p?.name]);
+ // @ts-ignore
+ delete args[p?.name];
+ });
+ } else {
+ argument.push(null);
+ }
+ argument.push(args);
+ // @ts-ignore
+ return api.client[o.operationId].apply(null, argument);
+ };
return s;
}, {}),
});
@@ -136,6 +175,7 @@ export const usePluginStore = createPersistStore(
const updatePlugin = { ...plugin };
updater(updatePlugin);
plugins[id] = updatePlugin;
+ FunctionToolService.add(updatePlugin, true);
set(() => ({ plugins }));
get().markUpdate();
},
diff --git a/next.config.mjs b/next.config.mjs
index 6b1dd0c35..27c60dd29 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -86,10 +86,6 @@ if (mode !== "export") {
source: "/api/proxy/anthropic/:path*",
destination: "https://api.anthropic.com/:path*",
},
- {
- source: "/api/proxy/gapier/:path*",
- destination: "https://a.gapier.com/:path*",
- },
{
source: "/google-fonts/:path*",
destination: "https://fonts.googleapis.com/:path*",