# 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 课程,变成组内可搜索、可分享、可回看的在线课程库。** [![Python](https://img.shields.io/badge/Python-3.8+-3776AB?logo=python&logoColor=white)]() [![Flask](https://img.shields.io/badge/Flask-3.x-000000?logo=flask&logoColor=white)]() [![SQLite](https://img.shields.io/badge/SQLite-3.x-003B57?logo=sqlite&logoColor=white)]() [![License](https://img.shields.io/badge/License-MIT-8A2BE2)]()
--- ## 🎯 这是什么? [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