From 4302b9fd313df2bd0f0edbe4186835deffe91254 Mon Sep 17 00:00:00 2001 From: yuany3721 Date: Sun, 28 Dec 2025 23:06:05 +0800 Subject: [PATCH] fix bug & update deployment --- .env.example | 16 +- DEPLOYMENT.md | 103 -- DOCKER_DEPLOY.md | 113 -- Dockerfile | 40 + README.md | 208 ++- backend/.env.example | 13 - backend/Dockerfile | 30 - backend/main.py | 45 +- backend/src/api/files.py | 43 +- backend/src/api/websocket.py | 59 +- backend/src/services/auth_service.py | 24 +- backend/src/services/file_service.py | 143 ++- backend/src/utils/config.py | 49 +- docker-compose.yml | 48 +- docker-entrypoint.sh | 10 + docker/default.conf | 54 + docker/nginx.conf | 44 + docker/supervisord.conf | 22 + docs/brainstorming-session-results.md | 224 ---- docs/front-end-spec.md | 435 ------- docs/fullstack-architecture.md | 1710 ------------------------- docs/prd.md | 399 ------ frontend/Dockerfile | 34 - frontend/favicon.ico | Bin 0 -> 4286 bytes frontend/index.html | 2 +- frontend/nginx.conf | 80 -- frontend/package.json | 2 +- frontend/src/services/api.ts | 30 +- frontend/src/services/websocket.ts | 161 ++- frontend/src/utils/eventBus.ts | 31 + frontend/src/views/EditorView.vue | 82 +- frontend/src/views/FileListView.vue | 52 +- frontend/tsconfig.tsbuildinfo | 1 + frontend/vite.config.ts | 15 +- 34 files changed, 698 insertions(+), 3624 deletions(-) delete mode 100644 DEPLOYMENT.md delete mode 100644 DOCKER_DEPLOY.md create mode 100644 Dockerfile delete mode 100644 backend/.env.example delete mode 100644 backend/Dockerfile create mode 100755 docker-entrypoint.sh create mode 100644 docker/default.conf create mode 100644 docker/nginx.conf create mode 100644 docker/supervisord.conf delete mode 100644 docs/brainstorming-session-results.md delete mode 100644 docs/front-end-spec.md delete mode 100644 docs/fullstack-architecture.md delete mode 100644 docs/prd.md delete mode 100644 frontend/Dockerfile create mode 100644 frontend/favicon.ico delete mode 100644 frontend/nginx.conf create mode 100644 frontend/src/utils/eventBus.ts create mode 100644 frontend/tsconfig.tsbuildinfo diff --git a/.env.example b/.env.example index 4c4d2a2..8f4b1e5 100644 --- a/.env.example +++ b/.env.example @@ -1,14 +1,16 @@ # 应用配置 -APP_NAME=Vue3 Python Notepad +APP_NAME=Notepad # 安全配置 -# 请修改这个密码,建议使用强密码 -FILE_LIST_PASSWORD=your_secure_password_here +FILE_LIST_PASSWORD=your_secure_password_change_this +JWT_SECRET_KEY=your_jwt_secret_key_change_this -# 端口配置 -# 容器对外暴露的端口(修改后需要更新 docker-compose.yml 中的端口映射) -EXTERNAL_PORT=80 +# 容器对外暴露的端口 +PORT=80 # 文件存储配置 # 容器外映射的文件存储目录(相对路径或绝对路径) -FILES_DIR=./files \ No newline at end of file +NOTES_DIR=./data + +# 允许的跨域来源(正则表达式),多个来源用 | 分隔 +# ALLOW_ORIGIN_REGEX= \ No newline at end of file diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md deleted file mode 100644 index 5f2bb35..0000000 --- a/DEPLOYMENT.md +++ /dev/null @@ -1,103 +0,0 @@ -# 生产环境部署指南 - -## 安全配置步骤 - -### 1. 配置环境变量 - -复制环境变量模板并修改敏感信息: - -```bash -# 复制模板文件 -cp .env.example .env - -# 编辑 .env 文件,修改以下敏感配置: -nano .env -``` - -**必须修改的配置:** -- `FILE_LIST_PASSWORD` - 设置强密码(建议使用随机生成的密码) - -**生成强密码的方法:** -```bash -# 生成密码 -openssl rand -base64 32 -``` - -### 2. 文件权限设置 - -```bash -# 创建文件目录(根据 .env 中的 FILES_DIR 配置) -mkdir -p ${FILES_DIR:-./files} - -# 设置适当的权限 -chmod 755 files -``` - -### 3. 部署到服务器 - -```bash -# 克隆仓库 -git clone https://github.com/YOUR_USERNAME/REPO_NAME.git -cd REPO_NAME - -# 配置环境变量 -cp .env.example .env -# 编辑 .env 文件... - -# 构建并启动 -docker-compose up -d --build -``` - -### 4. 验证部署 - -```bash -# 检查服务状态 -docker-compose ps - -# 查看日志 -docker-compose logs -f - -# 测试 API -curl http://localhost/api/health -``` - -## 环境变量说明 - -| 变量名 | 说明 | 默认值 | 是否必须修改 | -|--------|------|--------|------------| -| `APP_NAME` | 应用名称 | Vue3 Python Notepad | 否 | -| `FILE_LIST_PASSWORD` | 文件列表访问密码 | your_secure_password_here | **是** | -| `EXTERNAL_PORT` | 容器对外暴露的端口 | 80 | 否 | -| `FILES_DIR` | 文件存储目录(宿主机路径) | ./files | 否 | - -## 安全建议 - -1. **定期更换密码** - - 建议每 3 个月更换一次 `FILE_LIST_PASSWORD` - -2. **使用 HTTPS** - - 配置反向代理(Nginx/Traefik) - - 申请 SSL 证书(Let's Encrypt) - -4. **备份** - - 定期备份 `FILES_DIR` 配置的目录 - - 备份 `.env` 文件(存储在安全位置) - -4. **监控** - - 监控容器资源使用 - - 设置日志轮转 - - 配置告警机制 - -## 故障排除 - -1. **密码错误** - - 检查 `.env` 文件中的 `FILE_LIST_PASSWORD` - - 重启容器:`docker-compose restart backend` - -2. **认证错误** - - 检查 `.env` 文件中的 `FILE_LIST_PASSWORD` - - 清除浏览器 localStorage 中的 token - -3. **权限问题** - - 确保 `files` 目录有写权限 - - 检查 Docker 容器用户权限 \ No newline at end of file diff --git a/DOCKER_DEPLOY.md b/DOCKER_DEPLOY.md deleted file mode 100644 index 2f23fa8..0000000 --- a/DOCKER_DEPLOY.md +++ /dev/null @@ -1,113 +0,0 @@ -# Vue3 + Python Notepad Docker 部署指南 - -## 快速开始 - -### 前提条件 -- Docker -- Docker Compose - -### 部署步骤 - -1. **克隆项目** -```bash -git clone -cd bmadtest -``` - -2. **构建并启动服务** -```bash -docker-compose up -d --build -``` - -3. **访问应用** -- 前端:http://localhost -- API:http://localhost/api -- WebSocket:ws://localhost/ws - -### 常用命令 - -**查看服务状态** -```bash -docker-compose ps -``` - -**查看日志** -```bash -# 查看所有服务日志 -docker-compose logs - -# 查看特定服务日志 -docker-compose logs backend -docker-compose logs frontend -``` - -**停止服务** -```bash -docker-compose down -``` - -**重新构建并启动** -```bash -docker-compose up -d --build -``` - -### 数据持久化 - -文件数据存储在 `./files/notes/` 目录下。确保在宿主机上备份此目录以防止数据丢失。 - -### 环境变量配置 - -如需自定义配置,可以在 `docker-compose.yml` 中修改环境变量: - -```yaml -environment: - - PYTHONPATH=/app - - PYTHONUNBUFFERED=1 - # 其他环境变量... -``` - -### 端口配置 - -默认端口配置: -- 前端:80 -- 后端:8000(容器内部) - -如需修改前端端口,编辑 `docker-compose.yml` 中的 ports 配置: -```yaml -ports: - - "8080:80" # 将宿主机的 8080 端口映射到容器的 80 端口 -``` - -### 故障排除 - -1. **端口被占用** - - 修改 docker-compose.yml 中的端口映射 - - 或停止占用端口的服务 - -2. **权限问题** - - 确保 `./files` 目录有适当的写权限 - - 运行:`chmod -R 755 ./files` - -3. **构建失败** - - 清理 Docker 缓存:`docker system prune -a` - - 重新构建:`docker-compose build --no-cache` - -### 生产环境建议 - -1. **使用 HTTPS** - - 配置 SSL 证书 - - 使用反向代理(如 Traefik 或 Nginx) - -2. **安全配置** - - 修改默认密码 - - 限制 API 访问频率 - - 使用防火墙 - -3. **监控** - - 配置日志收集 - - 设置健康检查 - - 监控资源使用情况 - -4. **备份** - - 定期备份 `./files` 目录 - - 考虑使用云存储 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c5de464 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +FROM node:18-alpine AS frontend-build +WORKDIR /app/frontend +COPY frontend/package*.json ./ +COPY frontend/pnpm-lock.yaml ./ +RUN npm install -g pnpm +RUN pnpm install --frozen-lockfile +COPY frontend/ ./ +ARG APP_NAME=Notepad +RUN VITE_API_BASE_URL=/api VITE_WS_HOST= VITE_APP_TITLE=$APP_NAME pnpm build + +FROM python:3.12-slim AS backend-prep +WORKDIR /app +RUN apt-get update && apt-get install -y \ + nginx \ + supervisor \ + && rm -rf /var/lib/apt/lists/* +COPY backend/requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +FROM python:3.12-slim +WORKDIR /app +RUN apt-get update && apt-get install -y \ + nginx \ + supervisor \ + curl \ + && rm -rf /var/lib/apt/lists/* +COPY --from=backend-prep /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages +COPY --from=backend-prep /usr/local/bin /usr/local/bin +COPY backend/ ./ +COPY --from=frontend-build /app/frontend/dist ./frontend/dist +RUN mkdir -p /app/data/notes /var/log/supervisor /var/log/nginx +COPY docker/nginx.conf /etc/nginx/nginx.conf +COPY docker/default.conf /etc/nginx/sites-available/default +COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf +COPY docker-entrypoint.sh /app/docker-entrypoint.sh +RUN chmod +x /app/docker-entrypoint.sh +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:${PORT:-80}/notepad/health || exit 1 +EXPOSE ${PORT:-80} +CMD ["/app/docker-entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md index 0432534..0dc88f1 100644 --- a/README.md +++ b/README.md @@ -9,203 +9,153 @@ - ✅ 文件列表管理(需口令验证) - ✅ WebSocket实时通信 - ✅ 响应式设计 -- ✅ 文件大小限制(10万字符) +- ✅ 文件大小限制(100KB) - ✅ 文件名验证与安全过滤 +- ✅ 统一容器部署 ## 技术栈 - **前端**: Vue 3, TypeScript, Vite, Pinia, Vue Router -- **后端**: Python, FastAPI, WebSocket +- **后端**: Python 3.12, FastAPI, WebSocket +- **部署**: Docker, Nginx, Supervisor ## 快速开始 -### 前置要求 - -- Node.js 18+ -- Python 3.10+ -- pnpm 8+ - -### 安装步骤 +### Docker 部署(推荐) 1. **克隆项目** ```bash git clone -cd vue-python-notepad +cd notepad ``` -2. **安装依赖** +2. **配置环境变量** ```bash -# 安装前端依赖 +cp .env.example .env +``` + +3. **启动服务** +```bash +docker compose up -d +``` + +4. **访问应用** +- 应用地址: http://localhost:7024 +- 默认密码: `your_secure_password_change_this` + +### 开发环境 + +1. **安装依赖** +```bash +# 前端 cd frontend && pnpm install && cd .. -# 设置Python虚拟环境并安装后端依赖 +# 后端 cd backend python -m venv venv -# Windows -venv\Scripts\activate -# macOS/Linux -source venv/bin/activate +source venv/bin/activate # Linux/Mac +# 或 venv\Scripts\activate # Windows pip install -r requirements.txt cd .. ``` -3. **配置环境变量** +2. **启动服务** ```bash -# 复制环境变量模板 -cp backend/.env.example .env +# 后端 +cd backend && source venv/bin/activate && python main.py -# 编辑.env文件,设置必要的环境变量 +# 前端(新终端) +cd frontend && pnpm dev ``` -4. **启动开发服务器** - -**方法一:分别启动** -```bash -# 启动后端服务器(终端1) -cd backend -venv\Scripts\activate # Windows -python main.py - -# 启动前端开发服务器(终端2) -cd frontend -pnpm dev -``` - -**方法二:同时启动** -```bash -# 在项目根目录 -pnpm dev -``` - -5. **访问应用** -- 前端: http://localhost:5173 -- 后端API: http://localhost:8000 -- API文档: http://localhost:8000/docs - ## 使用说明 ### 基本操作 1. **创建/编辑文件** - - 访问 `http://localhost:5173/notes/filename.txt` - - 如果文件不存在,会自动创建 - - 输入内容会自动保存(500ms延迟) + - 访问 `http://localhost:7024/notes/filename.txt` + - 文件不存在时自动创建 + - 内容自动保存 -2. **文件列表** - - 访问 `http://localhost:5173/file-list` - - 输入口令(默认:your_secure_password) - - 查看和管理所有文件 +2. **文件列表管理** + - 访问 `http://localhost:7024/file-list` + - 输入口令验证 + - 查看、重命名、删除文件 3. **URL分享** - - 直接分享文件URL给他人 - - 例如:`http://localhost:5173/notes/shared-note.txt` + - 直接分享文件URL + - 例如:`http://localhost:7024/notes/shared-note.txt` ### 环境变量配置 ```bash -# 前端环境变量 -VITE_API_BASE_URL=http://localhost:8000/api -VITE_WS_BASE_URL=ws://localhost:8000/ws +# 应用配置 +PORT=7024 # 容器对外端口 +FILE_LIST_PASSWORD=your_password # 文件列表访问密码 -# 后端环境变量 -NOTES_DIR=./data/notes # 文件存储目录 -FILE_LIST_PASSWORD=your_secure_password # 文件列表访问口令 -JWT_SECRET_KEY=your_jwt_secret_key # JWT密钥 -JWT_EXPIRE_HOURS=24 # 令牌过期时间 +# 文件存储 +NOTES_DIR=./data # 容器外映射存储路径 ``` ## 项目结构 ``` -vue-python-notepad/ -├── frontend/ # Vue3前端应用 +notepad/ +├── frontend/ # Vue3前端 │ ├── src/ -│ │ ├── components/ # UI组件 │ │ ├── views/ # 页面组件 -│ │ ├── stores/ # 状态管理 │ │ ├── services/ # API服务 -│ │ └── router/ # 路由配置 -│ ├── package.json -│ └── vite.config.ts -├── backend/ # FastAPI后端应用 +│ │ └── stores/ # 状态管理 +│ └── package.json +├── backend/ # FastAPI后端 │ ├── src/ │ │ ├── api/ # API路由 │ │ ├── services/ # 业务逻辑 -│ │ ├── models/ # 数据模型 │ │ └── utils/ # 工具函数 -│ ├── main.py │ └── requirements.txt -├── docs/ # 项目文档 -├── package.json # 根配置文件 +├── docker/ # Docker配置 +│ ├── nginx.conf +│ ├── default.conf +│ └── supervisord.conf +├── docker-compose.yml # 容器编排 +├── Dockerfile # 统一构建文件 └── README.md ``` -## 开发指南 +## 部署架构 -### 添加新功能 +### 容器架构 +- **Nginx**: 静态文件服务 + API代理 + WebSocket代理 +- **Python FastAPI**: API服务 + WebSocket服务 +- **Supervisor**: 进程管理 -1. **前端组件** - - 在 `frontend/src/components/` 中创建组件 - - 在 `frontend/src/views/` 中创建页面 - - 使用 Pinia 管理状态 - -2. **后端API** - - 在 `backend/src/api/` 中添加路由 - - 在 `backend/src/services/` 中实现业务逻辑 - - 在 `backend/src/models/` 中定义数据模型 - -### 测试 - -```bash -# 运行前端测试 -pnpm test:frontend - -# 运行后端测试 -pnpm test:backend - -# 运行所有测试 -pnpm test +### 网络流程 ``` - -### 构建部署 - -```bash -# 构建前端 -pnpm build - -# 生产环境启动 -cd backend -venv\Scripts\activate # Windows -python main.py +用户请求 → Nginx(80) → 后端(7025) + ↓ + 前端静态文件 ``` ## 故障排除 ### 常见问题 -1. **端口冲突** - - 确保8000和5173端口未被占用 - - 或修改配置文件中的端口设置 +1. **容器启动失败** + ```bash + docker compose down + docker compose up -d --build + ``` -2. **Python依赖问题** - - 确保使用Python 3.10+ - - 激活虚拟环境后安装依赖 +2. **文件不持久化** + - 检查 `.env` 中的 `NOTES_DIR` 配置 + - 确认数据目录权限 -3. **前端代理错误** - - 检查 `frontend/vite.config.ts` 中的代理配置 - - 确保后端服务器正在运行 +3. **WebSocket连接失败** + - 检查 Nginx 配置 + - 确认防火墙设置 -4. **WebSocket连接失败** - - 检查防火墙设置 - - 确认WebSocket URL正确 - -## 贡献指南 - -1. Fork 项目 -2. 创建功能分支 -3. 提交更改 -4. 推送到分支 -5. 创建 Pull Request +4. **端口冲突** + - 修改 `.env` 中的 `PORT` 变量 ## 许可证 diff --git a/backend/.env.example b/backend/.env.example deleted file mode 100644 index 90f1d2d..0000000 --- a/backend/.env.example +++ /dev/null @@ -1,13 +0,0 @@ -# 文件存储配置 -NOTES_DIR=./data/notes - -# 认证配置 - 请在生产环境中更改这些值 -FILE_LIST_PASSWORD=your_secure_password_change_this -JWT_SECRET_KEY=your_jwt_secret_key_change_this_in_production -JWT_ALGORITHM=HS256 -JWT_EXPIRE_HOURS=24 - -# 服务器配置 -HOST=0.0.0.0 -PORT=8000 -DEBUG=true \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile deleted file mode 100644 index 16344ea..0000000 --- a/backend/Dockerfile +++ /dev/null @@ -1,30 +0,0 @@ -FROM python:3.11-slim - -WORKDIR /app - -# 安装系统依赖 -RUN apt-get update && apt-get install -y \ - gcc \ - && rm -rf /var/lib/apt/lists/* - -# 复制依赖文件 -COPY requirements.txt . - -# 安装 Python 依赖 -RUN pip install --no-cache-dir -r requirements.txt - -# 复制源代码 -COPY . . - -# 创建数据目录 -RUN mkdir -p data/notes - -# 暴露端口 -EXPOSE 8000 - -# 设置环境变量 -ENV PYTHONPATH=/app -ENV PYTHONUNBUFFERED=1 - -# 启动命令 -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index 5d66c6d..d792c09 100644 --- a/backend/main.py +++ b/backend/main.py @@ -7,41 +7,34 @@ import os from src.api import notes, files, websocket from src.utils.config import get_settings -app = FastAPI( - title="Vue3 + Python Notepad API", - description="简单的文本编辑器API,支持文件读写和列表功能", - version="1.0.0" -) +settings = get_settings() +app = FastAPI(title=f"{settings.app_name} API", description="NOTEPAD API", version="1.0.0") -# 配置CORS app.add_middleware( CORSMiddleware, - allow_origins=["http://localhost:5173", "ws://localhost:5173"], # Vue开发服务器 + allow_origin_regex=( + settings.allow_origin_regex + if settings.allow_origin_regex + else r"http[s]?://localhost:5173|http[s]?://127\.0\.0\.1:5173|http[s]?://172\.30\.0\..*:5173|ws[s]?://localhost:5173|ws[s]?://127\.0\.0\.1:5173" + ), allow_credentials=True, - allow_methods=["*"], + allow_methods=["GET", "POST"], allow_headers=["*"], ) -# 注册路由 -app.include_router(notes.router, prefix="/api", tags=["notes"]) -app.include_router(files.router, prefix="/api", tags=["files"]) -app.include_router(websocket.router, prefix="/ws", tags=["websocket"]) +app.include_router(notes.router, prefix="/notepad", tags=["notes"]) +app.include_router(files.router, prefix="/notepad", tags=["files"]) +app.include_router(websocket.router, prefix="/notepad/ws", tags=["websocket"]) + + +@app.get("/notepad/health") +async def health_check(): + return {"status": "healthy"} + -# 静态文件服务(用于生产环境) if os.path.exists("./static"): app.mount("/", StaticFiles(directory="./static", html=True), name="static") -@app.get("/health") -async def health_check(): - """健康检查端点""" - return {"status": "healthy"} - if __name__ == "__main__": - settings = get_settings() - uvicorn.run( - "main:app", - host="0.0.0.0", - port=8000, - reload=True, - log_level="info" - ) \ No newline at end of file + port = int(os.getenv("PORT", 7024)) + uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True, log_level="info") diff --git a/backend/src/api/files.py b/backend/src/api/files.py index 483b0f0..895d468 100644 --- a/backend/src/api/files.py +++ b/backend/src/api/files.py @@ -9,18 +9,17 @@ file_service = FileService() auth_service = AuthService() security = HTTPBearer() + async def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)): """验证JWT令牌""" token = credentials.credentials - + if not auth_service.validate_token(token): - raise HTTPException( - status_code=403, - detail="Invalid or expired token" - ) - + raise HTTPException(status_code=403, detail="Invalid or expired token") + return token + @router.post("/verify-password", response_model=AuthToken) async def verify_password(request: PasswordRequest): """验证访问口令""" @@ -32,32 +31,28 @@ async def verify_password(request: PasswordRequest): except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @router.get("/list", response_model=FileListResponse, dependencies=[Depends(verify_token)]) async def get_files_list( - page: int = Query(1, ge=1, description="页码"), - limit: int = Query(50, ge=1, le=100, description="每页文件数") + page: int = Query(1, ge=1, description="页码"), limit: int = Query(50, ge=1, le=100, description="每页文件数") ): """获取文件列表(需要认证)""" try: files = file_service.list_files(page, limit) total = file_service.get_total_files_count() - - return FileListResponse( - files=files, - total=total, - page=page, - limit=limit - ) + + return FileListResponse(files=files, total=total, page=page, limit=limit) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) -@router.delete("/{filename}") + +@router.post("/{filename}/delete") async def delete_file(filename: str): """删除文件""" try: if not file_service.file_exists(filename): raise HTTPException(status_code=404, detail="File not found") - + file_service.delete_file(filename) return {"message": f"File {filename} deleted successfully"} except FileNotFoundError: @@ -65,16 +60,20 @@ async def delete_file(filename: str): except Exception as e: raise HTTPException(status_code=500, detail=str(e)) -@router.put("/{filename}/rename") + +@router.post("/{filename}/rename") async def rename_file(filename: str, new_filename: str = Query(..., description="新文件名")): """重命名文件""" try: + if not file_service._validate_filename(new_filename): + raise HTTPException(status_code=400, detail="Invalid new filename") + if not file_service.file_exists(filename): - raise HTTPException(status_code=404, detail="File not found") - + return {"message": f"File renamed to {new_filename}"} + if file_service.file_exists(new_filename): raise HTTPException(status_code=400, detail="File with new name already exists") - + file_service.rename_file(filename, new_filename) return {"message": f"File renamed from {filename} to {new_filename}"} except FileNotFoundError: @@ -82,4 +81,4 @@ async def rename_file(filename: str, new_filename: str = Query(..., description= except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/src/api/websocket.py b/backend/src/api/websocket.py index fdad466..5701ebc 100644 --- a/backend/src/api/websocket.py +++ b/backend/src/api/websocket.py @@ -7,18 +7,18 @@ from ..services.file_service import FileService router = APIRouter() file_service = FileService() + class ConnectionManager: def __init__(self): self.active_connections: Dict[str, WebSocket] = {} - + async def connect(self, websocket: WebSocket, filename: str): - await websocket.accept() self.active_connections[filename] = websocket - + def disconnect(self, filename: str): if filename in self.active_connections: del self.active_connections[filename] - + async def send_message(self, message: dict, filename: str): if filename in self.active_connections: websocket = self.active_connections[filename] @@ -28,74 +28,73 @@ class ConnectionManager: # 连接已断开,清理 self.disconnect(filename) + manager = ConnectionManager() + @router.websocket("/{filename}") async def websocket_endpoint(websocket: WebSocket, filename: str): - print(f"WebSocket connection attempt for file: {filename}") + # print(f"WebSocket connection attempt for file: {filename}") + # print(f"WebSocket headers: {websocket.headers}") try: + await websocket.accept() await manager.connect(websocket, filename) - print(f"WebSocket connected for file: {filename}") + # print(f"WebSocket connected for file: {filename}") except Exception as e: - print(f"WebSocket connection failed: {e}") - await websocket.close(code=1006) - + # print(f"WebSocket connection failed: {e}") + # print(f"Exception type: {type(e)}") + try: + await websocket.close(code=1006) + except: + pass + return + try: while True: - # 接收客户端消息 data = await websocket.receive_text() message = json.loads(data) - - # 处理保存请求 + if message.get("type") == "save": try: content = message.get("content", "") - - # 检查内容是否为空 + if not content or content.strip() == "": - # 删除空文件 if file_service.file_exists(filename): file_service.delete_file(filename) response = { "type": "save_response", "status": "success", "message": "空文件已删除", - "timestamp": message.get("timestamp") + "timestamp": message.get("timestamp"), } else: response = { "type": "save_response", "status": "success", "message": "文件不存在", - "timestamp": message.get("timestamp") + "timestamp": message.get("timestamp"), } else: - # 保存非空文件 file_service.write_file(filename, content) response = { "type": "save_response", "status": "success", "message": "保存成功", - "timestamp": message.get("timestamp") + "timestamp": message.get("timestamp"), } except Exception as e: - # 发送失败响应 response = { - "type": "save_response", + "type": "save_response", "status": "error", "message": str(e), - "timestamp": message.get("timestamp") + "timestamp": message.get("timestamp"), } - + await manager.send_message(response, filename) - + except WebSocketDisconnect: manager.disconnect(filename) except Exception as e: - # 发送错误响应 - error_response = { - "type": "error", - "message": str(e) - } + error_response = {"type": "error", "message": str(e)} await manager.send_message(error_response, filename) - manager.disconnect(filename) \ No newline at end of file + manager.disconnect(filename) diff --git a/backend/src/services/auth_service.py b/backend/src/services/auth_service.py index 7eac5c3..527aeed 100644 --- a/backend/src/services/auth_service.py +++ b/backend/src/services/auth_service.py @@ -4,29 +4,24 @@ import jwt from ..models.text_file import AuthToken from ..utils.config import get_settings + class AuthService: def __init__(self): self.settings = get_settings() def verify_password(self, password: str) -> AuthToken: - """验证口令并生成令牌""" if password != self.settings.file_list_password: raise ValueError("Invalid password") - # 生成JWT令牌 - expires_delta = timedelta(hours=self.settings.jwt_expire_hours) + expires_delta = timedelta(minutes=self.settings.jwt_expire_minutes) expires_at = datetime.utcnow() + expires_delta - to_encode = { + payload = { "exp": expires_at, "sub": "file_list_access" } - token = jwt.encode( - to_encode, - self.settings.jwt_secret_key, - algorithm=self.settings.jwt_algorithm - ) + token = jwt.encode(payload, self.settings.jwt_secret_key, algorithm="HS256") return AuthToken( token=token, @@ -34,13 +29,8 @@ class AuthService: ) def validate_token(self, token: str) -> bool: - """验证JWT令牌""" try: - jwt.decode( - token, - self.settings.jwt_secret_key, - algorithms=[self.settings.jwt_algorithm] - ) + jwt.decode(token, self.settings.jwt_secret_key, algorithms=["HS256"]) return True - except jwt.PyJWTError: - return False \ No newline at end of file + except Exception: + return False diff --git a/backend/src/services/file_service.py b/backend/src/services/file_service.py index 8932d10..123ea1f 100644 --- a/backend/src/services/file_service.py +++ b/backend/src/services/file_service.py @@ -6,113 +6,113 @@ from pathlib import Path from ..models.text_file import TextFile, FileListItem from ..utils.config import get_settings + class FileService: def __init__(self): self.settings = get_settings() - self.notes_dir = Path(self.settings.notes_dir) + self.notes_dir = Path(self.settings.notes_path) self.notes_dir.mkdir(parents=True, exist_ok=True) - + def _validate_filename(self, filename: str) -> bool: - """验证文件名是否安全""" - # 检查文件名长度 if len(filename) > 200 or len(filename) < 1: return False - - # 检查文件名是否包含非法字符 - illegal_chars = r'[<>:"/\\|?*\x00-\x1f]' - if re.search(illegal_chars, filename): + + illegal_chars = r'[^a-zA-Z0-9_.-]' + if ( + re.search(illegal_chars, filename) + or filename.startswith("_") + or filename.startswith(".") + or filename.startswith("-") + ): return False - - # 检查路径遍历攻击 + if '..' in filename or filename.startswith('/'): return False - - # 确保文件扩展名为.txt - if not filename.lower().endswith('.txt'): - return False - + return True - + + def _add_txt_extension(self, filename: str) -> str: + if not filename.lower().endswith('.txt'): + return f"{filename}.txt" + return filename + def read_file(self, filename: str) -> TextFile: - """读取文件内容""" if not self._validate_filename(filename): raise ValueError(f"Invalid filename: {filename}") - + + filename = self._add_txt_extension(filename) file_path = self.notes_dir / filename - + if not file_path.exists(): raise FileNotFoundError(f"File {filename} not found") - + try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() - + stat = file_path.stat() return TextFile( filename=filename, content=content, created_at=datetime.fromtimestamp(stat.st_ctime), updated_at=datetime.fromtimestamp(stat.st_mtime), - size=len(content) + size=len(content), ) except UnicodeDecodeError: raise ValueError(f"File {filename} is not a valid text file") except Exception as e: raise RuntimeError(f"Error reading file {filename}: {str(e)}") - + def write_file(self, filename: str, content: str) -> TextFile: - """写入文件内容""" if not self._validate_filename(filename): raise ValueError(f"Invalid filename: {filename}") - + if content is None: raise ValueError("Content cannot be None") - + if len(content) > 100000: raise ValueError("File content too large (max 100,000 characters)") - + + filename = self._add_txt_extension(filename) file_path = self.notes_dir / filename - + try: with open(file_path, 'w', encoding='utf-8') as f: f.write(content) - + stat = file_path.stat() return TextFile( filename=filename, content=content, created_at=datetime.fromtimestamp(stat.st_ctime), updated_at=datetime.fromtimestamp(stat.st_mtime), - size=len(content) + size=len(content), ) except Exception as e: raise RuntimeError(f"Error writing file {filename}: {str(e)}") - + def create_file(self, filename: str) -> TextFile: """创建新文件""" return self.write_file(filename, "") - + def list_files(self, page: int = 1, limit: int = 50) -> List[FileListItem]: - """列出文件""" if page < 1: page = 1 - + if limit < 1 or limit > 100: limit = 50 - + files = [] - + try: for file_path in self.notes_dir.glob("*.txt"): stat = file_path.stat() - - # 读取文件前100个字符作为预览 + preview = "" if stat.st_size > 0: try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read(100) - # 清理预览内容,移除换行符 preview = content.replace('\n', ' ').replace('\r', '').strip() if len(preview) > 50: preview = preview[:50] + "..." @@ -120,75 +120,76 @@ class FileService: preview = "[非文本文件]" except Exception: preview = "" - - files.append(FileListItem( - filename=file_path.name, - created_at=datetime.fromtimestamp(stat.st_ctime), - size=stat.st_size, - preview=preview - )) + + files.append( + FileListItem( + filename=file_path.name, + created_at=datetime.fromtimestamp(stat.st_ctime), + size=stat.st_size, + preview=preview, + ) + ) except Exception as e: raise RuntimeError(f"Error listing files: {str(e)}") - - # 按创建时间倒序排列 + files.sort(key=lambda x: x.created_at, reverse=True) - - # 分页 + start = (page - 1) * limit end = start + limit - + return files[start:end] - + def get_total_files_count(self) -> int: - """获取文件总数""" try: return len(list(self.notes_dir.glob("*.txt"))) except Exception: return 0 - + def file_exists(self, filename: str) -> bool: - """检查文件是否存在""" if not self._validate_filename(filename): return False - + + filename = self._add_txt_extension(filename) file_path = self.notes_dir / filename return file_path.exists() and file_path.is_file() - + def delete_file(self, filename: str) -> bool: - """删除文件""" if not self._validate_filename(filename): raise ValueError(f"Invalid filename: {filename}") - + + filename = self._add_txt_extension(filename) file_path = self.notes_dir / filename - + if not file_path.exists(): - raise FileNotFoundError(f"File {filename} not found") - + return True # 文件已不存在 + try: file_path.unlink() return True except Exception as e: raise RuntimeError(f"Error deleting file {filename}: {str(e)}") - + def rename_file(self, old_filename: str, new_filename: str) -> bool: - """重命名文件""" if not self._validate_filename(old_filename): raise ValueError(f"Invalid old filename: {old_filename}") - + if not self._validate_filename(new_filename): raise ValueError(f"Invalid new filename: {new_filename}") - + + old_filename = self._add_txt_extension(old_filename) + new_filename = self._add_txt_extension(new_filename) + old_path = self.notes_dir / old_filename new_path = self.notes_dir / new_filename - + if not old_path.exists(): - raise FileNotFoundError(f"File {old_filename} not found") - + return True + if new_path.exists(): raise ValueError(f"File {new_filename} already exists") - + try: old_path.rename(new_path) return True except Exception as e: - raise RuntimeError(f"Error renaming file {old_filename}: {str(e)}") \ No newline at end of file + raise RuntimeError(f"Error renaming file {old_filename}: {str(e)}") diff --git a/backend/src/utils/config.py b/backend/src/utils/config.py index 5abde72..a98798c 100644 --- a/backend/src/utils/config.py +++ b/backend/src/utils/config.py @@ -1,41 +1,30 @@ from pydantic_settings import BaseSettings +from pydantic import ConfigDict from pathlib import Path import os -class Settings(BaseSettings): - """应用配置""" - - # 基础配置 - app_name: str = os.getenv("APP_NAME", "Vue3 + Python Notepad") - - # 文件存储配置 - notes_dir: str = "/app/data" - - # 安全配置 - file_list_password: str = os.getenv("FILE_LIST_PASSWORD", "admin123") - - # JWT配置(使用固定值,简化配置) - jwt_secret_key: str = "default-secret-key-change-in-production" - jwt_algorithm: str = "HS256" - jwt_expire_minutes: int = 1440 # 24小时 - - # 文件限制(使用固定值,简化配置) - max_file_size: int = 100000 # 100KB - max_filename_length: int = 200 - - class Config: - env_file = ".env" - env_file_encoding = "utf-8" -# 创建全局设置实例 +class Settings(BaseSettings): + app_name: str = os.getenv("APP_NAME", "Notepad") + notes_path: str = "/app/data/notes" + allow_origin_regex: str = os.getenv("ALLOW_ORIGIN_REGEX", "") + file_list_password: str = os.getenv("FILE_LIST_PASSWORD", "admin123") + jwt_secret_key: str = os.getenv("JWT_SECRET_KEY", "default-secret-key-change-in-production") + jwt_algorithm: str = "HS256" + jwt_expire_minutes: int = 1440 + max_file_size: int = 100000 + max_filename_length: int = 200 + + settings = Settings() + def get_settings() -> Settings: return settings -# 确保数据目录存在 -def ensure_data_directory(): - os.makedirs(settings.notes_dir, exist_ok=True) -# 在导入时创建目录 -ensure_data_directory() \ No newline at end of file +def ensure_data_directory(): + os.makedirs(settings.notes_path, exist_ok=True) + + +ensure_data_directory() diff --git a/docker-compose.yml b/docker-compose.yml index ca6b488..2610c21 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,38 +1,20 @@ -version: '3.8' - services: - backend: - build: - context: ./backend - dockerfile: Dockerfile - container_name: notepad-backend - restart: unless-stopped - volumes: - - ${FILES_DIR:-./files}:/app/data - env_file: - - .env - networks: - - notepad-network - - frontend: - build: - context: ./frontend - dockerfile: Dockerfile - container_name: notepad-frontend + notepad: + build: + context: . + args: + - APP_NAME=${APP_NAME:-Notepad} + container_name: notepad restart: unless-stopped ports: - - "${EXTERNAL_PORT:-80}:80" - depends_on: - - backend + - "${PORT:-80}:80" env_file: - .env - networks: - - notepad-network - -networks: - notepad-network: - driver: bridge - -volumes: - notepad-data: - driver: local \ No newline at end of file + volumes: + - ${NOTES_DIR:-./data}:/app/data/notes + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:${PORT:-80}/notepad/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s \ No newline at end of file diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..58f168d --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +export BACKEND_PORT=${BACKEND_PORT:-7025} +export NOTES_DIR=${NOTES_DIR:-./data} + +mkdir -p /app/data/notes +chown -R www-data:www-data /app/data/notes +chown -R www-data:www-data /app/frontend/dist + +exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf \ No newline at end of file diff --git a/docker/default.conf b/docker/default.conf new file mode 100644 index 0000000..6ffa64e --- /dev/null +++ b/docker/default.conf @@ -0,0 +1,54 @@ +server { + listen 80; + server_name localhost; + root /app/frontend/dist; + index index.html; + + # 处理前端路由 + location / { + try_files $uri $uri/ /index.html; + } + + # 代理 API 请求到后端 + location /api/ { + proxy_pass http://localhost:7025/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # 代理 WebSocket 请求到后端(必须放在/notepad之前) + location /notepad/ws { + proxy_pass http://localhost:7025; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400; + proxy_send_timeout 86400; + proxy_buffering off; + } + + # 代理 /notepad 路径到后端 + location /notepad { + proxy_pass http://localhost:7025; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_cache_bypass $http_upgrade; + } + + # 静态资源缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} \ No newline at end of file diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..4c8df31 --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,44 @@ +user www-data; +worker_processes auto; +pid /run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # 日志格式 + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + error_log /var/log/nginx/error.log warn; + + # 基本设置 + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + # Gzip 压缩 + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/xml+rss + application/json; + + include /etc/nginx/conf.d/*.conf; + include /etc/nginx/sites-enabled/*; +} \ No newline at end of file diff --git a/docker/supervisord.conf b/docker/supervisord.conf new file mode 100644 index 0000000..56b185f --- /dev/null +++ b/docker/supervisord.conf @@ -0,0 +1,22 @@ +[supervisord] +nodaemon=true +user=root +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid +childlogdir=/var/log/supervisor + +[program:nginx] +command=nginx -g "daemon off;" +stdout_logfile=/var/log/supervisor/nginx.log +stderr_logfile=/var/log/supervisor/nginx.log +autorestart=true +priority=10 + +[program:backend] +command=python3 main.py +directory=/app +stdout_logfile=/var/log/supervisor/backend.log +stderr_logfile=/var/log/supervisor/backend.log +autorestart=true +priority=20 +environment=PORT=7025 \ No newline at end of file diff --git a/docs/brainstorming-session-results.md b/docs/brainstorming-session-results.md deleted file mode 100644 index 42b1342..0000000 --- a/docs/brainstorming-session-results.md +++ /dev/null @@ -1,224 +0,0 @@ -# Vue3 + Python Notepad 应用 - Brainstorming 会话记录 - -**会话主题**: Vue3 + Python后端简单Notepad应用 -**会话日期**: 2025-12-18 -**目标**: 为核心功能 + 自动保存 + 文件列表页面生成创意 - ---- - -## 1. Question Storming (问题风暴) - -**技术说明**: 先生成关于需求、实现、用户体验的所有问题,而不是直接寻找答案。 - -**生成的问题列表**: -- 用户打开页面时,应该看到什么样的空白文档? -- 自动保存的频率应该是多少? -- 如果用户同时打开多个标签页编辑同一个文件怎么办? -- txt文件应该存储在什么目录结构? -- URL中的文件名应该支持中文吗? -- 文件列表页面的口令应该如何安全验证? -- 文件大小限制10万字符是否合理? -- 如何处理文件名冲突? -- 自动保存失败时如何通知用户? -- 是否需要支持文件删除功能? - -## 2. First Principles Thinking (第一性原理思维) - -**技术说明**: 将应用拆解到最基本的、不可再分的核心要素,然后从底层重新构建解决方案。 - -**核心功能拆解**: - -### 高层次功能要素(简化视角): - -基于用户的洞察,我们将技术实现细节抽象化,聚焦于核心功能需求: - -**1. 实时文档编辑与持久化** -- 用户在浏览器中编辑文本 -- 内容自动保存到服务器文件系统 -- 使用WebSocket或类似技术实现实时同步 -- 框架负责处理编辑、存储、保存的细节 - -**2. URL驱动的文档访问** -- URL路径映射到特定文件 -- 文件不存在时自动创建 -- 文件存在时加载内容 -- 支持通过URL分享文档 - -**3. 文件列表与管理** -- 展示所有txt文件列表 -- 后端口令验证保护 -- 点击文件进入编辑 -- 可选的文件操作(删除、重命名) - -**关键洞察**: -- 使用现代框架(Vue3 + Python后端)可以简化文档编辑、存储、自动保存的实现 -- WebSocket或HTTP长轮询可以实现实时自动保存 -- 不需要关心底层细节,框架会处理光标、字符存储、变更检测等 -- 聚焦于功能需求而非技术实现细节 - -**要素之间的依赖关系**: -- 文档编辑 ↔ 自动保存:实时同步机制 -- URL访问 ↔ 文件存储:文件名映射和存在性检查 -- 文件列表 ↔ 文件存储:目录遍历和文件操作 - -## 3. What If Scenarios (假设场景分析) - -**技术说明**: 探索边界情况、潜在问题和异常场景,提前识别风险。 - -### 需要优先处理的场景: - -#### 文件相关场景 -- **What if**: 用户访问的URL文件名包含特殊字符(如 `/`, `\\`, `..`)? -- **What if**: 两个用户同时创建同名文件(同一用户可能开多个标签)? -- **What if**: 文件达到10万字符限制后继续输入? -- **What if**: 文件在编辑过程中被外部删除或修改? -- **What if**: 磁盘空间不足,无法保存? - -#### 自动保存场景 -- **What if**: 自动保存过程中网络断开? -- **What if**: 用户快速连续输入,触发频繁保存? -- **What if**: 保存失败(权限问题、磁盘满)? - -#### URL访问场景 -- **What if**: 用户输入了不存在的文件名格式(如包含空格、中文)? -- **What if**: URL中的文件名与现有文件大小写不同(如 `Note.txt` vs `note.txt`)? -- **What if**: 用户直接访问文件列表页面而不输入口令? - -#### 安全性场景 -- **What if**: 有人暴力破解文件列表页面的口令? -- **What if**: 用户尝试访问系统文件(如 `/etc/passwd`)? - -#### 用户体验场景 -- **What if**: 用户意外关闭浏览器标签页? -- **What if**: 文件列表很长(几百个文件)? - -### 可忽略的场景(用户确认): -- ❌ 用户想找回之前编辑的内容(版本历史) -- ❌ 用户想重命名或删除文件 -- ❌ 用户想手动保存并看到保存确认 - -### 优先级评估: -**高优先级**: 特殊字符处理、文件大小限制、网络断开、安全访问控制 -**中优先级**: 大小写敏感、文件列表过长、快速输入触发频率 -**低优先级**: 磁盘空间不足、文件被外部修改(可接受简单错误提示) - -## 4. Mind Mapping (功能思维导图) - -**技术说明**: 将所有想法组织成清晰的功能结构图,展示功能之间的关系和层次。 - -``` -Notepad应用 (核心目标:简单、实时、URL驱动) -│ -├── 核心功能模块 -│ │ -│ ├── 文档编辑模块 -│ │ ├── 文本输入/显示 (Vue3组件) -│ │ ├── 实时内容同步 (WebSocket/HTTP) -│ │ └── 编辑状态检测 (变更检测) -│ │ -│ ├── 自动保存模块 -│ │ ├── 触发机制 (定时器 + 内容变更) -│ │ ├── 文件写入 (Python后端) -│ │ └── 失败处理 (错误提示) -│ │ -│ └── URL路由模块 -│ ├── 路径解析 (提取文件名) -│ ├── 文件存在检查 -│ ├── 文件加载 (存在时) -│ └── 文件创建 (不存在时) -│ -├── 文件管理模块 -│ │ -│ ├── 文件列表页面 -│ │ ├── 目录遍历 (读取所有txt文件) -│ │ ├── 列表展示 (文件名、创建时间) -│ │ └── 点击跳转 (URL链接) -│ │ -│ └── 安全控制 -│ ├── 后端口令验证 (POST请求) -│ ├── 会话管理 (简单令牌) -│ └── 失败处理 (拒绝访问) -│ -├── 边界处理模块 -│ │ -│ ├── 输入验证 -│ │ ├── 特殊字符过滤 (/、\\、..) -│ │ ├── 文件名长度限制 -│ │ └── URL编码处理 -│ │ -│ ├── 文件限制 -│ │ ├── 大小限制 (10万字符) -│ │ ├── 超出处理 (提示+阻止输入) -│ │ └── 磁盘空间检查 -│ │ -│ └── 异常处理 -│ ├── 网络断开 (重连机制) -│ ├── 文件被占用 (冲突提示) -│ └── 权限错误 (访问拒绝) -│ -└── 技术栈 - │ - ├── 前端 (Vue3) - │ ├── 文本编辑器组件 - │ ├── 路由管理 (Vue Router) - │ └── API调用 (Axios/Fetch) - │ - ├── 后端 (Python) - │ ├── Web框架 (FastAPI/Flask) - │ ├── 文件操作 (OS模块) - │ └── WebSocket支持 (实时通信) - │ - └── 部署 (Docker) - ├── 物理机: Ubuntu系统 - ├── Docker镜像: Python环境 - └── 静态文件代理: Nginx或类似工具 -``` - -**思维导图分析**: - -1. **核心功能模块**是最关键的三个互相依赖的模块 -2. **文件管理模块**提供了额外的列表查看功能 -3. **边界处理模块**确保了应用的健壮性 -4. **技术栈**选择了现代且简单的框架组合 - -## 5. 实现优先级规划 - -基于用户需求,建议按照以下顺序实现: - -### Phase 1: 网页基本功能 (最高优先级) -- Vue3文本编辑器组件 -- 基础UI布局和样式 -- 文本输入和显示功能 - -### Phase 2: 后端接口开发 -- Python Web框架搭建 (FastAPI/Flask) -- 文件读写API -- URL路由处理 -- WebSocket实时通信 - -### Phase 3: 异常和边界条件完善 -- 特殊字符过滤和验证 -- 文件大小限制检查 -- 错误处理和用户提示 -- 文件列表页面后端口令验证 - -### Phase 4: 部署配置 -- Docker镜像配置 (Python环境) -- Nginx静态文件代理配置 -- Docker Compose编排 -- 生产环境测试 - ---- - -## 会话总结 - -**使用的技术**: Question Storming → First Principles Thinking → What If Scenarios → Mind Mapping - -**生成的核心洞察**: -1. 使用现代框架可以简化文档编辑、存储、自动保存的实现 -2. 聚焦于功能需求而非底层技术细节 -3. 需要优先处理特殊字符、文件大小限制、网络安全等边界情况 -4. 实现顺序:前端功能 → 后端接口 → 异常处理 → 部署 - -**下一步行动**: 基于本brainstorming成果,开始Phase 1的网页基本功能开发 - diff --git a/docs/front-end-spec.md b/docs/front-end-spec.md deleted file mode 100644 index 1ffd1dd..0000000 --- a/docs/front-end-spec.md +++ /dev/null @@ -1,435 +0,0 @@ -# Vue3 + Python Notepad 应用 - UI/UX 规格文档 - -**文档版本**: 1.0 -**创建日期**: 2025-12-18 -**作者**: BMad UX Expert -**项目状态**: Greenfield (全新开发) - ---- - -## 1. Introduction - -This document defines the user experience goals, information architecture, user flows, and visual design specifications for Vue3 + Python Notepad 应用's user interface. It serves as the foundation for visual design and frontend development, ensuring a cohesive and user-centered experience. - -### 1.1 Overall UX Goals & Principles - -#### Target User Personas - -- **日常记录用户**: 需要快速记录想法、笔记的普通用户,重视简单易用 -- **技术工作者**: 开发者、研究人员等需要频繁记录技术内容,重视效率和可靠性 -- **团队协作者**: 小型团队成员,需要共享和访问文本文件,重视访问控制和文件管理 - -#### Usability Goals - -- **易学性**: 新用户无需阅读文档即可在30秒内开始编辑 -- **效率**: 熟练用户可以通过URL直接访问文件,无需额外点击 -- **错误预防**: 自动保存机制防止数据丢失,文件名验证防止路径遍历攻击 -- **可记忆性**: 界面设计遵循常见文本编辑器模式,用户可凭直觉操作 - -#### Design Principles - -1. **极简主义** - 去除所有非必要元素,聚焦于文本编辑核心体验 -2. **即时响应** - 每个用户操作都应有明确的视觉反馈 -3. **无干扰编辑** - 界面元素不应分散用户对内容的注意力 -4. **渐进式披露** - 只在需要时显示辅助功能 -5. **无障碍优先** - 从设计开始就确保所有用户都能使用 - -### 1.2 Change Log - -| 日期 | 版本 | 描述 | 作者 | -|------|------|------|------| -| 2025-12-18 | 1.0 | 初始UI/UX规格文档创建 | BMad UX Expert | - ---- - -## 2. Information Architecture (IA) - -### 2.1 Site Map / Screen Inventory - -```mermaid -graph TD - A[首页/编辑页面] --> B[文件列表页面] - A --> C[错误页面] - B --> D[口令验证界面] - B --> E[文件列表展示] - E --> A - - A --> F[编辑器区域] - A --> G[自动保存状态指示器] - A --> H[文件名显示] - - C --> I[网络错误提示] - C --> J[文件访问错误提示] - C --> K[权限错误提示] -``` - -### 2.2 Navigation Structure - -**Primary Navigation:** URL驱动的页面导航,通过修改URL直接访问不同文件 - -**Secondary Navigation:** 文件列表页面中的文件链接,点击直接跳转到对应编辑页面 - -**Breadcrumb Strategy:** 简单的路径显示,如 `Home > filename.txt`,仅在文件列表页面显示 - ---- - -## 3. User Flows - -### 3.1 Flow 1: 新建文件编辑 - -**User Goal:** 创建并编辑一个新的文本文件 - -**Entry Points:** -- 直接访问 `/notes/newfile.txt` (newfile.txt不存在) -- 从文件列表页面点击"新建文件"按钮 - -**Success Criteria:** -- 用户可以立即开始输入内容 -- 内容自动保存,无需手动操作 -- 文件成功创建并可通过URL访问 - -#### Flow Diagram - -```mermaid -graph TD - A[用户访问不存在的文件URL] --> B{文件存在?} - B -->|否| C[创建空白文件] - B -->|是| D[加载现有内容] - C --> E[显示编辑界面] - D --> E - E --> F[用户输入内容] - F --> G{内容变化?} - G -->|是| H[500ms后触发自动保存] - G -->|否| I[继续监听] - H --> J[保存到服务器] - J --> K[显示"已保存"状态] - K --> I - I --> F -``` - -#### Edge Cases & Error Handling: -- 网络断开时显示"连接断开"状态,自动重连后恢复保存 -- 保存失败时显示"保存失败"提示,提供重试按钮 -- 文件名包含非法字符时显示错误提示 - -#### Notes: -- 文件名大小写敏感,遵循Unix文件系统规则 -- 自动保存防抖时间设置为500ms,平衡实时性和性能 - -### 3.2 Flow 2: 文件列表访问 - -**User Goal:** 查看并访问所有已保存的文本文件 - -**Entry Points:** -- 直接访问 `/file-list` URL -- 编辑页面中的"文件列表"链接 - -**Success Criteria:** -- 用户通过口令验证后能看到所有文件列表 -- 点击文件名能直接跳转到编辑页面 -- 文件按创建时间倒序排列 - -#### Flow Diagram - -```mermaid -graph TD - A[用户访问文件列表页面] --> B{已验证?} - B -->|否| C[显示口令输入表单] - B -->|是| D[显示文件列表] - C --> E[用户输入口令] - E --> F[验证口令] - F --> G{口令正确?} - G -->|是| H[保存令牌到本地] - G -->|否| I[显示错误提示] - H --> D - I --> C - D --> J[用户点击文件名] - J --> K[跳转到编辑页面] -``` - -#### Edge Cases & Error Handling: -- 口令验证失败时显示明确的错误信息 -- 令牌过期时重新要求验证 -- 文件列表为空时显示友好提示 - -#### Notes: -- 令牌有效期设置为24小时,平衡安全性和便利性 -- 分页加载策略:每页显示50个文件,支持滚动加载 - ---- - -## 4. Wireframes & Mockups - -**Primary Design Files:** 设计将在开发过程中通过代码实现,不使用外部设计工具 - -### 4.1 Key Screen Layouts - -#### Screen 1: 文本编辑页面 - -**Purpose:** 提供无干扰的文本编辑体验 - -**Key Elements:** -- 顶部栏:文件名显示 + 自动保存状态指示器 -- 主体区域:全屏文本编辑器 -- 右上角:文件列表链接(小图标) - -**Interaction Notes:** -- 页面加载时自动聚焦到编辑器 -- 保存状态实时更新,不干扰编辑 -- 支持键盘快捷键(Ctrl+S 手动保存) - -#### Screen 2: 文件列表页面 - -**Purpose:** 展示所有文件并提供快速访问 - -**Key Elements:** -- 顶部:标题 + 口令验证状态 -- 主体:文件列表(文件名、创建时间) -- 底部:分页控件/加载更多按钮 - -**Interaction Notes:** -- 文件名可点击,跳转到编辑页面 -- 支持按文件名搜索/过滤 -- 响应式设计,适配移动设备 - ---- - -## 5. Component Library / Design System - -**Design System Approach:** 使用轻量级设计系统,基于原生HTML元素和CSS,避免过度工程化 - -### 5.1 Core Components - -#### Component: 编辑器 (TextEditor) - -**Purpose:** 提供文本输入和编辑功能 - -**Variants:** -- 全屏模式(默认) -- 紧凑模式(移动设备) - -**States:** -- 正常编辑状态 -- 保存中状态 -- 保存失败状态 -- 只读状态(文件被锁定时) - -**Usage Guidelines:** -- 占据主要视口空间 -- 使用等宽字体提高可读性 -- 支持Tab缩进 - -#### Component: 状态指示器 (StatusIndicator) - -**Purpose:** 显示系统状态和操作反馈 - -**Variants:** -- 保存状态(已保存/保存中/保存失败) -- 网络状态(已连接/断开连接) -- 错误提示 - -**States:** -- 正常状态(绿色) -- 处理中状态(黄色/动画) -- 错误状态(红色) - -**Usage Guidelines:** -- 使用颜色和图标组合 -- 状态变化时添加过渡动画 -- 位置固定,不随页面滚动 - -#### Component: 文件列表项 (FileListItem) - -**Purpose:** 在文件列表中显示单个文件信息 - -**Variants:** -- 标准列表项 -- 搜索高亮项 - -**States:** -- 正常状态 -- 悬停状态 -- 焦点状态 - -**Usage Guidelines:** -- 显示文件名和创建时间 -- 点击区域覆盖整个列表项 -- 支持键盘导航 - ---- - -## 6. Branding & Style Guide - -**Brand Guidelines:** 采用现代、简洁的设计风格,强调内容而非装饰 - -### 6.1 Color Palette - -| Color Type | Hex Code | Usage | -|------------|----------|-------| -| Primary | #2c3e50 | 主要文本、重要交互元素 | -| Secondary | #7f8c8d | 次要文本、辅助信息 | -| Accent | #3498db | 链接、按钮、强调元素 | -| Success | #27ae60 | 成功状态、保存完成 | -| Warning | #f39c12 | 警告信息、注意提示 | -| Error | #e74c3c | 错误状态、失败提示 | -| Neutral | #ecf0f1 | 背景、边框、分隔线 | - -### 6.2 Typography - -#### Font Families -- **Primary:** -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif -- **Secondary:** "Helvetica Neue", Arial, sans-serif -- **Monospace:** SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace - -#### Type Scale - -| Element | Size | Weight | Line Height | -|---------|------|--------|-------------| -| H1 | 24px | 600 | 1.4 | -| H2 | 20px | 600 | 1.4 | -| H3 | 16px | 600 | 1.4 | -| Body | 14px | 400 | 1.5 | -| Small | 12px | 400 | 1.4 | - -### 6.3 Iconography - -**Icon Library:** 使用简单的SVG图标,避免依赖外部图标库 - -**Usage Guidelines:** -- 图标大小统一为16px或24px -- 使用相同的线条粗细(2px) -- 保持简洁的视觉风格 - -### 6.4 Spacing & Layout - -**Grid System:** 使用CSS Flexbox布局,不依赖固定网格系统 - -**Spacing Scale:** 基于8px的间距系统 -- 小间距:8px -- 中等间距:16px -- 大间距:24px -- 特大间距:32px - ---- - -## 7. Accessibility Requirements - -### 7.1 Compliance Target - -**Standard:** WCAG 2.1 AA - -### 7.2 Key Requirements - -**Visual:** -- Color contrast ratios: 文本对比度至少4.5:1,大文本至少3:1 -- Focus indicators: 所有交互元素有清晰的焦点指示器 -- Text sizing: 支持浏览器缩放至200%而不影响功能 - -**Interaction:** -- Keyboard navigation: 所有功能可通过键盘访问 -- Screen reader support: 适当的ARIA标签和语义化HTML -- Touch targets: 最小触摸目标44px × 44px - -**Content:** -- Alternative text: 所有有意义的图像提供alt文本 -- Heading结构: 正确的标题层级结构 -- Form labels: 所有表单元素有关联的标签 - -### 7.3 Testing Strategy - -- 使用axe-core自动化测试工具 -- 键盘导航测试 -- 屏幕阅读器测试(NVDA, VoiceOver) -- 色彩对比度测试 - ---- - -## 8. Responsiveness Strategy - -### 8.1 Breakpoints - -| Breakpoint | Min Width | Max Width | Target Devices | -|------------|-----------|-----------|----------------| -| Mobile | 320px | 767px | 手机设备 | -| Tablet | 768px | 1023px | 平板设备 | -| Desktop | 1024px | 1439px | 笔记本、桌面显示器 | -| Wide | 1440px | - | 大屏显示器 | - -### 8.2 Adaptation Patterns - -**Layout Changes:** -- 移动设备隐藏文件名显示,使用汉堡菜单 -- 平板设备保持单列布局,调整间距 - -**Navigation Changes:** -- 移动设备使用底部导航栏 -- 桌面设备使用顶部导航 - -**Content Priority:** -- 移动设备优先显示编辑器,辅助信息折叠 -- 大屏设备并排显示更多信息 - -**Interaction Changes:** -- 移动设备增加触摸友好的按钮尺寸 -- 桌面设备支持右键菜单和快捷键 - ---- - -## 9. Animation & Micro-interactions - -### 9.1 Motion Principles - -- **功能性优先**: 动画服务于功能,而非装饰 -- **快速响应**: 动画时长不超过200ms -- **自然流畅**: 使用缓动函数模拟自然运动 -- **尊重用户偏好**: 遵循系统的减少动画设置 - -### 9.2 Key Animations - -- **保存状态切换**: 透明度变化(Duration: 150ms, Easing: ease-out) -- **错误提示滑入**: 从顶部滑入(Duration: 200ms, Easing: ease-out) -- **文件列表项悬停**: 背景色渐变(Duration: 100ms, Easing: ease-in-out) -- **页面切换**: 淡入淡出(Duration: 150ms, Easing: ease-out) - ---- - -## 10. Performance Considerations - -### 10.1 Performance Goals - -- **Page Load**: 首屏加载时间不超过2秒 -- **Interaction Response**: 交互响应时间不超过100ms -- **Animation FPS**: 动画帧率保持60fps - -### 10.2 Design Strategies - -- 使用CSS变量实现主题切换,减少重绘 -- 优化字体加载策略,使用font-display: swap -- 实现虚拟滚动处理大量文件列表 -- 使用Intersection Observer实现懒加载 - ---- - -## 11. Next Steps - -### 11.1 Immediate Actions - -1. 与开发团队确认技术可行性 -2. 创建原型验证核心交互流程 -3. 准备设计系统的基础组件 -4. 制定前端架构设计方案 - -### 11.2 Design Handoff Checklist - -- [x] 所有用户流程已文档化 -- [x] 组件清单已完成 -- [x] 无障碍需求已定义 -- [x] 响应式策略已明确 -- [x] 品牌指导原则已纳入 -- [x] 性能目标已建立 - ---- - -## 12. Checklist Results - -UI/UX规格文档已完成,包含所有必要的用户流程、组件定义、设计规范和实施指导。文档可作为前端开发的完整指导,确保用户体验的一致性和质量。 \ No newline at end of file diff --git a/docs/fullstack-architecture.md b/docs/fullstack-architecture.md deleted file mode 100644 index a5f5834..0000000 --- a/docs/fullstack-architecture.md +++ /dev/null @@ -1,1710 +0,0 @@ -# Vue3 + Python Notepad 应用 - 全栈架构文档 - -**文档版本**: 1.0 -**创建日期**: 2025-12-18 -**作者**: BMad Architect -**项目状态**: Greenfield (全新开发) - ---- - -## 1. Introduction - -This document outlines the complete fullstack architecture for Vue3 + Python Notepad 应用, including backend systems, frontend implementation, and their integration. It serves as the single source of truth for AI-driven development, ensuring consistency across the entire technology stack. - -This unified approach combines what would traditionally be separate backend and frontend architecture documents, streamlining the development process for modern fullstack applications where these concerns are increasingly intertwined. - -### 1.1 Starter Template or Existing Project - -N/A - Greenfield project - -### 1.2 Change Log - -| 日期 | 版本 | 描述 | 作者 | -|------|------|------|------| -| 2025-12-18 | 1.0 | 初始全栈架构文档创建 | BMad Architect | - ---- - -## 2. High Level Architecture - -### 2.1 Technical Summary - -This application uses a modern monolithic architecture with a Vue3 frontend and Python FastAPI backend, deployed via Docker containers on Ubuntu infrastructure. The system provides real-time text editing with automatic saving through WebSocket communication, while maintaining simplicity through file-based storage without database complexity. The architecture prioritizes performance, security, and ease of deployment while supporting URL-driven document access and secure file management. - -### 2.2 Platform and Infrastructure Choice - -**Platform:** Self-hosted Docker deployment -**Key Services:** Docker, Nginx, Python FastAPI, Node.js -**Deployment Host and Regions:** Ubuntu物理机部署,单区域部署 - -Rationale: Self-hosted approach provides maximum control over file storage and security while keeping infrastructure costs minimal. Docker containerization ensures consistent development and production environments. - -### 2.3 Repository Structure - -**Repository Structure:** Simple frontend/backend separation -**Monorepo Tool:** None (simplified structure) -**Package Organization:** - -- `frontend`: Vue3 frontend application -- `backend`: Python FastAPI backend - -Rationale: Simplified structure reduces complexity for this focused application, making it easier to understand and maintain. - -### 2.4 Architecture Diagram - -```mermaid -graph TD - A[用户浏览器] --> B[Nginx反向代理] - B --> C[Vue3前端应用] - B --> D[FastAPI后端] - D --> E[WebSocket服务] - D --> F[文件存储系统] - F --> G[./data/notes目录] - C --> H[Vue Router] - C --> I[Pinia状态管理] - C --> J[WebSocket客户端] - J --> E -``` - -### 2.5 Architectural Patterns - -- **Monolithic Architecture:** Single deployable unit with clear frontend/backend separation - _Rationale:_ Simplifies deployment and debugging for this focused application -- **Component-Based UI:** Reusable Vue3 components with TypeScript - _Rationale:_ Maintainability and type safety across the codebase -- **WebSocket Communication:** Real-time bidirectional communication - _Rationale:_ Enables instant auto-save without polling overhead -- **File-Based Storage:** Direct file system storage instead of database - _Rationale:_ Simplifies deployment and aligns with the notepad metaphor -- **Repository Pattern:** Abstract file operations - _Rationale:_ Enables testing and future storage migration -- **JWT Authentication:** Token-based authentication for file list access - _Rationale:_ Stateless authentication suitable for this simple use case - ---- - -## 3. Tech Stack - -### 3.1 Technology Stack Table - -| Category | Technology | Version | Purpose | Rationale | -|----------|------------|---------|---------|-----------| -| Frontend Language | TypeScript | 5.0+ | 类型安全的JavaScript开发 | 提供编译时类型检查,提高代码质量 | -| Frontend Framework | Vue 3 | 3.3+ | 构建响应式用户界面 | 轻量级、高性能、易学易用 | -| UI Component Library | 自定义组件 | - | 构建应用特定UI组件 | 避免过度工程化,保持轻量 | -| State Management | Pinia | 2.1+ | Vue状态管理 | Vue官方推荐,简单直观 | -| Backend Language | Python | 3.10+ | 后端服务开发 | 丰富的生态系统,易于部署 | -| Backend Framework | FastAPI | 0.104+ | 构建高性能API | 自动API文档,内置WebSocket支持 | -| API Style | REST + WebSocket | - | 前后端通信协议 | REST用于标准操作,WebSocket用于实时通信 | -| Database | 无 | - | 不使用数据库 | 简化架构,直接使用文件系统 | -| Cache | 内存缓存 | - | 提高文件读取性能 | 减少重复的文件系统操作 | -| File Storage | 本地文件系统 | - | 存储文本文件 | 简单直接,符合notepad概念 | -| Authentication | JWT | - | 文件列表访问验证 | 无状态,适合简单认证需求 | -| Frontend Testing | Vitest | 1.0+ | 前端单元测试 | Vite集成,快速测试 | -| Backend Testing | pytest | 7.4+ | 后端单元测试 | Python标准测试框架 | -| E2E Testing | Playwright | 1.40+ | 端到端测试 | 跨浏览器测试支持 | -| Build Tool | Vite | 5.0+ | 前端构建工具 | 快速开发服务器和构建 | -| Bundler | Rollup | - | 前端打包 | Vite内置,优化打包 | -| IaC Tool | Docker Compose | 2.20+ | 基础设施即代码 | 简化部署和环境管理 | -| CI/CD | GitHub Actions | - | 持续集成和部署 | 与代码仓库集成 | -| Monitoring | 自定义日志 | - | 应用监控 | 轻量级日志记录 | -| Logging | Python logging + 自定义 | - | 日志记录 | 结构化日志,便于调试 | -| CSS Framework | 自定义CSS | - | 样式管理 | 避免外部依赖,保持轻量 | - ---- - -## 4. Data Models - -### 4.1 TextFile - -**Purpose:** 表示存储在系统中的文本文件 - -**Key Attributes:** -- filename: string - 文件名(包含.txt扩展名) -- content: string - 文件内容 -- created_at: datetime - 创建时间 -- updated_at: datetime - 最后更新时间 -- size: number - 文件大小(字符数) - -#### TypeScript Interface - -```typescript -interface TextFile { - filename: string; - content: string; - created_at: Date; - updated_at: Date; - size: number; -} -``` - -#### Relationships - -- 无关系 - 独立实体 - -### 4.2 FileListItem - -**Purpose:** 文件列表页面显示的文件信息 - -**Key Attributes:** -- filename: string - 文件名 -- created_at: datetime - 创建时间 -- size: number - 文件大小 - -#### TypeScript Interface - -```typescript -interface FileListItem { - filename: string; - created_at: Date; - size: number; -} -``` - -#### Relationships - -- 无关系 - 聚合数据 - -### 4.3 AuthToken - -**Purpose:** 认证令牌 - -**Key Attributes:** -- token: string - JWT令牌 -- expires_at: datetime - 过期时间 - -#### TypeScript Interface - -```typescript -interface AuthToken { - token: string; - expires_at: Date; -} -``` - -#### Relationships - -- 无关系 - 临时认证数据 - ---- - -## 5. API Specification - -### 5.1 REST API Specification - -```yaml -openapi: 3.0.0 -info: - title: Vue3 + Python Notepad API - version: 1.0.0 - description: 简单的文本编辑器API,支持文件读写和列表功能 -servers: - - url: http://localhost:8000/api - description: 开发环境服务器 - - url: https://your-domain.com/api - description: 生产环境服务器 - -paths: - /notes/{filename}: - get: - summary: 获取文件内容 - parameters: - - name: filename - in: path - required: true - schema: - type: string - description: 文件名(包含.txt扩展名) - responses: - '200': - description: 成功返回文件内容 - content: - application/json: - schema: - $ref: '#/components/schemas/TextFile' - '404': - description: 文件不存在 - '400': - description: 文件名无效 - post: - summary: 保存文件内容 - parameters: - - name: filename - in: path - required: true - schema: - type: string - description: 文件名(包含.txt扩展名) - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - content: - type: string - description: 文件内容 - responses: - '200': - description: 保存成功 - content: - application/json: - schema: - $ref: '#/components/schemas/TextFile' - '413': - description: 文件过大 - '400': - description: 文件名无效或内容为空 - - /files/list: - get: - summary: 获取文件列表 - security: - - bearerAuth: [] - parameters: - - name: page - in: query - schema: - type: integer - default: 1 - description: 页码 - - name: limit - in: query - schema: - type: integer - default: 50 - description: 每页文件数 - responses: - '200': - description: 成功返回文件列表 - content: - application/json: - schema: - type: object - properties: - files: - type: array - items: - $ref: '#/components/schemas/FileListItem' - total: - type: integer - page: - type: integer - limit: - type: integer - '401': - description: 未授权访问 - '403': - description: 令牌无效或过期 - - /files/verify-password: - post: - summary: 验证访问口令 - requestBody: - required: true - content: - application/json: - schema: - type: object - properties: - password: - type: string - description: 访问口令 - responses: - '200': - description: 验证成功 - content: - application/json: - schema: - $ref: '#/components/schemas/AuthToken' - '401': - description: 口令错误 - - /health: - get: - summary: 健康检查 - responses: - '200': - description: 服务正常 - content: - application/json: - schema: - type: object - properties: - status: - type: string - example: "healthy" - -components: - schemas: - TextFile: - type: object - properties: - filename: - type: string - example: "note.txt" - content: - type: string - example: "这是文件内容" - created_at: - type: string - format: date-time - updated_at: - type: string - format: date-time - size: - type: integer - example: 6 - - FileListItem: - type: object - properties: - filename: - type: string - example: "note.txt" - created_at: - type: string - format: date-time - size: - type: integer - example: 6 - - AuthToken: - type: object - properties: - token: - type: string - example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - expires_at: - type: string - format: date-time - - securitySchemes: - bearerAuth: - type: http - scheme: bearer - bearerFormat: JWT -``` - -### 5.2 WebSocket API - -#### 连接端点 -`ws://localhost:8000/ws/{filename}` - -#### 消息格式 - -**客户端发送保存请求:** -```json -{ - "type": "save", - "content": "文件内容", - "timestamp": "2023-12-18T10:30:00Z" -} -``` - -**服务器响应:** -```json -{ - "type": "save_response", - "status": "success|error", - "message": "保存成功|错误信息", - "timestamp": "2023-12-18T10:30:00Z" -} -``` - ---- - -## 6. Components - -### 6.1 Frontend Components - -#### TextEditor - -**Responsibility:** 提供文本编辑功能和自动保存机制 - -**Key Interfaces:** -- `props.filename`: string - 要编辑的文件名 -- `props.initialContent`: string - 初始文件内容 -- `emits.save`: (content: string) => void - 保存事件 -- `emits.contentChange`: (content: string) => void - 内容变化事件 - -**Dependencies:** WebSocket客户端,Pinia store - -**Technology Stack:** Vue 3 Composition API, TypeScript, CSS - -#### FileList - -**Responsibility:** 显示文件列表并提供导航功能 - -**Key Interfaces:** -- `props.files`: FileListItem[] - 文件列表数据 -- `props.loading`: boolean - 加载状态 -- `emits.fileSelect`: (filename: string) => void - 文件选择事件 - -**Dependencies:** API客户端,Vue Router - -**Technology Stack:** Vue 3 Composition API, TypeScript, CSS - -#### AuthForm - -**Responsibility:** 处理口令验证表单 - -**Key Interfaces:** -- `emits.authSuccess`: (token: string) => void - 认证成功事件 -- `emits.authError`: (message: string) => void - 认证失败事件 - -**Dependencies:** API客户端,本地存储 - -**Technology Stack:** Vue 3 Composition API, TypeScript, CSS - -### 6.2 Backend Components - -#### FileService - -**Responsibility:** 处理文件读写操作 - -**Key Interfaces:** -- `readFile(filename: string): TextFile` - 读取文件 -- `writeFile(filename: string, content: string): TextFile` - 写入文件 -- `listFiles(page: number, limit: number): FileListItem[]` - 列出文件 -- `validateFilename(filename: string): boolean` - 验证文件名 - -**Dependencies:** 文件系统,验证模块 - -**Technology Stack:** Python 3.10+, OS模块 - -#### AuthService - -**Responsibility:** 处理认证和授权 - -**Key Interfaces:** -- `verifyPassword(password: string): AuthToken` - 验证口令 -- `validateToken(token: string): boolean` - 验证令牌 -- `generateToken(): string` - 生成令牌 - -**Dependencies:** JWT库,环境变量 - -**Technology Stack:** Python 3.10+, PyJWT - -#### WebSocketHandler - -**Responsibility:** 处理WebSocket连接和消息 - -**Key Interfaces:** -- `handleConnection(websocket: WebSocket, filename: string)` - 处理连接 -- `handleMessage(websocket: WebSocket, message: dict)` - 处理消息 -- `broadcastUpdate(filename: string, content: string)` - 广播更新 - -**Dependencies:** FastAPI WebSocket,FileService - -**Technology Stack:** Python 3.10+, FastAPI - ---- - -## 7. External APIs - -N/A - 本项目不依赖外部API服务 - ---- - -## 8. Core Workflows - -### 8.1 文件编辑和自动保存 - -```mermaid -sequenceDiagram - participant User - participant Browser - participant VueApp - participant WebSocket - participant FastAPI - participant FileSystem - - User->>Browser: 访问/notes/file.txt - Browser->>VueApp: 加载编辑页面 - VueApp->>FastAPI: GET /api/notes/file.txt - FastAPI->>FileSystem: 读取文件 - FileSystem-->>FastAPI: 返回文件内容 - FastAPI-->>VueApp: 返回TextFile对象 - VueApp-->>Browser: 显示编辑界面 - - User->>Browser: 输入文本 - Browser->>VueApp: 触发input事件 - VueApp->>VueApp: 防抖处理(500ms) - VueApp->>WebSocket: 发送保存消息 - WebSocket->>FastAPI: 处理WebSocket消息 - FastAPI->>FileSystem: 写入文件 - FileSystem-->>FastAPI: 写入成功 - FastAPI-->>WebSocket: 返回保存响应 - WebSocket-->>VueApp: 更新保存状态 - VueApp-->>Browser: 显示"已保存"状态 -``` - -### 8.2 文件列表访问认证 - -```mermaid -sequenceDiagram - participant User - participant Browser - participant VueApp - participant API - participant AuthService - - User->>Browser: 访问/file-list - Browser->>VueApp: 加载文件列表页面 - VueApp->>VueApp: 检查本地令牌 - alt 令牌存在且有效 - VueApp->>API: GET /api/files/list (带令牌) - API->>AuthService: 验证令牌 - AuthService-->>API: 令牌有效 - API->>API: 获取文件列表 - API-->>VueApp: 返回文件列表 - VueApp-->>Browser: 显示文件列表 - else 令牌不存在或无效 - VueApp-->>Browser: 显示口令输入表单 - User->>Browser: 输入口令 - Browser->>VueApp: 提交口令 - VueApp->>API: POST /api/files/verify-password - API->>AuthService: 验证口令 - AuthService-->>API: 生成令牌 - API-->>VueApp: 返回令牌 - VueApp->>VueApp: 保存令牌到本地 - VueApp->>API: GET /api/files/list (带新令牌) - API-->>VueApp: 返回文件列表 - VueApp-->>Browser: 显示文件列表 - end -``` - ---- - -## 9. Database Schema - -N/A - 本项目使用文件系统存储,不使用数据库 - ---- - -## 10. Frontend Architecture - -### 10.1 Component Architecture - -#### Component Organization - -``` -src/ -├── components/ # 可复用组件 -│ ├── common/ # 通用组件 -│ │ ├── Button.vue -│ │ ├── Modal.vue -│ │ └── StatusIndicator.vue -│ ├── editor/ # 编辑器相关组件 -│ │ ├── TextEditor.vue -│ │ └── SaveStatus.vue -│ └── files/ # 文件管理组件 -│ ├── FileList.vue -│ ├── FileListItem.vue -│ └── AuthForm.vue -├── views/ # 页面组件 -│ ├── EditorView.vue -│ └── FileListView.vue -├── stores/ # Pinia状态管理 -│ ├── editor.ts -│ ├── files.ts -│ └── auth.ts -├── services/ # API服务 -│ ├── api.ts -│ ├── websocket.ts -│ └── storage.ts -├── router/ # 路由配置 -│ └── index.ts -├── utils/ # 工具函数 -│ ├── validation.ts -│ └── helpers.ts -└── styles/ # 样式文件 - ├── main.css - └── components.css -``` - -#### Component Template - -```vue -