> For the complete documentation index, see [llms.txt](https://unsloth.ai/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://unsloth.ai/docs/zh/mo-xing/glm-5.2.md).

# GLM-5.2 - 如何在本地运行

GLM-5.2 是 Z.ai 的新开源模型，在长周期编码、推理和智能体任务上提供 SOTA 性能。配合 **744B 参数**、40B 激活参数，以及 **100万上下文** 窗口，它现在可以使用本地运行 [Unsloth Dynamic](/docs/zh/ji-chu/unsloth-dynamic-2.0-ggufs.md) GGUF。GLM-5.2 是 **迄今最强的开源模型** ，在 Artificial Analysis 以及许多其他基准上，其表现可与 Claude 4.8 Opus、GPT-5.5 和 Gemini 3.1 Pro 相媲美。

[Dynamic 1-bit 可达到 **\~76.2%**](/docs/zh/mo-xing/glm-5.2.md#quantization-analysis) 最高 1 名准确率，同时仅为 **小 86%**。Dynamic 2-bit 可达到 **\~82%** 准确率，同时仅为 **小 84%**。这意味着模型并不是因为小 84% 就差了 82%——相反，它只比完整的 **1.5TB** 模型低约 18% 的准确率。感谢 Z.ai 提供 Unsloth 的首发日访问权限。 [**GLM-5.2-GGUF**](https://huggingface.co/unsloth/GLM-5.2-GGUF)

<a href="/pages/986f178fb44971bd081c36c351922d60469ba3ee#run-glm-5.2-tutorials" class="button primary">运行 GLM-5.2 教程</a><a href="/pages/986f178fb44971bd081c36c351922d60469ba3ee#quantization-analysis" class="button secondary">量化结果</a>

#### &#x20;:gear: 使用指南

{% columns %}
{% column %}
2-bit 动态量化 `UD-IQ2_M` 占用 **239GB** 磁盘空间——这可以直接放进一台 **256GB 统一内存的 Mac** 中，并且在 **1x24GB GPU** 和 **256GB RAM** 配合 MoE offloading 时表现良好。

这个 **1-bit** 量化可放入 223GB RAM，而 8-bit 需要 810GB RAM。右侧可查看 1-bit GGUF 的实际效果：
{% endcolumn %}

{% column %}

<figure><img src="/files/987f97b87ca2ace6c3d5ac774a5af4bd1452417b" alt=""><figcaption></figcaption></figure>
{% endcolumn %}
{% endcolumns %}

**表：推理硬件需求** （单位 = 总内存：RAM + VRAM，或统一内存）

<table><thead><tr><th>1-bit</th><th>2-bit</th><th width="128">3-bit</th><th>4-bit</th><th>5-bit</th><th>8-bit</th></tr></thead><tbody><tr><td>223 GB</td><td>245 GB</td><td>290-360 GB</td><td>372-475 GB</td><td>570 GB</td><td>810 GB</td></tr></tbody></table>

{% hint style="success" %}
为获得最佳性能，请确保包括 VRAM 和系统 RAM 在内的总可用内存，比量化后的模型文件大小充裕一些。
{% endhint %}

### 推荐设置

GLM-5.2 有 **3 种思考模式**。两种模式：非思考和思考： **高** + **最大**。复杂任务请使用最大思考。在 [Unsloth Studio](#run-glm-5.2-in-unsloth-studio) 中，你可以通过 UI 轻松切换高 + 最大思考以及非思考。

大多数用例可使用以下设置：

| 默认设置（大多数任务）    | SWE-Bench Pro |
| -------------- | ------------- |
| `温度` = 1.0     | `温度` = 1.0    |
| `top_p` = 0.95 | `top_p` = 1.0 |

* **最大上下文窗口：** `1,048,576`.

#### 禁用思考，修改推理强度

GLM 5.2 默认使用推理。它还支持推理强度，其中 `reasoning_effort` 可以是“high”、“max”或禁用。

要禁用思考，请使用 `--chat-template-kwargs '{"enable_thinking":false}'`。如果你使用的是 **Windows** PowerShell，请使用： `--chat-template-kwargs "{\"enable_thinking\":false}"`

你也可以在 llama.cpp 中使用 `--reasoning 开启` 或 `--reasoning 关闭` 了！

如需自定义推理强度或禁用推理，请使用以下示例：

{% code overflow="wrap" expandable="true" %}

```bash
--chat-template-kwargs '{"reasoning_effort":"max"}'
--chat-template-kwargs '{"reasoning_effort":"high"}'
--chat-template-kwargs '{"enable_thinking":false}'
```

{% endcode %}

### 📈 量化分析

我们还运行了 KLD（KL 散度）基准，以评估我们对 GLM-5.2-GGUF 的量化准确度。动态 4-bit 的 UD-Q4\_K\_XL 和动态 5-bit 的 UD-Q5\_K\_XL 基本无损，而更小的量化也能通过动态地将重要层保留在更高精度、将不重要层降到低比特而表现出色。

在纯 1-top% 准确率上， **动态 1-bit 的准确率约为 76.2%，但体积小 86%**！动态 2-bit 的准确率约为 82%，同时体积小 84%。这表明将部分层动态量化到更高精度，并不会因为体积小 86% 就让模型差 86%——它只是比完整的 1.5TB 模型能力低约 24%。

但“76% 准确率”究竟描述的是什么？

76% 的 top-1% 并不意味着“法国的首都是”会变成 76% 选巴黎、24% 选悉尼。模型并不会因此“更笨” 24%。在这里，巴黎始终是 100%，悉尼是 0%。76% 这个数字包括了整个语料库中的填充词和停用词，例如提问：

{% columns %}
{% column %}

{% endcolumn %}

{% column %}

{% endcolumn %}
{% endcolumns %}

“创建一部小说”会由于 LLM 采样而得到：

* 我现在将创作一部小说……
* 小说如下：
* 你希望它是什么类型？

每个例子都是正确的，但 \[I, The, What] 的分布会发生变化——基线可能 100% 的时间都用 \[I]，但现在 \[I] 是 76%，\[The] 是 24%。

这并不 **意味着** 你会有 24% 的时间得到乱码或错误输出。

<figure><img src="/files/56fb444007638c9871603420d95794b326998ce0" alt=""><figcaption></figcaption></figure>

99.9% 的 KLD 通常也不错——不过从 4bit 开始会有更明显的提升，所以对于大规模分布外任务，动态 4-bit 可能是最佳选择。

Top-1% 是 KLD 本身的一个“强制”二项分布。KLD 是基线（BF16 或 Q8\_0）与量化版本之间概率的“距离”。量化的目标是最小化下面的目标函数：

$$
\text{minimize    } \frac{1}{n} \sum{\text{D}\_{\text{KL}}\[\text{ }f(q(W))\text{ }||\text{ } f(W))\text{ }]}
$$

其中 `f` 是语言模型的前向传播，而 `q` 是量化操作，而 `W` 是模型的参数或权重。目标是让基线的 logits 输出 `f(W)` 与量化模型输出之间的“距离”尽可能小。如果你能使 KLD 为 0，那你就完美重建了这个模型！

<figure><img src="/files/6403fa414cdce1933b3a025cc1effad04fd9ecff" alt=""><figcaption></figcaption></figure>

我们使用 **平均 KLD** ，如下所示，因为在整个训练语料库上运行 KLD 很昂贵（例如 15T tokens）——所以我们改为进行 **采样，** 并获得训练语料库的一个小型代表性子集 **/** 下游任务，并对其进行优化。 平均 KLD 通常与磁盘空间呈单调趋势，而且即使在 1-bit 下，GLM 5.2 也表现良好！

Top-1% 准确率本质上就是一个贪婪解码算子，我们假设会选择 argmax 项；而对于 1bit，76% 的情况它选择与基线的 argmax 相同。

<figure><img src="/files/9f06ac28998d05d5b31cd2cf5e6fdf50916fd1b0" alt=""><figcaption></figcaption></figure>

## 运行 GLM-5.2 教程：

现在你可以在 [llama.cpp](#run-in-llama.cpp) 和 [Unsloth Studio](https://unsloth.ai/docs/zh/mo-xing/pages/986f178fb44971bd081c36c351922d60469ba3ee#run-glm-5.2-in-unsloth-studio)中运行 GLM-5.2。 [`UD-IQ2_M`](https://huggingface.co/unsloth/GLM-5.2-GGUF/tree/main/UD-IQ2_M) 我们将使用 239GB

### 🦥 在 Unsloth Studio 中运行 GLM-5.2

GLM-5.2 可以运行在 [Unsloth Studio](/docs/zh/xin/studio.md)，这是一款用于本地 AI 的开源网页 UI。 **Unsloth Studio 会自动卸载到 RAM 并检测多 GPU 配置**。使用 Unsloth Studio，你可以在 **MacOS、Windows**、Linux 以及：

{% columns %}
{% column %}

* 搜索、下载、 [运行 GGUF](/docs/zh/xin/studio.md#run-models-locally) 和 safetensor 模型
* [**自我修复** 工具调用](/docs/zh/xin/studio.md#execute-code--heal-tool-calling) + **网页搜索**
* [**代码执行**](/docs/zh/xin/studio.md#run-models-locally) （Python、Bash）
* [自动推理](/docs/zh/xin/studio.md#model-arena) 参数调优（temp、top-p 等）
* 通过 llama.cpp 实现快速 CPU + GPU 推理
* [训练 LLM](/docs/zh/xin/studio.md#no-code-training) 速度快 2 倍，显存占用少 70%
  {% endcolumn %}

{% column %}

<figure><img src="/files/5df03bf298fa3050e0d7c80885f1195f18b47756" alt=""><figcaption><p>2-bit GLM-5.2 GGUF 输出</p></figcaption></figure>
{% endcolumn %}
{% endcolumns %}

{% stepper %}
{% step %}
**安装并启动 Unsloth**

安装时，请在终端中运行：

MacOS、Linux、WSL：

```bash
curl -fsSL https://unsloth.ai/install.sh | sh
```

Windows PowerShell：

```bash
irm https://unsloth.ai/install.ps1 | iex
```

**启动 Unsloth**

MacOS、Linux、WSL 和 Windows：

```bash
unsloth studio -H 0.0.0.0 -p 8888
```

然后打开 `http://127.0.0.1:8888` （或你的特定 URL）在浏览器中。

**通过 HTTPS 和 Cloudflare 安全启动 Unsloth**

**新！** Unsloth 现在提供了一种通过免费的 Cloudflare 隧道，以 HTTPS 安全启动 Studio 的方式。使用以下命令（适用于 Windows、Mac 和 Linux）：

```bash
unsloth studio --secure
```

{% endstep %}

{% step %}
**搜索并下载 GLM-5.2**

Unsloth Studio 会自动卸载到 RAM 并检测多 GPU 配置。首次启动时，你需要创建密码来保护你的账户，并在之后重新登录。

然后进入 [Studio Chat](/docs/zh/xin/studio/chat.md) 选项卡并搜索 **GLM-5.2** 在搜索栏中下载你想要的模型和量化版本。确保你有足够的算力来运行该模型。

<div data-with-frame="true"><figure><img src="/files/b8b3f8a3783bbc5e3ef3502df75a1f0e5769d166" alt="" width="563"><figcaption></figcaption></figure></div>
{% endstep %}

{% step %}
**运行 GLM-5.2**

使用 Unsloth Studio 时，推理参数应会自动设置，不过你仍然可以手动更改。你还可以编辑上下文长度、聊天模板和其他设置。

更多信息可查看我们的 [Unsloth Studio 推理指南](/docs/zh/xin/studio/chat.md).

<div data-with-frame="true"><figure><img src="/files/3bc54b8fb8cdc5dc26e557b72ddaba20e7a64232" alt=""><figcaption><p>在 Canvas 中运行 1-bit GLM-5.2 的示例</p></figcaption></figure></div>
{% endstep %}
{% endstepper %}

### 🦙 在 llama.cpp 中运行 GLM-5.2

在本指南中，我们将运行 `UD-IQ2_M` 量化版本，它至少需要 245GB RAM。你可以随意更改量化类型。对于这些教程，我们将使用 [llama.cpp](llama.cpphttps://github.com/ggml-org/llama.cpp) 用于快速本地推理。GGUF： [**GLM-5.2-GGUF**](https://huggingface.co/unsloth/GLM-5.2-GGUF)&#x20;

{% stepper %}
{% step %}
获取最新的 `llama.cpp` **在** [**GitHub 这里**](https://github.com/ggml-org/llama.cpp)。你也可以按照下面的构建说明进行操作。将 `-DGGML_CUDA=ON` 改为 `-DGGML_CUDA=OFF` 如果你没有 GPU，或者只想进行 CPU 推理。 **对于 Apple Mac / Metal 设备**，设置 `-DGGML_CUDA=OFF` 然后照常继续——Metal 支持默认开启。

```bash
apt-get update
apt-get install pciutils build-essential cmake curl libcurl4-openssl-dev -y
git clone https://github.com/ggml-org/llama.cpp
cmake llama.cpp -B llama.cpp/build \\
    -DBUILD_SHARED_LIBS=OFF -DGGML_CUDA=ON
cmake --build llama.cpp/build --config Release -j --clean-first --target llama-cli llama-mtmd-cli llama-server llama-gguf-split
cp llama.cpp/build/bin/llama-* llama.cpp
```

{% endstep %}

{% step %}
你现在可以使用 `llama.cpp` 直接加载和下载模型，就像 `ollama run`一样。首先，选择你想要的量化类型，例如 `UD-IQ2_M`。同时使用 `export LLAMA_CACHE="unsloth/GLM-5.2-GGUF"` 来强制 `llama.cpp` 保存到特定位置。 **注意，此下载过程可能会非常慢**，所以最好使用下一节中的手动下载流程。

```bash
export LLAMA_CACHE="unsloth/GLM-5.2-GGUF"
./llama.cpp/llama-cli \\
    -hf unsloth/GLM-5.2-GGUF:UD-IQ2_M \\
    --temp 1.0 \\
    --top-p 0.95 \\
    --min-p 0.01
```

{% endstep %}

{% step %}
如果你想手动下载模型 **（快得多！）**，我们可以通过下面的代码下载模型（在安装 `pip install huggingface_hub`之后）。如果下载卡住，请查看： [Hugging Face Hub、XET 调试](/docs/zh/ji-chu/troubleshooting-and-faqs/hugging-face-hub-xet-debugging.md)

```bash
hf download unsloth/GLM-5.2-GGUF \\
    --local-dir unsloth/GLM-5.2-GGUF \\
    --include "*UD-IQ2_M*" # 使用 "*UD-Q8_K_XL*" 可获得接近全精度
```

如果你想使用动态 1bit，请这样做：

{% code overflow="wrap" expandable="true" %}

```bash
hf download unsloth/GLM-5.2-GGUF \\
    --local-dir unsloth/GLM-5.2-GGUF \\
    --include "*UD-IQ1_S*"
```

{% endcode %}
{% endstep %}

{% step %}
然后以对话模式运行模型。使用 `unsloth/GLM-5.2-GGUF/UD-IQ2_M/GLM-5.2-UD-IQ2_M-00001-of-00006.gguf` 用于 2bit 或 `unsloth/GLM-5.2-GGUF/UD-IQ1_S/GLM-5.2-UD-IQ1_S-00001-of-00006.gguf`  用于 1bit。

{% code overflow="wrap" %}

```bash
./llama.cpp/llama-cli \\
    --model unsloth/GLM-5.2-GGUF/UD-IQ2_M/GLM-5.2-UD-IQ2_M-00001-of-00006.gguf \\
    --temp 1.0 \\
    --top-p 0.95 \\
    --min-p 0.01
```

{% endcode %}
{% endstep %}

{% step %}
启动 llama-cli 时，你会看到：

<figure><img src="/files/48643b68022ac721e3a17f6a7a04fb5af945e739" alt=""><figcaption></figcaption></figure>

然后在提示它制作一个简短的 Flappy Bird 游戏后，我们得到：

<figure><img src="/files/724ed044585167917048ea5bac1927ff98f45711" alt=""><figcaption></figcaption></figure>

完整对话和下面的游戏：

<details>

<summary>完整 HTML 游戏</summary>

{% code overflow="wrap" expandable="true" %}

```html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>日落飞鸟</title>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Fred:wght@400;700&display=swap" rel="stylesheet">
<style>
  :root {
    --sunset-1: #ff6b6b;
    --sunset-2: #feca50;
    --sunset-3: #ff9ff3;
    --dusk: #36306b;
    --night: #1a1746;
    --accent: #ffd93b;
    --coral: #ff6b6b;
    --pipe: #4a902b;
    --pipe-dark: #2d5a1a;
  }

  * { margin: 0; padding: 0; box-sizing: border-box; }

  html, body {
    height: 100%;
    width: 100%;
    overflow: hidden;
    background: var(--night);
    font-family: 'Fred', sans-serif;
    -webkit-user-select: none;
    user-select: none;
    touch-action: manipulation;
  }

  #game-wrap {
    position: relative;
    width: 100vw;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    background: linear-gradient(180deg, #1a1746 0%, #36306b 40%, #ff6b6b 70%, #feca50 100%);
  }

  #game-frame {
    position: relative;
    width: min(100vw, 480px);
    height: min(100vh, 720px);
    max-height: 100vh;
    box-shadow: 0 30px 80px rgba(0,0,0,0.6), inset 0 0 0 1px rgba(255,255,255,0.05);
    overflow: hidden;
    background: linear-gradient(180deg, #4a3a8e 0%, #ff6b6b 60%, #feca50 100%);
  }

  canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
  }

  .overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    pointer-events: none;
    z-index: 10;
    transition: opacity 0.3s ease;
  }

  .overlay.hidden {
    opacity: 0;
    pointer-events: none;
  }

  .overlay.visible {
    opacity: 1;
    pointer-events: auto;
  }

  .panel {
    background: rgba(26, 23, 70, 0.85);
    border: 3px solid var(--accent);
    border-radius: 16px;
    padding: 28px 36px;
    text-align: center;
    color: #fff;
    box-shadow: 0 12px 0 rgba(0,0,0,0.3), 0 0 40px rgba(255, 217, 59, 0.4);
    backdrop-filter: blur(4px);
    transform: translateY(0);
    animation: bob 3s ease-in-out infinite;
  }

  @keyframes bob {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-6px); }
  }

  .title {
    font-family: 'Press Start 2P', monospace;
    font-size: 26px;
    color: var(--accent);
    text-shadow: 3px 3px 0 #b87c0a, 6px 6px 0 rgba(0,0,0,0.3);
    letter-spacing: 1px;
    margin-bottom: 6px;
    line-height: 1.3;
  }

  .subtitle {
    font-size: 14px;
    color: #ffe8a8;
    margin-bottom: 20px;
    font-weight: 700;
  }

  .tap-icon {
    font-size: 42px;
    margin: 8px 0;
    animation: tap 1.2s ease-in-out infinite;
  }

  @keyframes tap {
    0%, 50% { transform: translateY(0) scale(1); }
    20% { transform: translateY(-8px) scale(1.1); }
  }

  .instructions {
    font-size: 13px;
    color: #fff;
    opacity: 0.85;
    margin-top: 10px;
    font-weight: 400;
  }

  .score-row {
    display: flex;
    gap: 24px;
    justify-content: center;
    margin: 12px 0 20px;
  }

  .score-box {
    background: rgba(0,0,0,0.4);
    border: 2px solid var(--accent);
    border-radius: 10px;
    padding: 10px 18px;
    min-width: 80px;
  }

  .score-box .label {
    font-family: 'Press Start 2p', monospace;
    font-size: 9px;
    color: var(--accent);
    margin-bottom: 4px;
    letter-spacing: 1px;
  }

  .score-box .value {
    font-family: 'Press Start 2p', monospace;
    font-size: 18px;
    color: #fff;
  }

  .score-box.best .value { color: var(--coral); }

  .btn {
    font-family: 'Press Start 2p', monospace;
    font-size: 12px;
    color: var(--night);
    background: var(--accent);
    border: none;
    padding: 12px 22px;
    border-radius: 8px;
    cursor: pointer;
    letter-spacing: 1px;
    box-shadow: 0 6px 0 #b87c0a, 0 8px 12px rgba(0,0,0,0.3);
    transition: transform 0.1s, box-shadow 0.1s;
    pointer-events: auto;
  }

  .btn:hover { transform: translateY(2px); box-shadow: 0 4px 0 #b87c0a, 0 6px 10px rgba(0,0,0,0.3); }
  .btn:active { transform: translateY(6px); box-shadow: 0 0 0 #b87c0a, 0 2px 6px rgba(0,0,0,0.3); }

  #hud {
    position: absolute;
    top: 24px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 5;
    font-family: 'Press Start 2P', monospace;
    font-size: 36px;
    color: #fff;
    text-shadow: 3px 3px 0 #b87c0a, 5px 5px 0 rgba(0,0,0,0.5);
    pointer-events: none;
    transition: opacity 0.3s;
    opacity: 0;
  }

  #hud.visible { opacity: 1; }

  #hud .new-best {
    font-size: 11px;
    color: var(--coral);
    text-shadow: 2px 2px 0 #000;
    margin-top: 8px;
    opacity: 0;
    transition: opacity 0.3s;
  }

  #hud.has-new-best .new-best { opacity: 1; animation: pulse 0.6s ease infinite alternate; }

  @keyframes pulse {
    from { transform: translateX(-50%) scale(1); }
    to { transform: translateX(-50%) scale(1.15); }
  }

  .medal {
    font-family: 'Press Start 2P', monospace;
    font-size: 48px;
    margin: 10px 0;
    text-shadow: 3px 3px 0 #000;
  }

  .flash {
    position: absolute;
    inset: 0;
    background: #fff;
    opacity: 0;
    pointer-events: none;
    z-index: 8;
  }

  .footer {
    position: absolute;
    bottom: 12px;
    left: 50%;
    transform: translateX(-50%);
    font-size: 11px;
    color: rgba(255,255,255,0.7);
    font-weight: 400;
    text-align: center;
    pointer-events: none;
    z-index: 9;
  }
</style>
</head>
<body>
<div id="game-wrap">
  <div id="game-frame">
    <canvas id="canvas"></canvas>
    <div id="hud" class="hud">
      <div id="hud-score">0</div>
      <div class="new-best">新纪录！</div>
    </div>
    <div id="flash" class="flash"></div>

    <!-- 开始界面 -->
    <div id="start-screen" class="overlay visible">
      <div class="panel">
        <div class="title">日落<br>飞行者</div>
        <div class="subtitle">— 黄昏飞行 —</div>
        <div class="tap-icon">✊</div>
        <div class="instructions">轻点 / 空格 / 点击 来拍动翅膀</div>
      </div>
    </div>

    <!-- 游戏结束界面 -->
    <div id="end-screen" class="overlay hidden">
      <div class="panel">
        <div class="title" style="font-size:22px;color:var(--coral);">游戏结束</div>
        <div id="medal" class="medal" style="display:none;">★</div>
        <div class="score-row">
          <div class="score-box">
            <div class="label">得分</div>
            <div class="value" id="end-score">0</div>
          </div>
          <div class="score-box best">
            <div class="label">最佳</div>
            <div class="value" id="end-best">0</div>
          </div>
        </div>
        <button class="btn" id="restart-btn">重试</button>
      </div>
    </div>

    <div class="footer">声音开启 • 轻点起飞</div>
  </div>
</div>

<script>
(() => {
  // ============ 设置 ============
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const frame = document.getElementById('game-frame');
  const hud = document.getElementById('hud');
  const hudScore = document.getElementById('hud-score');
  const startScreen = document.getElementById('start-screen');
  const endScreen = document.getElementById('end-screen');
  const endScore = document.getElementById('end-score');
  const endBest = document.getElementById('end-best');
  const medalEl = document.getElementById('medal');
  const flashEl = document.getElementById('flash');
  const restartBtn = document.getElementById('restart-btn');

  let W = 480, H = 720;
  const dpr = window.devicePixelRatio || 1;

  function resize() {
    const rect = frame.getBoundingClientRect();
    W = rect.width;
    H = rect.height;
    canvas.width = W * dpr;
    canvas.height = H * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }
  window.addEventListener('resize', resize);
  resize();

  // ============ 音频（Web Audio API） ============
  let audioCtx = null;
  function getAudio() {
    if (!audioCtx) {
      try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
      catch(e) { audioCtx = null; }
    }
    if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
    return audioCtx;
  }

  function beep(freq, dur=0.1, type='square', vol=0.1, slideTo=null) {
    const ac = getAudio();
    if (!ac) return;
    const osc = ac.createOscillator();
    const gain = ac.createGain();
    osc.type = type;
    osc.frequency.value = freq;
    if (slideTo) osc.frequency.linearRampToValueAtTime(slideTo, ac.currentTime + dur);
    gain.gain.value = vol;
    gain.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + dur);
    osc.connect(gain);
    gain.connect(ac.destination);
    osc.start();
    osc.stop(ac.currentTime + dur);
  }

  const sfx = {
    flap: () => beep(420, 0.12, 'square', 0.08, 720),
    score: () => { beep(880, 0.08, 'triangle', 0.12); setTimeout(()=>beep(1320, 0.1, 'triangle', 0.1), 60); }
    hit: () => { beep(180, 0.25, 'sawtooth', 0.15, 60); }
    die: () => { beep(120, 0.5, 'sawtooth', 0.15, 40); }
  };

  // ============ 游戏状态 ============
  const STATE = { READY: 0, PLAYING: 1, DYING: 2, OVER: 3 };
  let state = STATE.READY;

  let bird, pipes, particles, score, best, frameCount, shake, groundX, clouds, stars;

  best = parseInt(localStorage.getItem('sunsetFlierBest') || '0', 10) || 0;
  endBest.textContent = best;

  // ============ 小鸟 ============
  function makeBird() {
    return {
      x: W * 0.32,
      y: H * 0.5,
      vy: 0,
      radius: 14,
      wingPhase: 0,
      rotation: 0,
      alive: true
    };
  }

  // ============ 管道 ============
  const PIPE_W = 68;
  const PIPE_GAP = 180;
  const PIPE_SPEED = 2.6;
  const PIPE_SPACING = 220;

  function makePipe(x) {
    const minY = 90;
    const maxY = H - 90 - PIPE_GAP;
    const gapY = minY + Math.random() * (maxY - minY);
    return { x, gapY, scored: false, passed: false };
  }

  function initPipes() {
    pipes = [];
    let x = W + 80;
    for (let i = 0; i < 4; i++) {
      pipes.push(makePipe(x));
      x += PIPE_SPACING;
    }
  }

  // ============ 粒子 ============
  function spawnParticles(x, y, color, count=10, vel=3) {
    for (let i = 0; i < count; i++) {
      const angle = Math.random() * Math.PI * 2;
      const speed = (Math.random() * 0.6 + 0.4) * vel;
      particles.push({
        x, y,
        vx: Math.cos(angle) * speed,
        vy: Math.sin(angle) * speed - 1,
        life: 1,
        size: Math.random() * 4 + 2,
        color
      });
    }
  }

  // ============ 背景 ============
  function initBg() {
    clouds = [];
    for (let i = 0; i < 4; i++) {
      clouds.push({
        x: Math.random() * W,
        y: 80 + Math.random() * 180,
        scale: 0.6 + Math.random() * 0.8,
        speed: 0.2 + Math.random() * 0.3
      });
    }
    stars = [];
    for (let i = 0; i < 30; i++) {
      stars.push({
        x: Math.random() * W,
        y: Math.random() * H * 0.4,
        r: Math.random() * 1.5 + 0.3,
        tw: Math.random() * Math.PI * 2
      });
    }
  }

  // ============ 重置 / 开始 ============
  function reset() {
    bird = makeBird();
    pipes = [];
    particles = [];
    score = 0;
    frameCount = 0;
    shake = 0;
    groundX = 0;
    initPipes();
    initBg();
    hudScore.textContent = '0';
    hud.classList.remove('has-new-best');
  }

  function startGame() {
    state = STATE.PLAYING;
    startScreen.classList.add('hidden');
    startScreen.classList.remove('visible');
    hud.classList.add('visible');
    bird.vy = -7.2;
    sfx.flap();
  }

  function gameOver() {
    state = STATE.DYING;
    sfx.hit();
    setTimeout(sfx.die, 120);
    shake = 18;
    spawnParticles(bird.x, bird.y, '#ffd93b', 24, 5);
    spawnParticles(bird.x, bird.y, '#ff6b6b', 16, 4);
    flashScreen();
    bird.alive = false;

    const isNewBest = score > best;
    if (isNewBest) {
      best = score;
      localStorage.setItem('sunsetFlierBest', String(best));
    }

    setTimeout(() => {
      state = STATE.OVER;
      endScore.textContent = score;
      endBest.textContent = best;
      const medal = getMedal(score);
      if (medal) {
        medalEl.style.display = 'block';
        medalEl.textContent = medal.symbol;
        medalEl.style.color = medal.color;
      } else {
        medalEl.style.display = 'none';
      }
      endScreen.classList.remove('hidden');
      endScreen.classList.add('visible');
      if (isNewBest) {
        setTimeout(()=>hud.classList.add('has-new-best'), 0);
      }
    }, 700);
  }

  function flashScreen() {
    flashEl.style.transition = 'none';
    flashEl.style.opacity = '0.9';
    requestAnimationFrame(() => {
      flashEl.style.transition = 'opacity 0.4s ease-out';
      flashEl.style.opacity = '0';
    });
  }

  function getMedal(s) {
    if (s >= 30) return { symbol: '★', color: '#ffd93b' };
    if (s >= 20) return { symbol: '✦', color: '#c0c0c0' };
    if (s >= 10) return { symbol: '✧', color: '#cd7f32' };
    if (s >= 5)  return { symbol: '•', color: '#ff9ff3' };
    return null;
  }

  function restart() {
    reset();
    state = STATE.READY;
    endScreen.classList.add('hidden');
    endScreen.classList.remove('visible');
    startScreen.classList.remove('hidden');
    startScreen.classList.add('visible');
    hud.classList.remove('visible');
    hud.classList.remove('has-new-best');
  }

  // ============ 输入 ============
  function flap() {
    if (state === STATE.READY) { startGame(); return; }
    if (state === STATE.PLAYING) {
      bird.vy = -7.2;
      bird.wingPhase = 0;
      sfx.flap();
      spawnParticles(bird.x - 8, bird.y + 6, '#fff8d6', 4, 1.5);
    }
    if (state === STATE.OVER) {
      // 结束界面任意位置点击也会重新开始
      // 但我们主要让按钮来处理它
    }
  }

  function handleInput(e) {
    if (e.target.closest('button')) return;
    e.preventDefault();
    flap();
  }

  frame.addEventListener('mousedown', handleInput);
  frame.addEventListener('touchstart', handleInput, { passive: false });
  window.addEventListener('keydown', (e) => {
    if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'Enter') {
      e.preventDefault();
      if (state === STATE.OVER) restart();
      else flap();
    }
  });
  restartBtn.addEventListener('click', (e) => { e.stopPropagation(); restart(); });

  // ============ 更新 ============
  const GRAVITY = 0.42;
  const MAX_FALL = 9;

  function update() {
    frameCount++;
    if (state === STATE.READY) {
      // 待机摆动
      bird.y = H * 0.5 + Math.sin(frameCount * 0.08) * 14;
      bird.wingPhase += 0.2;
      bird.rotation = Math.sin(frameCount * 0.08) * 0.05;
    }

    if (state === STATE.PLAYING) {
      bird.vy += GRAVITY;
      if (bird.vy > MAX_FALL) bird.vy = MAX_FALL;
      bird.y += bird.vy;
      bird.wingPhase += 0.4;
      bird.rotation = Math.max(-0.4, Math.min(1.2, bird.vy * 0.08));

      // 管道
      for (const p of pipes) {
        p.x -= PIPE_SPEED;
        if (!p.scored && p.x + PIPE_W < bird.x) {
          p.scored = true;
          score++;
          hudScore.textContent = score;
          sfx.score();
          spawnParticles(bird.x, bird.y, '#ffd93b', 8, 2);
        }
      }
      // 移除屏幕外的管道，添加新的
      while (pipes.length > 0 && pipes[0].x + PIPE_W < -10) pipes.shift();
      const last = pipes[pipes.length - 1];
      if (last && last.x < W - PIPE_SPACING) {
        pipes.push(makePipe(last.x + PIPE_SPACING));
      }

      // 碰撞
      // 地面与天花板
      const groundY = H - 80;
      if (bird.y + bird.radius > groundY || bird.y - bird.radius < 0) {
        bird.y = Math.max(bird.radius + 1, Math.min(groundY - bird.radius, bird.y));
        gameOver();
        return;
      }
      // 管道
      for (const p of pipes) {
        if (p.x > bird.x + bird.radius || p.x + PIPE_W < bird.x - bird.radius) continue;
        const top = p.gapY;
        const bottom = p.gapY + PIPE_GAP;
        if (bird.y - bird.radius < top || bird.y + bird.radius > bottom) {
          // 碰撞
          gameOver();
          return;
        }
      }
    }

    if (state === STATE.DYING) {
      bird.vy += GRAVITY * 1.2;
      bird.y += bird.vy;
      bird.rotation += 0.06;
      const groundY = H - 80;
      if (bird.y + bird.radius > groundY) {
        bird.y = groundY - bird.radius;
        bird.vy = 0;
      }
    }

    // 粒子
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.x += p.vx;
      p.y += p.vy;
      p.vy += 0.15;
      p.life -= 0.02;
      if (p.life <= 0) particles.splice(i, 1);
    }

    // 抖动衰减
    if (shake > 0) shake = Math.max(0, shake - 1.2);

    // 背景动画
    for (const c of clouds) {
      c.x -= c.speed;
      if (c.x < -100) c.x = W + 100;
    }
    for (const s of stars) {
      s.tw += 0.05;
    }

    groundX = (groundX - PIPE_SPEED) % 120;
  }

  // ============ 渲染 ============
  function render() {
    ctx.save();
    // 抖动
    if (shake > 0) {
      const sx = (Math.random() - 0.5) * shake;
      const sy = (Math.random() - 0.5) * shake;
      ctx.translate(sx, sy);
    }

    // 天空渐变
    const sky = ctx.createLinearGradient(0, 0, 0, H);
    sky.addColorStop(0, '#1a1746');
    sky.addColorStop(0.3, '#4a3a8e');
    sky.addColorStop(0.55, '#ff6b6b');
    sky.addColorStop(0.8, '#feca50');
    sky.addColorStop(1, '#ffd93b');
    ctx.fillStyle = sky;
    ctx.fillRect(-20, -20, W + 40, H + 40);

    // 星星
    for (const s of stars) {
      const a = 0.4 + Math.sin(s.tw) * 0.4;
      ctx.fillStyle = `rgba(255,255,255,${a})`;
      ctx.beginPath();
      ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
      ctx.fill();
    }

    // 太阳
    const sunX = W * 0.75;
    const sunY = H * 0.65;
    const sunGlow = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, 180);
    sunGlow.addColorStop(0, 'rgba(255,240,160,0.7)');
    sunGlow.addColorStop(0.4, 'rgba(255,200,120,0.3)');
    sunGlow.addColorStop(1, 'rgba(255,200,120,0)');
    ctx.fillStyle = sunGlow;
    ctx.fillRect(sunX - 200, sunY - 200, 400, 400);

    ctx.fillStyle = '#fff6d0';
    ctx.beginPath();
    ctx.arc(sunX, sunY, 42, 0, Math.PI * 2);
    ctx.fill();

    // 云朵
    for (const c of clouds) {
      drawCloud(c.x, c.y, c.scale);
    }

    // 山脉剪影
    ctx.fillStyle = 'rgba(40, 30, 80, 0.7)';
    ctx.beginPath();
    ctx.moveTo(0, H * 0.72);
    const peaks = [
      [W * 0.15, H * 0.58],
      [W * 0.25, H * 0.68],
      [W * 0.4, H * 0.52],
      [W * 0.55, H * 0.66],
      [W * 0.7, H * 0.6],
      [W * 0.85, H * 0.68],
      [W, H * 0.62]
    ];
    for (const [px, py] of peaks) ctx.lineTo(px, py);
    ctx.lineTo(W, H);
    ctx.lineTo(0, H);
    ctx.closePath();
    ctx.fill();

    // 管道
    for (const p of pipes) {
      drawPipe(p);
    }

    // 地面
    drawGround();

    // 小鸟
    drawBird();

    // 粒子
    for (const part of particles) {
      ctx.fillStyle = part.color;
      ctx.globalAlpha = part.life;
      ctx.beginPath();
      ctx.arc(part.x, part.y, part.size, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;

    ctx.restore();
  }

  function drawCloud(x, y, scale) {
    ctx.fillStyle = 'rgba(255, 230, 200, 0.85)';
    const s = scale;
    ctx.beginPath();
    ctx.arc(x, y, 18 * s, 0, Math.PI * 2);
    ctx.arc(x + 18 * s, y + 4 * s, 22 * s, 0, Math.PI * 2);
    ctx.arc(x + 38 * s, y, 16 * s, 0, Math.PI * 2);
    ctx.arc(x + 18 * s, y - 6 * s, 14 * s, 0, Math.PI * 2);
    ctx.fill();
  }

  function drawPipe(p) {
    const x = p.x;
    const topY = p.gapY;
    const bottomY = p.gapY + PIPE_GAP;
    const groundY = H - 80;

    // 渐变
    const grad = ctx.createLinearGradient(x, 0, x + PIPE_W, 0);
    grad.addColorStop(0, '#2d5a1a');
    grad.addColorStop(0.15, '#4a902b');
    grad.addColorStop(0.5, '#7bc94a');
    grad.addColorStop(0.85, '#4a902b');
    grad.addColorStop(1, '#2d5a1a');

    // 上方管道
    ctx.fillStyle = grad;
    ctx.fillRect(x, 0, PIPE_W, topY - 18);
    // 上方管帽
    ctx.fillRect(x - 4, topY - 18, PIPE_W + 8, 18);
    // 上方管帽高光
    ctx.fillStyle = 'rgba(255,255,255,0.15)';
    ctx.fillRect(x - 4, topY - 18, 14, 18);

    // 下方管道
    ctx.fillStyle = grad;
    ctx.fillRect(x, bottomY + 18, PIPE_W, groundY - (bottomY + 18));
    // 下方管帽
    ctx.fillRect(x - 4, bottomY, PIPE_W + 8, 18);
    ctx.fillStyle = 'rgba(255,255,255,0.15)';
    ctx.fillRect(x - 4, bottomY, 14, 18);

    // 描边
    ctx.strokeStyle = '#1c3a0d';
    ctx.lineWidth = 2;
    ctx.strokeRect(x, 0, PIPE_W, topY - 18);
    ctx.strokeRect(x - 4, topY - 18, PIPE_W + 8, 18);
    ctx.strokeRect(x, bottomY + 18, PIPE_W, groundY - (bottomY + 18));
    ctx.strokeRect(x - 4, bottomY, PIPE_W + 8, 18);
  }

  function drawGround() {
    const groundY = H - 80;
    // 沙地/黄昏地面
    const g = ctx.createLinearGradient(0, groundY, 0, H);
    g.addColorStop(0, '#8b5a2b');
    g.addColorStop(0.4, '#5e3a1c');
    g.addColorStop(1, '#2d1a0c');
    ctx.fillStyle = g;
    ctx.fillRect(0, groundY, W, 80);

    // 表面线
    ctx.strokeStyle = '#1c3a0d';
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(0, groundY);
    ctx.lineTo(W, groundY);
    ctx.stroke();

    // 图案条纹（移动）
    ctx.fillStyle = 'rgba(0,0,0,0.15)';
    for (let i = -2; i < W / 40 + 2; i++) {
      const stripeX = (i * 80 + groundX + 120) % (W + 80) - 40;
      ctx.beginPath();
      ctx.moveTo(stripeX, groundY);
      ctx.lineTo(stripeX + 30, groundY + 80);
      ctx.lineTo(stripeX + 50, groundY + 80);
      ctx.lineTo(stripeX + 20, groundY);
      ctx.closePath();
      ctx.fill();
    }

    // 表面的卵石
    ctx.fillStyle = '#3d2412';
    for (let i = 0; i < 12; i++) {
      const px = (i * 60 + groundX + 60) % (W + 40);
      ctx.beginPath();
      ctx.arc(px, groundY + 4, 3, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawBird() {
    const b = bird;
    ctx.save();
    ctx.translate(b.x, b.y);
    ctx.rotate(b.rotation);

    // 阴影
    ctx.fillStyle = 'rgba(0,0,0,0.2)';
    ctx.beginPath();
    ctx.ellipse(2, 2, b.radius, b.radius * 0.85, 0, 0, Math.PI * 2);
    ctx.fill();

    // 身体
    const bodyGrad = ctx.createRadialGradient(-4, -4, 2, 0, 0, b.radius + 4);
    bodyGrad.addColorStop(0, '#ff9ff3');
    bodyGrad.addColorStop(0.5, '#ff6b6b');
    bodyGrad.addColorStop(1, '#c0334a');
    ctx.fillStyle = bodyGrad;
    ctx.beginPath();
    ctx.ellipse(0, 0, b.radius, b.radius * 0.92, 0, 0, Math.PI * 2);
    ctx.fill();

    // 翅膀（拍动）
    const wingY = Math.sin(b.wingPhase) * 8;
    ctx.fillStyle = '#ffd93b';
    ctx.beginPath();
    ctx.ellipse(-4, 4 + wingY, 9, 5, -0.4 + wingY * 0.05, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = '#b87c0a';
    ctx.lineWidth = 1.5;
    ctx.stroke();

    // 眼睛
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(8, -4, 5, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#1a1746';
    ctx.beginPath();
    ctx.arc(9.5, -4, 2.5, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(10, -5, 1, 0, Math.PI * 2);
    ctx.fill();

    // 喙
    ctx.fillStyle = '#ffaa33';
    ctx.beginPath();
    ctx.moveTo(13, -1);
    ctx.lineTo(20, 1);
    ctx.lineTo(13, 3);
    ctx.closePath();
    ctx.fill();
    ctx.strokeStyle = '#b87c0a';
    ctx.lineWidth = 1;
    ctx.stroke();

    // 尾巴
    ctx.fillStyle = '#ffd93b';
    ctx.beginPath();
    ctx.moveTo(-12, -2);
    ctx.lineTo(-18, -5);
    ctx.lineTo(-16, 0);
    ctx.lineTo(-18, 5);
    ctx.lineTo(-12, 3);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    ctx.restore();
  }

  // ============ 主循环 ============
  function loop() {
    update();
    render();
    requestAnimationFrame(loop);
  }

  reset();
  loop();
})();
</script>
</body>
</html>
```

{% endcode %}

</details>

<details>

<summary>完整对话</summary>

{% code overflow="wrap" expandable="true" %}

````
[开始思考]
用户想要一个简短的 Flappy Bird 游戏。我将创建一个完整、自包含的 HTML 文件，其中包括：

1. 经典的 Flappy Bird 风格游戏
2. 精美的图形和动画
3. 分数记录
4. 游戏结束界面
5. 重启功能
6. 良好的物理效果（重力、跳跃机制）
7. 会移动且随机生成的管道
8. 音效（使用 Web Audio API 生成音调）
9. 美观的视觉风格——我会避开经典的黄色小鸟，改用更有趣的配色方案

让我这样设计：
- 漂亮的天空渐变背景
- 会拍动翅膀的动画小鸟
- 具有漂亮绿色/金属质感外观的管道
- 分数显示
- 开始界面、游玩状态、游戏结束状态
- 使用 localStorage 记录最高分
- 得分时的粒子效果
- 流畅的动画

我会使用日落/黄昏配色，让它更具辨识度——温暖的橙色和紫色，小鸟则采用温暖的珊瑚红色。

让我认真把代码写出来：

主要功能：
- 基于 Canvas 的渲染
- requestAnimationFrame 游戏循环
- 小鸟物理：重力、跳跃冲量
- 管道生成：随机间隙，向左移动
- 碰撞检测
- 通过管道时分数增加
- 得分时的粒子效果
- 死亡动画
- 使用 localStorage 保存最高分
- 通过 Web Audio API 实现音效
- 移动端/触控支持
- 简洁、精致的 UI 覆盖层

让我来编写代码。
[结束思考]

我将创建一个精致的 Flappy Bird 游戏，具有独特的黄昏/日落美学、流畅的物理效果、粒子效果和音效。

```html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>日落飞鸟</title>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Fred:wght@400;700&display=swap" rel="stylesheet">
<style>
  :root {
    --sunset-1: #ff6b6b;
    --sunset-2: #feca50;
    --sunset-3: #ff9ff3;
    --dusk: #36306b;
    --night: #1a1746;
    --accent: #ffd93b;
    --coral: #ff6b6b;
    --pipe: #4a902b;
    --pipe-dark: #2d5a1a;
  }

  * { margin: 0; padding: 0; box-sizing: border-box; }

  html, body {
    height: 100%;
    width: 100%;
    overflow: hidden;
    background: var(--night);
    font-family: 'Fred', sans-serif;
    -webkit-user-select: none;
    user-select: none;
    touch-action: manipulation;
  }

  #game-wrap {
    position: relative;
    width: 100vw;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    background: linear-gradient(180deg, #1a1746 0%, #36306b 40%, #ff6b6b 70%, #feca50 100%);
  }

  #game-frame {
    position: relative;
    width: min(100vw, 480px);
    height: min(100vh, 720px);
    max-height: 100vh;
    box-shadow: 0 30px 80px rgba(0,0,0,0.6), inset 0 0 0 1px rgba(255,255,255,0.05);
    overflow: hidden;
    background: linear-gradient(180deg, #4a3a8e 0%, #ff6b6b 60%, #feca50 100%);
  }

  canvas {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
  }

  .overlay {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    pointer-events: none;
    z-index: 10;
    transition: opacity 0.3s ease;
  }

  .overlay.hidden {
    opacity: 0;
    pointer-events: none;
  }

  .overlay.visible {
    opacity: 1;
    pointer-events: auto;
  }

  .panel {
    background: rgba(26, 23, 70, 0.85);
    border: 3px solid var(--accent);
    border-radius: 16px;
    padding: 28px 36px;
    text-align: center;
    color: #fff;
    box-shadow: 0 12px 0 rgba(0,0,0,0.3), 0 0 40px rgba(255, 217, 59, 0.4);
    backdrop-filter: blur(4px);
    transform: translateY(0);
    animation: bob 3s ease-in-out infinite;
  }

  @keyframes bob {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-6px); }
  }

  .title {
    font-family: 'Press Start 2P', monospace;
    font-size: 26px;
    color: var(--accent);
    text-shadow: 3px 3px 0 #b87c0a, 6px 6px 0 rgba(0,0,0,0.3);
    letter-spacing: 1px;
    margin-bottom: 6px;
    line-height: 1.3;
  }

  .subtitle {
    font-size: 14px;
    color: #ffe8a8;
    margin-bottom: 20px;
    font-weight: 700;
  }

  .tap-icon {
    font-size: 42px;
    margin: 8px 0;
    animation: tap 1.2s ease-in-out infinite;
  }

  @keyframes tap {
    0%, 50% { transform: translateY(0) scale(1); }
    20% { transform: translateY(-8px) scale(1.1); }
  }

  .instructions {
    font-size: 13px;
    color: #fff;
    opacity: 0.85;
    margin-top: 10px;
    font-weight: 400;
  }

  .score-row {
    display: flex;
    gap: 24px;
    justify-content: center;
    margin: 12px 0 20px;
  }

  .score-box {
    background: rgba(0,0,0,0.4);
    border: 2px solid var(--accent);
    border-radius: 10px;
    padding: 10px 18px;
    min-width: 80px;
  }

  .score-box .label {
    font-family: 'Press Start 2p', monospace;
    font-size: 9px;
    color: var(--accent);
    margin-bottom: 4px;
    letter-spacing: 1px;
  }

  .score-box .value {
    font-family: 'Press Start 2p', monospace;
    font-size: 18px;
    color: #fff;
  }

  .score-box.best .value { color: var(--coral); }

  .btn {
    font-family: 'Press Start 2p', monospace;
    font-size: 12px;
    color: var(--night);
    background: var(--accent);
    border: none;
    padding: 12px 22px;
    border-radius: 8px;
    cursor: pointer;
    letter-spacing: 1px;
    box-shadow: 0 6px 0 #b87c0a, 0 8px 12px rgba(0,0,0,0.3);
    transition: transform 0.1s, box-shadow 0.1s;
    pointer-events: auto;
  }

  .btn:hover { transform: translateY(2px); box-shadow: 0 4px 0 #b87c0a, 0 6px 10px rgba(0,0,0,0.3); }
  .btn:active { transform: translateY(6px); box-shadow: 0 0 0 #b87c0a, 0 2px 6px rgba(0,0,0,0.3); }

  #hud {
    position: absolute;
    top: 24px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 5;
    font-family: 'Press Start 2P', monospace;
    font-size: 36px;
    color: #fff;
    text-shadow: 3px 3px 0 #b87c0a, 5px 5px 0 rgba(0,0,0,0.5);
    pointer-events: none;
    transition: opacity 0.3s;
    opacity: 0;
  }

  #hud.visible { opacity: 1; }

  #hud .new-best {
    font-size: 11px;
    color: var(--coral);
    text-shadow: 2px 2px 0 #000;
    margin-top: 8px;
    opacity: 0;
    transition: opacity 0.3s;
  }

  #hud.has-new-best .new-best { opacity: 1; animation: pulse 0.6s ease infinite alternate; }

  @keyframes pulse {
    from { transform: translateX(-50%) scale(1); }
    to { transform: translateX(-50%) scale(1.15); }
  }

  .medal {
    font-family: 'Press Start 2P', monospace;
    font-size: 48px;
    margin: 10px 0;
    text-shadow: 3px 3px 0 #000;
  }

  .flash {
    position: absolute;
    inset: 0;
    background: #fff;
    opacity: 0;
    pointer-events: none;
    z-index: 8;
  }

  .footer {
    position: absolute;
    bottom: 12px;
    left: 50%;
    transform: translateX(-50%);
    font-size: 11px;
    color: rgba(255,255,255,0.7);
    font-weight: 400;
    text-align: center;
    pointer-events: none;
    z-index: 9;
  }
</style>
</head>
<body>
<div id="game-wrap">
  <div id="game-frame">
    <canvas id="canvas"></canvas>
    <div id="hud" class="hud">
      <div id="hud-score">0</div>
      <div class="new-best">新纪录！</div>
    </div>
    <div id="flash" class="flash"></div>

    <!-- 开始界面 -->
    <div id="start-screen" class="overlay visible">
      <div class="panel">
        <div class="title">日落<br>飞行者</div>
        <div class="subtitle">— 黄昏飞行 —</div>
        <div class="tap-icon">✊</div>
        <div class="instructions">轻点 / 空格 / 点击 来拍动翅膀</div>
      </div>
    </div>

    <!-- 游戏结束界面 -->
    <div id="end-screen" class="overlay hidden">
      <div class="panel">
        <div class="title" style="font-size:22px;color:var(--coral);">游戏结束</div>
        <div id="medal" class="medal" style="display:none;">★</div>
        <div class="score-row">
          <div class="score-box">
            <div class="label">得分</div>
            <div class="value" id="end-score">0</div>
          </div>
          <div class="score-box best">
            <div class="label">最佳</div>
            <div class="value" id="end-best">0</div>
          </div>
        </div>
        <button class="btn" id="restart-btn">重试</button>
      </div>
    </div>

    <div class="footer">声音开启 • 轻点起飞</div>
  </div>
</div>

<script>
(() => {
  // ============ 设置 ============
  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const frame = document.getElementById('game-frame');
  const hud = document.getElementById('hud');
  const hudScore = document.getElementById('hud-score');
  const startScreen = document.getElementById('start-screen');
  const endScreen = document.getElementById('end-screen');
  const endScore = document.getElementById('end-score');
  const endBest = document.getElementById('end-best');
  const medalEl = document.getElementById('medal');
  const flashEl = document.getElementById('flash');
  const restartBtn = document.getElementById('restart-btn');

  let W = 480, H = 720;
  const dpr = window.devicePixelRatio || 1;

  function resize() {
    const rect = frame.getBoundingClientRect();
    W = rect.width;
    H = rect.height;
    canvas.width = W * dpr;
    canvas.height = H * dpr;
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }
  window.addEventListener('resize', resize);
  resize();

  // ============ 音频（Web Audio API） ============
  let audioCtx = null;
  function getAudio() {
    if (!audioCtx) {
      try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
      catch(e) { audioCtx = null; }
    }
    if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
    return audioCtx;
  }

  function beep(freq, dur=0.1, type='square', vol=0.1, slideTo=null) {
    const ac = getAudio();
    if (!ac) return;
    const osc = ac.createOscillator();
    const gain = ac.createGain();
    osc.type = type;
    osc.frequency.value = freq;
    if (slideTo) osc.frequency.linearRampToValueAtTime(slideTo, ac.currentTime + dur);
    gain.gain.value = vol;
    gain.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + dur);
    osc.connect(gain);
    gain.connect(ac.destination);
    osc.start();
    osc.stop(ac.currentTime + dur);
  }

  const sfx = {
    flap: () => beep(420, 0.12, 'square', 0.08, 720),
    score: () => { beep(880, 0.08, 'triangle', 0.12); setTimeout(()=>beep(1320, 0.1, 'triangle', 0.1), 60); }
    hit: () => { beep(180, 0.25, 'sawtooth', 0.15, 60); }
    die: () => { beep(120, 0.5, 'sawtooth', 0.15, 40); }
  };

  // ============ 游戏状态 ============
  const STATE = { READY: 0, PLAYING: 1, DYING: 2, OVER: 3 };
  let state = STATE.READY;

  let bird, pipes, particles, score, best, frameCount, shake, groundX, clouds, stars;

  best = parseInt(localStorage.getItem('sunsetFlierBest') || '0', 10) || 0;
  endBest.textContent = best;

  // ============ 小鸟 ============
  function makeBird() {
    return {
      x: W * 0.32,
      y: H * 0.5,
      vy: 0,
      radius: 14,
      wingPhase: 0,
      rotation: 0,
      alive: true
    };
  }

  // ============ 管道 ============
  const PIPE_W = 68;
  const PIPE_GAP = 180;
  const PIPE_SPEED = 2.6;
  const PIPE_SPACING = 220;

  function makePipe(x) {
    const minY = 90;
    const maxY = H - 90 - PIPE_GAP;
    const gapY = minY + Math.random() * (maxY - minY);
    return { x, gapY, scored: false, passed: false };
  }

  function initPipes() {
    pipes = [];
    let x = W + 80;
    for (let i = 0; i < 4; i++) {
      pipes.push(makePipe(x));
      x += PIPE_SPACING;
    }
  }

  // ============ 粒子 ============
  function spawnParticles(x, y, color, count=10, vel=3) {
    for (let i = 0; i < count; i++) {
      const angle = Math.random() * Math.PI * 2;
      const speed = (Math.random() * 0.6 + 0.4) * vel;
      particles.push({
        x, y,
        vx: Math.cos(angle) * speed,
        vy: Math.sin(angle) * speed - 1,
        life: 1,
        size: Math.random() * 4 + 2,
        color
      });
    }
  }

  // ============ 背景 ============
  function initBg() {
    clouds = [];
    for (let i = 0; i < 4; i++) {
      clouds.push({
        x: Math.random() * W,
        y: 80 + Math.random() * 180,
        scale: 0.6 + Math.random() * 0.8,
        speed: 0.2 + Math.random() * 0.3
      });
    }
    stars = [];
    for (let i = 0; i < 30; i++) {
      stars.push({
        x: Math.random() * W,
        y: Math.random() * H * 0.4,
        r: Math.random() * 1.5 + 0.3,
        tw: Math.random() * Math.PI * 2
      });
    }
  }

  // ============ 重置 / 开始 ============
  function reset() {
    bird = makeBird();
    pipes = [];
    particles = [];
    score = 0;
    frameCount = 0;
    shake = 0;
    groundX = 0;
    initPipes();
    initBg();
    hudScore.textContent = '0';
    hud.classList.remove('has-new-best');
  }

  function startGame() {
    state = STATE.PLAYING;
    startScreen.classList.add('hidden');
    startScreen.classList.remove('visible');
    hud.classList.add('visible');
    bird.vy = -7.2;
    sfx.flap();
  }

  function gameOver() {
    state = STATE.DYING;
    sfx.hit();
    setTimeout(sfx.die, 120);
    shake = 18;
    spawnParticles(bird.x, bird.y, '#ffd93b', 24, 5);
    spawnParticles(bird.x, bird.y, '#ff6b6b', 16, 4);
    flashScreen();
    bird.alive = false;

    const isNewBest = score > best;
    if (isNewBest) {
      best = score;
      localStorage.setItem('sunsetFlierBest', String(best));
    }

    setTimeout(() => {
      state = STATE.OVER;
      endScore.textContent = score;
      endBest.textContent = best;
      const medal = getMedal(score);
      if (medal) {
        medalEl.style.display = 'block';
        medalEl.textContent = medal.symbol;
        medalEl.style.color = medal.color;
      } else {
        medalEl.style.display = 'none';
      }
      endScreen.classList.remove('hidden');
      endScreen.classList.add('visible');
      if (isNewBest) {
        setTimeout(()=>hud.classList.add('has-new-best'), 0);
      }
    }, 700);
  }

  function flashScreen() {
    flashEl.style.transition = 'none';
    flashEl.style.opacity = '0.9';
    requestAnimationFrame(() => {
      flashEl.style.transition = 'opacity 0.4s ease-out';
      flashEl.style.opacity = '0';
    });
  }

  function getMedal(s) {
    if (s >= 30) return { symbol: '★', color: '#ffd93b' };
    if (s >= 20) return { symbol: '✦', color: '#c0c0c0' };
    if (s >= 10) return { symbol: '✧', color: '#cd7f32' };
    if (s >= 5)  return { symbol: '•', color: '#ff9ff3' };
    return null;
  }

  function restart() {
    reset();
    state = STATE.READY;
    endScreen.classList.add('hidden');
    endScreen.classList.remove('visible');
    startScreen.classList.remove('hidden');
    startScreen.classList.add('visible');
    hud.classList.remove('visible');
    hud.classList.remove('has-new-best');
  }

  // ============ 输入 ============
  function flap() {
    if (state === STATE.READY) { startGame(); return; }
    if (state === STATE.PLAYING) {
      bird.vy = -7.2;
      bird.wingPhase = 0;
      sfx.flap();
      spawnParticles(bird.x - 8, bird.y + 6, '#fff8d6', 4, 1.5);
    }
    if (state === STATE.OVER) {
      // 结束界面任意位置点击也会重新开始
      // 但我们主要让按钮来处理它
    }
  }

  function handleInput(e) {
    if (e.target.closest('button')) return;
    e.preventDefault();
    flap();
  }

  frame.addEventListener('mousedown', handleInput);
  frame.addEventListener('touchstart', handleInput, { passive: false });
  window.addEventListener('keydown', (e) => {
    if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'Enter') {
      e.preventDefault();
      if (state === STATE.OVER) restart();
      else flap();
    }
  });
  restartBtn.addEventListener('click', (e) => { e.stopPropagation(); restart(); });

  // ============ 更新 ============
  const GRAVITY = 0.42;
  const MAX_FALL = 9;

  function update() {
    frameCount++;
    if (state === STATE.READY) {
      // 待机摆动
      bird.y = H * 0.5 + Math.sin(frameCount * 0.08) * 14;
      bird.wingPhase += 0.2;
      bird.rotation = Math.sin(frameCount * 0.08) * 0.05;
    }

    if (state === STATE.PLAYING) {
      bird.vy += GRAVITY;
      if (bird.vy > MAX_FALL) bird.vy = MAX_FALL;
      bird.y += bird.vy;
      bird.wingPhase += 0.4;
      bird.rotation = Math.max(-0.4, Math.min(1.2, bird.vy * 0.08));

      // 管道
      for (const p of pipes) {
        p.x -= PIPE_SPEED;
        if (!p.scored && p.x + PIPE_W < bird.x) {
          p.scored = true;
          score++;
          hudScore.textContent = score;
          sfx.score();
          spawnParticles(bird.x, bird.y, '#ffd93b', 8, 2);
        }
      }
      // 移除屏幕外的管道，添加新的
      while (pipes.length > 0 && pipes[0].x + PIPE_W < -10) pipes.shift();
      const last = pipes[pipes.length - 1];
      if (last && last.x < W - PIPE_SPACING) {
        pipes.push(makePipe(last.x + PIPE_SPACING));
      }

      // 碰撞
      // 地面与天花板
      const groundY = H - 80;
      if (bird.y + bird.radius > groundY || bird.y - bird.radius < 0) {
        bird.y = Math.max(bird.radius + 1, Math.min(groundY - bird.radius, bird.y));
        gameOver();
        return;
      }
      // 管道
      for (const p of pipes) {
        if (p.x > bird.x + bird.radius || p.x + PIPE_W < bird.x - bird.radius) continue;
        const top = p.gapY;
        const bottom = p.gapY + PIPE_GAP;
        if (bird.y - bird.radius < top || bird.y + bird.radius > bottom) {
          // 碰撞
          gameOver();
          return;
        }
      }
    }

    if (state === STATE.DYING) {
      bird.vy += GRAVITY * 1.2;
      bird.y += bird.vy;
      bird.rotation += 0.06;
      const groundY = H - 80;
      if (bird.y + bird.radius > groundY) {
        bird.y = groundY - bird.radius;
        bird.vy = 0;
      }
    }

    // 粒子
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.x += p.vx;
      p.y += p.vy;
      p.vy += 0.15;
      p.life -= 0.02;
      if (p.life <= 0) particles.splice(i, 1);
    }

    // 抖动衰减
    if (shake > 0) shake = Math.max(0, shake - 1.2);

    // 背景动画
    for (const c of clouds) {
      c.x -= c.speed;
      if (c.x < -100) c.x = W + 100;
    }
    for (const s of stars) {
      s.tw += 0.05;
    }

    groundX = (groundX - PIPE_SPEED) % 120;
  }

  // ============ 渲染 ============
  function render() {
    ctx.save();
    // 抖动
    if (shake > 0) {
      const sx = (Math.random() - 0.5) * shake;
      const sy = (Math.random() - 0.5) * shake;
      ctx.translate(sx, sy);
    }

    // 天空渐变
    const sky = ctx.createLinearGradient(0, 0, 0, H);
    sky.addColorStop(0, '#1a1746');
    sky.addColorStop(0.3, '#4a3a8e');
    sky.addColorStop(0.55, '#ff6b6b');
    sky.addColorStop(0.8, '#feca50');
    sky.addColorStop(1, '#ffd93b');
    ctx.fillStyle = sky;
    ctx.fillRect(-20, -20, W + 40, H + 40);

    // 星星
    for (const s of stars) {
      const a = 0.4 + Math.sin(s.tw) * 0.4;
      ctx.fillStyle = `rgba(255,255,255,${a})`;
      ctx.beginPath();
      ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
      ctx.fill();
    }

    // 太阳
    const sunX = W * 0.75;
    const sunY = H * 0.65;
    const sunGlow = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, 180);
    sunGlow.addColorStop(0, 'rgba(255,240,160,0.7)');
    sunGlow.addColorStop(0.4, 'rgba(255,200,120,0.3)');
    sunGlow.addColorStop(1, 'rgba(255,200,120,0)');
    ctx.fillStyle = sunGlow;
    ctx.fillRect(sunX - 200, sunY - 200, 400, 400);

    ctx.fillStyle = '#fff6d0';
    ctx.beginPath();
    ctx.arc(sunX, sunY, 42, 0, Math.PI * 2);
    ctx.fill();

    // 云朵
    for (const c of clouds) {
      drawCloud(c.x, c.y, c.scale);
    }

    // 山脉剪影
    ctx.fillStyle = 'rgba(40, 30, 80, 0.7)';
    ctx.beginPath();
    ctx.moveTo(0, H * 0.72);
    const peaks = [
      [W * 0.15, H * 0.58],
      [W * 0.25, H * 0.68],
      [W * 0.4, H * 0.52],
      [W * 0.55, H * 0.66],
      [W * 0.7, H * 0.6],
      [W * 0.85, H * 0.68],
      [W, H * 0.62]
    ];
    for (const [px, py] of peaks) ctx.lineTo(px, py);
    ctx.lineTo(W, H);
    ctx.lineTo(0, H);
    ctx.closePath();
    ctx.fill();

    // 管道
    for (const p of pipes) {
      drawPipe(p);
    }

    // 地面
    drawGround();

    // 小鸟
    drawBird();

    // 粒子
    for (const part of particles) {
      ctx.fillStyle = part.color;
      ctx.globalAlpha = part.life;
      ctx.beginPath();
      ctx.arc(part.x, part.y, part.size, 0, Math.PI * 2);
      ctx.fill();
    }
    ctx.globalAlpha = 1;

    ctx.restore();
  }

  function drawCloud(x, y, scale) {
    ctx.fillStyle = 'rgba(255, 230, 200, 0.85)';
    const s = scale;
    ctx.beginPath();
    ctx.arc(x, y, 18 * s, 0, Math.PI * 2);
    ctx.arc(x + 18 * s, y + 4 * s, 22 * s, 0, Math.PI * 2);
    ctx.arc(x + 38 * s, y, 16 * s, 0, Math.PI * 2);
    ctx.arc(x + 18 * s, y - 6 * s, 14 * s, 0, Math.PI * 2);
    ctx.fill();
  }

  function drawPipe(p) {
    const x = p.x;
    const topY = p.gapY;
    const bottomY = p.gapY + PIPE_GAP;
    const groundY = H - 80;

    // 渐变
    const grad = ctx.createLinearGradient(x, 0, x + PIPE_W, 0);
    grad.addColorStop(0, '#2d5a1a');
    grad.addColorStop(0.15, '#4a902b');
    grad.addColorStop(0.5, '#7bc94a');
    grad.addColorStop(0.85, '#4a902b');
    grad.addColorStop(1, '#2d5a1a');

    // 上方管道
    ctx.fillStyle = grad;
    ctx.fillRect(x, 0, PIPE_W, topY - 18);
    // 上方管帽
    ctx.fillRect(x - 4, topY - 18, PIPE_W + 8, 18);
    // 上方管帽高光
    ctx.fillStyle = 'rgba(255,255,255,0.15)';
    ctx.fillRect(x - 4, topY - 18, 14, 18);

    // 下方管道
    ctx.fillStyle = grad;
    ctx.fillRect(x, bottomY + 18, PIPE_W, groundY - (bottomY + 18));
    // 下方管帽
    ctx.fillRect(x - 4, bottomY, PIPE_W + 8, 18);
    ctx.fillStyle = 'rgba(255,255,255,0.15)';
    ctx.fillRect(x - 4, bottomY, 14, 18);

    // 描边
    ctx.strokeStyle = '#1c3a0d';
    ctx.lineWidth = 2;
    ctx.strokeRect(x, 0, PIPE_W, topY - 18);
    ctx.strokeRect(x - 4, topY - 18, PIPE_W + 8, 18);
    ctx.strokeRect(x, bottomY + 18, PIPE_W, groundY - (bottomY + 18));
    ctx.strokeRect(x - 4, bottomY, PIPE_W + 8, 18);
  }

  function drawGround() {
    const groundY = H - 80;
    // 沙地/黄昏地面
    const g = ctx.createLinearGradient(0, groundY, 0, H);
    g.addColorStop(0, '#8b5a2b');
    g.addColorStop(0.4, '#5e3a1c');
    g.addColorStop(1, '#2d1a0c');
    ctx.fillStyle = g;
    ctx.fillRect(0, groundY, W, 80);

    // 表面线
    ctx.strokeStyle = '#1c3a0d';
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(0, groundY);
    ctx.lineTo(W, groundY);
    ctx.stroke();

    // 图案条纹（移动）
    ctx.fillStyle = 'rgba(0,0,0,0.15)';
    for (let i = -2; i < W / 40 + 2; i++) {
      const stripeX = (i * 80 + groundX + 120) % (W + 80) - 40;
      ctx.beginPath();
      ctx.moveTo(stripeX, groundY);
      ctx.lineTo(stripeX + 30, groundY + 80);
      ctx.lineTo(stripeX + 50, groundY + 80);
      ctx.lineTo(stripeX + 20, groundY);
      ctx.closePath();
      ctx.fill();
    }

    // 表面的卵石
    ctx.fillStyle = '#3d2412';
    for (let i = 0; i < 12; i++) {
      const px = (i * 60 + groundX + 60) % (W + 40);
      ctx.beginPath();
      ctx.arc(px, groundY + 4, 3, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function drawBird() {
    const b = bird;
    ctx.save();
    ctx.translate(b.x, b.y);
    ctx.rotate(b.rotation);

    // 阴影
    ctx.fillStyle = 'rgba(0,0,0,0.2)';
    ctx.beginPath();
    ctx.ellipse(2, 2, b.radius, b.radius * 0.85, 0, 0, Math.PI * 2);
    ctx.fill();

    // 身体
    const bodyGrad = ctx.createRadialGradient(-4, -4, 2, 0, 0, b.radius + 4);
    bodyGrad.addColorStop(0, '#ff9ff3');
    bodyGrad.addColorStop(0.5, '#ff6b6b');
    bodyGrad.addColorStop(1, '#c0334a');
    ctx.fillStyle = bodyGrad;
    ctx.beginPath();
    ctx.ellipse(0, 0, b.radius, b.radius * 0.92, 0, 0, Math.PI * 2);
    ctx.fill();

    // 翅膀（拍动）
    const wingY = Math.sin(b.wingPhase) * 8;
    ctx.fillStyle = '#ffd93b';
    ctx.beginPath();
    ctx.ellipse(-4, 4 + wingY, 9, 5, -0.4 + wingY * 0.05, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = '#b87c0a';
    ctx.lineWidth = 1.5;
    ctx.stroke();

    // 眼睛
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(8, -4, 5, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#1a1746';
    ctx.beginPath();
    ctx.arc(9.5, -4, 2.5, 0, Math.PI * 2);
    ctx.fill();
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(10, -5, 1, 0, Math.PI * 2);
    ctx.fill();

    // 喙
    ctx.fillStyle = '#ffaa33';
    ctx.beginPath();
    ctx.moveTo(13, -1);
    ctx.lineTo(20, 1);
    ctx.lineTo(13, 3);
    ctx.closePath();
    ctx.fill();
    ctx.strokeStyle = '#b87c0a';
    ctx.lineWidth = 1;
    ctx.stroke();

    // 尾巴
    ctx.fillStyle = '#ffd93b';
    ctx.beginPath();
    ctx.moveTo(-12, -2);
    ctx.lineTo(-18, -5);
    ctx.lineTo(-16, 0);
    ctx.lineTo(-18, 5);
    ctx.lineTo(-12, 3);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    ctx.restore();
  }

  // ============ 主循环 ============
  function loop() {
    update();
    render();
    requestAnimationFrame(loop);
  }

  reset();
  loop();
})();
</script>
</body>
</html>
```

我构建的内容如下：

**主题**："Sunset Flier"——我没有采用经典的绿色/蓝绿色 Flappy Bird 风格，而是选择了黄昏日落配色（深紫 → 珊瑚红 → 金色），并加入山脉剪影、发光的太阳、飘动的云朵以及上方天空中闪烁的星星。

**小鸟**：一只珊瑚粉色的小鸟，带有黄色拍动翅膀、会动的喙、尾巴和会跟踪的眼睛。当小鸟上升/下降时，翅膀会进行物理动画式拍动。

**机制**：
- 经典的重力 + 拍动冲量物理，调校得更灵敏
- 随机间隙的管道，平滑滚动，并通过带高光的管帽渲染，呈现厚实的 2D 外观
- 小鸟通过管道时分数增加，并伴随粒子爆裂和提示音特效
- 与管道、天花板和地面的碰撞会触发游戏结束，并带有屏幕闪烁、屏幕抖动和死亡粒子

**润色**：
- 使用 Press Start 2P 像素字体的 HUD，显示分数，并在刷新纪录时让 “NEW BEST!” 脉冲闪烁
- 游戏结束面板，带有基于分数阈值的奖牌系统（★ / ✦ / ✧ / •）
- 最高分通过 `localStorage` 持久保存
- 带有上下浮动面板的动画开始界面
- 通过 Web Audio API 实现的程序化音效（拍动、得分、撞击、死亡）——无需外部资源
- 移动的地面条纹/卵石增强速度感
- 响应式：可适配视口，支持鼠标、触控和键盘（Space/↑/Enter）
- 重试按钮 + 任意位置点击即可重启的便捷操作

只需在浏览器中打开文件——轻点/点击/按空格键即可飞行。
````

{% endcode %}

</details>

而且游戏有声音，运行得非常棒！提醒一下，这可是 1 位量化，而且效果很好！

<figure><img src="/files/66d373652b7bc6795ab4fee922b3bcc94282da3e" alt="" width="375"><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

### :triangular\_ruler:通过 KV Cache 量化实现长上下文

要在 llama.cpp 中利用长上下文，我们需要采用 KV cache 量化来减少内存占用。最近 llama.cpp 为 KV cache 量化加入了更高精度的技巧—— [见](https://github.com/ggml-org/llama.cpp/pull/21038) 以及其他 PR！

目前支持以下 KV cache 数据类型：

{% code overflow="wrap" expandable="true" %}

```
f32, f16, bf16, q8_0, q4_0, q4_1, iq4_nl, q5_0, q5_1
```

{% endcode %}

默认情况下 `f16` 会被使用。如果你使用 `q4_0` 它大约每个权重 4.5 位，你可以将上下文长度扩展到约 16 / 4.5 = **3.5 倍更长的上下文长度**！因此，如果你的模型原本支持 10K，那么 35K 也有望实现！ `q4_1` 可能更好，因为你还能获得一个位移参数，而且每个权重是 5 位——因此上下文长度可延长 3.2 倍。

可如下使用：

{% code overflow="wrap" expandable="true" %}

```bash
./llama.cpp/llama-cli \\
    --model unsloth/GLM-5.2-GGUF/UD-IQ2_M/GLM-5.2-UD-IQ2_M-00001-of-00006.gguf \\
    --temp 1.0 \\
    --top-p 0.95 \\
    --min-p 0.01 \\
    --cache-type-k q4_1 \\
    --cache-type-v q4_1
```

{% endcode %}

## 📊 基准测试

你可以在下方查看 GLM-5.2 的表格形式基准测试：

<div><figure><img src="/files/50c2128619f2e95ad4e319131bf476b1bca38331" alt=""><figcaption></figcaption></figure> <figure><img src="/files/622f79622eea2672e4dcb6b62dee842bf3cc1ca5" alt=""><figcaption></figcaption></figure></div>

| 基准                               | GLM-5.2 | Claude Opus 4.8 | GPT-5.5 | Gemini 3.1 Pro | GLM-5.1 | Qwen3.7-Max | MiniMax M3 | DeepSeek-V4-Pro |
| -------------------------------- | :-----: | :-------------: | :-----: | :------------: | :-----: | :---------: | :--------: | :-------------: |
| **推理**                           |         |                 |         |                |         |             |            |                 |
| HLE                              |   40.5  |      49.8\*     |  41.4\* |       45       |    31   |     41.4    |     37     |       37.7      |
| HLE（含工具）                         |   54.7  |      57.9\*     |  52.2\* |     51.4\*     |   52.3  |     53.5    |      -     |       48.2      |
| CritPt                           |   20.9  |       20.9      |   27.1  |      17.7      |   4.6   |     13.4    |     3.7    |       12.9      |
| AIME 2026                        |   99.2  |       95.7      |   98.3  |      98.2      |   95.3  |      97     |      -     |       94.6      |
| HMMT 2025 年 11 月                 |   94.4  |       96.5      |   96.5  |      94.8      |    94   |      95     |    84.4    |       94.4      |
| HMMT 2026 年 2 月                  |   92.5  |       96.7      |   96.7  |      87.3      |   82.6  |     97.1    |    84.4    |       95.2      |
| IMOAnswerBench                   |   91.0  |       83.5      |    -    |       81       |   83.8  |      90     |      -     |       89.8      |
| GPQA-Diamond                     |   91.2  |       93.6      |   93.6  |      94.3      |   86.2  |      90     |     93     |       90.1      |
| **编程**                           |         |                 |         |                |         |             |            |                 |
| SWE-bench Pro                    |   62.1  |       69.2      |   58.6  |      54.2      |   58.4  |     60.6    |     59     |       55.4      |
| NL2Repo                          |   48.9  |       69.7      |   50.7  |      33.4      |   42.7  |     47.2    |    42.1    |       35.5      |
| DeepSWE                          |   46.2  |        58       |    70   |       10       |    18   |      18     |     20     |        8        |
| ProgramBench                     |   63.7  |       71.9      |   70.8  |      39.5      |   50.9  |      -      |      -     |       47.8      |
| Terminal Bench 2.1（Terminus-2）   |   81.0  |        85       |    84   |       74       |   63.5  |      75     |     65     |        64       |
| Terminal Bench 2.1（最佳报告 Harness） |   82.7  |       78.9      |   83.4  |      70.7      |    69   |      -      |      -     |        -        |
| FrontierSWE（优势）                  |   74.4  |       75.1      |   72.6  |      39.6      |   30.5  |      -      |      -     |       29.0      |
| PostTrainBench                   |   34.3  |       37.2      |   28.4  |      21.6      |   20.1  |      -      |      -     |        -        |
| SWE-Marathon                     |   13.0  |       26.0      |   12.0  |       4.0      |   1.0   |      -      |      -     |        -        |
| **代理式**                          |         |                 |         |                |         |             |            |                 |
| MCP-Atlas（公开集）                   |   76.8  |       77.8      |   75.3  |      69.2      |   71.8  |     76.4    |    74.2    |       73.6      |
| 工具十项全能                           |   48.2  |       59.9      |   55.6  |      48.8      |   40.7  |      -      |      -     |       52.8      |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://unsloth.ai/docs/zh/mo-xing/glm-5.2.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
