فهرست منبع

plugin add auth config

lloydzhou 1 سال پیش
والد
کامیت
f652f73260
8فایلهای تغییر یافته به همراه237 افزوده شده و 51 حذف شده
  1. 5 1
      app/api/[provider]/[...path]/route.ts
  2. 75 0
      app/api/proxy.ts
  3. 91 42
      app/components/plugin.tsx
  4. 1 1
      app/components/ui-lib.tsx
  5. 11 0
      app/locales/cn.ts
  6. 11 0
      app/locales/en.ts
  7. 43 3
      app/store/plugin.ts
  8. 0 4
      next.config.mjs

+ 5 - 1
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 });
   }
 }
 

+ 75 - 0
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);
+  }
+}

+ 91 - 42
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={[
               <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>
       )}

+ 1 - 1
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;

+ 11 - 0
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: "格式错误",

+ 11 - 0
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",

+ 43 - 3
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<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();
     },

+ 0 - 4
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*",