Compare commits

..

10 Commits

Author SHA1 Message Date
Yifei Zhang
0f739f442e Merge pull request #697 from Yidadaa/bugfix-0410
fix: runtime config and proxy fix
2023-04-11 02:59:51 +08:00
Yidadaa
6841846613 fixup 2023-04-11 02:56:48 +08:00
Yidadaa
d6e6dd09f0 feat: dynamic config 2023-04-11 02:54:31 +08:00
Yidadaa
9b61cb1335 refactor: build/runtime/client configs 2023-04-11 01:21:34 +08:00
Yidadaa
7aee53ea05 fix: #507 break cjk chars in stream mode 2023-04-10 23:13:20 +08:00
Yidadaa
8df8ee8936 fix: #676 docker override old proxy files 2023-04-10 22:46:58 +08:00
Yifei Zhang
ec985f6a1d Merge pull request #679 from muhammetdemirel/main
Added new language, Turkish.
2023-04-10 18:03:39 +08:00
Yifei Zhang
2ec99bbb70 Update docker.yml 2023-04-10 17:39:21 +08:00
Muhammet Demirel
d7edcadec7 Added new language, Turkish. 2023-04-10 12:04:30 +03:00
Yifei Zhang
150735b001 Update access.ts 2023-04-10 10:57:16 +08:00
25 changed files with 365 additions and 114 deletions

View File

@@ -43,7 +43,7 @@ jobs:
uses: docker/build-push-action@v4 uses: docker/build-push-action@v4
with: with:
context: . context: .
platforms: linux/amd64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View File

@@ -17,7 +17,6 @@ RUN apk update && apk add --no-cache git
ENV OPENAI_API_KEY="" ENV OPENAI_API_KEY=""
ENV CODE="" ENV CODE=""
ARG DOCKER=true
WORKDIR /app WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
@@ -46,7 +45,7 @@ CMD if [ -n "$PROXY_URL" ]; then \
host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \ host=$(echo $PROXY_URL | cut -d/ -f3 | cut -d: -f1); \
port=$(echo $PROXY_URL | cut -d: -f3); \ port=$(echo $PROXY_URL | cut -d: -f3); \
conf=/etc/proxychains.conf; \ conf=/etc/proxychains.conf; \
echo "strict_chain" >> $conf; \ echo "strict_chain" > $conf; \
echo "proxy_dns" >> $conf; \ echo "proxy_dns" >> $conf; \
echo "remote_dns_subnet 224" >> $conf; \ echo "remote_dns_subnet 224" >> $conf; \
echo "tcp_read_time_out 15000" >> $conf; \ echo "tcp_read_time_out 15000" >> $conf; \

View File

@@ -1,17 +0,0 @@
import md5 from "spark-md5";
export function getAccessCodes(): Set<string> {
const code = process.env.CODE;
try {
const codes = (code?.split(",") ?? [])
.filter((v) => !!v)
.map((v) => md5.hash(v.trim()));
return new Set(codes);
} catch (e) {
return new Set();
}
}
export const ACCESS_CODES = getAccessCodes();
export const IS_IN_DOCKER = process.env.DOCKER;

View File

@@ -40,7 +40,7 @@ async function createStream(req: NextRequest) {
const parser = createParser(onParse); const parser = createParser(onParse);
for await (const chunk of res.body as any) { for await (const chunk of res.body as any) {
parser.feed(decoder.decode(chunk)); parser.feed(decoder.decode(chunk, { stream: true }));
} }
}, },
}); });

21
app/api/config/route.ts Normal file
View File

@@ -0,0 +1,21 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSideConfig } from "../../config/server";
const serverConfig = getServerSideConfig();
// Danger! Don not write any secret value here!
// 警告!不要在这里写入任何敏感信息!
const DANGER_CONFIG = {
needCode: serverConfig.needCode,
};
declare global {
type DangerConfig = typeof DANGER_CONFIG;
}
export async function POST(req: NextRequest) {
return NextResponse.json({
needCode: serverConfig.needCode,
});
}

View File

@@ -2,13 +2,7 @@
require("../polyfill"); require("../polyfill");
import { import { useState, useEffect, useRef } from "react";
useState,
useEffect,
useRef,
useCallback,
MouseEventHandler,
} from "react";
import { IconButton } from "./button"; import { IconButton } from "./button";
import styles from "./home.module.scss"; import styles from "./home.module.scss";
@@ -30,7 +24,6 @@ import { Chat } from "./chat";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { REPO_URL } from "../constant"; import { REPO_URL } from "../constant";
import { ErrorBoundary } from "./error"; import { ErrorBoundary } from "./error";
import { useDebounce } from "use-debounce";
export function Loading(props: { noLogo?: boolean }) { export function Loading(props: { noLogo?: boolean }) {
return ( return (

View File

@@ -26,7 +26,7 @@ import {
import { Avatar } from "./chat"; import { Avatar } from "./chat";
import Locale, { AllLangs, changeLang, getLang } from "../locales"; import Locale, { AllLangs, changeLang, getLang } from "../locales";
import { getCurrentVersion, getEmojiUrl } from "../utils"; import { getEmojiUrl } from "../utils";
import Link from "next/link"; import Link from "next/link";
import { UPDATE_URL } from "../constant"; import { UPDATE_URL } from "../constant";
import { SearchService, usePromptStore } from "../store/prompt"; import { SearchService, usePromptStore } from "../store/prompt";
@@ -88,13 +88,13 @@ export function Settings(props: { closeSettings: () => void }) {
const updateStore = useUpdateStore(); const updateStore = useUpdateStore();
const [checkingUpdate, setCheckingUpdate] = useState(false); const [checkingUpdate, setCheckingUpdate] = useState(false);
const currentId = getCurrentVersion(); const currentVersion = updateStore.version;
const remoteId = updateStore.remoteId; const remoteId = updateStore.remoteVersion;
const hasNewVersion = currentId !== remoteId; const hasNewVersion = currentVersion !== remoteId;
function checkUpdate(force = false) { function checkUpdate(force = false) {
setCheckingUpdate(true); setCheckingUpdate(true);
updateStore.getLatestCommitId(force).then(() => { updateStore.getLatestVersion(force).then(() => {
setCheckingUpdate(false); setCheckingUpdate(false);
}); });
} }
@@ -224,7 +224,7 @@ export function Settings(props: { closeSettings: () => void }) {
</SettingItem> </SettingItem>
<SettingItem <SettingItem
title={Locale.Settings.Update.Version(currentId)} title={Locale.Settings.Update.Version(currentVersion ?? "unknown")}
subTitle={ subTitle={
checkingUpdate checkingUpdate
? Locale.Settings.Update.IsChecking ? Locale.Settings.Update.IsChecking

27
app/config/build.ts Normal file
View File

@@ -0,0 +1,27 @@
const COMMIT_ID: string = (() => {
try {
const childProcess = require("child_process");
return (
childProcess
// .execSync("git describe --tags --abbrev=0")
.execSync("git rev-parse --short HEAD")
.toString()
.trim()
);
} catch (e) {
console.error("[Build Config] No git or not from git repo.");
return "unknown";
}
})();
export const getBuildConfig = () => {
if (typeof process === "undefined") {
throw Error(
"[Server Config] you are importing a nodejs-only module outside of nodejs",
);
}
return {
commitId: COMMIT_ID,
};
};

42
app/config/server.ts Normal file
View File

@@ -0,0 +1,42 @@
import md5 from "spark-md5";
declare global {
namespace NodeJS {
interface ProcessEnv {
OPENAI_API_KEY?: string;
CODE?: string;
PROXY_URL?: string;
VERCEL?: string;
}
}
}
const ACCESS_CODES = (function getAccessCodes(): Set<string> {
const code = process.env.CODE;
try {
const codes = (code?.split(",") ?? [])
.filter((v) => !!v)
.map((v) => md5.hash(v.trim()));
return new Set(codes);
} catch (e) {
return new Set();
}
})();
export const getServerSideConfig = () => {
if (typeof process === "undefined") {
throw Error(
"[Server Config] you are importing a nodejs-only module outside of nodejs",
);
}
return {
apiKey: process.env.OPENAI_API_KEY,
code: process.env.CODE,
codes: ACCESS_CODES,
needCode: ACCESS_CODES.size > 0,
proxyUrl: process.env.PROXY_URL,
isVercel: !!process.env.VERCEL,
};
};

View File

@@ -5,3 +5,4 @@ export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
export const UPDATE_URL = `${REPO_URL}#keep-updated`; export const UPDATE_URL = `${REPO_URL}#keep-updated`;
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";

View File

@@ -2,19 +2,9 @@
import "./styles/globals.scss"; import "./styles/globals.scss";
import "./styles/markdown.scss"; import "./styles/markdown.scss";
import "./styles/highlight.scss"; import "./styles/highlight.scss";
import process from "child_process"; import { getBuildConfig } from "./config/build";
import { ACCESS_CODES, IS_IN_DOCKER } from "./api/access";
let COMMIT_ID: string | undefined; const buildConfig = getBuildConfig();
try {
COMMIT_ID = process
// .execSync("git describe --tags --abbrev=0")
.execSync("git rev-parse --short HEAD")
.toString()
.trim();
} catch (e) {
console.error("No git or not from git repo.");
}
export const metadata = { export const metadata = {
title: "ChatGPT Next Web", title: "ChatGPT Next Web",
@@ -26,21 +16,6 @@ export const metadata = {
themeColor: "#fafafa", themeColor: "#fafafa",
}; };
function Meta() {
const metas = {
version: COMMIT_ID ?? "unknown",
access: ACCESS_CODES.size > 0 || IS_IN_DOCKER ? "enabled" : "disabled",
};
return (
<>
{Object.entries(metas).map(([k, v]) => (
<meta name={k} content={v} key={k} />
))}
</>
);
}
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
@@ -58,7 +33,7 @@ export default function RootLayout({
content="#151515" content="#151515"
media="(prefers-color-scheme: dark)" media="(prefers-color-scheme: dark)"
/> />
<Meta /> <meta name="version" content={buildConfig.commitId} />
<link rel="manifest" href="/site.webmanifest"></link> <link rel="manifest" href="/site.webmanifest"></link>
<link rel="preconnect" href="https://fonts.googleapis.com"></link> <link rel="preconnect" href="https://fonts.googleapis.com"></link>
<link rel="preconnect" href="https://fonts.gstatic.com"></link> <link rel="preconnect" href="https://fonts.gstatic.com"></link>

View File

@@ -72,6 +72,7 @@ const cn = {
tw: "繁體中文", tw: "繁體中文",
es: "Español", es: "Español",
it: "Italiano", it: "Italiano",
tr: "Türkçe",
}, },
}, },
Avatar: "头像", Avatar: "头像",

View File

@@ -75,6 +75,7 @@ const en: LocaleType = {
tw: "繁體中文", tw: "繁體中文",
es: "Español", es: "Español",
it: "Italiano", it: "Italiano",
tr: "Türkçe",
}, },
}, },
Avatar: "Avatar", Avatar: "Avatar",

View File

@@ -75,6 +75,7 @@ const es: LocaleType = {
tw: "繁體中文", tw: "繁體中文",
es: "Español", es: "Español",
it: "Italiano", it: "Italiano",
tr: "Türkçe",
}, },
}, },
Avatar: "Avatar", Avatar: "Avatar",

View File

@@ -3,10 +3,11 @@ import EN from "./en";
import TW from "./tw"; import TW from "./tw";
import ES from "./es"; import ES from "./es";
import IT from "./it"; import IT from "./it";
import TR from "./tr";
export type { LocaleType } from "./cn"; export type { LocaleType } from "./cn";
export const AllLangs = ["en", "cn", "tw", "es", "it"] as const; export const AllLangs = ["en", "cn", "tw", "es", "it", "tr"] as const;
type Lang = (typeof AllLangs)[number]; type Lang = (typeof AllLangs)[number];
const LANG_KEY = "lang"; const LANG_KEY = "lang";
@@ -50,6 +51,8 @@ export function getLang(): Lang {
return "es"; return "es";
} else if (lang.includes("it")) { } else if (lang.includes("it")) {
return "it"; return "it";
} else if (lang.includes("tr")) {
return "tr";
} else { } else {
return "en"; return "en";
} }
@@ -60,4 +63,4 @@ export function changeLang(lang: Lang) {
location.reload(); location.reload();
} }
export default { en: EN, cn: CN, tw: TW, es: ES, it: IT }[getLang()]; export default { en: EN, cn: CN, tw: TW, es: ES, it: IT, tr: TR }[getLang()];

View File

@@ -75,6 +75,7 @@ const it: LocaleType = {
tw: "繁體中文", tw: "繁體中文",
es: "Español", es: "Español",
it: "Italiano", it: "Italiano",
tr: "Türkçe",
}, },
}, },
Avatar: "Avatar", Avatar: "Avatar",

177
app/locales/tr.ts Normal file
View File

@@ -0,0 +1,177 @@
import { SubmitKey } from "../store/app";
import type { LocaleType } from "./index";
const tr: LocaleType = {
WIP: "Çalışma devam ediyor...",
Error: {
Unauthorized:
"Yetkisiz erişim, lütfen erişim kodunu ayarlar sayfasından giriniz.",
},
ChatItem: {
ChatItemCount: (count: number) => `${count} mesaj`,
},
Chat: {
SubTitle: (count: number) => `ChatGPT tarafından ${count} mesaj`,
Actions: {
ChatList: "Sohbet Listesine Git",
CompressedHistory: "Sıkıştırılmış Geçmiş Bellek Komutu",
Export: "Tüm Mesajları Markdown Olarak Dışa Aktar",
Copy: "Kopyala",
Stop: "Durdur",
Retry: "Tekrar Dene",
},
Rename: "Sohbeti Yeniden Adlandır",
Typing: "Yazıyor…",
Input: (submitKey: string) => {
var inputHints = `Göndermek için ${submitKey}`;
if (submitKey === String(SubmitKey.Enter)) {
inputHints += ", kaydırmak için Shift + Enter";
}
return inputHints + ", komutları aramak için / (eğik çizgi)";
},
Send: "Gönder",
},
Export: {
Title: "Tüm Mesajlar",
Copy: "Tümünü Kopyala",
Download: "İndir",
MessageFromYou: "Sizin Mesajınız",
MessageFromChatGPT: "ChatGPT'nin Mesajı",
},
Memory: {
Title: "Bellek Komutları",
EmptyContent: "Henüz değil.",
Send: "Belleği Gönder",
Copy: "Belleği Kopyala",
Reset: "Oturumu Sıfırla",
ResetConfirm:
"Sıfırlama, geçerli görüşme geçmişini ve geçmiş belleği siler. Sıfırlamak istediğinizden emin misiniz?",
},
Home: {
NewChat: "Yeni Sohbet",
DeleteChat: "Seçili sohbeti silmeyi onaylıyor musunuz?",
DeleteToast: "Sohbet Silindi",
Revert: "Geri Al",
},
Settings: {
Title: "Ayarlar",
SubTitle: "Tüm Ayarlar",
Actions: {
ClearAll: "Tüm Verileri Temizle",
ResetAll: "Tüm Ayarları Sıfırla",
Close: "Kapat",
ConfirmResetAll: {
Confirm: "Tüm ayarları sıfırlamak istediğinizden emin misiniz?",
},
ConfirmClearAll: {
Confirm: "Tüm sohbeti sıfırlamak istediğinizden emin misiniz?",
},
},
Lang: {
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
Options: {
cn: "简体中文",
en: "English",
tw: "繁體中文",
es: "Español",
it: "Italiano",
tr: "Türkçe",
},
},
Avatar: "Avatar",
FontSize: {
Title: "Yazı Boyutu",
SubTitle: "Sohbet içeriğinin yazı boyutunu ayarlayın",
},
Update: {
Version: (x: string) => `Sürüm: ${x}`,
IsLatest: "En son sürüm",
CheckUpdate: "Güncellemeyi Kontrol Et",
IsChecking: "Güncelleme kontrol ediliyor...",
FoundUpdate: (x: string) => `Yeni sürüm bulundu: ${x}`,
GoToUpdate: "Güncelle",
},
SendKey: "Gönder Tuşu",
Theme: "Tema",
TightBorder: "Tam Ekran",
SendPreviewBubble: "Mesaj Önizleme Balonu",
Prompt: {
Disable: {
Title: "Otomatik tamamlamayı devre dışı bırak",
SubTitle: "Otomatik tamamlamayı kullanmak için / (eğik çizgi) girin",
},
List: "Komut Listesi",
ListCount: (builtin: number, custom: number) =>
`${builtin} yerleşik, ${custom} kullanıcı tanımlı`,
Edit: "Düzenle",
},
HistoryCount: {
Title: "Ekli Mesaj Sayısı",
SubTitle: "İstek başına ekli gönderilen mesaj sayısı",
},
CompressThreshold: {
Title: "Geçmiş Sıkıştırma Eşiği",
SubTitle:
"Sıkıştırılmamış mesajların uzunluğu bu değeri aşarsa sıkıştırılır",
},
Token: {
Title: "API Anahtarı",
SubTitle: "Erişim kodu sınırını yoksaymak için anahtarınızı kullanın",
Placeholder: "OpenAI API Anahtarı",
},
Usage: {
Title: "Hesap Bakiyesi",
SubTitle(used: any, total: any) {
return `Bu ay kullanılan $${used}, abonelik $${total}`;
},
IsChecking: "Kontrol ediliyor...",
Check: "Tekrar Kontrol Et",
NoAccess: "Bakiyeyi kontrol etmek için API anahtarını girin",
},
AccessCode: {
Title: "Erişim Kodu",
SubTitle: "Erişim kontrolü etkinleştirme",
Placeholder: "Erişim Kodu Gerekiyor",
},
Model: "Model",
Temperature: {
Title: "Gerçeklik",
SubTitle: "Daha büyük bir değer girildiğinde gerçeklik oranı düşer ve daha rastgele çıktılar üretir",
},
MaxTokens: {
Title: "Maksimum Belirteç",
SubTitle: "Girdi belirteçlerinin ve oluşturulan belirteçlerin maksimum uzunluğu",
},
PresencePenlty: {
Title: "Varlık Cezası",
SubTitle:
"Daha büyük bir değer, yeni konular hakkında konuşma olasılığını artırır",
},
},
Store: {
DefaultTopic: "Yeni Konuşma",
BotHello: "Merhaba! Size bugün nasıl yardımcı olabilirim?",
Error: "Bir şeyler yanlış gitti. Lütfen daha sonra tekrar deneyiniz.",
Prompt: {
History: (content: string) =>
"Bu, yapay zeka ile kullanıcı arasındaki sohbet geçmişinin bir özetidir: " +
content,
Topic:
"Lütfen herhangi bir giriş, noktalama işareti, tırnak işareti, nokta, sembol veya ek metin olmadan konuşmamızı özetleyen dört ila beş kelimelik bir başlık oluşturun. Çevreleyen tırnak işaretlerini kaldırın.",
Summarize:
"Gelecekteki bağlam için bir bilgi istemi olarak kullanmak üzere tartışmamızı en fazla 200 kelimeyle özetleyin.",
},
ConfirmClearAll: "Tüm sohbet ve ayar verilerini temizlemeyi onaylıyor musunuz?",
},
Copy: {
Success: "Panoya kopyalandı",
Failed: "Kopyalama başarısız oldu, lütfen panoya erişim izni verin",
},
Context: {
Toast: (x: any) => `${x} bağlamsal bellek komutu`,
Edit: "Bağlamsal ve Bellek Komutları",
Add: "Yeni Ekle",
},
};
export default tr;

View File

@@ -73,6 +73,7 @@ const tw: LocaleType = {
tw: "繁體中文", tw: "繁體中文",
es: "Español", es: "Español",
it: "Italiano", it: "Italiano",
tr: "Türkçe",
}, },
}, },
Avatar: "大頭貼", Avatar: "大頭貼",

View File

@@ -2,11 +2,15 @@ import { Analytics } from "@vercel/analytics/react";
import { Home } from "./components/home"; import { Home } from "./components/home";
export default function App() { import { getServerSideConfig } from "./config/server";
const serverConfig = getServerSideConfig();
export default async function App() {
return ( return (
<> <>
<Home /> <Home />
<Analytics /> {serverConfig?.isVercel && <Analytics />}
</> </>
); );
} }

View File

@@ -171,7 +171,7 @@ export async function requestChatStream(
const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS); const resTimeoutId = setTimeout(() => finish(), TIME_OUT_MS);
const content = await reader?.read(); const content = await reader?.read();
clearTimeout(resTimeoutId); clearTimeout(resTimeoutId);
const text = decoder.decode(content?.value); const text = decoder.decode(content?.value, { stream: true });
responseText += text; responseText += text;
const done = !content || content.done; const done = !content || content.done;

View File

@@ -1,26 +1,33 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { queryMeta } from "../utils";
export interface AccessControlStore { export interface AccessControlStore {
accessCode: string; accessCode: string;
token: string; token: string;
needCode: boolean;
updateToken: (_: string) => void; updateToken: (_: string) => void;
updateCode: (_: string) => void; updateCode: (_: string) => void;
enabledAccessControl: () => boolean; enabledAccessControl: () => boolean;
isAuthorized: () => boolean; isAuthorized: () => boolean;
fetch: () => void;
} }
export const ACCESS_KEY = "access-control"; export const ACCESS_KEY = "access-control";
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
export const useAccessStore = create<AccessControlStore>()( export const useAccessStore = create<AccessControlStore>()(
persist( persist(
(set, get) => ({ (set, get) => ({
token: "", token: "",
accessCode: "", accessCode: "",
needCode: true,
enabledAccessControl() { enabledAccessControl() {
return queryMeta("access") === "enabled"; get().fetch();
return get().needCode;
}, },
updateCode(code: string) { updateCode(code: string) {
set((state) => ({ accessCode: code })); set((state) => ({ accessCode: code }));
@@ -29,7 +36,29 @@ export const useAccessStore = create<AccessControlStore>()(
set((state) => ({ token })); set((state) => ({ token }));
}, },
isAuthorized() { isAuthorized() {
return !!get().token || !!get().accessCode; // has token or has code or disabled access control
return (
!!get().token || !!get().accessCode || !get().enabledAccessControl()
);
},
fetch() {
if (fetchState > 0) return;
fetchState = 1;
fetch("/api/config", {
method: "post",
body: null,
})
.then((res) => res.json())
.then((res: DangerConfig) => {
console.log("[Config] got config from server", res);
set(() => ({ ...res }));
})
.catch(() => {
console.error("[Config] failed to fetch config");
})
.finally(() => {
fetchState = 2;
});
}, },
}), }),
{ {

View File

@@ -1,28 +1,46 @@
import { create } from "zustand"; import { create } from "zustand";
import { persist } from "zustand/middleware"; import { persist } from "zustand/middleware";
import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant"; import { FETCH_COMMIT_URL, FETCH_TAG_URL } from "../constant";
import { getCurrentVersion } from "../utils";
export interface UpdateStore { export interface UpdateStore {
lastUpdate: number; lastUpdate: number;
remoteId: string; remoteVersion: string;
getLatestCommitId: (force: boolean) => Promise<string>; version: string;
getLatestVersion: (force: boolean) => Promise<string>;
} }
export const UPDATE_KEY = "chat-update"; export const UPDATE_KEY = "chat-update";
function queryMeta(key: string, defaultValue?: string): string {
let ret: string;
if (document) {
const meta = document.head.querySelector(
`meta[name='${key}']`,
) as HTMLMetaElement;
ret = meta?.content ?? "";
} else {
ret = defaultValue ?? "";
}
return ret;
}
export const useUpdateStore = create<UpdateStore>()( export const useUpdateStore = create<UpdateStore>()(
persist( persist(
(set, get) => ({ (set, get) => ({
lastUpdate: 0, lastUpdate: 0,
remoteId: "", remoteVersion: "",
version: "unknown",
async getLatestVersion(force = false) {
set(() => ({ version: queryMeta("version") }));
async getLatestCommitId(force = false) {
const overTenMins = Date.now() - get().lastUpdate > 10 * 60 * 1000; const overTenMins = Date.now() - get().lastUpdate > 10 * 60 * 1000;
const shouldFetch = force || overTenMins; const shouldFetch = force || overTenMins;
if (!shouldFetch) { if (!shouldFetch) {
return getCurrentVersion(); return get().version ?? "unknown";
} }
try { try {
@@ -32,13 +50,13 @@ export const useUpdateStore = create<UpdateStore>()(
const remoteId = (data[0].sha as string).substring(0, 7); const remoteId = (data[0].sha as string).substring(0, 7);
set(() => ({ set(() => ({
lastUpdate: Date.now(), lastUpdate: Date.now(),
remoteId, remoteVersion: remoteId,
})); }));
console.log("[Got Upstream] ", remoteId); console.log("[Got Upstream] ", remoteId);
return remoteId; return remoteId;
} catch (error) { } catch (error) {
console.error("[Fetch Upstream Commit Id]", error); console.error("[Fetch Upstream Commit Id]", error);
return getCurrentVersion(); return get().version ?? "";
} }
}, },
}), }),

View File

@@ -69,31 +69,6 @@ export function selectOrCopy(el: HTMLElement, content: string) {
return true; return true;
} }
export function queryMeta(key: string, defaultValue?: string): string {
let ret: string;
if (document) {
const meta = document.head.querySelector(
`meta[name='${key}']`,
) as HTMLMetaElement;
ret = meta?.content ?? "";
} else {
ret = defaultValue ?? "";
}
return ret;
}
let currentId: string;
export function getCurrentVersion() {
if (currentId) {
return currentId;
}
currentId = queryMeta("version");
return currentId;
}
export function getEmojiUrl(unified: string, style: EmojiStyle) { export function getEmojiUrl(unified: string, style: EmojiStyle) {
return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`; return `https://cdn.staticfile.org/emoji-datasource-apple/14.0.0/img/${style}/64/${unified}.png`;
} }

View File

@@ -1,21 +1,23 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { ACCESS_CODES } from "./app/api/access"; import { getServerSideConfig } from "./app/config/server";
import md5 from "spark-md5"; import md5 from "spark-md5";
export const config = { export const config = {
matcher: ["/api/openai", "/api/chat-stream"], matcher: ["/api/openai", "/api/chat-stream"],
}; };
const serverConfig = getServerSideConfig();
export function middleware(req: NextRequest) { export function middleware(req: NextRequest) {
const accessCode = req.headers.get("access-code"); const accessCode = req.headers.get("access-code");
const token = req.headers.get("token"); const token = req.headers.get("token");
const hashedCode = md5.hash(accessCode ?? "").trim(); const hashedCode = md5.hash(accessCode ?? "").trim();
console.log("[Auth] allowed hashed codes: ", [...ACCESS_CODES]); console.log("[Auth] allowed hashed codes: ", [...serverConfig.codes]);
console.log("[Auth] got access code:", accessCode); console.log("[Auth] got access code:", accessCode);
console.log("[Auth] hashed access code:", hashedCode); console.log("[Auth] hashed access code:", hashedCode);
if (ACCESS_CODES.size > 0 && !ACCESS_CODES.has(hashedCode) && !token) { if (serverConfig.needCode && !serverConfig.codes.has(hashedCode) && !token) {
return NextResponse.json( return NextResponse.json(
{ {
error: true, error: true,
@@ -30,7 +32,7 @@ export function middleware(req: NextRequest) {
// inject api key // inject api key
if (!token) { if (!token) {
const apiKey = process.env.OPENAI_API_KEY; const apiKey = serverConfig.apiKey;
if (apiKey) { if (apiKey) {
console.log("[Auth] set system token"); console.log("[Auth] set system token");
req.headers.set("token", apiKey); req.headers.set("token", apiKey);

View File

@@ -8,14 +8,11 @@ const nextConfig = {
config.module.rules.push({ config.module.rules.push({
test: /\.svg$/, test: /\.svg$/,
use: ["@svgr/webpack"], use: ["@svgr/webpack"],
}); // 针对 SVG 的处理规则 });
return config; return config;
} },
output: "standalone",
}; };
if (process.env.DOCKER) {
nextConfig.output = 'standalone'
}
module.exports = nextConfig; module.exports = nextConfig;