diff --git a/.gitignore b/.gitignore index a24c6e047..2ff556f64 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ dev *.key *.key.pub + +masks.json diff --git a/README.md b/README.md index 472102cdc..290a7f6ac 100644 --- a/README.md +++ b/README.md @@ -326,6 +326,14 @@ You can use this option if you want to increase the number of webdav service add Customize the default template used to initialize the User Input Preprocessing configuration item in Settings. +### `STABILITY_API_KEY` (optional) + +Stability API key. + +### `STABILITY_URL` (optional) + +Customize Stability API url. + ## Requirements NodeJS >= 18, Docker >= 20 diff --git a/README_CN.md b/README_CN.md index e42288bb5..8c464dc09 100644 --- a/README_CN.md +++ b/README_CN.md @@ -218,6 +218,15 @@ ByteDance Api Url. 自定义默认的 template,用于初始化『设置』中的『用户输入预处理』配置项 +### `STABILITY_API_KEY` (optional) + +Stability API密钥 + +### `STABILITY_URL` (optional) + +自定义的Stability API请求地址 + + ## 开发 点击下方按钮,开始二次开发: diff --git a/app/api/auth.ts b/app/api/auth.ts index e3b88702e..2913a1477 100644 --- a/app/api/auth.ts +++ b/app/api/auth.ts @@ -67,6 +67,9 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) { let systemApiKey: string | undefined; switch (modelProvider) { + case ModelProvider.Stability: + systemApiKey = serverConfig.stabilityApiKey; + break; case ModelProvider.GeminiPro: systemApiKey = serverConfig.googleApiKey; break; diff --git a/app/api/stability/[...path]/route.ts b/app/api/stability/[...path]/route.ts new file mode 100644 index 000000000..4b2bcc305 --- /dev/null +++ b/app/api/stability/[...path]/route.ts @@ -0,0 +1,104 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getServerSideConfig } from "@/app/config/server"; +import { ModelProvider, STABILITY_BASE_URL } from "@/app/constant"; +import { auth } from "@/app/api/auth"; + +async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + console.log("[Stability] params ", params); + + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + + const controller = new AbortController(); + + const serverConfig = getServerSideConfig(); + + let baseUrl = serverConfig.stabilityUrl || STABILITY_BASE_URL; + + if (!baseUrl.startsWith("http")) { + baseUrl = `https://${baseUrl}`; + } + + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.slice(0, -1); + } + + let path = `${req.nextUrl.pathname}`.replaceAll("/api/stability/", ""); + + console.log("[Stability Proxy] ", path); + console.log("[Stability Base Url]", baseUrl); + + const timeoutId = setTimeout( + () => { + controller.abort(); + }, + 10 * 60 * 1000, + ); + + const authResult = auth(req, ModelProvider.Stability); + + if (authResult.error) { + return NextResponse.json(authResult, { + status: 401, + }); + } + + const bearToken = req.headers.get("Authorization") ?? ""; + const token = bearToken.trim().replaceAll("Bearer ", "").trim(); + + const key = token ? token : serverConfig.stabilityApiKey; + + if (!key) { + return NextResponse.json( + { + error: true, + message: `missing STABILITY_API_KEY in server env vars`, + }, + { + status: 401, + }, + ); + } + + const fetchUrl = `${baseUrl}/${path}`; + console.log("[Stability Url] ", fetchUrl); + const fetchOptions: RequestInit = { + headers: { + "Content-Type": req.headers.get("Content-Type") || "multipart/form-data", + Accept: req.headers.get("Accept") || "application/json", + Authorization: `Bearer ${key}`, + }, + 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, + }; + + 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"); + return new Response(res.body, { + status: res.status, + statusText: res.statusText, + headers: newHeaders, + }); + } finally { + clearTimeout(timeoutId); + } +} + +export const GET = handle; +export const POST = handle; + +export const runtime = "edge"; diff --git a/app/api/webdav/[...path]/route.ts b/app/api/webdav/[...path]/route.ts index 01286fc1b..1f58a884f 100644 --- a/app/api/webdav/[...path]/route.ts +++ b/app/api/webdav/[...path]/route.ts @@ -37,9 +37,13 @@ async function handle( const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint); const normalizedEndpoint = normalizeUrl(endpoint as string); - return normalizedEndpoint && + return ( + normalizedEndpoint && normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname && - normalizedEndpoint.pathname.startsWith(normalizedAllowedEndpoint.pathname); + normalizedEndpoint.pathname.startsWith( + normalizedAllowedEndpoint.pathname, + ) + ); }) ) { return NextResponse.json( diff --git a/app/client/api.ts b/app/client/api.ts index c0c71480c..102a4220f 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -168,6 +168,19 @@ export class ClientApi { } } +export function getBearerToken( + apiKey: string, + noBearer: boolean = false, +): string { + return validString(apiKey) + ? `${noBearer ? "" : "Bearer "}${apiKey.trim()}` + : ""; +} + +export function validString(x: string): boolean { + return x?.length > 0; +} + export function getHeaders() { const accessStore = useAccessStore.getState(); const chatStore = useChatStore.getState(); @@ -214,15 +227,6 @@ export function getHeaders() { return isAzure ? "api-key" : isAnthropic ? "x-api-key" : "Authorization"; } - function getBearerToken(apiKey: string, noBearer: boolean = false): string { - return validString(apiKey) - ? `${noBearer ? "" : "Bearer "}${apiKey.trim()}` - : ""; - } - - function validString(x: string): boolean { - return x?.length > 0; - } const { isGoogle, isAzure, diff --git a/app/components/button.tsx b/app/components/button.tsx index 7a5633924..c6039acc2 100644 --- a/app/components/button.tsx +++ b/app/components/button.tsx @@ -1,6 +1,7 @@ import * as React from "react"; import styles from "./button.module.scss"; +import { CSSProperties } from "react"; export type ButtonType = "primary" | "danger" | null; @@ -16,6 +17,7 @@ export function IconButton(props: { disabled?: boolean; tabIndex?: number; autoFocus?: boolean; + style?: CSSProperties; }) { return (