machinaix-docs 文档站维护手册
Static Docs Pipeline · Markdown → HTML → Cloudflare Workers
本站自己的说明书:它是怎么搭起来的、在新机器上怎么跑、怎么写新文档、怎么部署到 Cloudflare、部署时踩过哪些坑、以及怎样把整套东西交给 AI 阅读。看完这一篇就能独立维护和复现整套系统。
一概览
这是什么
一套静态文档流水线:你写 Markdown,它产出一整个带导航、搜索、代码高亮、深浅色主题的文档站,并自动部署到 Cloudflare 边缘节点。
它解决的四个问题
样式统一
全部文档共用一套模板。改一次配色或交互,全站生效,不用逐个文件改。
凭证安全
源文件只写占位符,真值单独存放且从不提交。公开产物出现真值时构建直接失败。
AI 可读
公开文档 AI 拿一个 URL 读完;受保护文档凭密钥 URL 单篇喂,同样零摩擦。
访问控制
front-matter 标 protected 的文档由边缘密码门把守:人输密码,AI 用密钥 URL。
核心特征
| 特征 | 说明 |
|---|---|
| 零第三方依赖 | 只用 Python 标准库,没有 requirements.txt、没有 node_modules |
| 自包含单文件 | 每个 HTML 内嵌全部 CSS/JS,可离线打开、可单独发给别人 |
| 双轨产出 | 公开版(脱敏)与完整版(含真值)由同一份源生成 |
| 构建即体检 | 每次构建自动扫描凭证泄漏,不通过就中断 |
| 推送即上线 | git push 后约 40 秒自动完成构建与部署 |
| 按文档访问控制 | front-matter 一行 "protected": true:人走密码、AI 走密钥 URL,公开文档零影响 |
| 更新时间自动记 | front-matter 的 updated 由提交钩子自动盖章,页头 / 首页卡片 / AI 入口自动展示 |
未来的你自己。当你换了电脑、隔了半年、或者想加一份新文档时,从这里开始读,不用回忆任何细节。
二架构与工作原理
目录结构
machinaix-docs/ ├── build.py # 生成器:Markdown 渲染 + 模板注入 + 锚点校验 + 泄漏扫描 ├── theme.html # 文档页模板(全部 CSS/JS/组件,8 套品牌配色) ├── theme-index.html # 文档中心首页模板 ├── site.json # 站点配置:文档顺序、卡片分组、扫描白名单、图标 ├── placeholders.json # 占位符标签表(可公开,只有标签没有真值) ├── worker.js # 边缘鉴权:受保护文档的密码门(2026-08-16 起) ├── wrangler.toml # Cloudflare 部署配置(worker.js + 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(全文合并,同一份内容两个名字)。AI 入口只收公开文档,受保护文档连正文带清单条目都不进去;另产出_auth.json(受保护 slug 清单,worker.js的鉴权依据,对外访问返回 404)。 - 校验内部锚点
收集全部页面的
id,检查每一个#锚点和xxx.html#锚点是否真实存在。只报警告不中断构建 —— 死链不影响其他内容,不值得挡住部署。 - 泄漏扫描
扫描
dist/里所有产物,命中真值或可疑模式则以非零码退出,构建失败。
关键设计:模板与内容分离
theme.html 里有一批 {{...}} 标记,build.py 填入内容:
| 标记 | 填入内容 |
|---|---|
{{ACCENT}} | 品牌色名(写进 <html data-accent>,CSS 据此切换整套配色) |
{{SIDEBAR}} | 根据 front-matter 的 nav 生成的分组侧栏 |
{{CONTENT}} | 渲染后的正文(一串 <section>) |
{{META_CHIPS}} | 首屏那排信息胶囊 |
{{BADGE}} | 顶栏右侧的状态徽章 |
{{RELATED}} | 正文末尾的「相关文档」卡片,由 front-matter 的 related 生成 |
{{DOC_INDEX}} | 全站「文档 + 章节标题」索引,供跨文档搜索用(见设计决策) |
好处是加一份文档不需要碰任何 CSS/JS,改一次样式全站同步。
str.replace这一篇文档的正文里就写着 {{CONTENT}}、{{RELATED}} 这些字面量(就是上面这张表)。 如果逐个替换,排在 {{CONTENT}} 后面的标记会把已经注入的正文再扫一遍, 把表格里那些字面量也当成待填标记换掉 —— 讲模板的段落被自己的模板结果覆盖。
实际发生过:{{RELATED}} 那一行的单元格在线上长期是空的,因为本文没写 related, 空字符串把它吃了。现在改成 fill() 用正则单次扫描,扫过不回头,天然免疫。
str.replace 没有的失败模式正则扫不到的标记会静默留在产物里。str.replace 不看字符集,所以从不漏; 正则看。第一版 fill() 的字符集写成 [A-Z_]+,漏了数字,于是 {{H1}} 原样出现在每一页的大标题上 —— 构建全绿、扫描全过、没有任何警告。
所以 fill() 现在两头都校验:填得进去的键必须是扫得到的键(repl 里的键 都要能被 RE_MARKER 完整匹配),模板里出现的标记必须有人填。任一条不满足直接 构建失败。校验只看模板不看产物 —— 产物里的同名字面量是这篇文档在讲解模板,那是内容。
三在新机器上开始
假设你换了一台电脑,或者半年后回来接手,从零开始的完整步骤。
前置条件
| 需要 | 版本 | 检查命令 |
|---|---|---|
| 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": "副标题(等宽字体,可省略)",
"updated": "2026-08-16",
"protected": false,
"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 / cyan / emerald。新增一套配色要同步四个文件:theme.html(--c-* 明暗两行 + data-accent 明暗两条)、theme-index.html(--c-* 明暗两行)、site.json 的 icons、build.py 的 palette/names_cn(漏了首页品牌色表会回退成灰色块) |
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 指向不存在的文档 |
updated | 最后更新日期。提交钩子自动盖章(build.py --stamp),别手工维护、也别在 meta chips 里再写一份日期。页头「最后更新」徽章、首页卡片、AI 入口都从它取值 |
protected | true 时该文档上线后需密码访问,并自动排除出 AI 入口与公开搜索索引,卡片加「🔒 需密码」。机制见第六章,AI 喂法见第七章 |
章节与锚点
## 一、架构总览 {#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> | 表格单元格内换行 |
两条容易踩的转义规则
表格里的竖线要写成 \|。 表格按竖线切单元格,内容里出现裸竖线会被当成分隔符 —— 一行两列被切成五列,跨在中间的反引号还会因此断开、渲染不成代码。
| 服务管理 | `systemctl [start\|stop\|restart\|status] xray` |忘了转义不会静默出错:构建会报 [warn] 表格列数与表头不符,并打出是哪一行。这类错误在产物里长得像正常表格,肉眼几乎发现不了,所以必须靠构建时点名。
宽表自动处理,不用手写标记。 表格里的行内代码只在空格处换行、不会把 /auth/oauth/token 这种 token 切成碎片;列数 ≥ 6 的表格自动加 wide 类,允许拉到容器 2.6 倍宽再横向滚动,长文字列才有地方铺开(不放宽时每列只剩「最长 token」那么窄,一行高出半屏)。三四列的普通表格不受影响。
段落内换行 = 行尾两个空格。 和标准 Markdown 一致,渲染成 <br>。段落末尾的两个空格会被忽略(那个换行没有意义)。
管理员密码默认 `admin123`,通过 `ADMIN_PASSWORD` 修改。␣␣
启动日志只打印后台 URL,**不会**打印密码。五脱敏与占位符
整套系统里最重要的一条规则:源文件里永远不写真实凭证。
工作方式
在 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 里写真值再想着"回头替换",很可能忘记 —— 而扫描只认已登记进 secrets.json 的真值和少数固定形状(IP/UUID/密钥前缀),还没登记的真值它不认识。一旦真值进了 Git 历史,就得改密码而不只是改文件。
要查真值的时候怎么做
有两种查法,用错会很别扭:
① 已经知道要哪个键
直接打开 private/secrets.json。
键名自解释(HY2_PASSWORD、PROXY_HOST、CF_API_TOKEN…),每个键在 placeholders.json 里都有中文标签。最快的一条路。
② 需要「用在哪、和什么配套」
生成完整版文档看上下文:
python build.py --private
start dist-private\proxy.html排版、导航、搜索和线上完全一样,只是占位符换成了真值。查运维步骤时用这个,别对着一堆裸 key 拼。
dist-private/ 是用完即弃的临时产物,不要让它常驻它是不受控的未脱敏副本。虽然 .gitignore 挡着进不了 Git,但留在磁盘上有两个实际问题:
- 它会悄悄过期。 2026-08-07 清理时发现盘上那份是几天前生成的,11 个文件里 9 个内容已经和当前源不一致 —— 真在事故里对着它操作,读到的是旧步骤。
.gitignore挡的是路径不是内容。目录改名、文件挪位置、或被网盘同步,保护立刻失效。
而重建的代价几乎为零:一条命令、不到一秒、两次构建字节一致(已验证)。所以正确用法是查完就删:
rmdir /s /q dist-private # PowerShell: Remove-Item -Recurse -Force dist-private泄漏扫描
每次构建都会扫描即将产出的内容:
| 触发条件 | 处理 |
|---|---|
出现 private/secrets.json 里的任何真值(≥4 字符;4–5 字符按词边界匹配防误报) | 构建失败 |
| 疑似 IPv4 | 构建失败(除非在白名单里) |
疑似 IPv6(5 段以上或含 ::) | 构建失败 |
| UUID 格式 | 构建失败 |
sk- / cfut_ / ghp_ 开头的密钥 | 构建失败 |
短于 4 字符的真值确实没法扫(纯数字短串会满屏误报),构建时会点名列出哪些键不在覆盖内 —— 排除必须看得见,不能静默发生。这条规矩来自两次同类教训:历史扫描用 len>=8、构建扫描用 len>=6,都恰好把 5 字符的 SSH 端口漏在过滤条件外。
产物之外,构建还会对全部 git 受控文件做一遍真值直接匹配。渲染产物只覆盖 src/*.md,而 NEXT.md、README.md、site.json、tools/ 这些不经过渲染的文件同样会推上 GitHub ——「顺手把真实命令贴进待办清单」恰恰是最容易发生的泄漏路径(AndrewBlog 那次历史泄漏走的就是文档以外的文件)。这一段只在本机有 secrets.json 时生效;Cloudflare 构建环境没有真值可比对,会安静跳过 —— 所以对密码这类无固定形状的值,本机钩子是唯一防线,不是「提前的一道」。
确认可以公开的字面量加进 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 的公开地址,不是你的服务器)。加任何新条目前先确认它不是你自己的资产。
把这道防线提前到「提交时」
构建时才拦有个致命问题:那时真值已经在 git 历史里了。进了历史就得改密码,不是改文件能了事的。
所以仓库里带了一个提交钩子,clone 后跑一次即可(每台机器一次,钩子不随仓库自动生效):
git config core.hooksPath tools/git-hooks之后每次 git commit 都会先跑 build.py --check,不通过就中止提交。钩子本身零依赖,只调用已有的 build.py。
--check 曾经是个假的绿灯它原本扫的是磁盘上的 dist/ 目录。而 --check 的定义就是不写文件 —— 于是它扫的永远是上一次构建留下的旧产物,源码里刚加进去的真值它根本看不见; dist/ 不存在时更是整个跳过扫描、退出码 0,报了成功却什么都没查。
现在改成扫「即将产出的内容」而不是磁盘目录,--check 才真正名副其实。 这个 bug 的危险之处在于:它偏偏出现在最该被信任的那条命令上。
报告里对已知真值只打键名不打值。原因是这段代码同样跑在 Cloudflare 的 构建日志里 —— 扫描器把真值抄进日志,等于换个地方再泄一次。 未登记的命中仍然打全值,因为你得看到它才能判断要不要加进 allowlist。
六新增与维护文档
加一份新文档
- 建 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 全部自动生成,不需要手工维护任何索引;受保护文档从 AI 入口的排除也是自动的。
保护一篇文档(密码门)
front-matter 加一行,推送后约 40 秒生效:
"protected": true生效后各方看到的行为:
| 访问方 | 行为 |
|---|---|
| 浏览器(无凭证) | 密码页;输对访问密码发 30 天签名 Cookie,同一浏览器免重输,/_logout 退出 |
| AI / 脚本 | URL 带 ?k=<访问密钥> 直接放行,喂法见第七章 |
llms.txt / llms-full.txt / 公开页搜索 | 自动排除这篇(搜索只留标题),首页卡片自动加「🔒 需密码」 |
鉴权在 Cloudflare 边缘的 worker.js 里做,受保护清单来自构建产物 _auth.json。密码与密钥是 Worker Secret(wrangler secret put DOCS_PASSWORD / SESSION_SECRET / AI_TOKEN),不在仓库也不在产物里;没配齐时受保护文档一律 503 —— 宁可关死,不会因缺配置而敞开。
正文仍明文存在于 git 历史与构建产物里,密钥 URL 给了谁谁就能读。受保护文档里依旧只写占位符、依旧不写自己未修复的弱点 —— 这道门挡的是路人和爬虫,不是脱敏的替代品。
日常修改流程
# 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 用最低成本读懂你的技术上下文。公开文档走下面两个入口;受保护文档单篇用密钥 URL 喂(见本章后文),两个入口里连它们的标题都不出现。
两个入口
| 地址 | 内容 | 什么时候用 |
|---|---|---|
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 改文档(不只是读)
上面讲的是「让 AI 读」。更常用的其实是「让 AI 改」,两者要用的工具完全不同:
| 让 AI 读:回答问题、出方案 | 让 AI 改:动文件 | |
|---|---|---|
| 用谁 | 能上网的 AI(ChatGPT / Claude 网页 / Gemini) | 仓库里的 Claude Code |
| 怎么喂 | 给它上面两个 URL | 什么都不用喂 |
| 产出 | 回答和片段,你自己贴回去 | 直接改 src/*.md、跑 build.py 验证、提交 |
CLAUDE.md 会自动加载 —— 铁律、写作语法、本机 Python 路径、验证命令全在里面。 AndrewBlog 和 apk-dist 也各有一份,会在那边开工时提醒你回来同步文档。
在仓库里干活的 AI 不需要提示词模板
克隆下来、在项目目录里打开 AI,直接说需求就行 —— 不用背模板,也不用交代规矩:
| 文件 | 谁自动读 |
|---|---|
AGENTS.md | 跨工具标准:Cursor、Codex、Gemini CLI、Copilot、aider、Windsurf、Zed、Devin… |
CLAUDE.md | Claude Code。它不读 AGENTS.md,所以这个文件只有一行 @AGENTS.md 把前者导入 |
铁律、写作语法、构建命令、本机 Python 路径、改动时的默认动作,全在 AGENTS.md 里,工具启动时自动进上下文。
AGENTS.md 一处CLAUDE.md 只做导入。两份各写一套的结果是规则打架,而工具遇到矛盾指令会随便挑一条执行。 同理,AndrewBlog 和 apk-dist 也各有一份 AGENTS.md,会在那边开工时提醒回来同步文档。
所以实际操作就是一句话:
proxy.md 里端口跳跃范围改成 40000-50000它会自己全文找、自己跑 build.py、自己报改了哪几处。
真正需要写下来的是下面两种情况 —— 那里没有 AGENTS.md。
让站外的 AI 读
不在仓库里时(手机上、别人的电脑上、或者用 ChatGPT),把地址给它:
先读 https://docs.machinaix.com/llms.txt 看有哪些文档,
再根据我的问题去读对应那一篇,然后回答。
问题:RustDesk 的中继端口是哪几个?之前卡顿的根因是什么?需要公开部分的全局上下文时直接给全文合并版(体量随公开文档增减变化,现代模型一次装得下):
读 https://docs.machinaix.com/llms-full.txt,
这是我服务器和项目的公开技术文档。省 token 的做法是先给 llms.txt(几百 token)让它自己挑,只在真的需要全局视角时才上 llms-full.txt。
受保护的文档怎么喂
受保护文档不进上面两个入口,喂法是单篇给带密钥的 URL:
读 https://docs.machinaix.com/<slug>.html?k=<访问密钥>,
这是某某项目的开发者文档(脱敏版),读完回答:……- 密钥就是 Worker Secret 里的
AI_TOKEN,本机备份在private/secrets.json的DOCS_AI_TOKEN - 一次喂一篇:页内链接不带密钥,AI 顺着「相关文档」点不开别的受保护篇目,要几篇就贴几条密钥 URL
- 脚本调用也可以走请求头:
Authorization: Bearer <访问密钥> - 这条 URL 是持有即可读的凭证 —— 给过哪家 AI,那家的日志里就有。怀疑外泄就轮换
AI_TOKEN(wrangler secret put AI_TOKEN,改完推送一次),所有旧链接立刻作废,代码零改动
让站外的 AI 写,你再贴回仓库
网页版 AI 没有 CLAUDE.md,得把约束交代清楚。最省事的办法是让它先读本文的写作语法那一章:
按这个文档站的写作语法输出 Markdown。
语法规范见 https://docs.machinaix.com/machinaix-docs.html#syntax
先读它,再动笔。
硬约束:
- 真实 IP / 密码 / Token 一律写成 %%占位符%%,绝不写真值
- 表格单元格里的竖线要转义成 \|
- 尖括号占位(如 <你的IP>)必须包在反引号里,否则会被转义并触发构建警告
- front-matter 是 JSON 不是 YAML
内容:……不用逐字审 AI 的输出 —— python build.py 的几道守卫就是给它兜底的:
| AI 容易犯的错 | 谁拦住它 |
|---|---|
| 写了真实 IP / 密码 | 泄漏扫描 → 构建失败 |
| 表格里竖线没转义 | 列数校验 → 报警并打出是哪一行 |
| 尖括号内容忘了包反引号 | 标签白名单 → 转义成可见文本并报警 |
锚点或 related 写错 | 死链校验 → 报警 |
| front-matter JSON 写坏 | 解析失败 → 构建失败 |
所以流程是:贴进 src/xxx.md → 跑 build.py → 有报警就把报警原文丢回给 AI 让它修。
注意事项
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会失败。上面是首次部署时的最小配置;2026-08-16 起本文件还多了main = "worker.js"(受保护文档的密码门)与run_worker_first = true,以仓库内wrangler.toml的注释为准。 - 创建 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找不到资源目录直接失败。详见踩坑第 1 条。⚠️版本命令别填成 deploy「版本命令」管的是非生产分支。填成
npx wrangler deploy的话,推任何一个分支都会直接覆盖生产版本,而不是生成预览。必须是npx wrangler versions upload。详见踩坑第 8 条。 - 部署并验证
点「部署」,等约 40 秒。成功后会得到一个
<项目名>.<你的子域>.workers.dev地址,打开能看到文档中心首页就说明通了。 - 绑定自定义域
Worker 页面 → 域 标签 → 添加域名,填
docs.machinaix.com。如果报「已有外部管理的 DNS 记录」,说明 DNS 里有一条同名的 A/CNAME 记录挡着,删掉它再加。如果提示「没有区域匹配」,不要点「查找相似项」(那是卖域名的页面),可以先用「添加路由」顶着。完整过程见踩坑第 3、4 条。
加完回列表核对名称 —— 输入框可能自动补后缀,拼出
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/*。
建站时自定义域加不上(见踩坑第 3 条),先用路由顶着。后来查明真正的拦路石是一条手工建的 docs A 记录,删掉它之后自定义域正常添加,路由随即删除。
迁移顺序是关键:先加自定义域、确认站点正常,再删路由。反过来的话,中间那段时间 docs.machinaix.com 会落回泛解析,显示成博客首页。
九部署踩坑实录
真实踩过的坑,按遇到顺序排列。每条都记了现象、原因、诊断方法和解决办法。最后一条是事前发现、还没踩上的,一并记在这里。
坑 1:首次构建失败,日志里没有构建步骤
现象
构建日志:
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 → 保存 → 回到「部署」标签点「重试构建」。
坑 2: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"(这是当时的最小配置;现今的 wrangler.toml 另有 main = "worker.js" 与 run_worker_first = true —— 受保护文档的密码门,以仓库内文件注释为准。)
坑 3:添加自定义域报「没有区域匹配」
现象
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。加完一定回列表看一眼名称对不对,建错了就删掉重来。
坑 4:域名绑好了,打开却是另一个站点
现象
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 有了自己的专属记录,不再走泛解析。
坑 5:.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)。
坑 6:不带 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 给这个主机名加放行规则。
坑 7: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 内容时,几乎一定是编码问题而不是业务问题。
坑 8:非生产分支会直接覆盖生产
现象
这条是事前发现的,还没踩上:建项目时「版本命令」被填成了 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/* |
| 线上显示别的站点 | 泛解析截胡 | 见踩坑第 4 条 |
| 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 失去独立性。真正的去重方案是模板化(已经做了),而不是拆分产物。
搜索为什么只搜「本页全文 + 别页标题」
顶栏搜索在文档页做两件事:当前这一篇搜正文全文,命中处给出前后约 30 字的上下文并高亮,点一下滚过去;其他文档只搜章节标题,点一下跳 xxx.html#锚点。
文档中心首页没有正文可搜,所以做的是另一件事:下拉列出全站匹配的章节直接跳过去,同时把卡片网格实时过滤掉不相关的(整组落空时组标题一并收起,不留空标题)。卡片的可搜文本不只是卡片自己那几行字,还拼上了该篇的全部章节标题 —— 搜一个只在某一章出现的词,也能把对应的卡片留下来。
这是在三个方案里选的:
| 方案 | 能搜到什么 | 代价 |
|---|---|---|
| A. 只搜当前这篇 | 本篇全文 | 零 —— 正文本来就在 DOM 里 |
| B. A + 别页章节标题 | 本篇全文 + 全站章节 | 每页多约 12 KB 标题索引 |
| C. 全站全文 | 所有正文 | 每页塞进全站正文,约 100 KB → 230 KB |
选 B。C 会让每份 HTML 体积翻倍,而"单文件能单独发出去"是上一条铁律 —— 为了搜索把它牺牲掉不划算;A 又漏掉了"我记得这事写在某一篇里,但想不起是哪篇"这个最常见的场景,而这恰恰是章节标题就能解决的。
实现要点:
- 当前页不需要任何构建期索引。浏览器直接遍历
main里的段落 / 列表项 / 表格单元格 / 代码块,取每个元素"自己的"文字(嵌套列表归下一层,否则外层会把整棵子树重复算一遍),子串匹配即可。 - 中文不分词,直接
indexOf。这个体量用不上 2-gram 之类的相关性排序。 - 跨文档索引是
{{DOC_INDEX}}注入的一个 JSON 数组,用位置而不是键名([slug, 标题, 品牌色, [[锚点, 章节名], ...]]),6 份文档约 12 KB。
为什么源是 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。公开 + 含真值 = 泄密,两者不可兼得。
所以选择:线上只放脱敏版,真值只在本机 dist-private/。这也是为什么「代理」这项 AI 策略必须保持允许 —— 它是整个设计的目的所在。2026-08-16 起受保护文档在脱敏之上又加了一道访问门,但它改变的只是「谁能看到脱敏版」,不改变「线上没有真值」—— 两层是叠加,不是替代。
为什么 AI 免密码的方案是密钥 URL
「人看要密码、AI 看不要密码」字面上自相矛盾:AI 访问就是一次不带登录态的 HTTP 请求,对 AI 无条件放行的 URL,对任何会用 curl 的人同样放行。三个候选:
| 方案 | 结论 |
|---|---|
| 按 User-Agent 放行 AI | UA 一伪造就穿,纯安全剧场 |
| 前端加密(浏览器内解密) | AI 读不了密文,直接违背「AI 可读」的目标 |
| 密钥 URL(选用) | AI 也要凭证,只是免交互 —— 把 ?k= 链接粘给它即可,零摩擦 |
代价:密钥 URL 是 bearer 凭证,泄了就得轮换(一条 wrangler secret put 的事)。收益:这是唯一既真实设防、又不牺牲 AI 可读性的方案。密码与密钥全部放 Worker Secret 而不是写进 worker.js,也是同一逻辑的延伸 —— 仓库和产物里永远没有可被翻出来的值。