mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-05-19 04:00:16 +09:00
plugin add auth config
This commit is contained in:
parent
b2965e1deb
commit
f652f73260
@ -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
75
app/api/proxy.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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>
|
||||
)}
|
||||
|
@ -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;
|
||||
|
@ -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: "格式错误",
|
||||
|
@ -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",
|
||||
|
@ -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();
|
||||
},
|
||||
|
@ -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*",
|
||||
|
Loading…
Reference in New Issue
Block a user