From fb5e9e5aed796129714bd4ceeef581d2bc9bd1f3 Mon Sep 17 00:00:00 2001 From: JiangYinjin Date: Thu, 26 Dec 2024 03:33:24 +0800 Subject: [PATCH 1/8] fix: allow isVisionModel function read runtime env var VISION_MODELS --- app/api/config/route.ts | 1 + app/client/platforms/anthropic.ts | 7 +++++-- app/client/platforms/google.ts | 2 +- app/client/platforms/openai.ts | 7 ++++++- app/client/platforms/tencent.ts | 6 +++++- app/components/chat.tsx | 11 +++++++---- app/config/server.ts | 7 ++++++- app/store/access.ts | 1 + app/utils.ts | 14 +++++++++----- test/vision-model-checker.test.ts | 23 +++++++++++++---------- 10 files changed, 54 insertions(+), 25 deletions(-) diff --git a/app/api/config/route.ts b/app/api/config/route.ts index b0d9da03103..445dae30c1f 100644 --- a/app/api/config/route.ts +++ b/app/api/config/route.ts @@ -13,6 +13,7 @@ const DANGER_CONFIG = { hideBalanceQuery: serverConfig.hideBalanceQuery, disableFastLink: serverConfig.disableFastLink, customModels: serverConfig.customModels, + visionModels: serverConfig.visionModels, defaultModel: serverConfig.defaultModel, }; diff --git a/app/client/platforms/anthropic.ts b/app/client/platforms/anthropic.ts index 6747221a861..6e619a4d9c4 100644 --- a/app/client/platforms/anthropic.ts +++ b/app/client/platforms/anthropic.ts @@ -84,10 +84,13 @@ export class ClaudeApi implements LLMApi { return res?.content?.[0]?.text; } async chat(options: ChatOptions): Promise { - const visionModel = isVisionModel(options.config.model); - const accessStore = useAccessStore.getState(); + const visionModel = isVisionModel( + options.config.model, + accessStore.visionModels, + ); + const shouldStream = !!options.config.stream; const modelConfig = { diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index a7bce4fc2d0..c10e4969d44 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -83,7 +83,7 @@ export class GeminiProApi implements LLMApi { } const messages = _messages.map((v) => { let parts: any[] = [{ text: getMessageTextContent(v) }]; - if (isVisionModel(options.config.model)) { + if (isVisionModel(options.config.model, accessStore.visionModels)) { const images = getMessageImages(v); if (images.length > 0) { multimodal = true; diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 15cfb7ca602..6d154251eb8 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -194,6 +194,8 @@ export class ChatGPTApi implements LLMApi { let requestPayload: RequestPayload | DalleRequestPayload; + const accessStore = useAccessStore.getState(); + const isDalle3 = _isDalle3(options.config.model); const isO1 = options.config.model.startsWith("o1"); if (isDalle3) { @@ -211,7 +213,10 @@ export class ChatGPTApi implements LLMApi { style: options.config?.style ?? "vivid", }; } else { - const visionModel = isVisionModel(options.config.model); + const visionModel = isVisionModel( + options.config.model, + accessStore.visionModels, + ); const messages: ChatOptions["messages"] = []; for (const v of options.messages) { const content = visionModel diff --git a/app/client/platforms/tencent.ts b/app/client/platforms/tencent.ts index 580844a5b31..5a1f39b392a 100644 --- a/app/client/platforms/tencent.ts +++ b/app/client/platforms/tencent.ts @@ -94,7 +94,11 @@ export class HunyuanApi implements LLMApi { } async chat(options: ChatOptions) { - const visionModel = isVisionModel(options.config.model); + const accessStore = useAccessStore.getState(); + const visionModel = isVisionModel( + options.config.model, + accessStore.visionModels, + ); const messages = options.messages.map((v, index) => ({ // "Messages 中 system 角色必须位于列表的最开始" role: index !== 0 && v.role === "system" ? "user" : v.role, diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 51fe74fe7be..7bb3b9586b9 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -490,6 +490,7 @@ export function ChatActions(props: { const currentProviderName = session.mask.modelConfig?.providerName || ServiceProvider.OpenAI; const allModels = useAllModels(); + const customVisionModels = useAccessStore().visionModels; const models = useMemo(() => { const filteredModels = allModels.filter((m) => m.available); const defaultModel = filteredModels.find((m) => m.isDefault); @@ -529,7 +530,7 @@ export function ChatActions(props: { const isMobileScreen = useMobileScreen(); useEffect(() => { - const show = isVisionModel(currentModel); + const show = isVisionModel(currentModel, customVisionModels); setShowUploadImage(show); if (!show) { props.setAttachImages([]); @@ -1457,10 +1458,12 @@ function _Chat() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const customVisionModels = useAccessStore().visionModels; + const handlePaste = useCallback( async (event: React.ClipboardEvent) => { const currentModel = chatStore.currentSession().mask.modelConfig.model; - if (!isVisionModel(currentModel)) { + if (!isVisionModel(currentModel, customVisionModels)) { return; } const items = (event.clipboardData || window.clipboardData).items; @@ -1497,7 +1500,7 @@ function _Chat() { } } }, - [attachImages, chatStore], + [attachImages, chatStore, customVisionModels], ); async function uploadImage() { @@ -1545,7 +1548,7 @@ function _Chat() { setAttachImages(images); } - // 快捷键 shortcut keys + // 捷键 shortcut keys const [showShortcutKeyModal, setShowShortcutKeyModal] = useState(false); useEffect(() => { diff --git a/app/config/server.ts b/app/config/server.ts index 9d6b3c2b8da..7f93822d5b5 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -21,6 +21,7 @@ declare global { ENABLE_BALANCE_QUERY?: string; // allow user to query balance or not DISABLE_FAST_LINK?: string; // disallow parse settings from url or not CUSTOM_MODELS?: string; // to control custom models + VISION_MODELS?: string; // to control vision models DEFAULT_MODEL?: string; // to control default model in every new chat window // stability only @@ -123,13 +124,16 @@ export const getServerSideConfig = () => { const disableGPT4 = !!process.env.DISABLE_GPT4; let customModels = process.env.CUSTOM_MODELS ?? ""; + let visionModels = process.env.VISION_MODELS ?? ""; let defaultModel = process.env.DEFAULT_MODEL ?? ""; if (disableGPT4) { if (customModels) customModels += ","; customModels += DEFAULT_MODELS.filter( (m) => - (m.name.startsWith("gpt-4") || m.name.startsWith("chatgpt-4o") || m.name.startsWith("o1")) && + (m.name.startsWith("gpt-4") || + m.name.startsWith("chatgpt-4o") || + m.name.startsWith("o1")) && !m.name.startsWith("gpt-4o-mini"), ) .map((m) => "-" + m.name) @@ -247,6 +251,7 @@ export const getServerSideConfig = () => { hideBalanceQuery: !process.env.ENABLE_BALANCE_QUERY, disableFastLink: !!process.env.DISABLE_FAST_LINK, customModels, + visionModels, defaultModel, allowedWebDavEndpoints, }; diff --git a/app/store/access.ts b/app/store/access.ts index 4796b2fe84e..82cea5236ec 100644 --- a/app/store/access.ts +++ b/app/store/access.ts @@ -123,6 +123,7 @@ const DEFAULT_ACCESS_STATE = { disableGPT4: false, disableFastLink: false, customModels: "", + visionModels: "", defaultModel: "", // tts config diff --git a/app/utils.ts b/app/utils.ts index 962e68a101c..30df0a5999c 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -7,6 +7,7 @@ import { ServiceProvider } from "./constant"; import { fetch as tauriStreamFetch } from "./utils/stream"; import { VISION_MODEL_REGEXES, EXCLUDE_VISION_MODEL_REGEXES } from "./constant"; import { getClientConfig } from "./config/client"; +import { getModelProvider } from "./utils/model"; export function trimTopic(topic: string) { // Fix an issue where double quotes still show in the Indonesian language @@ -253,12 +254,15 @@ export function getMessageImages(message: RequestMessage): string[] { return urls; } -export function isVisionModel(model: string) { +export function isVisionModel(model: string, customVisionModels: string) { const clientConfig = getClientConfig(); - const envVisionModels = clientConfig?.visionModels - ?.split(",") - .map((m) => m.trim()); - if (envVisionModels?.includes(model)) { + const allVisionModelsList = [customVisionModels, clientConfig?.visionModels] + ?.join(",") + .split(",") + .map((m) => m.trim()) + .filter(Boolean) + .map((m) => getModelProvider(m)[0]); + if (allVisionModelsList?.includes(model)) { return true; } return ( diff --git a/test/vision-model-checker.test.ts b/test/vision-model-checker.test.ts index 734e992d829..5e5ffe56700 100644 --- a/test/vision-model-checker.test.ts +++ b/test/vision-model-checker.test.ts @@ -2,6 +2,7 @@ import { isVisionModel } from "../app/utils"; describe("isVisionModel", () => { const originalEnv = process.env; + const customVisionModels = "custom-vlm,another-vlm"; beforeEach(() => { jest.resetModules(); @@ -27,12 +28,12 @@ describe("isVisionModel", () => { ]; visionModels.forEach((model) => { - expect(isVisionModel(model)).toBe(true); + expect(isVisionModel(model, customVisionModels)).toBe(true); }); }); test("should exclude specific models", () => { - expect(isVisionModel("claude-3-5-haiku-20241022")).toBe(false); + expect(isVisionModel("claude-3-5-haiku-20241022", customVisionModels)).toBe(false); }); test("should not identify non-vision models", () => { @@ -44,24 +45,26 @@ describe("isVisionModel", () => { ]; nonVisionModels.forEach((model) => { - expect(isVisionModel(model)).toBe(false); + expect(isVisionModel(model, customVisionModels)).toBe(false); }); }); test("should identify models from VISION_MODELS env var", () => { process.env.VISION_MODELS = "custom-vision-model,another-vision-model"; - - expect(isVisionModel("custom-vision-model")).toBe(true); - expect(isVisionModel("another-vision-model")).toBe(true); - expect(isVisionModel("unrelated-model")).toBe(false); + + expect(isVisionModel("custom-vision-model", customVisionModels)).toBe(true); + expect(isVisionModel("another-vision-model", customVisionModels)).toBe(true); + expect(isVisionModel("custom-vlm", customVisionModels)).toBe(true); + expect(isVisionModel("another-vlm", customVisionModels)).toBe(true); + expect(isVisionModel("unrelated-model", customVisionModels)).toBe(false); }); test("should handle empty or missing VISION_MODELS", () => { process.env.VISION_MODELS = ""; - expect(isVisionModel("unrelated-model")).toBe(false); + expect(isVisionModel("unrelated-model", customVisionModels)).toBe(false); delete process.env.VISION_MODELS; - expect(isVisionModel("unrelated-model")).toBe(false); - expect(isVisionModel("gpt-4-vision")).toBe(true); + expect(isVisionModel("unrelated-model", customVisionModels)).toBe(false); + expect(isVisionModel("gpt-4-vision", customVisionModels)).toBe(true); }); }); \ No newline at end of file From 1aa647688f5606390b9fad68a7837f112eec966a Mon Sep 17 00:00:00 2001 From: JiangYingjin Date: Sun, 2 Mar 2025 01:23:27 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/store/config.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/store/config.ts b/app/store/config.ts index 4256eba925d..4c2bf30cd33 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -4,7 +4,6 @@ import { getClientConfig } from "../config/client"; import { DEFAULT_INPUT_TEMPLATE, DEFAULT_MODELS, - DEFAULT_SIDEBAR_WIDTH, DEFAULT_TTS_ENGINE, DEFAULT_TTS_ENGINES, DEFAULT_TTS_MODEL, @@ -46,18 +45,20 @@ export const DEFAULT_CONFIG = { fontSize: 14, fontFamily: "", theme: Theme.Auto as Theme, - tightBorder: !!config?.isApp, - sendPreviewBubble: true, + // tightBorder: !!config?.isApp, + tightBorder: true, + sendPreviewBubble: false, enableAutoGenerateTitle: true, - sidebarWidth: DEFAULT_SIDEBAR_WIDTH, + // sidebarWidth: DEFAULT_SIDEBAR_WIDTH, + sidebarWidth: 100, enableArtifacts: true, // show artifacts config enableCodeFold: true, // code fold config - disablePromptHint: false, + disablePromptHint: true, - dontShowMaskSplashScreen: false, // dont show splash screen when create chat + dontShowMaskSplashScreen: true, // dont show splash screen when create chat hideBuiltinMasks: false, // dont add builtin masks customModels: "", @@ -68,12 +69,12 @@ export const DEFAULT_CONFIG = { providerName: "OpenAI" as ServiceProvider, temperature: 0.5, top_p: 1, - max_tokens: 4000, + max_tokens: 8000, presence_penalty: 0, frequency_penalty: 0, sendMemory: true, - historyMessageCount: 4, - compressMessageLengthThreshold: 1000, + historyMessageCount: 16, + compressMessageLengthThreshold: 1000000, compressModel: "", compressProviderName: "", enableInjectSystemPrompts: true, From a5289b39d04c871e522a42f30be59a71d05c1a1c Mon Sep 17 00:00:00 2001 From: JiangYingjin Date: Sun, 2 Mar 2025 01:46:03 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E4=B8=80=E9=94=AE=E5=A1=AB=E5=85=A5=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/chat.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 7bb3b9586b9..a701e018192 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1411,6 +1411,7 @@ function _Chat() { const payload = JSON.parse(text) as { key?: string; url?: string; + code?: string; }; console.log("[Command] got settings from url: ", payload); @@ -1432,6 +1433,10 @@ function _Chat() { accessStore.update((access) => (access.useCustomConfig = true)); }); } + + if (payload.code) { + accessStore.update((access) => (access.accessCode = payload.code!)); + } } catch { console.error("[Command] failed to get settings from url: ", text); } From d08af47342d3af2fed6c234f62b357f169db5e8f Mon Sep 17 00:00:00 2001 From: JiangYingjin Date: Sun, 2 Mar 2025 02:27:17 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=80=E9=94=AE?= =?UTF-8?q?=E5=A1=AB=E5=85=A5=E8=BF=87=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/chat.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/components/chat.tsx b/app/components/chat.tsx index a701e018192..ff38b262951 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1436,6 +1436,12 @@ function _Chat() { if (payload.code) { accessStore.update((access) => (access.accessCode = payload.code!)); + if (accessStore.isAuthorized()) { + context.pop(); + const copiedHello = Object.assign({}, BOT_HELLO); + context.push(copiedHello); + setUserInput(" "); + } } } catch { console.error("[Command] failed to get settings from url: ", text); From 1cccaa2e804a875697e7ffe6e329a33a45c65daa Mon Sep 17 00:00:00 2001 From: JiangYingjin Date: Sun, 2 Mar 2025 12:25:33 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/settings.tsx | 15 ++++++++++ app/locales/cn.ts | 1 + app/locales/en.ts | 1 + app/locales/fr.ts | 1 + app/locales/it.ts | 1 + app/locales/pt.ts | 1 + app/store/sync.ts | 20 ++++++++----- app/utils/sync.ts | 56 ++++++++++++++++++------------------- 8 files changed, 61 insertions(+), 35 deletions(-) diff --git a/app/components/settings.tsx b/app/components/settings.tsx index a74ff17b1f5..6c193702c25 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -528,6 +528,21 @@ function SyncItems() { setShowSyncConfigModal(true); }} /> + {couldSync && ( + } + text={Locale.UI.Overwrite} + onClick={async () => { + try { + await syncStore.overwrite(); + showToast(Locale.Settings.Sync.Success); + } catch (e) { + showToast(Locale.Settings.Sync.Fail); + console.error("[Sync]", e); + } + }} + /> + )} {couldSync && ( } diff --git a/app/locales/cn.ts b/app/locales/cn.ts index 47be019a809..4d75b529d4c 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -757,6 +757,7 @@ const cn = { Export: "导出", Import: "导入", Sync: "同步", + Overwrite: "覆盖", Config: "配置", }, Exporter: { diff --git a/app/locales/en.ts b/app/locales/en.ts index fddb6f09153..3ae1eb20878 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -762,6 +762,7 @@ const en: LocaleType = { Edit: "Edit", Export: "Export", Import: "Import", + Overwrite: "Overwrite", Sync: "Sync", Config: "Config", }, diff --git a/app/locales/fr.ts b/app/locales/fr.ts index d25c60eb6c8..5157bedca9d 100644 --- a/app/locales/fr.ts +++ b/app/locales/fr.ts @@ -589,6 +589,7 @@ const fr: PartialLocaleType = { Edit: "Modifier", Export: "Exporter", Import: "Importer", + Overwrite: "Remplacer", Sync: "Synchroniser", Config: "Configurer", }, diff --git a/app/locales/it.ts b/app/locales/it.ts index 59bc1eb1594..8ea29129e3c 100644 --- a/app/locales/it.ts +++ b/app/locales/it.ts @@ -590,6 +590,7 @@ const it: PartialLocaleType = { Edit: "Modifica", Export: "Esporta", Import: "Importa", + Overwrite: "Sostituisci", Sync: "Sincronizza", Config: "Configura", }, diff --git a/app/locales/pt.ts b/app/locales/pt.ts index 152f502284c..29afa149ae7 100644 --- a/app/locales/pt.ts +++ b/app/locales/pt.ts @@ -505,6 +505,7 @@ const pt: PartialLocaleType = { Edit: "Editar", Export: "Exportar", Import: "Importar", + Overwrite: "Substituir", Sync: "Sincronizar", Config: "Configurar", }, diff --git a/app/store/sync.ts b/app/store/sync.ts index 8477c1e4ba7..105e3e83eba 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -28,7 +28,7 @@ const DEFAULT_SYNC_STATE = { proxyUrl: ApiPath.Cors as string, webdav: { - endpoint: "", + endpoint: "https://dav.jyj.cx", username: "", password: "", }, @@ -88,7 +88,7 @@ export const useSyncStore = createPersistStore( return client; }, - async sync() { + async sync(overwrite = false) { const localState = getLocalAppState(); const provider = get().provider; const config = get()[provider]; @@ -103,11 +103,13 @@ export const useSyncStore = createPersistStore( ); return; } else { - const parsedRemoteState = JSON.parse( - await client.get(config.username), - ) as AppState; - mergeAppState(localState, parsedRemoteState); - setLocalAppState(localState); + if (!overwrite) { + const parsedRemoteState = JSON.parse( + await client.get(config.username), + ) as AppState; + mergeAppState(localState, parsedRemoteState); + setLocalAppState(localState); + } } } catch (e) { console.log("[Sync] failed to get remote state", e); @@ -119,6 +121,10 @@ export const useSyncStore = createPersistStore( this.markSyncTime(); }, + async overwrite() { + await this.sync(true); + }, + async check() { const client = this.getClient(); return await client.check(); diff --git a/app/utils/sync.ts b/app/utils/sync.ts index 1acfc1289de..eb119b3a387 100644 --- a/app/utils/sync.ts +++ b/app/utils/sync.ts @@ -1,11 +1,11 @@ import { ChatSession, - useAccessStore, - useAppConfig, + // useAccessStore, + // useAppConfig, useChatStore, } from "../store"; -import { useMaskStore } from "../store/mask"; -import { usePromptStore } from "../store/prompt"; +// import { useMaskStore } from "../store/mask"; +// import { usePromptStore } from "../store/prompt"; import { StoreKey } from "../constant"; import { merge } from "./merge"; @@ -32,18 +32,18 @@ export type GetStoreState = T extends { getState: () => infer U } const LocalStateSetters = { [StoreKey.Chat]: useChatStore.setState, - [StoreKey.Access]: useAccessStore.setState, - [StoreKey.Config]: useAppConfig.setState, - [StoreKey.Mask]: useMaskStore.setState, - [StoreKey.Prompt]: usePromptStore.setState, + // [StoreKey.Access]: useAccessStore.setState, + // [StoreKey.Config]: useAppConfig.setState, + // [StoreKey.Mask]: useMaskStore.setState, + // [StoreKey.Prompt]: usePromptStore.setState, } as const; const LocalStateGetters = { [StoreKey.Chat]: () => getNonFunctionFileds(useChatStore.getState()), - [StoreKey.Access]: () => getNonFunctionFileds(useAccessStore.getState()), - [StoreKey.Config]: () => getNonFunctionFileds(useAppConfig.getState()), - [StoreKey.Mask]: () => getNonFunctionFileds(useMaskStore.getState()), - [StoreKey.Prompt]: () => getNonFunctionFileds(usePromptStore.getState()), + // [StoreKey.Access]: () => getNonFunctionFileds(useAccessStore.getState()), + // [StoreKey.Config]: () => getNonFunctionFileds(useAppConfig.getState()), + // [StoreKey.Mask]: () => getNonFunctionFileds(useMaskStore.getState()), + // [StoreKey.Prompt]: () => getNonFunctionFileds(usePromptStore.getState()), } as const; export type AppState = { @@ -100,22 +100,22 @@ const MergeStates: StateMerger = { return localState; }, - [StoreKey.Prompt]: (localState, remoteState) => { - localState.prompts = { - ...remoteState.prompts, - ...localState.prompts, - }; - return localState; - }, - [StoreKey.Mask]: (localState, remoteState) => { - localState.masks = { - ...remoteState.masks, - ...localState.masks, - }; - return localState; - }, - [StoreKey.Config]: mergeWithUpdate, - [StoreKey.Access]: mergeWithUpdate, + // [StoreKey.Prompt]: (localState, remoteState) => { + // localState.prompts = { + // ...remoteState.prompts, + // ...localState.prompts, + // }; + // return localState; + // }, + // [StoreKey.Mask]: (localState, remoteState) => { + // localState.masks = { + // ...remoteState.masks, + // ...localState.masks, + // }; + // return localState; + // }, + // [StoreKey.Config]: mergeWithUpdate, + // [StoreKey.Access]: mergeWithUpdate, }; export function getLocalAppState() { From 88f8ca822f4cd38b1cb6536ce82c8abf5ba05ef8 Mon Sep 17 00:00:00 2001 From: JiangYingjin Date: Sun, 2 Mar 2025 14:04:57 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20webdav=20=E4=B8=80?= =?UTF-8?q?=E9=94=AE=E5=A1=AB=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/chat.tsx | 59 +++++++++++++++++++++-------------------- nextchat.json | 16 +++++++++++ 2 files changed, 46 insertions(+), 29 deletions(-) create mode 100644 nextchat.json diff --git a/app/components/chat.tsx b/app/components/chat.tsx index ff38b262951..d0dec7be512 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -107,6 +107,7 @@ import { } from "../constant"; import { Avatar } from "./emoji"; import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask"; +import { useSyncStore } from "../store/sync"; import { useMaskStore } from "../store/mask"; import { ChatCommandPrefix, useChatCommand, useCommand } from "../command"; import { prettyObject } from "../utils/format"; @@ -948,6 +949,8 @@ function _Chat() { const fontSize = config.fontSize; const fontFamily = config.fontFamily; + const syncStore = useSyncStore(); + const [showExport, setShowExport] = useState(false); const inputRef = useRef(null); @@ -1395,45 +1398,27 @@ function _Chat() { submit: (text) => { doSubmit(text); }, - code: (text) => { - if (accessStore.disableFastLink) return; - console.log("[Command] got code from url: ", text); - showConfirm(Locale.URLCommand.Code + `code = ${text}`).then((res) => { - if (res) { - accessStore.update((access) => (access.accessCode = text)); - } - }); - }, + // code: (text) => { + // if (accessStore.disableFastLink) return; + // console.log("[Command] got code from url: ", text); + // showConfirm(Locale.URLCommand.Code + `code = ${text}`).then((res) => { + // if (res) { + // accessStore.update((access) => (access.accessCode = text)); + // } + // }); + // }, settings: (text) => { if (accessStore.disableFastLink) return; try { const payload = JSON.parse(text) as { - key?: string; - url?: string; code?: string; + username?: string; + password?: string; }; console.log("[Command] got settings from url: ", payload); - if (payload.key || payload.url) { - showConfirm( - Locale.URLCommand.Settings + - `\n${JSON.stringify(payload, null, 4)}`, - ).then((res) => { - if (!res) return; - if (payload.key) { - accessStore.update( - (access) => (access.openaiApiKey = payload.key!), - ); - } - if (payload.url) { - accessStore.update((access) => (access.openaiUrl = payload.url!)); - } - accessStore.update((access) => (access.useCustomConfig = true)); - }); - } - if (payload.code) { accessStore.update((access) => (access.accessCode = payload.code!)); if (accessStore.isAuthorized()) { @@ -1443,6 +1428,22 @@ function _Chat() { setUserInput(" "); } } + + if (payload.username) { + syncStore.update( + (config) => (config.webdav.username = payload.username!), + ); + } + + if (payload.password) { + syncStore.update( + (config) => (config.webdav.password = payload.password!), + ); + } + + if (payload.username && payload.password) { + syncStore.sync(); + } } catch { console.error("[Command] failed to get settings from url: ", text); } diff --git a/nextchat.json b/nextchat.json new file mode 100644 index 00000000000..75775548002 --- /dev/null +++ b/nextchat.json @@ -0,0 +1,16 @@ +{ + "name": "nextchat", + "cwd": "/www/nextchat", + "script": "server.js", + "env": { + "PORT": 8032, + "CODE": "scut", + "BASE_URL": "https://oneapi.jyj.cx", + "OPENAI_API_KEY": "sk-jiangyj", + "HIDE_USER_API_KEY": true, + "CUSTOM_MODELS": "-all,gemini-2.0-pro-exp-02-05@openai,gemini-2.0-flash-thinking-exp-01-21@openai,gemini-2.0-flash-exp@openai,gemini-2.0-flash@openai,gemini-2.0-flash-lite@openai,gpt-4o-2024-11-20@openai,o3-mini@openai,deepseek-ai/deepseek-v3@openai,deepseek-ai/deepseek-r1@openai,deepseek-chat@openai,deepseek-reasoner@openai,ep-20250124104315-zsg4p@openai", + "DEFAULT_MODEL": "gemini-2.0-pro-exp-02-05@openai", + "WHITE_WEBDAV_ENDPOINTS": "https://dav.jyj.cx", + "VISION_MODELS": "gemini-2.0-flash-thinking-exp-01-21@openai,gemini-2.0-pro-exp-02-05@openai,gemini-2.0-flash-exp@openai,gemini-2.0-flash@openai,gemini-2.0-flash-lite@openai,gpt-4o-2024-11-20@openai,o3-mini@openai,deepseek-ai/DeepSeek-V3@openai,deepseek-ai/DeepSeek-R1@openai,deepseek-chat@openai,deepseek-reasoner@openai,ep-20250124104315-zsg4p@openai" + } +} From 3bc977cebdcef011748108643eac68a57dd6ff55 Mon Sep 17 00:00:00 2001 From: JiangYingjin Date: Mon, 3 Mar 2025 13:06:43 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E5=A4=A7=E5=B9=85=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/client/platforms/google.ts | 7 +++- app/components/chat.tsx | 1 + app/components/home.tsx | 3 ++ app/components/settings.tsx | 22 +++++++++- app/store/chat.ts | 15 ++++++- app/store/sync.ts | 74 ++++++++++++++++++++++++++-------- 6 files changed, 101 insertions(+), 21 deletions(-) diff --git a/app/client/platforms/google.ts b/app/client/platforms/google.ts index c10e4969d44..ee36ca9f229 100644 --- a/app/client/platforms/google.ts +++ b/app/client/platforms/google.ts @@ -83,7 +83,12 @@ export class GeminiProApi implements LLMApi { } const messages = _messages.map((v) => { let parts: any[] = [{ text: getMessageTextContent(v) }]; - if (isVisionModel(options.config.model, accessStore.visionModels)) { + if ( + isVisionModel( + options.config.model, + useAccessStore.getState().visionModels, + ) + ) { const images = getMessageImages(v); if (images.length > 0) { multimodal = true; diff --git a/app/components/chat.tsx b/app/components/chat.tsx index d0dec7be512..73f7c5ce328 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -1167,6 +1167,7 @@ function _Chat() { const onDelete = (msgId: string) => { deleteMessage(msgId); + syncStore.upload(); }; const onResend = (message: ChatMessage) => { diff --git a/app/components/home.tsx b/app/components/home.tsx index 5da49037885..3568cf54d3b 100644 --- a/app/components/home.tsx +++ b/app/components/home.tsx @@ -28,6 +28,7 @@ import { AuthPage } from "./auth"; import { getClientConfig } from "../config/client"; import { type ClientApi, getClientApi } from "../client/api"; import { useAccessStore } from "../store"; +import { useSyncStore } from "../store/sync"; import clsx from "clsx"; export function Loading(props: { noLogo?: boolean }) { @@ -239,6 +240,8 @@ export function Home() { return ; } + useSyncStore.getState().download(); + return ( diff --git a/app/components/settings.tsx b/app/components/settings.tsx index 6c193702c25..8acbce94a30 100644 --- a/app/components/settings.tsx +++ b/app/components/settings.tsx @@ -534,7 +534,22 @@ function SyncItems() { text={Locale.UI.Overwrite} onClick={async () => { try { - await syncStore.overwrite(); + await syncStore.upload(); + showToast(Locale.Settings.Sync.Success); + } catch (e) { + showToast(Locale.Settings.Sync.Fail); + console.error("[Sync]", e); + } + }} + /> + )} + {couldSync && ( + } + text={Locale.UI.Overwrite} + onClick={async () => { + try { + await syncStore.download(); showToast(Locale.Settings.Sync.Success); } catch (e) { showToast(Locale.Settings.Sync.Fail); @@ -1408,7 +1423,10 @@ export function Settings() { } - onClick={() => navigate(Path.Home)} + onClick={() => { + navigate(Path.Home); + useSyncStore.getState().sync(); + }} bordered /> diff --git a/app/store/chat.ts b/app/store/chat.ts index 63d7394ece6..12b038e69b9 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -29,6 +29,7 @@ import { ModelConfig, ModelType, useAppConfig } from "./config"; import { useAccessStore } from "./access"; import { collectModelsWithDefaultModel } from "../utils/model"; import { createEmptyMask, Mask } from "./mask"; +import { useSyncStore } from "./sync"; const localStorage = safeLocalStorage(); @@ -226,6 +227,8 @@ export const useChatStore = createPersistStore( currentSessionIndex: 0, sessions: [newSession, ...state.sessions], })); + + useSyncStore.getState().upload(); }, clearSessions() { @@ -233,6 +236,8 @@ export const useChatStore = createPersistStore( sessions: [createEmptySession()], currentSessionIndex: 0, })); + + useSyncStore.getState().upload(); }, selectSession(index: number) { @@ -264,6 +269,8 @@ export const useChatStore = createPersistStore( sessions: newSessions, }; }); + + useSyncStore.getState().upload(); }, newSession(mask?: Mask) { @@ -327,6 +334,8 @@ export const useChatStore = createPersistStore( sessions, })); + useSyncStore.getState().upload(); + showToast( Locale.Home.DeleteToast, { @@ -433,6 +442,8 @@ export const useChatStore = createPersistStore( get().onNewMessage(botMessage, session); } ChatControllerPool.remove(session.id, botMessage.id); + + useSyncStore.getState().upload(); }, onBeforeTool(tool: ChatMessageTool) { (botMessage.tools = botMessage?.tools || []).push(tool); @@ -727,8 +738,10 @@ export const useChatStore = createPersistStore( console.log("[Memory] ", message); get().updateTargetSession(session, (session) => { session.lastSummarizeIndex = lastSummarizeIndex; - session.memoryPrompt = message; // Update the memory prompt for stored it in local storage + session.memoryPrompt = message; }); + + useSyncStore.getState().upload(); } }, onError(err) { diff --git a/app/store/sync.ts b/app/store/sync.ts index 105e3e83eba..76699fbae2a 100644 --- a/app/store/sync.ts +++ b/app/store/sync.ts @@ -22,6 +22,12 @@ export interface WebDavConfig { const isApp = !!getClientConfig()?.isApp; export type SyncStore = GetStoreState; +export enum SyncAction { + SYNC = "SYNC", + UPLOAD = "UPLOAD", + DOWNLOAD = "DOWNLOAD", +} + const DEFAULT_SYNC_STATE = { provider: ProviderType.WebDAV, useProxy: true, @@ -88,41 +94,75 @@ export const useSyncStore = createPersistStore( return client; }, - async sync(overwrite = false) { + async sync(action: SyncAction = SyncAction.SYNC) { + if (!(await this.hasAccount())) { + console.log("[Sync] No account found, skipping sync."); + return; + } + const localState = getLocalAppState(); const provider = get().provider; const config = get()[provider]; const client = this.getClient(); - try { + if (action === SyncAction.SYNC) { + console.log("[Sync] Syncing state", config.username); + try { + const remoteState = await client.get(config.username); + if (!remoteState || remoteState === "") { + await client.set(config.username, JSON.stringify(localState)); + console.log( + "[Sync] Remote state is empty, using local state instead.", + ); + return; + } else { + const parsedRemoteState = JSON.parse( + await client.get(config.username), + ) as AppState; + mergeAppState(localState, parsedRemoteState); + setLocalAppState(localState); + } + } catch (e) { + console.log("[Sync] failed to get remote state", e); + throw e; + } + await client.set(config.username, JSON.stringify(localState)); + } else if (action === SyncAction.UPLOAD) { + console.log("[Sync] Uploading state", localState); + await client.set(config.username, JSON.stringify(localState)); + } else if (action === SyncAction.DOWNLOAD) { + console.log("[Sync] Downloading state", config.username); const remoteState = await client.get(config.username); if (!remoteState || remoteState === "") { - await client.set(config.username, JSON.stringify(localState)); console.log( "[Sync] Remote state is empty, using local state instead.", ); return; } else { - if (!overwrite) { - const parsedRemoteState = JSON.parse( - await client.get(config.username), - ) as AppState; - mergeAppState(localState, parsedRemoteState); - setLocalAppState(localState); - } + const parsedRemoteState = JSON.parse(remoteState) as AppState; + setLocalAppState(parsedRemoteState); } - } catch (e) { - console.log("[Sync] failed to get remote state", e); - throw e; } - await client.set(config.username, JSON.stringify(localState)); - this.markSyncTime(); }, - async overwrite() { - await this.sync(true); + async download() { + await this.sync(SyncAction.DOWNLOAD); + }, + + async upload() { + await this.sync(SyncAction.UPLOAD); + }, + + async hasAccount() { + const provider = get().provider; + const config = get()[provider] as any; + console.log("[Sync] hasAccount", provider, config); + // console.log("[Sync] hasAccount", !!(provider === ProviderType.WebDAV ? config.username && config.password : config.username && config.apiKey)); + return provider === ProviderType.WebDAV + ? config.username && config.password + : config.username && config.apiKey; }, async check() { From 630b3af441d4cd22bce8690f04889b781a5f3b56 Mon Sep 17 00:00:00 2001 From: JiangYingjin Date: Thu, 22 May 2025 02:49:21 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E5=B0=86=20o1=20=E6=94=B9=E6=88=90=20o=20?= =?UTF-8?q?=E7=B3=BB=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/client/platforms/openai.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 6d154251eb8..db40244f4de 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -197,7 +197,7 @@ export class ChatGPTApi implements LLMApi { const accessStore = useAccessStore.getState(); const isDalle3 = _isDalle3(options.config.model); - const isO1 = options.config.model.startsWith("o1"); + const isOseries = options.config.model.match(/^o\d/) !== null; if (isDalle3) { const prompt = getMessageTextContent( options.messages.slice(-1)?.pop() as any, @@ -222,7 +222,7 @@ export class ChatGPTApi implements LLMApi { const content = visionModel ? await preProcessImageContent(v.content) : getMessageTextContent(v); - if (!(isO1 && v.role === "system")) + if (!(isOseries && v.role === "system")) messages.push({ role: v.role, content }); } @@ -231,16 +231,16 @@ export class ChatGPTApi implements LLMApi { messages, stream: options.config.stream, model: modelConfig.model, - temperature: !isO1 ? modelConfig.temperature : 1, - presence_penalty: !isO1 ? modelConfig.presence_penalty : 0, - frequency_penalty: !isO1 ? modelConfig.frequency_penalty : 0, - top_p: !isO1 ? modelConfig.top_p : 1, + temperature: !isOseries ? modelConfig.temperature : 1, + presence_penalty: !isOseries ? modelConfig.presence_penalty : 0, + frequency_penalty: !isOseries ? modelConfig.frequency_penalty : 0, + top_p: !isOseries ? modelConfig.top_p : 1, // max_tokens: Math.max(modelConfig.max_tokens, 1024), // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore. }; // O1 使用 max_completion_tokens 控制token数 (https://platform.openai.com/docs/guides/reasoning#controlling-costs) - if (isO1) { + if (isOseries) { requestPayload["max_completion_tokens"] = modelConfig.max_tokens; } @@ -364,7 +364,7 @@ export class ChatGPTApi implements LLMApi { // make a fetch request const requestTimeoutId = setTimeout( () => controller.abort(), - isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 4 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow. + isDalle3 || isOseries ? REQUEST_TIMEOUT_MS * 4 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow. ); const res = await fetch(chatPath, chatPayload);