machinaix-docs 维护手册
📘 元文档
元文档 · 关于本站

machinaix-docs 文档站维护手册

Static Docs Pipeline · Markdown → HTML → Cloudflare Workers

本站自己的说明书:它是怎么搭起来的、在新机器上怎么跑、怎么写新文档、怎么部署到 Cloudflare、部署时踩过哪些坑、以及怎样把整套东西交给 AI 阅读。看完这一篇就能独立维护和复现整套系统。

线上地址 docs.machinaix.com仓库 ANDREW-SVIP/machinaix-docs(私有)托管 Cloudflare Workers依赖 仅 Python 标准库最后更新 2026-08-23

一概览

这是什么

一套静态文档流水线:你写 Markdown,它产出一整个带导航、搜索、代码高亮、深浅色主题的文档站,并自动部署到 Cloudflare 边缘节点。

src/*.md ┐ theme.html ├──► build.py ──► dist/ ──► worker.js 密码门 ──► docs.machinaix.com site.json ┘ │ 受保护文档拦下,其余透传 └──► llms.txt / llms-full.txt(仅公开文档)──► 供 AI 读取

它解决的四个问题

样式统一

consistency

全部文档共用一套模板。改一次配色或交互,全站生效,不用逐个文件改。

凭证安全

safety

源文件只写占位符,真值单独存放且从不提交。公开产物出现真值时构建直接失败。

AI 可读

machine-readable

公开文档 AI 拿一个 URL 读完;受保护文档凭密钥 URL 单篇喂,同样零摩擦。

访问控制

access control

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,仅本机)

一次构建发生了什么

  1. 读取配置

    读 site.json(站点级设置)和 placeholders.json(占位符标签),若存在 private/secrets.json 也一并读入。

  2. 逐份解析 Markdown

    每个 src/*.md 拆成两部分:顶部 --- 包裹的 JSON front-matter(标题、配色、导航分组、首页卡片信息),以及下方正文。

  3. 渲染正文

    正文按 ## 切分成 <section>,标题里的编号(一、 4.1)提取成徽章,{#id} 作为稳定锚点。表格、代码块、提示框、卡片、清单等逐一转成组件 HTML。

  4. 处理占位符

    %%KEY%% 在公开构建里渲染为脱敏块,在 --private 构建里替换为真实值。

  5. 注入模板

    把标题、导语、侧栏、正文塞进 theme.html 的占位标记,输出自包含单文件。

  6. 生成首页与 AI 入口

    汇总所有 front-matter 里的 card 字段生成 index.html;同时产出 llms.txt(文档清单)和 llms-full.txt / all.txt(全文合并,同一份内容两个名字)。AI 入口只收公开文档,受保护文档连正文带清单条目都不进去;另产出 _auth.json(受保护 slug 清单,worker.js 的鉴权依据,对外访问返回 404)。

  7. 校验内部锚点

    收集全部页面的 id,检查每一个 #锚点 和 xxx.html#锚点 是否真实存在。只报警告不中断构建 —— 死链不影响其他内容,不值得挡住部署。

  8. 泄漏扫描

    扫描 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 完整匹配),模板里出现的标记必须有人填。任一条不满足直接 构建失败。校验只看模板不看产物 —— 产物里的同名字面量是这篇文档在讲解模板,那是内容。

三在新机器上开始

假设你换了一台电脑,或者半年后回来接手,从零开始的完整步骤。

前置条件

需要版本检查命令
Python3.7 或更高python --version
Git任意git --version
浏览器任意现代浏览器—

不需要 Node.js、不需要 pip install、不需要 npm install。整个生成器只用 Python 标准库。

步骤

  1. 克隆仓库
    git clone https://github.com/ANDREW-SVIP/machinaix-docs.git
    cd machinaix-docs

    仓库是私有的,克隆时需要 ANDREW-SVIP 账号的 Git 凭证。首次在新机器上操作会弹出浏览器登录(Git Credential Manager)或要求输入 Personal Access Token。

  2. 构建公开版
    python build.py

    macOS / 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
  3. 本地预览

    直接双击 dist/index.html,或者:

    # Windows
    start dist\index.html
    
    # macOS
    open dist/index.html

    产物是自包含单文件,不需要起本地服务器,file:// 协议直接就能看。

  4. 恢复真实凭证(可选,仅本机需要完整版时)

    private/secrets.json 不在仓库里(这是设计使然)。新机器上有三种拿法:

    1. 从旧机器复制这个文件过来
    2. 从密码管理器里逐项恢复
    3. 照着模板重建:
    cp private/secrets.example.json private/secrets.json
    # 然后编辑 secrets.json 填入真实值

    填好后生成完整版:

    python build.py --private

    产物在 dist-private/,同样是 .gitignore 的,只在本机存在。

  5. 只做体检不写文件
    python build.py --check

    只跑解析和泄漏扫描,不产出文件。适合提交前快速验证。

  6. 新建一份文档
    python build.py --new proxy-v2

    生成带完整 front-matter 骨架的 src/proxy-v2.md,并自动把 "proxy-v2" 追加进 site.json 的 order。已存在则拒绝覆盖。

⚠️
没有 secrets.json 也能正常工作

缺少 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": "卡片底部的小字"
  }
}
字段取值
accentblue / 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 入口都从它取值
protectedtrue 时该文档上线后需密码访问,并自动排除出 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目录树:目录名加粗,# 和 ← 后的注释淡化
diagramASCII 图,支持 {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 密码> 这种尖括号形式,保持代码可读。

新增一个占位符

  1. 在 placeholders.json 加标签

    这个文件会提交,所以只放标签,不放真值:

    "NEW_SERVER_IP": { "label": "新服务器 IP", "note": "用途备注" }
  2. 在 private/secrets.json 加真值

    这个文件不会提交:

    "NEW_SERVER_IP": "这里填真实 IP"
  3. 在 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,但留在磁盘上有两个实际问题:

  1. 它会悄悄过期。 2026-08-07 清理时发现盘上那份是几天前生成的,11 个文件里 9 个内容已经和当前源不一致 —— 真在事故里对着它操作,读到的是旧步骤。
  2. .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。

六新增与维护文档

加一份新文档

  1. 建 Markdown 文件
    python build.py --new xxx

    生成 src/xxx.md 骨架并自动登记进 site.json。也可以手工新建,照抄现有文件的 front-matter 改。

  2. 登记到 site.json

    --new 已经自动做完这步。手工新建时,在 order 数组里加上文件名(不含扩展名),位置决定它在首页和 llms.txt 里的排序:

    "order": ["machinaix-docs", "andrewblog", "yunchuan", "xxx"]

    如果用了新的卡片分组,还要在 cardGroups 里加上组名来控制分组顺序。

  3. 构建验证
    python build.py

    看输出里有没有你的新文档,以及锚点校验和泄漏扫描是否通过。

  4. 推送上线
    git add .
    git commit -m "docs: 新增 xxx 文档"
    git push

    Cloudflare 检测到 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)。

ℹ️
为什么要带 User-Agent

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 Code 时不需要「喂」任何东西

CLAUDE.md 会自动加载 —— 铁律、写作语法、本机 Python 路径、验证命令全在里面。 AndrewBlog 和 apk-dist 也各有一份,会在那边开工时提醒你回来同步文档。

在仓库里干活的 AI 不需要提示词模板

克隆下来、在项目目录里打开 AI,直接说需求就行 —— 不用背模板,也不用交代规矩:

文件谁自动读
AGENTS.md跨工具标准:Cursor、Codex、Gemini CLI、Copilot、aider、Windsurf、Zed、Devin…
CLAUDE.mdClaude 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 私有仓库 │ push 触发 ▼ Cloudflare Workers 构建环境 │ ① python build.py -> 生成 dist/(含泄漏扫描) │ ② npx wrangler deploy -> 读 wrangler.toml,上传 dist/ ▼ Cloudflare 边缘节点 ──► docs.machinaix.com

步骤

  1. 准备 GitHub 私有仓库

    在你本机 Git 凭证所属的那个账号下创建仓库,设为 Private,然后:

    git remote add origin https://github.com/<账号>/machinaix-docs.git
    git branch -M main
    git push -u origin main
  2. 确认仓库里有 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 的注释为准。

  3. 创建 Cloudflare 应用

    Cloudflare Dashboard → Workers 和 Pages → 创建应用程序 → 找到「导入 Git 存储库」或「Pages」标签页。

    新版界面会走「创建 Worker」流程,这是正常的 —— Cloudflare 正在用 Workers 静态资源取代 Pages 新项目。

  4. 授权 GitHub

    选择账号 → Only select repositories → 勾选 machinaix-docs → Install & Authorize。

    必须勾选具体仓库,否则回到 Cloudflare 后在列表里找不到它(私有仓库尤其)。

    选 Only select repositories 而不是 All repositories,是最小权限原则 —— Cloudflare 只需要这一个仓库。

  5. 填写构建配置
    字段值什么时候执行
    项目名称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 条。

  6. 部署并验证

    点「部署」,等约 40 秒。成功后会得到一个 <项目名>.<你的子域>.workers.dev 地址,打开能看到文档中心首页就说明通了。

  7. 绑定自定义域

    Worker 页面 → 域 标签 → 添加域名,填 docs.machinaix.com。

    如果报「已有外部管理的 DNS 记录」,说明 DNS 里有一条同名的 A/CNAME 记录挡着,删掉它再加。如果提示「没有区域匹配」,不要点「查找相似项」(那是卖域名的页面),可以先用「添加路由」顶着。完整过程见踩坑第 3、4 条。

    加完回列表核对名称 —— 输入框可能自动补后缀,拼出 docs.machinaix.com.machinaix.com 这种东西。

  8. 回填 baseUrl

    域名生效后,把 site.json 的 baseUrl 填成正式地址:

    "baseUrl": "https://docs.machinaix.com"

    推送后 llms.txt 里的链接会从相对路径变成完整 URL,AI 抓取时才能正确跳转。

构建环境说明

项情况
Python构建镜像自带,直接 python build.py 即可
依赖安装无 —— 没有 requirements.txt,日志里会显示 No dependencies detected to cache
wranglernpx 首次执行时自动下载(日志里会看到 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 上。

排查过程

  1. 确认域名确实托管在 Cloudflare
    # 查 NS 记录
    dig NS machinaix.com +short

    返回 xxx.ns.cloudflare.com 就说明在 Cloudflare 上。

  2. 确认域名和 Worker 在同一个账号

    Cloudflare 账号主页会同时列出 Domains 和 Workers 两栏。如果域名和 Worker 都在里面,就排除了账号不一致的可能。

    💡

    Worker 只能绑定同账号下的域名,这是最常见的原因,但本例不是。

  3. 判定为界面异常

    域名在、账号对,弹窗却说没有区域匹配 —— 是这个弹窗自身的问题。

解决

改用 添加路由。路由走的是另一套接口,在它的域名选择列表里 machinaix.com 正常出现:

  1. Worker → 域 → + 添加路由
  2. 选择区域 machinaix.com
  3. 路由模式填 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 失效问题。脱敏版文档本来就是"要能单独发出去"的东西,这条约束不能破。

⚠️
不要改成共享 CSS 文件

抽出 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 放行 AIUA 一伪造就穿,纯安全剧场
前端加密(浏览器内解密)AI 读不了密文,直接违背「AI 可读」的目标
密钥 URL(选用)AI 也要凭证,只是免交互 —— 把 ?k= 链接粘给它即可,零摩擦

代价:密钥 URL 是 bearer 凭证,泄了就得轮换(一条 wrangler secret put 的事)。收益:这是唯一既真实设防、又不牺牲 AI 可读性的方案。密码与密钥全部放 Worker Secret 而不是写进 worker.js,也是同一逻辑的延伸 —— 仓库和产物里永远没有可被翻出来的值。