继续提交
@@ -1,70 +0,0 @@
|
|||||||
# Node.js 相关
|
|
||||||
node_modules
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# 源代码和开发文件(精简版不需要)
|
|
||||||
src
|
|
||||||
scripts
|
|
||||||
public
|
|
||||||
package*.json
|
|
||||||
vite.config.js
|
|
||||||
eslint.config.js
|
|
||||||
index.html
|
|
||||||
|
|
||||||
# 构建相关
|
|
||||||
dist
|
|
||||||
.vite
|
|
||||||
|
|
||||||
# 环境文件
|
|
||||||
.env*
|
|
||||||
|
|
||||||
# IDE/Editor files
|
|
||||||
.vscode
|
|
||||||
.idea
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
|
|
||||||
# OS generated files
|
|
||||||
.DS_Store
|
|
||||||
.DS_Store?
|
|
||||||
._*
|
|
||||||
.Spotlight-V100
|
|
||||||
.Trashes
|
|
||||||
ehthumbs.db
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Git
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
|
|
||||||
# Docker 相关(保留 Dockerfile)
|
|
||||||
docker-compose.yml
|
|
||||||
|
|
||||||
# 脚本文件(精简版不需要)
|
|
||||||
docker-entrypoint.sh
|
|
||||||
|
|
||||||
# 文档
|
|
||||||
README*.md
|
|
||||||
LICENSE
|
|
||||||
|
|
||||||
# 日志
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# ESLint cache
|
|
||||||
.eslintcache
|
|
||||||
50
Dockerfile
@@ -1,50 +0,0 @@
|
|||||||
# 极简静态资源镜像
|
|
||||||
FROM nginx:alpine
|
|
||||||
|
|
||||||
# 安装 wget 用于健康检查
|
|
||||||
RUN apk add --no-cache wget
|
|
||||||
|
|
||||||
# 极简静态资源镜像
|
|
||||||
FROM nginx:alpine
|
|
||||||
|
|
||||||
# 安装 wget 用于健康检查
|
|
||||||
RUN apk add --no-cache wget
|
|
||||||
|
|
||||||
# 创建自定义 nginx 配置,禁用缓存并确保正确读取挂载目录
|
|
||||||
RUN printf 'server {\n\
|
|
||||||
listen 80;\n\
|
|
||||||
server_name localhost;\n\
|
|
||||||
root /usr/share/nginx/html;\n\
|
|
||||||
index index.html index.htm;\n\
|
|
||||||
\n\
|
|
||||||
# 禁用缓存\n\
|
|
||||||
add_header Cache-Control "no-cache, no-store, must-revalidate";\n\
|
|
||||||
add_header Pragma "no-cache";\n\
|
|
||||||
add_header Expires "0";\n\
|
|
||||||
\n\
|
|
||||||
location / {\n\
|
|
||||||
try_files $uri $uri/ /index.html;\n\
|
|
||||||
# 确保每次都读取最新文件\n\
|
|
||||||
sendfile off;\n\
|
|
||||||
tcp_nodelay on;\n\
|
|
||||||
tcp_nopush off;\n\
|
|
||||||
}\n\
|
|
||||||
\n\
|
|
||||||
# 禁用所有静态文件缓存\n\
|
|
||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {\n\
|
|
||||||
add_header Cache-Control "no-cache, no-store, must-revalidate";\n\
|
|
||||||
add_header Pragma "no-cache";\n\
|
|
||||||
add_header Expires "0";\n\
|
|
||||||
}\n\
|
|
||||||
}\n' > /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
# 确保挂载目录存在但为空(避免与外部挂载冲突)
|
|
||||||
RUN mkdir -p /usr/share/nginx/html && \
|
|
||||||
rm -rf /usr/share/nginx/html/* && \
|
|
||||||
echo "Waiting for external mount..." > /usr/share/nginx/html/.placeholder
|
|
||||||
|
|
||||||
# 暴露静态服务端口
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
# 启动 nginx 前台运行
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
207
README.md
@@ -1,29 +1,198 @@
|
|||||||
# Markdown To Web
|
# 萌芽笔记 🌱
|
||||||
|
|
||||||
> 将本地 Markdown 笔记目录“一键”编译为可部署的静态 React 网站。
|
一个优雅的 Markdown 笔记 Web 应用,支持实时预览和 GitHub 风格渲染。
|
||||||
|
|
||||||
此仓库会读取 `public/mengyanote/` 下的所有 Markdown 文件,生成 JSON 数据,并在前端通过 React + react-markdown 渲染,最终可直接构建成 **纯静态站点**(可部署到 GitHub Pages / Netlify / Vercel / 任意 Nginx / OSS 等)。
|
> **🎉 v2.0 重要更新 (2025-12-19)**
|
||||||
|
> - ✅ 完全移除 localStorage 依赖,解决浏览器权限弹窗问题
|
||||||
|
> - ✅ 完美支持移动端浏览器(微信、Safari iOS 等)
|
||||||
|
> - ✅ 隐私模式/无痕模式下正常工作
|
||||||
|
> - ✅ 添加错误边界和友好的错误提示
|
||||||
|
> - ✅ 优化网络请求,支持相对路径部署
|
||||||
|
> - 📖 详细说明请查看 [前端优化说明.md](前端优化说明.md)
|
||||||
|
|
||||||
本 README(中文)包含:项目定位、特性说明、运行与构建、内部工作原理、目录结构、忽略与定制、部署方式、常见问题(FAQ)与未来计划(Roadmap)。
|
## ✨ 特性
|
||||||
|
|
||||||
如需英文版本,请查看 `README.en.md`。
|
- 📝 **Markdown 渲染**: 使用 `react-markdown` 提供强大的渲染能力
|
||||||
|
- 🎨 **GitHub 风格**: 标准的 GitHub Markdown 样式
|
||||||
|
- 🔤 **霞鹜文楷字体**: 全站使用 LXGW WenKai Mono 等宽字体
|
||||||
|
- 📂 **文件树导航**: 直观的侧边栏目录结构
|
||||||
|
- 🚀 **快速响应**: FastAPI 后端 + React 前端
|
||||||
|
- 💡 **代码高亮**: 支持多种编程语言的语法高亮
|
||||||
|
- 🧮 **数学公式**: KaTeX 数学公式渲染支持
|
||||||
|
- 📱 **响应式设计**: 完美适配桌面和移动设备
|
||||||
|
- 🌐 **跨浏览器兼容**: 支持所有主流浏览器和移动端
|
||||||
|
- 🔒 **隐私模式支持**: 在隐私/无痕模式下正常工作
|
||||||
|
|
||||||
## ✨ 主要特性
|
## 🛠 技术栈
|
||||||
|
|
||||||
- 🚀 **一键生成数据**:脚本自动递归扫描 `public/mengyanote`,提取目录树、文件内容与统计信息。
|
### 后端
|
||||||
- 📦 **纯静态输出**:构建阶段就准备好全部数据,无需后端 / Serverless API。
|
- **Python 3.8+**
|
||||||
- 🧭 **动态目录树**:可展开/折叠,每个节点保留唯一 `path` 与 `id`。
|
- **FastAPI** - 现代、快速的 Web 框架
|
||||||
- 📝 **Markdown 全面支持**:GFM(表格 / 任务列表 / 删除线)、换行、数学公式(KaTeX)、代码块高亮(Highlight.js)。
|
- **Uvicorn** - ASGI 服务器
|
||||||
- 🧮 **数学公式渲染**:`remark-math` + `rehype-katex`。
|
|
||||||
- 💡 **代码体验增强**:复制按钮、语言标签、自动高亮。
|
### 前端
|
||||||
- 📚 **面包屑导航**:基于文件路径实时生成。
|
- **React 19** - 用户界面库
|
||||||
- 🧱 **可定制渲染组件**:可移除/替换任意 remark/rehype 插件,或覆写组件(如链接、图片、表格、代码块)。
|
- **Vite 7** - 极速构建工具
|
||||||
- 🕶 **深色风格**(可自定义):CSS 易修改,可换成“极简/原味/图片优先”布局。
|
- **react-markdown** - Markdown 渲染
|
||||||
- 🔍 **预生成数据多路径兜底**:前端运行时尝试从 `/data` 或 `/src/data` 加载,兼容不同构建/部署结构。
|
- **github-markdown-css** - GitHub 风格样式
|
||||||
- 🧯 **忽略机制**:支持 `ignore.json` 自定义排除目录 / 文件。
|
- **rehype/remark 插件** - 扩展 Markdown 功能
|
||||||
- 📊 **统计信息**:生成文件总数、文件夹总数、生成时间等元数据。
|
- GFM (GitHub Flavored Markdown)
|
||||||
|
- 代码高亮 (highlight.js)
|
||||||
|
- 数学公式 (KaTeX)
|
||||||
|
- HTML 支持
|
||||||
|
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 方式一:使用批处理脚本(Windows推荐)
|
||||||
|
|
||||||
|
#### 启动后端
|
||||||
|
双击运行 `启动后端.bat`
|
||||||
|
|
||||||
|
#### 启动前端
|
||||||
|
双击运行 `启动前端.bat`
|
||||||
|
|
||||||
|
### 方式二:手动启动
|
||||||
|
|
||||||
|
#### 1. 启动后端服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 进入后端目录
|
||||||
|
cd mengyanote-backend
|
||||||
|
|
||||||
|
# 安装依赖(首次运行)
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
后端将在 `http://localhost:8000` 运行
|
||||||
|
|
||||||
|
#### 2. 启动前端服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 进入前端目录
|
||||||
|
cd mengyanote-frontend
|
||||||
|
|
||||||
|
# 安装依赖(首次运行)
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 启动开发服务器
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
前端将在 `http://localhost:5173` 运行
|
||||||
|
|
||||||
|
## 📁 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
mengyanote/
|
||||||
|
├── mengyanote-backend/ # 后端服务
|
||||||
|
│ ├── main.py # FastAPI 应用入口
|
||||||
|
│ ├── requirements.txt # Python 依赖
|
||||||
|
│ └── mengyanote/ # Markdown 文件存储目录
|
||||||
|
│ ├── 编程语言/
|
||||||
|
│ ├── 计算机科普/
|
||||||
|
│ ├── 计算机网络/
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
├── mengyanote-frontend/ # 前端应用
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── components/ # React 组件
|
||||||
|
│ │ │ ├── Sidebar.jsx # 侧边栏组件
|
||||||
|
│ │ │ ├── MarkdownRenderer.jsx # Markdown 渲染器
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ ├── context/ # React Context
|
||||||
|
│ │ ├── utils/ # 工具函数
|
||||||
|
│ │ ├── App.jsx # 主应用组件
|
||||||
|
│ │ └── main.jsx # 应用入口
|
||||||
|
│ ├── public/ # 静态资源
|
||||||
|
│ │ └── LXGWWenKaiMono-Medium.ttf # 字体文件
|
||||||
|
│ ├── package.json # 前端依赖
|
||||||
|
│ └── vite.config.js # Vite 配置
|
||||||
|
│
|
||||||
|
├── 启动后端.bat # 后端启动脚本
|
||||||
|
├── 启动前端.bat # 前端启动脚本
|
||||||
|
├── 启动说明.md # 详细启动说明
|
||||||
|
└── README.md # 项目说明
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 功能说明
|
||||||
|
|
||||||
|
### 后端 API
|
||||||
|
|
||||||
|
- `GET /api/tree` - 获取文件树结构
|
||||||
|
- `GET /api/file?path={path}` - 获取指定文件内容
|
||||||
|
- `GET /api/health` - 健康检查
|
||||||
|
- `GET /docs` - API 文档(Swagger UI)
|
||||||
|
|
||||||
|
### 前端功能
|
||||||
|
|
||||||
|
- **文件导航**: 点击左侧文件树浏览笔记
|
||||||
|
- **Markdown 渲染**: 自动渲染 GitHub 风格的 Markdown
|
||||||
|
- **代码高亮**: 自动识别并高亮代码块
|
||||||
|
- **数学公式**: 支持行内和块级数学公式
|
||||||
|
- **图片支持**: 点击图片可放大查看
|
||||||
|
- **外链处理**: 外部链接自动在新标签页打开
|
||||||
|
|
||||||
|
## 🎨 字体配置
|
||||||
|
|
||||||
|
项目使用 **霞鹜文楷等宽体 (LXGW WenKai Mono)** 作为全站字体。
|
||||||
|
|
||||||
|
字体文件位置:`mengyanote-frontend/public/LXGWWenKaiMono-Medium.ttf`
|
||||||
|
|
||||||
|
如果字体文件缺失,系统会自动回退到默认字体。
|
||||||
|
|
||||||
|
## 🔧 环境变量
|
||||||
|
|
||||||
|
### 前端环境变量(可选)
|
||||||
|
|
||||||
|
在 `mengyanote-frontend` 目录创建 `.env` 文件:
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_API_BASE=http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 构建生产版本
|
||||||
|
|
||||||
|
### 前端构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mengyanote-frontend
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
构建产物在 `dist` 目录,可部署到任何静态托管服务。
|
||||||
|
|
||||||
|
## 🐛 故障排除
|
||||||
|
|
||||||
|
### 前端无法连接后端
|
||||||
|
1. 确认后端服务正在运行(访问 http://localhost:8000/api/health)
|
||||||
|
2. 检查浏览器控制台的网络请求错误
|
||||||
|
3. 确认后端端口为 8000
|
||||||
|
|
||||||
|
### 字体未加载
|
||||||
|
1. 确认 `LXGWWenKaiMono-Medium.ttf` 存在于 `public` 目录
|
||||||
|
2. 检查浏览器开发者工具的网络面板
|
||||||
|
3. 清除浏览器缓存重试
|
||||||
|
|
||||||
|
### Markdown 渲染异常
|
||||||
|
1. 检查文件编码是否为 UTF-8
|
||||||
|
2. 查看浏览器控制台错误信息
|
||||||
|
3. 确认 react-markdown 相关依赖已正确安装
|
||||||
|
|
||||||
|
## 📝 开发建议
|
||||||
|
|
||||||
|
1. **IDE**: 推荐使用 VS Code
|
||||||
|
2. **代码规范**: 前端使用 ESLint 进行代码检查
|
||||||
|
3. **热重载**: 后端和前端都支持热重载,修改代码自动刷新
|
||||||
|
4. **调试**: 使用 Chrome DevTools 进行前端调试
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Made with ❤️ by 树萌芽**
|
||||||
|
|
||||||
> 该项目适合:知识库发布、学习笔记归档、团队内部静态文档、离线备份网页化。
|
|
||||||
|
|
||||||
## 🛠 技术栈
|
## 🛠 技术栈
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
web:
|
|
||||||
build: .
|
|
||||||
container_name: markdown-to-web
|
|
||||||
ports:
|
|
||||||
- "5173:80"
|
|
||||||
volumes:
|
|
||||||
- /shumengya/docker/storage/markdown2web/dist:/usr/share/nginx/html
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# 该脚本在简化后的 Docker 流程中不再使用,保留占位避免误调用。
|
|
||||||
echo "docker-entrypoint.sh 已弃用:当前镜像仅用于静态文件托管。"
|
|
||||||
18
mengyanote-backend/.dockerignore
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
*.so
|
||||||
|
*.egg
|
||||||
|
*.egg-info
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
*.md
|
||||||
|
README.md
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
26
mengyanote-backend/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# 使用 Python 3.11 slim 镜像作为基础镜像
|
||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
# 复制依赖文件
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# 安装 Python 依赖
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||||
|
|
||||||
|
# 复制应用代码
|
||||||
|
COPY main.py .
|
||||||
|
COPY mengyanote ./mengyanote
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# 启动命令
|
||||||
|
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
BIN
mengyanote-backend/__pycache__/main.cpython-313.pyc
Normal file
26
mengyanote-backend/docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
mengyanote-backend:
|
||||||
|
build: .
|
||||||
|
container_name: mengyanote-backend
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "2424:8000"
|
||||||
|
volumes:
|
||||||
|
# 持久化 mengyanote 数据目录
|
||||||
|
- /shumengya/docker/mengyanote/data:/app/mengyanote
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000/api/tree"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
networks:
|
||||||
|
- mengyanote-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
mengyanote-network:
|
||||||
|
driver: bridge
|
||||||
257
mengyanote-backend/main.py
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Literal, Optional, Set
|
||||||
|
|
||||||
|
from fastapi import FastAPI, HTTPException, Query
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
# Markdown 根目录:指向当前后端项目中的 `mengyanote` 文件夹
|
||||||
|
MARKDOWN_ROOT = BASE_DIR / "mengyanote"
|
||||||
|
# ignore.json 文件路径
|
||||||
|
IGNORE_FILE = MARKDOWN_ROOT / "ignore.json"
|
||||||
|
|
||||||
|
|
||||||
|
def load_ignore_list() -> Set[str]:
|
||||||
|
"""从 ignore.json 加载需要忽略的文件夹列表"""
|
||||||
|
if not IGNORE_FILE.exists():
|
||||||
|
return set()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(IGNORE_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return set(data.get('ignore', []))
|
||||||
|
except Exception:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
# 加载忽略列表
|
||||||
|
IGNORE_LIST = load_ignore_list()
|
||||||
|
|
||||||
|
|
||||||
|
class NodeType(str):
|
||||||
|
FOLDER: Literal["folder"] = "folder"
|
||||||
|
FILE: Literal["file"] = "file"
|
||||||
|
|
||||||
|
|
||||||
|
class DirectoryNode(BaseModel):
|
||||||
|
name: str
|
||||||
|
path: str # 相对于 MARKDOWN_ROOT 的路径,使用 / 作为分隔符
|
||||||
|
type: Literal["folder", "file"]
|
||||||
|
children: Optional[List["DirectoryNode"]] = None
|
||||||
|
|
||||||
|
|
||||||
|
DirectoryNode.update_forward_refs()
|
||||||
|
|
||||||
|
|
||||||
|
class FileContent(BaseModel):
|
||||||
|
path: str
|
||||||
|
content: str
|
||||||
|
word_count: int = 0
|
||||||
|
file_size: int = 0 # 文件大小,字节
|
||||||
|
created_time: str = ""
|
||||||
|
modified_time: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(title="MengyaNote Backend", version="1.0.0")
|
||||||
|
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
# 现在允许任意来源方便本地和静态托管访问,如 http://localhost:9090
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_markdown_file(path: Path) -> bool:
|
||||||
|
return path.is_file() and path.suffix.lower() == ".md"
|
||||||
|
|
||||||
|
|
||||||
|
def should_skip(entry: Path) -> bool:
|
||||||
|
"""判断是否应该跳过该文件或文件夹"""
|
||||||
|
name = entry.name
|
||||||
|
|
||||||
|
# 跳过隐藏文件/文件夹
|
||||||
|
if name.startswith("."):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 跳过 ignore.json 文件本身
|
||||||
|
if name == "ignore.json":
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 跳过 ignore.json 中配置的文件夹
|
||||||
|
if entry.is_dir() and name in IGNORE_LIST:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def build_directory_tree(root: Path) -> List[DirectoryNode]:
|
||||||
|
"""从文件系统构建目录树,结构尽量与原先 JSON 保持一致。"""
|
||||||
|
if not root.exists() or not root.is_dir():
|
||||||
|
return []
|
||||||
|
|
||||||
|
def walk(current: Path, rel: Path) -> DirectoryNode:
|
||||||
|
name = current.name
|
||||||
|
rel_path_str = rel.as_posix() if rel.as_posix() != "." else ""
|
||||||
|
|
||||||
|
if current.is_dir():
|
||||||
|
children_nodes: List[DirectoryNode] = []
|
||||||
|
for child in sorted(current.iterdir(), key=lambda p: (p.is_file(), p.name.lower())):
|
||||||
|
if should_skip(child):
|
||||||
|
continue
|
||||||
|
child_rel = rel / child.name
|
||||||
|
# 只收录 Markdown 文件和非空目录
|
||||||
|
if child.is_dir():
|
||||||
|
node = walk(child, child_rel)
|
||||||
|
# 如果目录下完全没有 md 文件/子目录,可以选择丢弃
|
||||||
|
if node.children:
|
||||||
|
children_nodes.append(node)
|
||||||
|
elif is_markdown_file(child):
|
||||||
|
children_nodes.append(
|
||||||
|
DirectoryNode(
|
||||||
|
name=child.name,
|
||||||
|
path=child_rel.as_posix(),
|
||||||
|
type="file",
|
||||||
|
children=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return DirectoryNode(
|
||||||
|
name=name,
|
||||||
|
path=rel_path_str or name,
|
||||||
|
type="folder",
|
||||||
|
children=children_nodes,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# 单独文件的情况一般不会作为根调用
|
||||||
|
return DirectoryNode(
|
||||||
|
name=name,
|
||||||
|
path=rel_path_str or name,
|
||||||
|
type="file",
|
||||||
|
children=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
nodes: List[DirectoryNode] = []
|
||||||
|
for child in sorted(MARKDOWN_ROOT.iterdir(), key=lambda p: (p.is_file(), p.name.lower())):
|
||||||
|
if should_skip(child):
|
||||||
|
continue
|
||||||
|
rel = Path(child.name)
|
||||||
|
if child.is_dir():
|
||||||
|
node = walk(child, rel)
|
||||||
|
if node.children:
|
||||||
|
nodes.append(node)
|
||||||
|
elif is_markdown_file(child):
|
||||||
|
nodes.append(
|
||||||
|
DirectoryNode(
|
||||||
|
name=child.name,
|
||||||
|
path=rel.as_posix(),
|
||||||
|
type="file",
|
||||||
|
children=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_markdown_path(relative_path: str) -> Path:
|
||||||
|
"""将前端传入的相对路径安全地转换为磁盘路径,防止目录穿越。"""
|
||||||
|
# 统一使用 / 分隔符
|
||||||
|
safe_path = relative_path.replace("\\", "/").lstrip("/")
|
||||||
|
candidate = MARKDOWN_ROOT / safe_path
|
||||||
|
try:
|
||||||
|
candidate_resolved = candidate.resolve()
|
||||||
|
except FileNotFoundError:
|
||||||
|
candidate_resolved = candidate
|
||||||
|
|
||||||
|
if not str(candidate_resolved).startswith(str(MARKDOWN_ROOT.resolve())):
|
||||||
|
raise HTTPException(status_code=400, detail="非法路径")
|
||||||
|
|
||||||
|
return candidate_resolved
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/tree", response_model=List[DirectoryNode])
|
||||||
|
def get_directory_tree() -> List[DirectoryNode]:
|
||||||
|
"""
|
||||||
|
获取 Markdown 目录树。
|
||||||
|
|
||||||
|
返回结构与原来的 directoryTree.json 尽量保持兼容:
|
||||||
|
- name: 文件或文件夹名
|
||||||
|
- path: 相对路径(使用 /)
|
||||||
|
- type: 'folder' | 'file'
|
||||||
|
- children: 子节点数组
|
||||||
|
"""
|
||||||
|
tree = build_directory_tree(MARKDOWN_ROOT)
|
||||||
|
return tree
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/file", response_model=FileContent)
|
||||||
|
def get_markdown_file(path: str = Query(..., description="相对于根目录的 Markdown 路径")) -> FileContent:
|
||||||
|
"""
|
||||||
|
获取指定 Markdown 文件内容。
|
||||||
|
|
||||||
|
Query 参数:
|
||||||
|
- path: 例如 'AI/大语言模型的API 调用.md'
|
||||||
|
"""
|
||||||
|
file_path = resolve_markdown_path(path)
|
||||||
|
|
||||||
|
if not file_path.exists() or not file_path.is_file() or not is_markdown_file(file_path):
|
||||||
|
raise HTTPException(status_code=404, detail="文件不存在")
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = file_path.read_text(encoding="utf-8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# 回退编码
|
||||||
|
content = file_path.read_text(encoding="utf-8", errors="ignore")
|
||||||
|
|
||||||
|
# 获取文件统计信息
|
||||||
|
file_stat = file_path.stat()
|
||||||
|
|
||||||
|
# 计算字数(去除空格和换行符)
|
||||||
|
word_count = len(content.replace(" ", "").replace("\n", "").replace("\r", "").replace("\t", ""))
|
||||||
|
|
||||||
|
# 获取文件大小(字节)
|
||||||
|
file_size = file_stat.st_size
|
||||||
|
|
||||||
|
# 获取创建时间和修改时间
|
||||||
|
try:
|
||||||
|
# Windows 上 st_ctime 是创建时间,Linux 上是元数据更改时间
|
||||||
|
created_time = datetime.fromtimestamp(file_stat.st_ctime).strftime("%Y年%m月%d日 %H:%M:%S")
|
||||||
|
except:
|
||||||
|
created_time = "未知"
|
||||||
|
|
||||||
|
try:
|
||||||
|
modified_time = datetime.fromtimestamp(file_stat.st_mtime).strftime("%Y年%m月%d日 %H:%M:%S")
|
||||||
|
except:
|
||||||
|
modified_time = "未知"
|
||||||
|
|
||||||
|
rel = file_path.relative_to(MARKDOWN_ROOT).as_posix()
|
||||||
|
return FileContent(
|
||||||
|
path=rel,
|
||||||
|
content=content,
|
||||||
|
word_count=word_count,
|
||||||
|
file_size=file_size,
|
||||||
|
created_time=created_time,
|
||||||
|
modified_time=modified_time
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/health")
|
||||||
|
def health_check():
|
||||||
|
"""简单健康检查接口。"""
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run("main:app", host="0.0.0.0", port=int(os.getenv("PORT", 8000)), reload=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -13,12 +13,12 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"type": "markdown",
|
"type": "markdown",
|
||||||
"state": {
|
"state": {
|
||||||
"file": "Docker/优秀好用的Docker镜像/Gitea-私有化仓库部署.md",
|
"file": "临时/代码片段.md",
|
||||||
"mode": "source",
|
"mode": "source",
|
||||||
"source": false
|
"source": false
|
||||||
},
|
},
|
||||||
"icon": "lucide-file",
|
"icon": "lucide-file",
|
||||||
"title": "Gitea-私有化仓库部署"
|
"title": "代码片段"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -154,42 +154,42 @@
|
|||||||
},
|
},
|
||||||
"active": "a93d6d3811397e7b",
|
"active": "a93d6d3811397e7b",
|
||||||
"lastOpenFiles": [
|
"lastOpenFiles": [
|
||||||
"编程语言/Java/JavaSpring标准项目架构.md",
|
"临时/Obsidion/Obsidion美化.md",
|
||||||
"树萌芽的小本本/树萌芽已部署网站(不定时持续更新).md",
|
"临时/Obsidion",
|
||||||
"Linux/ADB/ADB应用启动命令.md",
|
"运维/nginx快速配置反向代理.md",
|
||||||
"Linux/ADB/ADB常用命令.md",
|
"临时/代码片段.md",
|
||||||
"树萌芽的小本本/重要信息记录.md",
|
"计算机网络/计算机网络期末考试综合题简单论述.md",
|
||||||
"树萌芽の小想法/革命后的理想.md",
|
"计算机科普/网络协议科普.md",
|
||||||
|
"计算机科普/科普-Nagle 算法.md",
|
||||||
|
"计算机科普/多模态大模型识别图片,视频,音频原理.md",
|
||||||
|
"计算机科普/操作系统科普.md",
|
||||||
|
"计算机科普/编程语言之间的划分.md",
|
||||||
|
"计算机科普/编程语言科普.md",
|
||||||
|
"AI/大语言模型的API 调用.md",
|
||||||
|
"AI/大语言模型的API key.md",
|
||||||
|
"AI/控制台AI大模型.md",
|
||||||
|
"Docker/优秀好用的Docker镜像/registry-轻量级自建Docker镜像仓库.md",
|
||||||
"内网穿透/Wireguard/Wireguard基础命令.md",
|
"内网穿透/Wireguard/Wireguard基础命令.md",
|
||||||
"编程语言/Golang/Golang标准代码架构.md",
|
"生活科普/男生烫发术语.md",
|
||||||
"编程语言/前端/React标准项目架构.md",
|
"生活科普/中国免签知识科普.md",
|
||||||
"编程语言/前端/Vue标准项目架构.md",
|
"编程语言/Golang/Golang各平台编译教程.md",
|
||||||
"编程语言/前端/纯静态网页的强大功能与应用.md",
|
"树萌芽的小本本/大萌芽-Debian13服务器.md",
|
||||||
"编程语言/前端/JavaScript趣味题/JavaScript趣味题_128.md",
|
"树萌芽的小本本/树萌芽国外身份.md",
|
||||||
"编程语言/前端/React项目初始化教程.md",
|
"树萌芽的小本本/重要信息记录.md",
|
||||||
"编程语言/前端/OpenList美化代码.md",
|
"Linux/随身WiFi/随身WiFi一些记录.md",
|
||||||
"编程语言/前端/css注入代码合集.md",
|
"树萌芽的小本本/树萌芽の编程想法.md",
|
||||||
"编程语言/前端/Vue项目初始化教程.md",
|
"树萌芽的小本本/树萌芽の小秘密.md",
|
||||||
"编程语言/前端/代码片段/代码片段-特殊HelloWorld输出.md",
|
"树萌芽的小本本/树萌芽の吐槽.md",
|
||||||
"编程语言/前端/前端html导入css和js方法.md",
|
"树萌芽的小本本/革命后的理想.md",
|
||||||
"编程语言/前端/代码片段/代码片段-标准HelloWorld输出.md",
|
|
||||||
"编程语言/前端/代码片段",
|
"编程语言/前端/代码片段",
|
||||||
"编程语言/前端/JavaScript趣味题/JavaScript趣味题_28.md",
|
|
||||||
"编程语言/前端/React打包成Windows和Android软件方案.md",
|
|
||||||
"编程语言/前端/JavaScript趣味题/JavaScript趣味题_18.md",
|
|
||||||
"编程语言/前端/nodejs的markdown库.md",
|
|
||||||
"实习求职/面试经历/27双非本一腾讯IEG游戏安全后台实习面经.md",
|
|
||||||
"编程语言/前端/企业级标准React项目架构.md",
|
|
||||||
"编程语言/前端/JavaScript趣味题",
|
"编程语言/前端/JavaScript趣味题",
|
||||||
"实习求职/面试经历",
|
"实习求职/面试经历",
|
||||||
"编程语言/前端",
|
"编程语言/前端",
|
||||||
"编程语言/前端&HTML&CSS&JS/React项目初始化教程.md",
|
|
||||||
"数据库/SQLite",
|
"数据库/SQLite",
|
||||||
"数据库/MongoDB",
|
"数据库/MongoDB",
|
||||||
"数据库/MySQL",
|
"数据库/MySQL",
|
||||||
"内网穿透/Wireguard/配置/wgs-alyxg.conf",
|
"内网穿透/Wireguard/配置/wgs-alyxg.conf",
|
||||||
"内网穿透/Wireguard/配置/wgs-alycd.conf",
|
"内网穿透/Wireguard/配置/wgs-alycd.conf",
|
||||||
"内网穿透/Wireguard/配置/wgc-win.conf",
|
|
||||||
"Minecraft/基岩版服务器/NukkitLearn/images/5-04.png",
|
"Minecraft/基岩版服务器/NukkitLearn/images/5-04.png",
|
||||||
"Minecraft/基岩版服务器/NukkitLearn/images/5-03.png",
|
"Minecraft/基岩版服务器/NukkitLearn/images/5-03.png",
|
||||||
"Minecraft/基岩版服务器/NukkitLearn/images/5-02.png",
|
"Minecraft/基岩版服务器/NukkitLearn/images/5-02.png",
|
||||||
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 371 KiB After Width: | Height: | Size: 371 KiB |
|
Before Width: | Height: | Size: 4.9 MiB After Width: | Height: | Size: 4.9 MiB |
|
Before Width: | Height: | Size: 5.1 MiB After Width: | Height: | Size: 5.1 MiB |
|
Before Width: | Height: | Size: 4.8 MiB After Width: | Height: | Size: 4.8 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 924 KiB After Width: | Height: | Size: 924 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 4.8 MiB After Width: | Height: | Size: 4.8 MiB |
|
Before Width: | Height: | Size: 257 KiB After Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 4.7 MiB After Width: | Height: | Size: 4.7 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 493 KiB After Width: | Height: | Size: 493 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |