forked from XiaoMo/ChatGPT-Next-Web
Merge pull request #2819 from Yidadaa/webdav
This commit is contained in:
commit
74e32efa7d
38
app/api/cors/[...path]/route.ts
Normal file
38
app/api/cors/[...path]/route.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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 POST = handle;
|
||||||
|
|
||||||
|
export const runtime = "edge";
|
@ -12,6 +12,12 @@ import EditIcon from "../icons/edit.svg";
|
|||||||
import EyeIcon from "../icons/eye.svg";
|
import EyeIcon from "../icons/eye.svg";
|
||||||
import DownloadIcon from "../icons/download.svg";
|
import DownloadIcon from "../icons/download.svg";
|
||||||
import UploadIcon from "../icons/upload.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 {
|
import {
|
||||||
Input,
|
Input,
|
||||||
@ -54,6 +60,7 @@ import { getClientConfig } from "../config/client";
|
|||||||
import { useSyncStore } from "../store/sync";
|
import { useSyncStore } from "../store/sync";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { useMaskStore } from "../store/mask";
|
import { useMaskStore } from "../store/mask";
|
||||||
|
import { ProviderType } from "../utils/cloud";
|
||||||
|
|
||||||
function EditPromptModal(props: { id: string; onClose: () => void }) {
|
function EditPromptModal(props: { id: string; onClose: () => void }) {
|
||||||
const promptStore = usePromptStore();
|
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 (
|
||||||
|
<IconButton
|
||||||
|
text="检查可用性"
|
||||||
|
bordered
|
||||||
|
onClick={check}
|
||||||
|
icon={
|
||||||
|
checkState === "none" ? (
|
||||||
|
<ConnectionIcon />
|
||||||
|
) : checkState === "checking" ? (
|
||||||
|
<LoadingIcon />
|
||||||
|
) : checkState === "success" ? (
|
||||||
|
<CloudSuccessIcon />
|
||||||
|
) : checkState === "failed" ? (
|
||||||
|
<CloudFailIcon />
|
||||||
|
) : (
|
||||||
|
<ConnectionIcon />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
></IconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SyncConfigModal(props: { onClose?: () => void }) {
|
||||||
|
const syncStore = useSyncStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-mask">
|
||||||
|
<Modal
|
||||||
|
title={Locale.Settings.Sync.Config.Modal.Title}
|
||||||
|
onClose={() => props.onClose?.()}
|
||||||
|
actions={[
|
||||||
|
<CheckButton key="check" />,
|
||||||
|
<IconButton
|
||||||
|
key="confirm"
|
||||||
|
onClick={props.onClose}
|
||||||
|
icon={<ConfirmIcon />}
|
||||||
|
bordered
|
||||||
|
text={Locale.UI.Confirm}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<List>
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Sync.Config.SyncType.Title}
|
||||||
|
subTitle={Locale.Settings.Sync.Config.SyncType.SubTitle}
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
value={syncStore.provider}
|
||||||
|
onChange={(e) => {
|
||||||
|
syncStore.update(
|
||||||
|
(config) =>
|
||||||
|
(config.provider = e.target.value as ProviderType),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.entries(ProviderType).map(([k, v]) => (
|
||||||
|
<option value={v} key={k}>
|
||||||
|
{k}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Sync.Config.Proxy.Title}
|
||||||
|
subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={syncStore.useProxy}
|
||||||
|
onChange={(e) => {
|
||||||
|
syncStore.update(
|
||||||
|
(config) => (config.useProxy = e.currentTarget.checked),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
{syncStore.useProxy ? (
|
||||||
|
<ListItem
|
||||||
|
title={Locale.Settings.Sync.Config.ProxyUrl.Title}
|
||||||
|
subTitle={Locale.Settings.Sync.Config.ProxyUrl.SubTitle}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={syncStore.proxyUrl}
|
||||||
|
onChange={(e) => {
|
||||||
|
syncStore.update(
|
||||||
|
(config) => (config.proxyUrl = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
) : null}
|
||||||
|
</List>
|
||||||
|
|
||||||
|
{syncStore.provider === ProviderType.WebDAV && (
|
||||||
|
<>
|
||||||
|
<List>
|
||||||
|
<ListItem title={Locale.Settings.Sync.Config.WebDav.Endpoint}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={syncStore.webdav.endpoint}
|
||||||
|
onChange={(e) => {
|
||||||
|
syncStore.update(
|
||||||
|
(config) =>
|
||||||
|
(config.webdav.endpoint = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<ListItem title={Locale.Settings.Sync.Config.WebDav.UserName}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={syncStore.webdav.username}
|
||||||
|
onChange={(e) => {
|
||||||
|
syncStore.update(
|
||||||
|
(config) =>
|
||||||
|
(config.webdav.username = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></input>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem title={Locale.Settings.Sync.Config.WebDav.Password}>
|
||||||
|
<PasswordInput
|
||||||
|
value={syncStore.webdav.password}
|
||||||
|
onChange={(e) => {
|
||||||
|
syncStore.update(
|
||||||
|
(config) =>
|
||||||
|
(config.webdav.password = e.currentTarget.value),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></PasswordInput>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{syncStore.provider === ProviderType.UpStash && (
|
||||||
|
<List>
|
||||||
|
<ListItem title={Locale.WIP}></ListItem>
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SyncItems() {
|
function SyncItems() {
|
||||||
const syncStore = useSyncStore();
|
const syncStore = useSyncStore();
|
||||||
const webdav = syncStore.webDavConfig;
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const promptStore = usePromptStore();
|
const promptStore = usePromptStore();
|
||||||
const maskStore = useMaskStore();
|
const maskStore = useMaskStore();
|
||||||
|
const couldSync = useMemo(() => {
|
||||||
|
return syncStore.coundSync();
|
||||||
|
}, [syncStore]);
|
||||||
|
|
||||||
|
const [showSyncConfigModal, setShowSyncConfigModal] = useState(false);
|
||||||
|
|
||||||
const stateOverview = useMemo(() => {
|
const stateOverview = useMemo(() => {
|
||||||
const sessions = chatStore.sessions;
|
const sessions = chatStore.sessions;
|
||||||
@ -267,42 +445,71 @@ function SyncItems() {
|
|||||||
}, [chatStore.sessions, maskStore.masks, promptStore.prompts]);
|
}, [chatStore.sessions, maskStore.masks, promptStore.prompts]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List>
|
<>
|
||||||
<ListItem
|
<List>
|
||||||
title={Locale.Settings.Sync.LastUpdate}
|
<ListItem
|
||||||
subTitle={new Date(syncStore.lastSyncTime).toLocaleString()}
|
title={Locale.Settings.Sync.CloudState}
|
||||||
>
|
subTitle={
|
||||||
<IconButton
|
syncStore.lastProvider
|
||||||
icon={<ResetIcon />}
|
? `${new Date(syncStore.lastSyncTime).toLocaleString()} [${
|
||||||
text={Locale.UI.Sync}
|
syncStore.lastProvider
|
||||||
onClick={() => {
|
}]`
|
||||||
showToast(Locale.WIP);
|
: Locale.Settings.Sync.NotSyncYet
|
||||||
}}
|
}
|
||||||
/>
|
>
|
||||||
</ListItem>
|
<div style={{ display: "flex" }}>
|
||||||
|
<IconButton
|
||||||
|
icon={<ConfigIcon />}
|
||||||
|
text={Locale.UI.Config}
|
||||||
|
onClick={() => {
|
||||||
|
setShowSyncConfigModal(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{couldSync && (
|
||||||
|
<IconButton
|
||||||
|
icon={<ResetIcon />}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
title={Locale.Settings.Sync.LocalState}
|
title={Locale.Settings.Sync.LocalState}
|
||||||
subTitle={Locale.Settings.Sync.Overview(stateOverview)}
|
subTitle={Locale.Settings.Sync.Overview(stateOverview)}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex" }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<UploadIcon />}
|
icon={<UploadIcon />}
|
||||||
text={Locale.UI.Export}
|
text={Locale.UI.Export}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
syncStore.export();
|
syncStore.export();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<DownloadIcon />}
|
icon={<DownloadIcon />}
|
||||||
text={Locale.UI.Import}
|
text={Locale.UI.Import}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
syncStore.import();
|
syncStore.import();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
|
{showSyncConfigModal && (
|
||||||
|
<SyncConfigModal onClose={() => setShowSyncConfigModal(false)} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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_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 FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
|
||||||
export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
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 {
|
export enum Path {
|
||||||
Home = "/",
|
Home = "/",
|
||||||
@ -18,6 +20,10 @@ export enum Path {
|
|||||||
Auth = "/auth",
|
Auth = "/auth",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ApiPath {
|
||||||
|
Cors = "/api/cors",
|
||||||
|
}
|
||||||
|
|
||||||
export enum SlotID {
|
export enum SlotID {
|
||||||
AppBody = "app-body",
|
AppBody = "app-body",
|
||||||
}
|
}
|
||||||
@ -46,6 +52,8 @@ export const ACCESS_CODE_PREFIX = "nk-";
|
|||||||
export const LAST_INPUT_KEY = "last-input";
|
export const LAST_INPUT_KEY = "last-input";
|
||||||
export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id;
|
export const UNFINISHED_INPUT = (id: string) => "unfinished-input-" + id;
|
||||||
|
|
||||||
|
export const STORAGE_KEY = "chatgpt-next-web";
|
||||||
|
|
||||||
export const REQUEST_TIMEOUT_MS = 60000;
|
export const REQUEST_TIMEOUT_MS = 60000;
|
||||||
|
|
||||||
export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
|
export const EXPORT_MESSAGE_CLASS_NAME = "export-markdown";
|
||||||
|
1
app/icons/cloud-fail.svg
Normal file
1
app/icons/cloud-fail.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 6.2 KiB |
1
app/icons/cloud-success.svg
Normal file
1
app/icons/cloud-success.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><g opacity="1" transform="translate(0 0) rotate(0)"><mask id="bg-mask-0" fill="white"><use transform="translate(0 0) rotate(0)" xlink:href="#path_0"></use></mask><g mask="url(#bg-mask-0)" ><path id="路径 1" style="fill:#333333; opacity:1;" d="M4.00337,11.6633M4.00337,11.6633c-0.8391,0 -1.5514,-0.20763 -2.13691,-0.6229c-0.79984,-0.56727 -1.19975,-1.41519 -1.19975,-2.54377c0,-1.2521 0.52073,-2.20348 1.56218,-2.85415c0.68561,-0.42835 1.38711,-0.64252 2.10448,-0.64252v0.66667l-0.64163,-0.18098l0.64163,0.18098l-0.64163,-0.18098c0.65543,-2.32379 2.09264,-3.48569 4.31163,-3.48569c2.30635,0 3.74023,0.99523 4.30163,2.98569l-0.6416,0.18098l0.0729,-0.66267l-0.0729,0.66267l0.0729,-0.66267c2.3958,0.26354 3.5937,1.5411 3.5937,3.83267c0,2.21778 -1.10887,3.32667 -3.3266,3.32667c-0.0438,0 -0.08717,-0.00427 -0.1301,-0.0128c-0.04293,-0.00853 -0.0846,-0.0212 -0.125,-0.038c-0.04047,-0.01673 -0.0789,-0.03727 -0.1153,-0.0616c-0.0364,-0.02427 -0.07007,-0.0519 -0.101,-0.0829c-0.031,-0.03093 -0.05863,-0.0646 -0.0829,-0.101c-0.02433,-0.0364 -0.04487,-0.07483 -0.0616,-0.1153c-0.0168,-0.0404 -0.02947,-0.08207 -0.038,-0.125c-0.00853,-0.04293 -0.0128,-0.0863 -0.0128,-0.1301c0,-0.04373 0.00427,-0.08707 0.0128,-0.13c0.00853,-0.04293 0.0212,-0.08463 0.038,-0.1251c0.01673,-0.04047 0.03727,-0.0789 0.0616,-0.1153c0.02427,-0.0364 0.0519,-0.07007 0.0829,-0.101c0.03093,-0.03093 0.0646,-0.05857 0.101,-0.0829c0.0364,-0.02433 0.07483,-0.04487 0.1153,-0.0616c0.0404,-0.01673 0.08207,-0.02937 0.125,-0.0379c0.04293,-0.00853 0.0863,-0.0128 0.1301,-0.0128c1.32887,0 1.9933,-0.66446 1.9933,-1.99337c0,-1.4951 -0.80207,-2.33088 -2.4062,-2.50733c-0.27,-0.02971 -0.495,-0.22026 0,0c-0.27,-0.02971 -0.495,-0.22026 -0.5688,-0.4817c-0.37873,-1.34287 -1.38484,-2.01431 -3.01833,-2.01431c-1.54613,0 -2.55559,0.8381 -3.02836,2.51431c-0.08103,0.28728 -0.34314,0.48569 0,0c-0.08103,0.28728 -0.34314,0.48569 -0.64164,0.48569c-0.46252,0 -0.92852,0.14666 -1.39801,0.43998c-0.62355,0.38957 -0.93532,0.96402 -0.93532,1.72336c0,0.66927 0.21258,1.15467 0.63775,1.45621c0.35449,0.25144 0.80969,0.37716 1.36558,0.37716c0.04378,0 0.08713,0.00427 0.13006,0.0128c0.04293,0.00853 0.08462,0.02117 0.12507,0.0379c0.04044,0.01673 0.07886,0.03727 0.11525,0.0616c0.0364,0.02433 0.07008,0.05197 0.10103,0.0829c0.03095,0.03093 0.05859,0.0646 0.08291,0.101c0.02432,0.0364 0.04485,0.07483 0.0616,0.1153c0.01675,0.04047 0.0294,0.08217 0.03794,0.1251c0.00854,0.04293 0.01281,0.08627 0.01281,0.13c0,0.0438 -0.00427,0.08717 -0.01281,0.1301c-0.00854,0.04293 -0.02119,0.0846 -0.03794,0.125c-0.01675,0.04047 -0.03728,0.0789 -0.0616,0.1153c-0.02432,0.0364 -0.05196,0.07007 -0.08291,0.101c-0.03095,0.031 -0.06463,0.05863 -0.10103,0.0829c-0.03639,0.02433 -0.07481,0.04487 -0.11525,0.0616c-0.04045,0.0168 -0.08214,0.02947 -0.12507,0.038c-0.04293,0.00853 -0.08628,0.0128 -0.13006,0.0128z"></path><path id="路径 2" style="fill:#333333; opacity:1;" d="M6.42578,10.4903l2,1.66l-0.42578,0.513l-0.52012,-0.4171l2.67002,-3.32998c0.0274,-0.03415 0.05783,-0.06531 0.0913,-0.09346c0.03353,-0.02815 0.0695,-0.05277 0.1079,-0.07384c0.03833,-0.02107 0.07837,-0.0382 0.1201,-0.05138c0.04173,-0.01319 0.08437,-0.02217 0.1279,-0.02696c0.04353,-0.00479 0.0871,-0.00528 0.1307,-0.00149c0.0436,0.00379 0.0864,0.01181 0.1284,0.02404c0.04207,0.01223 0.0825,0.02844 0.1213,0.04863c0.03887,0.0202 0.07537,0.04399 0.1095,0.07137c0.0342,0.02738 0.06537,0.05783 0.0935,0.09135c0.02813,0.03352 0.05273,0.06946 0.0738,0.10783c0.02107,0.03837 0.0382,0.07843 0.0514,0.12017c0.0132,0.04174 0.0222,0.08437 0.027,0.12788c0.00473,0.04351 0.00523,0.08707 0.0015,0.13068c-0.0038,0.04361 -0.01183,0.08643 -0.0241,0.12846c-0.0122,0.04203 -0.0284,0.08247 -0.0486,0.1213c-0.0202,0.03884 -0.044,0.07534 -0.0714,0.10949l-2.66998,3.33001c-0.23308,0.2907 -0.65919,0.3339 -0.9459,0.0959l-2,-1.66c-0.03369,-0.02793 -0.06432,-0.0589 -0.0919,-0.0929c-0.02758,-0.034 -0.05158,-0.07037 -0.072,-0.1091c-0.02042,-0.03867 -0.03687,-0.079 -0.04934,-0.121c-0.01248,-0.04193 -0.02074,-0.0847 -0.02479,-0.1283c-0.00405,-0.0436 -0.0038,-0.08717 0.00073,-0.1307c0.00453,-0.04353 0.01326,-0.0862 0.0262,-0.128c0.01294,-0.0418 0.02983,-0.08197 0.05068,-0.1205c0.02085,-0.03847 0.04526,-0.07453 0.07321,-0.1082c0.02796,-0.03373 0.05893,-0.06437 0.09292,-0.0919c0.03399,-0.0276 0.07035,-0.0516 0.10907,-0.072c0.03872,-0.02047 0.07906,-0.03693 0.12102,-0.0494c0.04196,-0.01247 0.08473,-0.02073 0.12831,-0.0248c0.04359,-0.004 0.08715,-0.00373 0.13069,0.0008c0.04354,0.00453 0.08622,0.01327 0.12804,0.0262c0.04181,0.01293 0.08197,0.02983 0.12046,0.0507c0.03849,0.0208 0.07457,0.0452 0.10826,0.0732z"></path></g></g><defs><rect id="path_0" x="0" y="0" width="16" height="16" /></defs></svg>
|
After Width: | Height: | Size: 4.7 KiB |
1
app/icons/config.svg
Normal file
1
app/icons/config.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 15 KiB |
1
app/icons/connection.svg
Normal file
1
app/icons/connection.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.1 KiB |
@ -179,7 +179,35 @@ const cn = {
|
|||||||
SubTitle: "根据对话内容生成合适的标题",
|
SubTitle: "根据对话内容生成合适的标题",
|
||||||
},
|
},
|
||||||
Sync: {
|
Sync: {
|
||||||
LastUpdate: "上次同步",
|
CloudState: "云端数据",
|
||||||
|
NotSyncYet: "还没有进行过同步",
|
||||||
|
Success: "同步成功",
|
||||||
|
Fail: "同步失败",
|
||||||
|
|
||||||
|
Config: {
|
||||||
|
Modal: {
|
||||||
|
Title: "配置云同步",
|
||||||
|
},
|
||||||
|
SyncType: {
|
||||||
|
Title: "同步类型",
|
||||||
|
SubTitle: "选择喜爱的同步服务器",
|
||||||
|
},
|
||||||
|
Proxy: {
|
||||||
|
Title: "启用代理",
|
||||||
|
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",
|
||||||
|
},
|
||||||
|
ProxyUrl: {
|
||||||
|
Title: "代理地址",
|
||||||
|
SubTitle: "仅适用于本项目自带的跨域代理",
|
||||||
|
},
|
||||||
|
|
||||||
|
WebDav: {
|
||||||
|
Endpoint: "WebDAV 地址",
|
||||||
|
UserName: "用户名",
|
||||||
|
Password: "密码",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
LocalState: "本地数据",
|
LocalState: "本地数据",
|
||||||
Overview: (overview: any) => {
|
Overview: (overview: any) => {
|
||||||
return `${overview.chat} 次对话,${overview.message} 条消息,${overview.prompt} 条提示词,${overview.mask} 个面具`;
|
return `${overview.chat} 次对话,${overview.message} 条消息,${overview.prompt} 条提示词,${overview.mask} 个面具`;
|
||||||
@ -366,6 +394,7 @@ const cn = {
|
|||||||
Export: "导出",
|
Export: "导出",
|
||||||
Import: "导入",
|
Import: "导入",
|
||||||
Sync: "同步",
|
Sync: "同步",
|
||||||
|
Config: "配置",
|
||||||
},
|
},
|
||||||
Exporter: {
|
Exporter: {
|
||||||
Model: "模型",
|
Model: "模型",
|
||||||
|
@ -181,7 +181,36 @@ const en: LocaleType = {
|
|||||||
SubTitle: "Generate a suitable title based on the conversation content",
|
SubTitle: "Generate a suitable title based on the conversation content",
|
||||||
},
|
},
|
||||||
Sync: {
|
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",
|
LocalState: "Local Data",
|
||||||
Overview: (overview: any) => {
|
Overview: (overview: any) => {
|
||||||
return `${overview.chat} chats,${overview.message} messages,${overview.prompt} prompts,${overview.mask} masks`;
|
return `${overview.chat} chats,${overview.message} messages,${overview.prompt} prompts,${overview.mask} masks`;
|
||||||
@ -366,6 +395,7 @@ const en: LocaleType = {
|
|||||||
Export: "Export",
|
Export: "Export",
|
||||||
Import: "Import",
|
Import: "Import",
|
||||||
Sync: "Sync",
|
Sync: "Sync",
|
||||||
|
Config: "Config",
|
||||||
},
|
},
|
||||||
Exporter: {
|
Exporter: {
|
||||||
Model: "Model",
|
Model: "Model",
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import { Updater } from "../typing";
|
import { Updater } from "../typing";
|
||||||
import { StoreKey } from "../constant";
|
import { ApiPath, StoreKey } from "../constant";
|
||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
import {
|
import {
|
||||||
AppState,
|
AppState,
|
||||||
getLocalAppState,
|
getLocalAppState,
|
||||||
|
GetStoreState,
|
||||||
mergeAppState,
|
mergeAppState,
|
||||||
setLocalAppState,
|
setLocalAppState,
|
||||||
} from "../utils/sync";
|
} from "../utils/sync";
|
||||||
import { downloadAs, readFromFile } from "../utils";
|
import { downloadAs, readFromFile } from "../utils";
|
||||||
import { showToast } from "../components/ui-lib";
|
import { showToast } from "../components/ui-lib";
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
|
import { createSyncClient, ProviderType } from "../utils/cloud";
|
||||||
|
import { corsPath } from "../utils/cors";
|
||||||
|
|
||||||
export interface WebDavConfig {
|
export interface WebDavConfig {
|
||||||
server: string;
|
server: string;
|
||||||
@ -17,22 +20,43 @@ export interface WebDavConfig {
|
|||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SyncStore = GetStoreState<typeof useSyncStore>;
|
||||||
|
|
||||||
export const useSyncStore = createPersistStore(
|
export const useSyncStore = createPersistStore(
|
||||||
{
|
{
|
||||||
webDavConfig: {
|
provider: ProviderType.WebDAV,
|
||||||
server: "",
|
useProxy: true,
|
||||||
|
proxyUrl: corsPath(ApiPath.Cors),
|
||||||
|
|
||||||
|
webdav: {
|
||||||
|
endpoint: "",
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
upstash: {
|
||||||
|
endpoint: "",
|
||||||
|
username: "",
|
||||||
|
apiKey: "",
|
||||||
|
},
|
||||||
|
|
||||||
lastSyncTime: 0,
|
lastSyncTime: 0,
|
||||||
|
lastProvider: "",
|
||||||
},
|
},
|
||||||
(set, get) => ({
|
(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() {
|
export() {
|
||||||
const state = getLocalAppState();
|
const state = getLocalAppState();
|
||||||
const fileName = `Backup-${new Date().toLocaleString()}.json`;
|
const fileName = `Backup-${new Date().toLocaleString()}.json`;
|
||||||
downloadAs(JSON.stringify(state), fileName);
|
downloadAs(JSON.stringify(state), fileName);
|
||||||
set({ lastSyncTime: Date.now() });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async import() {
|
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 {
|
try {
|
||||||
const res = await fetch(this.path(""), {
|
const remoteState = JSON.parse(
|
||||||
method: "PROFIND",
|
await client.get(config.username),
|
||||||
headers: this.headers(),
|
) as AppState;
|
||||||
});
|
mergeAppState(localState, remoteState);
|
||||||
const sanitizedRes = {
|
setLocalAppState(localState);
|
||||||
status: res.status,
|
|
||||||
statusText: res.statusText,
|
|
||||||
headers: res.headers,
|
|
||||||
};
|
|
||||||
console.log(sanitizedRes);
|
|
||||||
return res.status === 207;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("[Sync] ", e);
|
console.log("[Sync] failed to get remoate state", e);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await client.set(config.username, JSON.stringify(localState));
|
||||||
|
|
||||||
|
this.markSyncTime();
|
||||||
},
|
},
|
||||||
|
|
||||||
path(path: string) {
|
async check() {
|
||||||
let url = get().webDavConfig.server;
|
const client = this.getClient();
|
||||||
|
return await client.check();
|
||||||
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}`,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
|
33
app/utils/cloud/index.ts
Normal file
33
app/utils/cloud/index.ts
Normal file
@ -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<string>;
|
||||||
|
set: (key: string, value: string) => Promise<void>;
|
||||||
|
check: () => Promise<boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createSyncClient<T extends ProviderType>(
|
||||||
|
provider: T,
|
||||||
|
config: SyncClientConfig[T],
|
||||||
|
): SyncClient {
|
||||||
|
return SyncClients[provider](config as any) as any;
|
||||||
|
}
|
39
app/utils/cloud/upstash.ts
Normal file
39
app/utils/cloud/upstash.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { SyncStore } from "@/app/store/sync";
|
||||||
|
|
||||||
|
export type UpstashConfig = SyncStore["upstash"];
|
||||||
|
export type UpStashClient = ReturnType<typeof createUpstashClient>;
|
||||||
|
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
78
app/utils/cloud/webdav.ts
Normal file
78
app/utils/cloud/webdav.ts
Normal file
@ -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<typeof createWebDavClient>;
|
||||||
|
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
50
app/utils/cors.ts
Normal file
50
app/utils/cors.ts
Normal file
@ -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);
|
||||||
|
}
|
@ -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") {
|
if (mode !== "export") {
|
||||||
nextConfig.headers = async () => {
|
nextConfig.headers = async () => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
source: "/api/:path*",
|
source: "/api/:path*",
|
||||||
headers: [
|
headers: 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",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@ -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 {
|
return {
|
||||||
beforeFiles: ret,
|
beforeFiles: ret,
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user