作者介绍:刘星辰,TRAE 技术专家
项目背景
Cloudreve 简介
项目地址:
https://github.com/cloudreve/cloudreve
Cloudreve 是一个功能完善的自托管文件管理与分享系统 ,支持将文件存储到多种云服务(如本地存储、S3、OneDrive、阿里云 OSS、腾讯 COS 等)。该项目提供了完整的网页版网盘体验 ,包括文件上传下载、在线预览、分享链接、多用户与分组管理,以及 WebDAV 接入等功能。
项目亮点包括:
-
支持多种云存储服务的统一管理
-
前后端一体化的自部署方案(Go + React 技术栈)
-
文件加密、压缩、断点续传、在线预览等功能齐全
-
适用于个人网盘、团队文件协作、轻量级私有云等场景
一句话总结: 🌩️ Cloudreve 是一个开源的、可自己搭建的多云文件管理平台,让你拥有属于自己的「网盘系统」。
项目理解
面对一个陌生的开源项目,如何快速上手?可以直接把问题交给 SOLO Coder,让它把复杂的项目拆解为可理解的模块。
根据任务拆解,Coder 写了 6 篇技术报告:
首先需要理解项目的宏观结构。Coder 绘制了整体架构分析文档,涵盖技术栈、部署方式和数据流程。
上下滑动查看完整内容
以下是文档完整内容:
01-project-overview.md - 项目整体架构概览
- 技术栈分析(React + Go + 数据库)
- 部署架构图和数据流程
- 核心功能模块介绍
上下滑动查看完整内容
# Cloudreve 项目整体架构分析
## 项目概述
Cloudreve 是一个功能完整的自托管网盘系统,支持多种存储后端,提供完整的文件管理、分享、用户管理等功能。项目采用现代化的前后端分离架构,具有良好的可扩展性和维护性。
## 技术栈总览
### 前端技术栈
- **框架**: React 18 + TypeScript
- **UI 库**: Material-UI (MUI) v6
- **状态管理**: Redux Toolkit
- **路由**: React Router v6
- **构建工具**: Vite
- **HTTP 客户端**: Axios
- **拖拽**: React DnD
- **国际化**: i18next
### 后端技术栈
- **语言**: Go 1.23
- **Web 框架**: Gin
- **ORM**: Ent (Facebook 开源的 Go ORM)
- **数据库**: 支持 MySQL、PostgreSQL、SQLite
- **认证**: JWT + Session
- **文件存储**: 支持本地、阿里云 OSS、AWS S3、腾讯云 COS 等
- **任务队列**: 内置任务调度系统
## 项目结构
```mermaid
graph TB
subgraph "Cloudreve 项目结构"
A[根目录] --> B[Cloudreve/]
A --> C[assets/]
A --> D[.trae/]
B --> B1[main.go - 入口文件]
B --> B2[routers/ - 路由层]
B --> B3[service/ - 业务逻辑层]
B --> B4[ent/ - 数据模型层]
B --> B5[pkg/ - 工具包]
B --> B6[middleware/ - 中间件]
B --> B7[inventory/ - 库存管理]
B --> B8[application/ - 应用配置]
C --> C1[src/ - 前端源码]
C --> C2[assets/ - 静态资源]
C --> C3[locales/ - 国际化文件]
C1 --> C11[component/ - React 组件]
C1 --> C12[redux/ - 状态管理]
C1 --> C13[api/ - API 接口]
C1 --> C14[router/ - 前端路由]
C1 --> C15[session/ - 会话管理]
end
核心架构设计
1. 分层架构
graph TB
subgraph "后端分层架构"
A[路由层 - routers/] --> B[控制器层 - controllers/]
B --> C[服务层 - service/]
C --> D[数据访问层 - ent/]
D --> E[数据库]
F[中间件层 - middleware/] --> B
G[工具包 - pkg/] --> C
end
2. 前端组件架构
graph TB
subgraph "前端组件架构"
A[App.tsx - 根组件] --> B[Frame/ - 框架组件]
A --> C[Pages/ - 页面组件]
B --> B1[NavBarFrame - 导航框架]
B --> B2[HeadlessFrame - 无头框架]
B1 --> B11[TopAppBar - 顶部栏]
B1 --> B12[AppDrawer - 侧边栏]
B1 --> B13[AppMain - 主内容区]
C --> C1[FileManager/ - 文件管理器]
C --> C2[Login/ - 登录页面]
C --> C3[Admin/ - 管理页面]
C1 --> C11[Explorer/ - 文件浏览器]
C1 --> C12[Uploader/ - 文件上传器]
C1 --> C13[Viewers/ - 文件查看器]
end
部署架构
Cloudreve 支持两种部署模式:
1. 单机模式 (Master Mode)
graph LR
A[用户] --> B[Nginx/反向代理]
B --> C[Cloudreve 主节点]
C --> D[数据库]
C --> E[文件存储]
2. 集群模式 (Master-Slave Mode)
graph TB
A[用户] --> B[负载均衡器]
B --> C[Cloudreve 主节点]
B --> D[Cloudreve 从节点 1]
B --> E[Cloudreve 从节点 2]
C --> F[共享数据库]
D --> G[本地存储 1]
E --> H[本地存储 2]
C -.-> D
C -.-> E
核心功能模块
1. 用户管理系统
- 用户注册、登录、权限管理
- 用户组管理
- 存储配额管理
- 2FA 双因子认证
- Passkey 支持
2. 文件管理系统
- 文件上传、下载、预览
- 文件夹管理
- 文件分享(公开/私密)
- 文件版本控制
- 批量操作
3. 存储系统
- 多存储后端支持
- 存储策略管理
- 文件加密
- 缩略图生成
- 媒体元数据提取
4. 任务系统
- 异步任务处理
- 离线下载
- 文件压缩/解压
- 定时任务
5. 聊天系统 (已实现)
- OpenRouter API 集成
- 流式对话支持
- 多模态文件处理
- 文件上下文分析
数据流架构
sequenceDiagram
participant U as 用户
participant F as 前端 (React)
participant R as 路由层 (Gin)
participant S as 服务层
participant D as 数据层 (Ent)
participant DB as 数据库
U->>F: 用户操作
F->>R: HTTP 请求
R->>R: 中间件处理 (认证/授权)
R->>S: 调用业务服务
S->>D: 数据操作
D->>DB: SQL 查询
DB-->>D: 返回数据
D-->>S: 返回实体
S-->>R: 返回结果
R-->>F: JSON 响应
F-->>U: 更新界面
关键设计特点
1. 模块化设计
- 前后端完全分离
- 组件化开发
- 插件式存储后端
- 中间件架构
2. 可扩展性
- 支持集群部署
- 水平扩展能力
- 插件系统
- API 版本管理
3. 安全性
- JWT 认证
- RBAC 权限控制
- 文件加密
- CSRF 保护
- 输入验证
4. 性能优化
- 缓存机制
- 异步任务处理
- 文件分片上传
- CDN 支持
- 数据库连接池
开发环境要求
前端开发
- Node.js 18+
- npm 或 yarn
- 现代浏览器
后端开发
- Go 1.23+
- 数据库 (MySQL/PostgreSQL/SQLite)
- Redis (可选,用于缓存)
项目特色功能
- 多存储后端: 支持本地、云存储等多种存储方式
- WebDAV 支持: 可通过 WebDAV 协议访问文件
- Office 在线编辑: 集成 WOPI 协议支持 Office 文档在线编辑
- 媒体处理: 支持图片、视频、音频的预览和元数据提取
- 离线下载: 支持 Aria2 离线下载
- 文件分享: 灵活的文件分享机制
- AI 聊天: 集成 OpenRouter API 的智能对话功能
总结
Cloudreve 是一个架构清晰、功能完整的现代化网盘系统。其前后端分离的设计、模块化的架构、以及丰富的功能特性,为开发者提供了良好的扩展基础。特别是已经实现的聊天功能,为你要开发的侧边栏对话功能提供了很好的参考和基础。
下一步建议深入了解前端组件架构和后端服务层的具体实现,这将有助于你更好地理解如何在现有架构基础上添加新功能。
**02-frontend-architecture.md - 前端架构详解**
* React + TypeScript + Material-UI 技术栈
* 组件层次结构和状态管理
* 路由系统和性能优化策略
**上下滑动查看完整内容**
前端技术栈详解
核心技术栈
- React 18: 使用最新的 React 特性,包括 Hooks、Suspense、Concurrent Features
- TypeScript: 提供类型安全和更好的开发体验
- Material-UI (MUI) v6: Google Material Design 设计语言的 React 实现
- Redux Toolkit: 现代化的 Redux 状态管理解决方案
- React Router v6: 声明式路由管理
- Vite: 快速的构建工具和开发服务器
开发工具链
- ESLint: 代码质量检查
- Prettier: 代码格式化
- Husky: Git hooks 管理
- TypeScript: 静态类型检查
项目结构详解
assets/src/
├── component/ # React 组件
│ ├── Common/ # 通用组件
│ ├── Frame/ # 框架组件
│ ├── FileManager/ # 文件管理器组件
│ ├── Pages/ # 页面组件
│ ├── Admin/ # 管理后台组件
│ ├── Viewers/ # 文件查看器组件
│ ├── Uploader/ # 文件上传组件
│ ├── Icons/ # 图标组件
│ └── Dialogs/ # 对话框组件
├── redux/ # 状态管理
│ ├── hooks.ts # Redux hooks
│ ├── store.ts # Store 配置
│ └── slices/ # Redux slices
├── api/ # API 接口
│ ├── api.ts # API 函数
│ ├── request.ts # 请求封装
│ └── types.ts # 类型定义
├── router/ # 路由配置
├── session/ # 会话管理
├── util/ # 工具函数
└── App.tsx # 根组件
组件架构设计
1. 框架组件层次结构
graph TB
subgraph "框架组件架构"
A[App.tsx] --> B[NavBarFrame]
A --> C[HeadlessFrame]
B --> D[TopAppBar]
B --> E[AppDrawer]
B --> F[AppMain]
D --> D1[UserAction]
D --> D2[SearchBar]
D --> D3[NavBarMainActions]
E --> E1[SideNavItem]
E --> E2[StorageSummary]
E --> E3[PageNavigation]
F --> F1[FileManager]
F --> F2[Pages]
F --> F3[Admin]
end
2. 文件管理器组件结构
graph TB
subgraph "文件管理器组件"
A[FileManager] --> B[TopBar]
A --> C[Explorer]
A --> D[Uploader]
A --> E[Viewers]
A --> F[Dialogs]
B --> B1[Breadcrumb]
B --> B2[TopActions]
B --> B3[ViewOptions]
C --> C1[FileList]
C --> C2[ContextMenu]
C --> C3[DragDrop]
D --> D1[UploadQueue]
D --> D2[ProgressBar]
E --> E1[ImageViewer]
E --> E2[VideoPlayer]
E --> E3[DocumentViewer]
F --> F1[CreateNew]
F --> F2[ShareDialog]
F --> F3[DeleteConfirm]
end
状态管理架构
Redux Store 结构
// store.ts 核心配置
exportconst store = configureStore({
reducer: {
// 全局状态
globalState: globalStateSlice.reducer,
// 文件管理器状态
explorer: explorerSlice.reducer,
// 用户状态
user: userSlice.reducer,
// 上传状态
uploader: uploaderSlice.reducer,
// 对话框状态
dialog: dialogSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
});
状态管理模式
graph LR
subgraph "Redux 数据流"
A[Component] --> B[Action]
B --> C[Reducer]
C --> D[Store]
D --> A
E[Middleware] --> C
F[DevTools] --> D
end
主要 Slice 说明
1. globalStateSlice
interface GlobalState {
// 侧边栏状态
drawerOpen: boolean;
mobileDrawerOpen: boolean;
// 主题设置
theme: 'light' | 'dark' | 'auto';
// 语言设置
language: string;
// 加载状态
loading: boolean;
}
2. explorerSlice
interface ExplorerState {
// 当前路径
currentPath: string;
// 文件列表
files: FileResponse[];
// 选中的文件
selectedFiles: string[];
// 视图模式
viewMode: 'list' | 'grid' | 'small';
// 排序方式
sortBy: string;
sortOrder: 'asc' | 'desc';
}
路由系统
路由配置结构
// router/index.tsx
exportconst router = createBrowserRouter([
{
path: "/",
element: <App />,
errorElement: <ErrorBoundary />,
children: [
{ path: "/", element: <HomeRedirect /> },
{
path: "/",
element: <HeadlessFrame />,
children: [
// 登录相关路由
{
path: "/session",
element: <SessionIntro />,
children: [
{ path: "/session", element: <SignIn /> },
{ path: "signup", element: <SignUp /> },
{ path: "activate", element: <Activate /> },
{ path: "reset", element: <Reset /> },
],
},
],
},
],
},
]);
路由守卫机制
sequenceDiagram
participant U as 用户
participant R as Router
participant A as Auth Guard
participant S as Session
participant C as Component
U->>R: 访问路由
R->>A: 检查权限
A->>S: 验证会话
alt 已登录
S-->>A: 返回用户信息
A-->>R: 允许访问
R->>C: 渲染组件
C-->>U: 显示页面
else 未登录
S-->>A: 返回未认证
A-->>R: 重定向登录
R->>U: 跳转登录页
end
组件设计模式
1. 容器组件 vs 展示组件
// 容器组件 - 负责数据和逻辑
const FileManagerContainer: React.FC = () => {
const dispatch = useAppDispatch();
const files = useAppSelector(state => state.explorer.files);
const handleFileSelect = (fileId: string) => {
dispatch(selectFile(fileId));
};
return (
<FileList
files={files}
onFileSelect={handleFileSelect}
/>
);
};
// 展示组件 - 负责 UI 渲染
interface FileListProps {
files: FileResponse[];
onFileSelect: (fileId: string) => void;
}
const FileList: React.FC<FileListProps> = ({ files, onFileSelect }) => {
return (
<List>
{files.map(file => (
<FileItem
key={file.id}
file={file}
onClick={() => onFileSelect(file.id)}
/>
))}
</List>
);
};
2. 自定义 Hooks
// hooks/useFileManager.ts
exportconst useFileManager = () => {
const dispatch = useAppDispatch();
const { files, currentPath, loading } = useAppSelector(state => state.explorer);
const loadFiles = useCallback(async (path: string) => {
dispatch(setLoading(true));
try {
const response = await api.listFiles(path);
dispatch(setFiles(response.data));
} catch (error) {
// 错误处理
} finally {
dispatch(setLoading(false));
}
}, [dispatch]);
return {
files,
currentPath,
loading,
loadFiles,
};
};
3. 高阶组件 (HOC)
// hoc/withAuth.tsx
exportconst withAuth = <P extends object>(
Component: React.ComponentType<P>
) => {
return (props: P) => {
const { user } = useAppSelector(state => state.user);
if (!user) {
return <Navigate to="/session" />;
}
return <Component {...props} />;
};
};
// 使用方式
const ProtectedPage = withAuth(FileManager);
Material-UI 主题系统
主题配置
// App.tsx 中的主题配置
exportconst applyThemeWithOverrides = (themeConfig: ThemeOptions): ThemeOptions => {
return {
...themeConfig,
shape: {
borderRadius: 12,
},
components: {
MuiButton: {
styleOverrides: {
root: {
textTransform: "none",
},
},
},
MuiTooltip: {
defaultProps: {
enterDelay: 500,
},
},
},
};
};
响应式设计
// 使用 MUI 的断点系统
const useStyles = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const isTablet = useMediaQuery(theme.breakpoints.down('md'));
return {
container: {
padding: isMobile ? theme.spacing(1) : theme.spacing(3),
gridTemplateColumns: isMobile ? '1fr' : 'repeat(auto-fill, minmax(200px, 1fr))',
},
};
};
API 集成模式
请求封装
// api/request.ts
const instance = axios.create({
baseURL: '/api/v4',
});
// 请求拦截器
instance.interceptors.request.use(async (config) => {
const token = await SessionManager.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器
instance.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
// 处理认证失败
await SessionManager.refreshToken();
}
return Promise.reject(error);
}
);
API 函数设计
// api/explorer.ts
exportconst explorerAPI = {
// 获取文件列表
listFiles: (path: string): Promise<Response<FileResponse[]>> => {
return request.get('/file/list', { params: { path } });
},
// 上传文件
uploadFile: (
file: File,
path: string,
onProgress?: (progress: number) => void
): Promise<Response<FileResponse>> => {
const formData = new FormData();
formData.append('file', file);
formData.append('path', path);
return request.post('/file/upload', formData, {
onUploadProgress: (progressEvent) => {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
onProgress?.(progress);
},
});
},
};
国际化 (i18n) 系统
配置结构
// i18n.ts
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(Backend)
.use(LanguageDetector)
.init({
fallbackLng: 'en-US',
debug: false,
interpolation: {
escapeValue: false,
},
backend: {
loadPath: '/assets/locales/{{lng}}/{{ns}}.json',
},
});
使用方式
// 在组件中使用
const FileManager: React.FC = () => {
const { t } = useTranslation();
return (
<Box>
<Typography variant="h6">
{t('fileManager.title')}
</Typography>
<Button>
{t('common.upload')}
</Button>
</Box>
);
};
性能优化策略
1. 代码分割
// 路由级别的代码分割
const FileManager = lazy(() => import('../component/FileManager/FileManager'));
const AdminPanel = lazy(() => import('../component/Admin/AdminPanel'));
// 在路由中使用
<Route
path="/files"
element={
<Suspense fallback={<Loading />}>
<FileManager />
</Suspense>
}
/>
2. 组件优化
// 使用 React.memo 优化渲染
const FileItem = React.memo<FileItemProps>(({ file, onSelect }) => {
return (
<ListItem onClick={() => onSelect(file.id)}>
<ListItemText primary={file.name} />
</ListItem>
);
});
// 使用 useMemo 优化计算
const FileList: React.FC<FileListProps> = ({ files, filter }) => {
const filteredFiles = useMemo(() => {
return files.filter(file =>
file.name.toLowerCase().includes(filter.toLowerCase())
);
}, [files, filter]);
return (
<List>
{filteredFiles.map(file => (
<FileItem key={file.id} file={file} />
))}
</List>
);
};
3. 虚拟滚动
// 使用 react-virtuoso 处理大列表
import { Virtuoso } from 'react-virtuoso';
const VirtualFileList: React.FC<{ files: FileResponse[] }> = ({ files }) => {
return (
<Virtuoso
data={files}
itemContent={(index, file) => (
<FileItem key={file.id} file={file} />
)}
style={{ height: '400px' }}
/>
);
};
错误处理机制
错误边界
// component/Common/ErrorBoundary.tsx
classErrorBoundaryextendsReact.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
staticgetDerivedStateFromError(error: Error): State {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <ErrorFallback />;
}
returnthis.props.children;
}
}
全局错误处理
// 在 request.ts 中统一处理错误
instance.interceptors.response.use(
(response) => response,
(error) => {
const message = error.response?.data?.message || error.message;
// 显示错误提示
enqueueSnackbar(message, {
variant: 'error',
action: <DefaultCloseAction />
});
return Promise.reject(error);
}
);
测试策略
单元测试
// __tests__/FileItem.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { FileItem } from '../FileItem';
describe('FileItem', () => {
const mockFile = {
id: '1',
name: 'test.txt',
size: 1024,
};
it('renders file name correctly', () => {
render(<FileItem file={mockFile} onSelect={jest.fn()} />);
expect(screen.getByText('test.txt')).toBeInTheDocument();
});
it('calls onSelect when clicked', () => {
const onSelect = jest.fn();
render(<FileItem file={mockFile} onSelect={onSelect} />);
fireEvent.click(screen.getByText('test.txt'));
expect(onSelect).toHaveBeenCalledWith('1');
});
});
开发最佳实践
1. 组件设计原则
- 单一职责: 每个组件只负责一个功能
- 可复用性: 设计通用的组件接口
- 可测试性: 便于编写单元测试
- 性能优化: 避免不必要的重渲染
2. 状态管理原则
- 最小化状态: 只存储必要的状态
- 不可变更新: 使用 Redux Toolkit 的 Immer
- 异步处理: 使用 createAsyncThunk 处理异步操作
3. 代码组织原则
- 按功能分组: 相关组件放在同一目录
- 类型定义: 为所有 props 和状态定义类型
- 文档注释: 为复杂组件添加 JSDoc 注释
总结
Cloudreve 的前端架构采用了现代化的 React 生态系统,具有以下特点:
- 类型安全: 全面使用 TypeScript
- 组件化: 高度模块化的组件设计
- 状态管理: 使用 Redux Toolkit 进行状态管理
- UI 一致性: 基于 Material-UI 的设计系统
- 性能优化: 代码分割、虚拟滚动等优化策略
- 国际化: 完整的多语言支持
- 错误处理: 完善的错误边界和错误处理机制
这个架构为你开发侧边栏对话功能提供了良好的基础,你可以参考现有的组件设计模式和状态管理方式来实现新功能。
**03-backend-architecture.md - 后端架构指南**
* Go 语言基础知识(专为前端开发者)
* Gin 框架和 Ent ORM 使用
* 服务层设计和认证系统
**上下滑动查看完整内容**
Go 语言基础知识
为什么选择 Go
- 高性能: 编译型语言,接近 C/C++ 的性能
- 并发支持: 原生的 goroutine 和 channel 支持
- 简洁语法: 学习曲线平缓,代码可读性强
- 丰富生态: 优秀的 Web 框架和工具链
- 内存安全: 垃圾回收机制,避免内存泄漏
Go 语言核心概念
1. 包 (Package) 系统
// 包声明
package main
// 导入其他包
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
// 函数定义
func main(){
fmt.Println("Hello, World!")
}
2. 结构体 (Struct)
// 定义结构体
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"-"` // 不序列化到 JSON
}
// 结构体方法
func (u *User) GetDisplayName() string {
return u.Name
}
3. 接口 (Interface)
// 定义接口
type UserService interface {
CreateUser(user *User) error
GetUser(id int) (*User, error)
UpdateUser(user *User) error
DeleteUser(id int) error
}
// 实现接口
type userServiceImpl struct {
db *sql.DB
}
func (s *userServiceImpl) CreateUser(user *User) error {
// 实现逻辑
return nil
}
4. 错误处理
func GetUser(id int) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("invalid user id: %d", id)
}
user, err := db.FindUser(id)
if err != nil {
return nil, fmt.Errorf("failed to find user: %w", err)
}
return user, nil
}
// 使用
user, err := GetUser(123)
if err != nil {
log.Printf("Error: %v", err)
return
}
Cloudreve 后端技术栈
核心框架和库
- Gin: 高性能的 HTTP Web 框架
- Ent: Facebook 开源的 Go ORM 框架
- Cobra: 命令行应用框架
- JWT: JSON Web Token 认证
- Viper: 配置管理
- Logrus: 结构化日志
数据库支持
- MySQL: 主要生产数据库
- PostgreSQL: 企业级数据库
- SQLite: 轻量级嵌入式数据库
项目架构设计
分层架构
graph TB
subgraph "Cloudreve 后端分层架构"
A[HTTP 层] --> B[路由层 - routers/]
B --> C[控制器层 - controllers/]
C --> D[服务层 - service/]
D --> E[数据访问层 - ent/]
E --> F[数据库]
G[中间件层 - middleware/] --> C
H[工具包 - pkg/] --> D
I[应用配置 - application/] --> D
end
目录结构详解
Cloudreve/
├── main.go # 应用入口
├── cmd/ # 命令行工具
│ ├── root.go # 根命令
│ ├── server.go # 服务器命令
│ └── migrate.go # 数据库迁移
├── routers/ # 路由层
│ ├── router.go # 路由配置
│ └── controllers/ # 控制器
├── service/ # 业务逻辑层
│ ├── admin/ # 管理服务
│ ├── user/ # 用户服务
│ ├── explorer/ # 文件管理服务
│ └── chat/ # 聊天服务
├── ent/ # 数据模型层 (Ent ORM)
│ ├── schema/ # 数据库模式定义
│ ├── migrate/ # 数据库迁移
│ └── *.go # 生成的实体代码
├── middleware/ # 中间件
│ ├── auth.go # 认证中间件
│ ├── cors.go # 跨域中间件
│ └── session.go # 会话中间件
├── pkg/ # 工具包
│ ├── auth/ # 认证工具
│ ├── cache/ # 缓存工具
│ ├── conf/ # 配置管理
│ └── util/ # 通用工具
├── inventory/ # 库存管理
└── application/ # 应用配置
Gin 框架详解
基本用法
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
// 创建 Gin 引擎
r := gin.Default()
// 定义路由
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
// 启动服务器
r.Run(":8080")
}
路由组织
// routers/router.go
func InitRouter(dep dependency.Dep) *gin.Engine {
r := gin.New()
// 全局中间件
r.Use(gin.Recovery())
r.Use(middleware.CORS())
// API 版本分组
v4 := r.Group("/api/v4")
// 用户相关路由
user := v4.Group("/user")
user.Use(middleware.LoginRequired())
{
user.GET("/info", controllers.GetUserInfo)
user.PUT("/info", controllers.UpdateUserInfo)
}
// 文件相关路由
file := v4.Group("/file")
{
file.GET("/list", controllers.ListFiles)
file.POST("/upload", controllers.UploadFile)
}
return r
}
中间件系统
// middleware/auth.go
func LoginRequired() gin.HandlerFunc {
return func(c *gin.Context) {
// 从请求头获取 token
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Missing authorization token",
})
c.Abort()
return
}
// 验证 token
user, err := validateToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "Invalid token",
})
c.Abort()
return
}
// 将用户信息存储到上下文
c.Set("user", user)
c.Next()
}
}
Ent ORM 框架
模式定义
// ent/schema/user.go
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/edge"
)
// User 用户实体
type User struct {
ent.Schema
}
// Fields 字段定义
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty().
Comment("用户名"),
field.String("email").
Unique().
Comment("邮箱"),
field.String("password").
Sensitive().
Comment("密码"),
field.Time("created_at").
Default(time.Now).
Comment("创建时间"),
}
}
// Edges 关联关系
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("files", File.Type).
Comment("用户文件"),
edge.To("groups", Group.Type).
Through("user_groups", UserGroup.Type).
Comment("用户组"),
}
}
数据库操作
// service/user/user.go
type UserService struct {
client *ent.Client
}
func NewUserService(client *ent.Client) *UserService {
return &UserService{client: client}
}
// 创建用户
func (s *UserService) CreateUser(ctx context.Context, req *CreateUserRequest) (*ent.User, error) {
user, err := s.client.User.
Create().
SetName(req.Name).
SetEmail(req.Email).
SetPassword(hashPassword(req.Password)).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return user, nil
}
// 查询用户
func (s *UserService) GetUser(ctx context.Context, id int) (*ent.User, error) {
user, err := s.client.User.
Query().
Where(user.ID(id)).
WithFiles(). // 预加载文件关联
Only(ctx)
if err != nil {
if ent.IsNotFound(err) {
return nil, fmt.Errorf("user not found")
}
return nil, fmt.Errorf("failed to get user: %w", err)
}
return user, nil
}
// 更新用户
func (s *UserService) UpdateUser(ctx context.Context, id int, req *UpdateUserRequest) (*ent.User, error) {
user, err := s.client.User.
UpdateOneID(id).
SetName(req.Name).
SetEmail(req.Email).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed to update user: %w", err)
}
return user, nil
}
控制器层设计
控制器结构
// routers/controllers/user.go
package controllers
import (
"github.com/gin-gonic/gin"
"github.com/cloudreve/Cloudreve/v4/service/user"
)
// GetUserInfo 获取用户信息
func GetUserInfo(c *gin.Context) {
// 从上下文获取用户
currentUser, exists := c.Get("user")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{
"error": "User not found in context",
})
return
}
user := currentUser.(*ent.User)
// 返回用户信息
c.JSON(http.StatusOK, gin.H{
"data": gin.H{
"id": user.ID,
"name": user.Name,
"email": user.Email,
},
})
}
// UpdateUserInfo 更新用户信息
func UpdateUserInfo(c *gin.Context) {
var req user.UpdateUserRequest
// 绑定请求参数
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
// 获取当前用户
currentUser := c.MustGet("user").(*ent.User)
// 调用服务层
userService := user.NewUserService(ent.GetClient())
updatedUser, err := userService.UpdateUser(c.Request.Context(), currentUser.ID, &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": updatedUser,
})
}
参数绑定和验证
// service/user/types.go
type CreateUserRequest struct {
Name string `json:"name" binding:"required,min=2,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
type UpdateUserRequest struct {
Name string `json:"name" binding:"omitempty,min=2,max=50"`
Email string `json:"email" binding:"omitempty,email"`
}
// 自定义验证器
func ValidatePassword(fl validator.FieldLevel) bool {
password := fl.Field().String()
// 密码必须包含字母和数字
hasLetter := regexp.MustCompile(`[a-zA-Z]`).MatchString(password)
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
return hasLetter && hasNumber
}
服务层设计模式
依赖注入
// application/dependency/dependency.go
type Dep interface {
Logger() logging.Logger
ConfigProvider() conf.Provider
DatabaseClient() *ent.Client
CacheProvider() cache.Driver
}
type dep struct {
logger logging.Logger
configProvider conf.Provider
dbClient *ent.Client
cacheProvider cache.Driver
}
func NewDep() Dep {
return &dep{
logger: logging.NewLogger(),
configProvider: conf.NewProvider(),
dbClient: ent.NewClient(),
cacheProvider: cache.NewRedisDriver(),
}
}
服务接口设计
// service/user/interface.go
type Service interface {
CreateUser(ctx context.Context, req *CreateUserRequest) (*ent.User, error)
GetUser(ctx context.Context, id int) (*ent.User, error)
UpdateUser(ctx context.Context, id int, req *UpdateUserRequest) (*ent.User, error)
DeleteUser(ctx context.Context, id int) error
ListUsers(ctx context.Context, req *ListUsersRequest) ([]*ent.User, error)
}
// service/user/service.go
type service struct {
dep dependency.Dep
client *ent.Client
logger logging.Logger
}
func NewService(dep dependency.Dep) Service {
return &service{
dep: dep,
client: dep.DatabaseClient(),
logger: dep.Logger(),
}
}
认证和授权系统
JWT 认证
// pkg/auth/jwt.go
type JWTAuth struct {
secretKey []byte
issuer string
}
type Claims struct {
UserID int `json:"user_id"`
Email string `json:"email"`
jwt.RegisteredClaims
}
func (j *JWTAuth) GenerateToken(user *ent.User) (string, error) {
claims := &Claims{
UserID: user.ID,
Email: user.Email,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: j.issuer,
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(j.secretKey)
}
func (j *JWTAuth) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return j.secretKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
权限控制
// middleware/rbac.go
func RequirePermission(permission string) gin.HandlerFunc {
return func(c *gin.Context) {
user := c.MustGet("user").(*ent.User)
// 检查用户权限
hasPermission, err := checkUserPermission(user.ID, permission)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to check permission",
})
c.Abort()
return
}
if !hasPermission {
c.JSON(http.StatusForbidden, gin.H{
"error": "Insufficient permissions",
})
c.Abort()
return
}
c.Next()
}
}
// 使用方式
admin := v4.Group("/admin")
admin.Use(middleware.LoginRequired())
admin.Use(middleware.RequirePermission("admin:read"))
{
admin.GET("/users", controllers.ListUsers)
}
文件存储系统
存储接口设计
// pkg/filesystem/driver.go
type Driver interface {
// 上传文件
Put(ctx context.Context, path string, file io.Reader) error
// 获取文件
Get(ctx context.Context, path string) (io.ReadCloser, error)
// 删除文件
Delete(ctx context.Context, path string) error
// 获取文件信息
Stat(ctx context.Context, path string) (*FileInfo, error)
// 列出文件
List(ctx context.Context, path string) ([]*FileInfo, error)
}
type FileInfo struct {
Name string
Size int64
ModTime time.Time
IsDir bool
}
本地存储实现
// pkg/filesystem/local.go
type LocalDriver struct {
rootPath string
}
func NewLocalDriver(rootPath string) Driver {
return &LocalDriver{rootPath: rootPath}
}
func (d *LocalDriver) Put(ctx context.Context, path string, file io.Reader) error {
fullPath := filepath.Join(d.rootPath, path)
// 确保目录存在
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
// 创建文件
dst, err := os.Create(fullPath)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer dst.Close()
// 复制文件内容
_, err = io.Copy(dst, file)
if err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
return nil
}
func (d *LocalDriver) Get(ctx context.Context, path string) (io.ReadCloser, error) {
fullPath := filepath.Join(d.rootPath, path)
file, err := os.Open(fullPath)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
return file, nil
}
聊天服务实现
OpenRouter 客户端
// pkg/openrouter/client.go
type Client struct {
apiKey string
baseURL string
httpClient *http.Client
logger logging.Logger
}
type ChatRequest struct {
Model string `json:"model"`
Messages []Message `json:"messages"`
Stream bool `json:"stream"`
Temperature float64 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
func (c *Client) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
reqBody, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/chat/completions", bytes.NewReader(reqBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Authorization", "Bearer "+c.apiKey)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
var chatResp ChatResponse
if err := json.NewDecoder(resp.Body).Decode(&chatResp); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return &chatResp, nil
}
流式响应处理
// service/chat/chat.go
type StreamCallback func(content string, done bool) error
func (s *ChatService) StreamChat(ctx context.Context, user *ent.User, message string, callback StreamCallback) error {
req := openrouter.ChatRequest{
Model: "google/gemini-2.5-pro",
Messages: []openrouter.Message{
{
Role: "system",
Content: "你是 Cloudreve 云盘的 AI 助手",
},
{
Role: "user",
Content: message,
},
},
Stream: true,
Temperature: 0.7,
MaxTokens: 2000,
}
return s.client.ChatStream(ctx, req, callback)
}
// 在控制器中使用
func ChatStream(c *gin.Context) {
// 设置 SSE 响应头
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
// 获取参数
service := ParametersFromContext[*chatsvc.Service](c, ChatParameterCtx)
user := c.MustGet("user").(*ent.User)
// 创建聊天服务
chatService := chatsvc.NewChatService(dep)
// 流式回调
callback := func(content string, done bool) error {
data := map[string]interface{}{
"choices": []map[string]interface{}{
{
"delta": map[string]interface{}{
"content": content,
},
"finish_reason": nil,
},
},
}
if done {
data["choices"].([]map[string]interface{})[0]["finish_reason"] = "stop"
}
jsonData, _ := json.Marshal(data)
fmt.Fprintf(c.Writer, "data: %s\n\n", jsonData)
c.Writer.Flush()
return nil
}
// 执行流式聊天
err := chatService.StreamChat(c.Request.Context(), user, service.Message, callback)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
错误处理和日志
统一错误处理
// pkg/errors/errors.go
type AppError struct {
Code int `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
}
func (e *AppError) Error() string {
return e.Message
}
func NewAppError(code int, message string) *AppError {
return &AppError{
Code: code,
Message: message,
}
}
// 预定义错误
var (
ErrUserNotFound = NewAppError(404, "User not found")
ErrInvalidPassword = NewAppError(400, "Invalid password")
ErrUnauthorized = NewAppError(401, "Unauthorized")
ErrForbidden = NewAppError(403, "Forbidden")
)
结构化日志
// pkg/logging/logger.go
type Logger interface {
Debug(msg string, fields ...Field)
Info(msg string, fields ...Field)
Warn(msg string, fields ...Field)
Error(msg string, fields ...Field)
Fatal(msg string, fields ...Field)
}
type Field struct {
Key string
Value interface{}
}
func String(key, value string) Field {
return Field{Key: key, Value: value}
}
func Int(key string, value int) Field {
return Field{Key: key, Value: value}
}
// 使用方式
logger.Info("User created successfully",
String("user_id", user.ID),
String("email", user.Email),
String("ip", c.ClientIP()),
)
配置管理
配置结构
// pkg/conf/conf.go
type Config struct {
System SystemConfig `mapstructure:"system"`
Database DatabaseConfig `mapstructure:"database"`
Redis RedisConfig `mapstructure:"redis"`
Storage StorageConfig `mapstructure:"storage"`
}
type SystemConfig struct {
Mode string `mapstructure:"mode"`
Port int `mapstructure:"port"`
Debug bool `mapstructure:"debug"`
SessionName string `mapstructure:"session_name"`
}
type DatabaseConfig struct {
Type string `mapstructure:"type"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Name string `mapstructure:"name"`
}
// 加载配置
func LoadConfig(path string) (*Config, error) {
viper.SetConfigFile(path)
viper.SetConfigType("yaml")
if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read config: %w", err)
}
var config Config
if err := viper.Unmarshal(&config); err != nil {
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
}
return &config, nil
}
测试策略
单元测试
// service/user/user_test.go
func TestUserService_CreateUser(t *testing.T) {
// 设置测试数据库
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()
// 创建服务
service := NewUserService(client)
// 测试数据
req := &CreateUserRequest{
Name: "Test User",
Email: "test@example.com",
Password: "password123",
}
// 执行测试
user, err := service.CreateUser(context.Background(), req)
// 断言
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, req.Name, user.Name)
assert.Equal(t, req.Email, user.Email)
}
集成测试
// test/integration/api_test.go
func TestUserAPI(t *testing.T) {
// 设置测试服务器
router := setupTestRouter()
// 创建用户
createReq := `{"name":"Test User","email":"test@example.com","password":"password123"}`
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v4/user", strings.NewReader(createReq))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
var response map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &response)
assert.Equal(t, "Test User", response["data"].(map[string]interface{})["name"])
}
性能优化
数据库优化
// 使用索引
func (User) Indexes() []ent.Index {
return []ent.Index{
index.Fields("email").Unique(),
index.Fields("name", "created_at"),
}
}
// 批量操作
func (s *UserService) CreateUsers(ctx context.Context, users []*CreateUserRequest) ([]*ent.User, error) {
bulk := make([]*ent.UserCreate, len(users))
for i, req := range users {
bulk[i] = s.client.User.Create().
SetName(req.Name).
SetEmail(req.Email).
SetPassword(hashPassword(req.Password))
}
return s.client.User.CreateBulk(bulk...).Save(ctx)
}
// 预加载关联
func (s *UserService) GetUserWithFiles(ctx context.Context, id int) (*ent.User, error) {
return s.client.User.
Query().
Where(user.ID(id)).
WithFiles(func(q *ent.FileQuery) {
q.Order(ent.Desc(file.FieldCreatedAt)).
Limit(10)
}).
Only(ctx)
}
缓存策略
// pkg/cache/cache.go
type Cache interface {
Get(ctx context.Context, key string) (string, error)
Set(ctx context.Context, key string, value string, ttl time.Duration) error
Delete(ctx context.Context, key string) error
}
// 在服务中使用缓存
func (s *UserService) GetUser(ctx context.Context, id int) (*ent.User, error) {
cacheKey := fmt.Sprintf("user:%d", id)
// 尝试从缓存获取
cached, err := s.cache.Get(ctx, cacheKey)
if err == nil {
var user ent.User
if json.Unmarshal([]byte(cached), &user) == nil {
return &user, nil
}
}
// 从数据库获取
user, err := s.client.User.Get(ctx, id)
if err != nil {
return nil, err
}
// 存入缓存
if data, err := json.Marshal(user); err == nil {
s.cache.Set(ctx, cacheKey, string(data), time.Hour)
}
return user, nil
}
部署和运维
Docker 化
# Dockerfile
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o cloudreve .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/cloudreve .
COPY --from=builder /app/conf.ini .
EXPOSE 5212
CMD ["./cloudreve"]
健康检查
// routers/controllers/health.go
func HealthCheck(c *gin.Context) {
// 检查数据库连接
if err := checkDatabase(); err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{
"status": "unhealthy",
"error": err.Error(),
})
return
}
// 检查缓存连接
if err := checkCache(); err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{
"status": "unhealthy",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"timestamp": time.Now().Unix(),
})
}
总结
Cloudreve 的后端架构具有以下特点:
- 清晰的分层设计: 路由、控制器、服务、数据访问层职责分明
- 现代化的 ORM: 使用 Ent 提供类型安全的数据库操作
- 完善的中间件系统: 认证、授权、日志、错误处理等
- 灵活的存储系统: 支持多种存储后端
- 强大的聊天功能: 集成 OpenRouter API 支持 AI 对话
- 良好的测试覆盖: 单元测试和集成测试
- 性能优化: 缓存、数据库优化、并发处理
这个架构为你开发侧边栏对话功能提供了坚实的基础,你可以参考现有的聊天服务实现来扩展新功能。
**04-api-communication.md - API 通信机制**
* RESTful API 设计规范
* JWT 认证流程和 SSE 流式传输
* 错误处理和缓存策略
**上下滑动查看完整内容**
API 设计概览
Cloudreve 采用 RESTful API 设计,前后端通过 HTTP/HTTPS 协议进行通信。API 遵循统一的设计规范,提供清晰的接口定义和错误处理机制。
API 版本管理
基础路径: /api/v4
当前版本: v4
所有 API 请求都使用 /api/v4 作为基础路径,便于版本管理和向后兼容。
请求响应格式
统一响应格式
interface Response<T> {
data: T; // 响应数据
code: number; // 状态码
msg: string; // 消息
error?: string; // 错误信息
correlation_id?: string; // 请求追踪ID
}
成功响应示例
{
"data": {
"id": 1,
"name": "用户名",
"email": "user@example.com"
},
"code": 200,
"msg": "success"
}
错误响应示例
{
"data": null,
"code": 400,
"msg": "参数验证失败",
"error": "email field is required",
"correlation_id": "req-123456789"
}
认证机制
JWT Token 认证
sequenceDiagram
participant C as 客户端
participant S as 服务器
participant DB as 数据库
C->>S: POST /api/v4/session/token (用户名/密码)
S->>DB: 验证用户凭据
DB-->>S: 返回用户信息
S-->>C: 返回 JWT Token
Note over C: 存储 Token 到 localStorage
C->>S: GET /api/v4/user/info (Authorization: Bearer <token>)
S->>S: 验证 Token
S-->>C: 返回用户信息
Token 管理
1. 获取 Token
// 前端登录请求
const loginRequest = {
email: "user@example.com",
password: "password123"
};
const response = await axios.post('/api/v4/session/token', loginRequest);
const { access_token, refresh_token } = response.data;
// 存储 Token
SessionManager.setTokens(access_token, refresh_token);
2. 请求拦截器
// api/request.ts
instance.interceptors.request.use(async (config) => {
const token = await SessionManager.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
3. Token 刷新机制
instance.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
try {
// 尝试刷新 Token
await SessionManager.refreshToken();
// 重试原请求
return instance.request(error.config);
} catch (refreshError) {
// 刷新失败,跳转登录页
router.push('/session');
}
}
return Promise.reject(error);
}
);
核心 API 接口
1. 用户认证 API
登录
POST /api/v4/session/token
Content-Type: application/json
{
"email": "user@example.com",
"password": "password123"
}
响应:
{
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"expires_in": 3600,
"user": {
"id": 1,
"name": "用户名",
"email": "user@example.com"
}
},
"code": 200,
"msg": "登录成功"
}
刷新 Token
POST /api/v4/session/refresh
Content-Type: application/json
{
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}
登出
DELETE /api/v4/session/token
Authorization: Bearer <access_token>
2. 文件管理 API
获取文件列表
GET /api/v4/file/list?path=/documents&sort=nameℴ=asc
Authorization: Bearer <access_token>
响应:
{
"data": {
"files": [
{
"id": "file_123",
"name": "document.pdf",
"size": 1024000,
"type": "file",
"created_at": "2023-01-01T00:00:00Z",
"path": "cloudreve://my/documents/document.pdf"
}
],
"total": 1,
"path": "/documents"
},
"code": 200,
"msg": "success"
}
文件上传
POST /api/v4/file/upload
Authorization: Bearer <access_token>
Content-Type: multipart/form-data
file: <binary_data>
path: /documents
文件下载
GET /api/v4/file/download/<file_id>
Authorization: Bearer <access_token>
3. 聊天 API
普通聊天
POST /api/v4/chat
Authorization: Bearer <access_token>
Content-Type: application/json
{
"message": "请帮我总结一下文档内容"
}
响应:
{
"data": {
"response": "根据您的文档内容,主要包含以下几个方面...",
"model": "google/gemini-2.5-pro",
"usage": {
"prompt_tokens": 100,
"completion_tokens": 200,
"total_tokens": 300
}
},
"code": 200,
"msg": "success"
}
流式聊天
POST /api/v4/chat/stream
Authorization: Bearer <access_token>
Content-Type: application/json
{
"message": "请帮我分析这些文件"
}
响应(SSE 格式):
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: {"choices":[{"delta":{"content":"根据"},"finish_reason":null}]}
data: {"choices":[{"delta":{"content":"您的"},"finish_reason":null}]}
data: {"choices":[{"delta":{"content":"文件"},"finish_reason":null}]}
data: {"choices":[{"delta":{"content":""},"finish_reason":"stop"}]}
错误处理机制
HTTP 状态码规范
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 200 | 成功 | 请求成功处理 |
| 201 | 创建成功 | 资源创建成功 |
| 400 | 请求错误 | 参数验证失败 |
| 401 | 未认证 | Token 无效或过期 |
| 403 | 权限不足 | 没有访问权限 |
| 404 | 资源不存在 | 请求的资源不存在 |
| 409 | 冲突 | 资源冲突(如邮箱已存在) |
| 422 | 验证失败 | 业务逻辑验证失败 |
| 500 | 服务器错误 | 内部服务器错误 |
错误响应格式
interface ErrorResponse {
code: number;
msg: string;
error?: string;
details?: {
field: string;
message: string;
}[];
correlation_id?: string;
}
前端错误处理
// 全局错误处理
instance.interceptors.response.use(
(response) => response,
(error) => {
const { response } = error;
if (response) {
const { status, data } = response;
switch (status) {
case400:
enqueueSnackbar(data.msg || '请求参数错误', { variant: 'error' });
break;
case401:
enqueueSnackbar('登录已过期,请重新登录', { variant: 'error' });
router.push('/session');
break;
case403:
enqueueSnackbar('权限不足', { variant: 'error' });
break;
case404:
enqueueSnackbar('请求的资源不存在', { variant: error' });
break;
case500:
enqueueSnackbar('服务器内部错误', { variant: 'error' });
break;
default:
enqueueSnackbar(data.msg || '未知错误', { variant: 'error' });
}
} else {
enqueueSnackbar('网络连接错误', { variant: 'error' });
}
return Promise.reject(error);
}
);
流式数据传输
Server-Sent Events(SSE)
Cloudreve 使用 SSE 协议实现流式数据传输,主要用于聊天功能的实时响应。
前端 SSE 处理
// 流式聊天实现
exportconst streamChat = async (
message: string,
onMessage: (content: string) => void,
onComplete: () => void,
onError: (error: Error) => void
) => {
try {
const token = await SessionManager.getAccessToken();
const response = await fetch('/api/v4/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({ message }),
});
if (!response.ok) {
thrownew Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader!.read();
if (done) {
onComplete();
break;
}
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
onComplete();
return;
}
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content;
if (content) {
onMessage(content);
}
if (parsed.choices?.[0]?.finish_reason === 'stop') {
onComplete();
return;
}
} catch (parseError) {
console.warn('Failed to parse SSE data:', data);
}
}
}
}
} catch (error) {
onError(error as Error);
}
};
后端 SSE 实现
// 控制器中的流式响应
func ChatStream(c *gin.Context){
// 设置 SSE 响应头
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("Access-Control-Allow-Origin", "*")
// 获取参数
service := ParametersFromContext[*chatsvc.Service](c, ChatParameterCtx)
user := c.MustGet("user").(*ent.User)
// 创建聊天服务
chatService := chatsvc.NewChatService(dep)
// 流式回调函数
callback := func(content string, done bool) error {
data := map[string]interface{}{
"id": "chatcmpl-" + generateID(),
"object": "chat.completion.chunk",
"created": time.Now().Unix(),
"model": "google/gemini-2.5-pro",
"choices": []map[string]interface{}{
{
"index": 0,
"delta": map[string]interface{}{
"content": content,
},
"finish_reason": nil,
},
},
}
if done {
data["choices"].([]map[string]interface{})[0]["finish_reason"] = "stop"
}
jsonData, _ := json.Marshal(data)
fmt.Fprintf(c.Writer, "data: %s\n\n", jsonData)
c.Writer.Flush()
return nil
}
// 执行流式聊天
err := chatService.StreamChatWithFiles(c.Request.Context(), user, service.Message, callback)
if err != nil {
errorData := map[string]interface{}{
"error": map[string]interface{}{
"message": err.Error(),
"type": "server_error",
},
}
jsonData, _ := json.Marshal(errorData)
fmt.Fprintf(c.Writer, "data: %s\n\n", jsonData)
c.Writer.Flush()
}
// 发送结束标记
fmt.Fprintf(c.Writer, "data: [DONE]\n\n")
c.Writer.Flush()
}
文件上传机制
分片上传
对于大文件,Cloudreve 支持分片上传机制:
sequenceDiagram
participant C as 客户端
participant S as 服务器
participant FS as 文件系统
C->>S: 1. 创建上传会话
S-->>C: 返回 session_id
loop 分片上传
C->>S: 2. 上传分片 (session_id, chunk_index, chunk_data)
S->>FS: 存储分片
S-->>C: 返回上传进度
end
C->>S: 3. 完成上传 (session_id)
S->>FS: 合并分片
S-->>C: 返回文件信息
前端分片上传实现
exportconst uploadFileWithProgress = async (
file: File,
path: string,
onProgress: (progress: number) => void
): Promise<FileResponse> => {
const CHUNK_SIZE = 1024 * 1024; // 1MB per chunk
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
// 1. 创建上传会话
const sessionResponse = await api.post('/file/upload/session', {
name: file.name,
size: file.size,
path: path,
chunks: totalChunks,
});
const sessionId = sessionResponse.data.session_id;
try {
// 2. 分片上传
for (let i = 0; i < totalChunks; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('session_id', sessionId);
formData.append('chunk_index', i.toString());
await api.post('/file/upload/chunk', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
// 更新进度
const progress = Math.round(((i + 1) / totalChunks) * 100);
onProgress(progress);
}
// 3. 完成上传
const completeResponse = await api.post('/file/upload/complete', {
session_id: sessionId,
});
return completeResponse.data;
} catch (error) {
// 上传失败,清理会话
await api.delete(`/file/upload/session/${sessionId}`);
throw error;
}
};
缓存策略
前端缓存
1. HTTP 缓存
// 为静态资源设置缓存
const cacheableRequests = [
'/api/v4/site/config',
'/api/v4/user/info',
];
instance.interceptors.request.use((config) => {
if (cacheableRequests.some(url => config.url?.includes(url))) {
config.headers['Cache-Control'] = 'max-age=300'; // 5分钟缓存
}
return config;
});
2. 内存缓存
classApiCache {
private cache = new Map<string, { data: any; expires: number }>();
get(key: string): any | null {
const item = this.cache.get(key);
if (!item || Date.now() > item.expires) {
this.cache.delete(key);
return null;
}
return item.data;
}
set(key: string, data: any, ttl: number = 300000): void {
this.cache.set(key, {
data,
expires: Date.now() + ttl,
});
}
clear(): void {
this.cache.clear();
}
}
const apiCache = new ApiCache();
// 在请求拦截器中使用缓存
instance.interceptors.request.use((config) => {
if (config.method === 'GET') {
const cacheKey = `${config.url}?${JSON.stringify(config.params)}`;
const cached = apiCache.get(cacheKey);
if (cached) {
// 返回缓存的 Promise
return Promise.resolve({
...config,
adapter: () => Promise.resolve({
data: cached,
status: 200,
statusText: 'OK',
headers: {},
config,
}),
});
}
}
return config;
});
后端缓存
// 缓存中间件
func CacheMiddleware(ttl time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Method != "GET" {
c.Next()
return
}
cacheKey := fmt.Sprintf("api:%s:%s", c.Request.URL.Path, c.Request.URL.RawQuery)
// 尝试从缓存获取
if cached, err := cache.Get(c.Request.Context(), cacheKey); err == nil {
c.Header("X-Cache", "HIT")
c.Data(200, "application/json", []byte(cached))
return
}
// 包装 ResponseWriter 以捕获响应
w := &responseWriter{
ResponseWriter: c.Writer,
body: &bytes.Buffer{},
}
c.Writer = w
c.Next()
// 缓存响应
if w.status == 200 {
cache.Set(c.Request.Context(), cacheKey, w.body.String(), ttl)
}
}
}
跨域处理
CORS 配置
// middleware/cors.go
func CORS() gin.HandlerFunc {
return cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:3000", "https://yourdomain.com"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
})
}
预检请求处理
// 前端处理预检请求
axios.defaults.withCredentials = true;
// 自定义预检请求处理
instance.interceptors.request.use((config) => {
// 对于复杂请求,浏览器会先发送 OPTIONS 预检请求
if (config.method !== 'GET' && config.method !== 'POST') {
config.headers['Content-Type'] = 'application/json';
}
return config;
});
API 版本兼容
版本策略
// API 版本管理
exportconst API_VERSIONS = {
V3: '/api/v3',
V4: '/api/v4',
} as const;
// 根据功能选择 API 版本
exportconst createApiClient = (version: keyof typeof API_VERSIONS = 'V4') => {
return axios.create({
baseURL: API_VERSIONS[version],
timeout: 10000,
});
};
// 向后兼容处理
exportconst legacyApiCall = async (endpoint: string, data: any) => {
try {
// 尝试使用新版本 API
return await v4Client.post(endpoint, data);
} catch (error) {
if (error.response?.status === 404) {
// 回退到旧版本 API
console.warn(`Falling back to v3 API for ${endpoint}`);
return await v3Client.post(endpoint, data);
}
throw error;
}
};
性能优化
请求优化
1. 请求合并
// 批量请求合并
classRequestBatcher {
private batches = new Map<string, any[]>();
private timers = new Map<string, NodeJS.Timeout>();
batch<T>(
key: string,
request: any,
batchFn: (requests: any[]) => Promise<T[]>,
delay: number = 100
): Promise<T> {
returnnew Promise((resolve, reject) => {
if (!this.batches.has(key)) {
this.batches.set(key, []);
}
const batch = this.batches.get(key)!;
batch.push({ request, resolve, reject });
// 清除之前的定时器
if (this.timers.has(key)) {
clearTimeout(this.timers.get(key)!);
}
// 设置新的定时器
const timer = setTimeout(async () => {
const currentBatch = this.batches.get(key)!;
this.batches.delete(key);
this.timers.delete(key);
try {
const requests = currentBatch.map(item => item.request);
const results = await batchFn(requests);
currentBatch.forEach((item, index) => {
item.resolve(results[index]);
});
} catch (error) {
currentBatch.forEach(item => {
item.reject(error);
});
}
}, delay);
this.timers.set(key, timer);
});
}
}
// 使用示例
const batcher = new RequestBatcher();
exportconst getUserInfo = (userId: number) => {
return batcher.batch(
'getUserInfo',
userId,
async (userIds: number[]) => {
const response = await api.post('/user/batch', { ids: userIds });
return response.data;
}
);
};
2. 请求去重
// 请求去重
classRequestDeduplicator {
private pending = new Map<string, Promise<any>>();
dedupe<T>(key: string, requestFn: () => Promise<T>): Promise<T> {
if (this.pending.has(key)) {
returnthis.pending.get(key)!;
}
const promise = requestFn().finally(() => {
this.pending.delete(key);
});
this.pending.set(key, promise);
return promise;
}
}
const deduplicator = new RequestDeduplicator();
exportconst getFileList = (path: string) => {
const key = `fileList:${path}`;
return deduplicator.dedupe(key, () =>
api.get('/file/list', { params: { path } })
);
};
监控和调试
请求日志
// 请求日志中间件
instance.interceptors.request.use((config) => {
const requestId = generateRequestId();
config.metadata = { requestId, startTime: Date.now() };
console.log(`[${requestId}] ${config.method?.toUpperCase()} ${config.url}`, {
params: config.params,
data: config.data,
});
return config;
});
instance.interceptors.response.use(
(response) => {
const { requestId, startTime } = response.config.metadata;
const duration = Date.now() - startTime;
console.log(`[${requestId}] Response ${response.status} (${duration}ms)`, {
data: response.data,
});
return response;
},
(error) => {
const { requestId, startTime } = error.config?.metadata || {};
const duration = Date.now() - startTime;
console.error(`[${requestId}] Error ${error.response?.status} (${duration}ms)`, {
error: error.response?.data,
});
return Promise.reject(error);
}
);
性能监控
// API 性能监控
classApiMonitor {
private metrics = new Map<string, {
count: number;
totalTime: number;
errors: number;
}>();
record(endpoint: string, duration: number, success: boolean) {
if (!this.metrics.has(endpoint)) {
this.metrics.set(endpoint, { count: 0, totalTime: 0, errors: 0 });
}
const metric = this.metrics.get(endpoint)!;
metric.count++;
metric.totalTime += duration;
if (!success) {
metric.errors++;
}
}
getStats() {
const stats = Array.from(this.metrics.entries()).map(([endpoint, metric]) => ({
endpoint,
count: metric.count,
avgTime: metric.totalTime / metric.count,
errorRate: metric.errors / metric.count,
}));
return stats.sort((a, b) => b.avgTime - a.avgTime);
}
}
const monitor = new ApiMonitor();
// 在拦截器中使用
instance.interceptors.response.use(
(response) => {
const duration = Date.now() - response.config.metadata.startTime;
monitor.record(response.config.url!, duration, true);
return response;
},
(error) => {
const duration = Date.now() - error.config?.metadata?.startTime;
monitor.record(error.config?.url!, duration, false);
return Promise.reject(error);
}
);
总结
Cloudreve 的前后端通信机制具有以下特点:
- 统一的 API 设计: RESTful 风格,清晰的接口规范
- 完善的认证机制: JWT Token + 自动刷新
- 灵活的错误处理: 统一的错误格式和处理策略
- 流式数据支持: SSE 协议实现实时通信
- 高效的文件传输: 分片上传支持大文件
- 智能缓存策略: 前后端多层缓存优化
- 性能监控: 完整的请求监控和调试机制
这套通信机制为你开发侧边栏对话功能提供了可靠的基础,你可以直接使用现有的 API 接口和通信模式来实现新功能。
**05-development-guide.md - 开发流程指南**
* 环境搭建和开发工作流
* 调试技巧和性能优化
* 部署流程和最佳实践
**上下滑动查看完整内容**
开发环境搭建
系统要求
前端开发环境
- Node.js: 18.0+ (推荐使用 LTS 版本)
- npm: 8.0+ 或 yarn: 1.22+
- 现代浏览器: Chrome 90+, Firefox 88+, Safari 14+
后端开发环境
- Go: 1.23+ (必须)
- 数据库: MySQL 8.0+ / PostgreSQL 13+ / SQLite 3.35+
- Redis: 6.0+ (可选,用于缓存)
- Git: 2.30+
环境安装步骤
1. 安装 Node.js 和 Go
# 安装 Node.js (使用 nvm 推荐)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install 18
nvm use 18
# 验证安装
node --version
npm --version
# 安装 Go (macOS)
brew install go
# 或者下载官方安装包
# https://golang.org/dl/
# 验证安装
go version
2. 配置 Go 环境
# 设置 Go 环境变量 (添加到 ~/.zshrc 或 ~/.bashrc)
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin
export GO111MODULE=on
export GOPROXY=https://goproxy.cn,direct
# 重新加载配置
source ~/.zshrc
3. 安装数据库
# MySQL (macOS)
brew install mysql
brew services start mysql
# PostgreSQL (macOS)
brew install postgresql
brew services start postgresql
# SQLite (通常已预装)
sqlite3 --version
项目克隆和初始化
# 克隆项目
git clone https://github.com/cloudreve/Cloudreve.git
cd Cloudreve
# 初始化 Git 子模块 (前端资源)
git submodule update --init --recursive
前端开发流程
1. 前端环境配置
# 进入前端目录
cd assets
# 安装依赖
npm install
# 或者使用 yarn
yarn install
# 启动开发服务器
npm run dev
# 或者
yarn dev
2. 前端项目结构
assets/
├── src/
│ ├── component/ # React 组件
│ │ ├── Common/ # 通用组件
│ │ ├── Frame/ # 框架组件
│ │ ├── FileManager/ # 文件管理器
│ │ └── ...
│ ├── redux/ # 状态管理
│ ├── api/ # API 接口
│ ├── router/ # 路由配置
│ └── util/ # 工具函数
├── public/ # 静态资源
├── package.json # 依赖配置
├── vite.config.ts # Vite 配置
└── tsconfig.json # TypeScript 配置
3. 前端开发工作流
创建新组件
// 1. 创建组件文件
// src/component/Chat/ChatSidebar.tsx
import React, { useState } from 'react';
import { Box, TextField, Button, Typography } from '@mui/material';
import { useAppSelector, useAppDispatch } from '../../redux/hooks';
interface ChatSidebarProps {
open: boolean;
onClose: () => void;
}
exportconst ChatSidebar: React.FC<ChatSidebarProps> = ({ open, onClose }) => {
const [message, setMessage] = useState('');
const dispatch = useAppDispatch();
const handleSendMessage = () => {
// 发送消息逻辑
console.log('Sending message:', message);
setMessage('');
};
return (
<Box
sx={{
width: 400,
height: '100%',
display: open ? 'flex' : 'none',
flexDirection: 'column',
borderLeft: 1,
borderColor: 'divider',
}}
>
<Typography variant="h6" sx={{ p: 2 }}>
AI 助手
</Typography>
<Box sx={{ flex: 1, p: 2 }}>
{/* 聊天内容区域 */}
</Box>
<Box sx={{ p: 2, borderTop: 1, borderColor: 'divider' }}>
<TextField
fullWidth
multiline
rows={3}
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="输入您的问题..."
sx={{ mb: 1 }}
/>
<Button
fullWidth
variant="contained"
onClick={handleSendMessage}
disabled={!message.trim()}
>
发送
</Button>
</Box>
</Box>
);
};
添加 Redux State
// 2. 创建 Redux Slice
// src/redux/chatSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface ChatMessage {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: number;
}
interface ChatState {
messages: ChatMessage[];
isLoading: boolean;
sidebarOpen: boolean;
}
const initialState: ChatState = {
messages: [],
isLoading: false,
sidebarOpen: false,
};
const chatSlice = createSlice({
name: 'chat',
initialState,
reducers: {
addMessage: (state, action: PayloadAction<ChatMessage>) => {
state.messages.push(action.payload);
},
setLoading: (state, action: PayloadAction<boolean>) => {
state.isLoading = action.payload;
},
setSidebarOpen: (state, action: PayloadAction<boolean>) => {
state.sidebarOpen = action.payload;
},
clearMessages: (state) => {
state.messages = [];
},
},
});
exportconst { addMessage, setLoading, setSidebarOpen, clearMessages } = chatSlice.actions;
exportdefault chatSlice.reducer;
添加 API 接口
// 3. 创建 API 接口
// src/api/chat.ts
import { request } from './request';
export interface ChatRequest {
message: string;
}
export interface ChatResponse {
response: string;
model: string;
usage: {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
};
}
exportconst chatAPI = {
// 普通聊天
sendMessage: (data: ChatRequest): Promise<ChatResponse> => {
return request.post('/chat', data);
},
// 流式聊天
streamChat: async (
message: string,
onMessage: (content: string) => void,
onComplete: () => void,
onError: (error: Error) => void
) => {
try {
const response = await fetch('/api/v4/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await getAccessToken()}`,
},
body: JSON.stringify({ message }),
});
if (!response.ok) {
thrownew Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader!.read();
if (done) {
onComplete();
break;
}
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') {
onComplete();
return;
}
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content;
if (content) {
onMessage(content);
}
} catch (parseError) {
console.warn('Failed to parse SSE data:', data);
}
}
}
}
} catch (error) {
onError(error as Error);
}
},
};
4. 前端构建和部署
# 开发环境构建
npm run build
# 生产环境构建
npm run build-prod
# 预览构建结果
npm run preview
# 代码检查
npm run lint
# 代码格式化
npm run format
后端开发流程
1. 后端环境配置
# 进入项目根目录
cd Cloudreve
# 下载 Go 依赖
go mod download
# 生成 Ent 代码 (如果修改了 schema)
go generate ./ent
# 编译项目
go build -o cloudreve .
# 运行项目
./cloudreve
2. 数据库配置
创建配置文件
# conf.ini
[System]
Mode = Master
Listen = :5212
Debug = true
[Database]
Type = mysql
Host = 127.0.0.1
Port = 3306
User = root
Password = your_password
Name = cloudreve
TablePrefix = cd_
[Redis]
Server = 127.0.0.1:6379
Password =
DB = 0
数据库迁移
# 创建数据库
mysql -u root -p -e "CREATE DATABASE cloudreve CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
# 运行迁移
./cloudreve migrate
# 或者使用 Go 命令
go run . migrate
3. 后端开发工作流
创建新的数据模型
// 1. 定义 Ent Schema
// ent/schema/chatmessage.go
package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/edge"
)
// ChatMessage 聊天消息实体
type ChatMessage struct {
ent.Schema
}
// Fields 字段定义
func (ChatMessage) Fields() []ent.Field {
return []ent.Field{
field.Time("created_at").
Default(time.Now).
Comment("创建时间"),
field.String("role").
Comment("角色: user, assistant"),
field.Text("content").
Comment("消息内容"),
field.Int("user_id").
Comment("用户ID"),
field.String("session_id").
Optional().
Comment("会话ID"),
}
}
// Edges 关联关系
func (ChatMessage) Edges() []ent.Edge {
return []ent.Edge{
edge.From("user", User.Type).
Ref("chat_messages").
Field("user_id").
Unique().
Required(),
}
}
生成 Ent 代码
# 生成 Ent 代码
go generate ./ent
# 创建迁移文件
go run -mod=mod entgo.io/ent/cmd/ent migrate diff --dir file://ent/migrate/migrations initial
创建服务层
// 2. 创建服务接口和实现
// service/chat/interface.go
package chat
import (
"context"
"github.com/cloudreve/Cloudreve/v4/ent"
)
type Service interface {
SendMessage(ctx context.Context, userID int, message string) (*ChatResponse, error)
StreamMessage(ctx context.Context, userID int, message string, callback StreamCallback) error
GetChatHistory(ctx context.Context, userID int, limit int) ([]*ent.ChatMessage, error)
}
type ChatResponse struct {
ID string `json:"id"`
Content string `json:"content"`
Model string `json:"model"`
Usage Usage `json:"usage"`
}
type Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
type StreamCallback func(content string, done bool) error
// service/chat/service.go
package chat
import (
"context"
"fmt"
"github.com/cloudreve/Cloudreve/v4/application/dependency"
"github.com/cloudreve/Cloudreve/v4/ent"
"github.com/cloudreve/Cloudreve/v4/pkg/openrouter"
)
type service struct {
dep dependency.Dep
client *ent.Client
openrouter *openrouter.Client
}
func NewService(dep dependency.Dep) Service {
return &service{
dep: dep,
client: dep.DatabaseClient(),
openrouter: openrouter.NewClient("your-api-key", dep.Logger()),
}
}
func (s *service) SendMessage(ctx context.Context, userID int, message string) (*ChatResponse, error) {
// 保存用户消息
userMsg, err := s.client.ChatMessage.
Create().
SetUserID(userID).
SetRole("user").
SetContent(message).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed to save user message: %w", err)
}
// 调用 OpenRouter API
req := openrouter.ChatRequest{
Model: "google/gemini-2.5-pro",
Messages: []openrouter.Message{
{Role: "user", Content: message},
},
Temperature: 0.7,
MaxTokens: 2000,
}
resp, err := s.openrouter.Chat(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to get AI response: %w", err)
}
// 保存 AI 响应
aiMsg, err := s.client.ChatMessage.
Create().
SetUserID(userID).
SetRole("assistant").
SetContent(resp.Choices[0].Message.Content).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed to save AI message: %w", err)
}
return &ChatResponse{
ID: fmt.Sprintf("msg_%d", aiMsg.ID),
Content: resp.Choices[0].Message.Content,
Model: req.Model,
Usage: Usage{
PromptTokens: resp.Usage.PromptTokens,
CompletionTokens: resp.Usage.CompletionTokens,
TotalTokens: resp.Usage.TotalTokens,
},
}, nil
}
创建控制器
// 3. 创建控制器
// routers/controllers/chat.go
package controllers
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/cloudreve/Cloudreve/v4/service/chat"
)
// ChatRequest 聊天请求结构
type ChatRequest struct {
Message string `json:"message" binding:"required"`
}
// SendMessage 发送聊天消息
func SendMessage(c *gin.Context) {
var req ChatRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
// 获取当前用户
user := c.MustGet("user").(*ent.User)
// 获取依赖
dep := c.MustGet("dep").(dependency.Dep)
// 创建聊天服务
chatService := chat.NewService(dep)
// 发送消息
response, err := chatService.SendMessage(c.Request.Context(), user.ID, req.Message)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": response,
})
}
添加路由
// 4. 添加路由
// routers/router.go
func initMasterRouter(dep dependency.Dep) *gin.Engine {
// ... 现有代码 ...
// 聊天相关路由
chat := auth.Group("chat")
{
chat.POST("", controllers.SendMessage)
chat.POST("stream", controllers.StreamMessage)
chat.GET("history", controllers.GetChatHistory)
}
// ... 现有代码 ...
}
4. 后端测试
单元测试
// service/chat/service_test.go
package chat
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/cloudreve/Cloudreve/v4/ent/enttest"
)
func TestChatService_SendMessage(t *testing.T) {
// 创建测试数据库
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()
// 创建测试用户
user, err := client.User.
Create().
SetName("Test User").
SetEmail("test@example.com").
Save(context.Background())
assert.NoError(t, err)
// 创建服务 (需要 mock OpenRouter 客户端)
service := NewService(mockDep)
// 测试发送消息
response, err := service.SendMessage(context.Background(), user.ID, "Hello, AI!")
assert.NoError(t, err)
assert.NotNil(t, response)
assert.NotEmpty(t, response.Content)
}
集成测试
// test/integration/chat_test.go
package integration
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestChatAPI(t *testing.T) {
// 设置测试路由
router := setupTestRouter()
// 登录获取 token
token := loginAndGetToken(t, router)
// 测试发送消息
reqBody := map[string]string{
"message": "Hello, AI assistant!",
}
jsonBody, _ := json.Marshal(reqBody)
req, _ := http.NewRequest("POST", "/api/v4/chat", bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Contains(t, response, "data")
}
调试技巧
前端调试
1. 浏览器开发者工具
// 使用 console 调试
console.log('Debug info:', { user, message });
console.table(files); // 表格形式显示数组
console.group('API Call');
console.log('Request:', request);
console.log('Response:', response);
console.groupEnd();
// 使用 debugger 断点
function handleSubmit(){
debugger; // 浏览器会在此处暂停
// ... 代码逻辑
}
2. Redux DevTools
// 在 store.ts 中启用 DevTools
exportconst store = configureStore({
reducer: {
// ... reducers
},
devTools: process.env.NODE_ENV !== 'production',
});
3. React Developer Tools
安装 React Developer Tools 浏览器扩展,可以:
- 查看组件树结构
- 检查 props 和 state
- 性能分析
- Hook 调试
后端调试
1. 日志调试
// 使用结构化日志
logger := dep.Logger()
logger.Info("Processing chat request",
logging.String("user_id", fmt.Sprintf("%d", userID)),
logging.String("message", message),
)
logger.Error("Failed to save message",
logging.String("error", err.Error()),
logging.String("user_id", fmt.Sprintf("%d", userID)),
)
2. Delve 调试器
# 安装 Delve
go install github.com/go-delve/delve/cmd/dlv@latest
# 启动调试模式
dlv debug . -- --config conf.ini
# 在代码中设置断点
(dlv) break main.main
(dlv) break service/chat.(*service).SendMessage
(dlv) continue
3. 单元测试调试
# 运行特定测试
go test -v ./service/chat -run TestSendMessage
# 运行测试并显示覆盖率
go test -v -cover ./service/chat
# 生成覆盖率报告
go test -coverprofile=coverage.out ./service/chat
go tool cover -html=coverage.out
性能优化
前端性能优化
1. 代码分割
// 路由级别的懒加载
const ChatSidebar = lazy(() => import('./component/Chat/ChatSidebar'));
// 组件级别的懒加载
const HeavyComponent = lazy(() => import('./component/Heavy/HeavyComponent'));
// 使用 Suspense 包装
<Suspense fallback={<CircularProgress />}>
<ChatSidebar />
</Suspense>
2. 组件优化
// 使用 React.memo
const ChatMessage = React.memo<ChatMessageProps>(({ message }) => {
return (
<Box>
<Typography>{message.content}</Typography>
</Box>
);
});
// 使用 useMemo 和 useCallback
const ChatList: React.FC<ChatListProps> = ({ messages, onSend }) => {
const sortedMessages = useMemo(() => {
return messages.sort((a, b) => a.timestamp - b.timestamp);
}, [messages]);
const handleSend = useCallback((message: string) => {
onSend(message);
}, [onSend]);
return (
<List>
{sortedMessages.map(message => (
<ChatMessage key={message.id} message={message} />
))}
</List>
);
};
3. 虚拟滚动
// 使用 react-virtuoso 处理大量消息
import { Virtuoso } from 'react-virtuoso';
const ChatMessageList: React.FC<{ messages: ChatMessage[] }> = ({ messages }) => {
return (
<Virtuoso
data={messages}
itemContent={(index, message) => (
<ChatMessage key={message.id} message={message} />
)}
style={{ height: '400px' }}
/>
);
};
后端性能优化
1. 数据库优化
// 使用索引
func (ChatMessage) Indexes() []ent.Index {
return []ent.Index{
index.Fields("user_id", "created_at"),
index.Fields("session_id"),
}
}
// 批量操作
func (s *service) SaveMessages(ctx context.Context, messages []*ChatMessage) error {
bulk := make([]*ent.ChatMessageCreate, len(messages))
for i, msg := range messages {
bulk[i] = s.client.ChatMessage.Create().
SetUserID(msg.UserID).
SetRole(msg.Role).
SetContent(msg.Content)
}
_, err := s.client.ChatMessage.CreateBulk(bulk...).Save(ctx)
return err
}
// 分页查询
func (s *service) GetChatHistory(ctx context.Context, userID int, page, size int) ([]*ent.ChatMessage, error) {
return s.client.ChatMessage.
Query().
Where(chatmessage.UserID(userID)).
Order(ent.Desc(chatmessage.FieldCreatedAt)).
Offset((page - 1) * size).
Limit(size).
All(ctx)
}
2. 缓存策略
// Redis 缓存
func (s *service) GetUserChatHistory(ctx context.Context, userID int) ([]*ent.ChatMessage, error) {
cacheKey := fmt.Sprintf("chat:history:%d", userID)
// 尝试从缓存获取
cached, err := s.cache.Get(ctx, cacheKey)
if err == nil {
var messages []*ent.ChatMessage
if json.Unmarshal([]byte(cached), &messages) == nil {
return messages, nil
}
}
// 从数据库获取
messages, err := s.client.ChatMessage.
Query().
Where(chatmessage.UserID(userID)).
Order(ent.Desc(chatmessage.FieldCreatedAt)).
Limit(50).
All(ctx)
if err != nil {
return nil, err
}
// 存入缓存
if data, err := json.Marshal(messages); err == nil {
s.cache.Set(ctx, cacheKey, string(data), time.Hour)
}
return messages, nil
}
部署流程
开发环境部署
# 1. 启动后端服务
cd Cloudreve
go run . --config conf.ini
# 2. 启动前端开发服务器
cd assets
npm run dev
# 3. 访问应用
# 前端: http://localhost:5173
# 后端: http://localhost:5212
生产环境部署
1. 构建前端
cd assets
npm run build-prod
# 构建产物会输出到 ../statics/ 目录
2. 构建后端
cd Cloudreve
# 构建二进制文件
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o cloudreve .
# 或者使用 Docker 构建
docker build -t cloudreve:latest .
3. Docker 部署
# Dockerfile
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o cloudreve .
FROM alpine:latest
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /cloudreve
COPY --from=builder /app/cloudreve .
COPY --from=builder /app/statics ./statics
EXPOSE 5212
CMD ["./cloudreve"]
# docker-compose.yml
version: '3.8'
services:
cloudreve:
build: .
ports:
- "5212:5212"
volumes:
- ./conf.ini:/cloudreve/conf.ini
- ./uploads:/cloudreve/uploads
depends_on:
- mysql
- redis
environment:
- TZ=Asia/Shanghai
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: your_password
MYSQL_DATABASE: cloudreve
volumes:
- mysql_data:/var/lib/mysql
ports:
- "3306:3306"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
mysql_data:
常见问题解决
前端问题
1. 依赖安装失败
# 清理缓存
npm cache clean --force
rm -rf node_modules package-lock.json
# 重新安装
npm install
# 或者使用 yarn
yarn install --frozen-lockfile
2. 类型错误
// 确保类型定义正确
interface ApiResponse<T> {
data: T;
code: number;
msg: string;
}
// 使用类型断言
const response = await api.get('/user/info') as ApiResponse<UserInfo>;
// 或者使用泛型
const response = await api.get<UserInfo>('/user/info');
3. 构建错误
# 检查 TypeScript 错误
npx tsc --noEmit
# 检查 ESLint 错误
npm run lint
# 修复格式问题
npm run format
后端问题
1. 依赖下载失败
# 设置代理
go env -w GOPROXY=https://goproxy.cn,direct
# 清理模块缓存
go clean -modcache
# 重新下载
go mod download
2. 数据库连接问题
# 检查数据库服务状态
brew services list | grep mysql
# 重启数据库服务
brew services restart mysql
# 测试连接
mysql -u root -p -e "SELECT 1"
3. 编译错误
# 更新 Ent 生成的代码
go generate ./ent
# 检查 Go 版本
go version
# 更新依赖
go mod tidy
总结
这个开发流程指南涵盖了 Cloudreve 项目的完整开发周期:
- 环境搭建: 详细的开发环境配置步骤
- 前端开发: React + TypeScript 开发工作流
- 后端开发: Go + Gin + Ent 开发模式
- 调试技巧: 前后端调试方法和工具
- 性能优化: 代码优化和性能提升策略
- 部署流程: 从开发到生产的部署方案
- 问题解决: 常见问题的解决方法
通过遵循这个指南,你可以高效地进行 Cloudreve 项目的开发工作,并为实现侧边栏对话功能做好充分准备。
**06-chat-feature-extension.md - 聊天功能扩展实现方案**
* 基于现有聊天功能的侧边栏扩展
* 完整的前后端代码示例
* 文件夹上下文读取和 AI 对话集成
**上下滑动查看完整内容**
概述
本文档将指导您如何在现有的 Cloudreve 聊天功能基础上,实现一个侧边栏聊天对话框,该功能可以自动读取当前文件夹下的所有 .md 文件作为上下文,并通过 OpenRouter SDK 进行 AI 对话。
现有聊天功能分析
后端现有实现
Cloudreve 已经实现了基础的聊天功能:
graph TB
A[前端聊天界面] --> B[Chat Controller]
B --> C[Chat Service]
C --> D[OpenRouter API]
C --> E[文件系统]
D --> F[AI 模型响应]
E --> G[文件内容读取]
F --> H[SSE 流式响应]
G --> H
关键文件结构
service/chat/chat.go- 聊天服务核心逻辑routers/controllers/chat.go- 聊天 API 控制器pkg/openrouter/- OpenRouter SDK 集成
现有 API 端点
// 现有聊天相关路由
POST /api/v4/chat/completions // 发起聊天对话
GET /api/v4/chat/stream // SSE 流式响应
前端现有实现
前端已有基础聊天组件:
- 聊天消息显示
- 流式消息接收
- 文件上传和处理
扩展实现方案
1. 后端扩展
1.1 新增 API 端点
在 routers/controllers/chat.go 中添加新的控制器方法:
// ChatWithFolderContext 基于文件夹上下文的聊天
func ChatWithFolderContext(c *gin.Context){
// 获取当前用户和文件夹路径
user := c.MustGet("user").(*model.User)
folderPath := c.Query("folder_path")
// 读取文件夹下所有 .md 文件
mdFiles, err := service.GetMDFilesInFolder(user.ID, folderPath)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to read folder files"})
return
}
// 构建带文件上下文的聊天请求
chatRequest := buildChatRequestWithContext(c, mdFiles)
// 调用聊天服务
service.ChatWithContext(c, chatRequest)
}
1.2 文件读取服务
在 service/chat/ 目录下创建 folder_context.go:
package chat
import (
"path/filepath"
"strings"
"github.com/cloudreve/Cloudreve/v3/pkg/filesystem"
)
// MDFileContent 表示 Markdown 文件内容
type MDFileContent struct {
FileName string `json:"file_name"`
Content string `json:"content"`
Path string `json:"path"`
}
// GetMDFilesInFolder 获取文件夹下所有 .md 文件内容
func GetMDFilesInFolder(userID uint, folderPath string) ([]MDFileContent, error) {
var mdFiles []MDFileContent
// 初始化文件系统
fs, err := filesystem.NewFileSystemFromContext(context.Background(), userID)
if err != nil {
return nil, err
}
// 列出文件夹内容
files, err := fs.List(context.Background(), folderPath, nil)
if err != nil {
return nil, err
}
// 筛选并读取 .md 文件
for _, file := range files {
if strings.HasSuffix(strings.ToLower(file.Name), ".md") {
content, err := readFileContent(fs, filepath.Join(folderPath, file.Name))
if err != nil {
continue// 跳过读取失败的文件
}
mdFiles = append(mdFiles, MDFileContent{
FileName: file.Name,
Content: content,
Path: filepath.Join(folderPath, file.Name),
})
}
}
return mdFiles, nil
}
// readFileContent 读取文件内容
func readFileContent(fs *filesystem.FileSystem, filePath string) (string, error) {
file, err := fs.GetFileContent(context.Background(), filePath)
if err != nil {
return"", err
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
return"", err
}
returnstring(content), nil
}
1.3 上下文构建服务
扩展现有的聊天服务,添加文件上下文处理:
// buildChatRequestWithContext 构建带文件上下文的聊天请求
func buildChatRequestWithContext(c *gin.Context, mdFiles []MDFileContent) *openrouter.ChatRequest {
var contextMessages []openrouter.Message
// 添加系统提示,说明文件上下文
systemPrompt := "你是一个文件助手,可以基于用户提供的 Markdown 文件内容回答问题。以下是当前文件夹中的文件内容:\n\n"
for _, file := range mdFiles {
systemPrompt += fmt.Sprintf("## 文件:%s\n\n%s\n\n", file.FileName, file.Content)
}
contextMessages = append(contextMessages, openrouter.Message{
Role: "system",
Content: systemPrompt,
})
// 添加用户消息
var userRequest struct {
Message string `json:"message"`
}
c.ShouldBindJSON(&userRequest)
contextMessages = append(contextMessages, openrouter.Message{
Role: "user",
Content: userRequest.Message,
})
return &openrouter.ChatRequest{
Model: "google/gemini-2.5-pro", // 使用项目配置的模型
Messages: contextMessages,
Stream: true,
}
}
1.4 路由注册
在 routers/router.go 中添加新路由:
// 在现有聊天路由组中添加
chatGroup := v4.Group("/chat")
{
chatGroup.POST("/completions", controllers.ChatCompletions)
chatGroup.GET("/stream", controllers.ChatStream)
// 新增:基于文件夹上下文的聊天
chatGroup.POST("/folder-context", controllers.ChatWithFolderContext)
}
2. 前端扩展
2.1 侧边栏组件结构
graph TB
A[FileExplorer 文件浏览器] --> B[SidebarChat 侧边栏聊天]
B --> C[ChatToggle 聊天开关]
B --> D[ChatPanel 聊天面板]
D --> E[ChatMessages 消息列表]
D --> F[ChatInput 输入框]
D --> G[FolderContext 文件夹上下文显示]
2.2 创建侧边栏聊天组件
在 assets/src/components/FileManager/ 目录下创建 SidebarChat/ 文件夹:
SidebarChat/index.tsx
import React, { useState, useEffect } from 'react';
import {
Drawer,
Box,
IconButton,
Typography,
Divider,
Chip,
Stack
} from '@mui/material';
import {
Chat as ChatIcon,
Close as CloseIcon,
Folder as FolderIcon
} from '@mui/icons-material';
import ChatPanel from './ChatPanel';
import { useSelector } from 'react-redux';
import { RootState } from '../../store';
interface SidebarChatProps {
currentPath: string;
open: boolean;
onToggle: () => void;
}
const SidebarChat: React.FC<SidebarChatProps> = ({
currentPath,
open,
onToggle
}) => {
const [mdFiles, setMdFiles] = useState<string[]>([]);
const user = useSelector((state: RootState) => state.auth.user);
// 获取当前文件夹的 .md 文件列表
useEffect(() => {
if (open && currentPath) {
fetchMDFiles();
}
}, [open, currentPath]);
const fetchMDFiles = async () => {
try {
const response = await fetch(`/api/v4/directory${currentPath}`, {
headers: {
'Authorization': `Bearer ${user?.token}`
}
});
const data = await response.json();
const mdFileNames = data.objects
?.filter((file: any) => file.name.toLowerCase().endsWith('.md'))
?.map((file: any) => file.name) || [];
setMdFiles(mdFileNames);
} catch (error) {
console.error('Failed to fetch MD files:', error);
}
};
return (
<Drawer
anchor="right"
open={open}
onClose={onToggle}
variant="persistent"
sx={{
width: 400,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: 400,
boxSizing: 'border-box',
},
}}
>
<Box sx={{ p: 2 }}>
{/* 头部 */}
<Box display="flex" alignItems="center" justifyContent="space-between">
<Box display="flex" alignItems="center" gap={1}>
<ChatIcon color="primary" />
<Typography variant="h6">文件夹助手</Typography>
</Box>
<IconButton onClick={onToggle} size="small">
<CloseIcon />
</IconButton>
</Box>
<Divider sx={{ my: 2 }} />
{/* 当前文件夹信息 */}
<Box mb={2}>
<Box display="flex" alignItems="center" gap={1} mb={1}>
<FolderIcon fontSize="small" />
<Typography variant="body2" color="text.secondary">
当前文件夹:{currentPath || '/'}
</Typography>
</Box>
{mdFiles.length > 0 && (
<Box>
<Typography variant="body2" color="text.secondary" mb={1}>
可用的 Markdown 文件:
</Typography>
<Stack direction="row" spacing={1} flexWrap="wrap">
{mdFiles.map((fileName) => (
<Chip
key={fileName}
label={fileName}
size="small"
variant="outlined"
/>
))}
</Stack>
</Box>
)}
</Box>
<Divider sx={{ mb: 2 }} />
{/* 聊天面板 */}
<ChatPanel currentPath={currentPath} mdFiles={mdFiles} />
</Box>
</Drawer>
);
};
exportdefault SidebarChat;
SidebarChat/ChatPanel.tsx
import React, { useState, useRef, useEffect } from 'react';
import {
Box,
TextField,
IconButton,
Paper,
Typography,
CircularProgress
} from '@mui/material';
import { Send as SendIcon } from '@mui/icons-material';
import { useSelector } from 'react-redux';
import { RootState } from '../../../store';
interface ChatPanelProps {
currentPath: string;
mdFiles: string[];
}
interface ChatMessage {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
}
const ChatPanel: React.FC<ChatPanelProps> = ({ currentPath, mdFiles }) => {
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [inputValue, setInputValue] = useState('');
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const user = useSelector((state: RootState) => state.auth.user);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const sendMessage = async () => {
if (!inputValue.trim() || isLoading) return;
const userMessage: ChatMessage = {
id: Date.now().toString(),
role: 'user',
content: inputValue,
timestamp: new Date()
};
setMessages(prev => [...prev, userMessage]);
setInputValue('');
setIsLoading(true);
try {
const response = await fetch('/api/v4/chat/folder-context', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${user?.token}`
},
body: JSON.stringify({
message: inputValue,
folder_path: currentPath
})
});
if (!response.ok) {
thrownew Error('Chat request failed');
}
// 处理 SSE 流式响应
const reader = response.body?.getReader();
const decoder = new TextDecoder();
let assistantMessage = '';
const assistantMessageObj: ChatMessage = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: '',
timestamp: new Date()
};
setMessages(prev => [...prev, assistantMessageObj]);
while (reader) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const parsed = JSON.parse(data);
const content = parsed.choices?.[0]?.delta?.content || '';
assistantMessage += content;
setMessages(prev => prev.map(msg =>
msg.id === assistantMessageObj.id
? { ...msg, content: assistantMessage }
: msg
));
} catch (e) {
// 忽略解析错误
}
}
}
}
} catch (error) {
console.error('Chat error:', error);
const errorMessage: ChatMessage = {
id: (Date.now() + 2).toString(),
role: 'assistant',
content: '抱歉,发生了错误,请稍后重试。',
timestamp: new Date()
};
setMessages(prev => [...prev, errorMessage]);
} finally {
setIsLoading(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
return (
<Box display="flex" flexDirection="column" height="calc(100vh - 200px)">
{/* 消息列表 */}
<Box flex={1} overflow="auto" mb={2}>
{messages.length === 0 && (
<Box textAlign="center" py={4}>
<Typography variant="body2" color="text.secondary">
开始与文件夹助手对话吧!
{mdFiles.length > 0 && (
<>
<br />
我可以基于当前文件夹中的 {mdFiles.length} 个 Markdown 文件回答问题。
</>
)}
</Typography>
</Box>
)}
{messages.map((message) => (
<Paper
key={message.id}
elevation={1}
sx={{
p: 2,
mb: 1,
backgroundColor: message.role === 'user' ? 'primary.light' : 'grey.100',
color: message.role === 'user' ? 'primary.contrastText' : 'text.primary',
alignSelf: message.role === 'user' ? 'flex-end' : 'flex-start',
maxWidth: '85%',
ml: message.role === 'user' ? 'auto' : 0,
mr: message.role === 'assistant' ? 'auto' : 0,
}}
>
<Typography variant="body2" sx={{ whiteSpace: 'pre-wrap' }}>
{message.content}
</Typography>
<Typography variant="caption" color="text.secondary" mt={1} display="block">
{message.timestamp.toLocaleTimeString()}
</Typography>
</Paper>
))}
{isLoading && (
<Box display="flex" alignItems="center" gap={1} p={2}>
<CircularProgress size={16} />
<Typography variant="body2" color="text.secondary">
正在思考...
</Typography>
</Box>
)}
<div ref={messagesEndRef} />
</Box>
{/* 输入框 */}
<Box display="flex" gap={1}>
<TextField
fullWidth
multiline
maxRows={3}
placeholder="输入消息..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
disabled={isLoading}
size="small"
/>
<IconButton
onClick={sendMessage}
disabled={!inputValue.trim() || isLoading}
color="primary"
>
<SendIcon />
</IconButton>
</Box>
</Box>
);
};
exportdefault ChatPanel;
2.3 集成到文件管理器
修改主文件管理器组件,添加聊天侧边栏:
FileManager/index.tsx (部分修改)
import SidebarChat from './SidebarChat';
const FileManager: React.FC = () => {
const [chatOpen, setChatOpen] = useState(false);
const currentPath = useSelector((state: RootState) => state.explorer.path);
return (
<Box display="flex" height="100vh">
{/* 主文件管理区域 */}
<Box flex={1} overflow="hidden">
{/* 工具栏添加聊天按钮 */}
<Toolbar>
{/* 现有工具栏内容 */}
<IconButton
onClick={() => setChatOpen(!chatOpen)}
color={chatOpen ? 'primary' : 'default'}
>
<ChatIcon />
</IconButton>
</Toolbar>
{/* 文件列表等现有内容 */}
{/* ... */}
</Box>
{/* 聊天侧边栏 */}
<SidebarChat
currentPath={currentPath}
open={chatOpen}
onToggle={() => setChatOpen(!chatOpen)}
/>
</Box>
);
};
实现步骤
阶段 1:后端基础功能
- 创建文件夹上下文读取服务
- 实现新的聊天 API 端点
- 测试文件读取和上下文构建功能
阶段 2:前端界面开发
- 创建侧边栏聊天组件
- 实现聊天界面和交互逻辑
- 集成到主文件管理器
阶段 3:功能完善
- 添加错误处理和用户反馈
- 优化性能和用户体验
- 添加配置选项
测试方案
单元测试
// 测试文件读取功能
func TestGetMDFilesInFolder(t *testing.T){
// 创建测试文件夹和 .md 文件
// 测试文件读取功能
// 验证返回结果
}
集成测试
// 测试前端聊天组件
describe('SidebarChat', () => {
it('should display MD files in current folder', () => {
// 测试文件列表显示
});
it('should send chat message with folder context', () => {
// 测试聊天功能
});
});
部署注意事项
- 权限检查:确保用户只能访问有权限的文件夹
- 性能优化:大文件夹下的 .md 文件数量限制
- 内存管理:避免一次性加载过多文件内容
- 错误处理:文件读取失败时的优雅降级
扩展功能建议
- 文件类型扩展:支持更多文件类型(.txt, .json 等)
- 智能摘要:自动生成文件夹内容摘要
- 历史记录:保存聊天历史
- 快捷操作:预设常用问题模板
通过以上实现方案,您可以在现有 Cloudreve 项目基础上,成功添加基于文件夹上下文的智能聊天功能。

**模块重构**
**分析并规划重构**


**重构计划详情**
**上下滑动查看完整内容**
📊 项目现状分析
技术栈概况
- React 18.2.0 + TypeScript 5.2.2 - 现代化技术栈
- Material-UI 6.x - 成熟的组件库
- Redux Toolkit - 现代状态管理
- 119个依赖包 - 依赖较为复杂
主要问题识别
- 性能问题: Bundle过大、缺少虚拟化、不必要的重渲染
- 代码质量: 75个文件使用any类型、组件职责不清、代码重复
- 架构问题: 组件耦合度高、状态管理分散、缺少抽象层
- 技术债务: 8个TODO标记、重复样式定义、错误处理不统一
🎯 重构优先级排序
第一优先级:快速胜利项目(2-3周)
风险:🟢低 | 收益:💰💰中高
- 代码规范统一
- 统一TypeScript配置,消除any类型滥用
- 建立组件命名规范和文件组织标准
- 实施自动化代码格式化
- 国际化优化
- 优化10种语言的翻译文件结构
- 实施按需加载机制
- 统一翻译key命名规范
- 工具函数抽取
- 创建统一的时间格式化、文件类型判断工具
- 抽取重复的API调用模式
- 建立公共Hook库
第二优先级:性能优化项目(3-4周)
风险:🔴高 | 收益:💰💰💰高
- Bundle优化
- 实施精细化代码分割,预计减少35%包大小
- 优化Monaco Editor、Excalidraw等大型库的加载
- 实施懒加载和预加载策略
- 组件性能优化
- 为核心组件添加React.memo和useCallback
- 实施虚拟滚动解决长列表性能问题
- 优化文件缩略图加载机制
- 运行时优化
- 修复内存泄漏问题
- 优化状态更新频率
- 实施智能缓存策略
第三优先级:架构重构项目(4-6周)
风险:🔴高 | 收益:💰💰💰高
- 组件架构重构
- 重构GridFile等大型组件(463行→拆分为多个小组件)
- 分离数据获取和UI渲染逻辑
- 建立统一的错误处理和加载状态管理
- 状态管理优化
- 重构聊天系统的状态管理
- 统一Redux和本地状态的使用模式
- 实施状态机管理复杂状态转换
- 依赖升级
- 分批升级核心依赖包
- 解决安全漏洞和兼容性问题
- 获得新版本特性支持
🚀 实施策略
渐进式重构方案
阶段一(2-3周):基础优化
├── 代码规范统一
├── 国际化优化
└── 技术债务清理
阶段二(3-4周):性能优化
├── Bundle分割优化
├── 组件渲染优化
└── 缓存策略实施
阶段三(4-6周):架构重构
├── 组件拆分重构
├── 状态管理统一
└── 依赖版本升级
测试和质量保证
- 自动化测试: 核心组件90%+覆盖率
- 性能测试: FCP<1.5s, LCP<2.5s目标
- 回滚计划: 每阶段设置稳定版本标签
- 监控告警: 实时监控关键性能指标
📈 预期收益
性能提升
- 加载速度: 提升50%
- Bundle大小: 减少35%
- 内存占用: 降低20-30%
开发效率
- 新功能开发: 速度提升40%
- 组件复用率: 提升60%
- Bug修复时间: 减少40%
代码质量
- 技术债务: 减少80%
- 测试覆盖率: 达到85%+
- 类型安全: 消除75个any类型使用
⚠️ 风险控制
技术风险缓解
- 渐进式重构避免大爆炸式改动
- 充分的自动化和手工测试
- 灰度发布和实时监控
项目风险管理
- 每周进度和质量评审
- 明确的里程碑和交付物
- 备用方案和知识分享机制
💼 资源需求
- 人力: 前端架构师1人 + 高级工程师2人 + 工程师1人
- 时间: 总计13-16周
- 工具: 自动化测试框架、性能监控工具、CI/CD流水线
这个重构计划通过科学的优先级排序和风险控制,确保在提升代码质量的同时保持项目稳定性,为长期的可维护性和开发效率奠定基础。
**重构实施结果**




**功能迭代**
在完成项目理解和代码优化后,正式进入功能开发阶段。我计划为 Cloudreve 添加一个智能对话功能,让用户可以通过自然语言与云盘交互。
**准备工作:接入 Context7 MCP**
为了提高 AI 对 OpenRouter API 的理解和使用能力,我先接入了 Context7 MCP 服务。
**Context7 链接:**
https://context7.com?q=openrouter
**MCP 配置:**
{
"mcpServers": {
"context7": {
"url": "https://mcp.context7.com/mcp"
}
}
}

Context7 MCP 配置界面

MCP 服务状态
**第一阶段:基础对话功能**
**需求规划**
打开 Plan,明确基础对话功能的核心需求。
**需求描述:**
需求:
给云盘加个 AI 对话功能,用户可以用自然语言跟 AI 聊天
后端 :
- 新增流式对话接口并注册到路由中,需要身份鉴权
- 集成 OpenRouter 调用模型
前端 :
- 右下角加个圆形悬浮按钮
- 点击弹出 600px 宽的侧边栏对话面板
- 基本的聊天界面:消息列表 + 输入框
- 调用流式对话接口时,处理好鉴权参数、流式输出状态

Coder 会自动调用内置的 Search Agent 分析项目结构,理解代码上下文:

Search Agent 工作过程
分析完成后,Coder 将技术方案以文档形式呈现,支持实时修改和编辑:

技术方案文档
**任务执行**
确认方案后,Coder 会根据技术规划自动拆解任务并分步执行

任务拆解列表
在实施过程中,可以实时看到每个步骤的状态更新:

实施进度追踪
开发完成后,Coder 会自动启动项目进行预览验证:

自动启动预览
**实现效果:**
对话功能已经成功实现,UI 风格与 Cloudreve 原生界面保持高度一致:

对话功能界面
**第二阶段:增量迭代,增加多模态召回**
基础对话功能完成后,我进一步提出了更高级的需求:让 AI 能够理解和检索云盘中的文件内容。
**增强需求**
**需求描述:**
需求:
AI 聊天支持多模态文件智能检索
后端:
- 流式对话接口中读取全量云盘文内容作为上下文提供给模型,模型通过文件元信息及多模态内容进行文件召回
- 返回:summary + file list 的结构,前端渲染为文本 + GUI 列表
- 问"帮我找包含小狗的图片"这类问题时,能够召回文件元信息或文件内容与用户意图相关的文件
前端:
- 消息中展示文件列表
- 3 列网格布局,显示缩略图、文件名
**实现效果**
Coder 会自动启动前后端服务,并通过终端进行功能测试:

功能测试过程
**最终效果:**
系统成功实现了文件内容识别和智能召回功能。当用户询问"帮我找包含小狗的图片"时,AI 能够准确识别图片内容并返回相关文件:

文件召回效果展示

**总结与思考**
通过这次实践,可以看到 SOLO Coder 在项目理解、代码重构到功能开发的每个环节都可以提供支持。这个项目未来还可以进一步探索的有:更复杂的多模态交互场景、智能文件管理和推荐和基于用户行为的个性化体验,欢迎大家去创造体验。

