diff --git a/.env.template b/.env.template
index d73f0aa2a..a9eda4d61 100644
--- a/.env.template
+++ b/.env.template
@@ -1,6 +1,9 @@
# Your openai api key. (required)
OPENAI_API_KEY=sk-xxxx
+# DeepSeek Api Key. (Optional)
+DEEPSEEK_API_KEY=
+
# Access password, separated by comma. (optional)
CODE=your-password
@@ -70,10 +73,13 @@ ANTHROPIC_API_VERSION=
### anthropic claude Api url (optional)
ANTHROPIC_URL=
+
### (optional)
WHITE_WEBDAV_ENDPOINTS=
+
### bedrock (optional)
AWS_REGION=
AWS_ACCESS_KEY=AKIA
-AWS_SECRET_KEY=
\ No newline at end of file
+AWS_SECRET_KEY=
+
diff --git a/.eslintignore b/.eslintignore
index 8109e6bec..61e76e59a 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,2 +1,3 @@
public/serviceWorker.js
-app/mcp/mcp_config.json
\ No newline at end of file
+app/mcp/mcp_config.json
+app/mcp/mcp_config.default.json
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index ff009b178..d3e4193ee 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -42,7 +42,7 @@ COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/.next/server ./.next/server
RUN mkdir -p /app/app/mcp && chmod 777 /app/app/mcp
-COPY --from=builder /app/app/mcp/mcp_config.json /app/app/mcp/
+COPY --from=builder /app/app/mcp/mcp_config.default.json /app/app/mcp/mcp_config.json
EXPOSE 3000
diff --git a/README.md b/README.md
index 33a847397..3c23f4993 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,13 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with Claude, GPT
+## 🥳 Cheer for DeepSeek, China's AI star!
+ > Purpose-Built UI for DeepSeek Reasoner Model
+
+
+
+
+
## 🫣 NextChat Support MCP !
> Before build, please set env ENABLE_MCP=true
diff --git a/app/client/platforms/deepseek.ts b/app/client/platforms/deepseek.ts
index e2ae645c6..2bf3b2338 100644
--- a/app/client/platforms/deepseek.ts
+++ b/app/client/platforms/deepseek.ts
@@ -13,7 +13,7 @@ import {
ChatMessageTool,
usePluginStore,
} from "@/app/store";
-import { stream } from "@/app/utils/chat";
+import { streamWithThink } from "@/app/utils/chat";
import {
ChatOptions,
getHeaders,
@@ -22,7 +22,10 @@ import {
SpeechOptions,
} from "../api";
import { getClientConfig } from "@/app/config/client";
-import { getMessageTextContent } from "@/app/utils";
+import {
+ getMessageTextContent,
+ getMessageTextContentWithoutThinking,
+} from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
@@ -67,8 +70,13 @@ export class DeepSeekApi implements LLMApi {
async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
- const content = getMessageTextContent(v);
- messages.push({ role: v.role, content });
+ if (v.role === "assistant") {
+ const content = getMessageTextContentWithoutThinking(v);
+ messages.push({ role: v.role, content });
+ } else {
+ const content = getMessageTextContent(v);
+ messages.push({ role: v.role, content });
+ }
}
const modelConfig = {
@@ -107,6 +115,8 @@ export class DeepSeekApi implements LLMApi {
headers: getHeaders(),
};
+ // console.log(chatPayload);
+
// make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
@@ -119,7 +129,7 @@ export class DeepSeekApi implements LLMApi {
.getAsTools(
useChatStore.getState().currentSession().mask?.plugin || [],
);
- return stream(
+ return streamWithThink(
chatPath,
requestPayload,
getHeaders(),
@@ -132,8 +142,9 @@ export class DeepSeekApi implements LLMApi {
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: {
- content: string;
+ content: string | null;
tool_calls: ChatMessageTool[];
+ reasoning_content: string | null;
};
}>;
const tool_calls = choices[0]?.delta?.tool_calls;
@@ -155,7 +166,36 @@ export class DeepSeekApi implements LLMApi {
runTools[index]["function"]["arguments"] += args;
}
}
- return choices[0]?.delta?.content;
+ const reasoning = choices[0]?.delta?.reasoning_content;
+ const content = choices[0]?.delta?.content;
+
+ // Skip if both content and reasoning_content are empty or null
+ if (
+ (!reasoning || reasoning.trim().length === 0) &&
+ (!content || content.trim().length === 0)
+ ) {
+ return {
+ isThinking: false,
+ content: "",
+ };
+ }
+
+ if (reasoning && reasoning.trim().length > 0) {
+ return {
+ isThinking: true,
+ content: reasoning,
+ };
+ } else if (content && content.trim().length > 0) {
+ return {
+ isThinking: false,
+ content: content,
+ };
+ }
+
+ return {
+ isThinking: false,
+ content: "",
+ };
},
// processToolMessage, include tool_calls message and tool call results
(
diff --git a/app/config/server.ts b/app/config/server.ts
index 3d12f6697..7eb8ff3ef 100644
--- a/app/config/server.ts
+++ b/app/config/server.ts
@@ -270,6 +270,6 @@ export const getServerSideConfig = () => {
defaultModel,
visionModels,
allowedWebDavEndpoints,
- enableMcp: !!process.env.ENABLE_MCP,
+ enableMcp: process.env.ENABLE_MCP === "true",
};
};
diff --git a/app/constant.ts b/app/constant.ts
index acbe61e67..f95bdbab3 100644
--- a/app/constant.ts
+++ b/app/constant.ts
@@ -405,6 +405,7 @@ You are an AI assistant with access to system tools. Your role is to help users
export const SUMMARIZE_MODEL = "gpt-4o-mini";
export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";
+export const DEEPSEEK_SUMMARIZE_MODEL = "deepseek-chat";
export const KnowledgeCutOffDate: Record = {
default: "2021-09",
@@ -597,7 +598,7 @@ const iflytekModels = [
"4.0Ultra",
];
-const deepseekModels = ["deepseek-chat", "deepseek-coder"];
+const deepseekModels = ["deepseek-chat", "deepseek-coder", "deepseek-reasoner"];
const xAIModes = ["grok-beta"];
diff --git a/app/mcp/actions.ts b/app/mcp/actions.ts
index b4611d934..e8b1ad1d0 100644
--- a/app/mcp/actions.ts
+++ b/app/mcp/actions.ts
@@ -365,6 +365,8 @@ export async function getMcpConfigFromFile(): Promise {
// 更新 MCP 配置文件
async function updateMcpConfig(config: McpConfigData): Promise {
try {
+ // 确保目录存在
+ await fs.mkdir(path.dirname(CONFIG_PATH), { recursive: true });
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
} catch (error) {
throw error;
diff --git a/app/mcp/mcp_config.default.json b/app/mcp/mcp_config.default.json
new file mode 100644
index 000000000..da39e4ffa
--- /dev/null
+++ b/app/mcp/mcp_config.default.json
@@ -0,0 +1,3 @@
+{
+ "mcpServers": {}
+}
diff --git a/app/store/chat.ts b/app/store/chat.ts
index 5c95ac02c..87c1a8beb 100644
--- a/app/store/chat.ts
+++ b/app/store/chat.ts
@@ -20,6 +20,7 @@ import {
DEFAULT_MODELS,
DEFAULT_SYSTEM_TEMPLATE,
GEMINI_SUMMARIZE_MODEL,
+ DEEPSEEK_SUMMARIZE_MODEL,
KnowledgeCutOffDate,
MCP_SYSTEM_TEMPLATE,
MCP_TOOLS_TEMPLATE,
@@ -35,7 +36,7 @@ import { ModelConfig, ModelType, useAppConfig } from "./config";
import { useAccessStore } from "./access";
import { collectModelsWithDefaultModel } from "../utils/model";
import { createEmptyMask, Mask } from "./mask";
-import { executeMcpAction, getAllTools } from "../mcp/actions";
+import { executeMcpAction, getAllTools, isMcpEnabled } from "../mcp/actions";
import { extractMcpJson, isMcpJson } from "../mcp/utils";
const localStorage = safeLocalStorage();
@@ -143,7 +144,10 @@ function getSummarizeModel(
}
if (currentModel.startsWith("gemini")) {
return [GEMINI_SUMMARIZE_MODEL, ServiceProvider.Google];
+ } else if (currentModel.startsWith("deepseek-")) {
+ return [DEEPSEEK_SUMMARIZE_MODEL, ServiceProvider.DeepSeek];
}
+
return [currentModel, providerName];
}
@@ -245,7 +249,7 @@ export const useChatStore = createPersistStore(
newSession.topic = currentSession.topic;
// 深拷贝消息
- newSession.messages = currentSession.messages.map(msg => ({
+ newSession.messages = currentSession.messages.map((msg) => ({
...msg,
id: nanoid(), // 生成新的消息 ID
}));
@@ -551,27 +555,32 @@ export const useChatStore = createPersistStore(
(session.mask.modelConfig.model.startsWith("gpt-") ||
session.mask.modelConfig.model.startsWith("chatgpt-"));
- const mcpSystemPrompt = await getMcpSystemPrompt();
+ const mcpEnabled = await isMcpEnabled();
+ const mcpSystemPrompt = mcpEnabled ? await getMcpSystemPrompt() : "";
var systemPrompts: ChatMessage[] = [];
- systemPrompts = shouldInjectSystemPrompts
- ? [
- createMessage({
- role: "system",
- content:
- fillTemplateWith("", {
- ...modelConfig,
- template: DEFAULT_SYSTEM_TEMPLATE,
- }) + mcpSystemPrompt,
- }),
- ]
- : [
- createMessage({
- role: "system",
- content: mcpSystemPrompt,
- }),
- ];
+
if (shouldInjectSystemPrompts) {
+ systemPrompts = [
+ createMessage({
+ role: "system",
+ content:
+ fillTemplateWith("", {
+ ...modelConfig,
+ template: DEFAULT_SYSTEM_TEMPLATE,
+ }) + mcpSystemPrompt,
+ }),
+ ];
+ } else if (mcpEnabled) {
+ systemPrompts = [
+ createMessage({
+ role: "system",
+ content: mcpSystemPrompt,
+ }),
+ ];
+ }
+
+ if (shouldInjectSystemPrompts || mcpEnabled) {
console.log(
"[Global System Prompt] ",
systemPrompts.at(0)?.content ?? "empty",
@@ -816,6 +825,8 @@ export const useChatStore = createPersistStore(
/** check if the message contains MCP JSON and execute the MCP action */
checkMcpJson(message: ChatMessage) {
+ const mcpEnabled = isMcpEnabled();
+ if (!mcpEnabled) return;
const content = getMessageTextContent(message);
if (isMcpJson(content)) {
try {
diff --git a/app/utils.ts b/app/utils.ts
index 684af7a08..0b5fd8347 100644
--- a/app/utils.ts
+++ b/app/utils.ts
@@ -241,6 +241,28 @@ export function getMessageTextContent(message: RequestMessage) {
return "";
}
+export function getMessageTextContentWithoutThinking(message: RequestMessage) {
+ let content = "";
+
+ if (typeof message.content === "string") {
+ content = message.content;
+ } else {
+ for (const c of message.content) {
+ if (c.type === "text") {
+ content = c.text ?? "";
+ break;
+ }
+ }
+ }
+
+ // Filter out thinking lines (starting with "> ")
+ return content
+ .split("\n")
+ .filter((line) => !line.startsWith("> ") && line.trim() !== "")
+ .join("\n")
+ .trim();
+}
+
export function getMessageImages(message: RequestMessage): string[] {
if (typeof message.content === "string") {
return [];
@@ -256,9 +278,7 @@ export function getMessageImages(message: RequestMessage): string[] {
export function isVisionModel(model: string) {
const visionModels = useAccessStore.getState().visionModels;
- const envVisionModels = visionModels
- ?.split(",")
- .map((m) => m.trim());
+ const envVisionModels = visionModels?.split(",").map((m) => m.trim());
if (envVisionModels?.includes(model)) {
return true;
}
diff --git a/app/utils/chat.ts b/app/utils/chat.ts
index abace88e8..c04d33cbf 100644
--- a/app/utils/chat.ts
+++ b/app/utils/chat.ts
@@ -344,8 +344,12 @@ export function stream(
return finish();
}
const text = msg.data;
+ // Skip empty messages
+ if (!text || text.trim().length === 0) {
+ return;
+ }
try {
- const chunk = parseSSE(msg.data, runTools);
+ const chunk = parseSSE(text, runTools);
if (chunk) {
remainText += chunk;
}
@@ -366,3 +370,262 @@ export function stream(
console.debug("[ChatAPI] start");
chatApi(chatPath, headers, requestPayload, tools); // call fetchEventSource
}
+
+export function streamWithThink(
+ chatPath: string,
+ requestPayload: any,
+ headers: any,
+ tools: any[],
+ funcs: Record,
+ controller: AbortController,
+ parseSSE: (
+ text: string,
+ runTools: any[],
+ ) => {
+ isThinking: boolean;
+ content: string | undefined;
+ },
+ processToolMessage: (
+ requestPayload: any,
+ toolCallMessage: any,
+ toolCallResult: any[],
+ ) => void,
+ options: any,
+) {
+ let responseText = "";
+ let remainText = "";
+ let finished = false;
+ let running = false;
+ let runTools: any[] = [];
+ let responseRes: Response;
+ let isInThinkingMode = false;
+ let lastIsThinking = false;
+
+ // animate response to make it looks smooth
+ function animateResponseText() {
+ if (finished || controller.signal.aborted) {
+ responseText += remainText;
+ console.log("[Response Animation] finished");
+ if (responseText?.length === 0) {
+ options.onError?.(new Error("empty response from server"));
+ }
+ return;
+ }
+
+ if (remainText.length > 0) {
+ const fetchCount = Math.max(1, Math.round(remainText.length / 60));
+ const fetchText = remainText.slice(0, fetchCount);
+ responseText += fetchText;
+ remainText = remainText.slice(fetchCount);
+ options.onUpdate?.(responseText, fetchText);
+ }
+
+ requestAnimationFrame(animateResponseText);
+ }
+
+ // start animaion
+ animateResponseText();
+
+ const finish = () => {
+ if (!finished) {
+ if (!running && runTools.length > 0) {
+ const toolCallMessage = {
+ role: "assistant",
+ tool_calls: [...runTools],
+ };
+ running = true;
+ runTools.splice(0, runTools.length); // empty runTools
+ return Promise.all(
+ toolCallMessage.tool_calls.map((tool) => {
+ options?.onBeforeTool?.(tool);
+ return Promise.resolve(
+ // @ts-ignore
+ funcs[tool.function.name](
+ // @ts-ignore
+ tool?.function?.arguments
+ ? JSON.parse(tool?.function?.arguments)
+ : {},
+ ),
+ )
+ .then((res) => {
+ let content = res.data || res?.statusText;
+ // hotfix #5614
+ content =
+ typeof content === "string"
+ ? content
+ : JSON.stringify(content);
+ if (res.status >= 300) {
+ return Promise.reject(content);
+ }
+ return content;
+ })
+ .then((content) => {
+ options?.onAfterTool?.({
+ ...tool,
+ content,
+ isError: false,
+ });
+ return content;
+ })
+ .catch((e) => {
+ options?.onAfterTool?.({
+ ...tool,
+ isError: true,
+ errorMsg: e.toString(),
+ });
+ return e.toString();
+ })
+ .then((content) => ({
+ name: tool.function.name,
+ role: "tool",
+ content,
+ tool_call_id: tool.id,
+ }));
+ }),
+ ).then((toolCallResult) => {
+ processToolMessage(requestPayload, toolCallMessage, toolCallResult);
+ setTimeout(() => {
+ // call again
+ console.debug("[ChatAPI] restart");
+ running = false;
+ chatApi(chatPath, headers, requestPayload, tools); // call fetchEventSource
+ }, 60);
+ });
+ return;
+ }
+ if (running) {
+ return;
+ }
+ console.debug("[ChatAPI] end");
+ finished = true;
+ options.onFinish(responseText + remainText, responseRes);
+ }
+ };
+
+ controller.signal.onabort = finish;
+
+ function chatApi(
+ chatPath: string,
+ headers: any,
+ requestPayload: any,
+ tools: any,
+ ) {
+ const chatPayload = {
+ method: "POST",
+ body: JSON.stringify({
+ ...requestPayload,
+ tools: tools && tools.length ? tools : undefined,
+ }),
+ signal: controller.signal,
+ headers,
+ };
+ const requestTimeoutId = setTimeout(
+ () => controller.abort(),
+ REQUEST_TIMEOUT_MS,
+ );
+ fetchEventSource(chatPath, {
+ fetch: tauriFetch as any,
+ ...chatPayload,
+ async onopen(res) {
+ clearTimeout(requestTimeoutId);
+ const contentType = res.headers.get("content-type");
+ console.log("[Request] response content type: ", contentType);
+ responseRes = res;
+
+ if (contentType?.startsWith("text/plain")) {
+ responseText = await res.clone().text();
+ return finish();
+ }
+
+ if (
+ !res.ok ||
+ !res.headers
+ .get("content-type")
+ ?.startsWith(EventStreamContentType) ||
+ res.status !== 200
+ ) {
+ const responseTexts = [responseText];
+ let extraInfo = await res.clone().text();
+ try {
+ const resJson = await res.clone().json();
+ extraInfo = prettyObject(resJson);
+ } catch {}
+
+ if (res.status === 401) {
+ responseTexts.push(Locale.Error.Unauthorized);
+ }
+
+ if (extraInfo) {
+ responseTexts.push(extraInfo);
+ }
+
+ responseText = responseTexts.join("\n\n");
+
+ return finish();
+ }
+ },
+ onmessage(msg) {
+ if (msg.data === "[DONE]" || finished) {
+ return finish();
+ }
+ const text = msg.data;
+ // Skip empty messages
+ if (!text || text.trim().length === 0) {
+ return;
+ }
+ try {
+ const chunk = parseSSE(text, runTools);
+ // Skip if content is empty
+ if (!chunk?.content || chunk.content.trim().length === 0) {
+ return;
+ }
+ // Check if thinking mode changed
+ const isThinkingChanged = lastIsThinking !== chunk.isThinking;
+ lastIsThinking = chunk.isThinking;
+
+ if (chunk.isThinking) {
+ // If in thinking mode
+ if (!isInThinkingMode || isThinkingChanged) {
+ // If this is a new thinking block or mode changed, add prefix
+ isInThinkingMode = true;
+ if (remainText.length > 0) {
+ remainText += "\n";
+ }
+ remainText += "> " + chunk.content;
+ } else {
+ // Handle newlines in thinking content
+ if (chunk.content.includes("\n\n")) {
+ const lines = chunk.content.split("\n\n");
+ remainText += lines.join("\n\n> ");
+ } else {
+ remainText += chunk.content;
+ }
+ }
+ } else {
+ // If in normal mode
+ if (isInThinkingMode || isThinkingChanged) {
+ // If switching from thinking mode to normal mode
+ isInThinkingMode = false;
+ remainText += "\n\n" + chunk.content;
+ } else {
+ remainText += chunk.content;
+ }
+ }
+ } catch (e) {
+ console.error("[Request] parse error", text, msg, e);
+ // Don't throw error for parse failures, just log them
+ }
+ },
+ onclose() {
+ finish();
+ },
+ onerror(e) {
+ options?.onError?.(e);
+ throw e;
+ },
+ openWhenHidden: true,
+ });
+ }
+ console.debug("[ChatAPI] start");
+ chatApi(chatPath, headers, requestPayload, tools); // call fetchEventSource
+}