machinaix-docs 文档站维护手册
Static Docs Pipeline · Markdown → HTML → Cloudflare Workers
本站自己的说明书:它是怎么搭起来的、在新机器上怎么跑、怎么写新文档、怎么部署到 Cloudflare、部署时踩过哪些坑、以及怎样把整套东西交给 AI 阅读。看完这一篇就能独立维护和复现整套系统。
一概览
这是什么
一套静态文档流水线:你写 Markdown,它产出一整个带导航、搜索、代码高亮、深浅色主题的文档站,并自动部署到 Cloudflare 边缘节点。
它解决的三个问题
样式统一
6 份文档共用一套模板。改一次配色或交互,全站生效,不用逐个文件改。
凭证安全
源文件只写占位符,真值单独存放且从不提交。公开产物出现真值时构建直接失败。
AI 可读
产出 llms.txt 与 llms-full.txt,AI 拿一个 URL 就能读完整个知识库。
核心特征
| 特征 | 说明 |
|---|---|
| 零第三方依赖 | 只用 Python 标准库,没有 requirements.txt、没有 node_modules |
| 自包含单文件 | 每个 HTML 内嵌全部 CSS/JS,可离线打开、可单独发给别人 |
| 双轨产出 | 公开版(脱敏)与完整版(含真值)由同一份源生成 |
| 构建即体检 | 每次构建自动扫描凭证泄漏,不通过就中断 |
| 推送即上线 | git push 后约 40 秒自动完成构建与部署 |
未来的你自己。当你换了电脑、隔了半年、或者想加一份新文档时,从这里开始读,不用回忆任何细节。
二架构与工作原理
目录结构
machinaix-docs/ ├── build.py # 生成器:Markdown 渲染 + 模板注入 + 锚点校验 + 泄漏扫描 ├── theme.html # 文档页模板(全部 CSS/JS/组件,6 套品牌配色) ├── theme-index.html # 文档中心首页模板 ├── site.json # 站点配置:文档顺序、卡片分组、扫描白名单、图标 ├── placeholders.json # 占位符标签表(可公开,只有标签没有真值) ├── wrangler.toml # Cloudflare 部署配置(指明 dist/ 为静态资源目录) ├── README.md # 仓库说明(GitHub 上展示) │ ├── src/ # ← 唯一的内容源,日常只改这里 │ ├── machinaix-docs.md # 本文 │ ├── andrewblog.md │ ├── yunchuan.md │ ├── proxy.md │ ├── rustdesk.md │ └── new-api.md │ ├── private/ │ ├── secrets.json # 真实凭证(.gitignore,绝不提交) │ └── secrets.example.json # 模板(会提交,只有占位值) │ ├── dist/ # 公开产物(.gitignore,由 Cloudflare 构建时生成) └── dist-private/ # 完整产物(.gitignore,仅本机)
一次构建发生了什么
- 读取配置
读
site.json(站点级设置)和placeholders.json(占位符标签),若存在private/secrets.json也一并读入。 - 逐份解析 Markdown
每个
src/*.md拆成两部分:顶部---包裹的 JSON front-matter(标题、配色、导航分组、首页卡片信息),以及下方正文。 - 渲染正文
正文按
##切分成<section>,标题里的编号(一、4.1)提取成徽章,{#id}作为稳定锚点。表格、代码块、提示框、卡片、清单等逐一转成组件 HTML。 - 处理占位符
%%KEY%%在公开构建里渲染为脱敏块,在--private构建里替换为真实值。 - 注入模板
把标题、导语、侧栏、正文塞进
theme.html的占位标记,输出自包含单文件。 - 生成首页与 AI 入口
汇总所有 front-matter 里的
card字段生成index.html;同时产出llms.txt(文档清单)和llms-full.txt/all.txt(全文合并,同一份内容两个名字)。 - 校验内部锚点
收集全部页面的
id,检查每一个#锚点和xxx.html#锚点是否真实存在。只报警告不中断构建 —— 死链不影响其他内容,不值得挡住部署。 - 泄漏扫描
扫描
dist/里所有产物,命中真值或可疑模式则以非零码退出,构建失败。
关键设计:模板与内容分离
theme.html 里有一批 {{...}} 标记,build.py 用字符串替换填入内容:
| 标记 | 填入内容 |
|---|---|
{{ACCENT}} | 品牌色名(写进 <html data-accent>,CSS 据此切换整套配色) |
{{SIDEBAR}} | 根据 front-matter 的 nav 生成的分组侧栏 |
{{CONTENT}} | 渲染后的正文(一串 <section>) |
{{META_CHIPS}} | 首屏那排信息胶囊 |
{{BADGE}} | 顶栏右侧的状态徽章 |
| 正文末尾的「相关文档」卡片,由 front-matter 的 related 生成 |
好处是加一份文档不需要碰任何 CSS/JS,改一次样式全站同步。
三在新机器上开始
假设你换了一台电脑,或者半年后回来接手,从零开始的完整步骤。
前置条件
| 需要 | 版本 | 检查命令 |
|---|---|---|
| Python | 3.7 或更高 | python --version |
| Git | 任意 | git --version |
| 浏览器 | 任意现代浏览器 | — |
不需要 Node.js、不需要 pip install、不需要 npm install。整个生成器只用 Python 标准库。
步骤
- 克隆仓库
git clone https://github.com/ANDREW-SVIP/machinaix-docs.git cd machinaix-docs仓库是私有的,克隆时需要
ANDREW-SVIP账号的 Git 凭证。首次在新机器上操作会弹出浏览器登录(Git Credential Manager)或要求输入 Personal Access Token。 - 构建公开版
python build.pymacOS / Linux 上如果
python指向 Python 2,改用python3 build.py。正常输出:
[doc] machinaix-docs 11 sections machinaix-docs 文档站维护手册 [doc] andrewblog 15 sections AndrewBlog 开发者文档 [doc] yunchuan 12 sections 云传 · 开发者文档 [doc] proxy 16 sections 代理服务器部署文档 [doc] rustdesk 14 sections RustDesk 自建服务器 · 运维与诊断文档 [doc] new-api 11 sections New API 运维手册 [gen] index.html / llms.txt / all.txt / llms-full.txt [scan] 内部锚点校验通过 [scan] 泄漏扫描通过(6 份文档) 完成 -> .../machinaix-docs/dist - 本地预览
直接双击
dist/index.html,或者:# Windows start dist\index.html # macOS open dist/index.html产物是自包含单文件,不需要起本地服务器,
file://协议直接就能看。 - 恢复真实凭证(可选,仅本机需要完整版时)
private/secrets.json不在仓库里(这是设计使然)。新机器上有三种拿法:- 从旧机器复制这个文件过来
- 从密码管理器里逐项恢复
- 照着模板重建:
cp private/secrets.example.json private/secrets.json # 然后编辑 secrets.json 填入真实值填好后生成完整版:
python build.py --private产物在
dist-private/,同样是.gitignore的,只在本机存在。 - 只做体检不写文件
python build.py --check只跑解析和泄漏扫描,不产出文件。适合提交前快速验证。
- 新建一份文档
python build.py --new proxy-v2生成带完整 front-matter 骨架的
src/proxy-v2.md,并自动把"proxy-v2"追加进site.json的order。已存在则拒绝覆盖。
缺少 private/secrets.json 时,公开构建完全正常(占位符渲染成脱敏块);只有 --private 会因为拿不到真值而退化成脱敏输出。所以在任何一台机器上 clone 下来就能直接构建和预览。
四写作语法
标准 Markdown 的一个子集,加上几个专用扩展。
Front-matter(JSON)
每份 .md 顶部用 --- 包一段 JSON。这是唯一必须的结构:
{
"title": "文档标题(首屏 H1 与浏览器标签)",
"brand": "顶栏品牌名",
"brandSub": "顶栏副标题",
"accent": "teal",
"eyebrow": "运维文档",
"subtitle": "副标题(等宽字体,可省略)",
"lede": "首屏导语,一两句话说清这份文档是什么",
"badge": { "text": "🔒 脱敏版", "tone": "ok" },
"meta": [["框架", "Flask 3.1"], ["许可证", "MIT"]],
"footer": ["页脚左侧", "页脚右侧"],
"related": ["proxy", "new-api"],
"nav": [
{ "group": "开始", "items": ["s1", "s2"] },
{ "group": "进阶", "items": ["s3"] }
],
"card": {
"group": "基础设施文档",
"initial": "R",
"sub": "首页卡片的等宽副标题",
"desc": "首页卡片正文描述",
"role": "配色表里的定位说明",
"tags": [["🔒 脱敏版", "ok"], ["14 章节", ""]],
"meta": "卡片底部的小字"
}
}| 字段 | 取值 |
|---|---|
accent | blue / indigo / teal / violet / fuchsia / slate |
tone、标签颜色 | ok(绿)/ warn(黄)/ danger(红)/ accent(品牌色)/ 空(灰) |
nav.items | 章节 id 数组;写成 {"id":"s31","label":"3.1 节点","sub":true} 可自定义标签并缩进为子项 |
card.group | 决定首页归到哪一组,组的顺序在 site.json 的 cardGroups 里 |
related | 其他文档的 slug 数组(可省略)。正文末尾自动渲染「相关文档」卡片,标题和描述从对方的 front-matter 取,不用手写。slug 写错会在构建时报 [warn] related 指向不存在的文档 |
章节与锚点
## 一、架构总览 {#s1}
### 4.1 UFW 防火墙##自动切分成<section>,是侧栏导航的单位- 标题开头的编号(
一、1.4.1)会被提取成左侧的方块徽章 {#s1}是稳定锚点,交叉引用写[见第七节](#s7)###进入右侧「本节内容」目录,跟随滚动高亮
没写 {#id} 时,## 用出现顺序编号(s1、s2……),### 用标题内容的哈希(h-a1b2c3d4)。两者都只在你不改动文档结构时才稳定:
不写 {#id} 时 | 什么情况下锚点会变 |
|---|---|
## 二级标题 | 增删任何一个二级标题 —— 后面所有编号整体移位 |
### 三级标题 | 改动这个标题的文字(哪怕只改一个字) |
所以凡是要被交叉引用或对外分享的章节,都写显式 {#id}。构建时会校验所有内部锚点,指向不存在的 id 会在输出里报 [warn] 内部锚点死链。
提示框
> [!WARNING] 可选标题
> 正文,可以有多行、列表、代码块。| 类型 | 呈现 |
|---|---|
NOTE | 品牌色,💡 |
TIP / INFO | 灰底,💡 / ℹ️ |
WARNING / CAUTION | 黄色,⚠️ |
DANGER / IMPORTANT | 红色,⛔ |
SUCCESS | 绿色,✅ |
KEY / LOCK | 灰底,🔑 / 🔒 |
代码块
围栏后面跟语言名,支持这些:
| 语言 | 效果 |
|---|---|
bash powershell json yaml nginx go env python | 语法高亮 + 语言标签 + 一键复制 |
text | 纯文本,无高亮 |
text wrap | 长链接自动换行(连接串这类超长内容用它) |
tree | 目录树:目录名加粗,# 和 ← 后的注释淡化 |
diagram | ASCII 图,支持 {good:文字}、{bad:文字}、{hl:文字} 上色 |
=html | 原样输出 HTML,用于流程图之类模板没覆盖的复杂组件 |
勾选清单
- [x] **① 已完成**
- [ ] **② 待办**
- [!] **③ 阻塞**
- [~] **④ 已并入别处**四种状态分别渲染成绿勾、空框、琥珀感叹号、灰虚线(带删除线效果)。
卡片 / 步骤 / 折叠块
用 ::: 包裹,内部用 --- 分隔条目:
::: cards
#### 卡片标题
*等宽副标题*
卡片正文
---
#### 第二张卡片
正文
:::
::: steps
#### 第一步的标题
正文、代码块、表格都可以放
---
#### [2.5] 自定义编号的步骤
方括号里的内容会替换默认序号
:::
::: expandable 展开完整日志
内容超过 600px 时自动折叠,底部出现展开按钮
:::行内元素
支持 **粗体**、*斜体*、` 代码 、链接`,以及原始 HTML 标签。
常用行内组件:
| 写法 | 效果 |
|---|---|
<span class="pill green">已开启</span> | 绿色胶囊(还有 red / yellow / blue / gray / accent) |
<span class="badge get">GET</span> | 方法徽章(get / post / delete) |
<span class="stars">★★★★☆</span> | 星级 |
<span class="muted">补充说明</span> | 弱化小字 |
<br> | 表格单元格内换行 |
五脱敏与占位符
整套系统里最重要的一条规则:源文件里永远不写真实凭证。
工作方式
在 Markdown 里写占位符:
| 密码 | %%HY2_PASSWORD%% |
| 服务器 | %%PROXY_HOST%% |两种构建产出不同结果:
| 构建 | %%HY2_PASSWORD%% 渲染为 | 产物目录 | 用途 |
|---|---|---|---|
python build.py | 脱敏块「HY2 密码」 | dist/ | 上云、给 AI 读、对外分享 |
python build.py --private | 真实值 | dist-private/ | 本机查阅、实际运维 |
代码块里的占位符会渲染成 <HY2 密码> 这种尖括号形式,保持代码可读。
新增一个占位符
- 在 placeholders.json 加标签
这个文件会提交,所以只放标签,不放真值:
"NEW_SERVER_IP": { "label": "新服务器 IP", "note": "用途备注" } - 在 private/secrets.json 加真值
这个文件不会提交:
"NEW_SERVER_IP": "这里填真实 IP" - 在 Markdown 里引用
服务器地址:%%NEW_SERVER_IP%%
在前面加反斜杠:写 %%KEY%% 会原样输出 %%KEY%% 而不被替换。本文的所有语法示例用的都是这个转义。
先加标签、后加真值、最后引用。如果直接在 md 里写真值再想着"回头替换",很可能忘记,而泄漏扫描只拦公开产物里的真值 —— 一旦真值进了 Git 历史,就得改密码而不只是改文件。
泄漏扫描
每次构建都会扫描 dist/ 里的所有产物:
| 触发条件 | 处理 |
|---|---|
出现 private/secrets.json 里的任何真值(≥6 字符) | 构建失败 |
| 疑似 IPv4 | 构建失败(除非在白名单里) |
疑似 IPv6(5 段以上或含 ::) | 构建失败 |
| UUID 格式 | 构建失败 |
sk- / cfut_ / ghp_ 开头的密钥 | 构建失败 |
确认可以公开的字面量加进 site.json 的 allowlist:
"allowlist": ["127.0.0.1", "0.0.0.0", "66.249.68.32"]127.0.0.1、0.0.0.0(回环与通配,无意义)、两个 Googlebot 的 IP(博客文档里的日志示例,是 Google 的公开地址,不是你的服务器)。加任何新条目前先确认它不是你自己的资产。
六新增与维护文档
加一份新文档
- 建 Markdown 文件
python build.py --new xxx生成
src/xxx.md骨架并自动登记进site.json。也可以手工新建,照抄现有文件的 front-matter 改。 - 登记到 site.json
--new已经自动做完这步。手工新建时,在order数组里加上文件名(不含扩展名),位置决定它在首页和llms.txt里的排序:"order": ["machinaix-docs", "andrewblog", "yunchuan", "xxx"]如果用了新的卡片分组,还要在
cardGroups里加上组名来控制分组顺序。 - 构建验证
python build.py看输出里有没有你的新文档,以及锚点校验和泄漏扫描是否通过。
- 推送上线
git add . git commit -m "docs: 新增 xxx 文档" git pushCloudflare 检测到 push 后自动构建部署,约 40 秒后生效。
首页卡片、侧栏分组、llms.txt、llms-full.txt 全部自动生成,不需要手工维护任何索引。
日常修改流程
# 1. 改内容
# 编辑 src/xxx.md
# 2. 本地看效果
python build.py
start dist\xxx.html
# 3. 满意后推送
git add .
git commit -m "docs: 更新 xxx 的某某章节"
git push
# 4. 等约 40 秒,线上生效验证线上是否已更新
curl -s -A "Mozilla/5.0" https://docs.machinaix.com/llms.txt | head -5或者直接浏览器强制刷新(Ctrl + Shift + R)。
Cloudflare 的机器人防护会拦掉不带 User-Agent 的请求,返回 403。用 curl 测试时记得加 -A。
改样式或交互
改 theme.html(文档页)或 theme-index.html(首页),重新构建即可,全站同步生效。这是模板化最大的收益 —— 不用逐个文件改。
七给 AI 的入口
这是整套系统的核心目的之一:让 AI 用最低成本读懂你的全部技术上下文。
两个入口
| 地址 | 内容 | 什么时候用 |
|---|---|---|
https://docs.machinaix.com/llms.txt | 全部文档的 URL 清单 + 一句话描述 | 让 AI 先看清单,再决定读哪一篇 |
https://docs.machinaix.com/llms-full.txt | 全部文档合并成一份纯文本 | 让 AI 一次性拿到完整上下文 |
https://docs.machinaix.com/all.txt 是全文合并版的别名,内容与 llms-full.txt 逐字节相同。llms-full.txt 是社区正在形成的约定名,AI 更可能主动去猜这个地址;all.txt 是本站原有的名字,保留是为了不让已经发出去的链接失效。
怎么用
对话开始时直接把地址给 AI:
先读 https://docs.machinaix.com/llms.txt,
然后根据我的问题去读对应的文档,再回答。或者需要全局上下文时:
读 https://docs.machinaix.com/llms-full.txt,这是我全部项目和服务器的技术文档。注意事项
AI 读到的服务器 IP、密码、UUID 全是占位符。这是刻意设计 —— 让 AI 理解架构和流程,但拿不到能实际访问你系统的凭证。需要 AI 帮你处理真实值时,用本机 dist-private/ 的内容,不要把线上地址当作真值来源。
Cloudflare 会拦掉不带 User-Agent 的请求。绝大多数 AI 抓取器都会带 UA,所以正常可用。万一某个 AI 读不到,去 Cloudflare 的 安全性 → WAF / 机器人管理 给 docs.machinaix.com 放行。
AI 策略设置
在 Cloudflare 域名设置里配置的三项:
| 项目 | 当前设置 | 含义 |
|---|---|---|
| 搜索 | 阻止 | 搜索引擎爬虫拿不到内容,站点不会出现在搜索结果里 |
| 代理 | 允许 | AI 回答问题时抓取网页走这条,必须开 |
| 训练 | 阻止 | AI 厂商的训练爬虫拿不到内容 |
关掉「代理」等于 AI 读不到你的文档,整套系统的核心价值就没了。
八部署到 Cloudflare Workers
从零复现整套部署的完整步骤。
整体架构
步骤
- 准备 GitHub 私有仓库
在你本机 Git 凭证所属的那个账号下创建仓库,设为 Private,然后:
git remote add origin https://github.com/<账号>/machinaix-docs.git git branch -M main git push -u origin main - 确认仓库里有 wrangler.toml
这是 Workers 静态资源部署的必需文件:
name = "machinaix-docs" compatibility_date = "2026-08-06" [assets] directory = "./dist"[assets]段告诉 wrangler 把dist/作为静态资源上传。没有这个文件,npx wrangler deploy会失败。 - 创建 Cloudflare 应用
Cloudflare Dashboard → Workers 和 Pages → 创建应用程序 → 找到「导入 Git 存储库」或「Pages」标签页。
新版界面会走「创建 Worker」流程,这是正常的 —— Cloudflare 正在用 Workers 静态资源取代 Pages 新项目。
- 授权 GitHub
选择账号 → Only select repositories → 勾选
machinaix-docs→ Install & Authorize。必须勾选具体仓库,否则回到 Cloudflare 后在列表里找不到它(私有仓库尤其)。
选
Only select repositories而不是All repositories,是最小权限原则 —— Cloudflare 只需要这一个仓库。 - 填写构建配置
字段 值 什么时候执行 项目名称 machinaix-docs—— 生产分支 main—— 构建命令 python build.py每次构建 部署命令 npx wrangler deploy只在生产分支 版本命令 npx wrangler versions upload只在非生产分支 根目录 留空 / /—— ⛔构建命令最容易漏这一栏标着「可选」,很容易跳过。但漏了它整个部署就是错的 ——
dist/不会生成,wrangler deploy找不到资源目录直接失败。详见踩坑第 2 条。⚠️版本命令别填成 deploy「版本命令」管的是非生产分支。填成
npx wrangler deploy的话,推任何一个分支都会直接覆盖生产版本,而不是生成预览。必须是npx wrangler versions upload。详见踩坑第 9 条。 - 部署并验证
点「部署」,等约 40 秒。成功后会得到一个
<项目名>.<你的子域>.workers.dev地址,打开能看到文档中心首页就说明通了。 - 绑定自定义域
Worker 页面 → 域 标签 → 添加域名,填
docs.machinaix.com。如果报「已有外部管理的 DNS 记录」,说明 DNS 里有一条同名的 A/CNAME 记录挡着,删掉它再加。如果提示「没有区域匹配」,不要点「查找相似项」(那是卖域名的页面),可以先用「添加路由」顶着。完整过程见踩坑第 4、5 条。
加完回列表核对名称 —— 输入框可能自动补后缀,拼出
docs.machinaix.com.machinaix.com这种东西。 - 回填 baseUrl
域名生效后,把
site.json的baseUrl填成正式地址:"baseUrl": "https://docs.machinaix.com"推送后
llms.txt里的链接会从相对路径变成完整 URL,AI 抓取时才能正确跳转。
构建环境说明
| 项 | 情况 |
|---|---|
| Python | 构建镜像自带,直接 python build.py 即可 |
| 依赖安装 | 无 —— 没有 requirements.txt,日志里会显示 No dependencies detected to cache |
| wrangler | npx 首次执行时自动下载(日志里会看到 will be installed: wrangler@4.x) |
| 构建耗时 | 约 40 秒,其中大半是初始化环境和下载 wrangler |
如果遇到 Python 版本问题,在 设置 → 变量和机密 加一条 PYTHON_VERSION = 3.11。
自定义域 vs 路由
两种把域名接到 Worker 的方式,理解差异能少走弯路:
| 自定义域(Custom Domain) | 路由(Route) | |
|---|---|---|
| DNS 记录 | Cloudflare 自动创建一条 Worker 类型记录并管理 | 需要该主机名已能通过 Cloudflare 解析(泛解析也算) |
| 配置形式 | 填主机名 docs.machinaix.com | 填 URL 模式 docs.machinaix.com/* |
| 前置条件 | 该主机名不能已有你自己建的 A/CNAME 记录 | 无 |
| 适用场景 | 该主机名专属于这个 Worker | 想按路径分流,或不方便动 DNS 记录 |
| 本站采用 | ✅ 现用方案(2026-08-07 起) | 建站初期用它顶过一段时间 |
/* 不能漏写成 docs.machinaix.com 只匹配根路径,/llms.txt、/proxy.html 这些子路径全会 404。必须写 docs.machinaix.com/*。
建站时自定义域加不上(见踩坑第 4 条),先用路由顶着。后来查明真正的拦路石是一条手工建的 docs A 记录,删掉它之后自定义域正常添加,路由随即删除。
迁移顺序是关键:先加自定义域、确认站点正常,再删路由。反过来的话,中间那段时间 docs.machinaix.com 会落回泛解析,显示成博客首页。
九部署踩坑实录
真实踩过的坑,按遇到顺序排列。每条都记了现象、原因、诊断方法和解决办法。最后一条是事前发现、还没踩上的,一并记在这里。
坑 1:git push 报 "Repository not found"
现象
remote: Repository not found.
fatal: repository 'https://github.com/xxx/machinaix-docs.git/' not found仓库明明在网页上看得见,push 却说找不到。
原因
仓库建在了 A 账号下,而本机 Git 凭证是 B 账号的。GitHub 对无权访问的私有仓库统一返回 404 而不是 403 —— 这是防止通过错误码探测私有仓库是否存在的安全设计,但也让报错具有误导性。
诊断
# 看本机存的是哪个 GitHub 账号
cmdkey /list | Select-String "github" -Context 0,2
# 确认目标账号类型(User 还是 Organization)
Invoke-RestMethod "https://api.github.com/users/<账号名>"解决
三选一:把仓库转移到凭证所属账号 / 把凭证账号加为协作者 / 用带用户名的 remote URL 让 Git 凭证管理器单独登录。本站采用的是第一种(在正确账号下重建)。
GitHub 的授权页面可能显示显示名(Name 字段)而不是登录名(login)。看到一个陌生名字先别慌,用 https://api.github.com/users/<登录名> 查一下 name 字段就能确认是不是同一个账号。
坑 2:首次构建失败,日志里没有构建步骤
现象
构建日志:
Cloning repository...
No build output detected to cache. Skipping.
Executing user deploy command: npx wrangler deploy克隆完直接跳到部署,中间没有任何构建动作,然后部署失败。
原因
创建项目时「构建命令」那一栏是空的(它标着「可选」)。于是 dist/ 从未生成,wrangler deploy 按 wrangler.toml 去找 ./dist 却找不到。
诊断
在构建详情页展开「构建设置」,看 构建命令 是不是显示「无」。
解决
项目 → 设置 → 构建 → 构建配置 → 填 python build.py → 保存 → 回到「部署」标签点「重试构建」。
坑 3:wrangler deploy 需要配置文件
现象
npx wrangler deploy 报错找不到入口或资源目录。
原因
Workers 部署需要 wrangler.toml(或 wrangler.json)声明要部署什么。纯静态站点属于「仅静态资源 Worker」,没有 main 入口脚本,必须用 [assets] 段指明目录。
解决
仓库根目录放 wrangler.toml:
name = "machinaix-docs"
compatibility_date = "2026-08-06"
[assets]
directory = "./dist"坑 4:添加自定义域报「没有区域匹配」
现象
Worker → 域 → 添加域名 → 填 docs.machinaix.com,弹窗提示:
没有区域匹配 docs.machinaix.com。
如果您拥有此域名并希望将其连接到您的 Worker,请先将该域名添加到 Cloudflare。但域名明明就在 Cloudflare 上。
排查过程
- 确认域名确实托管在 Cloudflare
# 查 NS 记录 dig NS machinaix.com +short返回
xxx.ns.cloudflare.com就说明在 Cloudflare 上。 - 确认域名和 Worker 在同一个账号
Cloudflare 账号主页会同时列出 Domains 和 Workers 两栏。如果域名和 Worker 都在里面,就排除了账号不一致的可能。
💡Worker 只能绑定同账号下的域名,这是最常见的原因,但本例不是。
- 判定为界面异常
域名在、账号对,弹窗却说没有区域匹配 —— 是这个弹窗自身的问题。
解决
改用 添加路由。路由走的是另一套接口,在它的域名选择列表里 machinaix.com 正常出现:
- Worker → 域 → + 添加路由
- 选择区域
machinaix.com - 路由模式填
docs.machinaix.com/*
「查找相似项」会跳到 Cloudflare 域名销售页,向你推销 docsmachinaix.com 这类新域名 —— 你根本不需要买。 「接入域名」是把一个新域名迁入 Cloudflare 的流程(要改 NS),你的域名早就在 Cloudflare 里了,点了只会把事情搞乱。
后续:2026-08-07 查明真正原因
再试一次时,弹窗不再说「没有区域匹配」,而是给出了真正有用的报错:
Hostname 'docs.machinaix.com' already has externally managed
DNS records (A, CNAME, etc). Delete them first or try a different hostname.拦路石是一条手工建的 docs A 记录(建站早期留下的)。自定义域要求由 Cloudflare 自己创建并管理那条 Worker 类型记录,所以不接受同名的既有记录。
删掉那条 A 记录之后,自定义域一次就加上了,路由随即删除。建站时那个「没有区域匹配」的提示是误导 —— 它把「主机名已被占用」说成了「找不到区域」,白白多花了一小时。
这次还顺手踩了个小坑:在输入框里填完整的 docs.machinaix.com,实际建出来的是 docs.machinaix.com.machinaix.com。加完一定回列表看一眼名称对不对,建错了就删掉重来。
坑 5:域名绑好了,打开却是另一个站点
现象
docs.machinaix.com 能打开、HTTPS 正常,但显示的是博客首页而不是文档站;/llms.txt 返回 404。
原因
主域名有一条 *.machinaix.com 泛解析记录指向博客服务器。在没有为 docs 建专属记录、也没有路由拦截时,docs.machinaix.com 命中泛解析 → 打到博客服务器的 Nginx → 返回默认站点。
诊断技巧
拿一个根本不存在的子域去解析:
dig zzz-does-not-exist-9421.machinaix.com +short如果它也能解析出 IP,就证明存在泛解析记录。这个方法很好用,能立刻定位这类"看起来像成功但内容是错的"问题。
解决
加上 Worker 路由 docs.machinaix.com/* 之后自动解决 —— 路由在 Cloudflare 边缘节点就拦截请求,根本不会回源到博客服务器,优先级天然高于泛解析。
现在改用自定义域后同样安全:Cloudflare 为 docs 建了一条专属的 Worker 类型记录,专属记录的优先级高于泛解析。
*.machinaix.com 是博客其他子域在用的,删掉会连带搞垮它们。它跟文档站现在已经没有关系 —— docs 有了自己的专属记录,不再走泛解析。
坑 6:.txt 文件中文可能乱码
现象
llms.txt / all.txt 的响应头是 Content-Type: text/plain,不带 charset。浏览器可能按本地编码解析,中文变乱码。
HTML 不受影响,因为文件头部有 <meta charset="UTF-8">。
解决
build.py 构建时生成 _headers 文件(Cloudflare 静态资源支持这个约定):
/*
Cache-Control: public, max-age=300, must-revalidate
/*.txt
Content-Type: text/plain; charset=utf-8修复后响应头变成 text/plain; charset=utf-8。
顺带声明了 5 分钟的浏览器缓存:改完推上去很快能看到新内容,同时挡住反复刷新时的回源。想让改动立刻可见就强制刷新(Ctrl + Shift + R)。
坑 7:不带 User-Agent 的请求被 403
现象
用脚本测试站点时返回 HTTP Error 403: Forbidden,浏览器里却完全正常。
原因
Cloudflare 默认的机器人防护会拦掉没有 User-Agent 的请求。
解决
测试时带上 UA:
curl -s -A "Mozilla/5.0" https://docs.machinaix.com/llms.txt绝大多数 AI 抓取器都会带 UA,正常使用不受影响。万一某个 AI 读不到,去 Cloudflare 安全性 → WAF 给这个主机名加放行规则。
坑 8:PowerShell 取网页内容中文乱码(误报)
现象
用 Invoke-WebRequest 检查线上内容,中文关键词全都"找不到",ASCII 关键词却能找到。
原因
响应头没有 charset 时,PowerShell 的 .Content 会按 ISO-8859-1 解码,中文全乱。这是检查工具的问题,不是站点的问题。
解决
验证脚本改用 Python 显式按 UTF-8 解码:
import urllib.request as u
req = u.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
text = u.urlopen(req, timeout=25).read().decode('utf-8')排查问题时先怀疑自己的检查工具。当"部分检查失败、部分成功"且失败的都是非 ASCII 内容时,几乎一定是编码问题而不是业务问题。
坑 9:非生产分支会直接覆盖生产
现象
这条是事前发现的,还没踩上:建项目时「版本命令」被填成了 npx wrangler deploy,而「非生产分支构建」是开启的。
原因
Cloudflare Workers Builds 有两个部署命令:
| 字段 | 触发分支 | 应该是 |
|---|---|---|
| 部署命令 | 生产分支(main) | npx wrangler deploy |
| 版本命令 | 其他所有分支 | npx wrangler versions upload |
两个都填 deploy 的话,随便开个分支改点东西一推,线上立刻被这个半成品分支覆盖 —— 而你以为自己只是在做预览。
解决
项目 → 设置 → 构建 → 构建配置 → 版本命令 改成 npx wrangler versions upload。改完非生产分支只生成带独立预览地址的版本,生产纹丝不动。
不需要分支预览的话,也可以直接在 分支控制 里关掉「非生产分支构建」。
建个临时分支推一次,看构建日志里执行的是 versions upload 还是 deploy,同时确认 docs.machinaix.com 的内容没变。验完删分支。
十故障排查
| 现象 | 可能原因 | 处理 |
|---|---|---|
构建失败:front-matter JSON 解析失败 | md 顶部的 JSON 语法错(多逗号、缺引号) | 按报错里的位置改;JSON 不支持注释和尾逗号 |
| 构建失败:泄漏扫描不通过 | 源文件里写了真值,或出现新的 IP/UUID | 改成 %%占位符%%,或把确认安全的值加进 site.json 的 allowlist |
构建警告:placeholders.json 中缺少标签 | md 里用了未登记的占位符 | 在 placeholders.json 补一条标签 |
| 侧栏少了某个章节 | front-matter 的 nav.items 里没写这个 id | 补上 id;未列出的章节不会出现在侧栏 |
| 章节锚点失效 | 没写 {#id}:## 的 s1/s2 会随增删移位,### 的 h-xxxxxxxx 会随标题改字而变 | 给需要交叉引用的章节写显式 {#id} |
构建警告:内部锚点死链 | 文档里的 [见第七节](#s7) 指向了不存在的 id | 按提示改链接或补 {#id}。只是警告,不会挡住构建 |
| 首页没有某份文档的卡片 | front-matter 缺 card 字段,或 site.json 的 order 里没登记 | 补 card 并加进 order |
| 线上没更新 | 构建失败,或浏览器缓存 | 看 Cloudflare 构建日志;Ctrl+Shift+R 强刷 |
| 线上 404 | 路由模式漏了 /* | 改成 docs.machinaix.com/* |
| 线上显示别的站点 | 泛解析截胡 | 见踩坑第 5 条 |
| curl 返回 403 | 没带 User-Agent | 加 -A "Mozilla/5.0" |
本地快速自检
# 只跑解析和泄漏扫描,不写文件
python build.py --check
# 完整构建后核对产物
python build.py
ls dist线上快速自检
curl -s -o /dev/null -w "%{http_code}\n" -A "Mozilla/5.0" https://docs.machinaix.com/
curl -s -A "Mozilla/5.0" https://docs.machinaix.com/llms.txt | head -8十一设计决策
记录为什么这样设计,避免以后"优化"掉关键约束。
为什么产物是自包含单文件
每个 HTML 内嵌全部 CSS 和 JS,不引用任何外部资源。
代价:每份文档重复约 20 KB 的模板代码。 收益:可以离线打开、可以单独发给别人、可以塞进邮件附件、十年后没有 CDN 失效问题。脱敏版文档本来就是"要能单独发出去"的东西,这条约束不能破。
抽出 docs.css 看起来更"专业",但会让单个 HTML 失去独立性。真正的去重方案是模板化(已经做了),而不是拆分产物。
为什么源是 Markdown 而不是 HTML
最初的 6 份文档是手写 HTML,每份都内嵌一整套 CSS/JS。改一次配色要改 6 个文件,到 10 份就彻底失控。
改成 Markdown + 模板后:写作成本降低、AI 改 md 的可靠性远高于改上千行 HTML、样式改一次全站生效。
为什么零第三方依赖
build.py 只用 Python 标准库,手写了一个 Markdown 子集渲染器。
代价:不支持完整 Markdown 语法(够用即可)。 收益:Cloudflare 构建环境不用装任何东西、构建快、没有供应链风险、几年后依然能跑。对一个文档站来说,这个权衡明显划算。
为什么用占位符而不是"两份文件"
最早的做法是维护「完整版」和「脱敏版」两份独立文档。问题是改一次密码要改两处,迟早不同步。
占位符方案让真值只存在于 private/secrets.json 一处,两个版本由同一份源生成,结构永远一致。
为什么泄漏扫描要让构建失败而不是只警告
警告会被忽略,尤其在 CI 日志里。让构建硬失败意味着含真值的产物永远不可能上线 —— 这条防线的价值就在于它不给人"下次再说"的机会。
为什么线上只放脱敏版
因为目标是让 AI 能读,而 AI 抓取需要公开无鉴权的 URL。公开 + 含真值 = 泄密,两者不可兼得。
所以选择:线上公开脱敏版供 AI 阅读,真值只在本机 dist-private/。这也是为什么「代理」这项 AI 策略必须保持允许 —— 它是整个设计的目的所在。