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 moonshotHandler } from "../../moonshot";
|
||||||
import { handle as stabilityHandler } from "../../stability";
|
import { handle as stabilityHandler } from "../../stability";
|
||||||
import { handle as iflytekHandler } from "../../iflytek";
|
import { handle as iflytekHandler } from "../../iflytek";
|
||||||
|
import { handle as proxyHandler } from "../../proxy";
|
||||||
|
|
||||||
async function handle(
|
async function handle(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: { provider: string; path: string[] } },
|
{ params }: { params: { provider: string; path: string[] } },
|
||||||
@ -36,8 +38,10 @@ async function handle(
|
|||||||
return stabilityHandler(req, { params });
|
return stabilityHandler(req, { params });
|
||||||
case ApiPath.Iflytek:
|
case ApiPath.Iflytek:
|
||||||
return iflytekHandler(req, { params });
|
return iflytekHandler(req, { params });
|
||||||
default:
|
case ApiPath.OpenAI:
|
||||||
return openaiHandler(req, { params });
|
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 DeleteIcon from "../icons/delete.svg";
|
||||||
import EyeIcon from "../icons/eye.svg";
|
import EyeIcon from "../icons/eye.svg";
|
||||||
import CopyIcon from "../icons/copy.svg";
|
import CopyIcon from "../icons/copy.svg";
|
||||||
|
import ConfirmIcon from "../icons/confirm.svg";
|
||||||
|
|
||||||
import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
|
import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
|
||||||
import {
|
import {
|
||||||
Input,
|
Input,
|
||||||
|
PasswordInput,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
Modal,
|
Modal,
|
||||||
@ -191,55 +193,102 @@ export function PluginPage() {
|
|||||||
onClose={closePluginModal}
|
onClose={closePluginModal}
|
||||||
actions={[
|
actions={[
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<DownloadIcon />}
|
icon={<ConfirmIcon />}
|
||||||
text={Locale.Plugin.EditModal.Download}
|
text={Locale.UI.Confirm}
|
||||||
key="export"
|
key="export"
|
||||||
bordered
|
bordered
|
||||||
onClick={() =>
|
onClick={() => setEditingPluginId("")}
|
||||||
downloadAs(
|
|
||||||
JSON.stringify(editingPlugin),
|
|
||||||
`${editingPlugin.title}@${editingPlugin.version}.json`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>,
|
/>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<div className={styles["mask-page"]}>
|
<List>
|
||||||
<div className={pluginStyles["plugin-title"]}>
|
<ListItem title={Locale.Plugin.EditModal.Auth}>
|
||||||
{Locale.Plugin.EditModal.Content}
|
<select
|
||||||
</div>
|
value={editingPlugin?.authType}
|
||||||
<div
|
onChange={(e) => {
|
||||||
className={`markdown-body ${pluginStyles["plugin-content"]}`}
|
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
|
||||||
dir="auto"
|
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>
|
<input
|
||||||
<code
|
type="checkbox"
|
||||||
contentEditable={true}
|
checked={editingPlugin?.usingProxy}
|
||||||
dangerouslySetInnerHTML={{ __html: editingPlugin.content }}
|
style={{ minWidth: 16 }}
|
||||||
onBlur={onChangePlugin}
|
onChange={(e) => {
|
||||||
></code>
|
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
|
||||||
</pre>
|
plugin.usingProxy = e.currentTarget.checked;
|
||||||
</div>
|
});
|
||||||
<div className={pluginStyles["plugin-title"]}>
|
}}
|
||||||
{Locale.Plugin.EditModal.Method}
|
></input>
|
||||||
</div>
|
</ListItem>
|
||||||
<div className={styles["mask-page-body"]} style={{ padding: 0 }}>
|
</List>
|
||||||
{editingPluginTool?.tools.map((tool, index) => (
|
<List>
|
||||||
<div className={styles["mask-item"]} key={index}>
|
<ListItem
|
||||||
<div className={styles["mask-header"]}>
|
title={Locale.Plugin.EditModal.Content}
|
||||||
<div className={styles["mask-title"]}>
|
subTitle={
|
||||||
<div className={styles["mask-name"]}>
|
<div
|
||||||
{tool?.function?.name}
|
className={`markdown-body ${pluginStyles["plugin-content"]}`}
|
||||||
</div>
|
dir="auto"
|
||||||
<div className={styles["mask-info"] + " one-line"}>
|
>
|
||||||
{tool?.function?.description}
|
<pre>
|
||||||
</div>
|
<code
|
||||||
</div>
|
contentEditable={true}
|
||||||
</div>
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: editingPlugin.content,
|
||||||
|
}}
|
||||||
|
onBlur={onChangePlugin}
|
||||||
|
></code>
|
||||||
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
))}
|
}
|
||||||
</div>
|
></ListItem>
|
||||||
</div>
|
{editingPluginTool?.tools.map((tool, index) => (
|
||||||
|
<ListItem
|
||||||
|
key={index}
|
||||||
|
title={tool?.function?.name}
|
||||||
|
subTitle={tool?.function?.description}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -51,7 +51,7 @@ export function Card(props: { children: JSX.Element[]; className?: string }) {
|
|||||||
|
|
||||||
export function ListItem(props: {
|
export function ListItem(props: {
|
||||||
title: string;
|
title: string;
|
||||||
subTitle?: string;
|
subTitle?: string | JSX.Element;
|
||||||
children?: JSX.Element | JSX.Element[];
|
children?: JSX.Element | JSX.Element[];
|
||||||
icon?: JSX.Element;
|
icon?: JSX.Element;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
@ -546,9 +546,20 @@ const cn = {
|
|||||||
Delete: "删除",
|
Delete: "删除",
|
||||||
DeleteConfirm: "确认删除?",
|
DeleteConfirm: "确认删除?",
|
||||||
},
|
},
|
||||||
|
Auth: {
|
||||||
|
None: "不需要授权",
|
||||||
|
Basic: "Basic",
|
||||||
|
Bearer: "Bearer",
|
||||||
|
Custom: "自定义",
|
||||||
|
CustomHeader: "自定义头",
|
||||||
|
Token: "Token",
|
||||||
|
Proxy: "使用代理",
|
||||||
|
ProxyDescription: "使用代理解决 CORS 错误",
|
||||||
|
},
|
||||||
EditModal: {
|
EditModal: {
|
||||||
Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`,
|
Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`,
|
||||||
Download: "下载",
|
Download: "下载",
|
||||||
|
Auth: "授权方式",
|
||||||
Content: "OpenAPI Schema",
|
Content: "OpenAPI Schema",
|
||||||
Method: "方法",
|
Method: "方法",
|
||||||
Error: "格式错误",
|
Error: "格式错误",
|
||||||
|
@ -554,10 +554,21 @@ const en: LocaleType = {
|
|||||||
Delete: "Delete",
|
Delete: "Delete",
|
||||||
DeleteConfirm: "Confirm to 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: {
|
EditModal: {
|
||||||
Title: (readonly: boolean) =>
|
Title: (readonly: boolean) =>
|
||||||
`Edit Plugin ${readonly ? "(readonly)" : ""}`,
|
`Edit Plugin ${readonly ? "(readonly)" : ""}`,
|
||||||
Download: "Download",
|
Download: "Download",
|
||||||
|
Auth: "Authentication Type",
|
||||||
Content: "OpenAPI Schema",
|
Content: "OpenAPI Schema",
|
||||||
Method: "Method",
|
Method: "Method",
|
||||||
Error: "OpenAPI Schema Error",
|
Error: "OpenAPI Schema Error",
|
||||||
|
@ -12,6 +12,10 @@ export type Plugin = {
|
|||||||
version: string;
|
version: string;
|
||||||
content: string;
|
content: string;
|
||||||
builtin: boolean;
|
builtin: boolean;
|
||||||
|
authType?: string;
|
||||||
|
authHeader?: string;
|
||||||
|
authToken?: string;
|
||||||
|
usingProxy?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FunctionToolItem = {
|
export type FunctionToolItem = {
|
||||||
@ -34,10 +38,30 @@ export const FunctionToolService = {
|
|||||||
tools: {} as Record<string, FunctionToolServiceItem>,
|
tools: {} as Record<string, FunctionToolServiceItem>,
|
||||||
add(plugin: Plugin, replace = false) {
|
add(plugin: Plugin, replace = false) {
|
||||||
if (!replace && this.tools[plugin.id]) return this.tools[plugin.id];
|
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({
|
const api = new OpenAPIClientAxios({
|
||||||
definition: yaml.load(plugin.content) as any,
|
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 {
|
try {
|
||||||
api.initSync();
|
api.initSync();
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
@ -79,14 +103,29 @@ export const FunctionToolService = {
|
|||||||
type: "function",
|
type: "function",
|
||||||
function: {
|
function: {
|
||||||
name: o.operationId,
|
name: o.operationId,
|
||||||
description: o.description,
|
description: o.description || o.summary,
|
||||||
parameters: parameters,
|
parameters: parameters,
|
||||||
},
|
},
|
||||||
} as FunctionToolItem;
|
} as FunctionToolItem;
|
||||||
}),
|
}),
|
||||||
funcs: operations.reduce((s, o) => {
|
funcs: operations.reduce((s, o) => {
|
||||||
// @ts-ignore
|
// @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;
|
return s;
|
||||||
}, {}),
|
}, {}),
|
||||||
});
|
});
|
||||||
@ -136,6 +175,7 @@ export const usePluginStore = createPersistStore(
|
|||||||
const updatePlugin = { ...plugin };
|
const updatePlugin = { ...plugin };
|
||||||
updater(updatePlugin);
|
updater(updatePlugin);
|
||||||
plugins[id] = updatePlugin;
|
plugins[id] = updatePlugin;
|
||||||
|
FunctionToolService.add(updatePlugin, true);
|
||||||
set(() => ({ plugins }));
|
set(() => ({ plugins }));
|
||||||
get().markUpdate();
|
get().markUpdate();
|
||||||
},
|
},
|
||||||
|
@ -86,10 +86,6 @@ if (mode !== "export") {
|
|||||||
source: "/api/proxy/anthropic/:path*",
|
source: "/api/proxy/anthropic/:path*",
|
||||||
destination: "https://api.anthropic.com/:path*",
|
destination: "https://api.anthropic.com/:path*",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
source: "/api/proxy/gapier/:path*",
|
|
||||||
destination: "https://a.gapier.com/:path*",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
source: "/google-fonts/:path*",
|
source: "/google-fonts/:path*",
|
||||||
destination: "https://fonts.googleapis.com/:path*",
|
destination: "https://fonts.googleapis.com/:path*",
|
||||||
|
Loading…
Reference in New Issue
Block a user