From d298174439a14ae6fa97b43b7fb1af68b7201b1d Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Thu, 19 Mar 2026 15:43:55 +0800 Subject: [PATCH] bug fix --- docs/UPGRADE_v0.9.6.md | 184 +++++++++++++++++++ frontend/src/pages/Document/DocumentPage.jsx | 46 ++++- 2 files changed, 223 insertions(+), 7 deletions(-) create mode 100644 docs/UPGRADE_v0.9.6.md diff --git a/docs/UPGRADE_v0.9.6.md b/docs/UPGRADE_v0.9.6.md new file mode 100644 index 0000000..503ba78 --- /dev/null +++ b/docs/UPGRADE_v0.9.6.md @@ -0,0 +1,184 @@ +# NexDocs v0.9.6 升级日志 + +## 版本信息 + +- 版本号:`v0.9.6` +- 更新时间:`2026-03-11` + + +## 本次升级摘要 + +`v0.9.6` 主要完成了以下升级: + +1. 新增后端内集成的 MCP Server 能力 +2. 新增用户级 MCP 凭证管理 +3. 打通通过 Nginx 统一入口访问 MCP +4. 调整部署运行环境到 Python 3.12 +5. 将 MySQL、Redis 数据目录改为项目根目录 `storage/` 持久化 +6. 优化个人中心与文档页面部分交互体验 + + +## 功能升级 + +### 1. MCP Server 内集成 + +系统已支持直接在 backend 中提供 MCP `streamableHttp` 服务,不再依赖项目根目录下的独立 `mcp_server/` 进程。 + +当前 MCP 入口: + +```text +/mcp/ +``` + +当前支持的工具: + +1. `list_created_projects`:列出当前用户的项目 +2. `get_project_tree`:列出项目文件树结构 +3. `get_file`:获取指定文件 +4. `create_file`:在项目中创建新文件 +5. `update_file`:修改指定文件 +6. `delete_file`:删除指定文件 + + +### 2. MCP 认证与用户凭证管理 + +新增基于 `X-Bot-Id` 和 `X-Bot-Secret` 的 MCP 认证模型。 + +能力包括: + +- 每个用户可拥有自己的 MCP 凭证 +- 通过数据库表 `mcp_bots` 维护 bot 与用户的映射 +- 支持在个人中心查看和重新生成凭证 +- 服务端自动按凭证映射到对应 NexDocs 用户身份执行工具 + +相关接口: + +- `GET /api/v1/auth/mcp-credentials` +- `POST /api/v1/auth/mcp-credentials/rotate-secret` + + +### 3. Nginx 统一入口支持 MCP + +前端 Nginx 已新增 `/mcp/` 反向代理配置。 + +现在在只暴露一个公网入口端口的部署模式下,可以通过统一入口访问: + +```text +http(s):///mcp/ +``` + +不再要求调用端直连 backend 容器端口。 + + +## 前端与交互优化 + +### 1. 个人中心布局调整 + +个人中心由上下结构调整为左右结构: + +- 左侧为纵向导航 +- 右侧为内容区域 +- 移动端会自动回退为上下布局 + + +### 2. 文档浏览/编辑切换优化 + +本轮已对项目文档页做过一组交互统一: + +- 浏览/编辑模式切换样式统一 +- 页面头部高度与操作区对齐 +- 切换时保留当前文件上下文 +- 登录跳转回原目标页的逻辑补齐 + + +### 3. 认证异常提示优化 + +对未登录和 token 失效场景做了重复错误提示抑制: + +- 避免并发请求弹出多条重复 Toast +- 统一 401 处理与跳转逻辑 + + +## 部署与运行环境变更 + +### 1. Python 运行环境升级 + +backend 运行环境已调整为: + +```text +Python 3.12 +``` + +原因: + +- MCP Python SDK 需要 Python 3.10+ +- 当前版本已按 Python 3.12 完成调试与验证 + + +### 2. Docker 构建链调整 + +backend Docker 镜像构建已做以下处理: + +- 基础镜像切换为 `python:3.12-slim` +- 增加 pip 构建工具升级 +- pip 安装支持国内源失败后回退官方源 +- Nginx 已补充 `/mcp/` 代理配置 + + +### 3. 数据持久化目录调整 + +本次升级将 MySQL、Redis 的数据目录改为挂载到项目根目录下的 `storage/`: + +```text +storage/ +├── mysql/ +├── redis/ +├── projects/ +└── temp/ +``` + +当前挂载关系: + +- `storage/mysql -> /var/lib/mysql` +- `storage/redis -> /data` +- `storage -> /data/nex_docus_store` + +这样做的目的: + +- 宿主机可直接看到数据库与缓存数据目录 +- 便于整体备份 +- 避免数据只留在 Docker named volume 中 + + +## 升级迁移注意事项 + +### 1. MCP 调用地址调整 + +如果原先通过 backend 端口直连 MCP,可以继续使用。 + +如果当前部署是通过前端 Nginx 暴露统一入口,推荐改为: + +```text +http(s):///mcp/ +``` + +调用头保持不变: + +- `X-Bot-Id` +- `X-Bot-Secret` + + +### 部署与文档 + +- [docker-compose.yml](/Users/jiliu/工作/projects/NexDocus/docker-compose.yml) +- [MCP_HTTP_INTEGRATION.md](/Users/jiliu/工作/projects/NexDocus/docs/MCP_HTTP_INTEGRATION.md) + + +## 升级建议 + +建议升级到 `v0.9.6` 后按以下顺序验证: + +1. 验证 backend `/health` 是否正常 +2. 验证统一入口 `/api/` 与 `/mcp/` 是否可访问 +3. 验证 MCP client 是否能正常完成 `initialize` +4. 验证个人中心的 MCP 凭证展示与轮换功能 diff --git a/frontend/src/pages/Document/DocumentPage.jsx b/frontend/src/pages/Document/DocumentPage.jsx index 5cd27be..2c9dad2 100644 --- a/frontend/src/pages/Document/DocumentPage.jsx +++ b/frontend/src/pages/Document/DocumentPage.jsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef, useMemo } from 'react' import { useParams, useNavigate, useSearchParams } from 'react-router-dom' -import { Layout, Menu, Spin, FloatButton, Button, Tooltip, message, Anchor, Modal, Input, Space, Dropdown, Empty } from 'antd' +import { Layout, Menu, Spin, FloatButton, Button, Tooltip, message, Anchor, Modal, Input, Space, Dropdown, Empty, Switch } from 'antd' import { VerticalAlignTopOutlined, ShareAltOutlined, MenuFoldOutlined, MenuUnfoldOutlined, FileTextOutlined, FolderOutlined, FilePdfOutlined, CopyOutlined, LockOutlined, CloudDownloadOutlined, CloudUploadOutlined, DownOutlined, SearchOutlined, CloseOutlined, MenuOutlined } from '@ant-design/icons' import ReactMarkdown from 'react-markdown' import remarkGfm from 'remark-gfm' @@ -41,6 +41,7 @@ function DocumentPage() { const [searchParams, setSearchParams] = useSearchParams() const [fileTree, setFileTree] = useState([]) const [selectedFile, setSelectedFile] = useState('') + const [selectedNodeKey, setSelectedNodeKey] = useState('') const [markdownContent, setMarkdownContent] = useState('') const [loading, setLoading] = useState(false) const [openKeys, setOpenKeys] = useState([]) @@ -122,6 +123,7 @@ function DocumentPage() { if (fileParam) { if (fileParam !== selectedFile) { setSelectedFile(fileParam) + setSelectedNodeKey(fileParam) // 展开父目录 const parts = fileParam.split('/') @@ -156,6 +158,7 @@ function DocumentPage() { const readmeNode = findReadme(fileTree) if (readmeNode) { setSelectedFile(readmeNode.key) + setSelectedNodeKey(readmeNode.key) updateFileParam(readmeNode.key) loadMarkdown(readmeNode.key) } @@ -258,6 +261,21 @@ function DocumentPage() { return null } + const findNodeByKey = (nodes, key) => { + for (const node of nodes) { + if (node.key === key) { + return node + } + if (node.children?.length) { + const found = findNodeByKey(node.children, key) + if (found) { + return found + } + } + } + return null + } + // 转换文件树为菜单项 const convertTreeToMenuItems = (nodes) => { return nodes.map((node) => { @@ -270,6 +288,7 @@ function DocumentPage() { key: node.key, label: node.title, icon: , + onTitleClick: () => setSelectedNodeKey(node.key), children: node.children ? convertTreeToMenuItems(node.children) : [], } } else if (node.title && node.title.endsWith('.md')) { @@ -342,6 +361,7 @@ function DocumentPage() { // 处理菜单点击 const handleMenuClick = ({ key }) => { setSelectedFile(key) + setSelectedNodeKey(key) updateFileParam(key) // 检查是否是PDF文件 @@ -443,6 +463,7 @@ function DocumentPage() { // 选中文件并加载 setSelectedFile(targetPath) + setSelectedNodeKey(targetPath) updateFileParam(targetPath) if (isPdf) { @@ -635,6 +656,12 @@ function DocumentPage() { // 打开分享设置 const handleShare = async () => { + const selectedNode = selectedNodeKey ? findNodeByKey(fileTree, selectedNodeKey) : null + if (selectedNode && !selectedNode.isLeaf) { + Toast.warning('提示', '当前选中的是文件夹,不能直接分享,请选择具体文件后再试') + return + } + try { const res = await getProjectShareInfo(projectId) setShareInfo(res.data) @@ -650,9 +677,14 @@ function DocumentPage() { // 复制分享链接 const handleCopyLink = async () => { if (!shareInfo) return + + const shareTargetFile = selectedNodeKey && findNodeByKey(fileTree, selectedNodeKey)?.isLeaf + ? selectedNodeKey + : '' + let fullUrl = `${window.location.origin}${shareInfo.share_url}` - if (selectedFile) { - fullUrl += `?file=${encodeURIComponent(selectedFile)}` + if (shareTargetFile) { + fullUrl += `?file=${encodeURIComponent(shareTargetFile)}` } try { @@ -867,7 +899,7 @@ function DocumentPage() { {filteredTreeData.length > 0 ? (