# codebase_to_blog
**Repository Path**: Morphlng/codebase_to_blog
## Basic Information
- **Project Name**: codebase_to_blog
- **Description**: Let https://github.com/zarazhangrui/codebase-to-course generate the HTMLs, we will serve it for you then!
- **Primary Language**: Python
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-08-16
- **Last Updated**: 2026-08-28
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 📚 codebase_to_blog
**把任意代码库生成的交互式 HTML 课程,变成组内可搜索、可分享、可回看的在线课程库。**
[]()
[]()
[]()
[]()
---
## 🎯 这是什么?
[codebase-to-course](https://github.com/zarazhangrui/codebase-to-course) 能把代码库变成精美的交互式课程,但产物是一个**目录**(`index.html` + `styles.css` + `main.js` + 多个子文件),不适合直接分享。
`codebase_to_blog` 解决最后一步:
```text
课程目录 → 打包 zip → 上传到 codebase_to_blog → 获得可分享的在线阅读链接
```
服务会帮你完成解包校验、元数据提取、版本管理、安全隔离、搜索索引和静态文件托管。
---
## ✨ 功能一览
| 模块 | 功能 |
| --- | --- |
| 🔍 课程库首页 | 搜索课程名 / 仓库链接 / 描述 / 标签,支持来源筛选、排序、分页 |
| 📤 上传课程 | 拖拽或选择 zip / 单个 HTML 文件,填写课程名 / 仓库链接 / 描述 / 标签 / 访问密码 |
| ⏳ 上传状态弹窗 | 独立弹窗展示上传中、校验中、解包中、发布中、成功 / 失败 |
| 🧬 版本管理 | 相同仓库链接重复上传自动追加新版本;课程卡片可直接**更新**(新版本覆盖,需同类型) |
| 📖 课程阅读页 | 顶部工具栏 + 全屏 iframe,支持复制链接、新窗口、全屏 |
| 🧑🏫 讲演模式 | 会议室无投屏时一键生成只读同步链接,实时同步主讲人的翻页、滚动 |
| 🔒 密码保护 | 上传时可设置访问密码:阅读页与课程文件需解锁,删除需密码(管理员免密) |
| 🗑️ 删除与恢复 | 组内用户可删除(二次确认);管理员可恢复或彻底删除 |
| ♻️ 自动清理 | 软删除课程超过保留期后,由后台任务自动物理删除并释放空间 |
| 🛡️ 安全隔离 | 课程 HTML/JS 被视为不可信内容,使用 iframe sandbox + CSP 双重隔离 |
| 🌐 部署友好 | Flask + Gunicorn,支持 Nginx `ProxyFix` 与子路径挂载 |
---
## 📦 上传内容的要求
支持两种格式:**单个自包含 HTML 文件**(最简单)或**课程 zip**(codebase-to-course 产物)。
### ✅ 方式 A:单个自包含 HTML 文件
直接上传 `.html` / `.htm` 文件即可,无需打包:
- 内容必须是完整 HTML 文档(含 `` / `` 等标记),样式与脚本需内联自包含。
- 单个文件默认上限 `MAX_SINGLE_FILE_MB`(50MB),标题/主题色/模块数会从 HTML 自动提取。
### ✅ 方式 B:课程 zip(需含三个必需文件)
上传内容必须包含 `index.html`、`styles.css`、`main.js` 三个文件。
**布局 A:直接是文件内容**
进入课程目录后,打包目录内容:
```bash
cd deepseek-harness-course
zip -r ../deepseek-harness-course.zip .
```
```text
deepseek-harness-course.zip
├── index.html
├── styles.css
├── main.js
├── _base.html # 可选,默认不对外公开
├── _footer.html # 可选,默认不对外公开
├── build.sh # 可选,默认不对外公开
├── briefs/ # 可选,默认不对外公开
└── modules/ # 可选
```
**兼容布局 B:zip 内只有一层课程目录**
直接右键压缩课程文件夹也可以:
```text
deepseek-harness-course.zip
└── deepseek-harness-course/
├── index.html
├── styles.css
├── main.js
└── ...
```
程序会自动剥离唯一的一层目录。
### ❌ 不接受的情况
- 非 zip 文件、加密 zip
- 路径穿越、符号链接、zip 炸弹
- zip 缺少 `index.html` / `styles.css` / `main.js`
- 存在多个候选课程根目录且无法唯一判断
- `.html` 文件内容不是有效的 HTML 文档
---
## 🚀 快速开始
### 1. 安装依赖
需要 Python 3.8+ 和 [uv](https://docs.astral.sh/uv/):
```bash
cd codebase_to_blog
uv sync --dev
```
### 2. 准备配置
```bash
cp .env.example .env
```
至少修改两项:
```dotenv
SECRET_KEY=请改成足够长的随机字符串
ADMIN_TOKEN=你们组内的管理令牌
```
### 3. 启动开发服务
```bash
# 方式一:flask CLI
uv run flask --app 'app.main:create_app()' run --debug --host 0.0.0.0 --port 8000
# 方式二:直接运行(支持命令行覆盖 host / port)
uv run python -m app.main --host 127.0.0.1 --port 8000
```
打开 👉
---
## ⚙️ 配置说明
完整示例见 `.env.example`。
| 配置项 | 默认值 | 必填 | 说明 |
| --- | --- | --- | --- |
| `APP_ENV` | `development` | 否 | `development` / `production`;production 会开启 Secure Cookie |
| `SECRET_KEY` | 开发默认值 | ✅ 生产必改 | 签名 Flask session;CSRF Token 和管理员登录都依赖它 |
| `DATA_DIR` | `./data` | 否 | SQLite 数据库和课程文件存储目录 |
| `ADMIN_TOKEN` | 空 | 建议配置 | `/admin` 登录令牌;不配置时管理页显示指引 |
| `PUBLIC_BASE_URL` | 空 | Nginx 子路径部署时配置 | 对外分享地址,需包含子路径前缀 |
| `MAX_UPLOAD_MB` | `200` | 否 | 单个 zip 上传大小上限(首页会按此值提示) |
| `MAX_SINGLE_FILE_MB` | `50` | 否 | 单个 HTML 文件 / zip 内单文件大小上限 |
| `SOFT_DELETE_RETENTION_DAYS` | `30` | 否 | 软删除课程保留天数,超期自动物理删除 |
| `MAINTENANCE_INTERVAL_HOURS` | `24` | 否 | 后台自动物理清理间隔 |
| `SUBMISSION_HISTORY_LIMIT` | `5` | 否 | 提交历史默认展示条数(弹窗内可切 5/10/20/50/全部) |
| `TRUST_PROXY_HEADERS` | `false` | 仅 Nginx 后置时 | 是否启用 Werkzeug `ProxyFix` |
| `TRUSTED_PROXIES` | `127.0.0.1,::1` | 仅 Nginx 后置时 | 可信反向代理 IP 列表 |
> `SECRET_KEY` 看起来“没用到”,但它用于签名 Flask session。CSRF Token 和管理员登录状态都存在 session cookie 中;使用默认值会允许他人伪造登录态。
---
## 🖥️ 使用说明
### 首页
- 顶部搜索框是主要入口,支持课程名、仓库名、仓库链接、描述和标签。
- 可以按 `GitHub / GitLab / Gitee / 其他 / 无链接` 筛选,按最近更新、浏览量、名称排序。
- 每个卡片显示课程标题、来源、标签、版本、模块数、更新时间;受密码保护的课程带 🔒 标记。
### 上传课程
1. 点击右上角 **+ 上传课程**。
2. 填写课程名称(必填)和仓库链接(选填)。
3. 拖拽或选择 zip / 单个 HTML 文件。
4. (选填)设置**访问密码**:设置后阅读与删除都需要密码;留空表示公开。
5. (选填)填写**提交说明**:如"修正错别字",会显示在提交历史中,便于区分各版本。
6. 点击 **开始上传**,会弹出独立的状态弹窗。
7. 成功后可选择:
- **立即查看**:直接打开课程;
- **返回上传**:关闭状态弹窗,回到上传表单;
- **确认**:关闭所有弹窗。
8. 失败后可以选择 **返回上传**,已填写的表单和文件都会保留。
### 更新课程(修改文件内容)
课程卡片上的 **更新** 按钮用于修改课程内容,无需删除重传:
1. 点击课程卡片上的 **更新**(与"删除"并排)。
2. 弹窗进入更新模式:课程名称与仓库链接自动锁定,隐藏重复策略。
3. 上传**与当前课程同类型**的内容:
- 当前是 zip 课程包 → 上传 zip;
- 当前是单个 HTML → 上传 HTML。
类型不一致会提示错误(`CONTENT_KIND_MISMATCH`),不会发布。
4. 受密码保护的课程需输入课程密码才能更新(管理员免密);更新**不会**修改课程密码。
5. 更新成功后生成**新版本**:首页与 `/courses//` 指向最新版本,旧版本直链仍然有效。
> 接口层:上传时携带 `update_slug` 参数即可(浏览器与 Agent 接口均支持),无需依赖仓库链接匹配。
### 查看课程
- 点击课程卡片进入阅读页。
- iframe 内运行原始课程 HTML/JS/CSS。
- 顶部工具栏支持:**提交历史**、复制分享链接、新窗口打开、全屏浏览。
- 受密码保护的课程会先显示密码输入页;解锁后本浏览器会话内可正常访问(课程文件同样受保护,无法绕过)。
#### 提交历史(Submission history)
点击阅读页工具栏的 **历史** 按钮,可查看该课程的提交记录(arXiv 风格):
- 弹窗顶部可切换显示条数:**5 / 10 / 20 / 50 / 全部**(默认档位由 `SUBMISSION_HISTORY_LIMIT` 配置,默认 5)。
- 每条记录显示:版本号、提交时间(UTC)、内容类型(zip 课程包 / 单个 HTML)、大小、校验和前缀,以及**提交说明**(上传时填写的 `comment`)。
- 当前版本高亮标记;点击 **查看 ↗** 可直接打开对应历史版本(旧版本直链长期有效)。
- 历史版本数据全部保留,面板仅控制展示条数;完整版本列表可通过 `GET /api/v1/courses//versions` 获取。
#### 🧑🏫 讲演模式(Presentation Mode)
当在没有投屏的会议室或需要向他人同步演示时,可在课程阅读页使用讲演模式:
1. 主讲人打开课程阅读页,点击工具栏中的 **开启讲演**。
2. 在弹出的选项中选择**链接有效期**(支持 30分钟 / 1小时 / 2小时 / 4小时 / 24小时),点击“生成讲演链接”。
3. 系统自动复制专属讲演链接(形如 `/view/?presentation=`),主讲人端显示“讲演同步中 🟢”。
4. 其他参会者打开该链接后:
- 看到的是**只读同步视图**(覆盖透明防误触遮罩,不可主动操作干扰进度)。
- 主讲人的**页面滚动、滑动**会实时同步给所有参会者。
- 即使原课程受密码保护,参会人在讲演有效期内也无需输入密码即可直接进入同步视图,保护密码不泄露。
- 讲演会话到期后自动失效,后台定时维护任务会自动清理过期记录。
### 删除课程
- 任何组内用户都可以在课程卡片上点击 **删除**。
- 必须经过确认弹窗,避免误操作。
- 受密码保护的课程,删除时需输入正确的课程密码,否则删除失败(管理员免密)。
- 删除后课程从首页消失,原链接返回 404。
### 管理页面
直接访问 `/admin`,或点击首页右上角 **管理**。
| 操作 | 谁可以做 |
| --- | --- |
| 首页删除课程 | 任何组内用户(确认弹窗) |
| 恢复软删除课程 | 仅管理员 |
| 立即彻底删除某课程 | 仅管理员 |
| 批量彻底删除已软删除课程 | 仅管理员(勾选多个或一键全部) |
| 设置 / 修改 / 清除课程访问密码 | 仅管理员(普通用户无法自行改密) |
| 立即清理所有已过期课程 | 仅管理员 |
| 查看上传任务与统计 | 仅管理员 |
管理页面还提供:
- **搜索**:按课程名称 / 标题 / slug / 仓库链接实时过滤课程列表。
- **状态过滤**:全部 / 已发布 / 已软删除三个 Tab。
- **批量删除**:在“已软删除”Tab 下勾选课程(或全选)后**彻底删除选中**,或一键**全部彻底删除**(均需二次确认,操作不可恢复)。
---
## 🗑️ 软删除与物理清理
```text
已发布 ──删除──▶ 已软删除 ──30 天后──▶ 自动物理删除
│
└──管理员恢复──▶ 已发布
```
- **软删除**:首页不可见、直链 404,但数据库和磁盘文件仍保留。
- **恢复**:管理员可在 `/admin` 恢复。
- **物理删除**:删除磁盘文件、数据库记录和浏览统计,不可恢复。
- **自动清理**:后台线程每 `24` 小时扫描一次,物理删除超过保留期的课程。
- **手动清理**:管理员可在 `/admin` 点击“立即清理已过期课程”。
---
## 🌐 Nginx 部署
推荐拓扑:
```text
浏览器 → Nginx (TLS) → 127.0.0.1:8000 Gunicorn/Flask
```
子路径 `/blog/` 示例:
```nginx
server {
listen 443 ssl;
server_name courses.example.com;
client_max_body_size 220m;
location /blog/ {
proxy_pass http://127.0.0.1:8000/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Prefix /blog;
proxy_read_timeout 300s;
}
}
```
对应 `.env`:
```dotenv
APP_ENV=production
TRUST_PROXY_HEADERS=true
TRUSTED_PROXIES=127.0.0.1,::1
PUBLIC_BASE_URL=https://courses.example.com/blog
```
生产进程:
```bash
uv run gunicorn -w 1 --threads 8 -b 127.0.0.1:8000 'app.main:create_app()'
```
> 应用端口只应监听 `127.0.0.1`,外部访问统一由 Nginx 进入。
---
## 🔌 API 概览
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| `GET` | `/` | 课程库首页 |
| `GET` | `/view/` | 课程阅读页 |
| `GET` | `/courses//` | 跳转到课程最新版本 |
| `GET` | `/v//` | 课程原始 HTML(带沙箱 CSP) |
| `GET` | `/api/v1/courses` | 搜索 / 筛选 / 分页 |
| `GET` | `/api/v1/courses/lookup` | 上传前查重 |
| `POST` | `/api/v1/uploads` | 上传课程 zip 或单个 HTML,返回任务 ID(支持 `update_slug` 更新已有课程) |
| `POST` | `/api/v1/agent/uploads` | Agent 上传课程(Bearer Token 认证,无需 CSRF,支持 `update_slug`) |
| `GET` | `/api/v1/uploads/` | 查询上传进度 |
| `POST` | `/api/v1/courses//unlock` | 输入密码解锁课程(受保护课程) |
| `POST` | `/api/v1/courses//presentation` | 开启讲演会话并生成同步链接(支持 `expires_minutes`) |
| `GET` | `/api/v1/presentation/` | 查询讲演会话状态及当前快照 |
| `POST` | `/api/v1/presentation//state` | 主讲人更新讲演状态快照(翻页/滚动/高亮) |
| `DELETE` | `/api/v1/courses/` | 软删除课程(组内公开,需 CSRF;受保护课程需带密码,管理员免密) |
| `POST` | `/api/v1/admin/session` | 管理员登录 |
| `GET` | `/api/v1/admin/courses?q=&status=` | 全部课程(管理员,支持搜索与状态过滤) |
| `POST` | `/api/v1/admin/courses//password` | 设置 / 修改 / 清除课程密码(管理员) |
| `POST` | `/api/v1/admin/courses/purge-batch` | 批量彻底删除已软删除课程(管理员) |
| `POST` | `/api/v1/courses//restore` | 恢复课程(管理员) |
| `POST` | `/api/v1/courses//purge` | 立即物理删除课程(管理员) |
| `POST` | `/api/v1/admin/maintenance/purge` | 清理所有已过期课程(管理员) |
| `GET` | `/api/v1/healthz` / `/api/v1/readyz` | 健康检查 |
---
## 🤖 AI Agent 接入(Claude Code 等)
`codebase_to_blog` 提供面向 AI Agent 的上传接口:Agent 可以直接帮用户提交课程 zip,**不需要模拟浏览器操作**(浏览器接口依赖 CSRF 会话,Agent 无法完成)。
### Agent API
- **端点**:`POST /api/v1/agent/uploads`(multipart/form-data,字段与浏览器上传一致:`name` / `repo_url` / `description` / `tags` / `on_duplicate` / `password` / `update_slug` / `file`)
- **认证**:`Authorization: Bearer `(复用 `.env` 中的 `ADMIN_TOKEN`;未配置时接口返回 `503 AGENT_DISABLED`)
- **流程**:提交返回 `202 + job_id` → 轮询公开的 `GET /api/v1/uploads/` 直到 `succeeded`(读取 `course_slug`)或 `failed`(读取 `error_code` / `error_message`)
```bash
# 1. 提交
curl -sS -X POST "${BASE_URL}/api/v1/agent/uploads" \
-H "Authorization: Bearer ${ADMIN_TOKEN}" \
-F "name=my-course" -F "repo_url=https://github.com/org/repo" \
-F "description=一句话说明" -F "tags=agent evaluation" \
-F "file=@course.zip"
# 2. 轮询(直到 succeeded / failed)
curl -sS "${BASE_URL}/api/v1/uploads/"
```
分享链接格式:`${BASE_URL}/view/`。
### 安装 Skill(可选)
仓库内已提供一个 Claude Code 风格 Skill:`skills/code2blog-course-upload/SKILL.md`,它描述了 zip 要求、调用步骤、轮询与错误码对照,Agent 看到"上传课程"类请求时会自动按此流程操作。安装方式(任选其一):
```bash
# 用户级(所有项目可用)
mkdir -p ~/.claude/skills && cp -r skills/code2blog-course-upload ~/.claude/skills/
# 或项目级(仅当前项目)
mkdir -p .claude/skills && cp -r skills/code2blog-course-upload .claude/skills/
```
> Agent 使用前需要知道服务地址与 `ADMIN_TOKEN`(可在部署时通过环境变量提供给 Agent,或由用户在对话中提供)。
---
## 📁 项目结构
```text
codebase_to_blog/
├── app/
│ ├── main.py # Flask 应用工厂
│ ├── core/ # 配置、日志、异常、安全、响应
│ ├── db/ # SQLAlchemy engine / session
│ ├── models/ # Course、CourseVersion、UploadJob、统计
│ ├── schemas/ # Pydantic 请求 / 响应模型
│ ├── repositories/ # 数据库查询层
│ ├── services/ # 上传、解包、发布、搜索、存储、清理
│ ├── workers/ # 上传任务与后台维护线程
│ ├── api/ # JSON API Blueprints(含 Agent 接口)
│ └── web/ # 页面路由、Jinja2 模板、CSS/JS
├── skills/
│ └── code2blog-course-upload/ # Claude Code Skill(Agent 上传流程)
├── tests/
│ ├── unit/ # 归档校验、ThreadRunner 等单元测试
│ └── api/ # 课程、上传、Agent、管理、ProxyFix API 测试
├── .env.example # 配置示例
├── pyproject.toml
└── uv.lock
```
---
## 🧪 开发与测试
```bash
# 运行测试
uv run pytest -q
# 格式化(顺序:先 isort,后 black)
uv run isort .
uv run black .
# 语法检查
python -m compileall -q app tests
```
当前测试覆盖:上传校验、zip 安全、搜索分页、版本追加、课程文件 CSP、软删除/恢复/物理清理、ProxyFix 子路径等。
---
## ❓ FAQ
**Q1:为什么课程在 iframe 里能跑,但管理页面不受影响?**
课程 HTML/JS 被视为不可信内容。iframe 使用 `sandbox="allow-scripts"`,课程响应额外附加 CSP `sandbox allow-scripts`,课程脚本运行在 opaque origin 中,无法读取管理页面、Cookie 或调用管理 API。
**Q2:课程依赖 Google Fonts,离线会坏吗?**
不会。字体加载失败只影响字体外观,课程 HTML/CSS/JS 本身不依赖外部服务。
**Q3:重复上传同一个仓库会覆盖旧版本吗?**
不会。默认追加为新版本,旧版本直链继续有效;首页和 `/courses//` 指向最新版本。
**Q4:删除课程后磁盘空间什么时候释放?**
默认 30 天后由后台任务物理删除;管理员也可以在 `/admin` 中立即彻底删除或手动触发清理。
**Q5:为什么我没有管理页面登录入口?**
管理入口始终在首页右上角。只有 `.env` 中配置了 `ADMIN_TOKEN`,登录功能才会启用;否则页面会显示配置指引。
**Q6:课程设置了密码,为什么分享链接还是能打开密码页?**
这是预期行为:密码页本身公开,但**课程内容与课程文件**必须输入正确密码解锁后才能访问(课程文件接口同样校验,无法绕过)。解锁状态保存在当前浏览器的会话中。
**Q7:忘记课程密码怎么办?**
联系管理员。管理员可以在 `/admin` 的课程列表中点击 **修改密码** 直接重置,或 **清除密码** 让课程恢复公开;普通用户无法自行修改课程密码。
**Q8:单个 HTML 文件上传后还能更新吗?**
可以。点击课程卡片上的 **更新**(或上传时携带 `update_slug` 参数)即可为任意课程追加新版本;要求新上传内容与课程当前类型一致(单 HTML ↔ zip 不能混用)。旧版本直链在更新后仍然有效。
---
## 📄 License
[MIT](./LICENSE) © Morphlng