plugin add auth config

This commit is contained in:
lloydzhou 2024-09-02 18:11:19 +08:00
parent b2965e1deb
commit f652f73260
8 changed files with 237 additions and 51 deletions

View File

@ -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 });
}
}

75
app/api/proxy.ts Normal file
View File

@ -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);
}
}

View File

@ -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={[
<IconButton
icon={<DownloadIcon />}
text={Locale.Plugin.EditModal.Download}
icon={<ConfirmIcon />}
text={Locale.UI.Confirm}
key="export"
bordered
onClick={() =>
downloadAs(
JSON.stringify(editingPlugin),
`${editingPlugin.title}@${editingPlugin.version}.json`,
)
}
onClick={() => setEditingPluginId("")}
/>,
]}
>
<div className={styles["mask-page"]}>
<div className={pluginStyles["plugin-title"]}>
{Locale.Plugin.EditModal.Content}
</div>
<div
className={`markdown-body ${pluginStyles["plugin-content"]}`}
dir="auto"
<List>
<ListItem title={Locale.Plugin.EditModal.Auth}>
<select
value={editingPlugin?.authType}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.authType = e.target.value;
});
}}
>
<option value="">{Locale.Plugin.Auth.None}</option>
<option value="bearer">{Locale.Plugin.Auth.Bearer}</option>
<option value="basic">{Locale.Plugin.Auth.Basic}</option>
<option value="custom">{Locale.Plugin.Auth.Custom}</option>
</select>
</ListItem>
{editingPlugin.authType == "custom" && (
<ListItem title={Locale.Plugin.Auth.CustomHeader}>
<input
type="text"
value={editingPlugin?.authHeader}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.authHeader = e.target.value;
});
}}
></input>
</ListItem>
)}
{["bearer", "basic", "custom"].includes(
editingPlugin.authType as string,
) && (
<ListItem title={Locale.Plugin.Auth.Token}>
<PasswordInput
type="text"
value={editingPlugin?.authToken}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.authToken = e.currentTarget.value;
});
}}
></PasswordInput>
</ListItem>
)}
<ListItem
title={Locale.Plugin.Auth.Proxy}
subTitle={Locale.Plugin.Auth.ProxyDescription}
>
<pre>
<code
contentEditable={true}
dangerouslySetInnerHTML={{ __html: editingPlugin.content }}
onBlur={onChangePlugin}
></code>
</pre>
</div>
<div className={pluginStyles["plugin-title"]}>
{Locale.Plugin.EditModal.Method}
</div>
<div className={styles["mask-page-body"]} style={{ padding: 0 }}>
{editingPluginTool?.tools.map((tool, index) => (
<div className={styles["mask-item"]} key={index}>
<div className={styles["mask-header"]}>
<div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>
{tool?.function?.name}
</div>
<div className={styles["mask-info"] + " one-line"}>
{tool?.function?.description}
</div>
</div>
</div>
<input
type="checkbox"
checked={editingPlugin?.usingProxy}
style={{ minWidth: 16 }}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.usingProxy = e.currentTarget.checked;
});
}}
></input>
</ListItem>
</List>
<List>
<ListItem
title={Locale.Plugin.EditModal.Content}
subTitle={
<div
className={`markdown-body ${pluginStyles["plugin-content"]}`}
dir="auto"
>
<pre>
<code
contentEditable={true}
dangerouslySetInnerHTML={{
__html: editingPlugin.content,
}}
onBlur={onChangePlugin}
></code>
</pre>
</div>
))}
</div>
</div>
}
></ListItem>
{editingPluginTool?.tools.map((tool, index) => (
<ListItem
key={index}
title={tool?.function?.name}
subTitle={tool?.function?.description}
/>
))}
</List>
</Modal>
</div>
)}

View File

@ -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;

View File

@ -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: "格式错误",

View File

@ -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",

View File

@ -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<string, FunctionToolServiceItem>,
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();
},

View File

@ -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*",