mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-05-22 05:30:19 +09:00
Merge branch 'main' into main
This commit is contained in:
commit
40c00374e7
@ -7,6 +7,11 @@ CODE=your-password
|
|||||||
# You can start service behind a proxy. (optional)
|
# You can start service behind a proxy. (optional)
|
||||||
PROXY_URL=http://localhost:7890
|
PROXY_URL=http://localhost:7890
|
||||||
|
|
||||||
|
# Enable MCP functionality (optional)
|
||||||
|
# Default: Empty (disabled)
|
||||||
|
# Set to "true" to enable MCP functionality
|
||||||
|
ENABLE_MCP=
|
||||||
|
|
||||||
# (optional)
|
# (optional)
|
||||||
# Default: Empty
|
# Default: Empty
|
||||||
# Google Gemini Pro API key, set if you want to use Google Gemini Pro API.
|
# Google Gemini Pro API key, set if you want to use Google Gemini Pro API.
|
||||||
|
@ -1 +1,2 @@
|
|||||||
public/serviceWorker.js
|
public/serviceWorker.js
|
||||||
|
app/mcp/mcp_config.json
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -46,3 +46,6 @@ dev
|
|||||||
*.key.pub
|
*.key.pub
|
||||||
|
|
||||||
masks.json
|
masks.json
|
||||||
|
|
||||||
|
# mcp config
|
||||||
|
app/mcp/mcp_config.json
|
||||||
|
@ -34,12 +34,16 @@ ENV PROXY_URL=""
|
|||||||
ENV OPENAI_API_KEY=""
|
ENV OPENAI_API_KEY=""
|
||||||
ENV GOOGLE_API_KEY=""
|
ENV GOOGLE_API_KEY=""
|
||||||
ENV CODE=""
|
ENV CODE=""
|
||||||
|
ENV ENABLE_MCP=""
|
||||||
|
|
||||||
COPY --from=builder /app/public ./public
|
COPY --from=builder /app/public ./public
|
||||||
COPY --from=builder /app/.next/standalone ./
|
COPY --from=builder /app/.next/standalone ./
|
||||||
COPY --from=builder /app/.next/static ./.next/static
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
COPY --from=builder /app/.next/server ./.next/server
|
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/
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD if [ -n "$PROXY_URL" ]; then \
|
CMD if [ -n "$PROXY_URL" ]; then \
|
||||||
|
23
README.md
23
README.md
@ -5,6 +5,7 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<h1 align="center">NextChat (ChatGPT Next Web)</h1>
|
<h1 align="center">NextChat (ChatGPT Next Web)</h1>
|
||||||
|
|
||||||
English / [简体中文](./README_CN.md)
|
English / [简体中文](./README_CN.md)
|
||||||
@ -39,6 +40,12 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with Claude, GPT
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## 🫣 NextChat Support MCP !
|
||||||
|
> Before build, please set env ENABLE_MCP=true
|
||||||
|
|
||||||
|
<img src="https://github.com/user-attachments/assets/d8851f40-4e36-4335-b1a4-ec1e11488c7e"/>
|
||||||
|
|
||||||
|
|
||||||
## Enterprise Edition
|
## Enterprise Edition
|
||||||
|
|
||||||
Meeting Your Company's Privatization and Customization Deployment Requirements:
|
Meeting Your Company's Privatization and Customization Deployment Requirements:
|
||||||
@ -333,6 +340,12 @@ Stability API key.
|
|||||||
|
|
||||||
Customize Stability API url.
|
Customize Stability API url.
|
||||||
|
|
||||||
|
|
||||||
|
### `ENABLE_MCP` (optional)
|
||||||
|
|
||||||
|
Enable MCP(Model Context Protocol)Feature
|
||||||
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
NodeJS >= 18, Docker >= 20
|
NodeJS >= 18, Docker >= 20
|
||||||
@ -391,6 +404,16 @@ If your proxy needs password, use:
|
|||||||
-e PROXY_URL="http://127.0.0.1:7890 user pass"
|
-e PROXY_URL="http://127.0.0.1:7890 user pass"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If enable MCP, use:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run -d -p 3000:3000 \
|
||||||
|
-e OPENAI_API_KEY=sk-xxxx \
|
||||||
|
-e CODE=your-password \
|
||||||
|
-e ENABLE_MCP=true \
|
||||||
|
yidadaa/chatgpt-next-web
|
||||||
|
```
|
||||||
|
|
||||||
### Shell
|
### Shell
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
19
README_CN.md
19
README_CN.md
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<h1 align="center">NextChat</h1>
|
<h1 align="center">NextChat</h1>
|
||||||
|
|
||||||
一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。
|
一键免费部署你的私人 ChatGPT 网页应用,支持 Claude, GPT4 & Gemini Pro 模型。
|
||||||
|
|
||||||
[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
|
[NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
|
||||||
|
|
||||||
@ -27,7 +27,8 @@
|
|||||||
|
|
||||||
企业版咨询: **business@nextchat.dev**
|
企业版咨询: **business@nextchat.dev**
|
||||||
|
|
||||||
<img width="300" src="https://github.com/user-attachments/assets/3daeb7b6-ab63-4542-9141-2e4a12c80601">
|
<img width="300" src="https://github.com/user-attachments/assets/bb29a11d-ff75-48a8-b1f8-d2d7238cf987">
|
||||||
|
|
||||||
|
|
||||||
## 开始使用
|
## 开始使用
|
||||||
|
|
||||||
@ -262,6 +263,10 @@ Stability API密钥
|
|||||||
|
|
||||||
自定义的Stability API请求地址
|
自定义的Stability API请求地址
|
||||||
|
|
||||||
|
### `ENABLE_MCP` (optional)
|
||||||
|
|
||||||
|
启用MCP(Model Context Protocol)功能
|
||||||
|
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
@ -315,6 +320,16 @@ docker run -d -p 3000:3000 \
|
|||||||
yidadaa/chatgpt-next-web
|
yidadaa/chatgpt-next-web
|
||||||
```
|
```
|
||||||
|
|
||||||
|
如需启用 MCP 功能,可以使用:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run -d -p 3000:3000 \
|
||||||
|
-e OPENAI_API_KEY=sk-xxxx \
|
||||||
|
-e CODE=页面访问密码 \
|
||||||
|
-e ENABLE_MCP=true \
|
||||||
|
yidadaa/chatgpt-next-web
|
||||||
|
```
|
||||||
|
|
||||||
如果你的本地代理需要账号密码,可以使用:
|
如果你的本地代理需要账号密码,可以使用:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import React, {
|
import React, {
|
||||||
useState,
|
|
||||||
useRef,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useCallback,
|
|
||||||
Fragment,
|
Fragment,
|
||||||
RefObject,
|
RefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import SendWhiteIcon from "../icons/send-white.svg";
|
import SendWhiteIcon from "../icons/send-white.svg";
|
||||||
import BrainIcon from "../icons/brain.svg";
|
import BrainIcon from "../icons/brain.svg";
|
||||||
import RenameIcon from "../icons/rename.svg";
|
import RenameIcon from "../icons/rename.svg";
|
||||||
|
import EditIcon from "../icons/rename.svg";
|
||||||
import ExportIcon from "../icons/share.svg";
|
import ExportIcon from "../icons/share.svg";
|
||||||
import ReturnIcon from "../icons/return.svg";
|
import ReturnIcon from "../icons/return.svg";
|
||||||
import CopyIcon from "../icons/copy.svg";
|
import CopyIcon from "../icons/copy.svg";
|
||||||
@ -24,11 +25,11 @@ import MaskIcon from "../icons/mask.svg";
|
|||||||
import MaxIcon from "../icons/max.svg";
|
import MaxIcon from "../icons/max.svg";
|
||||||
import MinIcon from "../icons/min.svg";
|
import MinIcon from "../icons/min.svg";
|
||||||
import ResetIcon from "../icons/reload.svg";
|
import ResetIcon from "../icons/reload.svg";
|
||||||
|
import ReloadIcon from "../icons/reload.svg";
|
||||||
import BreakIcon from "../icons/break.svg";
|
import BreakIcon from "../icons/break.svg";
|
||||||
import SettingsIcon from "../icons/chat-settings.svg";
|
import SettingsIcon from "../icons/chat-settings.svg";
|
||||||
import DeleteIcon from "../icons/clear.svg";
|
import DeleteIcon from "../icons/clear.svg";
|
||||||
import PinIcon from "../icons/pin.svg";
|
import PinIcon from "../icons/pin.svg";
|
||||||
import EditIcon from "../icons/rename.svg";
|
|
||||||
import ConfirmIcon from "../icons/confirm.svg";
|
import ConfirmIcon from "../icons/confirm.svg";
|
||||||
import CloseIcon from "../icons/close.svg";
|
import CloseIcon from "../icons/close.svg";
|
||||||
import CancelIcon from "../icons/cancel.svg";
|
import CancelIcon from "../icons/cancel.svg";
|
||||||
@ -45,35 +46,35 @@ import QualityIcon from "../icons/hd.svg";
|
|||||||
import StyleIcon from "../icons/palette.svg";
|
import StyleIcon from "../icons/palette.svg";
|
||||||
import PluginIcon from "../icons/plugin.svg";
|
import PluginIcon from "../icons/plugin.svg";
|
||||||
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
|
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
|
||||||
import ReloadIcon from "../icons/reload.svg";
|
import McpToolIcon from "../icons/tool.svg";
|
||||||
import HeadphoneIcon from "../icons/headphone.svg";
|
import HeadphoneIcon from "../icons/headphone.svg";
|
||||||
import {
|
import {
|
||||||
ChatMessage,
|
|
||||||
SubmitKey,
|
|
||||||
useChatStore,
|
|
||||||
BOT_HELLO,
|
BOT_HELLO,
|
||||||
|
ChatMessage,
|
||||||
createMessage,
|
createMessage,
|
||||||
useAccessStore,
|
|
||||||
Theme,
|
|
||||||
useAppConfig,
|
|
||||||
DEFAULT_TOPIC,
|
DEFAULT_TOPIC,
|
||||||
ModelType,
|
ModelType,
|
||||||
|
SubmitKey,
|
||||||
|
Theme,
|
||||||
|
useAccessStore,
|
||||||
|
useAppConfig,
|
||||||
|
useChatStore,
|
||||||
usePluginStore,
|
usePluginStore,
|
||||||
} from "../store";
|
} from "../store";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
copyToClipboard,
|
|
||||||
selectOrCopy,
|
|
||||||
autoGrowTextArea,
|
autoGrowTextArea,
|
||||||
useMobileScreen,
|
copyToClipboard,
|
||||||
getMessageTextContent,
|
|
||||||
getMessageImages,
|
getMessageImages,
|
||||||
isVisionModel,
|
getMessageTextContent,
|
||||||
isDalle3,
|
isDalle3,
|
||||||
showPlugins,
|
isVisionModel,
|
||||||
safeLocalStorage,
|
safeLocalStorage,
|
||||||
getModelSizes,
|
getModelSizes,
|
||||||
supportsCustomSize,
|
supportsCustomSize,
|
||||||
|
useMobileScreen,
|
||||||
|
selectOrCopy,
|
||||||
|
showPlugins,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
|
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
|
||||||
@ -104,8 +105,8 @@ import {
|
|||||||
ModelProvider,
|
ModelProvider,
|
||||||
Path,
|
Path,
|
||||||
REQUEST_TIMEOUT_MS,
|
REQUEST_TIMEOUT_MS,
|
||||||
UNFINISHED_INPUT,
|
|
||||||
ServiceProvider,
|
ServiceProvider,
|
||||||
|
UNFINISHED_INPUT,
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
import { Avatar } from "./emoji";
|
import { Avatar } from "./emoji";
|
||||||
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
|
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
|
||||||
@ -115,9 +116,7 @@ import { prettyObject } from "../utils/format";
|
|||||||
import { ExportMessageModal } from "./exporter";
|
import { ExportMessageModal } from "./exporter";
|
||||||
import { getClientConfig } from "../config/client";
|
import { getClientConfig } from "../config/client";
|
||||||
import { useAllModels } from "../utils/hooks";
|
import { useAllModels } from "../utils/hooks";
|
||||||
import { MultimodalContent } from "../client/api";
|
import { ClientApi, MultimodalContent } from "../client/api";
|
||||||
|
|
||||||
import { ClientApi } from "../client/api";
|
|
||||||
import { createTTSPlayer } from "../utils/audio";
|
import { createTTSPlayer } from "../utils/audio";
|
||||||
import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
|
import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
|
||||||
|
|
||||||
@ -125,6 +124,7 @@ import { isEmpty } from "lodash-es";
|
|||||||
import { getModelProvider } from "../utils/model";
|
import { getModelProvider } from "../utils/model";
|
||||||
import { RealtimeChat } from "@/app/components/realtime-chat";
|
import { RealtimeChat } from "@/app/components/realtime-chat";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { getAvailableClientsCount, isMcpEnabled } from "../mcp/actions";
|
||||||
|
|
||||||
const localStorage = safeLocalStorage();
|
const localStorage = safeLocalStorage();
|
||||||
|
|
||||||
@ -134,6 +134,34 @@ const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
|||||||
loading: () => <LoadingIcon />,
|
loading: () => <LoadingIcon />,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const MCPAction = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [count, setCount] = useState<number>(0);
|
||||||
|
const [mcpEnabled, setMcpEnabled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkMcpStatus = async () => {
|
||||||
|
const enabled = await isMcpEnabled();
|
||||||
|
setMcpEnabled(enabled);
|
||||||
|
if (enabled) {
|
||||||
|
const count = await getAvailableClientsCount();
|
||||||
|
setCount(count);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkMcpStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mcpEnabled) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChatAction
|
||||||
|
onClick={() => navigate(Path.McpMarket)}
|
||||||
|
text={`MCP${count ? ` (${count})` : ""}`}
|
||||||
|
icon={<McpToolIcon />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export function SessionConfigModel(props: { onClose: () => void }) {
|
export function SessionConfigModel(props: { onClose: () => void }) {
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const session = chatStore.currentSession();
|
const session = chatStore.currentSession();
|
||||||
@ -425,11 +453,11 @@ export function ChatAction(props: {
|
|||||||
function useScrollToBottom(
|
function useScrollToBottom(
|
||||||
scrollRef: RefObject<HTMLDivElement>,
|
scrollRef: RefObject<HTMLDivElement>,
|
||||||
detach: boolean = false,
|
detach: boolean = false,
|
||||||
|
messages: ChatMessage[],
|
||||||
) {
|
) {
|
||||||
// for auto-scroll
|
// for auto-scroll
|
||||||
|
|
||||||
const [autoScroll, setAutoScroll] = useState(true);
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
function scrollDomToBottom() {
|
const scrollDomToBottom = useCallback(() => {
|
||||||
const dom = scrollRef.current;
|
const dom = scrollRef.current;
|
||||||
if (dom) {
|
if (dom) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@ -437,7 +465,7 @@ function useScrollToBottom(
|
|||||||
dom.scrollTo(0, dom.scrollHeight);
|
dom.scrollTo(0, dom.scrollHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}, [scrollRef]);
|
||||||
|
|
||||||
// auto scroll
|
// auto scroll
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -446,6 +474,15 @@ function useScrollToBottom(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// auto scroll when messages length changes
|
||||||
|
const lastMessagesLength = useRef(messages.length);
|
||||||
|
useEffect(() => {
|
||||||
|
if (messages.length > lastMessagesLength.current && !detach) {
|
||||||
|
scrollDomToBottom();
|
||||||
|
}
|
||||||
|
lastMessagesLength.current = messages.length;
|
||||||
|
}, [messages.length, detach, scrollDomToBottom]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scrollRef,
|
scrollRef,
|
||||||
autoScroll,
|
autoScroll,
|
||||||
@ -475,6 +512,7 @@ export function ChatActions(props: {
|
|||||||
|
|
||||||
// switch themes
|
// switch themes
|
||||||
const theme = config.theme;
|
const theme = config.theme;
|
||||||
|
|
||||||
function nextTheme() {
|
function nextTheme() {
|
||||||
const themes = [Theme.Auto, Theme.Light, Theme.Dark];
|
const themes = [Theme.Auto, Theme.Light, Theme.Dark];
|
||||||
const themeIndex = themes.indexOf(theme);
|
const themeIndex = themes.indexOf(theme);
|
||||||
@ -794,6 +832,7 @@ export function ChatActions(props: {
|
|||||||
icon={<ShortcutkeyIcon />}
|
icon={<ShortcutkeyIcon />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{!isMobileScreen && <MCPAction />}
|
||||||
</>
|
</>
|
||||||
<div className={styles["chat-input-actions-end"]}>
|
<div className={styles["chat-input-actions-end"]}>
|
||||||
{config.realtimeConfig.enable && (
|
{config.realtimeConfig.enable && (
|
||||||
@ -987,6 +1026,7 @@ function _Chat() {
|
|||||||
const { setAutoScroll, scrollDomToBottom } = useScrollToBottom(
|
const { setAutoScroll, scrollDomToBottom } = useScrollToBottom(
|
||||||
scrollRef,
|
scrollRef,
|
||||||
(isScrolledToBottom || isAttachWithTop) && !isTyping,
|
(isScrolledToBottom || isAttachWithTop) && !isTyping,
|
||||||
|
session.messages,
|
||||||
);
|
);
|
||||||
const [hitBottom, setHitBottom] = useState(true);
|
const [hitBottom, setHitBottom] = useState(true);
|
||||||
const isMobileScreen = useMobileScreen();
|
const isMobileScreen = useMobileScreen();
|
||||||
@ -1246,6 +1286,7 @@ function _Chat() {
|
|||||||
const accessStore = useAccessStore();
|
const accessStore = useAccessStore();
|
||||||
const [speechStatus, setSpeechStatus] = useState(false);
|
const [speechStatus, setSpeechStatus] = useState(false);
|
||||||
const [speechLoading, setSpeechLoading] = useState(false);
|
const [speechLoading, setSpeechLoading] = useState(false);
|
||||||
|
|
||||||
async function openaiSpeech(text: string) {
|
async function openaiSpeech(text: string) {
|
||||||
if (speechStatus) {
|
if (speechStatus) {
|
||||||
ttsPlayer.stop();
|
ttsPlayer.stop();
|
||||||
@ -1345,6 +1386,7 @@ function _Chat() {
|
|||||||
const [msgRenderIndex, _setMsgRenderIndex] = useState(
|
const [msgRenderIndex, _setMsgRenderIndex] = useState(
|
||||||
Math.max(0, renderMessages.length - CHAT_PAGE_SIZE),
|
Math.max(0, renderMessages.length - CHAT_PAGE_SIZE),
|
||||||
);
|
);
|
||||||
|
|
||||||
function setMsgRenderIndex(newIndex: number) {
|
function setMsgRenderIndex(newIndex: number) {
|
||||||
newIndex = Math.min(renderMessages.length - CHAT_PAGE_SIZE, newIndex);
|
newIndex = Math.min(renderMessages.length - CHAT_PAGE_SIZE, newIndex);
|
||||||
newIndex = Math.max(0, newIndex);
|
newIndex = Math.max(0, newIndex);
|
||||||
@ -1380,6 +1422,7 @@ function _Chat() {
|
|||||||
setHitBottom(isHitBottom);
|
setHitBottom(isHitBottom);
|
||||||
setAutoScroll(isHitBottom);
|
setAutoScroll(isHitBottom);
|
||||||
};
|
};
|
||||||
|
|
||||||
function scrollToBottom() {
|
function scrollToBottom() {
|
||||||
setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE);
|
setMsgRenderIndex(renderMessages.length - CHAT_PAGE_SIZE);
|
||||||
scrollDomToBottom();
|
scrollDomToBottom();
|
||||||
@ -1737,7 +1780,10 @@ function _Chat() {
|
|||||||
setAutoScroll(false);
|
setAutoScroll(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{messages.map((message, i) => {
|
{messages
|
||||||
|
// TODO
|
||||||
|
// .filter((m) => !m.isMcpResponse)
|
||||||
|
.map((message, i) => {
|
||||||
const isUser = message.role === "user";
|
const isUser = message.role === "user";
|
||||||
const isContext = i < context.length;
|
const isContext = i < context.length;
|
||||||
const showActions =
|
const showActions =
|
||||||
@ -1771,8 +1817,9 @@ function _Chat() {
|
|||||||
getMessageTextContent(message),
|
getMessageTextContent(message),
|
||||||
10,
|
10,
|
||||||
);
|
);
|
||||||
let newContent: string | MultimodalContent[] =
|
let newContent:
|
||||||
newMessage;
|
| string
|
||||||
|
| MultimodalContent[] = newMessage;
|
||||||
const images = getMessageImages(message);
|
const images = getMessageImages(message);
|
||||||
if (images.length > 0) {
|
if (images.length > 0) {
|
||||||
newContent = [
|
newContent = [
|
||||||
@ -1832,7 +1879,9 @@ function _Chat() {
|
|||||||
<ChatAction
|
<ChatAction
|
||||||
text={Locale.Chat.Actions.Stop}
|
text={Locale.Chat.Actions.Stop}
|
||||||
icon={<StopIcon />}
|
icon={<StopIcon />}
|
||||||
onClick={() => onUserStop(message.id ?? i)}
|
onClick={() =>
|
||||||
|
onUserStop(message.id ?? i)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@ -1845,7 +1894,9 @@ function _Chat() {
|
|||||||
<ChatAction
|
<ChatAction
|
||||||
text={Locale.Chat.Actions.Delete}
|
text={Locale.Chat.Actions.Delete}
|
||||||
icon={<DeleteIcon />}
|
icon={<DeleteIcon />}
|
||||||
onClick={() => onDelete(message.id ?? i)}
|
onClick={() =>
|
||||||
|
onDelete(message.id ?? i)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ChatAction
|
<ChatAction
|
||||||
@ -1951,18 +2002,22 @@ function _Chat() {
|
|||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{getMessageImages(message).map((image, index) => {
|
{getMessageImages(message).map(
|
||||||
|
(image, index) => {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
className={
|
className={
|
||||||
styles["chat-message-item-image-multi"]
|
styles[
|
||||||
|
"chat-message-item-image-multi"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
key={index}
|
key={index}
|
||||||
src={image}
|
src={image}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
},
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
require("../polyfill");
|
require("../polyfill");
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import styles from "./home.module.scss";
|
import styles from "./home.module.scss";
|
||||||
|
|
||||||
import BotIcon from "../icons/bot.svg";
|
import BotIcon from "../icons/bot.svg";
|
||||||
@ -18,8 +18,8 @@ import { getISOLang, getLang } from "../locales";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
HashRouter as Router,
|
HashRouter as Router,
|
||||||
Routes,
|
|
||||||
Route,
|
Route,
|
||||||
|
Routes,
|
||||||
useLocation,
|
useLocation,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import { SideBar } from "./sidebar";
|
import { SideBar } from "./sidebar";
|
||||||
@ -29,6 +29,7 @@ import { getClientConfig } from "../config/client";
|
|||||||
import { type ClientApi, getClientApi } from "../client/api";
|
import { type ClientApi, getClientApi } from "../client/api";
|
||||||
import { useAccessStore } from "../store";
|
import { useAccessStore } from "../store";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { initializeMcpSystem, isMcpEnabled } from "../mcp/actions";
|
||||||
|
|
||||||
export function Loading(props: { noLogo?: boolean }) {
|
export function Loading(props: { noLogo?: boolean }) {
|
||||||
return (
|
return (
|
||||||
@ -74,6 +75,13 @@ const Sd = dynamic(async () => (await import("./sd")).Sd, {
|
|||||||
loading: () => <Loading noLogo />,
|
loading: () => <Loading noLogo />,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const McpMarketPage = dynamic(
|
||||||
|
async () => (await import("./mcp-market")).McpMarketPage,
|
||||||
|
{
|
||||||
|
loading: () => <Loading noLogo />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export function useSwitchTheme() {
|
export function useSwitchTheme() {
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
|
|
||||||
@ -193,6 +201,7 @@ function Screen() {
|
|||||||
<Route path={Path.SearchChat} element={<SearchChat />} />
|
<Route path={Path.SearchChat} element={<SearchChat />} />
|
||||||
<Route path={Path.Chat} element={<Chat />} />
|
<Route path={Path.Chat} element={<Chat />} />
|
||||||
<Route path={Path.Settings} element={<Settings />} />
|
<Route path={Path.Settings} element={<Settings />} />
|
||||||
|
<Route path={Path.McpMarket} element={<McpMarketPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</WindowContent>
|
</WindowContent>
|
||||||
</>
|
</>
|
||||||
@ -233,6 +242,20 @@ export function Home() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("[Config] got config from build time", getClientConfig());
|
console.log("[Config] got config from build time", getClientConfig());
|
||||||
useAccessStore.getState().fetch();
|
useAccessStore.getState().fetch();
|
||||||
|
|
||||||
|
const initMcp = async () => {
|
||||||
|
try {
|
||||||
|
const enabled = await isMcpEnabled();
|
||||||
|
if (enabled) {
|
||||||
|
console.log("[MCP] initializing...");
|
||||||
|
await initializeMcpSystem();
|
||||||
|
console.log("[MCP] initialized");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[MCP] failed to initialize:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
initMcp();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!useHasHydrated()) {
|
if (!useHasHydrated()) {
|
||||||
|
657
app/components/mcp-market.module.scss
Normal file
657
app/components/mcp-market.module.scss
Normal file
@ -0,0 +1,657 @@
|
|||||||
|
@import "../styles/animation.scss";
|
||||||
|
|
||||||
|
.mcp-market-page {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.loading-indicator {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--primary);
|
||||||
|
margin-left: 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-market-page-body {
|
||||||
|
padding: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.loading-container,
|
||||||
|
.empty-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 200px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--white);
|
||||||
|
border: var(--border-in-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
animation: slide-in ease 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text,
|
||||||
|
.empty-text {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--black);
|
||||||
|
opacity: 0.5;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-market-filter {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
animation: slide-in ease 0.3s;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-market-item {
|
||||||
|
padding: 20px;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
animation: slide-in ease 0.3s;
|
||||||
|
background-color: var(--white);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.loading {
|
||||||
|
position: relative;
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.2),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: loading-pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: #16a34a;
|
||||||
|
color: #fff;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
|
||||||
|
&[data-status="stopping"] {
|
||||||
|
background-color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-status="starting"] {
|
||||||
|
background-color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-status="error"] {
|
||||||
|
background-color: #f87171;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-market-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.mcp-market-title {
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-right: 20px;
|
||||||
|
max-width: calc(100% - 300px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-market-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
.server-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: #22c55e;
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.stopped {
|
||||||
|
background-color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.initializing {
|
||||||
|
background-color: #f59e0b;
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-link {
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: 12px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
opacity: 0.8;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
background: var(--gray);
|
||||||
|
color: var(--black);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-market-info {
|
||||||
|
color: var(--black);
|
||||||
|
font-size: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcp-market-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 180px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.array-input {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: var(--white);
|
||||||
|
|
||||||
|
.array-input-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: var(--gray-50);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--gray-100);
|
||||||
|
border-color: var(--gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: var(--white);
|
||||||
|
border-color: var(--primary);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--primary-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--gray-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.icon-button.add-path-button) {
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
height: 36px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 4px;
|
||||||
|
filter: brightness(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.path-list {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.path-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: var(--white);
|
||||||
|
color: var(--black);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--primary-10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.browse-button {
|
||||||
|
padding: 8px;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--black-50);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button {
|
||||||
|
padding: 8px;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--black-50);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--danger);
|
||||||
|
color: var(--danger);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button {
|
||||||
|
align-self: flex-start;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: transparent;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: var(--black);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.config-header {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.config-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--black);
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--gray-500);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.array-input {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: var(--white);
|
||||||
|
|
||||||
|
.array-input-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: var(--gray-50);
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--gray-100);
|
||||||
|
border-color: var(--gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: var(--white);
|
||||||
|
border-color: var(--primary);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--primary-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--gray-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.icon-button) {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--gray-100);
|
||||||
|
border-color: var(--gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.icon-button.add-path-button) {
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none;
|
||||||
|
height: 36px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--primary-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 4px;
|
||||||
|
filter: brightness(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-item {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: var(--border-in-light);
|
||||||
|
border-radius: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 14px;
|
||||||
|
background-color: var(--white);
|
||||||
|
color: var(--black);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--gray-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px var(--primary-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--gray-300) !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.tool-item {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.tool-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--black);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-left: 12px;
|
||||||
|
border-left: 3px solid var(--primary);
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-description {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--gray-500);
|
||||||
|
line-height: 1.6;
|
||||||
|
padding-left: 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.modal-content {
|
||||||
|
margin-top: 20px;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background-color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.list-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: capitalize;
|
||||||
|
color: var(--black);
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-sub-title {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--gray-500);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading-pulse {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
755
app/components/mcp-market.tsx
Normal file
755
app/components/mcp-market.tsx
Normal file
@ -0,0 +1,755 @@
|
|||||||
|
import { IconButton } from "./button";
|
||||||
|
import { ErrorBoundary } from "./error";
|
||||||
|
import styles from "./mcp-market.module.scss";
|
||||||
|
import EditIcon from "../icons/edit.svg";
|
||||||
|
import AddIcon from "../icons/add.svg";
|
||||||
|
import CloseIcon from "../icons/close.svg";
|
||||||
|
import DeleteIcon from "../icons/delete.svg";
|
||||||
|
import RestartIcon from "../icons/reload.svg";
|
||||||
|
import EyeIcon from "../icons/eye.svg";
|
||||||
|
import GithubIcon from "../icons/github.svg";
|
||||||
|
import { List, ListItem, Modal, showToast } from "./ui-lib";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
addMcpServer,
|
||||||
|
getClientsStatus,
|
||||||
|
getClientTools,
|
||||||
|
getMcpConfigFromFile,
|
||||||
|
isMcpEnabled,
|
||||||
|
pauseMcpServer,
|
||||||
|
restartAllClients,
|
||||||
|
resumeMcpServer,
|
||||||
|
} from "../mcp/actions";
|
||||||
|
import {
|
||||||
|
ListToolsResponse,
|
||||||
|
McpConfigData,
|
||||||
|
PresetServer,
|
||||||
|
ServerConfig,
|
||||||
|
ServerStatusResponse,
|
||||||
|
} from "../mcp/types";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import PlayIcon from "../icons/play.svg";
|
||||||
|
import StopIcon from "../icons/pause.svg";
|
||||||
|
import { Path } from "../constant";
|
||||||
|
|
||||||
|
interface ConfigProperty {
|
||||||
|
type: string;
|
||||||
|
description?: string;
|
||||||
|
required?: boolean;
|
||||||
|
minItems?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function McpMarketPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [mcpEnabled, setMcpEnabled] = useState(false);
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
const [userConfig, setUserConfig] = useState<Record<string, any>>({});
|
||||||
|
const [editingServerId, setEditingServerId] = useState<string | undefined>();
|
||||||
|
const [tools, setTools] = useState<ListToolsResponse["tools"] | null>(null);
|
||||||
|
const [viewingServerId, setViewingServerId] = useState<string | undefined>();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [config, setConfig] = useState<McpConfigData>();
|
||||||
|
const [clientStatuses, setClientStatuses] = useState<
|
||||||
|
Record<string, ServerStatusResponse>
|
||||||
|
>({});
|
||||||
|
const [loadingPresets, setLoadingPresets] = useState(true);
|
||||||
|
const [presetServers, setPresetServers] = useState<PresetServer[]>([]);
|
||||||
|
const [loadingStates, setLoadingStates] = useState<Record<string, string>>(
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 检查 MCP 是否启用
|
||||||
|
useEffect(() => {
|
||||||
|
const checkMcpStatus = async () => {
|
||||||
|
const enabled = await isMcpEnabled();
|
||||||
|
setMcpEnabled(enabled);
|
||||||
|
if (!enabled) {
|
||||||
|
navigate(Path.Home);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
checkMcpStatus();
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
// 添加状态轮询
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mcpEnabled || !config) return;
|
||||||
|
|
||||||
|
const updateStatuses = async () => {
|
||||||
|
const statuses = await getClientsStatus();
|
||||||
|
setClientStatuses(statuses);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 立即执行一次
|
||||||
|
updateStatuses();
|
||||||
|
// 每 1000ms 轮询一次
|
||||||
|
const timer = setInterval(updateStatuses, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [mcpEnabled, config]);
|
||||||
|
|
||||||
|
// 加载预设服务器
|
||||||
|
useEffect(() => {
|
||||||
|
const loadPresetServers = async () => {
|
||||||
|
if (!mcpEnabled) return;
|
||||||
|
try {
|
||||||
|
setLoadingPresets(true);
|
||||||
|
const response = await fetch("https://nextchat.club/mcp/list");
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to load preset servers");
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
setPresetServers(data?.data ?? []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load preset servers:", error);
|
||||||
|
showToast("Failed to load preset servers");
|
||||||
|
} finally {
|
||||||
|
setLoadingPresets(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadPresetServers();
|
||||||
|
}, [mcpEnabled]);
|
||||||
|
|
||||||
|
// 加载初始状态
|
||||||
|
useEffect(() => {
|
||||||
|
const loadInitialState = async () => {
|
||||||
|
if (!mcpEnabled) return;
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const config = await getMcpConfigFromFile();
|
||||||
|
setConfig(config);
|
||||||
|
|
||||||
|
// 获取所有客户端的状态
|
||||||
|
const statuses = await getClientsStatus();
|
||||||
|
setClientStatuses(statuses);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load initial state:", error);
|
||||||
|
showToast("Failed to load initial state");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadInitialState();
|
||||||
|
}, [mcpEnabled]);
|
||||||
|
|
||||||
|
// 加载当前编辑服务器的配置
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editingServerId || !config) return;
|
||||||
|
const currentConfig = config.mcpServers[editingServerId];
|
||||||
|
if (currentConfig) {
|
||||||
|
// 从当前配置中提取用户配置
|
||||||
|
const preset = presetServers.find((s) => s.id === editingServerId);
|
||||||
|
if (preset?.configSchema) {
|
||||||
|
const userConfig: Record<string, any> = {};
|
||||||
|
Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => {
|
||||||
|
if (mapping.type === "spread") {
|
||||||
|
// For spread types, extract the array from args.
|
||||||
|
const startPos = mapping.position ?? 0;
|
||||||
|
userConfig[key] = currentConfig.args.slice(startPos);
|
||||||
|
} else if (mapping.type === "single") {
|
||||||
|
// For single types, get a single value
|
||||||
|
userConfig[key] = currentConfig.args[mapping.position ?? 0];
|
||||||
|
} else if (
|
||||||
|
mapping.type === "env" &&
|
||||||
|
mapping.key &&
|
||||||
|
currentConfig.env
|
||||||
|
) {
|
||||||
|
// For env types, get values from environment variables
|
||||||
|
userConfig[key] = currentConfig.env[mapping.key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setUserConfig(userConfig);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setUserConfig({});
|
||||||
|
}
|
||||||
|
}, [editingServerId, config, presetServers]);
|
||||||
|
|
||||||
|
if (!mcpEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查服务器是否已添加
|
||||||
|
const isServerAdded = (id: string) => {
|
||||||
|
return id in (config?.mcpServers ?? {});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存服务器配置
|
||||||
|
const saveServerConfig = async () => {
|
||||||
|
const preset = presetServers.find((s) => s.id === editingServerId);
|
||||||
|
if (!preset || !preset.configSchema || !editingServerId) return;
|
||||||
|
|
||||||
|
const savingServerId = editingServerId;
|
||||||
|
setEditingServerId(undefined);
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateLoadingState(savingServerId, "Updating configuration...");
|
||||||
|
// 构建服务器配置
|
||||||
|
const args = [...preset.baseArgs];
|
||||||
|
const env: Record<string, string> = {};
|
||||||
|
|
||||||
|
Object.entries(preset.argsMapping || {}).forEach(([key, mapping]) => {
|
||||||
|
const value = userConfig[key];
|
||||||
|
if (mapping.type === "spread" && Array.isArray(value)) {
|
||||||
|
const pos = mapping.position ?? 0;
|
||||||
|
args.splice(pos, 0, ...value);
|
||||||
|
} else if (
|
||||||
|
mapping.type === "single" &&
|
||||||
|
mapping.position !== undefined
|
||||||
|
) {
|
||||||
|
args[mapping.position] = value;
|
||||||
|
} else if (
|
||||||
|
mapping.type === "env" &&
|
||||||
|
mapping.key &&
|
||||||
|
typeof value === "string"
|
||||||
|
) {
|
||||||
|
env[mapping.key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const serverConfig: ServerConfig = {
|
||||||
|
command: preset.command,
|
||||||
|
args,
|
||||||
|
...(Object.keys(env).length > 0 ? { env } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const newConfig = await addMcpServer(savingServerId, serverConfig);
|
||||||
|
setConfig(newConfig);
|
||||||
|
showToast("Server configuration updated successfully");
|
||||||
|
} catch (error) {
|
||||||
|
showToast(
|
||||||
|
error instanceof Error ? error.message : "Failed to save configuration",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
updateLoadingState(savingServerId, null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取服务器支持的 Tools
|
||||||
|
const loadTools = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const result = await getClientTools(id);
|
||||||
|
if (result) {
|
||||||
|
setTools(result);
|
||||||
|
} else {
|
||||||
|
throw new Error("Failed to load tools");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showToast("Failed to load tools");
|
||||||
|
console.error(error);
|
||||||
|
setTools(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新加载状态的辅助函数
|
||||||
|
const updateLoadingState = (id: string, message: string | null) => {
|
||||||
|
setLoadingStates((prev) => {
|
||||||
|
if (message === null) {
|
||||||
|
const { [id]: _, ...rest } = prev;
|
||||||
|
return rest;
|
||||||
|
}
|
||||||
|
return { ...prev, [id]: message };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改添加服务器函数
|
||||||
|
const addServer = async (preset: PresetServer) => {
|
||||||
|
if (!preset.configurable) {
|
||||||
|
try {
|
||||||
|
const serverId = preset.id;
|
||||||
|
updateLoadingState(serverId, "Creating MCP client...");
|
||||||
|
|
||||||
|
const serverConfig: ServerConfig = {
|
||||||
|
command: preset.command,
|
||||||
|
args: [...preset.baseArgs],
|
||||||
|
};
|
||||||
|
const newConfig = await addMcpServer(preset.id, serverConfig);
|
||||||
|
setConfig(newConfig);
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
const statuses = await getClientsStatus();
|
||||||
|
setClientStatuses(statuses);
|
||||||
|
} finally {
|
||||||
|
updateLoadingState(preset.id, null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果需要配置,打开配置对话框
|
||||||
|
setEditingServerId(preset.id);
|
||||||
|
setUserConfig({});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改暂停服务器函数
|
||||||
|
const pauseServer = async (id: string) => {
|
||||||
|
try {
|
||||||
|
updateLoadingState(id, "Stopping server...");
|
||||||
|
const newConfig = await pauseMcpServer(id);
|
||||||
|
setConfig(newConfig);
|
||||||
|
showToast("Server stopped successfully");
|
||||||
|
} catch (error) {
|
||||||
|
showToast("Failed to stop server");
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
updateLoadingState(id, null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Restart server
|
||||||
|
const restartServer = async (id: string) => {
|
||||||
|
try {
|
||||||
|
updateLoadingState(id, "Starting server...");
|
||||||
|
await resumeMcpServer(id);
|
||||||
|
} catch (error) {
|
||||||
|
showToast(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to start server, please check logs",
|
||||||
|
);
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
updateLoadingState(id, null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Restart all clients
|
||||||
|
const handleRestartAll = async () => {
|
||||||
|
try {
|
||||||
|
updateLoadingState("all", "Restarting all servers...");
|
||||||
|
const newConfig = await restartAllClients();
|
||||||
|
setConfig(newConfig);
|
||||||
|
showToast("Restarting all clients");
|
||||||
|
} catch (error) {
|
||||||
|
showToast("Failed to restart clients");
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
updateLoadingState("all", null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render configuration form
|
||||||
|
const renderConfigForm = () => {
|
||||||
|
const preset = presetServers.find((s) => s.id === editingServerId);
|
||||||
|
if (!preset?.configSchema) return null;
|
||||||
|
|
||||||
|
return Object.entries(preset.configSchema.properties).map(
|
||||||
|
([key, prop]: [string, ConfigProperty]) => {
|
||||||
|
if (prop.type === "array") {
|
||||||
|
const currentValue = userConfig[key as keyof typeof userConfig] || [];
|
||||||
|
const itemLabel = (prop as any).itemLabel || key;
|
||||||
|
const addButtonText =
|
||||||
|
(prop as any).addButtonText || `Add ${itemLabel}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={key}
|
||||||
|
title={key}
|
||||||
|
subTitle={prop.description}
|
||||||
|
vertical
|
||||||
|
>
|
||||||
|
<div className={styles["path-list"]}>
|
||||||
|
{(currentValue as string[]).map(
|
||||||
|
(value: string, index: number) => (
|
||||||
|
<div key={index} className={styles["path-item"]}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
placeholder={`${itemLabel} ${index + 1}`}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = [...currentValue] as string[];
|
||||||
|
newValue[index] = e.target.value;
|
||||||
|
setUserConfig({ ...userConfig, [key]: newValue });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
className={styles["delete-button"]}
|
||||||
|
onClick={() => {
|
||||||
|
const newValue = [...currentValue] as string[];
|
||||||
|
newValue.splice(index, 1);
|
||||||
|
setUserConfig({ ...userConfig, [key]: newValue });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
icon={<AddIcon />}
|
||||||
|
text={addButtonText}
|
||||||
|
className={styles["add-button"]}
|
||||||
|
bordered
|
||||||
|
onClick={() => {
|
||||||
|
const newValue = [...currentValue, ""] as string[];
|
||||||
|
setUserConfig({ ...userConfig, [key]: newValue });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
} else if (prop.type === "string") {
|
||||||
|
const currentValue = userConfig[key as keyof typeof userConfig] || "";
|
||||||
|
return (
|
||||||
|
<ListItem key={key} title={key} subTitle={prop.description}>
|
||||||
|
<input
|
||||||
|
aria-label={key}
|
||||||
|
type="text"
|
||||||
|
value={currentValue}
|
||||||
|
placeholder={`Enter ${key}`}
|
||||||
|
onChange={(e) => {
|
||||||
|
setUserConfig({ ...userConfig, [key]: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkServerStatus = (clientId: string) => {
|
||||||
|
return clientStatuses[clientId] || { status: "undefined", errorMsg: null };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getServerStatusDisplay = (clientId: string) => {
|
||||||
|
const status = checkServerStatus(clientId);
|
||||||
|
|
||||||
|
const statusMap = {
|
||||||
|
undefined: null, // 未配置/未找到不显示
|
||||||
|
// 添加初始化状态
|
||||||
|
initializing: (
|
||||||
|
<span className={clsx(styles["server-status"], styles["initializing"])}>
|
||||||
|
Initializing
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
paused: (
|
||||||
|
<span className={clsx(styles["server-status"], styles["stopped"])}>
|
||||||
|
Stopped
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
active: <span className={styles["server-status"]}>Running</span>,
|
||||||
|
error: (
|
||||||
|
<span className={clsx(styles["server-status"], styles["error"])}>
|
||||||
|
Error
|
||||||
|
<span className={styles["error-message"]}>: {status.errorMsg}</span>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return statusMap[status.status];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the type of operation status
|
||||||
|
const getOperationStatusType = (message: string) => {
|
||||||
|
if (message.toLowerCase().includes("stopping")) return "stopping";
|
||||||
|
if (message.toLowerCase().includes("starting")) return "starting";
|
||||||
|
if (message.toLowerCase().includes("error")) return "error";
|
||||||
|
return "default";
|
||||||
|
};
|
||||||
|
|
||||||
|
// 渲染服务器列表
|
||||||
|
const renderServerList = () => {
|
||||||
|
if (loadingPresets) {
|
||||||
|
return (
|
||||||
|
<div className={styles["loading-container"]}>
|
||||||
|
<div className={styles["loading-text"]}>
|
||||||
|
Loading preset server list...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(presetServers) || presetServers.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className={styles["empty-container"]}>
|
||||||
|
<div className={styles["empty-text"]}>No servers available</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return presetServers
|
||||||
|
.filter((server) => {
|
||||||
|
if (searchText.length === 0) return true;
|
||||||
|
const searchLower = searchText.toLowerCase();
|
||||||
|
return (
|
||||||
|
server.name.toLowerCase().includes(searchLower) ||
|
||||||
|
server.description.toLowerCase().includes(searchLower) ||
|
||||||
|
server.tags.some((tag) => tag.toLowerCase().includes(searchLower))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aStatus = checkServerStatus(a.id).status;
|
||||||
|
const bStatus = checkServerStatus(b.id).status;
|
||||||
|
const aLoading = loadingStates[a.id];
|
||||||
|
const bLoading = loadingStates[b.id];
|
||||||
|
|
||||||
|
// 定义状态优先级
|
||||||
|
const statusPriority: Record<string, number> = {
|
||||||
|
error: 0, // Highest priority for error status
|
||||||
|
active: 1, // Second for active
|
||||||
|
initializing: 2, // Initializing
|
||||||
|
starting: 3, // Starting
|
||||||
|
stopping: 4, // Stopping
|
||||||
|
paused: 5, // Paused
|
||||||
|
undefined: 6, // Lowest priority for undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get actual status (including loading status)
|
||||||
|
const getEffectiveStatus = (status: string, loading?: string) => {
|
||||||
|
if (loading) {
|
||||||
|
const operationType = getOperationStatusType(loading);
|
||||||
|
return operationType === "default" ? status : operationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "initializing" && !loading) {
|
||||||
|
return "active";
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
};
|
||||||
|
|
||||||
|
const aEffectiveStatus = getEffectiveStatus(aStatus, aLoading);
|
||||||
|
const bEffectiveStatus = getEffectiveStatus(bStatus, bLoading);
|
||||||
|
|
||||||
|
// 首先按状态排序
|
||||||
|
if (aEffectiveStatus !== bEffectiveStatus) {
|
||||||
|
return (
|
||||||
|
(statusPriority[aEffectiveStatus] ?? 6) -
|
||||||
|
(statusPriority[bEffectiveStatus] ?? 6)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by name when statuses are the same
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
})
|
||||||
|
.map((server) => (
|
||||||
|
<div
|
||||||
|
className={clsx(styles["mcp-market-item"], {
|
||||||
|
[styles["loading"]]: loadingStates[server.id],
|
||||||
|
})}
|
||||||
|
key={server.id}
|
||||||
|
>
|
||||||
|
<div className={styles["mcp-market-header"]}>
|
||||||
|
<div className={styles["mcp-market-title"]}>
|
||||||
|
<div className={styles["mcp-market-name"]}>
|
||||||
|
{server.name}
|
||||||
|
{loadingStates[server.id] && (
|
||||||
|
<span
|
||||||
|
className={styles["operation-status"]}
|
||||||
|
data-status={getOperationStatusType(
|
||||||
|
loadingStates[server.id],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{loadingStates[server.id]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!loadingStates[server.id] && getServerStatusDisplay(server.id)}
|
||||||
|
{server.repo && (
|
||||||
|
<a
|
||||||
|
href={server.repo}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className={styles["repo-link"]}
|
||||||
|
title="Open repository"
|
||||||
|
>
|
||||||
|
<GithubIcon />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={styles["tags-container"]}>
|
||||||
|
{server.tags.map((tag, index) => (
|
||||||
|
<span key={index} className={styles["tag"]}>
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={clsx(styles["mcp-market-info"], "one-line")}
|
||||||
|
title={server.description}
|
||||||
|
>
|
||||||
|
{server.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles["mcp-market-actions"]}>
|
||||||
|
{isServerAdded(server.id) ? (
|
||||||
|
<>
|
||||||
|
{server.configurable && (
|
||||||
|
<IconButton
|
||||||
|
icon={<EditIcon />}
|
||||||
|
text="Configure"
|
||||||
|
onClick={() => setEditingServerId(server.id)}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{checkServerStatus(server.id).status === "paused" ? (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
icon={<PlayIcon />}
|
||||||
|
text="Start"
|
||||||
|
onClick={() => restartServer(server.id)}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
{/* <IconButton
|
||||||
|
icon={<DeleteIcon />}
|
||||||
|
text="Remove"
|
||||||
|
onClick={() => removeServer(server.id)}
|
||||||
|
disabled={isLoading}
|
||||||
|
/> */}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
icon={<EyeIcon />}
|
||||||
|
text="Tools"
|
||||||
|
onClick={async () => {
|
||||||
|
setViewingServerId(server.id);
|
||||||
|
await loadTools(server.id);
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
isLoading ||
|
||||||
|
checkServerStatus(server.id).status === "error"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
icon={<StopIcon />}
|
||||||
|
text="Stop"
|
||||||
|
onClick={() => pauseServer(server.id)}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<IconButton
|
||||||
|
icon={<AddIcon />}
|
||||||
|
text="Add"
|
||||||
|
onClick={() => addServer(server)}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<div className={styles["mcp-market-page"]}>
|
||||||
|
<div className="window-header">
|
||||||
|
<div className="window-header-title">
|
||||||
|
<div className="window-header-main-title">
|
||||||
|
MCP Market
|
||||||
|
{loadingStates["all"] && (
|
||||||
|
<span className={styles["loading-indicator"]}>
|
||||||
|
{loadingStates["all"]}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="window-header-sub-title">
|
||||||
|
{Object.keys(config?.mcpServers ?? {}).length} servers configured
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="window-actions">
|
||||||
|
<div className="window-action-button">
|
||||||
|
<IconButton
|
||||||
|
icon={<RestartIcon />}
|
||||||
|
bordered
|
||||||
|
onClick={handleRestartAll}
|
||||||
|
text="Restart All"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="window-action-button">
|
||||||
|
<IconButton
|
||||||
|
icon={<CloseIcon />}
|
||||||
|
bordered
|
||||||
|
onClick={() => navigate(-1)}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles["mcp-market-page-body"]}>
|
||||||
|
<div className={styles["mcp-market-filter"]}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles["search-bar"]}
|
||||||
|
placeholder={"Search MCP Server"}
|
||||||
|
autoFocus
|
||||||
|
onInput={(e) => setSearchText(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles["server-list"]}>{renderServerList()}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/*编辑服务器配置*/}
|
||||||
|
{editingServerId && (
|
||||||
|
<div className="modal-mask">
|
||||||
|
<Modal
|
||||||
|
title={`Configure Server - ${editingServerId}`}
|
||||||
|
onClose={() => !isLoading && setEditingServerId(undefined)}
|
||||||
|
actions={[
|
||||||
|
<IconButton
|
||||||
|
key="cancel"
|
||||||
|
text="Cancel"
|
||||||
|
onClick={() => setEditingServerId(undefined)}
|
||||||
|
bordered
|
||||||
|
disabled={isLoading}
|
||||||
|
/>,
|
||||||
|
<IconButton
|
||||||
|
key="confirm"
|
||||||
|
text="Save"
|
||||||
|
type="primary"
|
||||||
|
onClick={saveServerConfig}
|
||||||
|
bordered
|
||||||
|
disabled={isLoading}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<List>{renderConfigForm()}</List>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{viewingServerId && (
|
||||||
|
<div className="modal-mask">
|
||||||
|
<Modal
|
||||||
|
title={`Server Details - ${viewingServerId}`}
|
||||||
|
onClose={() => setViewingServerId(undefined)}
|
||||||
|
actions={[
|
||||||
|
<IconButton
|
||||||
|
key="close"
|
||||||
|
text="Close"
|
||||||
|
onClick={() => setViewingServerId(undefined)}
|
||||||
|
bordered
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div className={styles["tools-list"]}>
|
||||||
|
{isLoading ? (
|
||||||
|
<div>Loading...</div>
|
||||||
|
) : tools?.tools ? (
|
||||||
|
tools.tools.map(
|
||||||
|
(tool: ListToolsResponse["tools"], index: number) => (
|
||||||
|
<div key={index} className={styles["tool-item"]}>
|
||||||
|
<div className={styles["tool-name"]}>{tool.name}</div>
|
||||||
|
<div className={styles["tool-description"]}>
|
||||||
|
{tool.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div>No tools available</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef, useMemo, useState, Fragment } from "react";
|
import React, { Fragment, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
|
||||||
import styles from "./home.module.scss";
|
import styles from "./home.module.scss";
|
||||||
|
|
||||||
@ -9,6 +9,7 @@ import ChatGptIcon from "../icons/chatgpt.svg";
|
|||||||
import AddIcon from "../icons/add.svg";
|
import AddIcon from "../icons/add.svg";
|
||||||
import DeleteIcon from "../icons/delete.svg";
|
import DeleteIcon from "../icons/delete.svg";
|
||||||
import MaskIcon from "../icons/mask.svg";
|
import MaskIcon from "../icons/mask.svg";
|
||||||
|
import McpIcon from "../icons/mcp.svg";
|
||||||
import DragIcon from "../icons/drag.svg";
|
import DragIcon from "../icons/drag.svg";
|
||||||
import DiscoveryIcon from "../icons/discovery.svg";
|
import DiscoveryIcon from "../icons/discovery.svg";
|
||||||
|
|
||||||
@ -28,8 +29,9 @@ import {
|
|||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { isIOS, useMobileScreen } from "../utils";
|
import { isIOS, useMobileScreen } from "../utils";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { showConfirm, Selector } from "./ui-lib";
|
import { Selector, showConfirm } from "./ui-lib";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { isMcpEnabled } from "../mcp/actions";
|
||||||
|
|
||||||
const DISCOVERY = [
|
const DISCOVERY = [
|
||||||
{ name: Locale.Plugin.Name, path: Path.Plugins },
|
{ name: Locale.Plugin.Name, path: Path.Plugins },
|
||||||
@ -133,6 +135,7 @@ export function useDragSideBar() {
|
|||||||
shouldNarrow,
|
shouldNarrow,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SideBarContainer(props: {
|
export function SideBarContainer(props: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
onDragStart: (e: MouseEvent) => void;
|
onDragStart: (e: MouseEvent) => void;
|
||||||
@ -228,6 +231,17 @@ export function SideBar(props: { className?: string }) {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const config = useAppConfig();
|
const config = useAppConfig();
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
|
const [mcpEnabled, setMcpEnabled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 检查 MCP 是否启用
|
||||||
|
const checkMcpStatus = async () => {
|
||||||
|
const enabled = await isMcpEnabled();
|
||||||
|
setMcpEnabled(enabled);
|
||||||
|
console.log("[SideBar] MCP enabled:", enabled);
|
||||||
|
};
|
||||||
|
checkMcpStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SideBarContainer
|
<SideBarContainer
|
||||||
@ -255,6 +269,17 @@ export function SideBar(props: { className?: string }) {
|
|||||||
}}
|
}}
|
||||||
shadow
|
shadow
|
||||||
/>
|
/>
|
||||||
|
{mcpEnabled && (
|
||||||
|
<IconButton
|
||||||
|
icon={<McpIcon />}
|
||||||
|
text={shouldNarrow ? undefined : Locale.Mcp.Name}
|
||||||
|
className={styles["sidebar-bar-button"]}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(Path.McpMarket, { state: { fromHome: true } });
|
||||||
|
}}
|
||||||
|
shadow
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<DiscoveryIcon />}
|
icon={<DiscoveryIcon />}
|
||||||
text={shouldNarrow ? undefined : Locale.Discovery.Name}
|
text={shouldNarrow ? undefined : Locale.Discovery.Name}
|
||||||
|
@ -92,6 +92,8 @@ declare global {
|
|||||||
|
|
||||||
// custom template for preprocessing user input
|
// custom template for preprocessing user input
|
||||||
DEFAULT_INPUT_TEMPLATE?: string;
|
DEFAULT_INPUT_TEMPLATE?: string;
|
||||||
|
|
||||||
|
ENABLE_MCP?: string; // enable mcp functionality
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -268,5 +270,6 @@ export const getServerSideConfig = () => {
|
|||||||
defaultModel,
|
defaultModel,
|
||||||
visionModels,
|
visionModels,
|
||||||
allowedWebDavEndpoints,
|
allowedWebDavEndpoints,
|
||||||
|
enableMcp: !!process.env.ENABLE_MCP,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
126
app/constant.ts
126
app/constant.ts
@ -49,6 +49,7 @@ export enum Path {
|
|||||||
SdNew = "/sd-new",
|
SdNew = "/sd-new",
|
||||||
Artifacts = "/artifacts",
|
Artifacts = "/artifacts",
|
||||||
SearchChat = "/search-chat",
|
SearchChat = "/search-chat",
|
||||||
|
McpMarket = "/mcp-market",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ApiPath {
|
export enum ApiPath {
|
||||||
@ -91,6 +92,7 @@ export enum StoreKey {
|
|||||||
Update = "chat-update",
|
Update = "chat-update",
|
||||||
Sync = "sync",
|
Sync = "sync",
|
||||||
SdList = "sd-list",
|
SdList = "sd-list",
|
||||||
|
Mcp = "mcp-store",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_SIDEBAR_WIDTH = 300;
|
export const DEFAULT_SIDEBAR_WIDTH = 300;
|
||||||
@ -277,6 +279,130 @@ Latex inline: \\(x^2\\)
|
|||||||
Latex block: $$e=mc^2$$
|
Latex block: $$e=mc^2$$
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const MCP_TOOLS_TEMPLATE = `
|
||||||
|
[clientId]
|
||||||
|
{{ clientId }}
|
||||||
|
[tools]
|
||||||
|
{{ tools }}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MCP_SYSTEM_TEMPLATE = `
|
||||||
|
You are an AI assistant with access to system tools. Your role is to help users by combining natural language understanding with tool operations when needed.
|
||||||
|
|
||||||
|
1. AVAILABLE TOOLS:
|
||||||
|
{{ MCP_TOOLS }}
|
||||||
|
|
||||||
|
2. WHEN TO USE TOOLS:
|
||||||
|
- ALWAYS USE TOOLS when they can help answer user questions
|
||||||
|
- DO NOT just describe what you could do - TAKE ACTION immediately
|
||||||
|
- If you're not sure whether to use a tool, USE IT
|
||||||
|
- Common triggers for tool use:
|
||||||
|
* Questions about files or directories
|
||||||
|
* Requests to check, list, or manipulate system resources
|
||||||
|
* Any query that can be answered with available tools
|
||||||
|
|
||||||
|
3. HOW TO USE TOOLS:
|
||||||
|
A. Tool Call Format:
|
||||||
|
- Use markdown code blocks with format: \`\`\`json:mcp:{clientId}\`\`\`
|
||||||
|
- Always include:
|
||||||
|
* method: "tools/call"(Only this method is supported)
|
||||||
|
* params:
|
||||||
|
- name: must match an available primitive name
|
||||||
|
- arguments: required parameters for the primitive
|
||||||
|
|
||||||
|
B. Response Format:
|
||||||
|
- Tool responses will come as user messages
|
||||||
|
- Format: \`\`\`json:mcp-response:{clientId}\`\`\`
|
||||||
|
- Wait for response before making another tool call
|
||||||
|
|
||||||
|
C. Important Rules:
|
||||||
|
- Only use tools/call method
|
||||||
|
- Only ONE tool call per message
|
||||||
|
- ALWAYS TAKE ACTION instead of just describing what you could do
|
||||||
|
- Include the correct clientId in code block language tag
|
||||||
|
- Verify arguments match the primitive's requirements
|
||||||
|
|
||||||
|
4. INTERACTION FLOW:
|
||||||
|
A. When user makes a request:
|
||||||
|
- IMMEDIATELY use appropriate tool if available
|
||||||
|
- DO NOT ask if user wants you to use the tool
|
||||||
|
- DO NOT just describe what you could do
|
||||||
|
B. After receiving tool response:
|
||||||
|
- Explain results clearly
|
||||||
|
- Take next appropriate action if needed
|
||||||
|
C. If tools fail:
|
||||||
|
- Explain the error
|
||||||
|
- Try alternative approach immediately
|
||||||
|
|
||||||
|
5. EXAMPLE INTERACTION:
|
||||||
|
|
||||||
|
good example:
|
||||||
|
|
||||||
|
\`\`\`json:mcp:filesystem
|
||||||
|
{
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "list_allowed_directories",
|
||||||
|
"arguments": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\`\`\`"
|
||||||
|
|
||||||
|
|
||||||
|
\`\`\`json:mcp-response:filesystem
|
||||||
|
{
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "write_file",
|
||||||
|
"arguments": {
|
||||||
|
"path": "/Users/river/dev/nextchat/test/joke.txt",
|
||||||
|
"content": "为什么数学书总是感到忧伤?因为它有太多的问题。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
follwing is the wrong! mcp json example:
|
||||||
|
|
||||||
|
\`\`\`json:mcp:filesystem
|
||||||
|
{
|
||||||
|
"method": "write_file",
|
||||||
|
"params": {
|
||||||
|
"path": "NextChat_Information.txt",
|
||||||
|
"content": "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
This is wrong because the method is not tools/call.
|
||||||
|
|
||||||
|
\`\`\`{
|
||||||
|
"method": "search_repositories",
|
||||||
|
"params": {
|
||||||
|
"query": "2oeee"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
This is wrong because the method is not tools/call.!!!!!!!!!!!
|
||||||
|
|
||||||
|
the right format is:
|
||||||
|
\`\`\`json:mcp:filesystem
|
||||||
|
{
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "search_repositories",
|
||||||
|
"arguments": {
|
||||||
|
"query": "2oeee"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
please follow the format strictly ONLY use tools/call method!!!!!!!!!!!
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
export const SUMMARIZE_MODEL = "gpt-4o-mini";
|
export const SUMMARIZE_MODEL = "gpt-4o-mini";
|
||||||
export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";
|
export const GEMINI_SUMMARIZE_MODEL = "gemini-pro";
|
||||||
|
|
||||||
|
15
app/icons/mcp.svg
Normal file
15
app/icons/mcp.svg
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 180 180" fill="none">
|
||||||
|
<g clip-path="url(#clip0_19_13)">
|
||||||
|
<path d="M18 84.8528L85.8822 16.9706C95.2548 7.59798 110.451 7.59798 119.823 16.9706V16.9706C129.196 26.3431 129.196 41.5391 119.823 50.9117L68.5581 102.177"
|
||||||
|
stroke="black" stroke-width="12" stroke-linecap="round"/>
|
||||||
|
<path d="M69.2652 101.47L119.823 50.9117C129.196 41.5391 144.392 41.5391 153.765 50.9117L154.118 51.2652C163.491 60.6378 163.491 75.8338 154.118 85.2063L92.7248 146.6C89.6006 149.724 89.6006 154.789 92.7248 157.913L105.331 170.52"
|
||||||
|
stroke="black" stroke-width="12" stroke-linecap="round"/>
|
||||||
|
<path d="M102.853 33.9411L52.6482 84.1457C43.2756 93.5183 43.2756 108.714 52.6482 118.087V118.087C62.0208 127.459 77.2167 127.459 86.5893 118.087L136.794 67.8822"
|
||||||
|
stroke="black" stroke-width="12" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_19_13">
|
||||||
|
<rect width="180" height="180" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -1 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" fill="none" viewBox="0 0 16 16"><defs><rect id="path_0" width="16" height="16" x="0" y="0"/></defs><g opacity="1" transform="translate(0 0) rotate(0 8 8)"><mask id="bg-mask-0" fill="#fff"><use xlink:href="#path_0"/></mask><g mask="url(#bg-mask-0)"><path id="路径 1" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M13.33,6.67C13.33,2.98 10.35,0 6.67,0C2.98,0 0,2.98 0,6.67C0,10.35 2.98,13.33 6.67,13.33C10.35,13.33 13.33,10.35 13.33,6.67Z" transform="translate(1.3333333333333333 1.3333333333333333) rotate(0 6.666666666666666 6.666666666666666)"/><path id="路径 2" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L0,4" transform="translate(6.333333333333333 6) rotate(0 0 2)"/><path id="路径 3" style="stroke:#333;stroke-width:1.3333333333333333;stroke-opacity:1;stroke-dasharray:0 0" d="M0,0L0,4" transform="translate(9.666666666666666 6) rotate(0 0 2)"/></g></g></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
||||||
|
</svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 253 B |
3
app/icons/play.svg
Normal file
3
app/icons/play.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polygon points="5 3 19 12 5 21 5 3"></polygon>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 239 B |
1
app/icons/tool.svg
Normal file
1
app/icons/tool.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M10.155 3.247c-.519.396-1.129 1.004-2.012 1.887s-1.49 1.493-1.887 2.012c-.383.502-.497.83-.497 1.14s.114.638.497 1.14c.397.52 1.004 1.13 1.887 2.012l4.419 4.419c.883.883 1.493 1.49 2.012 1.887c.502.383.83.497 1.14.497s.638-.114 1.14-.497c.519-.396 1.129-1.004 2.012-1.887s1.49-1.493 1.887-2.012c.383-.503.497-.83.497-1.14s-.114-.638-.497-1.14c-.396-.52-1.004-1.13-1.887-2.012l-4.419-4.419c-.883-.883-1.493-1.49-2.012-1.887c-.502-.383-.83-.497-1.14-.497s-.637.114-1.14.497m-.91-1.192c.636-.485 1.28-.805 2.05-.805s1.414.32 2.05.805c.609.464 1.29 1.145 2.125 1.98l.244.245c.239-.238.451-.44.685-.574a2.31 2.31 0 0 1 2.312 0c.267.154.505.393.787.675l.06.06l.061.061c.282.282.521.52.675.787a2.31 2.31 0 0 1 0 2.312c-.135.234-.336.446-.574.685l.245.244c.835.836 1.516 1.516 1.98 2.125c.485.636.805 1.28.805 2.05s-.32 1.414-.805 2.05c-.464.608-1.145 1.289-1.98 2.124l-.077.077c-.835.835-1.516 1.516-2.125 1.98c-.635.485-1.28.805-2.05.805c-.768 0-1.413-.32-2.049-.805c-.609-.464-1.29-1.145-2.125-1.98l-.244-.245l-4.993 4.994l-.06.06c-.282.282-.52.521-.787.675a2.31 2.31 0 0 1-2.312 0c-.267-.154-.505-.393-.787-.675l-.06-.06l-.061-.061c-.282-.282-.521-.52-.675-.787a2.31 2.31 0 0 1 0-2.312c.154-.266.393-.505.675-.786l.06-.061l4.994-4.993l-.245-.244c-.835-.836-1.516-1.516-1.98-2.125c-.485-.636-.805-1.28-.805-2.05s.32-1.414.805-2.05c.464-.608 1.145-1.289 1.98-2.124l.077-.077c.835-.835 1.516-1.516 2.125-1.98m-.896 11.71L3.356 18.76c-.376.376-.456.465-.497.536a.81.81 0 0 0 0 .812c.04.072.12.16.497.537c.377.376.466.456.537.497a.81.81 0 0 0 .812 0c.07-.04.16-.12.536-.497l4.994-4.993zm10.31-6.54c.24-.243.302-.314.336-.374a.81.81 0 0 0 0-.812c-.041-.071-.12-.16-.497-.537c-.377-.376-.466-.456-.537-.497a.81.81 0 0 0-.812 0c-.06.034-.131.096-.374.336z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 1.9 KiB |
@ -5,9 +5,8 @@ import "./styles/highlight.scss";
|
|||||||
import { getClientConfig } from "./config/client";
|
import { getClientConfig } from "./config/client";
|
||||||
import type { Metadata, Viewport } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||||
import { getServerSideConfig } from "./config/server";
|
|
||||||
import { GoogleTagManager, GoogleAnalytics } from "@next/third-parties/google";
|
import { GoogleTagManager, GoogleAnalytics } from "@next/third-parties/google";
|
||||||
const serverConfig = getServerSideConfig();
|
import { getServerSideConfig } from "./config/server";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "NextChat",
|
title: "NextChat",
|
||||||
@ -33,6 +32,8 @@ export default function RootLayout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
const serverConfig = getServerSideConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
@ -664,6 +664,9 @@ const cn = {
|
|||||||
Discovery: {
|
Discovery: {
|
||||||
Name: "发现",
|
Name: "发现",
|
||||||
},
|
},
|
||||||
|
Mcp: {
|
||||||
|
Name: "MCP",
|
||||||
|
},
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "你是一个助手",
|
Sysmessage: "你是一个助手",
|
||||||
},
|
},
|
||||||
|
@ -674,6 +674,9 @@ const en: LocaleType = {
|
|||||||
Discovery: {
|
Discovery: {
|
||||||
Name: "Discovery",
|
Name: "Discovery",
|
||||||
},
|
},
|
||||||
|
Mcp: {
|
||||||
|
Name: "MCP",
|
||||||
|
},
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "You are an assistant that",
|
Sysmessage: "You are an assistant that",
|
||||||
},
|
},
|
||||||
|
383
app/mcp/actions.ts
Normal file
383
app/mcp/actions.ts
Normal file
@ -0,0 +1,383 @@
|
|||||||
|
"use server";
|
||||||
|
import {
|
||||||
|
createClient,
|
||||||
|
executeRequest,
|
||||||
|
listTools,
|
||||||
|
removeClient,
|
||||||
|
} from "./client";
|
||||||
|
import { MCPClientLogger } from "./logger";
|
||||||
|
import {
|
||||||
|
DEFAULT_MCP_CONFIG,
|
||||||
|
McpClientData,
|
||||||
|
McpConfigData,
|
||||||
|
McpRequestMessage,
|
||||||
|
ServerConfig,
|
||||||
|
ServerStatusResponse,
|
||||||
|
} from "./types";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import { getServerSideConfig } from "../config/server";
|
||||||
|
|
||||||
|
const logger = new MCPClientLogger("MCP Actions");
|
||||||
|
const CONFIG_PATH = path.join(process.cwd(), "app/mcp/mcp_config.json");
|
||||||
|
|
||||||
|
const clientsMap = new Map<string, McpClientData>();
|
||||||
|
|
||||||
|
// 获取客户端状态
|
||||||
|
export async function getClientsStatus(): Promise<
|
||||||
|
Record<string, ServerStatusResponse>
|
||||||
|
> {
|
||||||
|
const config = await getMcpConfigFromFile();
|
||||||
|
const result: Record<string, ServerStatusResponse> = {};
|
||||||
|
|
||||||
|
for (const clientId of Object.keys(config.mcpServers)) {
|
||||||
|
const status = clientsMap.get(clientId);
|
||||||
|
const serverConfig = config.mcpServers[clientId];
|
||||||
|
|
||||||
|
if (!serverConfig) {
|
||||||
|
result[clientId] = { status: "undefined", errorMsg: null };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverConfig.status === "paused") {
|
||||||
|
result[clientId] = { status: "paused", errorMsg: null };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
result[clientId] = { status: "undefined", errorMsg: null };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
status.client === null &&
|
||||||
|
status.tools === null &&
|
||||||
|
status.errorMsg === null
|
||||||
|
) {
|
||||||
|
result[clientId] = { status: "initializing", errorMsg: null };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.errorMsg) {
|
||||||
|
result[clientId] = { status: "error", errorMsg: status.errorMsg };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.client) {
|
||||||
|
result[clientId] = { status: "active", errorMsg: null };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result[clientId] = { status: "error", errorMsg: "Client not found" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取客户端工具
|
||||||
|
export async function getClientTools(clientId: string) {
|
||||||
|
return clientsMap.get(clientId)?.tools ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取可用客户端数量
|
||||||
|
export async function getAvailableClientsCount() {
|
||||||
|
let count = 0;
|
||||||
|
clientsMap.forEach((map) => !map.errorMsg && count++);
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有客户端工具
|
||||||
|
export async function getAllTools() {
|
||||||
|
const result = [];
|
||||||
|
for (const [clientId, status] of clientsMap.entries()) {
|
||||||
|
result.push({
|
||||||
|
clientId,
|
||||||
|
tools: status.tools,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化单个客户端
|
||||||
|
async function initializeSingleClient(
|
||||||
|
clientId: string,
|
||||||
|
serverConfig: ServerConfig,
|
||||||
|
) {
|
||||||
|
// 如果服务器状态是暂停,则不初始化
|
||||||
|
if (serverConfig.status === "paused") {
|
||||||
|
logger.info(`Skipping initialization for paused client [${clientId}]`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Initializing client [${clientId}]...`);
|
||||||
|
|
||||||
|
// 先设置初始化状态
|
||||||
|
clientsMap.set(clientId, {
|
||||||
|
client: null,
|
||||||
|
tools: null,
|
||||||
|
errorMsg: null, // null 表示正在初始化
|
||||||
|
});
|
||||||
|
|
||||||
|
// 异步初始化
|
||||||
|
createClient(clientId, serverConfig)
|
||||||
|
.then(async (client) => {
|
||||||
|
const tools = await listTools(client);
|
||||||
|
logger.info(
|
||||||
|
`Supported tools for [${clientId}]: ${JSON.stringify(tools, null, 2)}`,
|
||||||
|
);
|
||||||
|
clientsMap.set(clientId, { client, tools, errorMsg: null });
|
||||||
|
logger.success(`Client [${clientId}] initialized successfully`);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
clientsMap.set(clientId, {
|
||||||
|
client: null,
|
||||||
|
tools: null,
|
||||||
|
errorMsg: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
logger.error(`Failed to initialize client [${clientId}]: ${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化系统
|
||||||
|
export async function initializeMcpSystem() {
|
||||||
|
logger.info("MCP Actions starting...");
|
||||||
|
try {
|
||||||
|
// 检查是否已有活跃的客户端
|
||||||
|
if (clientsMap.size > 0) {
|
||||||
|
logger.info("MCP system already initialized, skipping...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await getMcpConfigFromFile();
|
||||||
|
// 初始化所有客户端
|
||||||
|
for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
|
||||||
|
await initializeSingleClient(clientId, serverConfig);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to initialize MCP system: ${error}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加服务器
|
||||||
|
export async function addMcpServer(clientId: string, config: ServerConfig) {
|
||||||
|
try {
|
||||||
|
const currentConfig = await getMcpConfigFromFile();
|
||||||
|
const isNewServer = !(clientId in currentConfig.mcpServers);
|
||||||
|
|
||||||
|
// 如果是新服务器,设置默认状态为 active
|
||||||
|
if (isNewServer && !config.status) {
|
||||||
|
config.status = "active";
|
||||||
|
}
|
||||||
|
|
||||||
|
const newConfig = {
|
||||||
|
...currentConfig,
|
||||||
|
mcpServers: {
|
||||||
|
...currentConfig.mcpServers,
|
||||||
|
[clientId]: config,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await updateMcpConfig(newConfig);
|
||||||
|
|
||||||
|
// 只有新服务器或状态为 active 的服务器才初始化
|
||||||
|
if (isNewServer || config.status === "active") {
|
||||||
|
await initializeSingleClient(clientId, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newConfig;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to add server [${clientId}]: ${error}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暂停服务器
|
||||||
|
export async function pauseMcpServer(clientId: string) {
|
||||||
|
try {
|
||||||
|
const currentConfig = await getMcpConfigFromFile();
|
||||||
|
const serverConfig = currentConfig.mcpServers[clientId];
|
||||||
|
if (!serverConfig) {
|
||||||
|
throw new Error(`Server ${clientId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先更新配置
|
||||||
|
const newConfig: McpConfigData = {
|
||||||
|
...currentConfig,
|
||||||
|
mcpServers: {
|
||||||
|
...currentConfig.mcpServers,
|
||||||
|
[clientId]: {
|
||||||
|
...serverConfig,
|
||||||
|
status: "paused",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await updateMcpConfig(newConfig);
|
||||||
|
|
||||||
|
// 然后关闭客户端
|
||||||
|
const client = clientsMap.get(clientId);
|
||||||
|
if (client?.client) {
|
||||||
|
await removeClient(client.client);
|
||||||
|
}
|
||||||
|
clientsMap.delete(clientId);
|
||||||
|
|
||||||
|
return newConfig;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to pause server [${clientId}]: ${error}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 恢复服务器
|
||||||
|
export async function resumeMcpServer(clientId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const currentConfig = await getMcpConfigFromFile();
|
||||||
|
const serverConfig = currentConfig.mcpServers[clientId];
|
||||||
|
if (!serverConfig) {
|
||||||
|
throw new Error(`Server ${clientId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先尝试初始化客户端
|
||||||
|
logger.info(`Trying to initialize client [${clientId}]...`);
|
||||||
|
try {
|
||||||
|
const client = await createClient(clientId, serverConfig);
|
||||||
|
const tools = await listTools(client);
|
||||||
|
clientsMap.set(clientId, { client, tools, errorMsg: null });
|
||||||
|
logger.success(`Client [${clientId}] initialized successfully`);
|
||||||
|
|
||||||
|
// 初始化成功后更新配置
|
||||||
|
const newConfig: McpConfigData = {
|
||||||
|
...currentConfig,
|
||||||
|
mcpServers: {
|
||||||
|
...currentConfig.mcpServers,
|
||||||
|
[clientId]: {
|
||||||
|
...serverConfig,
|
||||||
|
status: "active" as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await updateMcpConfig(newConfig);
|
||||||
|
} catch (error) {
|
||||||
|
const currentConfig = await getMcpConfigFromFile();
|
||||||
|
const serverConfig = currentConfig.mcpServers[clientId];
|
||||||
|
|
||||||
|
// 如果配置中存在该服务器,则更新其状态为 error
|
||||||
|
if (serverConfig) {
|
||||||
|
serverConfig.status = "error";
|
||||||
|
await updateMcpConfig(currentConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化失败
|
||||||
|
clientsMap.set(clientId, {
|
||||||
|
client: null,
|
||||||
|
tools: null,
|
||||||
|
errorMsg: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
logger.error(`Failed to initialize client [${clientId}]: ${error}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to resume server [${clientId}]: ${error}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除服务器
|
||||||
|
export async function removeMcpServer(clientId: string) {
|
||||||
|
try {
|
||||||
|
const currentConfig = await getMcpConfigFromFile();
|
||||||
|
const { [clientId]: _, ...rest } = currentConfig.mcpServers;
|
||||||
|
const newConfig = {
|
||||||
|
...currentConfig,
|
||||||
|
mcpServers: rest,
|
||||||
|
};
|
||||||
|
await updateMcpConfig(newConfig);
|
||||||
|
|
||||||
|
// 关闭并移除客户端
|
||||||
|
const client = clientsMap.get(clientId);
|
||||||
|
if (client?.client) {
|
||||||
|
await removeClient(client.client);
|
||||||
|
}
|
||||||
|
clientsMap.delete(clientId);
|
||||||
|
|
||||||
|
return newConfig;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to remove server [${clientId}]: ${error}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重启所有客户端
|
||||||
|
export async function restartAllClients() {
|
||||||
|
logger.info("Restarting all clients...");
|
||||||
|
try {
|
||||||
|
// 关闭所有客户端
|
||||||
|
for (const client of clientsMap.values()) {
|
||||||
|
if (client.client) {
|
||||||
|
await removeClient(client.client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空状态
|
||||||
|
clientsMap.clear();
|
||||||
|
|
||||||
|
// 重新初始化
|
||||||
|
const config = await getMcpConfigFromFile();
|
||||||
|
for (const [clientId, serverConfig] of Object.entries(config.mcpServers)) {
|
||||||
|
await initializeSingleClient(clientId, serverConfig);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to restart clients: ${error}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行 MCP 请求
|
||||||
|
export async function executeMcpAction(
|
||||||
|
clientId: string,
|
||||||
|
request: McpRequestMessage,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const client = clientsMap.get(clientId);
|
||||||
|
if (!client?.client) {
|
||||||
|
throw new Error(`Client ${clientId} not found`);
|
||||||
|
}
|
||||||
|
logger.info(`Executing request for [${clientId}]`);
|
||||||
|
return await executeRequest(client.client, request);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to execute request for [${clientId}]: ${error}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 MCP 配置文件
|
||||||
|
export async function getMcpConfigFromFile(): Promise<McpConfigData> {
|
||||||
|
try {
|
||||||
|
const configStr = await fs.readFile(CONFIG_PATH, "utf-8");
|
||||||
|
return JSON.parse(configStr);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to load MCP config, using default config: ${error}`);
|
||||||
|
return DEFAULT_MCP_CONFIG;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 MCP 配置文件
|
||||||
|
async function updateMcpConfig(config: McpConfigData): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2));
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 MCP 是否启用
|
||||||
|
export async function isMcpEnabled() {
|
||||||
|
try {
|
||||||
|
const serverConfig = getServerSideConfig();
|
||||||
|
return serverConfig.enableMcp;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Failed to check MCP status: ${error}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
55
app/mcp/client.ts
Normal file
55
app/mcp/client.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||||
|
import { MCPClientLogger } from "./logger";
|
||||||
|
import { ListToolsResponse, McpRequestMessage, ServerConfig } from "./types";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const logger = new MCPClientLogger();
|
||||||
|
|
||||||
|
export async function createClient(
|
||||||
|
id: string,
|
||||||
|
config: ServerConfig,
|
||||||
|
): Promise<Client> {
|
||||||
|
logger.info(`Creating client for ${id}...`);
|
||||||
|
|
||||||
|
const transport = new StdioClientTransport({
|
||||||
|
command: config.command,
|
||||||
|
args: config.args,
|
||||||
|
env: {
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(process.env)
|
||||||
|
.filter(([_, v]) => v !== undefined)
|
||||||
|
.map(([k, v]) => [k, v as string]),
|
||||||
|
),
|
||||||
|
...(config.env || {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = new Client(
|
||||||
|
{
|
||||||
|
name: `nextchat-mcp-client-${id}`,
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await client.connect(transport);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeClient(client: Client) {
|
||||||
|
logger.info(`Removing client...`);
|
||||||
|
await client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listTools(client: Client): Promise<ListToolsResponse> {
|
||||||
|
return client.listTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function executeRequest(
|
||||||
|
client: Client,
|
||||||
|
request: McpRequestMessage,
|
||||||
|
) {
|
||||||
|
return client.request(request, z.any());
|
||||||
|
}
|
65
app/mcp/logger.ts
Normal file
65
app/mcp/logger.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// ANSI color codes for terminal output
|
||||||
|
const colors = {
|
||||||
|
reset: "\x1b[0m",
|
||||||
|
bright: "\x1b[1m",
|
||||||
|
dim: "\x1b[2m",
|
||||||
|
green: "\x1b[32m",
|
||||||
|
yellow: "\x1b[33m",
|
||||||
|
red: "\x1b[31m",
|
||||||
|
blue: "\x1b[34m",
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MCPClientLogger {
|
||||||
|
private readonly prefix: string;
|
||||||
|
private readonly debugMode: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
prefix: string = "NextChat MCP Client",
|
||||||
|
debugMode: boolean = false,
|
||||||
|
) {
|
||||||
|
this.prefix = prefix;
|
||||||
|
this.debugMode = debugMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message: any) {
|
||||||
|
this.print(colors.blue, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
success(message: any) {
|
||||||
|
this.print(colors.green, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: any) {
|
||||||
|
this.print(colors.red, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message: any) {
|
||||||
|
this.print(colors.yellow, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(message: any) {
|
||||||
|
if (this.debugMode) {
|
||||||
|
this.print(colors.dim, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format message to string, if message is object, convert to JSON string
|
||||||
|
*/
|
||||||
|
private formatMessage(message: any): string {
|
||||||
|
return typeof message === "object"
|
||||||
|
? JSON.stringify(message, null, 2)
|
||||||
|
: message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Print formatted message to console
|
||||||
|
*/
|
||||||
|
private print(color: string, message: any) {
|
||||||
|
const formattedMessage = this.formatMessage(message);
|
||||||
|
const logMessage = `${color}${colors.bright}[${this.prefix}]${colors.reset} ${formattedMessage}`;
|
||||||
|
|
||||||
|
// 只使用 console.log,这样日志会显示在 Tauri 的终端中
|
||||||
|
console.log(logMessage);
|
||||||
|
}
|
||||||
|
}
|
180
app/mcp/types.ts
Normal file
180
app/mcp/types.ts
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
// ref: https://spec.modelcontextprotocol.io/specification/basic/messages/
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
|
||||||
|
export interface McpRequestMessage {
|
||||||
|
jsonrpc?: "2.0";
|
||||||
|
id?: string | number;
|
||||||
|
method: "tools/call" | string;
|
||||||
|
params?: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const McpRequestMessageSchema: z.ZodType<McpRequestMessage> = z.object({
|
||||||
|
jsonrpc: z.literal("2.0").optional(),
|
||||||
|
id: z.union([z.string(), z.number()]).optional(),
|
||||||
|
method: z.string(),
|
||||||
|
params: z.record(z.unknown()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface McpResponseMessage {
|
||||||
|
jsonrpc?: "2.0";
|
||||||
|
id?: string | number;
|
||||||
|
result?: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
error?: {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
data?: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const McpResponseMessageSchema: z.ZodType<McpResponseMessage> = z.object(
|
||||||
|
{
|
||||||
|
jsonrpc: z.literal("2.0").optional(),
|
||||||
|
id: z.union([z.string(), z.number()]).optional(),
|
||||||
|
result: z.record(z.unknown()).optional(),
|
||||||
|
error: z
|
||||||
|
.object({
|
||||||
|
code: z.number(),
|
||||||
|
message: z.string(),
|
||||||
|
data: z.unknown().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface McpNotifications {
|
||||||
|
jsonrpc?: "2.0";
|
||||||
|
method: string;
|
||||||
|
params?: {
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const McpNotificationsSchema: z.ZodType<McpNotifications> = z.object({
|
||||||
|
jsonrpc: z.literal("2.0").optional(),
|
||||||
|
method: z.string(),
|
||||||
|
params: z.record(z.unknown()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
////////////
|
||||||
|
// Next Chat
|
||||||
|
////////////
|
||||||
|
export interface ListToolsResponse {
|
||||||
|
tools: {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
inputSchema?: object;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type McpClientData =
|
||||||
|
| McpActiveClient
|
||||||
|
| McpErrorClient
|
||||||
|
| McpInitializingClient;
|
||||||
|
|
||||||
|
interface McpInitializingClient {
|
||||||
|
client: null;
|
||||||
|
tools: null;
|
||||||
|
errorMsg: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface McpActiveClient {
|
||||||
|
client: Client;
|
||||||
|
tools: ListToolsResponse;
|
||||||
|
errorMsg: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface McpErrorClient {
|
||||||
|
client: null;
|
||||||
|
tools: null;
|
||||||
|
errorMsg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务器状态类型
|
||||||
|
export type ServerStatus =
|
||||||
|
| "undefined"
|
||||||
|
| "active"
|
||||||
|
| "paused"
|
||||||
|
| "error"
|
||||||
|
| "initializing";
|
||||||
|
|
||||||
|
export interface ServerStatusResponse {
|
||||||
|
status: ServerStatus;
|
||||||
|
errorMsg: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MCP 服务器配置相关类型
|
||||||
|
export interface ServerConfig {
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
env?: Record<string, string>;
|
||||||
|
status?: "active" | "paused" | "error";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface McpConfigData {
|
||||||
|
// MCP Server 的配置
|
||||||
|
mcpServers: Record<string, ServerConfig>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_MCP_CONFIG: McpConfigData = {
|
||||||
|
mcpServers: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ArgsMapping {
|
||||||
|
// 参数映射的类型
|
||||||
|
type: "spread" | "single" | "env";
|
||||||
|
|
||||||
|
// 参数映射的位置
|
||||||
|
position?: number;
|
||||||
|
|
||||||
|
// 参数映射的 key
|
||||||
|
key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PresetServer {
|
||||||
|
// MCP Server 的唯一标识,作为最终配置文件 Json 的 key
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
// MCP Server 的显示名称
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
// MCP Server 的描述
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
// MCP Server 的仓库地址
|
||||||
|
repo: string;
|
||||||
|
|
||||||
|
// MCP Server 的标签
|
||||||
|
tags: string[];
|
||||||
|
|
||||||
|
// MCP Server 的命令
|
||||||
|
command: string;
|
||||||
|
|
||||||
|
// MCP Server 的参数
|
||||||
|
baseArgs: string[];
|
||||||
|
|
||||||
|
// MCP Server 是否需要配置
|
||||||
|
configurable: boolean;
|
||||||
|
|
||||||
|
// MCP Server 的配置 schema
|
||||||
|
configSchema?: {
|
||||||
|
properties: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
type: string;
|
||||||
|
description?: string;
|
||||||
|
required?: boolean;
|
||||||
|
minItems?: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// MCP Server 的参数映射
|
||||||
|
argsMapping?: Record<string, ArgsMapping>;
|
||||||
|
}
|
11
app/mcp/utils.ts
Normal file
11
app/mcp/utils.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export function isMcpJson(content: string) {
|
||||||
|
return content.match(/```json:mcp:([^{\s]+)([\s\S]*?)```/);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractMcpJson(content: string) {
|
||||||
|
const match = content.match(/```json:mcp:([^{\s]+)([\s\S]*?)```/);
|
||||||
|
if (match && match.length === 3) {
|
||||||
|
return { clientId: match[1], mcp: JSON.parse(match[2]) };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
@ -1,7 +1,5 @@
|
|||||||
import { Analytics } from "@vercel/analytics/react";
|
import { Analytics } from "@vercel/analytics/react";
|
||||||
|
|
||||||
import { Home } from "./components/home";
|
import { Home } from "./components/home";
|
||||||
|
|
||||||
import { getServerSideConfig } from "./config/server";
|
import { getServerSideConfig } from "./config/server";
|
||||||
|
|
||||||
const serverConfig = getServerSideConfig();
|
const serverConfig = getServerSideConfig();
|
||||||
|
@ -1,4 +1,9 @@
|
|||||||
import { getMessageTextContent, trimTopic } from "../utils";
|
import {
|
||||||
|
getMessageTextContent,
|
||||||
|
isDalle3,
|
||||||
|
safeLocalStorage,
|
||||||
|
trimTopic,
|
||||||
|
} from "../utils";
|
||||||
|
|
||||||
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
|
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
@ -14,14 +19,15 @@ import {
|
|||||||
DEFAULT_INPUT_TEMPLATE,
|
DEFAULT_INPUT_TEMPLATE,
|
||||||
DEFAULT_MODELS,
|
DEFAULT_MODELS,
|
||||||
DEFAULT_SYSTEM_TEMPLATE,
|
DEFAULT_SYSTEM_TEMPLATE,
|
||||||
|
GEMINI_SUMMARIZE_MODEL,
|
||||||
KnowledgeCutOffDate,
|
KnowledgeCutOffDate,
|
||||||
|
MCP_SYSTEM_TEMPLATE,
|
||||||
|
MCP_TOOLS_TEMPLATE,
|
||||||
|
ServiceProvider,
|
||||||
StoreKey,
|
StoreKey,
|
||||||
SUMMARIZE_MODEL,
|
SUMMARIZE_MODEL,
|
||||||
GEMINI_SUMMARIZE_MODEL,
|
|
||||||
ServiceProvider,
|
|
||||||
} from "../constant";
|
} from "../constant";
|
||||||
import Locale, { getLang } from "../locales";
|
import Locale, { getLang } from "../locales";
|
||||||
import { isDalle3, safeLocalStorage } from "../utils";
|
|
||||||
import { prettyObject } from "../utils/format";
|
import { prettyObject } from "../utils/format";
|
||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
import { estimateTokenLength } from "../utils/token";
|
import { estimateTokenLength } from "../utils/token";
|
||||||
@ -29,6 +35,8 @@ import { ModelConfig, ModelType, useAppConfig } from "./config";
|
|||||||
import { useAccessStore } from "./access";
|
import { useAccessStore } from "./access";
|
||||||
import { collectModelsWithDefaultModel } from "../utils/model";
|
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||||
import { createEmptyMask, Mask } from "./mask";
|
import { createEmptyMask, Mask } from "./mask";
|
||||||
|
import { executeMcpAction, getAllTools } from "../mcp/actions";
|
||||||
|
import { extractMcpJson, isMcpJson } from "../mcp/utils";
|
||||||
|
|
||||||
const localStorage = safeLocalStorage();
|
const localStorage = safeLocalStorage();
|
||||||
|
|
||||||
@ -53,6 +61,7 @@ export type ChatMessage = RequestMessage & {
|
|||||||
model?: ModelType;
|
model?: ModelType;
|
||||||
tools?: ChatMessageTool[];
|
tools?: ChatMessageTool[];
|
||||||
audio_url?: string;
|
audio_url?: string;
|
||||||
|
isMcpResponse?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createMessage(override: Partial<ChatMessage>): ChatMessage {
|
export function createMessage(override: Partial<ChatMessage>): ChatMessage {
|
||||||
@ -189,6 +198,27 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getMcpSystemPrompt(): Promise<string> {
|
||||||
|
const tools = await getAllTools();
|
||||||
|
|
||||||
|
let toolsStr = "";
|
||||||
|
|
||||||
|
tools.forEach((i) => {
|
||||||
|
// error client has no tools
|
||||||
|
if (!i.tools) return;
|
||||||
|
|
||||||
|
toolsStr += MCP_TOOLS_TEMPLATE.replace(
|
||||||
|
"{{ clientId }}",
|
||||||
|
i.clientId,
|
||||||
|
).replace(
|
||||||
|
"{{ tools }}",
|
||||||
|
i.tools.tools.map((p: object) => JSON.stringify(p, null, 2)).join("\n"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return MCP_SYSTEM_TEMPLATE.replace("{{ MCP_TOOLS }}", toolsStr);
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_CHAT_STATE = {
|
const DEFAULT_CHAT_STATE = {
|
||||||
sessions: [createEmptySession()],
|
sessions: [createEmptySession()],
|
||||||
currentSessionIndex: 0,
|
currentSessionIndex: 0,
|
||||||
@ -362,24 +392,30 @@ export const useChatStore = createPersistStore(
|
|||||||
session.messages = session.messages.concat();
|
session.messages = session.messages.concat();
|
||||||
session.lastUpdate = Date.now();
|
session.lastUpdate = Date.now();
|
||||||
});
|
});
|
||||||
|
|
||||||
get().updateStat(message, targetSession);
|
get().updateStat(message, targetSession);
|
||||||
|
|
||||||
|
get().checkMcpJson(message);
|
||||||
|
|
||||||
get().summarizeSession(false, targetSession);
|
get().summarizeSession(false, targetSession);
|
||||||
},
|
},
|
||||||
|
|
||||||
async onUserInput(content: string, attachImages?: string[]) {
|
async onUserInput(
|
||||||
|
content: string,
|
||||||
|
attachImages?: string[],
|
||||||
|
isMcpResponse?: boolean,
|
||||||
|
) {
|
||||||
const session = get().currentSession();
|
const session = get().currentSession();
|
||||||
const modelConfig = session.mask.modelConfig;
|
const modelConfig = session.mask.modelConfig;
|
||||||
|
|
||||||
const userContent = fillTemplateWith(content, modelConfig);
|
// MCP Response no need to fill template
|
||||||
console.log("[User Input] after template: ", userContent);
|
let mContent: string | MultimodalContent[] = isMcpResponse
|
||||||
|
? content
|
||||||
|
: fillTemplateWith(content, modelConfig);
|
||||||
|
|
||||||
let mContent: string | MultimodalContent[] = userContent;
|
if (!isMcpResponse && attachImages && attachImages.length > 0) {
|
||||||
|
|
||||||
if (attachImages && attachImages.length > 0) {
|
|
||||||
mContent = [
|
mContent = [
|
||||||
...(userContent
|
...(content ? [{ type: "text" as const, text: content }] : []),
|
||||||
? [{ type: "text" as const, text: userContent }]
|
|
||||||
: []),
|
|
||||||
...attachImages.map((url) => ({
|
...attachImages.map((url) => ({
|
||||||
type: "image_url" as const,
|
type: "image_url" as const,
|
||||||
image_url: { url },
|
image_url: { url },
|
||||||
@ -390,6 +426,7 @@ export const useChatStore = createPersistStore(
|
|||||||
let userMessage: ChatMessage = createMessage({
|
let userMessage: ChatMessage = createMessage({
|
||||||
role: "user",
|
role: "user",
|
||||||
content: mContent,
|
content: mContent,
|
||||||
|
isMcpResponse,
|
||||||
});
|
});
|
||||||
|
|
||||||
const botMessage: ChatMessage = createMessage({
|
const botMessage: ChatMessage = createMessage({
|
||||||
@ -399,7 +436,7 @@ export const useChatStore = createPersistStore(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// get recent messages
|
// get recent messages
|
||||||
const recentMessages = get().getMessagesWithMemory();
|
const recentMessages = await get().getMessagesWithMemory();
|
||||||
const sendMessages = recentMessages.concat(userMessage);
|
const sendMessages = recentMessages.concat(userMessage);
|
||||||
const messageIndex = session.messages.length + 1;
|
const messageIndex = session.messages.length + 1;
|
||||||
|
|
||||||
@ -429,7 +466,7 @@ export const useChatStore = createPersistStore(
|
|||||||
session.messages = session.messages.concat();
|
session.messages = session.messages.concat();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onFinish(message) {
|
async onFinish(message) {
|
||||||
botMessage.streaming = false;
|
botMessage.streaming = false;
|
||||||
if (message) {
|
if (message) {
|
||||||
botMessage.content = message;
|
botMessage.content = message;
|
||||||
@ -498,7 +535,7 @@ export const useChatStore = createPersistStore(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getMessagesWithMemory() {
|
async getMessagesWithMemory() {
|
||||||
const session = get().currentSession();
|
const session = get().currentSession();
|
||||||
const modelConfig = session.mask.modelConfig;
|
const modelConfig = session.mask.modelConfig;
|
||||||
const clearContextIndex = session.clearContextIndex ?? 0;
|
const clearContextIndex = session.clearContextIndex ?? 0;
|
||||||
@ -514,18 +551,26 @@ export const useChatStore = createPersistStore(
|
|||||||
(session.mask.modelConfig.model.startsWith("gpt-") ||
|
(session.mask.modelConfig.model.startsWith("gpt-") ||
|
||||||
session.mask.modelConfig.model.startsWith("chatgpt-"));
|
session.mask.modelConfig.model.startsWith("chatgpt-"));
|
||||||
|
|
||||||
|
const mcpSystemPrompt = await getMcpSystemPrompt();
|
||||||
|
|
||||||
var systemPrompts: ChatMessage[] = [];
|
var systemPrompts: ChatMessage[] = [];
|
||||||
systemPrompts = shouldInjectSystemPrompts
|
systemPrompts = shouldInjectSystemPrompts
|
||||||
? [
|
? [
|
||||||
createMessage({
|
createMessage({
|
||||||
role: "system",
|
role: "system",
|
||||||
content: fillTemplateWith("", {
|
content:
|
||||||
|
fillTemplateWith("", {
|
||||||
...modelConfig,
|
...modelConfig,
|
||||||
template: DEFAULT_SYSTEM_TEMPLATE,
|
template: DEFAULT_SYSTEM_TEMPLATE,
|
||||||
}),
|
}) + mcpSystemPrompt,
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
: [];
|
: [
|
||||||
|
createMessage({
|
||||||
|
role: "system",
|
||||||
|
content: mcpSystemPrompt,
|
||||||
|
}),
|
||||||
|
];
|
||||||
if (shouldInjectSystemPrompts) {
|
if (shouldInjectSystemPrompts) {
|
||||||
console.log(
|
console.log(
|
||||||
"[Global System Prompt] ",
|
"[Global System Prompt] ",
|
||||||
@ -768,6 +813,36 @@ export const useChatStore = createPersistStore(
|
|||||||
lastInput,
|
lastInput,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** check if the message contains MCP JSON and execute the MCP action */
|
||||||
|
checkMcpJson(message: ChatMessage) {
|
||||||
|
const content = getMessageTextContent(message);
|
||||||
|
if (isMcpJson(content)) {
|
||||||
|
try {
|
||||||
|
const mcpRequest = extractMcpJson(content);
|
||||||
|
if (mcpRequest) {
|
||||||
|
console.debug("[MCP Request]", mcpRequest);
|
||||||
|
|
||||||
|
executeMcpAction(mcpRequest.clientId, mcpRequest.mcp)
|
||||||
|
.then((result) => {
|
||||||
|
console.log("[MCP Response]", result);
|
||||||
|
const mcpResponse =
|
||||||
|
typeof result === "object"
|
||||||
|
? JSON.stringify(result)
|
||||||
|
: String(result);
|
||||||
|
get().onUserInput(
|
||||||
|
`\`\`\`json:mcp-response:${mcpRequest.clientId}\n${mcpResponse}\n\`\`\``,
|
||||||
|
[],
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => showToast("MCP execution failed", error));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Check MCP JSON]", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return methods;
|
return methods;
|
||||||
|
@ -71,8 +71,10 @@ if (mode !== "export") {
|
|||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
// https://{resource_name}.openai.azure.com/openai/deployments/{deploy_name}/chat/completions
|
// https://{resource_name}.openai.azure.com/openai/deployments/{deploy_name}/chat/completions
|
||||||
source: "/api/proxy/azure/:resource_name/deployments/:deploy_name/:path*",
|
source:
|
||||||
destination: "https://:resource_name.openai.azure.com/openai/deployments/:deploy_name/:path*",
|
"/api/proxy/azure/:resource_name/deployments/:deploy_name/:path*",
|
||||||
|
destination:
|
||||||
|
"https://:resource_name.openai.azure.com/openai/deployments/:deploy_name/:path*",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: "/api/proxy/google/:path*",
|
source: "/api/proxy/google/:path*",
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
"export:dev": "concurrently -r \"yarn mask:watch\" \"cross-env BUILD_MODE=export BUILD_APP=1 next dev\"",
|
"export:dev": "concurrently -r \"yarn mask:watch\" \"cross-env BUILD_MODE=export BUILD_APP=1 next dev\"",
|
||||||
"app:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"",
|
"app:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"",
|
||||||
"app:build": "yarn mask && yarn tauri build",
|
"app:build": "yarn mask && yarn tauri build",
|
||||||
|
"app:clear": "yarn tauri dev",
|
||||||
"prompts": "node ./scripts/fetch-prompts.mjs",
|
"prompts": "node ./scripts/fetch-prompts.mjs",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev",
|
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev",
|
||||||
@ -22,6 +23,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortaine/fetch-event-source": "^3.0.6",
|
"@fortaine/fetch-event-source": "^3.0.6",
|
||||||
"@hello-pangea/dnd": "^16.5.0",
|
"@hello-pangea/dnd": "^16.5.0",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
"@next/third-parties": "^14.1.0",
|
"@next/third-parties": "^14.1.0",
|
||||||
"@svgr/webpack": "^6.5.1",
|
"@svgr/webpack": "^6.5.1",
|
||||||
"@vercel/analytics": "^0.1.11",
|
"@vercel/analytics": "^0.1.11",
|
||||||
@ -49,14 +51,15 @@
|
|||||||
"remark-breaks": "^3.0.2",
|
"remark-breaks": "^3.0.2",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
|
"rt-client": "https://github.com/Azure-Samples/aoai-realtime-audio-sdk/releases/download/js/v0.5.0/rt-client-0.5.0.tgz",
|
||||||
"sass": "^1.59.2",
|
"sass": "^1.59.2",
|
||||||
"spark-md5": "^3.0.2",
|
"spark-md5": "^3.0.2",
|
||||||
"use-debounce": "^9.0.4",
|
"use-debounce": "^9.0.4",
|
||||||
"zustand": "^4.3.8",
|
"zod": "^3.24.1",
|
||||||
"rt-client": "https://github.com/Azure-Samples/aoai-realtime-audio-sdk/releases/download/js/v0.5.0/rt-client-0.5.0.tgz"
|
"zustand": "^4.3.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/api": "^1.6.0",
|
"@tauri-apps/api": "^2.1.1",
|
||||||
"@tauri-apps/cli": "1.5.11",
|
"@tauri-apps/cli": "1.5.11",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2015",
|
"target": "ES2022",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@ -23,6 +23,6 @@
|
|||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/calcTextareaHeight.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
95
yarn.lock
95
yarn.lock
@ -2251,6 +2251,15 @@
|
|||||||
"@jridgewell/resolve-uri" "3.1.0"
|
"@jridgewell/resolve-uri" "3.1.0"
|
||||||
"@jridgewell/sourcemap-codec" "1.4.14"
|
"@jridgewell/sourcemap-codec" "1.4.14"
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk@^1.0.4":
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.0.4.tgz#34ad1edd3db7dd7154e782312dfb29d2d0c11d21"
|
||||||
|
integrity sha512-C+jw1lF6HSGzs7EZpzHbXfzz9rj9him4BaoumlTciW/IDDgIpweF/qiCWKlP02QKg5PPcgY6xY2WCt5y2tpYow==
|
||||||
|
dependencies:
|
||||||
|
content-type "^1.0.5"
|
||||||
|
raw-body "^3.0.0"
|
||||||
|
zod "^3.23.8"
|
||||||
|
|
||||||
"@next/env@14.1.1":
|
"@next/env@14.1.1":
|
||||||
version "14.1.1"
|
version "14.1.1"
|
||||||
resolved "https://registry.npmjs.org/@next/env/-/env-14.1.1.tgz"
|
resolved "https://registry.npmjs.org/@next/env/-/env-14.1.1.tgz"
|
||||||
@ -2921,10 +2930,12 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib "^2.4.0"
|
tslib "^2.4.0"
|
||||||
|
|
||||||
"@tauri-apps/api@^1.6.0":
|
|
||||||
version "1.6.0"
|
"@tauri-apps/api@^2.1.1":
|
||||||
resolved "https://registry.npmjs.org/@tauri-apps/api/-/api-1.6.0.tgz"
|
version "2.1.1"
|
||||||
integrity sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg==
|
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.1.1.tgz#77d4ddb683d31072de4e6a47c8613d9db011652b"
|
||||||
|
integrity sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A==
|
||||||
|
|
||||||
|
|
||||||
"@tauri-apps/cli-darwin-arm64@1.5.11":
|
"@tauri-apps/cli-darwin-arm64@1.5.11":
|
||||||
version "1.5.11"
|
version "1.5.11"
|
||||||
@ -3942,6 +3953,11 @@ busboy@1.6.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
streamsearch "^1.1.0"
|
streamsearch "^1.1.0"
|
||||||
|
|
||||||
|
bytes@3.1.2:
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
||||||
|
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
||||||
|
|
||||||
call-bind@^1.0.0, call-bind@^1.0.2:
|
call-bind@^1.0.0, call-bind@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz"
|
resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz"
|
||||||
@ -3965,15 +3981,10 @@ camelcase@^6.2.0:
|
|||||||
resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz"
|
resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz"
|
||||||
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001579:
|
caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001503, caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001646:
|
||||||
version "1.0.30001617"
|
version "1.0.30001692"
|
||||||
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001617.tgz"
|
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz"
|
||||||
integrity sha512-mLyjzNI9I+Pix8zwcrpxEbGlfqOkF9kM3ptzmKNw5tizSyYwMe+nGLTqMK9cO+0E+Bh6TsBxNAaHWEM8xwSsmA==
|
integrity sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001646:
|
|
||||||
version "1.0.30001649"
|
|
||||||
resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001649.tgz"
|
|
||||||
integrity sha512-fJegqZZ0ZX8HOWr6rcafGr72+xcgJKI9oWfDW5DrD7ExUtgZC7a7R7ZYmZqplh7XDocFdGeIFn7roAxhOeYrPQ==
|
|
||||||
|
|
||||||
ccount@^2.0.0:
|
ccount@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
@ -4188,6 +4199,11 @@ concurrently@^8.2.2:
|
|||||||
tree-kill "^1.2.2"
|
tree-kill "^1.2.2"
|
||||||
yargs "^17.7.2"
|
yargs "^17.7.2"
|
||||||
|
|
||||||
|
content-type@^1.0.5:
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
|
||||||
|
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
|
||||||
|
|
||||||
convert-source-map@^1.7.0:
|
convert-source-map@^1.7.0:
|
||||||
version "1.9.0"
|
version "1.9.0"
|
||||||
resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz"
|
resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz"
|
||||||
@ -4757,6 +4773,11 @@ delayed-stream@~1.0.0:
|
|||||||
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
|
||||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||||
|
|
||||||
|
depd@2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
|
||||||
|
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
|
||||||
|
|
||||||
dequal@^2.0.0, dequal@^2.0.3:
|
dequal@^2.0.0, dequal@^2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz"
|
resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz"
|
||||||
@ -5922,6 +5943,17 @@ html-to-image@^1.11.11:
|
|||||||
resolved "https://registry.npmmirror.com/html-to-image/-/html-to-image-1.11.11.tgz"
|
resolved "https://registry.npmmirror.com/html-to-image/-/html-to-image-1.11.11.tgz"
|
||||||
integrity sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==
|
integrity sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==
|
||||||
|
|
||||||
|
http-errors@2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
|
||||||
|
integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
|
||||||
|
dependencies:
|
||||||
|
depd "2.0.0"
|
||||||
|
inherits "2.0.4"
|
||||||
|
setprototypeof "1.2.0"
|
||||||
|
statuses "2.0.1"
|
||||||
|
toidentifier "1.0.1"
|
||||||
|
|
||||||
http-proxy-agent@^5.0.0:
|
http-proxy-agent@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.0.0"
|
||||||
resolved "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz"
|
resolved "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz"
|
||||||
@ -6010,7 +6042,7 @@ inflight@^1.0.4:
|
|||||||
once "^1.3.0"
|
once "^1.3.0"
|
||||||
wrappy "1"
|
wrappy "1"
|
||||||
|
|
||||||
inherits@2:
|
inherits@2, inherits@2.0.4:
|
||||||
version "2.0.4"
|
version "2.0.4"
|
||||||
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
|
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
|
||||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
@ -8053,6 +8085,16 @@ randombytes@^2.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "^5.1.0"
|
safe-buffer "^5.1.0"
|
||||||
|
|
||||||
|
raw-body@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f"
|
||||||
|
integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==
|
||||||
|
dependencies:
|
||||||
|
bytes "3.1.2"
|
||||||
|
http-errors "2.0.0"
|
||||||
|
iconv-lite "0.6.3"
|
||||||
|
unpipe "1.0.0"
|
||||||
|
|
||||||
react-dom@^18.2.0:
|
react-dom@^18.2.0:
|
||||||
version "18.2.0"
|
version "18.2.0"
|
||||||
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz"
|
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz"
|
||||||
@ -8484,6 +8526,11 @@ serialize-javascript@^6.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
randombytes "^2.1.0"
|
randombytes "^2.1.0"
|
||||||
|
|
||||||
|
setprototypeof@1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
||||||
|
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
|
||||||
|
|
||||||
shebang-command@^2.0.0:
|
shebang-command@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz"
|
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz"
|
||||||
@ -8614,6 +8661,11 @@ stack-utils@^2.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
escape-string-regexp "^2.0.0"
|
escape-string-regexp "^2.0.0"
|
||||||
|
|
||||||
|
statuses@2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
||||||
|
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
|
||||||
|
|
||||||
stop-iteration-iterator@^1.0.0:
|
stop-iteration-iterator@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz"
|
resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz"
|
||||||
@ -8897,6 +8949,11 @@ to-regex-range@^5.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-number "^7.0.0"
|
is-number "^7.0.0"
|
||||||
|
|
||||||
|
toidentifier@1.0.1:
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||||
|
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||||
|
|
||||||
tough-cookie@^4.1.2:
|
tough-cookie@^4.1.2:
|
||||||
version "4.1.4"
|
version "4.1.4"
|
||||||
resolved "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-4.1.4.tgz"
|
resolved "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-4.1.4.tgz"
|
||||||
@ -9139,6 +9196,11 @@ universalify@^0.2.0:
|
|||||||
resolved "https://registry.npmmirror.com/universalify/-/universalify-0.2.0.tgz"
|
resolved "https://registry.npmmirror.com/universalify/-/universalify-0.2.0.tgz"
|
||||||
integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==
|
integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==
|
||||||
|
|
||||||
|
unpipe@1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||||
|
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
|
||||||
|
|
||||||
update-browserslist-db@^1.0.10:
|
update-browserslist-db@^1.0.10:
|
||||||
version "1.0.10"
|
version "1.0.10"
|
||||||
resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz"
|
resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz"
|
||||||
@ -9497,6 +9559,11 @@ yocto-queue@^0.1.0:
|
|||||||
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
|
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
|
zod@^3.23.8, zod@^3.24.1:
|
||||||
|
version "3.24.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.1.tgz#27445c912738c8ad1e9de1bea0359fa44d9d35ee"
|
||||||
|
integrity sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==
|
||||||
|
|
||||||
zustand@^4.3.8:
|
zustand@^4.3.8:
|
||||||
version "4.3.8"
|
version "4.3.8"
|
||||||
resolved "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz"
|
resolved "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user