diff --git a/README.md b/README.md
index 5da3e8f..ee98df5 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,53 @@
Electron 跨平台桌面网盘应用
+## 技术栈
+
+### 前端
+- Electron 28+ (桌面框架)
+- React 18 (UI框架)
+- TypeScript (类型安全)
+- Zustand (状态管理)
+- Ant Design 5 (UI组件库)
+
+### 后端
+- Node.js 20+ (运行环境)
+- Express 4 (Web框架)
+- SQLite3 (本地数据库)
+- JWT (用户认证)
+- Multer (文件上传)
+
+## 功能特性
+
+- ✅ 用户注册/登录
+- ✅ 文件上传/下载/删除
+- ✅ 文件夹管理
+- ✅ 分享功能(密码保护/链接分享)
+- 🔄 云端同步
+- ⏳ 离线访问
+- ⏳ 主题切换
+
+## 开发指南
+
+### 前端开发
+```bash
+cd frontend
+npm install
+npm run dev
+```
+
+### 后端开发
+```bash
+cd backend
+npm install
+npm run dev
+```
+
+### 测试
+```bash
+npm test
+```
+
## 团队成员
| 角色 | 账号 |
@@ -21,7 +68,11 @@ Electron 跨平台桌面网盘应用
├── prd/ # 产品需求文档
├── ui-design/ # UI设计文件
├── architecture/ # 架构设计文档
-├── frontend/ # 前端代码
+├── frontend/ # 前端代码
├── backend/ # 后端代码
└── tests/ # 测试代码
```
+
+## Git 仓库
+
+https://git.liantu.tech/openclaw_product_manager/clouddisk-project
diff --git a/backend/src/index.js b/backend/src/index.js
index 3e7e4e7..f1f0bc7 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -3,6 +3,7 @@ const cors = require('cors');
const authRoutes = require('./routes/auth');
const fileRoutes = require('./routes/files');
const shareRoutes = require('./routes/share');
+const syncRoutes = require('./routes/sync');
const app = express();
const PORT = process.env.PORT || 3000;
@@ -14,6 +15,7 @@ app.use(express.json());
app.use('/api/auth', authRoutes);
app.use('/api/files', fileRoutes);
app.use('/api/share', shareRoutes);
+app.use('/api/sync', syncRoutes);
// Health check
app.get('/api/health', (req, res) => {
diff --git a/backend/src/routes/sync.js b/backend/src/routes/sync.js
new file mode 100644
index 0000000..88d30ec
--- /dev/null
+++ b/backend/src/routes/sync.js
@@ -0,0 +1,152 @@
+const express = require('express');
+const crypto = require('crypto');
+const fs = require('fs');
+const path = require('path');
+const db = require('../db');
+
+const router = express.Router();
+
+// Get sync status
+router.get('/status', (req, res) => {
+ const token = req.headers.authorization?.replace('Bearer ', '');
+ if (!token) return res.status(401).json({ error: 'No token' });
+
+ try {
+ const jwt = require('jsonwebtoken');
+ const decoded = jwt.verify(token, process.env.JWT_SECRET || 'clouddisk-secret-key');
+
+ const status = db.query(
+ 'SELECT * FROM sync_status WHERE user_id = ?',
+ [decoded.userId]
+ );
+
+ if (status.length === 0) {
+ // Create initial sync status
+ const result = db.run(
+ 'INSERT INTO sync_status (user_id) VALUES (?)',
+ [decoded.userId]
+ );
+ return res.json({
+ status: 'idle',
+ totalFiles: 0,
+ syncedFiles: 0
+ });
+ }
+
+ res.json({
+ status: status[0].status,
+ lastSyncTime: status[0].last_sync_time,
+ totalFiles: status[0].total_files,
+ syncedFiles: status[0].synced_files
+ });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+// Start sync
+router.post('/start', (req, res) => {
+ const token = req.headers.authorization?.replace('Bearer ', '');
+ if (!token) return res.status(401).json({ error: 'No token' });
+
+ try {
+ const jwt = require('jsonwebtoken');
+ const decoded = jwt.verify(token, process.env.JWT_SECRET || 'clouddisk-secret-key');
+
+ // Update sync status
+ db.run(
+ 'UPDATE sync_status SET status = ?, last_sync_time = CURRENT_TIMESTAMP WHERE user_id = ?',
+ ['syncing', decoded.userId]
+ );
+
+ res.json({ success: true, message: 'Sync started' });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+// Calculate file hash for deduplication
+function calculateFileHash(filePath) {
+ return new Promise((resolve, reject) => {
+ const hash = crypto.createHash('md5');
+ const stream = fs.createReadStream(filePath);
+ stream.on('data', data => hash.update(data));
+ stream.on('end', () => resolve(hash.digest('hex')));
+ stream.on('error', reject);
+ });
+}
+
+// Sync single file (for client to call)
+router.post('/file', (req, res) => {
+ const token = req.headers.authorization?.replace('Bearer ', '');
+ if (!token) return res.status(401).json({ error: 'No token' });
+
+ try {
+ const jwt = require('jsonwebtoken');
+ const decoded = jwt.verify(token, process.env.JWT_SECRET || 'clouddisk-secret-key');
+
+ const { fileId, localHash, localModified } = req.body;
+
+ // Get server file info
+ const serverFile = db.query(
+ 'SELECT * FROM files WHERE id = ? AND user_id = ?',
+ [fileId, decoded.userId]
+ );
+
+ if (serverFile.length === 0) {
+ return res.status(404).json({ error: 'File not found' });
+ }
+
+ // Determine sync action
+ let action = 'up-to-date';
+
+ if (!serverFile[0].hash || serverFile[0].hash !== localHash) {
+ // Need to upload
+ action = 'upload';
+ }
+
+ res.json({
+ action,
+ serverModified: serverFile[0].updated_at,
+ serverHash: serverFile[0].hash
+ });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+// Get local files that need syncing
+router.get('/pending', (req, res) => {
+ const token = req.headers.authorization?.replace('Bearer ', '');
+ if (!token) return res.status(401).json({ error: 'No token' });
+
+ try {
+ const jwt = require('jsonwebtoken');
+ const decoded = jwt.verify(token, process.env.JWT_SECRET || 'clouddisk-secret-key');
+
+ // Get files modified since last sync
+ const lastSync = db.query(
+ 'SELECT last_sync_time FROM sync_status WHERE user_id = ?',
+ [decoded.userId]
+ );
+
+ let query;
+ let params;
+
+ if (lastSync.length > 0 && lastSync[0].last_sync_time) {
+ query = 'SELECT * FROM files WHERE user_id = ? AND updated_at > ?';
+ params = [decoded.userId, lastSync[0].last_sync_time];
+ } else {
+ query = 'SELECT * FROM files WHERE user_id = ?';
+ params = [decoded.userId];
+ }
+
+ const files = db.query(query, params);
+
+ res.json({ files });
+ } catch (error) {
+ res.status(500).json({ error: error.message });
+ }
+});
+
+module.exports = router;
diff --git a/frontend/src/renderer/components/SyncStatus.jsx b/frontend/src/renderer/components/SyncStatus.jsx
new file mode 100644
index 0000000..c41e9d8
--- /dev/null
+++ b/frontend/src/renderer/components/SyncStatus.jsx
@@ -0,0 +1,114 @@
+import React, { useState, useEffect } from 'react';
+import { Badge, Button, Progress, Tooltip } from 'antd';
+import { SyncOutlined, CheckCircleOutlined, ClockCircleOutlined } from '@ant-design/icons';
+
+function SyncStatus() {
+ const [status, setStatus] = useState('idle');
+ const [lastSync, setLastSync] = useState(null);
+ const [syncedFiles, setSyncedFiles] = useState(0);
+ const [totalFiles, setTotalFiles] = useState(0);
+ const [syncing, setSyncing] = useState(false);
+
+ useEffect(() => {
+ fetchSyncStatus();
+ // Poll status every 30 seconds
+ const interval = setInterval(fetchSyncStatus, 30000);
+ return () => clearInterval(interval);
+ }, []);
+
+ const fetchSyncStatus = async () => {
+ try {
+ const token = localStorage.getItem('token');
+ const response = await fetch('/api/sync/status', {
+ headers: { 'Authorization': `Bearer ${token}` }
+ });
+ const data = await response.json();
+ setStatus(data.status);
+ setLastSync(data.lastSyncTime);
+ setSyncedFiles(data.syncedFiles);
+ setTotalFiles(data.totalFiles);
+ } catch (error) {
+ console.error('Failed to fetch sync status:', error);
+ }
+ };
+
+ const handleSync = async () => {
+ setSyncing(true);
+ try {
+ const token = localStorage.getItem('token');
+ await fetch('/api/sync/start', {
+ method: 'POST',
+ headers: { 'Authorization': `Bearer ${token}` }
+ });
+ // Start polling for progress
+ const interval = setInterval(async () => {
+ await fetchSyncStatus();
+ if (status === 'idle' || status === 'completed') {
+ setSyncing(false);
+ clearInterval(interval);
+ }
+ }, 2000);
+ } catch (error) {
+ console.error('Failed to start sync:', error);
+ setSyncing(false);
+ }
+ };
+
+ const getStatusBadge = () => {
+ switch (status) {
+ case 'syncing':
+ return