From b5e89d444055ee92b806855499f505f8b12bfa52 Mon Sep 17 00:00:00 2001 From: B0zal Date: Tue, 12 Sep 2023 06:56:55 +0700 Subject: [PATCH 1/5] [+] Updated Auth Page - Made changes to the Auth Page to reset the input field for the access code when the "Later" button is clicked. This ensures that only expected user is logged by entering access code or entering their OpenAI API Key, mitigating the risk of small bug issue --- app/components/auth.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/components/auth.tsx b/app/components/auth.tsx index 1ca83dcd3..9a5b0c655 100644 --- a/app/components/auth.tsx +++ b/app/components/auth.tsx @@ -15,6 +15,7 @@ export function AuthPage() { const access = useAccessStore(); const goHome = () => navigate(Path.Home); + const resetAccessCode = () => access.updateCode(""); // Reset access code to empty string useEffect(() => { if (getClientConfig()?.isApp) { @@ -48,7 +49,10 @@ export function AuthPage() { type="primary" onClick={goHome} /> - + { + resetAccessCode(); + goHome(); + }} /> ); From 6f83fbd21278c90cd978108abe54291c38ec10d7 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Wed, 13 Sep 2023 02:51:02 +0800 Subject: [PATCH 2/5] feat: add webdav support --- 3 | 119 ++++++++++++++ app/api/cors/[...path]/route.ts | 44 +++++ app/components/settings.tsx | 279 +++++++++++++++++++++++++++----- app/constant.ts | 10 +- app/icons/cloud-fail.svg | 1 + app/icons/cloud-success.svg | 1 + app/icons/config.svg | 1 + app/icons/connection.svg | 1 + app/locales/cn.ts | 31 +++- app/locales/en.ts | 32 +++- app/store/sync.ts | 93 ++++++----- app/utils/cloud/index.ts | 33 ++++ app/utils/cloud/upstash.ts | 39 +++++ app/utils/cloud/webdav.ts | 78 +++++++++ app/utils/cors.ts | 50 ++++++ next.config.mjs | 43 +++-- 16 files changed, 751 insertions(+), 104 deletions(-) create mode 100644 3 create mode 100644 app/api/cors/[...path]/route.ts create mode 100644 app/icons/cloud-fail.svg create mode 100644 app/icons/cloud-success.svg create mode 100644 app/icons/config.svg create mode 100644 app/icons/connection.svg create mode 100644 app/utils/cloud/index.ts create mode 100644 app/utils/cloud/upstash.ts create mode 100644 app/utils/cloud/webdav.ts create mode 100644 app/utils/cors.ts diff --git a/3 b/3 new file mode 100644 index 000000000..371bd01ac --- /dev/null +++ b/3 @@ -0,0 +1,119 @@ +export const OWNER = "Yidadaa"; +export const REPO = "ChatGPT-Next-Web"; +export const REPO_URL = `https://github.com/${OWNER}/${REPO}`; +export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`; +export const UPDATE_URL = `${REPO_URL}#keep-updated`; +export const RELEASE_URL = `${REPO_URL}/releases`; +export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`; +export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`; +export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; +export const DEFAULT_API_HOST = "https://chatgpt1.nextweb.fun/api/proxy"; + +export enum Path { + Home = "/", + Chat = "/chat", + Settings = "/settings", + NewChat = "/new-chat", + Masks = "/masks", + Auth = "/auth", +} + +export enum SlotID { + AppBody = "app-body", +} + +export enum FileName { + Masks = "masks.json", + Prompts = "prompts.json", +} + +export enum StoreKey { + Chat = "chat-next-web-store", + Access = "access-control", + Config = "app-config", + Mask = "mask-store", + Prompt = "prompt-store", + Update = "chat-update", + Sync = "sync", +} + +export const MAX_SIDEBAR_WIDTH = 500; +export const MIN_SIDEBAR_WIDTH = 230; +export const NARROW_SIDEBAR_WIDTH = 100; + +export const ACCESS_CODE_PREFIX = "nk-"; + +export const LAST_INPUT_KEY = "last-input"; +export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id; + +export const STORAGE_KEY = "chatgpt-next-web"; + +export const REQUEST_TIMEOUT_MS = 60000; + +export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; + +export const OpenaiPath = { + ChatPath: "v1/chat/completions", + UsagePath: "dashboard/billing/usage", + SubsPath: "dashboard/billing/subscription", + ListModelPath: "v1/models", +}; + +export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang +export const DEFAULT_SYSTEM_TEMPLATE = ` +You are ChatGPT, a large language model trained by OpenAI. +Knowledge cutoff: 2021-09 +Current model: {{model}} +Current time: {{time}}`; + +export const SUMMARIZE_MODEL = "gpt-3.5-turbo"; + +export const DEFAULT_MODELS = [ + { + name: "gpt-4", + available: true, + }, + { + name: "gpt-4-0314", + available: true, + }, + { + name: "gpt-4-0613", + available: true, + }, + { + name: "gpt-4-32k", + available: true, + }, + { + name: "gpt-4-32k-0314", + available: true, + }, + { + name: "gpt-4-32k-0613", + available: true, + }, + { + name: "gpt-3.5-turbo", + available: true, + }, + { + name: "gpt-3.5-turbo-0301", + available: true, + }, + { + name: "gpt-3.5-turbo-0613", + available: true, + }, + { + name: "gpt-3.5-turbo-16k", + available: true, + }, + { + name: "gpt-3.5-turbo-16k-0613", + available: true, + }, +] as const; + +export const CHAT_PAGE_SIZE = 15; +export const MAX_RENDER_MSG_COUNT = 45; diff --git a/app/api/cors/[...path]/route.ts b/app/api/cors/[...path]/route.ts new file mode 100644 index 000000000..c461d250b --- /dev/null +++ b/app/api/cors/[...path]/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from "next/server"; + +async function handle( + req: NextRequest, + { params }: { params: { path: string[] } }, +) { + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + + const [protocol, ...subpath] = params.path; + const targetUrl = `${protocol}://${subpath.join("/")}`; + + const method = req.headers.get("method") ?? undefined; + const shouldNotHaveBody = ["get", "head"].includes( + method?.toLowerCase() ?? "", + ); + + const fetchOptions: RequestInit = { + headers: { + authorization: req.headers.get("authorization") ?? "", + }, + body: shouldNotHaveBody ? null : req.body, + method, + // @ts-ignore + duplex: "half", + }; + + console.log("[Any Proxy]", targetUrl); + + const fetchResult = fetch(targetUrl, fetchOptions); + + return fetchResult; +} + +export const GET = handle; +export const POST = handle; +export const PUT = handle; + +// nextjs dose not support those https methods, sucks +export const PROFIND = handle; +export const MKCOL = handle; + +export const runtime = "edge"; diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 9de603bb3..8e43e1d1a 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -12,6 +12,12 @@ import EditIcon from "../icons/edit.svg"; import EyeIcon from "../icons/eye.svg"; import DownloadIcon from "../icons/download.svg"; import UploadIcon from "../icons/upload.svg"; +import ConfigIcon from "../icons/config.svg"; +import ConfirmIcon from "../icons/confirm.svg"; + +import ConnectionIcon from "../icons/connection.svg"; +import CloudSuccessIcon from "../icons/cloud-success.svg"; +import CloudFailIcon from "../icons/cloud-fail.svg"; import { Input, @@ -54,6 +60,7 @@ import { getClientConfig } from "../config/client"; import { useSyncStore } from "../store/sync"; import { nanoid } from "nanoid"; import { useMaskStore } from "../store/mask"; +import { ProviderType } from "../utils/cloud"; function EditPromptModal(props: { id: string; onClose: () => void }) { const promptStore = usePromptStore(); @@ -247,12 +254,183 @@ function DangerItems() { ); } +function CheckButton() { + const syncStore = useSyncStore(); + + const couldCheck = useMemo(() => { + return syncStore.coundSync(); + }, [syncStore]); + + const [checkState, setCheckState] = useState< + "none" | "checking" | "success" | "failed" + >("none"); + + async function check() { + setCheckState("checking"); + const valid = await syncStore.check(); + setCheckState(valid ? "success" : "failed"); + } + + if (!couldCheck) return null; + + return ( + + ) : checkState === "checking" ? ( + + ) : checkState === "success" ? ( + + ) : checkState === "failed" ? ( + + ) : ( + + ) + } + > + ); +} + +function SyncConfigModal(props: { onClose?: () => void }) { + const syncStore = useSyncStore(); + + return ( +
+ props.onClose?.()} + actions={[ + , + } + bordered + text={Locale.UI.Confirm} + />, + ]} + > + + + + + + + { + syncStore.update( + (config) => (config.useProxy = e.currentTarget.checked), + ); + }} + > + + {syncStore.useProxy ? ( + + { + syncStore.update( + (config) => (config.proxyUrl = e.currentTarget.value), + ); + }} + > + + ) : null} + + + {syncStore.provider === ProviderType.WebDAV && ( + <> + + + { + syncStore.update( + (config) => + (config.webdav.endpoint = e.currentTarget.value), + ); + }} + > + + + + { + syncStore.update( + (config) => + (config.webdav.username = e.currentTarget.value), + ); + }} + > + + + { + syncStore.update( + (config) => + (config.webdav.password = e.currentTarget.value), + ); + }} + > + + + + )} + + {syncStore.provider === ProviderType.UpStash && ( + + + + )} + +
+ ); +} + function SyncItems() { const syncStore = useSyncStore(); - const webdav = syncStore.webDavConfig; const chatStore = useChatStore(); const promptStore = usePromptStore(); const maskStore = useMaskStore(); + const couldSync = useMemo(() => { + return syncStore.coundSync(); + }, [syncStore]); + + const [showSyncConfigModal, setShowSyncConfigModal] = useState(false); const stateOverview = useMemo(() => { const sessions = chatStore.sessions; @@ -267,42 +445,71 @@ function SyncItems() { }, [chatStore.sessions, maskStore.masks, promptStore.prompts]); return ( - - - } - text={Locale.UI.Sync} - onClick={() => { - showToast(Locale.WIP); - }} - /> - + <> + + +
+ } + text={Locale.UI.Config} + onClick={() => { + setShowSyncConfigModal(true); + }} + /> + {couldSync && ( + } + text={Locale.UI.Sync} + onClick={async () => { + try { + await syncStore.sync(); + showToast(Locale.Settings.Sync.Success); + } catch (e) { + showToast(Locale.Settings.Sync.Fail); + console.error("[Sync]", e); + } + }} + /> + )} +
+
- -
- } - text={Locale.UI.Export} - onClick={() => { - syncStore.export(); - }} - /> - } - text={Locale.UI.Import} - onClick={() => { - syncStore.import(); - }} - /> -
-
-
+ +
+ } + text={Locale.UI.Export} + onClick={() => { + syncStore.export(); + }} + /> + } + text={Locale.UI.Import} + onClick={() => { + syncStore.import(); + }} + /> +
+
+
+ + {showSyncConfigModal && ( + setShowSyncConfigModal(false)} /> + )} + ); } diff --git a/app/constant.ts b/app/constant.ts index 2141820ce..f76eb3a97 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -7,7 +7,9 @@ export const RELEASE_URL = `${REPO_URL}/releases`; export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`; export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`; export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; -export const DEFAULT_API_HOST = "https://chatgpt1.nextweb.fun/api/proxy"; + +export const DEFAULT_CORS_HOST = "https://chatgpt2.nextweb.fun"; +export const DEFAULT_API_HOST = `${DEFAULT_CORS_HOST}/api/proxy`; export enum Path { Home = "/", @@ -18,6 +20,10 @@ export enum Path { Auth = "/auth", } +export enum ApiPath { + Cors = "/api/cors", +} + export enum SlotID { AppBody = "app-body", } @@ -46,6 +52,8 @@ export const ACCESS_CODE_PREFIX = "nk-"; export const LAST_INPUT_KEY = "last-input"; export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id; +export const STORAGE_KEY = "chatgpt-next-web"; + export const REQUEST_TIMEOUT_MS = 60000; export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; diff --git a/app/icons/cloud-fail.svg b/app/icons/cloud-fail.svg new file mode 100644 index 000000000..6e6a35fe5 --- /dev/null +++ b/app/icons/cloud-fail.svg @@ -0,0 +1 @@ + diff --git a/app/icons/cloud-success.svg b/app/icons/cloud-success.svg new file mode 100644 index 000000000..8c5f3d6fd --- /dev/null +++ b/app/icons/cloud-success.svg @@ -0,0 +1 @@ + diff --git a/app/icons/config.svg b/app/icons/config.svg new file mode 100644 index 000000000..7e1d23a27 --- /dev/null +++ b/app/icons/config.svg @@ -0,0 +1 @@ + diff --git a/app/icons/connection.svg b/app/icons/connection.svg new file mode 100644 index 000000000..036873020 --- /dev/null +++ b/app/icons/connection.svg @@ -0,0 +1 @@ + diff --git a/app/locales/cn.ts b/app/locales/cn.ts index a1753417a..1b8850f45 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -179,7 +179,35 @@ const cn = { SubTitle: "根据对话内容生成合适的标题", }, Sync: { - LastUpdate: "上次同步", + CloudState: "云端数据", + NotSyncYet: "还没有进行过同步", + Success: "同步成功", + Fail: "同步失败", + + Config: { + Modal: { + Title: "配置云同步", + }, + SyncType: { + Title: "同步类型", + SubTitle: "选择喜爱的同步服务器", + }, + Proxy: { + Title: "启用代理", + SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制", + }, + ProxyUrl: { + Title: "代理地址", + SubTitle: "仅适用于本项目自带的跨域代理", + }, + + WebDav: { + Endpoint: "WebDAV 地址", + UserName: "用户名", + Password: "密码", + }, + }, + LocalState: "本地数据", Overview: (overview: any) => { return `${overview.chat} 次对话,${overview.message} 条消息,${overview.prompt} 条提示词,${overview.mask} 个面具`; @@ -366,6 +394,7 @@ const cn = { Export: "导出", Import: "导入", Sync: "同步", + Config: "配置", }, Exporter: { Model: "模型", diff --git a/app/locales/en.ts b/app/locales/en.ts index e31295787..ebbf1a376 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -181,7 +181,36 @@ const en: LocaleType = { SubTitle: "Generate a suitable title based on the conversation content", }, Sync: { - LastUpdate: "Last Update", + CloudState: "Last Update", + NotSyncYet: "Not sync yet", + Success: "Sync Success", + Fail: "Sync Fail", + + Config: { + Modal: { + Title: "Config Sync", + }, + SyncType: { + Title: "Sync Type", + SubTitle: "Choose your favorite sync service", + }, + Proxy: { + Title: "Enable CORS Proxy", + SubTitle: "Enable a proxy to avoid cross-origin restrictions", + }, + ProxyUrl: { + Title: "Proxy Endpoint", + SubTitle: + "Only applicable to the built-in CORS proxy for this project", + }, + + WebDav: { + Endpoint: "WebDAV Endpoint", + UserName: "User Name", + Password: "Password", + }, + }, + LocalState: "Local Data", Overview: (overview: any) => { return `${overview.chat} chats,${overview.message} messages,${overview.prompt} prompts,${overview.mask} masks`; @@ -366,6 +395,7 @@ const en: LocaleType = { Export: "Export", Import: "Import", Sync: "Sync", + Config: "Config", }, Exporter: { Model: "Model", diff --git a/app/store/sync.ts b/app/store/sync.ts index 502cf71cb..29b6a82c2 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -1,15 +1,18 @@ import { Updater } from "../typing"; -import { StoreKey } from "../constant"; +import { ApiPath, StoreKey } from "../constant"; import { createPersistStore } from "../utils/store"; import { AppState, getLocalAppState, + GetStoreState, mergeAppState, setLocalAppState, } from "../utils/sync"; import { downloadAs, readFromFile } from "../utils"; import { showToast } from "../components/ui-lib"; import Locale from "../locales"; +import { createSyncClient, ProviderType } from "../utils/cloud"; +import { corsPath } from "../utils/cors"; export interface WebDavConfig { server: string; @@ -17,22 +20,43 @@ export interface WebDavConfig { password: string; } +export type SyncStore = GetStoreState; + export const useSyncStore = createPersistStore( { - webDavConfig: { - server: "", + provider: ProviderType.WebDAV, + useProxy: true, + proxyUrl: corsPath(ApiPath.Cors), + + webdav: { + endpoint: "", username: "", password: "", }, + upstash: { + endpoint: "", + username: "", + apiKey: "", + }, + lastSyncTime: 0, + lastProvider: "", }, (set, get) => ({ + coundSync() { + const config = get()[get().provider]; + return Object.values(config).every((c) => c.toString().length > 0); + }, + + markSyncTime() { + set({ lastSyncTime: Date.now(), lastProvider: get().provider }); + }, + export() { const state = getLocalAppState(); const fileName = `Backup-${new Date().toLocaleString()}.json`; downloadAs(JSON.stringify(state), fileName); - set({ lastSyncTime: Date.now() }); }, async import() { @@ -50,47 +74,36 @@ export const useSyncStore = createPersistStore( } }, - async check() { + getClient() { + const provider = get().provider; + const client = createSyncClient(provider, get()); + return client; + }, + + async sync() { + const localState = getLocalAppState(); + const provider = get().provider; + const config = get()[provider]; + const client = this.getClient(); + try { - const res = await fetch(this.path(""), { - method: "PROFIND", - headers: this.headers(), - }); - const sanitizedRes = { - status: res.status, - statusText: res.statusText, - headers: res.headers, - }; - console.log(sanitizedRes); - return res.status === 207; + const remoteState = JSON.parse( + await client.get(config.username), + ) as AppState; + mergeAppState(localState, remoteState); + setLocalAppState(localState); } catch (e) { - console.error("[Sync] ", e); - return false; + console.log("[Sync] failed to get remoate state", e); } + + await client.set(config.username, JSON.stringify(localState)); + + this.markSyncTime(); }, - path(path: string) { - let url = get().webDavConfig.server; - - if (!url.endsWith("/")) { - url += "/"; - } - - if (path.startsWith("/")) { - path = path.slice(1); - } - - return url + path; - }, - - headers() { - const auth = btoa( - [get().webDavConfig.username, get().webDavConfig.password].join(":"), - ); - - return { - Authorization: `Basic ${auth}`, - }; + async check() { + const client = this.getClient(); + return await client.check(); }, }), { diff --git a/app/utils/cloud/index.ts b/app/utils/cloud/index.ts new file mode 100644 index 000000000..63908249e --- /dev/null +++ b/app/utils/cloud/index.ts @@ -0,0 +1,33 @@ +import { createWebDavClient } from "./webdav"; +import { createUpstashClient } from "./upstash"; + +export enum ProviderType { + WebDAV = "webdav", + UpStash = "upstash", +} + +export const SyncClients = { + [ProviderType.UpStash]: createUpstashClient, + [ProviderType.WebDAV]: createWebDavClient, +} as const; + +type SyncClientConfig = { + [K in keyof typeof SyncClients]: (typeof SyncClients)[K] extends ( + _: infer C, + ) => any + ? C + : never; +}; + +export type SyncClient = { + get: (key: string) => Promise; + set: (key: string, value: string) => Promise; + check: () => Promise; +}; + +export function createSyncClient( + provider: T, + config: SyncClientConfig[T], +): SyncClient { + return SyncClients[provider](config as any) as any; +} diff --git a/app/utils/cloud/upstash.ts b/app/utils/cloud/upstash.ts new file mode 100644 index 000000000..6f9b30f6b --- /dev/null +++ b/app/utils/cloud/upstash.ts @@ -0,0 +1,39 @@ +import { SyncStore } from "@/app/store/sync"; + +export type UpstashConfig = SyncStore["upstash"]; +export type UpStashClient = ReturnType; + +export function createUpstashClient(config: UpstashConfig) { + return { + async check() { + return true; + }, + + async get() { + throw Error("[Sync] not implemented"); + }, + + async set() { + throw Error("[Sync] not implemented"); + }, + + headers() { + return { + Authorization: `Basic ${config.apiKey}`, + }; + }, + path(path: string) { + let url = config.endpoint; + + if (!url.endsWith("/")) { + url += "/"; + } + + if (path.startsWith("/")) { + path = path.slice(1); + } + + return url + path; + }, + }; +} diff --git a/app/utils/cloud/webdav.ts b/app/utils/cloud/webdav.ts new file mode 100644 index 000000000..5386b4d19 --- /dev/null +++ b/app/utils/cloud/webdav.ts @@ -0,0 +1,78 @@ +import { STORAGE_KEY } from "@/app/constant"; +import { SyncStore } from "@/app/store/sync"; +import { corsFetch } from "../cors"; + +export type WebDAVConfig = SyncStore["webdav"]; +export type WebDavClient = ReturnType; + +export function createWebDavClient(store: SyncStore) { + const folder = STORAGE_KEY; + const fileName = `${folder}/backup.json`; + const config = store.webdav; + const proxyUrl = + store.useProxy && store.proxyUrl.length > 0 ? store.proxyUrl : undefined; + + return { + async check() { + try { + const res = await corsFetch(this.path(folder), { + method: "MKCOL", + headers: this.headers(), + proxyUrl, + }); + + console.log("[WebDav] check", res.status, res.statusText); + + return [201, 200, 404].includes(res.status); + } catch (e) { + console.error("[WebDav] failed to check", e); + } + + return false; + }, + + async get(key: string) { + const res = await corsFetch(this.path(fileName), { + method: "GET", + headers: this.headers(), + proxyUrl, + }); + + console.log("[WebDav] get key = ", key, res.status, res.statusText); + + return await res.text(); + }, + + async set(key: string, value: string) { + const res = await corsFetch(this.path(fileName), { + method: "PUT", + headers: this.headers(), + body: value, + proxyUrl, + }); + + console.log("[WebDav] set key = ", key, res.status, res.statusText); + }, + + headers() { + const auth = btoa(config.username + ":" + config.password); + + return { + authorization: `Basic ${auth}`, + }; + }, + path(path: string) { + let url = config.endpoint; + + if (!url.endsWith("/")) { + url += "/"; + } + + if (path.startsWith("/")) { + path = path.slice(1); + } + + return url + path; + }, + }; +} diff --git a/app/utils/cors.ts b/app/utils/cors.ts new file mode 100644 index 000000000..773f152aa --- /dev/null +++ b/app/utils/cors.ts @@ -0,0 +1,50 @@ +import { getClientConfig } from "../config/client"; +import { ApiPath, DEFAULT_CORS_HOST } from "../constant"; + +export function corsPath(path: string) { + const baseUrl = getClientConfig()?.isApp ? `${DEFAULT_CORS_HOST}` : ""; + + if (!path.startsWith("/")) { + path = "/" + path; + } + + if (!path.endsWith("/")) { + path += "/"; + } + + return `${baseUrl}${path}`; +} + +export function corsFetch( + url: string, + options: RequestInit & { + proxyUrl?: string; + }, +) { + if (!url.startsWith("http")) { + throw Error("[CORS Fetch] url must starts with http/https"); + } + + let proxyUrl = options.proxyUrl ?? corsPath(ApiPath.Cors); + if (!proxyUrl.endsWith("/")) { + proxyUrl += "/"; + } + + url = url.replace("://", "/"); + + const corsOptions = { + ...options, + method: "POST", + headers: options.method + ? { + ...options.headers, + method: options.method, + } + : options.headers, + }; + + const corsUrl = proxyUrl + url; + console.info("[CORS] target = ", corsUrl); + + return fetch(corsUrl, corsOptions); +} diff --git a/next.config.mjs b/next.config.mjs index c8f17de8c..4faa63e54 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -35,27 +35,29 @@ const nextConfig = { }, }; +const CorsHeaders = [ + { key: "Access-Control-Allow-Credentials", value: "true" }, + { key: "Access-Control-Allow-Origin", value: "*" }, + { + key: "Access-Control-Allow-Methods", + value: "*", + }, + { + key: "Access-Control-Allow-Headers", + value: "*", + }, + { + key: "Access-Control-Max-Age", + value: "86400", + }, +]; + if (mode !== "export") { nextConfig.headers = async () => { return [ { source: "/api/:path*", - headers: [ - { key: "Access-Control-Allow-Credentials", value: "true" }, - { key: "Access-Control-Allow-Origin", value: "*" }, - { - key: "Access-Control-Allow-Methods", - value: "*", - }, - { - key: "Access-Control-Allow-Headers", - value: "*", - }, - { - key: "Access-Control-Max-Age", - value: "86400", - }, - ], + headers: CorsHeaders, }, ]; }; @@ -76,15 +78,6 @@ if (mode !== "export") { }, ]; - const apiUrl = process.env.API_URL; - if (apiUrl) { - console.log("[Next] using api url ", apiUrl); - ret.push({ - source: "/api/:path*", - destination: `${apiUrl}/:path*`, - }); - } - return { beforeFiles: ret, }; From 859cf6930fc3cbe5b1eeb52d8c481a6cd95d63c0 Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Wed, 13 Sep 2023 02:51:57 +0800 Subject: [PATCH 3/5] fixup --- 3 | 119 -------------------------------------------------------------- 1 file changed, 119 deletions(-) delete mode 100644 3 diff --git a/3 b/3 deleted file mode 100644 index 371bd01ac..000000000 --- a/3 +++ /dev/null @@ -1,119 +0,0 @@ -export const OWNER = "Yidadaa"; -export const REPO = "ChatGPT-Next-Web"; -export const REPO_URL = `https://github.com/${OWNER}/${REPO}`; -export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`; -export const UPDATE_URL = `${REPO_URL}#keep-updated`; -export const RELEASE_URL = `${REPO_URL}/releases`; -export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`; -export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`; -export const RUNTIME_CONFIG_DOM = "danger-runtime-config"; -export const DEFAULT_API_HOST = "https://chatgpt1.nextweb.fun/api/proxy"; - -export enum Path { - Home = "/", - Chat = "/chat", - Settings = "/settings", - NewChat = "/new-chat", - Masks = "/masks", - Auth = "/auth", -} - -export enum SlotID { - AppBody = "app-body", -} - -export enum FileName { - Masks = "masks.json", - Prompts = "prompts.json", -} - -export enum StoreKey { - Chat = "chat-next-web-store", - Access = "access-control", - Config = "app-config", - Mask = "mask-store", - Prompt = "prompt-store", - Update = "chat-update", - Sync = "sync", -} - -export const MAX_SIDEBAR_WIDTH = 500; -export const MIN_SIDEBAR_WIDTH = 230; -export const NARROW_SIDEBAR_WIDTH = 100; - -export const ACCESS_CODE_PREFIX = "nk-"; - -export const LAST_INPUT_KEY = "last-input"; -export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id; - -export const STORAGE_KEY = "chatgpt-next-web"; - -export const REQUEST_TIMEOUT_MS = 60000; - -export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown"; - -export const OpenaiPath = { - ChatPath: "v1/chat/completions", - UsagePath: "dashboard/billing/usage", - SubsPath: "dashboard/billing/subscription", - ListModelPath: "v1/models", -}; - -export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang -export const DEFAULT_SYSTEM_TEMPLATE = ` -You are ChatGPT, a large language model trained by OpenAI. -Knowledge cutoff: 2021-09 -Current model: {{model}} -Current time: {{time}}`; - -export const SUMMARIZE_MODEL = "gpt-3.5-turbo"; - -export const DEFAULT_MODELS = [ - { - name: "gpt-4", - available: true, - }, - { - name: "gpt-4-0314", - available: true, - }, - { - name: "gpt-4-0613", - available: true, - }, - { - name: "gpt-4-32k", - available: true, - }, - { - name: "gpt-4-32k-0314", - available: true, - }, - { - name: "gpt-4-32k-0613", - available: true, - }, - { - name: "gpt-3.5-turbo", - available: true, - }, - { - name: "gpt-3.5-turbo-0301", - available: true, - }, - { - name: "gpt-3.5-turbo-0613", - available: true, - }, - { - name: "gpt-3.5-turbo-16k", - available: true, - }, - { - name: "gpt-3.5-turbo-16k-0613", - available: true, - }, -] as const; - -export const CHAT_PAGE_SIZE = 15; -export const MAX_RENDER_MSG_COUNT = 45; From dc555b2206ae84ce2598774398f49d967357d37d Mon Sep 17 00:00:00 2001 From: Yidadaa Date: Wed, 13 Sep 2023 02:52:28 +0800 Subject: [PATCH 4/5] fixup --- app/api/cors/[...path]/route.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/api/cors/[...path]/route.ts b/app/api/cors/[...path]/route.ts index c461d250b..90404cf89 100644 --- a/app/api/cors/[...path]/route.ts +++ b/app/api/cors/[...path]/route.ts @@ -33,12 +33,6 @@ async function handle( return fetchResult; } -export const GET = handle; export const POST = handle; -export const PUT = handle; - -// nextjs dose not support those https methods, sucks -export const PROFIND = handle; -export const MKCOL = handle; export const runtime = "edge"; From b589f48aa99e1bc3b5544b4fc81cab27385c699e Mon Sep 17 00:00:00 2001 From: Yifei Zhang Date: Wed, 13 Sep 2023 03:01:28 +0800 Subject: [PATCH 5/5] Update tauri.conf.json --- src-tauri/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2256d5b34..d8b677bf6 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -9,7 +9,7 @@ }, "package": { "productName": "ChatGPT Next Web", - "version": "2.9.5" + "version": "2.9.6" }, "tauri": { "allowlist": {