目录

内容控件演示

目录

这篇文章把主题支持的内容控件逐个演示一遍,方便速查。

图片

普通 Markdown 写法就够了,主题会自动加上懒加载:

演示图片

想让图片居中并带图注,用 figure 短路代码:

图注支持 Markdown,比如这里加粗
图注支持 Markdown,比如这里加粗

还可以加小标题、限制宽度、让点击图片跳转:

宽度限制为 420px
小标题宽度限制为 420px

正文里的图片都可以点击放大,按 Esc 或点空白处关闭。

图册

多张图片用 gallery 短路代码,每行写「图片地址 | 说明」:

想固定列数就加 cols="2":

附件下载

把文件放到 static/files/ 下,用 attachment 短路代码做成下载卡片, 文件名和大小会自动读出来:

CSV 演示数据 demo-data.csv · 91 B 下载
点击即可下载
CSV CSV数据 demo-data.csv · 91 B 下载
点击即可下载

PDF 内嵌预览

用 pdf 短路代码把 PDF 直接嵌在文章里。桌面浏览器都能正常预览; 手机浏览器不支持内嵌时会显示"点这里打开"的提示,图注里也始终附有打开链接。

这个浏览器不能直接预览 PDF, 点这里打开或下载。

演示文档一篇PDF演示文档 在新窗口打开

代码

普通代码块还是老写法,鼠标移上去右上角会出现「复制」按钮:

const greeting = (name) => `你好,${name}`;

console.log(greeting('世界'));

想带上文件名和语言标签,用 code 短路代码(可以加 opts="linenos=table" 显示行号):

assets/js/main.js js

引用本地代码片段

同一段代码要在好几篇文章里出现时,不用复制粘贴,把它放进 assets/snippets/, 然后用 snippet 短路代码引用:

new-post.sh bash
#!/usr/bin/env bash
#
# 新建一篇文章,然后直接用编辑器打开。
#
# 用法:
#   ./new-post.sh 我的新文章
#
# 想换编辑器就改下面的 EDITOR,或者先 export EDITOR=vim 再运行。
set -euo pipefail

name="${1:?用法: ./new-post.sh 文章文件名}"
EDITOR="${EDITOR:-code}"

SITE_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SITE_DIR"

hugo new content "posts/${name}.md"
"$EDITOR" "content/posts/${name}.md"

文件名和语言都会自动读出来(语言按扩展名判断),也可以手动指定:

{{< snippet path="nginx.conf" file="网站配置" lang="nginx" opts="linenos=table" >}}

好处是只有一份文件需要维护:改了片段,所有引用它的文章一起更新; 而且引用的是本地文件,构建不需要联网,内容也不会在你不知情的时候变。

音频

把音频文件放进 static/media/,用 audio 短路代码引用:

演示音频浏览器原生播放器,支持键盘操作、倍速和下载

视频

放在自己服务器上的视频用 video 短路代码,可以配封面图:

演示视频6 秒的渐变动画,封面图来自第 2 秒

终端录制(asciinema)

用 asciinema 短路代码播放终端操作录制。下面这段是本地文件(static/casts/demo.cast):

演示录制把鼠标移到画面上,底部会出现播放控件

倍速与循环播放

2 倍速 + 循环播放speed=“2” 两倍速,loop=“true” 循环,autoplay=“true” 一进页面就开始播

让读者自己调速

加上 speeds 参数,播放器下面会出现一个速度选择框:

可调速的播放器读者可以自己切 0.5× / 1× / 1.5× / 2×,切的时候播放进度不会丢

常用参数:

参数作用
speed="2"播放速度,默认 1
loop="true"循环播放
autoplay="true"自动播放(常和 loop 一起用,做成"一直跑着"的效果)
speeds="0.5,1,2"播放器下面加一个速度选择框,让读者自己调速
idle-time-limit="2"空闲时间上限(秒),超过就快进,用来跳过录制里无聊的等待
start-at="3"从第 3 秒开始播
theme="solarized-light"配色,可选 asciinema / tango / solarized-dark / solarized-light / monokai / nord

也可以直接引用 asciinema.org 上的录制,填网址里的编号即可:

{{< asciinema id="335480" autoplay="true" speed="1.5" idle-time-limit="2" theme="solarized-light" >}}

提示框

用来强调说明、注意事项或警告,只有左边一条竖线和标题颜色不同:

折叠面板

把不常看的内容收起来,页面会清爽不少:

点开看看里面有什么

这里是折叠起来的内容,同样支持 Markdown,也可以放图片、代码块等。

  • 第一步
  • 第二步
默认就展开的面板
写了 open="true" 的面板一进来就是展开的。

图表(Mermaid)

用文字描述就能画图,语法参考 Mermaid 文档。 下面三种最常用,图表在浏览器里实时绘制,只有用到它的页面才会加载 Mermaid。

流程图

graph LR
  A[写 Markdown] --> B[hugo 构建]
  B --> C{自己看一眼}
  C -->|没问题| D[public/ 上线]
  C -->|有问题| A
graph LR 从左到右画流程,判断用花括号,箭头上的文字写在两条竖线之间

时序图

sequenceDiagram
  participant U as 读者
  participant B as 浏览器
  participant S as 静态服务器
  U->>B: 打开文章
  B->>S: 请求 HTML
  S-->>B: 返回页面
  B->>S: 再请求 CSS 和 JS
  S-->>B: 返回资源
  B-->>U: 渲染完成
sequenceDiagram 画「谁先跟谁说了什么」,适合写接口调用、协议交互的顺序
sequenceDiagram
    autonumber
    actor User as 用户
    participant CLI as 调试器 CLI
    participant Files as 本地配置/音频文件
    participant OTA as OTA 服务
    participant WS as 小智 WebSocket 服务
    participant ASR as 服务端 STT
    participant LLM as 服务端 LLM
    participant TTS as 服务端 TTS
    participant Player as 本机音频播放器

    User->>CLI: 启动 python debug_websocket.py
    CLI->>Files: 读取 config.json
    Files-->>CLI: device_id、client_id、OTA、交互和音频参数

    alt ota.enabled = true
        CLI->>OTA: HTTPS POST OTA 查询<br/>Activation-Version: 1<br/>Device-Id / Client-Id / User-Agent / Accept-Language
        Note over CLI,OTA: JSON 系统信息包含模拟固件版本、芯片、板型、MAC、UUID、Flash、分区表等

        alt OTA 只返回 activation,没有 websocket 配置
            OTA-->>CLI: activation.message、activation.code
            CLI-->>User: 显示验证码并等待,不退出
            User->>OTA: 在小智平台添加设备并输入验证码
            User->>CLI: 回到终端按 Enter
            CLI->>OTA: 再次 POST OTA 查询
            OTA-->>CLI: websocket.url、token、version 等配置
        else OTA 已返回 websocket 配置
            OTA-->>CLI: websocket.url、token、version 等配置
        end
    else ota.enabled = false
        CLI->>Files: 读取 websocket 直连配置
        Files-->>CLI: URL、token、协议版本
    end

    CLI->>Files: 读取首轮 input_audio 的 Ogg Opus 文件
    Files-->>CLI: 解析出 OpusHead、OpusTags 和音频 Opus packets

    CLI->>WS: 建立 WebSocket 连接
    Note over CLI,WS: 握手 HTTP Headers:Protocol-Version、Device-Id、Client-Id、可选 Authorization

    CLI->>WS: JSON hello<br/>version、transport=websocket、features.mcp=true<br/>audio_params: Opus / 16000 Hz / mono / 60 ms
    WS-->>CLI: JSON hello<br/>transport=websocket、session_id、服务端 audio_params
    Note over CLI,WS: 例如客户端请求 16000 Hz;服务端可能返回 24000 Hz,后续按服务端采样率封装回复音频

    loop 每一轮语音对话
        CLI->>WS: JSON listen/start<br/>session_id、mode=manual
        loop 上传录音中的每个 Opus packet
            CLI->>WS: WebSocket Binary:一个 Opus 音频包
            Note over CLI,WS: 调试器按音频包时长节奏发送;协议 v1 为裸 Opus,v2/v3 带二进制帧头
        end
        CLI->>WS: JSON listen/stop<br/>session_id

        opt 服务端发送 MCP 消息
            WS-->>CLI: JSON type=mcp<br/>例如 initialize / 工具相关 payload
        end

        WS->>ASR: 将收到的语音交给语音识别
        ASR-->>WS: 识别出的用户文本
        WS-->>CLI: JSON type=stt、text
        Note over CLI,User: 这是识别结果,不是调试器上传的文本

        WS->>LLM: 将识别文本交给模型生成回复
        LLM-->>WS: 回复文本;可伴随情绪等信息
        opt 服务端发送模型状态或情绪
            WS-->>CLI: JSON type=llm、emotion 等
        end

        WS->>TTS: 将回复文本合成为语音
        TTS-->>WS: 合成音频及句子边界

        WS-->>CLI: JSON type=tts、state=start
        loop 每个 TTS 句子,可能有多个句子
            WS-->>CLI: JSON type=tts、state=sentence_start、text
            Note over CLI,WS: sentence_start 中的 text 是当前播报句子的文本
            loop 当前句子的每个音频包
                WS-->>CLI: WebSocket Binary:一个下行 Opus 音频包
            end
            WS-->>CLI: JSON type=tts、state=sentence_end
        end
        opt 服务端发送结束标记
            WS-->>CLI: JSON type=tts、state=stop
        end

        alt 收到 tts/stop
            Note over CLI: 本轮 TTS 音频接收完成
        else 服务端未发送 tts/stop,但已收到 sentence_end
            Note over CLI: 等待最后一条消息后的静默时间,默认 3 秒;有新消息则重新计时
        else 回复等待超时
            Note over CLI: response_timeout_seconds 默认 60 秒;若已开始收到 TTS,则保存已收到的音频
        end

        CLI->>Files: 将下行 Opus packets 封装成 Ogg Opus
        Files-->>CLI: response.ogg 或 response_02.ogg 等
        opt 自动播放已启用
            CLI->>Player: 使用系统默认播放器打开回复文件
            Player-->>User: 播放 TTS 回复
        end

        alt interactive = false 或启动参数 --once
            CLI->>WS: 关闭 WebSocket 连接
        else 交互模式
            CLI-->>User: 提示输入下一段 Ogg Opus 录音路径
            alt 输入录音路径
                User->>CLI: 文件路径
                CLI->>Files: 读取并解析下一段录音
            else 直接按 Enter
                User->>CLI: 回车
                CLI->>Files: 重放默认样例录音
            else 输入 q
                User->>CLI: q
                CLI->>WS: 关闭 WebSocket 连接
            end
        end
    end
sequenceDiagram 画「谁先跟谁说了什么」,适合写接口调用、协议交互的顺序

甘特图

gantt
  title 博客改版排期
  dateFormat YYYY-MM-DD
  section 内容
    整理旧文章   :done, a1, 2026-09-01, 5d
    写新文章     :active, a2, after a1, 10d
  section 样式
    定配色       :done, b1, 2026-09-03, 3d
    调正文排版   :b2, after b1, 4d
gantt 画排期:done 是已完成、active 是进行中,after 表示紧接上一项

思维导图

底层同样是 Mermaid,缩进决定层级:

mindmap
root((博客))
  内容
    文章
    关于页
  样式
    颜色变量
    排版
  功能
    搜索
    图册
每多缩进两个空格就是下一层

数学公式

这个主题用 Hugo 内置的 KaTeX,公式在构建时就渲染成 HTML, 页面不加载任何公式脚本,也不会出现「公式先闪一下」。

输入语法就是标准 LaTeX——和 MathJax 是同一套语法。

行内公式

用一对美元符号包起来:E=mc2E = mc^2、α+β=γ\alpha + \beta = \gamma、12\frac{1}{2}、 x2+y2\sqrt{x^2 + y^2}、xi2x_i^2、∑i=1nai\sum_{i=1}^{n} a_i、lim⁡x→0\lim_{x \to 0}、∫0∞e−xdx\int_0^\infty e^{-x}dx。

块级公式

单独占一段,用两个美元符号:

∫−∞∞e−x2 dx=π \int_{-\infty}^{\infty} e^{-x^2} \, dx = \sqrt{\pi} ∑i=1ni=n(n+1)2 \sum_{i=1}^{n} i = \frac{n(n+1)}{2}

常用结构

分数、根号、上下标:

a+bcx3abxi2an+1\frac{a+b}{c} \quad \sqrt[3]{x} \quad \sqrt{\frac{a}{b}} \quad x_i^2 \quad a_{n+1}

向量、希腊字母、关系符:

v⃗⋅w⃗=∑i=1nviwiθ,φ,ε,Δ,Ωx≤y≠z≈w\vec{v} \cdot \vec{w} = \sum_{i=1}^{n} v_i w_i \qquad \theta, \varphi, \varepsilon, \Delta, \Omega \qquad x \leq y \neq z \approx w

极限与导数:

lim⁡n→∞(1+1n)n=e∂2u∂t2=c2∇2u\lim_{n \to \infty} \left( 1 + \frac{1}{n} \right)^n = e \qquad \frac{\partial^2 u}{\partial t^2} = c^2 \nabla^2 u

矩阵:

(abcd)[1001]\begin{pmatrix} a & b \\ c & d \end{pmatrix} \qquad \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix}

分段函数:

f(x)={x2x≥0−xx<0f(x) = \begin{cases} x^2 & x \geq 0 \\ -x & x < 0 \end{cases}

多行对齐(& 是对齐点,\\ 换行):

(a+b)2=a2+2ab+b2=a2+b2+2ab\begin{aligned} (a+b)^2 &= a^2 + 2ab + b^2 \\ &= a^2 + b^2 + 2ab \end{aligned}

化学式(KaTeX 自带 mhchem 扩展):

2 HX2+OX2→2 HX2O\ce{2H2 + O2 -> 2H2O}

算法复杂度:

T(n)=O(nlog⁡n)T(n) = O(n \log n)

公式其他要点

  • 想显示美元符号本身:前面加反斜杠,写成 \$100
  • 公式里放中文:要用 \text{} 包起来,例如 当 x>0\text{当 } x > 0
  • 颜色:可以跟着站点的强调色走,例如 x2+y2{\color{#2a5bd7} x^2 + y^2}
  • 行内公式定界符是 $...$,不要用 \(...\)(符号会被 Markdown 的转义规则吃掉)
  • 公式写错时不会让构建失败,KaTeX 会把它标红显示在原来的位置,正好一眼能看到

外部视频(YouTube / Bilibili)

视频放在视频网站上时,用 youtube / bilibili 短路代码嵌入,会自动按 16:9 铺满正文:

YouTube 示例id 填 YouTube 地址里 watch?v= 后面那串
Bilibili 示例bvid 填 B 站地址里 /video/ 后面那串
{{< youtube id="aqz-KE-bpKQ" start="30" caption="视频下方的说明" >}}
{{< bilibili bvid="BV12341117rG" page="2" >}}

竖屏视频加 ratio="9 / 16" 就能改成竖向。

链接与脚注

站外链接会自动在新标签页打开,并在后面加一个小小的 ↗ 标记,例如 Hugo 官方文档;站内链接保持原样,例如 归档页。

脚注用 Markdown 原生写法就行1,会自动收到文章底部。

分栏卡片

用 columns + column 把内容并排放在卡片里,放不下会自动折行:

前端

  • 只用 CSS 变量
  • 不用前端框架
  • 骨架屏?不必了

后端

  • 没有后端
  • 构建即发布

部署

  • hugo --minify
  • 丢到任意静态托管

看板

kanban + kanban-column 用来展示任务状态,每一栏里的列表项会变成卡片。 屏幕放不下时整块可以左右滑动:

待办

  • 写新文章
  • 整理旧笔记

进行中

  • 给主题补文档

已完成

  • 站内搜索
  • 图册

密码访问

有两种用法。整篇文章加密:在 front matter 里写一行 password = '秘密' (演示文章见「这篇需要密码」)。只加密一段内容:用 protected 短路代码:

这段内容需要密码才能查看

Emoji

hugo.toml 里已经打开了 enableEmoji,所以正文里可以直接写短代码:

写 :smile: :rocket: :sparkles: 会显示成 😄 🚀 ✨。

也可以直接用输入法打出来的表情符号,效果一样 👍


  1. 这就是脚注的内容。另外,鼠标移到任意标题上会出现一个 # 号,点它就能复制这一节的链接地址。 ↩︎

← 返回技术