openpilot 文档站开发指南:用 docs/serve.py 构建与发布 docs.comma.ai
openpilot 官方文档站的源码就是仓库里的 docs/ 目录,本文围绕 docs/README.md 讲解文档站的两条核心命令(一次性构建与本地热重载预览),并深入 docs/serve.py 这个零依赖的 Markdown 转 HTML 站点生成器的实现细节,以及 .github/workflows/docs.yaml 如何将构建产物自动推送到线上站点。读完本文,你可以独立完成本地构建、预览文档改动,并理解从一次 master 推送直到站点更新的完整 CI 链路。
文档即源码:docs/ 目录的结构
docs/README.md 开宗明义:docs/ 树就是线上文档站的源码,站点在推送 master 时由工作流自动更新。当前 docs/ 目录的组织方式是:
- docs/index.md:站点首页,回答 "What is openpilot?",内容概括了 openpilot 提供的 ACC、ALC、FCW、LDW 与驾驶员监控功能;
docs/how-to/:操作指南,如 car-port.md(添加车辆支持)、connect-to-comma.md(连接 comma 设备)、replay-a-drive.md;docs/concepts/:概念文档,如 logs.md、safety.md;docs/contributing/:贡献相关文档,如 architecture.md、roadmap.md;- docs/CARS.md、docs/SAFETY.md、docs/LIMITATIONS.md 等顶层文档;
- docs/assets/:站点静态资源(favicon、logo 等);
- 站点基础设施:docs/serve.py(构建器 + 服务器)、docs/template.html(页面模板)。
从源码结构看,侧边栏导航不是从文件树自动扫描出来的,而是由 docs/serve.py 中硬编码的 NAV 列表显式定义的(一组 (标题, 目标) 元组,目标为 None 表示分节标题)。这意味着新增一个文档页时,除了在对应目录放 .md 文件外,还应考虑是否需要在 NAV 中登记入口。
两条核心命令:构建与本地运行
docs/README.md 给出了文档站开发的全部两条命令:
1. 构建站点
python docs/serve.py --build
2. 本地运行站点(文件变化时自动重建)
python docs/serve.py
--build:一次性构建
--build 参数的解析位于 docs/serve.py 的入口处,仅有一个布尔开关。执行后调用 build() 函数(docs/serve.py),其流程是:
- 读取 docs/template.html 模板;
- 递归收集
docs/下所有*.md文件,但排除 docs/README.md 本身以及_site、__pycache__目录(见EXCLUDE_DIRS,docs/serve.py); - 追加一个动态生成的术语表页(
concepts/glossary.md),其内容由GLOSSARY_DESCRIPTIONS字典渲染,覆盖 onroad、offroad、route、segment、panda、comma four 等术语(docs/serve.py); - 删除并重建输出目录
docs/_site/,把模板以外的所有资源文件(图片、SVG 等)按原相对路径拷贝进去(copy_assets(),docs/serve.py); - 对每个 Markdown 页渲染正文、提取标题,用字符串替换填充模板占位符
{{TITLE}}、{{ROOT}}、{{HOME_HREF}}、{{NAV}}、{{BODY}}、{{EDIT_URL}},输出到docs/_site/<路由>/index.html,并打印docs: built N pages into ...。
其中 {{EDIT_URL}} 指向该页面对应的 GitHub 编辑地址(docs/serve.py),模板底部据此渲染 "Edit this page on GitHub" 链接(docs/template.html)。
不带参数:本地热重载服务
python docs/serve.py 会先进入构建,然后启动一个 ThreadingHTTPServer,监听随机可用端口,并在终端打印实际地址(如 http://localhost:PORT/)。其监视逻辑在 docs/serve.py:
- 每 0.5 秒轮询一次
docs/下所有受监视文件(同样排除_site和__pycache__)的 mtime; - 一旦发现 mtime 发生变化,打印
docs: change detected, rebuilding...并重新执行完整构建; - 构建失败会打印
docs: build failed: ...但服务不退出,继续监视; Ctrl-C触发KeyboardInterrupt,服务器优雅关闭。
这种"轮询 mtime + 全量重建"的设计意味着本地预览是秒级反馈、且永远与构建逻辑一致,适合编辑文档时开一个终端挂着。
深入 serve.py:一个零依赖的 Markdown 站点生成器
serve.py 只使用 Python 标准库(argparse、http.server、re、html、pathlib、threading 等),没有引入任何第三方 Markdown 解析库,全部渲染逻辑手写。对文档作者最有价值的行为细节如下:
页面路由规则
page_route()(docs/serve.py)决定每个 Markdown 文件发布后的 URL 形态:
index.md映射到所在目录本身,例如index.md→ 站点根路径,how-to/connect-to-comma.md这类非 index 页映射到去掉.md后缀的路径;- 对于非 index 页,构建器还会额外写一个
foo.html跳转文件(write_html_redirect(),docs/serve.py),通过 meta refresh 和location.replace把访问旧式.html地址的用户重定向到foo/目录形式,并保留 query 与 hash——这是对旧链接的兼容处理; - 页面间的相对链接由
page_href()/rewrite_relative_url()统一重写成基于路由的相对地址(docs/serve.py)。注意其边界条件:以#、/开头、带 scheme 或域名、以及**向上跳出版本目录(../)**的目标都不会被重写。因此写文档时,跨目录引用要么用同目录相对路径、要么用外部 URL,不要依赖../跨越站点根。
内置的 Markdown 语法支持
_render_blocks()(docs/serve.py)按块级元素逐个识别,支持的特性包括:
| 语法 | 渲染行为 |
|---|---|
| 围栏代码块 ``` | 输出 <pre><code class="language-xxx">,语言名取自围栏后缀;模板侧还会为每个代码块注入 copy 按钮(docs/template.html) |
#~###### 标题 |
生成 slug 锚点 id,并追加 hover 才显示的 headerlink(# 永久链接) |
--- 分隔线 |
<hr> |
| 表格 | 识别分隔行中的 :---、:--:、---: 并转换为 th/td 的左右/居中对齐样式 |
| 有序/无序列表 | 支持嵌套(缩进按 4 空格一级)、同一层级 ul/ol 混排 |
> 引用块 |
普通 blockquote;若首行为 [!NOTE]、[!TIP]、[!IMPORTANT]、[!WARNING]、[!CAUTION],则渲染为带颜色左边框的 admonition 提示框(docs/template.html) |
| 行内 HTML / 注释 | 原样保留(会先对其中的 href/src 做相对路径重写) |
| 行内语法 | 反引号行内代码、图片、链接、**粗体**、*斜体*、行尾双空格换行,以及裸 URL 自动加链(autolink_plain()) |
另外 esc() 在转义 HTML 时会保留已存在的 HTML 实体(如 &、')不被二次转义(docs/serve.py)。
全局术语表注入
inject_glossary()(docs/serve.py)会在除术语表页以外的所有页面正文中,把 GLOSSARY_DESCRIPTIONS 定义的术语(如 "onroad"、"route"、"segment"、"panda")替换为带悬停 tooltip 的链接,点击可跳转到术语表对应锚点。注入时会跳过 code、h1~h6、pre、kbd、script、style 等标签内部(GLOSSARY_SKIP,docs/serve.py),避免在代码或标题里误伤词面。tooltip 的悬浮样式见 docs/template.html。
页面标题的取法
page_title()(docs/serve.py)取文档第一个 # 开头行作为 <title>,最终呈现为 "页标题 · openpilot docs"(docs/template.html);若文档没有一级标题则回退为 "openpilot docs"。所以每篇文档的第一行 # 标题 直接决定浏览器标签与站点导航外的页面身份,撰写时不要省略。
CI 发布链路:从 master 推送到线上站点
docs/README.md 提到站点由工作流在推送 master 时更新,对应 .github/workflows/docs.yaml。该工作流的关键环节:
- 触发条件:
master分支 push、任何 PR,以及可带run_number输入的workflow_call;通过concurrency组 +cancel-in-progress: true保证同分支只保留最新一次运行; - 环境准备:runner 为
ubuntu-24.04,checkout 带子模块,并通过环境变量lfs.fetchexclude在 clone 阶段排除openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx这个大文件(docs.yaml); - 构建:先
git lfs pull再python docs/serve.py --build(docs.yaml)。git lfs pull这一步是必需的——因为 .gitattributes 将*.png、*.svg、*.ttf等资源声明为 LFS 对象,docs 站点要拷贝的静态资源正是这些文件; - 推送线上(仅当
github.ref == 'refs/heads/master'且仓库为commaai/openpilot时执行):- 使用 secret
OPENPILOT_DOCS_KEY以 SSH 检出独立仓库commaai/openpilot-docs到openpilot-docs/目录(docs.yaml); - 通过
source tools/release/identity.sh(tools/release/identity.sh 统一设置提交者身份)创建一个 orphan 分支,清空后执行cp -r ../docs/_site/ docs/; touch docs/.nojekyll关闭 Jekyll 处理,并写入CNAME内容为docs.comma.ai;- 提交后
git push -f origin tmp:gh-pages,即文档最终托管在该仓库的gh-pages分支上。workflow 中的注释也说明了独立仓库的动机:避免构建产物撑大 openpilot 主仓库的全量克隆体积。
- 使用 secret
撰写与维护文档的实操建议
结合上述实现,向 docs/ 贡献内容时可以遵循以下约定:
- 新页面:放在
how-to/、concepts/、contributing/等已有分类下;首行必须是一级标题# ...(它决定页面标题);如希望出现在侧边栏,需要更新 docs/serve.py 的NAV; - 相对链接:只写同目录或站内相对路径,避免依赖
../上跳;外部链接会保持原样输出; - 提示框:在引用块首行写
[!NOTE]/[!TIP]/[!IMPORTANT]/[!WARNING]/[!CAUTION](大小写不敏感),即可获得对应配色的 admonition 样式; - 术语:新增术语应加进
GLOSSARY_DESCRIPTIONS(docs/serve.py),全站正文会自动获得 tooltip 链接,术语表页也会同步更新; - 资源文件:图片等资源放
docs/assets/或就近目录,copy_assets()会原样拷贝;注意二进制资源走 Git LFS,拉取代码后若资源为空壳,本地预览前需先git lfs pull(CI 正是这么做的); - 验证方式:改完跑一次
python docs/serve.py --build确认输出docs: built N pages into ...且无异常,或直接用python docs/serve.py在浏览器里检查渲染效果与导航高亮。
整套机制的核心思想是:文档以纯 Markdown 维护、以零依赖脚本构建、以独立仓库 + gh-pages 分支托管,构建逻辑集中在单个 docs/serve.py 中,本地开发(热重载预览)与线上发布(CI 构建推送)复用同一条构建路径,保证所见即所得。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00