diff --git a/.gitignore b/.gitignore index a24c6e047d5..2ff556f646e 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ dev *.key *.key.pub + +masks.json diff --git a/README.md b/README.md index 472102cdca8..290a7f6ac8d 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 e42288bb5b0..8c464dc098c 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 e3b88702e66..2913a147795 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 00000000000..4b2bcc30527 --- /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 01286fc1bf9..1f58a884fe3 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 c0c71480cd0..102a4220ff2 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 7a5633924c5..c6039acc292 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 (