<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>梦辰の小窝</title><description>Eagerly Anticipating Our Reunion</description><link>https://mc.mimeng.top/</link><language>zh_CN</language><item><title>Context Engineering：从 OpenAI Codex 架构看 AI Agent 的性能优化之道</title><link>https://mc.mimeng.top/posts/agent/codex-context-caching/</link><guid isPermaLink="true">https://mc.mimeng.top/posts/agent/codex-context-caching/</guid><description>基于 OpenAI Codex 的技术实践，本文探讨了 AI Agent 开发中往往被忽视的性能关键——Context Caching。通过五大工程法则，解析如何在昂贵的 KV Cache 和有限的上下文窗口间寻求平衡，实现低延迟的生产级架构。</description><pubDate>Sun, 25 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;基于 OpenAI Codex 的技术实践，本文探讨了 AI Agent 开发中往往被忽视的性能关键——Context Caching。通过五大工程法则，解析如何在昂贵的 KV Cache 和有限的上下文窗口间寻求平衡，实现低延迟的生产级架构。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;最近，我深入阅读了 OpenAI 发布的关于 Codex CLI 架构的技术文章 &lt;a href=&quot;https://openai.com/index/unrolling-the-codex-agent-loop/&quot;&gt;Unrolling the Codex agent loop&lt;/a&gt;。这篇文章虽然主要在讲 Codex Agent 的工作流，但其字里行间透露出的关于 &lt;strong&gt;Context（上下文）构建与管理&lt;/strong&gt; 的细节，让我对 AI Agent 的工程化有了全新的认识。&lt;/p&gt;
&lt;p&gt;很多人认为 Agent 开发就是写好 Prompt，但 OpenAI 告诉我们：&lt;strong&gt;Context Engineering（上下文工程）本质上是系统架构设计。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;对于生产级 Agent 而言，&lt;strong&gt;Prompt Caching（前缀缓存）&lt;/strong&gt; 是降低延迟（Latency）和成本的关键。然而，如果不理解其底层原理，我们很容易写出导致缓存失效的代码。在这篇文章中，我们将深入探讨如何通过精密的 Context 结构设计，在昂贵的计算资源（KV Cache）和有限的注意力窗口（Context Window）之间寻找平衡。&lt;/p&gt;
&lt;h2&gt;核心原理：为什么 Cache Hit 如此重要？&lt;/h2&gt;
&lt;p&gt;在深入法则之前，我们需要纠正一个常见的认知误区。&lt;/p&gt;
&lt;p&gt;Prompt Caching 的核心机制是 &lt;strong&gt;前缀匹配（Prefix Matching）&lt;/strong&gt;。缓存是按顺序存储的，一旦中间某个 Token 发生变化，其后所有内容的 KV Cache（Key-Value Cache）都会失效，必须重新计算。&lt;/p&gt;
&lt;p&gt;:::note[技术深潜：Compute Bound vs. Memory Bound]
我们常说 Cache Hit 能“省下算力”，这里的技术细节非常关键：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Cache Miss (全量计算)&lt;/strong&gt;：GPU 需要对输入的所有 Token 进行 &lt;strong&gt;Prefill（前处理）&lt;/strong&gt;。这是一个&lt;strong&gt;计算密集型 (Compute Bound)&lt;/strong&gt; 的过程，涉及大量的矩阵乘法运算。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache Hit (增量计算)&lt;/strong&gt;：如果前缀匹配，我们跳过了昂贵的 Prefill 阶段。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Attention 开销&lt;/strong&gt;：虽然跳过了 Prefill，但新生成的 Token 依然需要通过 Attention 机制“关注”所有的历史 Token。这并非零开销，但它主要变成了 &lt;strong&gt;内存带宽密集型 (Memory Bound)&lt;/strong&gt; 的操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;结论：&lt;/strong&gt; Cache Hit 主要大幅降低了 &lt;strong&gt;首字延迟 (Time to First Token, TTFT)&lt;/strong&gt;，让 Agent 的响应感觉更加敏捷。
:::&lt;/p&gt;
&lt;p&gt;基于 Codex 的实践，我总结了以下五条 Context Engineering 的 &lt;strong&gt;“黄金法则”&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;第一部分：追求极致 Cache Hit 的五大法则&lt;/h2&gt;
&lt;h3&gt;法则一：静态在前，动态在后 (The Static-First Rule)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;核心原则：&lt;/strong&gt; Context 的结构设计必须像“千层饼”一样分层。&lt;strong&gt;越是静态、不可变的内容，越要放在最前面。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;❌ 常见误区：把时间戳放在第一行&lt;/h4&gt;
&lt;p&gt;很多开发者觉得在 System Prompt 第一行告知模型当前时间是很严谨的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[System Message]
Current Time: 2026-01-25 10:00:01  &amp;lt;-- 这一秒在变，导致此行之后的所有缓存每秒都在失效！
Role: You are a helpful assistant...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning[后果]
Prompt Caching 的核心是&lt;strong&gt;前缀匹配（Prefix Matching）&lt;/strong&gt;。缓存是按顺序存储的，一旦开头的 Token 变了，其后所有内容的 KV Cache（Key-Value Cache）都会失效。&lt;/p&gt;
&lt;p&gt;这就像你去图书馆借书，虽然人没变，但每次你都换了一张不同号码的身份证。图书管理员系统无法识别你是“老用户”，必须把你当作全新用户重新录入资料，导致首字延迟（TTFT）大幅增加。
:::&lt;/p&gt;
&lt;h4&gt;✅ 优化方案：动态信息沉底&lt;/h4&gt;
&lt;p&gt;将时间戳、Request ID 等高度动态的信息，移到 Context 的&lt;strong&gt;最底部&lt;/strong&gt;，或者放在 User Message 中，让 System Prompt 的头部保持绝对静止。&lt;/p&gt;
&lt;h3&gt;法则二：追加优于修改 —— “合同”与“补充协议” (Append Over Modify)&lt;/h3&gt;
&lt;p&gt;这是最具启发性的一点。核心在于权衡：&lt;strong&gt;用少量的 Context 长度增长，换取昂贵的 Prefill 计算节省。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;场景分析&lt;/h4&gt;
&lt;p&gt;当 Agent 运行中途，环境发生变化（例如用户将权限从“只读”改为“可写”）。&lt;/p&gt;
&lt;h4&gt;❌ 错误做法：修改原始定义&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;修改前：&lt;/em&gt; &lt;code&gt;System: [Sandbox: Read-Only]&lt;/code&gt; ... (后接 5000 token 历史)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;修改后：&lt;/em&gt; &lt;code&gt;System: [Sandbox: Write-Allowed]&lt;/code&gt; ... (后接 5000 token 历史)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;技术后果：&lt;/strong&gt; 修改开头意味着破坏前缀。GPU 必须&lt;strong&gt;重新计算&lt;/strong&gt;这 5000 个 Token 的 KV 向量（Prefill 阶段），这涉及大量的矩阵乘法运算，带来显著延迟。&lt;/p&gt;
&lt;h4&gt;✅ 正确做法：追加补充协议&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;保持开头：&lt;/em&gt; &lt;code&gt;System: [Sandbox: Read-Only]&lt;/code&gt; (保持不变，骗过缓存机制)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;...中间的对话历史...&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;追加结尾：&lt;/em&gt; &lt;code&gt;System: [Update: User has approved Write Access.]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip[原理解析]
这避免了对历史 Token 进行昂贵的 &lt;strong&gt;Prefill（前处理）&lt;/strong&gt; 计算。&lt;/p&gt;
&lt;p&gt;虽然新生成的 Token 依然需要去“关注（Attention）”所有的历史信息（这依然消耗显存带宽），但我们跳过了最耗时的&lt;strong&gt;历史 Token 的特征提取与投影计算&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;收益：&lt;/strong&gt; 大幅降低 Time to First Token (TTFT)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代价：&lt;/strong&gt; Context 长度缓慢增加，直到触发窗口限制（见法则五）。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip[比喻]
就像签署商业合同，不要因为条款变更就撕毁合同重签（Cache Miss，很贵），而是在合同最后钉上一页 &lt;strong&gt;“补充协议”&lt;/strong&gt;（Cache Hit，很便宜）。模型完全有能力理解“后文覆盖前文”的逻辑。
:::&lt;/p&gt;
&lt;h3&gt;法则三：确定性是工程的底线 (Determinism is Key)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;核心原则：&lt;/strong&gt; 缓存匹配通常是 &lt;strong&gt;字节级（Byte-level）&lt;/strong&gt; 的精确匹配。对于人类来说语义相同的列表，如果顺序不同，对于缓存机制来说就是完全不同的序列。&lt;/p&gt;
&lt;h4&gt;❌ 错误案例：随机的工具列表&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;请求 A: &lt;code&gt;Tools: [Shell, Search]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;请求 B: &lt;code&gt;Tools: [Search, Shell]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这会导致莫名其妙的 Cache Miss。&lt;/p&gt;
&lt;h4&gt;✅ 优化方案：强制确定性排序&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;工具列表排序：&lt;/strong&gt; 始终按字母顺序（A-Z）排列工具定义。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSON 序列化：&lt;/strong&gt; 确保 JSON 对象在转为字符串时，Key 的顺序是固定的（许多语言的 Map 是无序的，需要专门处理）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;法则四：将不确定性隔离与“打补丁” (Isolation &amp;amp; Patching)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;核心原则：&lt;/strong&gt; 将不稳定的外部定义隔离，对环境状态的变更采用“打补丁”策略。&lt;/p&gt;
&lt;h4&gt;场景 A：外部工具 (Isolation)&lt;/h4&gt;
&lt;p&gt;Codex 将工具分为“内置工具”（如 Shell，定义稳定）和“外部工具”（如 MCP 协议插件）。如果引入了外部工具，其 Schema 可能随时变化。&lt;br /&gt;
&lt;strong&gt;策略：&lt;/strong&gt; 将这些不稳定的内容与核心 System Prompt 隔离。一旦外部配置变更，尽量通过追加说明而非重构上下文来处理。&lt;/p&gt;
&lt;h4&gt;场景 B：环境变更 (Patching)&lt;/h4&gt;
&lt;p&gt;用户执行了 &lt;code&gt;cd /var/www&lt;/code&gt;，导致当前工作目录（CWD）改变。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;❌ 错误：&lt;/strong&gt; 回去修改 System Prompt 中的 &lt;code&gt;&amp;lt;cwd&amp;gt;&lt;/code&gt; 标签。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;✅ 正确：&lt;/strong&gt; 在对话流中插入一条事件消息：&lt;code&gt;User executed cd. CWD is now /var/www&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note[工程权衡 Trade-off]
这种“打补丁”的方式依赖于模型能够理解“后文覆盖前文”的逻辑。现代强模型通常能处理得很好，但对于极长上下文，模型可能会出现“遗忘”或“幻觉”。这是一种为了性能而在准确性边缘试探的策略。
:::&lt;/p&gt;
&lt;h3&gt;法则五：正视窗口限制与压缩 (The Compaction Reality)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;核心原则：&lt;/strong&gt; “追加优于修改”会导致 Context 像滚雪球一样变大，必须在达到阈值（Context Window）前进行智能压缩。&lt;/p&gt;
&lt;p&gt;当 Token 数量达到阈值（如 &lt;code&gt;auto_compact_limit&lt;/code&gt;）时，必须进行&lt;strong&gt;压缩（Compaction）&lt;/strong&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;非简单截断：&lt;/strong&gt; 不能只保留最后几句，否则会丢失任务目标。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隐式摘要与重置：&lt;/strong&gt; 高级的做法是调用压缩端点，将长历史压缩为摘要。
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;进阶技巧：&lt;/em&gt; 这个摘要不仅包含文字，甚至可以包含 &lt;strong&gt;隐状态（Latent State）&lt;/strong&gt; 或 &lt;code&gt;encrypted_content&lt;/code&gt;，以保留模型对任务的“潜意识”理解。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接受代价：&lt;/strong&gt; 压缩发生的瞬间，Cache 会被迫刷新（Miss），但这是为了防止 Context 溢出并维持长期运行所必须支付的代价。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第二部分：实战思考 —— 从“日志折叠”到“价值过滤”&lt;/h2&gt;
&lt;p&gt;理解了上述法则后，我们来解决一个 Agent 开发中极度棘手的真实场景。&lt;/p&gt;
&lt;h3&gt;场景描述：复杂的 Read-Edit Loop&lt;/h3&gt;
&lt;p&gt;在处理大型代码库时，Agent 的行为往往伴随着大量的试错和分块读取：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;探索：&lt;/strong&gt; Agent 试图修复一个 Bug，先读取了 &lt;code&gt;main.py&lt;/code&gt; 的 1-100 行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深入：&lt;/strong&gt; 没找到，又读取了 101-300 行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发现：&lt;/strong&gt; 最终发现只有 &lt;strong&gt;251-300 行&lt;/strong&gt; 是相关的核心逻辑，而前面的 1-250 行全是无关的配置代码。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此时，Context 中堆积了数千 Token 的代码片段。&lt;/p&gt;
&lt;h3&gt;演进思考：如何优雅地“压缩”？&lt;/h3&gt;
&lt;p&gt;根据 &lt;strong&gt;法则五（正视窗口限制）&lt;/strong&gt;，我们直觉的解决方案是：&lt;strong&gt;定期压缩 Context，将繁杂的 Tool Log 替换为摘要。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;第一阶段：朴素的摘要机制 (The Naive Approach)&lt;/h4&gt;
&lt;p&gt;我们可以设计一个机制，将上述操作总结为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Read &lt;code&gt;main.py&lt;/code&gt; from line 1 to 300.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;并提供一个 &lt;code&gt;expand&lt;/code&gt; 工具，允许模型在需要时查看详情。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但在实战中，我发现这个方案有一个致命缺陷：&lt;/strong&gt;
当 Agent 后续需要“展开”这段记忆时，系统会机械地把 1-300 行代码&lt;strong&gt;全部&lt;/strong&gt;塞回 Context。这意味着，我们刚刚费力扔出去的 &lt;strong&gt;1-250 行噪音（Noise）&lt;/strong&gt;，又被模型自己捡回来了。这不仅浪费 Token，更会用无关代码干扰模型的注意力。&lt;/p&gt;
&lt;h4&gt;第二阶段：基于“价值过滤”的高精度折叠 (Value Filtering)&lt;/h4&gt;
&lt;p&gt;为了解决这个问题，我意识到：&lt;strong&gt;压缩不仅仅是变短，更是一次信息清洗。&lt;/strong&gt;
我们需要在生成 Summary 的阶段，就将“沙子”和“金子”分离开。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优化后的 Context 结构：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当子任务完成时，我们要求 LLM 总结操作，并明确区分 &lt;strong&gt;“已读但无用（Discarded）”&lt;/strong&gt; 和 &lt;strong&gt;“已读且有用（Retained）”&lt;/strong&gt; 的部分。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[System Event: Context Compressed]
- Processed `main.py` (Lines 1-300):
  - Lines 1-250: Discarded (Irrelevant boilerplate).
  - Lines 251-300: **Retained**. Contains `init_db_connection` logic. (ID: ref_m1_valid)
**NOTE**
Context is compressed. Use `expand_context(ids=[&apos;ref_m1_valid&apos;])` to view ONLY the relevant code snippets.
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;设计亮点：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;高召回率 (High Recall)：&lt;/strong&gt; 明确记录 1-250 行“被丢弃了”。这告诉模型“那部分我看过了，没用”，防止模型因不知道读过而重新去读。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高精确度 (High Precision)：&lt;/strong&gt; &lt;code&gt;ID: ref_m1_valid&lt;/code&gt; 并不指向 1-300 行的原始记录，而是指向 &lt;strong&gt;经过裁剪的 251-300 行切片&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;配套工具设计&lt;/h3&gt;
&lt;p&gt;为了支撑这个机制，我们需要配套的工具定义。&lt;/p&gt;
&lt;h4&gt;1. 批量懒加载工具 (Batch Expansion Tool)&lt;/h4&gt;
&lt;p&gt;为了减少交互轮次，工具应当支持批量操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  name: &quot;expand_context&quot;,
  description: &quot;Expand the RELEVANT details of compressed logs.&quot;,
  parameters: {
    ids: {
      type: &quot;array&quot;,
      items: { type: &quot;string&quot; },
      description: &quot;List of IDs to expand. Note: This will only expand the useful code snippets retained during compression.&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2. 展开策略：只返回“黄金切片” (应用法则二与四)&lt;/h4&gt;
&lt;p&gt;当模型调用 &lt;code&gt;expand_context(ids=[&quot;ref_m1_valid&quot;])&lt;/code&gt; 时，系统只会返回之前被标记为“有用”的那 50 行代码。&lt;/p&gt;
&lt;p&gt;且为了遵守 &lt;strong&gt;法则二（追加优于修改）&lt;/strong&gt;，我们不修改历史摘要，而是将内容追加到对话末尾：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;... (前面是压缩后的摘要，作为静态地基，保持不动) ...

[Current Turn]
User: I need to fix the db connection bug found in main.py.
Agent: Call tool `expand_context(ids=[&quot;ref_m1_valid&quot;])`
System: [Tool Output] Expanding relevant snippets:
1. `main.py` (ref_m1_valid, lines 251-300): 
   &quot;def init_db_connection():
      # ... Only the 50 relevant lines are shown here ...
   &quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过这种方式，我们既保留了完整的&lt;strong&gt;探索路径&lt;/strong&gt;（我知道我读过哪），又实现了&lt;strong&gt;上下文的极致精简&lt;/strong&gt;（我只展开有用的）。&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;Context Engineering 不仅仅是关于 Prompt 怎么写，更是一种 &lt;strong&gt;数据结构的设计哲学&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;通过 Codex 的文章和这个实战案例，我们可以看到：优秀的 Agent 架构，应该像数据库索引一样：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;利用 &lt;strong&gt;静态前缀&lt;/strong&gt; 最大化缓存命中。&lt;/li&gt;
&lt;li&gt;利用 &lt;strong&gt;追加更新&lt;/strong&gt; 最小化重算开销。&lt;/li&gt;
&lt;li&gt;利用 &lt;strong&gt;智能压缩&lt;/strong&gt; 在有限的窗口内保留高价值信息（如读取进度）。&lt;/li&gt;
&lt;li&gt;利用 &lt;strong&gt;懒加载&lt;/strong&gt; 机制按需获取细节。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;只有理解了这些底层的算力逻辑，你的 Agent 才能在生产环境中跑得既快又稳。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Reference:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://openai.com/index/unrolling-the-codex-agent-loop/&quot;&gt;Unrolling the Codex agent loop | OpenAI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>AI Native ：重塑从 0 到 1 的生产力法则与实战复盘</title><link>https://mc.mimeng.top/posts/agent/ai-native/</link><guid isPermaLink="true">https://mc.mimeng.top/posts/agent/ai-native/</guid><description>拒绝营销词汇，回归工程本质。本文复盘了我如何利用 OpenCode、OpenSpec 和 SPEC Driven 理念，构建（半）全自动化流水线 Vibe Coding 的全过程。</description><pubDate>Wed, 21 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;拒绝营销词汇，回归工程本质。本文复盘了我如何利用 OpenCode、OpenSpec 和 SPEC Driven 理念，构建（半）全自动化流水线 Vibe Coding 的全过程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. 重新定义 AI Native：极致的“去人工化”&lt;/h2&gt;
&lt;p&gt;在我看来，所谓的 &lt;strong&gt;AI Native（AI 原生）&lt;/strong&gt; 不应仅仅是一个被滥用的营销词汇，它应当是一条铁律，一种近乎偏执的工程原则：&lt;strong&gt;凡是能用 AI 解决的环节，绝不消耗哪怕一分钟的人力。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;传统的软件工程是“人主导，工具辅助”，而我所实践的 AI Native，是将人类从“执行者”晋升为“决策者”和“审视者”。在这个新的范式里，人的核心价值不再是敲击键盘的速度，而是判断力与想象力。&lt;/p&gt;
&lt;p&gt;:::important[核心观念转变]
在 AI Native 的工作流中，开发者不再是 &lt;strong&gt;Coder&lt;/strong&gt;，而是 &lt;strong&gt;Architect&lt;/strong&gt; 与 &lt;strong&gt;Reviewer&lt;/strong&gt; 的结合体。你的代码产出量不应取决于你的手速，而应取决于你描述问题的清晰度。
:::&lt;/p&gt;
&lt;h2&gt;2. 全生命周期的“AI 饱和式”参与&lt;/h2&gt;
&lt;p&gt;从一个模糊的念头（Idea）诞生，到最终产品上线运营（DevOps &amp;amp; Marketing），我要求 AI 必须高度参与，甚至主导每一个环节。这不仅仅是效率的提升，更是生产流程的重构。&lt;/p&gt;
&lt;p&gt;为了践行这一理念，我以最近正在筹备的项目 &lt;strong&gt;Contextfy&lt;/strong&gt;（一个针对垂直领域的 CodeAgent 工具）为例，复盘我如何构建这套以 AI 为核心的流水线。&lt;/p&gt;
&lt;p&gt;:::note[📢 资源分享：从 0.5 开始的 AI Agent 学习之旅]
最近在清华学习，打算边学习边总结过去半年做 AI Agent 开发的实践经验，于是开了一份文档记录这个过程，想分享给大家。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;📝 这份文档是什么？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;非入门教程&lt;/strong&gt;：不是从 0 到 1 的入门教程，而是实战经验与工程实践的总结。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contextfy 实战&lt;/strong&gt;：围绕本文提到的实战项目 &lt;strong&gt;Contextfy&lt;/strong&gt; 展开，解决 AI Coding 在私有框架/遗留系统中“最后一公里”的问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live 开发日志&lt;/strong&gt;：从全栈视角出发，每天多次更新，实时记录技术决策、踩坑现场与迭代思路。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;⚠️ 适合人群&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;已经有一定 AI Agent 基础，想看看实践经验的朋友。&lt;/li&gt;
&lt;li&gt;对 Vibe Coding 感兴趣，或已经在用 AI Coding 但想进一步提升能力的开发者。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;不适合 0 基础（文档里有推荐的系统学习资源）。&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你也想要一起编辑、补充内容，可以私信我邀请你成为协作者，欢迎大家一起交流探讨！
👉 &lt;strong&gt;&lt;a href=&quot;https://www.notion.so/0-5-AI-Agent-2eceda5b79ad806c8e88e313b5cae2e7?source=copy_link&quot;&gt;点击查看 Notion 文档&lt;/a&gt;&lt;/strong&gt;
:::&lt;/p&gt;
&lt;h2&gt;3. 实战复盘：我的项目开发工作流&lt;/h2&gt;
&lt;h3&gt;Phase 1: 立项与调研 —— 一个人活成一支队伍&lt;/h3&gt;
&lt;p&gt;如果你身处一个正常的研发团队，立项、竞品调研、PRD 撰写通常是产品经理（PM）的职责。但作为一个独立开发者（Indie Hacker），借助 AI，我们可以快速补齐这块短板。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Contextfy&lt;/strong&gt; 的诞生源于我在开发 UGC 游戏内容时的痛点：市面上缺乏针对特定垂直领域的 CodeAgent。在立项阶段，我没有急着写代码，而是构建了一个“共生”的决策流：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Idea 具象化&lt;/strong&gt;：我负责提供核心价值观和灵感。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 市场调研&lt;/strong&gt;：我要求 AI 搜索并分析市面上现有的产品，列出优劣势对比。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;功能定型&lt;/strong&gt;：通过多轮对话，将模糊的想法打磨成严谨的 PRD（产品需求文档）和 MVP 规划。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::tip[Prompt Trick: 逆向思维]
在做竞品分析时，不要只问“有哪些产品”，尝试让 AI 扮演“挑剔的资深用户”，列出它在使用现有产品时遇到的 10 个最无法忍受的痛点。这些痛点往往就是你产品的&lt;strong&gt;核心机会点（Unique Selling Point）&lt;/strong&gt;。
:::&lt;/p&gt;
&lt;h3&gt;Phase 2: 开发 —— SPEC Driven 的降维打击&lt;/h3&gt;
&lt;p&gt;进入编码环节，我的核心理念是 &lt;strong&gt;SPEC Driven（文档驱动开发）&lt;/strong&gt;。我不直接写代码，而是编写高精度的技术规范（Spec），让 AI 严格遵循规范产出代码。&lt;/p&gt;
&lt;h4&gt;2.1 工具链选择：丰俭由人，适合即最优&lt;/h4&gt;
&lt;p&gt;工欲善其事，必先利其器。市面上的 &lt;strong&gt;CodeAgent&lt;/strong&gt; 和 &lt;strong&gt;AI IDE&lt;/strong&gt; 琳琅满目，选择哪一款完全取决于你的预算、使用习惯以及对“控制力”的需求。&lt;/p&gt;
&lt;p&gt;我个人习惯使用 &lt;strong&gt;OpenCode&lt;/strong&gt; 配合 &lt;strong&gt;OpenSpec&lt;/strong&gt;，因为我喜欢 CLI 的纯粹和高度可定制性。但你完全可以根据自己的情况选择合适的 &lt;code&gt;Agent Runtime&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OpenCode&lt;/strong&gt;: 一个强大的 CLI Agent Runtime，虽然不如商业软件出名，但它的可定制性极强。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenSpec&lt;/strong&gt;: 一个专门的规范驱动开发工具，用于定义和验证 AI 生成的代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::note[工具说明]
&lt;strong&gt;你不必非要使用 OpenCode。&lt;/strong&gt;
对于大多数开发者来说，&lt;a href=&quot;https://code.claude.com/docs/zh-CN/overview&quot;&gt;Claude Code&lt;/a&gt; 、&lt;a href=&quot;https://cursor.com/cn&quot;&gt;Cursor&lt;/a&gt; 或 &lt;a href=&quot;https://antigravity.google/&quot;&gt;Antigravity&lt;/a&gt; 是更容易上手的选择。如果你使用的是 &lt;a href=&quot;https://kiro.dev/&quot;&gt;Kiro&lt;/a&gt;，那么恭喜你，Kiro 本身就是 Spec Driven 理念的集大成者，它内置的 &lt;code&gt;requirements.md&lt;/code&gt; / &lt;code&gt;design.md&lt;/code&gt; 逻辑与我下面描述的流程异曲同工。
:::&lt;/p&gt;
&lt;p&gt;:::note[名词解释: Agent Runtime]
你可以把 Agent Runtime 理解为 AI 的“操作系统”。它负责管理上下文（Memory）、调用工具（Tools）、执行文件操作（File I/O）。无论是 OpenCode 还是 Cursor，本质上都是在充当这个角色。
:::&lt;/p&gt;
&lt;p&gt;:::warning[成本陷阱]
&lt;strong&gt;请量力而行！&lt;/strong&gt; Spec Driven 开发模式会消耗大量的 Context Token。如果你使用的是 Claude 4.5 Sonnet/Ops 、GPT-5.2-Codex 或 Gemini 3 Pro 这样的顶级模型，一个复杂的重构任务可能会消耗几美元。对于个人开发者，建议搭配 GLM-4.7、MiniMax 2.1 以及 DeepSeek 等高性价比模型，或只在关键架构设计时使用顶级模型。
:::&lt;/p&gt;
&lt;h4&gt;2.2 架构与任务拆解 (The Strategic Split)&lt;/h4&gt;
&lt;p&gt;项目初始化时，我会先将立项文档放入文档目录（注意：OpenSpec 通常在 &lt;code&gt;openspec/&lt;/code&gt;，Kiro 在 &lt;code&gt;.kiro/&lt;/code&gt;，请根据你的工具自行判断）。&lt;/p&gt;
&lt;p&gt;这里有一个关键的策略调整：虽然 &lt;strong&gt;OpenSpec&lt;/strong&gt; 能够全自动生成详细 Design 和 Tasks，但在项目初期，它往往显得“太重”且昂贵（Context 消耗巨大），或者因细节不足导致代码不确定。
因此，我的做法是 &lt;strong&gt;“分而治之”&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;全局视野 (Global Context)&lt;/strong&gt;：使用 &lt;strong&gt;Gemini 3 Pro&lt;/strong&gt;（或其他超长上下文模型）阅读全局文档，输出一份全局的 &lt;code&gt;Architecture.md&lt;/code&gt; 和合理的 &lt;code&gt;Tasks.md&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;局部执行 (Local Execution)&lt;/strong&gt;：在全局规划定型后，再让 OpenSpec 介入具体开发。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2.3 The Loop: Proposal &amp;amp; Apply&lt;/h4&gt;
&lt;p&gt;进入具体开发后，我的工作流遵循严格的循环：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;初始化&lt;/strong&gt;：&lt;code&gt;/openspec proposal&lt;/code&gt; 初始化项目结构，仅实现最基础的 Hello World。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;循环迭代&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;根据 &lt;code&gt;Tasks.md&lt;/code&gt;，针对&lt;strong&gt;单个功能模块&lt;/strong&gt;执行 &lt;code&gt;/openspec proposal&lt;/code&gt; 生成局部技术方案。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review&lt;/strong&gt;: 人类介入，审查 Proposal 是否符合架构设计。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Execute&lt;/strong&gt;: 执行 &lt;code&gt;/openspec apply&lt;/code&gt; 让 AI 编写代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Loop&lt;/strong&gt;: 不断测试、修补，直到功能跑通。后续需求变更，也是通过添加新的 Proposal 来实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::important[Review 的重要性]
永远不要跳过 &lt;strong&gt;Review Proposal&lt;/strong&gt; 的环节。这是人类防止 AI 产生“幻觉架构”的最后一道防线。一旦 Apply 了错误的代码，回滚的成本远比阅读文档高。
:::&lt;/p&gt;
&lt;h3&gt;Phase 3: 前后端对接 —— 上帝视角的 Monorepo&lt;/h3&gt;
&lt;p&gt;传统的开发模式是：后端写文档 -&amp;gt; 丢给前端 -&amp;gt; 前端对着文档一顿猜 -&amp;gt; 联调撕逼。&lt;/p&gt;
&lt;p&gt;在我的工作流中，我通常采用 Monorepo 策略，将前后端仓库放在同一个 Workspace 中。AI 拥有上帝视角，它能直接阅读后端的 Controller、DTO 代码，自动生成前端的 API Client 和类型定义。
&lt;strong&gt;没有沟通成本，因为“沟通”在 Context 内部已经完成了。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Phase 4: 测试与 Debug —— 拒绝手动劳动&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mock 数据&lt;/strong&gt;：对于前端，我让 AI 根据 TypeScript 接口定义直接生成海量 Mock 数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;E2E 测试&lt;/strong&gt;：如果你使用的是 &lt;a href=&quot;https://antigravity.google/&quot;&gt;Antigravity&lt;/a&gt;，你甚至可以直接编排一个 Workflow，让 AI Agent 自动完成全链路的端到端测试（这是 Antigravity 最强的地方）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后端测试&lt;/strong&gt;：让 AI 针对每个 Service 方法编写单元测试或者集成测试，并自动生成 API 文档。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::caution[警惕测试覆盖率]
虽然 AI 能秒生成 100% 覆盖率的单测，但&lt;strong&gt;覆盖率不等于正确性&lt;/strong&gt;。AI 很擅长写“只会通过的测试”（Trivial Tests）。你需要抽查测试逻辑，或者让 AI 故意生成极端的测试案例，确保它真的测到了边界情况（Edge Cases）。
:::&lt;/p&gt;
&lt;h3&gt;Phase 5: CR 与 CI/CD —— 自动化守门员&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Code Review (CR)&lt;/strong&gt;：CR 往往能预先修复很多潜在问题。我建议配置 GitHub Action 接入 OpenCode（当然市面上也有很多其他做 CR 的产品，比如 Github Copilot） 或使用 Cursor 等 AI IDE 自带的 CR 功能，在 Commit 之前就拦截低级错误。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/CD&lt;/strong&gt;：这年头谁还自己写 YAML 脚本？直接告诉 AI 你的部署环境（Vercel, Docker, K8s），让它生成完整的 Pipeline。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Phase 6: 文档工程 —— 知识的沉淀&lt;/h3&gt;
&lt;p&gt;如果你按照 Spec Driven 开发，当你执行 &lt;code&gt;/openspec archive&lt;/code&gt; 时，确实已经产生了一堆 Spec 文档。&lt;strong&gt;但请注意，这些文档并不适合人类阅读。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;:::caution[Spec 文档的局限性]
OpenSpec（或 Kiro）产出的文档通常是碎片化的（Fragmented）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;过于分散&lt;/strong&gt;：它们分散在各个 proposal 归档中，缺乏系统性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内容贫瘠&lt;/strong&gt;：往往只有功能描述、粗略架构和 Tasks，缺乏具体的 API 使用指南或最佳实践。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;难以聚合&lt;/strong&gt;：一个组件可能经历了多次 Proposal 迭代，你很难快速拼接出它的完整全貌。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;我的选择：Nextra (DIY)&lt;/h4&gt;
&lt;p&gt;我选择 &lt;strong&gt;&lt;a href=&quot;https://nextra.site/&quot;&gt;Nextra&lt;/a&gt;&lt;/strong&gt; (基于 Next.js 的 MDX 框架) 作为载体，原因有三：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;速度与权限&lt;/strong&gt;：Next.js 的服务端渲染让加载极快。更重要的是，我可以自己写 Auth 逻辑（接入飞书、Github Org 登录），确保团队内部文档的安全性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MDX 的魔力&lt;/strong&gt;：这对前端文档至关重要。文档不应只是静态文本，我可以在 MDX 中直接嵌入 React 组件（例如一个活的 API 调试台，或者组件的实时预览）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 友好&lt;/strong&gt;：MDX 结构清晰，非常适合作为一个标准格式，让 AI 一键 Copy 并作为后续任务的 Context。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;其他选择 (SaaS &amp;amp; AI Knowledge Base)&lt;/h4&gt;
&lt;p&gt;当然，这只是我的个人偏好。如果你的团队不想维护文档站代码，市面上还有很多优秀的方案：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Mintlify / GitBook&lt;/strong&gt;: 开箱即用的文档 SaaS，界面美观，适合对外发布。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI Native 知识库&lt;/strong&gt;: 像 &lt;strong&gt;Greptile&lt;/strong&gt; 或 &lt;strong&gt;Mem0&lt;/strong&gt; 这样的产品，它们不只是静态文档，而是能直接理解你的代码库，并提供类似 Chat 的查询接口。这可能是未来团队文档的终极形态。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip[文档即代码 (Docs as Code)]
无论你选择什么方案，尽量确保文档是&lt;strong&gt;随着代码一起提交的&lt;/strong&gt;。只有与代码同源，才能最大程度避免“文档过期”的诅咒。
:::&lt;/p&gt;
&lt;h3&gt;Phase 7: 上线与 MCP&lt;/h3&gt;
&lt;p&gt;最后，本着“能不动手就不动手”的原则，上线过程同样交给 AI。&lt;/p&gt;
&lt;p&gt;如果部署平台支持 &lt;strong&gt;&lt;a href=&quot;https://modelcontextprotocol.io/&quot;&gt;MCP (Model Context Protocol)&lt;/a&gt;&lt;/strong&gt;，我甚至会直接配置好 MCP Server，让 AI 自行调用工具完成部署、域名解析等操作。MCP 正在成为连接 AI 与基础设施的标准协议，掌握它，你的 Agent 就能获得“物理世界”的操作能力。&lt;/p&gt;
&lt;h2&gt;结语：只有掌控过程，才能驾驭结果&lt;/h2&gt;
&lt;p&gt;很多人担心 AI 写代码容易“失控”，或者 AI 做产品容易“幻觉”。这并非 AI 的能力问题，而是&lt;strong&gt;协作模式&lt;/strong&gt;的问题。&lt;/p&gt;
&lt;p&gt;通过 &lt;strong&gt;SPEC Driven&lt;/strong&gt; 约束输出，通过 &lt;strong&gt;Global Context&lt;/strong&gt; 进行全局规划，通过 &lt;strong&gt;Nextra&lt;/strong&gt; 沉淀知识。我们不再是代码的搬运工，我们是这套数字化流水线的指挥官。&lt;/p&gt;
&lt;p&gt;在 AI 浪潮下，&lt;strong&gt;生产力的上限，取决于你对工具链整合的想象力。&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>2025 年度总结：破茧、AI 浪潮与新的羁绊</title><link>https://mc.mimeng.top/posts/general/2025-annual-summary/</link><guid isPermaLink="true">https://mc.mimeng.top/posts/general/2025-annual-summary/</guid><description>回顾魔幻的 2025 年，从竞赛卷王到 AI Agent 开发者的蜕变，经历了至暗时刻，也拥抱了新的生活。</description><pubDate>Wed, 31 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;坐在 2025 年的尾巴上，回看这一年，感觉真的有点魔幻。如果说 2024 是迷茫中摸索，那 2025 绝对是疯狂加速、跌宕起伏的一年。这一年，我从一个还在为期末考发愁的大一新生，变成了一个能熟练“奴役”AI、手搓复杂架构的开发者；经历过人生目前为止最黑暗的低谷，也幸运地抓住了新的光亮。&lt;/p&gt;
&lt;p&gt;话不多说，还是按时间线来捋一捋这充实得过分的一年吧。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://d988089.webp.li/2025/12/31/20251231190110881.avif&quot; alt=&quot;新年新气象&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;第一季度：蓄力与起步&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1 月：期末大逃杀&lt;/strong&gt;
大一上学期的期末周，真的可以用“惨烈”来形容。平时基本没怎么学，最后关头极限冲刺。那时候每天都在怀疑人生，好在最后运气不错，不仅没挂科，绩点居然还混到了前 20%。算是惊险过关，给了我一个还算安稳的寒假。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2 月：技术栈储备&lt;/strong&gt;
寒假没闲着，因为社团有个项目要开搞。为了这个，我提前开始啃技术栈。1 月底就开始看 Tauri，然后为了搞懂大型 React 应用的架构，硬着头皮啃了好几个开源的 React Admin 项目源码。现在回看，那段时间虽然痛苦，但确实让我对 React SPA 的基本架构有了更深入的见解，不再是只会写写组件了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://d988089.webp.li/2025/12/31/20251231185347373.avif&quot; alt=&quot;展示设备&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3 月：Vue 插曲与项目启动&lt;/strong&gt;
为了准备蓝桥杯，我花时间突击了一下 Vue3。顺手把学的笔记整理了一下，写了个实战文档：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;../frontend/vue3-mimeng-store&quot;&gt;Vue 3 实战 - 迷梦甄选&lt;/a&gt;
&lt;em&gt;这是一个 Vue3 企业级实战项目笔记，涵盖了大部分实际开发中可能会使用到的库。&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;第二季度：竞赛、爆肝与高光时刻&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;3月 - 5月：实验室闭关与「聪言」诞生&lt;/strong&gt;
这几个月基本就是在实验室住下了。我们开发了一个叫「聪言」的项目（Github: &lt;code&gt;TatsukiMengChen/CongYan&lt;/code&gt;）。这个项目对我前端能力的提升简直是决定性的，让我真正具备了从 0 到 1 设计并实现一个架构合理的 React 项目的能力。&lt;/p&gt;
&lt;p&gt;其实这个项目去年底就开始了，起初用的是 React Native。但到了 4 月，我实在受不了 RN 的开发体验了（懂得都懂，HMR 不好用，各种奇奇怪怪的问题），毅然决然迁移到了 Tauri。事实证明这个决定无比正确，Web 开发体验真香，而且能同时顾及 Android 端，少了很多折磨。&lt;/p&gt;
&lt;p&gt;这段时间为了赚点外快和积累经验，我还顺手做了好几个项目，什么发卡网、管理面板，还玩了玩 Live2D 做 AI Chat，每天都在高强度输出。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;竞赛成果&lt;/strong&gt;
4 月抽空去打了蓝桥杯 Web 组，毫不意外拿了省一。可惜后面比赛太多实在抽不出时间去国赛，不然感觉国一也是有机会冲一下的。&lt;/p&gt;
&lt;p&gt;5 月参加了携程的前端训练营，体验极佳。得到了技术老师的特别表扬，技术面也过了，HR 都给了口头 Offer。虽然最后因为一些个人规划原因没去实习，但这段经历对我来说是个很大的肯定。&lt;/p&gt;
&lt;p&gt;那几个月是真的忙，但也真的拿了不少奖，这里简单晒一下「聪言」的战绩：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2025年（第18届）中国大学生计算机设计大赛江西省级赛&lt;/strong&gt;：二等奖（5 月）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2025中国高校计算机大赛-AIGC创新赛华中赛区&lt;/strong&gt;：二等奖（5 月初赛，7 月复赛）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2025年（第二十届）海峡两岸暨港澳地区大学生计算机创新作品赛江西省赛&lt;/strong&gt;：一等奖（5 月）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第二十七届中国机器人及人工智能大赛&lt;/strong&gt;：人工智能创新赛全国二等奖（5 月省二，8 月国二）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://d988089.webp.li/2025/12/31/20251231185400769.avif&quot; alt=&quot;回报&quot; /&gt;&lt;/p&gt;
&lt;p&gt;5、6 月份还顺手把学校心愿单项目的后端给搓出来了。那段时间感觉自己就是个无情的代码机器。&lt;/p&gt;
&lt;h2&gt;第三季度：转折点，拥抱 AI&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;7 月：AIGC 的启蒙&lt;/strong&gt;
为了打 AIGC 创新赛，我在「聪言」里加了很多 AIGC 的内容。这是我首次接触 Agent 开发，算是一个重要的转折点。从这以后，我学的很多东西都跟 Agent 脱不开关系了。&lt;/p&gt;
&lt;p&gt;月底把博客从 Halo 迁移到了 Astro，换上了现在这个清爽的 Fuwari 主题，看着舒服多了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;8 月：命运的齿轮开始转动&lt;/strong&gt;
这个月尝试迭代自己的迷梦工坊项目，虽然半途而废了，但也积累了不少 React 经验。还尝试了第一次开源贡献。&lt;/p&gt;
&lt;p&gt;最重要的是，我开始了人生第一次实习求职。面了几家 AI 方向的小厂和初创，手里拿了 3 个 Offer：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;北京某做 AIGC 出海的（类似星野 APP）：收入可观，架构稳定，但我感觉进去也就是写写页面，提升不大。&lt;/li&gt;
&lt;li&gt;深圳某 AI 应用小厂：要写 RN，我实在不想在 RN 这一块做太多纠缠（还是觉得不好用）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;清华系初创公司（做 Agentic AI）&lt;/strong&gt;：Boss 人很好，直面初创的不足；虽然团队大部分是清北 C9 的大佬，要去线下，但允许我远程（这对我这个课多的大二学生太重要了）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后选了清华的初创。理由很简单：方向是 Agentic AI，我觉得是未来。&lt;/p&gt;
&lt;p&gt;于是，我咬牙买了台 MacBook Air M4 当主力开发机，我的老战友 &lt;strong&gt;机械革命 翼龙 15 Pro&lt;/strong&gt; 正式退役成纯游戏本了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://d988089.webp.li/2025/12/31/20251231185413841.avif&quot; alt=&quot;展示工位&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实习带来的技术爆炸&lt;/strong&gt;
从 8 月中旬到现在，我一直在这家公司实习。这段经历真的让我脱胎换骨。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;前端深度&lt;/strong&gt;：不再局限于框架使用，开始深入移动端适配、性能优化、工程化、自动化、微前端、容器化。为了搞定复杂需求，甚至钻研了 ShadowDOM、Proxy、React 底层机制和 AST 抽象语法树这些以前看着就头大的东西。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后端与架构&lt;/strong&gt;：对微服务有了更深的理解，能设计更复杂的前后端架构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI 能力（核心）&lt;/strong&gt;：这是我提升最大的地方。我现在的开发工作流已经完全 AI 化了，效率+++++。我能熟练驾驭多个 AI 协同开发，深入理解了像 ClaudeCode 这样的架构，学了 LangChain、LangGraph，玩转各种 Agentic AI 工具。我现在甚至能自己手搓 CodeAgent，设计那种 AI 生成应用给前端用，后端还要管理静态资源、打包优化、准备沙盒环境的复杂系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;即便公司的项目没有太大的起色，但我积累的经验是实打实的。从一开始在一群清北 C9 同事面前的学历自卑，到后来慢慢建立自信，我开始坚信：&lt;strong&gt;能力比学历重要&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://d988089.webp.li/2025/12/31/20251231185459402.avif&quot; alt=&quot;美图&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;第四季度：至暗时刻与新的羁绊&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;9 月：人生最黑暗的时光&lt;/strong&gt;
这一年并不只有光鲜亮丽。9 月初，我经历了人生中最黑暗的一段日子。&lt;/p&gt;
&lt;p&gt;因为一些莫须有的造谣和误解，我仿佛一夜之间被世界隔离。周围的人几乎都避开我，和前任分手，迫于舆论压力退出了心爱的社团。那段时间，每天都吃不下饭，世界是灰暗的。我很害怕，不知道明天该怎么过，甚至怀疑有没有明天...&lt;/p&gt;
&lt;p&gt;好在，后来真相大白，成功辟谣。在好兄弟的支持、学姐的安慰和理解下，我慢慢找回了自信，重新开始生活，回到了追求技术的正轨。真的非常感谢那段时间没有放弃我的人。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;10 月：重新连接世界&lt;/strong&gt;
9 月的大事之后，我几乎断绝了所有社交。直到 10 月中旬，很幸运遇到了一个新的玩伴。她带我重新找回了游戏的乐趣。我们每天一起玩原神、MC、双影奇境、泰拉瑞亚、Pummel Party…… 感谢她，让我那个自闭的角落重新照进了阳光。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://d988089.webp.li/2025/12/31/20251231185508406.avif&quot; alt=&quot;Genshin Impact&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;11 月 - 12 月：遇见 Momoka&lt;/strong&gt;
11 月，我有幸遇到了 Momoka。她接纳了全部的我。我们在一起了。&lt;/p&gt;
&lt;p&gt;虽然是异地，但我们经常挂着电话，一起打原神，玩恐怖游戏（又菜又爱玩），分享彼此的日常。她身体不太好，而且是个超级无敌大社恐（我也在努力帮她脱敏哈哈）。&lt;/p&gt;
&lt;p&gt;她其实是我 11 月之后的快乐源泉。即便实习上班有时候真的很累很搞心态，但只要想到有她在，好像一切都没那么辛苦了。&lt;/p&gt;
&lt;p&gt;ps：感兴趣的话我可以在 QQ 空间或者朋友圈分享恋爱日常，嗯对。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://d988089.webp.li/2025/12/31/20251231185517348.avif&quot; alt=&quot;Momoka&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;2025 年，对我来说是极速成长的一年。技术上，我从一个普通的校园开发者进化成了能独当一面的 AI Agent 工程师；心智上，我挺过了至暗时刻，学会了珍惜真正重要的人。&lt;/p&gt;
&lt;p&gt;新的一年，希望自己能继续保持对技术的热爱，在 AI 的浪潮里冲得更远。也希望 Momoka 身体健康，希望我们能一直走下去。&lt;/p&gt;
&lt;p&gt;2026，请多关照！&lt;/p&gt;
</content:encoded></item><item><title>为客户端应用(Web/Native)构建低成本、高扩展性的聚合登录架构(完整实现 + 官方文档)</title><link>https://mc.mimeng.top/posts/backend/workos-oauth/</link><guid isPermaLink="true">https://mc.mimeng.top/posts/backend/workos-oauth/</guid><description>这是一篇可直接落地到生产的完整指南。包含跨 Web 与原生端的细粒度数据流、端到端实现代码、Native 回传登录成功的 URI Scheme 方案、后端会话与 JWT 管理、渐进式授权(Gmail/GitHub 私有仓库)的完整流程,每个设计选择的详细说明,以及所有关键环节的 WorkOS AuthKit 官方文档引用。</description><pubDate>Mon, 27 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;这是一篇可直接落地到生产的完整指南。包含跨 Web 与原生端的细粒度数据流、端到端实现代码、Native 回传登录成功的 URI Scheme 方案、后端会话与 JWT 管理、渐进式授权(Gmail/GitHub 私有仓库)的完整流程,每个设计选择的详细说明,以及所有关键环节的 WorkOS AuthKit 官方文档引用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;背景：身份验证的“重复造轮”困境&lt;/h2&gt;
&lt;p&gt;在当今的软件生态中，为应用提供多样化的登录选项已成为标配。用户期望能够使用他们信任的身份提供商（IdP）—— 无论是社交登录（如 Google, GitHub, Twitter）还是企业身份（如 Microsoft, 飞书）。&lt;/p&gt;
&lt;p&gt;对于开发团队而言，逐个接入这些平台是一项高成本、低回报的重复性工作。这不仅意味着要为每一个 IdP 单独编写、调试和维护 OAuth 2.0/OIDC 的集成代码，处理它们之间微妙的 API 差异（如不同的 Scope、令牌端点和错误码），还必须自行应对更复杂的企业级需求，例如 SAML 2.0 断言和 SCIM 目录同步。&lt;/p&gt;
&lt;p&gt;这种碎片化的集成方式带来了几个核心痛点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;开发效率低下&lt;/strong&gt;：宝贵的时间被消耗在身份验证这种“重复造轮子”的基础设施上，而非核心业务功能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;维护噩梦&lt;/strong&gt;：每个 IdP 的 API 都可能变更、弃用或更新安全策略，维护 5 到 10 个不同的集成点极易出错且耗时。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全风险&lt;/strong&gt;：在 Web 和 Native 客户端之间安全地处理和传递 &lt;code&gt;code&lt;/code&gt;、&lt;code&gt;access_token&lt;/code&gt; 和 &lt;code&gt;refresh_token&lt;/code&gt; 极具挑战性，很容易因疏忽导致令牌泄露。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;WorkOS 及其 AuthKit 旨在从根本上解决这一问题。它提供了一个统一的抽象层，将所有复杂的身份验证协议（OAuth, OIDC, SAML, Magic Link）聚合为单个、简洁的 API。开发者不再需要关心特定 IdP 的实现细节，只需与 WorkOS 对接一次，即可为应用“即插即用”地启用所有主流登录方式。&lt;/p&gt;
&lt;p&gt;本文的目标，就是展示如何利用 WorkOS AuthKit 这一利器，构建一个低成本、高扩展性，且横跨 Web 和 Native 客户端的生产级聚合登录架构。我们将跳过所有繁琐的 IdP 单独配置，直奔主题，深入探讨最关键的架构设计与端到端实现。&lt;/p&gt;
&lt;h2&gt;总体架构与数据流&lt;/h2&gt;
&lt;p&gt;以下是完整的认证流程图,展示了 Web 和 Native 客户端如何通过 WorkOS AuthKit 实现统一的身份认证:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant NativeApp as 原生客户端
    participant WebApp as Web 前端
    participant BFF as 你的后端(BFF/API)
    participant WorkOS as WorkOS AuthKit

    rect rgb(245,245,245)
    Note over NativeApp,WebApp: 登录入口（统一走 /login）
    NativeApp-&amp;gt;&amp;gt;BFF: 打开系统浏览器访问 GET /login?origin=native
    WebApp-&amp;gt;&amp;gt;BFF: 点击登录按钮访问 GET /login
    BFF-&amp;gt;&amp;gt;WorkOS: getAuthorizationUrl(provider=authkit, redirectUri=/callback, state=...)
    BFF--&amp;gt;&amp;gt;NativeApp: 302 重定向到 WorkOS 托管登录页
    BFF--&amp;gt;&amp;gt;WebApp: 302 重定向到 WorkOS 托管登录页
    end

    rect rgb(245,245,245)
    Note over WorkOS,BFF: 回调交换授权码（仅后端）
    WorkOS--&amp;gt;&amp;gt;BFF: 302 /callback?code=...&amp;amp;state=...
    BFF-&amp;gt;&amp;gt;WorkOS: authenticateWithCode(code, clientId)
    BFF--&amp;gt;&amp;gt;WebApp: 写入 sealedSession Cookie + 302 到首页
    BFF--&amp;gt;&amp;gt;NativeApp: 302 yourappscheme://login/success?ok=1&amp;amp;uid=...
    end

    rect rgb(245,245,245)
    Note over NativeApp,BFF: Native 拉取业务 JWT
    NativeApp-&amp;gt;&amp;gt;BFF: POST /native/session/jwt { uid, deviceProof }
    BFF--&amp;gt;&amp;gt;NativeApp: 200 { token: MyApp_JWT }
    end

    rect rgb(245,245,245)
    Note over WebApp,BFF: 受保护资源访问（Web 端）
    WebApp-&amp;gt;&amp;gt;BFF: 带 wos-session Cookie 访问 /dashboard
    BFF-&amp;gt;&amp;gt;WorkOS: loadSealedSession + session.authenticate() 或 refresh()
    BFF--&amp;gt;&amp;gt;WebApp: 200 页面/JSON
    end

    rect rgb(245,245,245)
    Note over BFF,WorkOS: 登出
    WebApp-&amp;gt;&amp;gt;BFF: GET /logout
    NativeApp-&amp;gt;&amp;gt;BFF: GET /logout
    BFF-&amp;gt;&amp;gt;WorkOS: session.getLogOutUrl()
    BFF--&amp;gt;&amp;gt;WebApp: 清 Cookie + 302 到 logout URL
    BFF--&amp;gt;&amp;gt;NativeApp: 清 Cookie/JWT + 302 到 logout URL
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;架构设计要点&lt;/h3&gt;
&lt;p&gt;以下是这套架构的核心设计原则及其原因:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;统一登录入口 &lt;code&gt;/login&lt;/code&gt;&lt;/strong&gt;: 由后端生成 WorkOS 授权 URL,可动态附加 &lt;code&gt;state&lt;/code&gt;、Scope 与来源标记,避免前端随意拼接参数,保障安全与审计一致性。&lt;br /&gt;
📖 参考:&lt;a href=&quot;https://workos.com/docs/reference/authkit/authentication/get-authorization-url&quot;&gt;AuthKit API Reference | Get an authorization URL&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;回调 &lt;code&gt;/callback&lt;/code&gt; 在后端完成&lt;/strong&gt;: 授权码交换与会话封存在后端完成,防止刷新令牌或会话信息暴露到前端。&lt;br /&gt;
📖 参考:&lt;a href=&quot;https://workos.com/docs/reference/authkit/authentication/code&quot;&gt;AuthKit API Reference | Authenticate with Code&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Web 使用 sealedSession Cookie&lt;/strong&gt;: 后端用 &lt;code&gt;authenticateWithSessionCookie&lt;/code&gt;/&lt;code&gt;refreshAndSealSessionData&lt;/code&gt; 校验与刷新,这是 WorkOS AuthKit 的原生能力。&lt;br /&gt;
📖 参考:&lt;a href=&quot;https://workos.com/docs/authkit/sessions&quot;&gt;Sessions – AuthKit – WorkOS Docs&lt;/a&gt;、&lt;a href=&quot;https://workos.com/docs/reference/authkit/session&quot;&gt;AuthKit API Reference | Session&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Native 使用 URI Scheme&lt;/strong&gt;: 只传递&quot;登录成功信号&quot;,令牌通过安全 API 拉取,保证令牌仅在后端与客户端之间的 HTTPS 通道传输。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;环境配置与前置条件&lt;/h2&gt;
&lt;h3&gt;WorkOS 控制台配置&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Redirect URIs 配置&lt;/strong&gt;&lt;br /&gt;
添加 &lt;code&gt;https://yourapp.com/callback&lt;/code&gt;(生产必须 HTTPS;Staging 才允许 &lt;code&gt;http/localhost&lt;/code&gt;;生产下为支持原生客户端,允许 &lt;code&gt;http://127.0.0.1&lt;/code&gt; 作为唯一例外)。&lt;br /&gt;
📖 参考:&lt;a href=&quot;https://workos.com/docs/reference/authkit/authentication/get-authorization-url/redirect-uri&quot;&gt;AuthKit API Reference | Redirect URI&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;获取 API 密钥&lt;/strong&gt;&lt;br /&gt;
在 WorkOS Dashboard 获取 &lt;code&gt;WORKOS_API_KEY&lt;/code&gt; 与 &lt;code&gt;WORKOS_CLIENT_ID&lt;/code&gt;,并安全注入到后端环境变量。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;配置 Logout Redirect&lt;/strong&gt;&lt;br /&gt;
确保登出跳转正确完成(Session helpers 中的 getLogOutUrl 对应)。&lt;br /&gt;
📖 参考:&lt;a href=&quot;https://workos.com/docs/reference/authkit/logout&quot;&gt;AuthKit API Reference ｜ Logout&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SSO/组织域策略&lt;/strong&gt;(可选)&lt;br /&gt;
如需 SSO/组织域策略,参考 Organizations/SSO 文档进行连接与域策略设置。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Cookie 会话密钥&lt;/h3&gt;
&lt;p&gt;设置 &lt;code&gt;WORKOS_COOKIE_PASSWORD&lt;/code&gt;: 至少 32 字符强密钥,用于 sealedSession 的加密封存与解封。弱密钥会导致会话校验失败。&lt;br /&gt;
📖 参考:&lt;a href=&quot;https://workos.com/docs/reference/authkit/session-helpers&quot;&gt;AuthKit API Reference | Session helpers&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;端到端实现（代码 + 设计原因）&lt;/h2&gt;
&lt;h3&gt;后端初始化&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// server/app.js
import express from &quot;express&quot;;
import cookieParser from &quot;cookie-parser&quot;;
import { WorkOS } from &quot;@workos-inc/node&quot;;

const app = express();
app.use(express.json());
app.use(cookieParser());

const workos = new WorkOS(process.env.WORKOS_API_KEY, {
  clientId: process.env.WORKOS_CLIENT_ID,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[为什么这样初始化]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WorkOS Node SDK 提供 &lt;code&gt;userManagement.getAuthorizationUrl&lt;/code&gt;、&lt;code&gt;authenticate&lt;/code&gt;、&lt;code&gt;loadSealedSession&lt;/code&gt;、&lt;code&gt;refreshAndSealSessionData&lt;/code&gt; 等能力,用于统一处理授权、会话与刷新。
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;📖 参考:&lt;a href=&quot;https://workos.com/docs/reference&quot;&gt;API Reference – WorkOS Docs&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;登录入口：GET /login（动态生成授权 URL）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// server/routes/auth.js
app.get(&quot;/login&quot;, (req, res) =&amp;gt; {
  const isNative = req.query.origin === &quot;native&quot;;
  const state = isNative ? &quot;origin=native&quot; : &quot;origin=web&quot;;

  const authorizationUrl = workos.userManagement.getAuthorizationUrl({
    provider: &quot;authkit&quot;,                      // 托管登录页
    redirectUri: &quot;https://yourapp.com/callback&quot;,
    clientId: process.env.WORKOS_CLIENT_ID,
    state,
    // 可选:初次登录就附加额外 scope(一般不建议,推荐渐进式)
    // providerScopes: [&quot;https://www.googleapis.com/auth/gmail.modify&quot;],
  });

  return res.redirect(authorizationUrl);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[为什么后端生成授权 URL]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;安全与可控&lt;/strong&gt;: 前端不直接拼 URL,避免误传或伪造;后端统一埋点审计与来源标记&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数校验&lt;/strong&gt;: 授权 URL 必须包含 &lt;code&gt;client_id&lt;/code&gt;、&lt;code&gt;redirect_uri&lt;/code&gt;、&lt;code&gt;state&lt;/code&gt; 等参数,WorkOS 将对重定向地址进行校验&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态场景&lt;/strong&gt;: 通过后端可动态附加场景标记与 Scope,符合官方授权 URL 生成规范
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;📖 参考: &lt;a href=&quot;https://workos.com/docs/reference/authkit/authentication/get-authorization-url&quot;&gt;AuthKit API Reference | Get Authorization URL&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;回调交换授权码:GET /callback&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;app.get(&quot;/callback&quot;, async (req, res) =&amp;gt; {
  const { code, state } = req.query;
  if (!code) return res.status(400).send(&quot;No code provided&quot;);

  try {
    const { user, accessToken, refreshToken, authenticationMethod } =
      await workos.userManagement.authenticate({
        // 等同于 authenticateWithCode;Node SDK 将路由到 /user_management/authenticate
        clientId: process.env.WORKOS_CLIENT_ID,
        grantType: &quot;authorization_code&quot;,
        code,
        ipAddress: req.ip,
        userAgent: req.headers[&quot;user-agent&quot;],
      });

    // 会话封存(sealedSession)供 Web 使用
    const { sealedSession } = await workos.userManagement.refreshAndSealSessionData({
      sessionData: JSON.stringify({ accessToken, refreshToken }),
      cookiePassword: process.env.WORKOS_COOKIE_PASSWORD,
    });

    res.cookie(&quot;wos-session&quot;, sealedSession, {
      path: &quot;/&quot;,
      httpOnly: true,
      secure: true,
      sameSite: &quot;lax&quot;,
    });

    // 统一身份联邦映射(find_or_create)
    const localUser = await db.users.findOrCreateByWorkOS({
      workosUserId: user.id,
      email: user.email,
      profile: user,
      authMethod: authenticationMethod,
    });

    // 为 Native 颁发业务 JWT(不透传在 URI;由 Native 通过 HTTPS 拉取)
    const myAppJwt = await jwt.issueForUser(localUser.id, {
      roles: localUser.roles,
      orgId: user.organization_id,
    });
    await db.tokens.bindLatestJwt(localUser.id, myAppJwt);

    // 根据 state 判断来源并处理重定向
    if (state &amp;amp;&amp;amp; String(state).includes(&quot;origin=native&quot;)) {
      // Native 登录成功:通过 URI Scheme 通知,不带敏感令牌
      const nativeUri = `yourappscheme://login/success?ok=1&amp;amp;uid=${encodeURIComponent(localUser.id)}`;
      return res.redirect(nativeUri);
    }

    // Web 登录成功:直接回首页或目标页
    return res.redirect(&quot;/&quot;);
  } catch (err) {
    // 错误码示例:access_denied/organization_invalid/ambiguous_connection_selector 等
    console.error(&quot;AuthKit callback error:&quot;, err);
    return res.redirect(&quot;/login?error=auth_failed&quot;);
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[为什么在后端处理回调]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;安全性&lt;/strong&gt;: 令牌与刷新逻辑只在后端处理,前端仅持有封存会话(浏览器端),Native 不经手敏感令牌&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;标准流程&lt;/strong&gt;: 授权码交换使用 &lt;code&gt;/user_management/authenticate&lt;/code&gt; 并返回用户对象与令牌,随后通过 Session helpers 封存会话到 Cookie,这是 AuthKit 的标准流程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;统一管理&lt;/strong&gt;: 立即进行用户映射与会话封存,保持认证状态一致性
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;📖 参考:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://workos.com/docs/reference/authkit/authentication/code&quot;&gt;AuthKit API Reference | Authenticate with code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://workos.com/docs/reference/authkit/authentication/refresh-and-seal-session-data&quot;&gt;AuthKit API Reference | Refresh and seal session data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/workos/workos-node/issues/959&quot;&gt;Types for errors · Issue #959 · workos/workos-node&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Web 端受保护路由:会话校验与刷新&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;function withAuth(req, res, next) {
  const sealed = req.cookies[&quot;wos-session&quot;];
  if (!sealed) return res.redirect(&quot;/login&quot;);

  (async () =&amp;gt; {
    const authResp = await workos.userManagement.authenticateWithSessionCookie({
      sessionData: sealed,
      cookiePassword: process.env.WORKOS_COOKIE_PASSWORD,
    });

    if (authResp.authenticated) {
      req.authUser = authResp.user;
      return next();
    }

    // 无会话或过期:尝试刷新并回写 Cookie
    const refreshResp = await workos.userManagement.refreshAndSealSessionData({
      sessionData: sealed,
      cookiePassword: process.env.WORKOS_COOKIE_PASSWORD,
    });

    if (!refreshResp.authenticated) {
      res.clearCookie(&quot;wos-session&quot;);
      return res.redirect(&quot;/login&quot;);
    }

    res.cookie(&quot;wos-session&quot;, refreshResp.sealedSession, {
      path: &quot;/&quot;,
      httpOnly: true,
      secure: true,
      sameSite: &quot;lax&quot;,
    });
    return res.redirect(req.originalUrl); // 确保新会话生效
  })().catch(() =&amp;gt; {
    res.clearCookie(&quot;wos-session&quot;);
    return res.redirect(&quot;/login&quot;);
  });
}

app.get(&quot;/dashboard&quot;, withAuth, (req, res) =&amp;gt; {
  res.send(`Welcome ${req.authUser.email}`);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[为什么使用 sealedSession Cookie]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;浏览器天然适配&lt;/strong&gt;: 浏览器端天然适配 Cookie,&lt;code&gt;httpOnly&lt;/code&gt; 防 XSS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;官方推荐&lt;/strong&gt;: 使用 &lt;code&gt;authenticateWithSessionCookie&lt;/code&gt; 与 &lt;code&gt;refreshAndSealSessionData&lt;/code&gt; 管理会话,无需自己维护 Refresh Token,这是 WorkOS 提供的&quot;Session helpers&quot;推荐做法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动刷新&lt;/strong&gt;: 会话刷新统一在后端完成,前端不暴露令牌
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;📖 参考:&lt;a href=&quot;https://workos.com/docs/reference/authkit/session-helpers&quot;&gt;AuthKit API Reference | Session helpers&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;原生端:URI Scheme 回传 + 拉取业务 JWT&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- AndroidManifest.xml 配置深链接 --&amp;gt;
&amp;lt;activity android:name=&quot;.LoginCallbackActivity&quot;&amp;gt;
    &amp;lt;intent-filter&amp;gt;
        &amp;lt;action android:name=&quot;android.intent.action.VIEW&quot; /&amp;gt;
        &amp;lt;category android:name=&quot;android.intent.category.DEFAULT&quot; /&amp;gt;
        &amp;lt;category android:name=&quot;android.intent.category.BROWSABLE&quot; /&amp;gt;
        &amp;lt;data
            android:scheme=&quot;yourappscheme&quot;
            android:host=&quot;login&quot;
            android:pathPrefix=&quot;/success&quot; /&amp;gt;
    &amp;lt;/intent-filter&amp;gt;
&amp;lt;/activity&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// LoginCallbackActivity.kt - 处理登录回调
class LoginCallbackActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 处理深链接
        intent?.data?.let { uri -&amp;gt;
            if (uri.scheme == &quot;yourappscheme&quot; &amp;amp;&amp;amp; 
                uri.host == &quot;login&quot; &amp;amp;&amp;amp; 
                uri.path == &quot;/success&quot;) {
                
                val ok = uri.getQueryParameter(&quot;ok&quot;) == &quot;1&quot;
                val uid = uri.getQueryParameter(&quot;uid&quot;)
                
                if (ok &amp;amp;&amp;amp; uid != null) {
                    // 通过 HTTPS 拉取 JWT(不在 URI 中携带令牌)
                    lifecycleScope.launch {
                        try {
                            val token = NativeApi.fetchJWT(
                                uid = uid,
                                deviceProof = DeviceProof.current()
                            )
                            Session.storeJWT(token)
                            AppState.setLoggedIn(true)
                            
                            // 跳转到主界面
                            startActivity(Intent(this@LoginCallbackActivity, MainActivity::class.java))
                            finish()
                        } catch (e: Exception) {
                            Log.e(&quot;LoginCallback&quot;, &quot;Failed to fetch JWT&quot;, e)
                            Toast.makeText(this@LoginCallbackActivity, &quot;登录失败&quot;, Toast.LENGTH_SHORT).show()
                            finish()
                        }
                    }
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;// 后端:Native 拉取业务 JWT
app.post(&quot;/native/session/jwt&quot;, async (req, res) =&amp;gt; {
  const { uid, deviceProof } = req.body;
  if (!uid) return res.status(400).json({ error: &quot;missing_uid&quot; });

  // 校验设备证明(防止任意人持 uid 取 JWT)
  const ok = await verifyDeviceProof(uid, deviceProof);
  if (!ok) return res.status(401).json({ error: &quot;invalid_device_proof&quot; });

  const record = await db.tokens.getLatestJwt(uid);
  if (!record) return res.status(404).json({ error: &quot;no_jwt&quot; });

  return res.json({ token: record.value, tokenType: &quot;Bearer&quot;, expiresIn: 3600 });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[为什么使用 URI Scheme + HTTPS 拉取]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;安全性&lt;/strong&gt;: URI Scheme 不携令牌,仅通知成功;真正的 JWT 通过 HTTPS 拉取并校验设备证明,降低泄露风险&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跨平台一致&lt;/strong&gt;: Redirect URI 在生产要求 HTTPS,Native 端允许 &lt;code&gt;http://127.0.0.1&lt;/code&gt; 例外以支持本地侦听;本方案不依赖本地回环端口,使用 URI Scheme 更通用,在多平台一致性更好&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;平台支持&lt;/strong&gt;: iOS/Android/桌面端均原生支持 URI Scheme
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;📖 参考:&lt;a href=&quot;https://workos.com/docs/reference/authkit/authentication/get-authorization-url/redirect-uri&quot;&gt;AuthKit API Reference | Redirect URI&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;登出：GET /logout&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;app.get(&quot;/logout&quot;, async (req, res) =&amp;gt; {
  try {
    const session = await workos.userManagement.loadSealedSession({
      sessionData: req.cookies[&quot;wos-session&quot;],
      cookiePassword: process.env.WORKOS_COOKIE_PASSWORD,
    });

    const logoutUrl = await session.getLogOutUrl();
    res.clearCookie(&quot;wos-session&quot;);
    return res.redirect(logoutUrl);
  } catch {
    res.clearCookie(&quot;wos-session&quot;);
    return res.redirect(&quot;/login&quot;);
  }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[为什么使用 WorkOS 登出 URL]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;统一管理&lt;/strong&gt;: 官方提供 &lt;code&gt;getLogOutUrl&lt;/code&gt; 与对应的 Dashboard 配置,用于正确终止会话与跳转&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多 IdP 一致性&lt;/strong&gt;: 统一通过 WorkOS 结束会话,保持多 IdP/多方式一致性
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;📖 参考:&lt;a href=&quot;https://workos.com/docs/reference/authkit/logout&quot;&gt;AuthKit API Reference | Logout&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;渐进式授权(案例:Gmail 与 GitHub 私有仓库)&lt;/h2&gt;
&lt;p&gt;:::tip[为什么渐进式(原因)]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;最小权限原则&lt;/strong&gt;: 初次登录只拿基础身份,避免一开始请求高敏感权限导致用户拒绝或平台审核不通过&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户体验与合规&lt;/strong&gt;: 用户明确点击&quot;连接某资源&quot;再授权,形成清晰的同意链路(WorkOS 的授权 URL 支持追加 provider_scopes 与 state)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全边界&lt;/strong&gt;: 高敏感 Refresh Token 仅存后端,支持离线任务且不暴露给客户端
:::&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;📖 参考:&lt;a href=&quot;https://workos.com/docs/reference/authkit/authentication/get-authorization-url&quot;&gt;AuthKit API Reference | Get Authorization URL&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Gmail 邮箱访问(高敏感)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;所需 Scope&lt;/strong&gt;: &lt;code&gt;https://www.googleapis.com/auth/gmail.modify&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;流程&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户在应用中点击&quot;连接 Gmail&quot;&lt;/li&gt;
&lt;li&gt;后端生成新的授权 URL,追加 Gmail Scope、&lt;code&gt;state=feature=gmail&lt;/code&gt;(可在前端发起到 &lt;code&gt;/oauth/gmail/connect&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;WorkOS 完成授权并回调 &lt;code&gt;/callback&lt;/code&gt;,后端交换并持久化 Gmail 模块的 Refresh Token(与基础登录分开存储)&lt;/li&gt;
&lt;li&gt;后端定时任务使用 Refresh Token 刷新 Access Token,并调用 Gmail API&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;代码示例&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 生成 Gmail 授权 URL
app.get(&quot;/oauth/gmail/connect&quot;, (req, res) =&amp;gt; {
  const authorizationUrl = workos.userManagement.getAuthorizationUrl({
    provider: &quot;authkit&quot;,
    redirectUri: &quot;https://yourapp.com/callback&quot;,
    clientId: process.env.WORKOS_CLIENT_ID,
    state: &quot;feature=gmail&quot;,
    providerScopes: [&quot;https://www.googleapis.com/auth/gmail.modify&quot;],
  });
  res.redirect(authorizationUrl);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;📖 参考:&lt;a href=&quot;https://workos.com/docs/reference/authkit/authentication/get-authorization-url&quot;&gt;AuthKit API Reference | Get Authorization URL&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;GitHub 私有仓库(细粒度)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;所需 Scope&lt;/strong&gt;: &lt;code&gt;repo&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;流程&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用户点击&quot;连接 GitHub 私有仓库&quot;&lt;/li&gt;
&lt;li&gt;后端生成授权 URL,请求 &lt;code&gt;repo&lt;/code&gt; Scope&lt;/li&gt;
&lt;li&gt;用户拒绝时,基础登录功能不受影响(不覆盖原有低权限令牌)&lt;/li&gt;
&lt;li&gt;成功后才启用私有仓库功能;令牌按模块分区存储&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;📖 参考:&lt;a href=&quot;https://workos.com/docs/reference/authkit/authentication/get-authorization-url&quot;&gt;AuthKit API Reference | Get Authorization URL&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;数据模型与安全建议&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用户表： &lt;code&gt;id&lt;/code&gt;（你方）、&lt;code&gt;workos_user_id&lt;/code&gt;、&lt;code&gt;email&lt;/code&gt;、&lt;code&gt;profile_json&lt;/code&gt;、&lt;code&gt;last_sign_in_at&lt;/code&gt;、&lt;code&gt;organization_id&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;令牌表（模块分区）： &lt;code&gt;user_id&lt;/code&gt;、&lt;code&gt;module&lt;/code&gt;（gmail/github）、&lt;code&gt;refresh_token_hash&lt;/code&gt;、&lt;code&gt;scope_set_hash&lt;/code&gt;、&lt;code&gt;created_at&lt;/code&gt;、&lt;code&gt;rotated_at&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;业务 JWT 表： &lt;code&gt;user_id&lt;/code&gt;、&lt;code&gt;jwt_hash&lt;/code&gt;、&lt;code&gt;issued_at&lt;/code&gt;、&lt;code&gt;expires_at&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;会话（可选）： 多数场景直接用 sealedSession 无需自管；如需审计，记录 &lt;code&gt;session_id&lt;/code&gt; 与快照元数据。&lt;/li&gt;
&lt;li&gt;原生端安全：
&lt;ul&gt;
&lt;li&gt;URI Scheme 不携令牌，仅状态标记。&lt;/li&gt;
&lt;li&gt;拉取 JWT 时校验 &lt;code&gt;deviceProof&lt;/code&gt;（一次性票据/设备指纹），短有效期防重放。&lt;/li&gt;
&lt;li&gt;JWT 仅用于你方 API，第三方 API 访问由后端凭存储的 Refresh Token 完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Web 与 Native 客户端落地细节对照&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Web：
&lt;ul&gt;
&lt;li&gt;登录： 跳 &lt;code&gt;/login&lt;/code&gt;，浏览器自动跟随 302。&lt;/li&gt;
&lt;li&gt;回调： 后端写 Cookie，重定向首页。&lt;/li&gt;
&lt;li&gt;受保护路由： 后端中间件用 &lt;code&gt;authenticateWithSessionCookie&lt;/code&gt; 校验；必要时 &lt;code&gt;refreshAndSealSessionData&lt;/code&gt; 刷新并回写 Cookie。&lt;/li&gt;
&lt;li&gt;登出： &lt;code&gt;/logout&lt;/code&gt; 清 Cookie + 重定向到 WorkOS logout URL。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Native（iOS/Android/桌面）：
&lt;ul&gt;
&lt;li&gt;登录： 打开系统浏览器访问 &lt;code&gt;/login?origin=native&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;回调： 后端完成交换，302 跳 &lt;code&gt;yourappscheme://login/success?ok=1&amp;amp;uid=...&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;拉取令牌： POST &lt;code&gt;/native/session/jwt&lt;/code&gt;，校验设备证明，返回 &lt;code&gt;MyApp_JWT&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;后续： 持 JWT 调你方业务 API；第三方资源访问由后端凭存储的 Refresh Token 完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;常见错误与排查&lt;/h2&gt;
&lt;p&gt;以下是实施过程中可能遇到的常见错误及解决方案:&lt;/p&gt;
&lt;h3&gt;Callback 缺少 code&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;: 回调时 URL 中没有 &lt;code&gt;code&lt;/code&gt; 参数&lt;br /&gt;
&lt;strong&gt;原因&lt;/strong&gt;: Redirect URI 配置不匹配&lt;br /&gt;
&lt;strong&gt;解决&lt;/strong&gt;: 确认 Dashboard 中 Redirect URIs 与后端 &lt;code&gt;/callback&lt;/code&gt; 完整一致;WorkOS 对 Redirect URI 有严格匹配与生产环境 HTTPS 要求(127.0.0.1 例外)&lt;br /&gt;
📖 参考:&lt;a href=&quot;https://workos.com/docs/reference/authkit/authentication/get-authorization-url&quot;&gt;AuthKit API Reference | Get Authorization URL&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;会话封存失败&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;: 无法创建或验证 sealedSession&lt;br /&gt;
&lt;strong&gt;原因&lt;/strong&gt;: &lt;code&gt;WORKOS_COOKIE_PASSWORD&lt;/code&gt; 太短或不一致&lt;br /&gt;
&lt;strong&gt;解决&lt;/strong&gt;: 使用至少 32 字符的强密钥,并确保 Session helpers 封存与解封使用同一加密口令&lt;br /&gt;
📖 参考:&lt;a href=&quot;https://workos.com/docs/reference/authkit/session-helpers&quot;&gt;AuthKit API Reference | Session helpers&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;频繁 302 循环&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;: 不断重定向到登录页&lt;br /&gt;
&lt;strong&gt;原因&lt;/strong&gt;: 刷新后未回写新的 sealedSession&lt;br /&gt;
&lt;strong&gt;解决&lt;/strong&gt;: 在 &lt;code&gt;refreshAndSealSessionData&lt;/code&gt; 后重写 Cookie 并重载当前路由&lt;br /&gt;
📖 参考:&lt;a href=&quot;https://workos.com/docs/reference/authkit/session-helpers&quot;&gt;AuthKit API Reference | Session helpers&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Logout 提示错误&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;: 登出时出现错误或无法跳转&lt;br /&gt;
&lt;strong&gt;原因&lt;/strong&gt;: 未配置 Logout Redirect&lt;br /&gt;
&lt;strong&gt;解决&lt;/strong&gt;: 在 Dashboard 补充 Logout Redirect 配置&lt;br /&gt;
📖 参考:&lt;a href=&quot;https://workos.com/docs/reference/authkit/session-helpers/get-logout-url&quot;&gt;AuthKit API Reference | Get log out URL&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;Native 无法捕获 URI&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;现象&lt;/strong&gt;:原生应用无法接收登录回调&lt;br /&gt;
&lt;strong&gt;原因&lt;/strong&gt;:未正确注册 URL Types/Intent Filter/系统 URI Handler&lt;br /&gt;
&lt;strong&gt;解决&lt;/strong&gt;:检查平台注册与 &lt;code&gt;yourappscheme&lt;/code&gt; 唯一性&lt;/p&gt;
&lt;h2&gt;参考文档&lt;/h2&gt;
&lt;h3&gt;核心 API 文档&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;WorkOS API Reference 总览&lt;/strong&gt;(所有接口、错误码、会话等)&lt;br /&gt;
👉 https://workos.com/docs/reference&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;授权与认证&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Get Authorization URL&lt;/strong&gt;&lt;br /&gt;
👉 https://workos.com/docs/reference/authkit/authentication/get-authorization-url&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authenticate with Code&lt;/strong&gt;&lt;br /&gt;
👉 https://workos.com/docs/reference/authkit/authentication/code&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AuthKit 集成指南&lt;/strong&gt;(Node.js)&lt;br /&gt;
👉 https://workos.com/docs/authkit/vanilla/nodejs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;会话管理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sessions 文档&lt;/strong&gt;(Session helpers、getLogOutUrl)&lt;br /&gt;
👉 https://workos.com/docs/authkit/sessions&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;错误处理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;错误码完整列表&lt;/strong&gt;&lt;br /&gt;
👉 https://workos.com/docs/reference/errors&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;错误类型与处理建议&lt;/strong&gt;(GitHub Issues)&lt;br /&gt;
👉 https://github.com/workos/workos-node/issues/959&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;这套端到端方案将 WorkOS AuthKit 的能力以最安全与可维护的方式融入你的体系：统一后端集中式认证、Web 以 sealedSession 驱动、Native 用 URI Scheme 通知并通过 HTTPS 拉取业务 JWT、后端独占刷新令牌与离线访问、按模块管理高敏感权限。每个环节的实现与设计原因都已给出，并辅以官方文档引用，确保能无缝落地到生产环境。&lt;/p&gt;
</content:encoded></item><item><title>解构 Claude Code：构建代码原生 AI 代理的技术蓝图</title><link>https://mc.mimeng.top/posts/agent/claude-code/</link><guid isPermaLink="true">https://mc.mimeng.top/posts/agent/claude-code/</guid><description>深度剖析 Claude Code 的设计哲学与核心架构，从零开始构建一个生产级的代码原生 AI 代理。本文涵盖规划器-执行器模型、针对代码优化的 RAG、LangGraph 状态机以及 Docker 安全沙箱等关键技术。</description><pubDate>Wed, 08 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;深度剖析 Claude Code 的设计哲学与核心架构，从零开始构建一个生产级的代码原生 AI 代理。本文涵盖规划器-执行器模型、针对代码优化的 RAG、LangGraph 状态机以及 Docker 安全沙箱等关键技术。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;:::important[免责声明]
本文由 AI 协作于国庆假期耗时约 8 天，整理了大量资料并总结内容进行编写，本人仅做了初步的内容验证，请您自行分辨其中内容的真伪。
:::&lt;/p&gt;
&lt;h2&gt;第一部分：代码原生代理的剖析：哲学与架构&lt;/h2&gt;
&lt;p&gt;本部分将阐述我们代理的基本原则，这些原则直接借鉴自 Claude Code 的设计哲学。我们将介绍“迷你版 Claude”的高层架构，并说明为何选择规划器-执行器模型作为处理复杂编码任务的最适范式。&lt;/p&gt;
&lt;h3&gt;1.1 Claude Code 的哲学：一个低层级、无偏见的强大工具&lt;/h3&gt;
&lt;p&gt;Anthropic 的 Claude Code 背后的设计哲学，代表了与高度结构化、有主见的 AI 助手的一次重大分野。其核心原则是“有意保持低层级和无偏见，提供近乎原始的模型访问，而不强加特定的工作流程”&lt;a href=&quot;https://www.anthropic.com/engineering/claude-code-best-practices&quot;&gt;1&lt;/a&gt;。这种方法将 Claude Code 定位为一个灵活、可定制、可编写脚本的强大工具，而非一个规定特定开发流程的僵化助手。其目标是赋能开发者，增强他们现有的工作流程，而不是取而代之。这种哲学体现在一种常被描述为“令人愉悦”的用户体验中，它在自主性与控制之间提供了精心校准的平衡。用户反馈称，该代理拥有足够的自主性来执行复杂有趣的任务，但又不会引发那种在更具侵略性的全自主系统中可能出现的“令人不安的失控感”&lt;a href=&quot;https://minusx.ai/blog/decoding-claude-code/&quot;&gt;2&lt;/a&gt;, &lt;a href=&quot;https://prismic.io/blog/claude-code&quot;&gt;3&lt;/a&gt;, &lt;a href=&quot;https://rafaelquintanilha.com/is-claude-code-worth-the-hype-or-just-expensive-vibe-coding/&quot;&gt;4&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这种设计的一个关键方面是它与开发者主要工作空间——终端的原生集成&lt;a href=&quot;https://prismic.io/blog/claude-code&quot;&gt;3&lt;/a&gt;。通过在命令行中操作，Claude Code 最大限度地减少了上下文切换，成为开发者环境的无缝扩展，就像代码编辑器或版本控制系统一样。这种深度集成使其能够与整个项目结构互动，自动化繁琐的任务，如解决 linting 问题或合并冲突，甚至能从外部来源（如网络文档或 Figma 中的设计文件）提取信息&lt;a href=&quot;https://docs.claude.com/en/docs/claude-code/overview#:~:text=Claude%20Code%20maintains%20awareness%20of%20your%20entire%20project%20structure%2C%20can%20find&quot;&gt;5&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这种在强大自主性与精确用户引导性之间的平衡，并非偶然，而是一项深思熟虑的工程决策。大型语言模型（LLM）固有的不确定性和“黑箱”特性，使得调试复杂的代理系统异常困难。正如逆向工程分析所指出的，尽管 AI 开发的趋势是复杂的“多代理系统”，但这种复杂性会使调试“难度增加10倍”&lt;a href=&quot;https://minusx.ai/blog/decoding-claude-code/&quot;&gt;2&lt;/a&gt;。一个未能按预期执行任务的 LLM，几乎不会提供任何堆栈跟踪或可复现的错误。当多个代理互动时，这个问题会变得更加复杂，使得诊断失败的根本原因几乎不可能。因此，Claude Code 的架构优先考虑了简单性和可观察性。它倾向于采用单一、有状态的控制循环，而非复杂的交互代理网络&lt;a href=&quot;https://minusx.ai/blog/decoding-claude-code/&quot;&gt;2&lt;/a&gt;。这一“简单性原则”是源于构建 LLM 实际挑战的核心工程信条。它承认，要构建一个可靠的工具，必须保持理解和调试其行为的能力。因此，我们的“迷你版 Claude”将采纳这一哲学，避免不必要的复杂性，转而采用一个强大、可观察且有状态的架构，并始终置于开发者的掌控之下。&lt;/p&gt;
&lt;h3&gt;1.2 高层架构：“迷你版 Claude”的三大支柱&lt;/h3&gt;
&lt;p&gt;为了构建一个体现灵活性、强大功能和可控性原则的代理，我们的“迷你版 Claude”将基于一个由三个核心、相互关联的系统组成的模块化架构。这种设计超越了依赖单一、庞大的 LLM 调用来解决问题的单体方法。相反，它认识到一个复杂的编码代理需要专门的组件，每个组件都为软件开发任务的特定子问题进行优化。&lt;/p&gt;
&lt;p&gt;我们代理架构的三大支柱是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;具备代码库感知的 RAG（检索增强生成）：&lt;/strong&gt; 这是代理的“长期记忆”和上下文理解系统。它是一个专门的 RAG 管道，旨在消化整个软件项目，并为代理提供关于代码库的深度、上下文知识。为了有效，该系统必须超越简单的文本检索，理解源代码的句法和语义结构，使代理能够推理函数依赖、类结构和架构模式&lt;a href=&quot;https://medium.com/google-cloud/build-a-rag-system-for-your-codebase-in-5-easy-steps-a3506c10599b&quot;&gt;6&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;规划器-执行器控制循环：&lt;/strong&gt; 这是代理的“大脑”或中枢神经系统。它是一个健壮的、有状态的控制循环，管理任务的整个生命周期。它负责将高层级的用户请求分解为一系列较小的、可执行的步骤（规划），然后使用一组定义的工具来执行这些步骤（执行）。我们将使用 LangGraph 框架将其实现为一个状态机，这非常适合构建我们设计哲学所要求的可观察、单进程的控制流&lt;a href=&quot;https://www.datacamp.com/tutorial/langgraph-agents&quot;&gt;7&lt;/a&gt;, &lt;a href=&quot;https://www.langchain.com/langgraph&quot;&gt;8&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker化执行沙箱：&lt;/strong&gt; 这是代理的“双手”及其关键的安全机制。它为执行 AI 生成的代码和 shell 命令提供了一个安全、隔离的环境。通过容器化执行，我们防止代理对主机系统构成任何风险，使其能够安全地编译代码、运行测试或使用命令行工具，而不会产生意外副作用的危险&lt;a href=&quot;https://anukriti-ranjan.medium.com/building-a-sandboxed-environment-for-ai-generated-code-execution-e1351301268a&quot;&gt;9&lt;/a&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种模块化设计确保每个组件都可以独立开发、测试和优化，从而形成一个更健壮、更有能力的最终系统。它直接反映了这样一种理解：构建一个成功的代理是一项系统工程，而不仅仅是提示工程。&lt;/p&gt;
&lt;h3&gt;1.3 为何选择规划器-执行器？超越简单的 ReAct 循环&lt;/h3&gt;
&lt;p&gt;控制循环架构的选择对代理的能力至关重要。一个常见且简单的架构是 ReAct（推理-行动）模型，它在一个“生成思考、选择工具、执行、观察结果”的紧密循环中运行&lt;a href=&quot;https://blog.langchain.com/planning-agents/&quot;&gt;10&lt;/a&gt;。虽然对于简单的单步任务有效，但当面对软件开发中固有的复杂、多步、长周期的任务时，ReAct 模型显示出显著的局限性&lt;a href=&quot;https://blog.langchain.com/planning-agents/&quot;&gt;10&lt;/a&gt;, &lt;a href=&quot;https://arxiv.org/html/2503.09572v2&quot;&gt;11&lt;/a&gt;。其主要缺点是缺乏远见——它一次只规划一个步骤，这可能导致效率低下或路径次优——以及其高昂的运营成本，因为它每次行动都需要调用一个强大（且通常昂贵）的 LLM。&lt;/p&gt;
&lt;p&gt;为了克服这些局限，我们的“迷你版 Claude”将基于更先进的&lt;strong&gt;规划器-执行器&lt;/strong&gt;架构&lt;a href=&quot;https://arxiv.org/html/2503.09572v2&quot;&gt;11&lt;/a&gt;, &lt;a href=&quot;https://www.emergentmind.com/topics/planner-executor-architecture-4c9e0097-fe2b-4870-b41c-9519c49a07c8&quot;&gt;12&lt;/a&gt;, &lt;a href=&quot;https://www.promptlayer.com/glossary/plan-and-execute-agents&quot;&gt;13&lt;/a&gt;。该范式明确地将战略规划的高层任务与执行的低层任务分离开来。其过程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;规划：&lt;/strong&gt; 一个由强大 LLM 驱动的中央&lt;strong&gt;规划器&lt;/strong&gt;模块接收用户的高层目标。它分析整个任务，并将其分解为一个全面的、多步骤的计划。这个计划不仅仅是下一个动作，而是一个为实现最终目标而设计的完整步骤序列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行：&lt;/strong&gt; 一个或多个&lt;strong&gt;执行器&lt;/strong&gt;模块接收此计划。然后，它们使用预定义的工具集按顺序执行计划中的每一步。关键在于，执行器通常可以是更简单的代理，甚至是直接的函数调用，它们在每次行动后无需咨询主规划器 LLM。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重新规划：&lt;/strong&gt; 在执行一系列步骤后，结果会反馈给规划器。规划器随后可以评估进展，验证目标是否已达成，或者在初始策略不足或遇到意外障碍时生成一个新的、修订过的计划&lt;a href=&quot;https://arxiv.org/html/2503.09572v2&quot;&gt;11&lt;/a&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种架构的优势是巨大的，并直接解决了 ReAct 模型的弱点。它提高了效率和执行速度，因为对主要推理 LLM 的调用次数减少了&lt;a href=&quot;https://blog.langchain.com/planning-agents/&quot;&gt;10&lt;/a&gt;, &lt;a href=&quot;https://arxiv.org/html/2503.09572v2&quot;&gt;11&lt;/a&gt;, &lt;a href=&quot;https://blog.langchain.com/planning-agents/&quot;&gt;14&lt;/a&gt;。出于同样的原因，它也更具成本效益。最重要的是，它带来了更好的性能和更高的任务完成率，因为初始规划阶段迫使代理对整个问题进行整体性推理，从而产生更连贯、更有效的策略&lt;a href=&quot;https://arxiv.org/html/2503.09572v2&quot;&gt;11&lt;/a&gt;。这一架构选择直接支持了对代理编码最有效的复杂、多阶段工作流，例如为 Claude Code 用户推荐的“探索、规划、编码、提交”模式&lt;a href=&quot;https://www.anthropic.com/engineering/claude-code-best-practices&quot;&gt;1&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;第二部分：代理的心智：控制循环与高级提示工程&lt;/h2&gt;
&lt;p&gt;本部分详细介绍了代理“大脑”的实现。我们将首先逆向工程驱动 Claude Code 行为的复杂提示技术，然后使用 LangGraph 构建有状态的控制循环来管理代理的生命周期。&lt;/p&gt;
&lt;h3&gt;2.1 解构 Claude Code 系统提示&lt;/h3&gt;
&lt;p&gt;一个由 LLM 驱动的代理的效能，深受其系统提示的质量和结构的影响。对 Claude Code 的逆向工程分析显示，其提示并非简单的单行指令，而是庞大、精细且高度工程化的产物&lt;a href=&quot;https://minusx.ai/blog/decoding-claude-code/&quot;&gt;2&lt;/a&gt;。仅系统提示就估计约有 2800 个令牌，而其可用工具的描述则占据了惊人的 9400 个令牌。这种详尽程度表明，提示同时充当了配置文件、行为指南和操作启发式规则集。基础 LLM 的无偏见特性，需要一个高度有主见和结构化的提示来有效引导其行为。复杂性被有意地从代理的硬编码逻辑转移到了其更灵活的自然语言配置中。&lt;/p&gt;
&lt;p&gt;要构建一个有效的“迷你版 Claude”，我们必须将其系统提示视为一个主要的工程产物，应进行版本控制、测试和优化。基于对 Claude Code 提示的解构，我们的系统提示应包含以下关键组件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;角色与人格定义：&lt;/strong&gt; 此部分设定代理的整体行为。它应定义其语调（例如，“你是一位专业且严谨的 AI 软件工程师”）、风格（例如，“你的代码应整洁、注释良好，并遵循行业最佳实践”）以及主动性水平（例如，“如果用户的请求含糊不清，请在继续之前提出澄清问题”）&lt;a href=&quot;https://minusx.ai/blog/decoding-claude-code/&quot;&gt;2&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;任务管理与显式算法：&lt;/strong&gt; 这是最关键的部分之一。提示不应让 LLM 自由决定如何处理问题，而应明确概述其必须遵循的算法。对于一个规划器-执行器代理，这可以是：“1. 彻底分析用户的请求和提供的上下文。2. 制定一个详细的、分步的计划以实现目标。3. 按顺序执行计划的每一步。4. 执行后，审查结果并确定目标是否已达成，或是否需要重新规划。” 这构建了代理的推理过程，使其行为更可预测、更可靠&lt;a href=&quot;https://minusx.ai/blog/decoding-claude-code/&quot;&gt;2&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;启发式规则与带 XML 标签的示例：&lt;/strong&gt; LLM 从示例中学习的效果非常好。提示应利用这一点，提供良好和不良行为的具体示例，并用特殊的 XML 标签括起来。例如，为了指导工具的使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;good-example&amp;gt;
用户请求：“查找项目中的所有测试文件。”
工具调用：Glob(pattern=&quot;**/*_test.py&quot;)
&amp;lt;/good-example&amp;gt;
&amp;lt;bad-example&amp;gt;
用户请求：“查找项目中的所有测试文件。”
工具调用：Bash(command=&quot;find. -name &apos;*_test.py&apos;&quot;)
&amp;lt;/bad-example&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种在 Claude Code 中观察到的技术，有助于将最佳实践编码化，并引导模型避开效率较低或更易出错的方法&lt;a href=&quot;https://minusx.ai/blog/decoding-claude-code/&quot;&gt;2&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;引导性与提醒：&lt;/strong&gt; 为了强制执行关键约束并防止常见的失败模式，提示应使用强烈的强调标记和提醒标签。诸如 &lt;code&gt;IMPORTANT&lt;/code&gt;、&lt;code&gt;VERY IMPORTANT&lt;/code&gt;、&lt;code&gt;NEVER&lt;/code&gt; 和 &lt;code&gt;ALWAYS&lt;/code&gt; 等短语可用于吸引模型对关键规则的注意（例如，&lt;code&gt;VERY IMPORTANT: 你必须在进行任何代码更改后验证测试是否通过&lt;/code&gt;）&lt;a href=&quot;https://minusx.ai/blog/decoding-claude-code/&quot;&gt;2&lt;/a&gt;。此外，&lt;code&gt;&amp;lt;system-reminder&amp;gt;&lt;/code&gt; 标签可用于重申模型在长对话中可能忘记的规则，例如“你有一个 &lt;code&gt;write_file&lt;/code&gt; 工具；除非是最终答案的一部分，否则不要直接向用户输出代码”&lt;a href=&quot;https://minusx.ai/blog/decoding-claude-code/&quot;&gt;2&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;动态上下文信息：&lt;/strong&gt; 提示应动态填充有关代理环境的实时信息。这包括当前日期和时间、操作系统和平台、当前工作目录，甚至是 &lt;code&gt;git log -n 5&lt;/code&gt; 的输出来提供近期更改的上下文。这将代理置于其当前的操作现实中&lt;a href=&quot;https://minusx.ai/blog/decoding-claude-code/&quot;&gt;2&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 实现 &lt;code&gt;claude.md&lt;/code&gt; 模式以实现持久化上下文&lt;/h3&gt;
&lt;p&gt;虽然系统提示提供了一套全局指令，但软件开发任务是高度依赖上下文的。适用于一个代码仓库的规则（例如，“使用制表符进行缩进”）可能在另一个代码仓库中是错误的。为了解决这个问题，Claude Code 采用了一种优雅的机制来进行项目特定的配置：&lt;code&gt;claude.md&lt;/code&gt; 文件&lt;a href=&quot;https://www.anthropic.com/engineering/claude-code-best-practices&quot;&gt;1&lt;/a&gt;, &lt;a href=&quot;https://minusx.ai/blog/decoding-claude-code/&quot;&gt;2&lt;/a&gt;。这是一个特殊的 markdown 文件，如果存在于项目目录中，其内容会在会话开始时自动被读取并附加到用户提示的前面。这实际上允许开发者根据每个项目来“编程”代理的上下文和偏好。&lt;/p&gt;
&lt;p&gt;在我们的“迷你版 Claude”中实现这种模式是一个直接但功能强大的增强。应用程序的入口点应包含一个函数，该函数在当前工作目录及其所有父目录中搜索指定的上下文文件（例如，&lt;code&gt;.mini_claude_rules.md&lt;/code&gt;），直到代码仓库的根目录。这允许分层配置，其中根级文件可以为整个 monorepo 定义规则，而子目录的文件可以为特定服务添加或覆盖规则。&lt;/p&gt;
&lt;p&gt;该文件的内容充当代理的“备忘单”，为其提供关于项目的基本、非显而易见的信息。这是用户级“提示编程”的一个关键机制，使代理能够适应新的代码库，而无需对其核心代码进行任何更改。应鼓励开发者将这些文件检入版本控制，以便整个团队都能从提供给代理的共享上下文中受益。在 &lt;code&gt;.mini_claude_rules.md&lt;/code&gt; 文件中包含的有价值信息的示例如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;构建和测试命令：&lt;/strong&gt; “要运行此项目的测试套件，请使用命令 &lt;code&gt;npm run test:unit&lt;/code&gt;。要运行 linter，请使用 &lt;code&gt;npm run lint&lt;/code&gt;。”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;架构指引：&lt;/strong&gt; “用户认证的核心业务逻辑位于 &lt;code&gt;src/services/auth.py&lt;/code&gt;。主要的数据库模型定义在 &lt;code&gt;src/models/&lt;/code&gt; 中。”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编码风格和约定：&lt;/strong&gt; “此项目使用 Black 代码格式化工具。始终使用双引号表示字符串。所有新组件必须是使用 React Hooks 的函数式组件。”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码仓库礼仪：&lt;/strong&gt; “所有分支应使用 &lt;code&gt;feature/TICKET-123-description&lt;/code&gt; 的格式命名。提交信息必须遵循 Conventional Commits 规范。”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;已知问题或变通方法：&lt;/strong&gt; “运行本地开发服务器时，你可能会看到关于 &lt;code&gt;some-dependency&lt;/code&gt; 的良性警告；这可以安全地忽略。” &lt;a href=&quot;https://www.anthropic.com/engineering/claude-code-best-practices&quot;&gt;1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.3 使用 LangGraph 构建状态机&lt;/h3&gt;
&lt;p&gt;通过高级提示定义了代理的指令框架后，我们现在转向其操作核心：控制循环。如前所述，我们将使用规划器-执行器架构来管理复杂任务。实现这一目标的理想现代框架是 LangGraph，这是一个构建在 LangChain 之上的库，允许开发者将代理工作流定义为有状态的图&lt;a href=&quot;https://www.datacamp.com/tutorial/langgraph-agents&quot;&gt;7&lt;/a&gt;, &lt;a href=&quot;https://www.langchain.com/langgraph&quot;&gt;8&lt;/a&gt;。这种方法与我们的“简单性原则”完美匹配，因为它允许我们将整个代理逻辑构建为单一、可观察的状态机，而不是一个复杂的交互代理网络。&lt;/p&gt;
&lt;p&gt;使用 LangGraph 构建的第一步是定义中央 &lt;code&gt;State&lt;/code&gt; 对象。这是一个 Python &lt;code&gt;TypedDict&lt;/code&gt;，代表了我们代理在任何时间点的全部状态。它将被传递给图中的每个节点，并且每个节点都可以修改它。对于我们的规划器-执行器代理，状态将跟踪管理任务从开始到结束所需的所有关键信息。根据该架构的标准模式，我们的状态将定义如下 &lt;a href=&quot;https://langchain-ai.github.io/langgraph/tutorials/plan-and-execute/plan-and-execute/&quot;&gt;15&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import operator
from typing import Annotated, List, Tuple
from typing_extensions import TypedDict
from langchain_core.messages import BaseMessage

class AgentState(TypedDict):
    # 初始用户请求
    input: str
    
    # 规划器生成的多步计划
    plan: List[str]
    
    # 已执行步骤及其结果的历史记录，用于重新规划
    past_steps: Annotated[List[Tuple[str, str]], operator.add]
    
    # 返回给用户的最终响应
    response: str
    
    # 我们还可以为执行器子代理添加消息历史记录
    messages: Annotated[List[BaseMessage], operator.add]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;带有 &lt;code&gt;operator.add&lt;/code&gt; 的 &lt;code&gt;Annotated&lt;/code&gt; 类型是 LangGraph 的一个特性，它确保当一个节点为 &lt;code&gt;past_steps&lt;/code&gt; 或 &lt;code&gt;messages&lt;/code&gt; 返回一个值时，该值会被附加到现有列表中，而不是覆盖它，从而允许状态随时间累积&lt;a href=&quot;https://www.langchain.com/langgraph&quot;&gt;8&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;定义了状态之后，我们现在可以概述图的节点和边。流程将精确地反映规划器-执行器模式，并将在第四部分中详细实现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;planner&lt;/code&gt; 节点：&lt;/strong&gt; 图的入口点。它从状态中接收 &lt;code&gt;input&lt;/code&gt; 并生成初始 &lt;code&gt;plan&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;executor&lt;/code&gt; 节点：&lt;/strong&gt; 此节点获取当前的 &lt;code&gt;plan&lt;/code&gt;，使用其工具执行下一步，并将结果附加到 &lt;code&gt;past_steps&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;re-planner&lt;/code&gt; 节点：&lt;/strong&gt; 执行后，此节点检查 &lt;code&gt;past_steps&lt;/code&gt; 并决定任务是否完成，或者 &lt;code&gt;plan&lt;/code&gt; 是否需要更新。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;条件边：&lt;/strong&gt; 一个路由函数，检查 &lt;code&gt;re-planner&lt;/code&gt; 的输出。如果已生成最终的 &lt;code&gt;response&lt;/code&gt;，它将图转换到特殊的 &lt;code&gt;END&lt;/code&gt; 状态。否则，它会循环回到 &lt;code&gt;executor&lt;/code&gt; 节点以继续执行计划。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种图结构为我们代理的核心逻辑提供了一个健壮、灵活且可调试的基础。&lt;/p&gt;
&lt;h2&gt;第三部分：实现代码库感知：构建特定于代码的 RAG 管道&lt;/h2&gt;
&lt;p&gt;本部分深入探讨如何创建代理理解软件项目的能力。我们将从头开始构建一个检索增强生成（RAG）管道，重点关注专门为源代码优化的技术，以便为代理提供准确、相关的上下文。&lt;/p&gt;
&lt;h3&gt;3.1 针对代码的天真 RAG 的问题&lt;/h3&gt;
&lt;p&gt;检索增强生成是一种强大的技术，用于将 LLM 植根于外部知识，有效地为它们提供一本“开卷书”以在回答问题前进行查阅&lt;a href=&quot;https://github.com/resources/articles/ai/software-development-with-retrieval-augmentation-generation-rag&quot;&gt;16&lt;/a&gt;。对于编码代理来说，这本“书”就是目标代码库。然而，将为自然语言文本设计的标准 RAG 技术应用于源代码是注定要失败的。&lt;/p&gt;
&lt;p&gt;最常见的分块策略，如固定大小分块（每 N 个字符分割文本）或递归字符分块（按段落、句子、单词分割），从根本上不适合代码的高度结构化特性&lt;a href=&quot;https://medium.com/@jouryjc0409/ast-enables-code-rag-models-to-overcome-traditional-chunking-limitations-b0bc1e61bdab&quot;&gt;17&lt;/a&gt;, &lt;a href=&quot;https://www.ibm.com/think/tutorials/chunking-strategies-for-rag-with-langchain-watsonx-ai&quot;&gt;18&lt;/a&gt;。这些方法对编程语言的句法结构一无所知。固定大小的分块器可能会将一个函数定义一分为二，将函数签名与其主体分开。它可能会破坏一个类定义、一个循环，甚至一个简单的条件语句。当这些支离破碎、句法上无意义的片段被检索并作为上下文提供给 LLM 时，它们提供了一个被破坏和令人困惑的代码库视图。这会导致幻觉、不正确的代码生成，以及代理普遍无法有效推理项目架构&lt;a href=&quot;https://medium.com/@jouryjc0409/ast-enables-code-rag-models-to-overcome-traditional-chunking-limitations-b0bc1e61bdab&quot;&gt;17&lt;/a&gt;, &lt;a href=&quot;https://arxiv.org/html/2506.15655v1&quot;&gt;19&lt;/a&gt;。要构建一个真正具备代码库感知的代理，我们必须超越这些天真的方法，采用一种尊重源代码结构完整性的方法。&lt;/p&gt;
&lt;h3&gt;3.2 使用抽象语法树（AST）进行智能代码分块&lt;/h3&gt;
&lt;p&gt;解决代码分块问题的方案在于使用一种本身就能理解代码结构的表示方法：&lt;strong&gt;抽象语法树（AST）&lt;/strong&gt;。AST 是由解析器生成的源代码的层次化、树状表示。树中的每个节点代表代码中的一个构造，如函数定义、类声明、导入语句或循环&lt;a href=&quot;https://medium.com/@jouryjc0409/ast-enables-code-rag-models-to-overcome-traditional-chunking-limitations-b0bc1e61bdab&quot;&gt;17&lt;/a&gt;, &lt;a href=&quot;https://www.unite.ai/code-embedding-a-comprehensive-guide/&quot;&gt;20&lt;/a&gt;。通过操作 AST 而不是原始文本，我们可以设计出一种保留代码单元语义和句法完整性的分块策略。&lt;/p&gt;
&lt;p&gt;我们的实现将基于 &lt;code&gt;cAST&lt;/code&gt;（通过抽象语法树进行分块）方法论，该方法论已被证明在代码 RAG 管道中非常有效&lt;a href=&quot;https://medium.com/@jouryjc0409/ast-enables-code-rag-models-to-overcome-traditional-chunking-limitations-b0bc1e61bdab&quot;&gt;17&lt;/a&gt;, &lt;a href=&quot;https://arxiv.org/html/2506.15655v1&quot;&gt;19&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;AST 解析工具&lt;/h4&gt;
&lt;p&gt;为了将各种语言的代码解析成 AST，我们将使用 &lt;code&gt;tree-sitter&lt;/code&gt;。&lt;code&gt;tree-sitter&lt;/code&gt; 是一个增量解析库，它速度快、健壮，并通过一个可插拔的语法系统支持大量编程语言&lt;a href=&quot;https://medium.com/@shreshthg30/a-beginners-guide-to-tree-sitter-6698f2696b48&quot;&gt;21&lt;/a&gt;, &lt;a href=&quot;https://tree-sitter.github.io/tree-sitter/using-parsers/&quot;&gt;22&lt;/a&gt;。我们将使用其官方 Python 绑定将其集成到我们的 RAG 管道中。&lt;/p&gt;
&lt;h4&gt;“先分割后合并”的 AST 分块算法&lt;/h4&gt;
&lt;p&gt;我们分块逻辑的核心将是一个递归的“先分割后合并”算法，该算法遍历 AST。目标是创建尽可能大且信息密集的块，同时不超过预定义的大小限制，并尊重 AST 定义的句法边界&lt;a href=&quot;https://medium.com/@jouryjc0409/ast-enables-code-rag-models-to-overcome-traditional-chunking-limitations-b0bc1e61bdab&quot;&gt;17&lt;/a&gt;, &lt;a href=&quot;https://arxiv.org/html/2506.15655v1&quot;&gt;19&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;该算法的步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;解析：&lt;/strong&gt; 首先使用 &lt;code&gt;tree-sitter&lt;/code&gt; 将源代码文件解析成一个完整的 AST。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自顶向下遍历（分割）：&lt;/strong&gt; 算法从 AST 的根节点开始，自顶向下遍历。对于每个节点（例如，一个类定义），它检查整个节点的文本内容是否在我们的最大块大小之内。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;递归分解：&lt;/strong&gt; 如果一个节点太大而无法放入单个块中，算法不会简单地将其分割。相反，它会递归地下降到该节点的子节点（例如，类中的各个方法），并对它们应用相同的逻辑。这确保我们总是尝试将尽可能大的完整句法单元保持在一起。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;贪婪兄弟节点合并（合并）：&lt;/strong&gt; 在递归分割过程之后，我们可能会剩下许多小的、相邻的节点（例如，多个导入语句或一系列简单的函数）。为了最大化信息密度，算法随后执行一个贪婪的合并步骤，将相邻的兄弟节点组合成一个块，只要它们的组合大小不超过限制。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;块大小度量&lt;/h4&gt;
&lt;p&gt;此过程中的一个关键细节是我们如何衡量块的大小。按行数衡量是不可靠的，因为两个行数相同的代码段由于格式和空白的不同，其实际内容量可能大相径庭。一个更健壮和一致的度量标准是&lt;strong&gt;非空白字符数&lt;/strong&gt;。这确保我们的块大小预算反映了代码内容的实际密度，使其在不同的文件、语言和编码风格之间具有可比性&lt;a href=&quot;https://medium.com/@jouryjc0409/ast-enables-code-rag-models-to-overcome-traditional-chunking-limitations-b0bc1e61bdab&quot;&gt;17&lt;/a&gt;, &lt;a href=&quot;https://arxiv.org/html/2506.15655v1&quot;&gt;19&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;使用 &lt;code&gt;astchunk&lt;/code&gt; 的实际实现&lt;/h4&gt;
&lt;p&gt;虽然可以使用 &lt;code&gt;tree-sitter&lt;/code&gt; 库从头开始实现此算法，但有几个开源库提供了预构建的解决方案。&lt;code&gt;astchunk&lt;/code&gt; 库是实现 &lt;code&gt;cAST&lt;/code&gt; 方法论的一个典型例子&lt;a href=&quot;https://github.com/yilinjz/astchunk&quot;&gt;23&lt;/a&gt;。以下 Python 代码演示了如何使用它来智能地分块源代码文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from astchunk import ASTChunkBuilder

# 示例 Python 源代码
source_code = &quot;&quot;&quot;
import os
import sys

class DataProcessor:
    def __init__(self, data):
        self.data = data

    def process(self):
        # 一个非常复杂的处理步骤
        processed_data = [item * 2 for item in self.data]
        return processed_data

def main():
    processor = DataProcessor()
    result = processor.process()
    print(f&quot;Result: {result}&quot;)

if __name__ == &quot;__main__&quot;:
    main()
&quot;&quot;&quot;

# 初始化基于 AST 的分块构建器
# 我们设置一个小的块大小来演示分割逻辑
configs = {
    &quot;max_chunk_size&quot;: 150,  # 最大非空白字符数
    &quot;language&quot;: &quot;python&quot;,
    &quot;metadata_template&quot;: &quot;default&quot;
}
chunk_builder = ASTChunkBuilder(**configs)

# 生成语法感知的块
chunks = chunk_builder.chunkify(source_code)

# 显示结果块
for i, chunk in enumerate(chunks):
    print(f&quot;--- Chunk {i+1} ---&quot;)
    print(chunk[&apos;content&apos;])
    print(f&quot;Metadata: {chunk[&apos;metadata&apos;]}&quot;)
    print(&quot;-&quot; * 20)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行此代码可能会产生三个不同的块：一个用于导入语句，一个用于 &lt;code&gt;DataProcessor&lt;/code&gt; 类，一个用于 &lt;code&gt;main&lt;/code&gt; 函数及其执行块，这演示了句法结构是如何被保留的。&lt;/p&gt;
&lt;h3&gt;3.3 代码优化的嵌入和向量检索&lt;/h3&gt;
&lt;p&gt;一旦代码库被智能地分块，RAG 管道的下一步就是将这些块转换为数值表示（嵌入），以便进行索引以实现高效的相似性搜索。正如天真的分块对代码无效一样，通用的文本嵌入模型也是次优的。主要在自然语言文本上训练的模型不能充分捕捉源代码中存在的独特语义关系&lt;a href=&quot;https://www.unite.ai/code-embedding-a-comprehensive-guide/&quot;&gt;20&lt;/a&gt;, &lt;a href=&quot;https://modal.com/blog/6-best-code-embedding-models-compared&quot;&gt;24&lt;/a&gt;。例如，在通用文本模型中，令牌 &lt;code&gt;request&lt;/code&gt; 可能在语义上接近 &lt;code&gt;plea&lt;/code&gt; 或 &lt;code&gt;inquiry&lt;/code&gt;。在代码感知模型中，它应该更接近 &lt;code&gt;response&lt;/code&gt;、&lt;code&gt;API&lt;/code&gt; 或 &lt;code&gt;HTTP&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;因此，使用一个专门在大量源代码语料库上训练的嵌入模型至关重要。这些模型学习的向量表示能够理解算法相似性、库使用模式和语言语法等概念&lt;a href=&quot;https://modal.com/blog/6-best-code-embedding-models-compared&quot;&gt;24&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;选择代码嵌入模型&lt;/h4&gt;
&lt;p&gt;代码嵌入模型领域正在迅速发展。在为我们的“迷你版 Claude”选择模型时，应考虑几个因素：在代码检索基准上的性能、上下文长度、嵌入维度和可访问性（即，专有 API 与开源、可自托管的模型）。下表根据最近的分析，比较了当今几种领先的模型&lt;a href=&quot;https://modal.com/blog/6-best-code-embedding-models-compared&quot;&gt;24&lt;/a&gt;。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模型名称&lt;/th&gt;
&lt;th&gt;上下文长度&lt;/th&gt;
&lt;th&gt;输出维度&lt;/th&gt;
&lt;th&gt;关键特性&lt;/th&gt;
&lt;th&gt;访问方式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;VoyageCode-2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;32,768 令牌&lt;/td&gt;
&lt;td&gt;256 至 2048&lt;/td&gt;
&lt;td&gt;专为代码设计；在超过300种语言的数万亿令牌上训练。&lt;/td&gt;
&lt;td&gt;Voyage AI API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenAI text-embedding-3-large&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8,191 令牌&lt;/td&gt;
&lt;td&gt;3072&lt;/td&gt;
&lt;td&gt;顶级的通用模型，在代码任务上表现非常出色。&lt;/td&gt;
&lt;td&gt;OpenAI API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Jina Code Embeddings V2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8,192 令牌&lt;/td&gt;
&lt;td&gt;1.37亿参数&lt;/td&gt;
&lt;td&gt;开源，推理速度快，为代码相似性和搜索任务优化。&lt;/td&gt;
&lt;td&gt;Hugging Face / 自托管&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Nomic Embed Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2,048 令牌&lt;/td&gt;
&lt;td&gt;70亿参数&lt;/td&gt;
&lt;td&gt;完全开源的模型、数据和训练代码；出色的检索性能。&lt;/td&gt;
&lt;td&gt;Hugging Face / 自托管&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;对于寻求最大控制和隐私的开发者来说，像 Jina Code V2 或 Nomic Embed Code 这样的开源模型将是绝佳选择。对于那些优先考虑尖端性能和易用性的人来说，来自 Voyage AI 或 OpenAI 的基于 API 的模型是强有力的竞争者。&lt;/p&gt;
&lt;h4&gt;索引和检索实现&lt;/h4&gt;
&lt;p&gt;最后一步是实现数据注入和检索工作流 &lt;a href=&quot;https://github.com/resources/articles/ai/software-development-with-retrieval-augmentation-generation-rag&quot;&gt;16&lt;/a&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据注入脚本：&lt;/strong&gt; 将创建一个 Python 脚本来协调索引过程。该脚本将：
&lt;ul&gt;
&lt;li&gt;遍历用户提供的目标项目目录。&lt;/li&gt;
&lt;li&gt;对于每个支持的源文件（例如，&lt;code&gt;.py&lt;/code&gt;、&lt;code&gt;.js&lt;/code&gt;、&lt;code&gt;.java&lt;/code&gt;），读取其内容。&lt;/li&gt;
&lt;li&gt;使用我们的 AST 分块器将文件分割成语义完整的块。&lt;/li&gt;
&lt;li&gt;对于每个块，调用所选的嵌入模型（通过 API 或本地实例）以生成其向量嵌入。&lt;/li&gt;
&lt;li&gt;将块的文本内容及其对应的向量嵌入存储在向量数据库中。对于本地开发，像 FAISS 或 Chroma 这样的库是绝佳选择。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检索函数：&lt;/strong&gt; 将定义一个函数作为我们 RAG 工具的核心。该函数将：
&lt;ul&gt;
&lt;li&gt;接受一个自然语言查询（例如，用户的问题或代理计划中的一个步骤）。&lt;/li&gt;
&lt;li&gt;使用相同的代码嵌入模型为该查询生成一个嵌入。&lt;/li&gt;
&lt;li&gt;查询向量数据库，以根据余弦相似度找到 top-k 个最相似的代码块。&lt;/li&gt;
&lt;li&gt;将这些检索到的块的文本内容连接成一个单一的字符串。&lt;/li&gt;
&lt;li&gt;这个上下文信息字符串随后被直接注入到发送给代理主 LLM 的提示中，为其提供执行任务所需的具体、相关知识。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;第四部分：引擎室：实现规划器-执行器和工具集&lt;/h2&gt;
&lt;p&gt;本部分在第二部分建立的 LangGraph 框架内构建代理的核心操作逻辑。我们将实现规划器、执行器和重新规划器节点，并为代理定义一套基础工具，以便与环境交互。&lt;/p&gt;
&lt;h3&gt;4.1 实现图节点&lt;/h3&gt;
&lt;p&gt;我们代理的逻辑被定义为一个状态图，其中每个节点都是一个可以修改代理状态的函数或可运行对象。这些节点的实现将紧密遵循官方 LangGraph 规划与执行教程中展示的健壮模式&lt;a href=&quot;https://langchain-ai.github.io/langgraph/tutorials/plan-and-execute/plan-and-execute/&quot;&gt;15&lt;/a&gt;, &lt;a href=&quot;https://www.youtube.com/watch?v=vpD9kf5Xwo0&amp;amp;vl=en&quot;&gt;25&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;规划器节点&lt;/h4&gt;
&lt;p&gt;规划器是代理的战略核心。其作用是接收用户的高层目标，并将其分解为一个具体的、分步的计划。为确保输出结构化且可靠，我们将使用 LLM 的函数调用或结构化输出功能，强制其返回符合预定义 Pydantic 模式的计划。&lt;/p&gt;
&lt;p&gt;首先，我们定义 &lt;code&gt;Plan&lt;/code&gt; 模式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pydantic import BaseModel, Field
from typing import List

class Plan(BaseModel):
    &quot;&quot;&quot;一个结构化的、分步的计划，以实现用户的目标。&quot;&quot;&quot;
    steps: List[str] = Field(
        description=&quot;为解决任务而按正确顺序列出的离散、可操作的步骤列表。&quot;
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，我们创建规划器本身，它将一个提示模板与一个配置为使用 &lt;code&gt;Plan&lt;/code&gt; 模式进行输出的 LLM 结合起来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

planner_prompt = ChatPromptTemplate.from_template(
    &quot;&quot;&quot;对于给定的目标，创建一个简单的、分步的计划。
这个计划应由独立的、可执行的任务组成，如果正确执行，将导致预期的结果。
不要包含任何不必要的步骤。最后一步的结果应为最终答案。

目标: {objective}
&quot;&quot;&quot;
)

# 使用一个强大的模型进行规划
planner_llm = ChatOpenAI(model=&quot;gpt-4o&quot;, temperature=0)
planner = planner_prompt | planner_llm.with_structured_output(Plan)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，我们图的 &lt;code&gt;planner_node&lt;/code&gt; 函数只需用状态中的用户输入调用此链：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def planner_node(state: AgentState):
    plan_result = await planner.ainvoke({&quot;objective&quot;: state[&quot;input&quot;]})
    return {&quot;plan&quot;: plan_result.steps}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;执行器节点&lt;/h4&gt;
&lt;p&gt;执行器的任务是获取状态中的当前计划，执行&lt;em&gt;下一个&lt;/em&gt;步骤，并记录结果。一个非常有效的模式是将执行器本身实现为一个更简单的“微代理”。这创建了一个层次化的控制结构：主图充当项目经理，而执行器是派去处理一个特定任务的专业工人。这种模块化使得整个系统更容易理解和调试。管理者不需要知道例如“重构一个函数”所需的复杂工具调用序列；它只需要知道该步骤是成功还是失败&lt;a href=&quot;https://minusx.ai/blog/decoding-claude-code/&quot;&gt;2&lt;/a&gt;, &lt;a href=&quot;https://arxiv.org/html/2503.09572v2&quot;&gt;11&lt;/a&gt;, &lt;a href=&quot;https://langchain-ai.github.io/langgraph/tutorials/plan-and-execute/plan-and-execute/&quot;&gt;15&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;我们将使用 LangGraph 预构建的 &lt;code&gt;create_react_agent&lt;/code&gt; 来创建这个执行器微代理。它将配备下一小节中定义的工具。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from langgraph.prebuilt import create_react_agent

# 工具将在 4.2 节中定义
# 如果需要，executor_llm 可以是一个更小、更快的模型
executor_agent = create_react_agent(executor_llm, tools)

async def executor_node(state: AgentState):
    # 获取要执行的下一步
    step = state[&quot;plan&quot;]
    
    # 使用 ReAct 代理执行该步骤
    result = await executor_agent.ainvoke({&quot;messages&quot;: [(&quot;user&quot;, step)]})
    
    # ReAct 代理的结果通常在最后一条消息中
    result_text = result[&apos;messages&apos;][-1].content
    
    # 用已完成的步骤及其结果更新状态
    return {
        &quot;past_steps&quot;: [(step, result_text)],
        &quot;plan&quot;: state[&quot;plan&quot;][1:] # 移除已完成的步骤
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;重新规划器节点&lt;/h4&gt;
&lt;p&gt;每执行一个步骤后，都会调用重新规划器节点以提供一个关键的反馈循环。它评估到目前为止取得的进展，并决定下一步的行动方案。它可以决定任务已完成并制定最终响应，或者更新计划以继续工作。这使得代理能够在步骤失败或初始计划不足时进行自我纠正。&lt;/p&gt;
&lt;p&gt;与规划器类似，重新规划器将使用一个结构化输出模式 &lt;code&gt;Act&lt;/code&gt;，它可以包含最终的 &lt;code&gt;Response&lt;/code&gt; 或一个新的 &lt;code&gt;Plan&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from typing import Union

class Response(BaseModel):
    &quot;&quot;&quot;给用户的最终响应。&quot;&quot;&quot;
    response: str

class Act(BaseModel):
    &quot;&quot;&quot;下一步要采取的行动。&quot;&quot;&quot;
    action: Union[Response, Plan] = Field(
        description=&quot;要执行的行动。使用 &apos;Response&apos; 回复用户，或使用 &apos;Plan&apos; 继续执行新计划。&quot;
    )

replanner_prompt = ChatPromptTemplate.from_template(
    &quot;&quot;&quot;你是一位专家项目经理。你的角色是评估任务的进展并决定下一步的行动。
    
原始目标: {input}
原始计划: {original_plan}
已采取的步骤和结果: {past_steps}

根据进展，用剩余的步骤更新计划。如果目标已完成，请向用户提供最终响应。
只包括仍然需要完成的步骤。
&quot;&quot;&quot;
)

replanner_llm = ChatOpenAI(model=&quot;gpt-4o&quot;, temperature=0)
replanner = replanner_prompt | replanner_llm.with_structured_output(Act)

async def replanner_node(state: AgentState):
    output = await replanner.ainvoke(state)
    if isinstance(output.action, Response):
        return {&quot;response&quot;: output.action.response}
    else:
        return {&quot;plan&quot;: output.action.steps}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;条件边&lt;/h4&gt;
&lt;p&gt;图逻辑的最后一部分是条件边，它在重新规划器行动后指导流程。这个简单的函数检查状态，如果最终响应可用，则将代理路由到 &lt;code&gt;END&lt;/code&gt;，否则返回到 &lt;code&gt;executor_node&lt;/code&gt; 以继续执行新计划。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from langgraph.graph import END

def should_continue(state: AgentState):
    if state.get(&quot;response&quot;):
        return END
    else:
        return &quot;executor_node&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 设计代理的工具箱&lt;/h3&gt;
&lt;p&gt;工具是代理与世界交互的接口，使其能够执行超越简单生成文本的行动。受 Claude Code 中观察到的分层工具设计（低、中、高级）的启发，我们将定义一套最小但功能强大的基础工具集&lt;a href=&quot;https://minusx.ai/blog/decoding-claude-code/&quot;&gt;2&lt;/a&gt;。每个工具都实现为一个 Python 函数，并用 LangChain 的 &lt;code&gt;@tool&lt;/code&gt; 装饰器进行装饰，该装饰器会自动为 LLM 生成一个模式和描述。&lt;/p&gt;
&lt;h4&gt;文件 I/O 工具&lt;/h4&gt;
&lt;p&gt;这些工具允许代理在项目目录中读取和写入文件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from langchain_core.tools import tool
import os

@tool
def read_file(path: str) -&amp;gt; str:
    &quot;&quot;&quot;读取指定路径下文件的全部内容。&quot;&quot;&quot;
    try:
        with open(path, &apos;r&apos;, encoding=&apos;utf-8&apos;) as f:
            return f.read()
    except Exception as e:
        return f&quot;读取文件时出错: {e}&quot;

@tool
def write_file(path: str, content: str) -&amp;gt; str:
    &quot;&quot;&quot;将给定内容写入指定路径的文件，如果文件存在则覆盖。&quot;&quot;&quot;
    try:
        os.makedirs(os.path.dirname(path), exist_ok=True)
        with open(path, &apos;w&apos;, encoding=&apos;utf-8&apos;) as f:
            f.write(content)
        return f&quot;成功写入 {path}&quot;
    except Exception as e:
        return f&quot;写入文件时出错: {e}&quot;

# 一个更高级的工具，使用 LLM 进行原地编辑
@tool
def edit_file(path: str, instructions: str) -&amp;gt; str:
    &quot;&quot;&quot;读取一个文件，根据自然语言指令应用编辑，然后写回。&quot;&quot;&quot;
    try:
        original_content = read_file.invoke(path)
        if &quot;Error&quot; in original_content:
            return original_content
            
        edit_prompt = ChatPromptTemplate.from_template(
            &quot;根据提供的代码应用以下编辑指令。\n&quot;
            &quot;只输出完整的、修改后的代码。不要添加任何评论或 markdown 格式。\n\n&quot;
            &quot;指令: {instructions}\n\n&quot;
            &quot;原始代码:\n```\n{code}\n```&quot;
        )
        
        editor_llm = ChatOpenAI(model=&quot;claude-3-5-sonnet-20240620&quot;, temperature=0)
        editor_chain = edit_prompt | editor_llm
        
        edited_content = editor_chain.invoke({
            &quot;instructions&quot;: instructions,
            &quot;code&quot;: original_content
        }).content
        
        return write_file.invoke(path, edited_content)
    except Exception as e:
        return f&quot;编辑文件时出错: {e}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;文件系统工具&lt;/h4&gt;
&lt;p&gt;这些工具提供基本的文件系统导航功能。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from typing import List

@tool
def list_files(path: str = &apos;.&apos;) -&amp;gt; List[str]:
    &quot;&quot;&quot;列出指定路径下的所有文件和目录。&quot;&quot;&quot;
    try:
        return os.listdir(path)
    except Exception as e:
        return f&quot;列出文件时出错: {e}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;执行工具&lt;/h4&gt;
&lt;p&gt;这是最强大且可能最危险的工具，允许代理执行任意的 shell 命令。其实现对于运行测试或构建脚本等功能至关重要。&lt;strong&gt;此工具的安全实现将是第五部分的唯一重点。&lt;/strong&gt; 目前，我们定义其接口，以便可以将其包含在提供给执行器代理的工具集中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@tool
def execute_bash(command: str) -&amp;gt; str:
    &quot;&quot;&quot;在一个安全的、沙箱化的环境中执行一个 shell 命令，并返回其 stdout 和 stderr。&quot;&quot;&quot;
    # 完整的、安全的实现将在第五部分详细介绍。
    # 这只是接口的占位符。
    return &quot;此工具将在沙箱化部分实现。&quot;

# 执行器代理的最终工具列表
tools = [read_file, write_file, edit_file, list_files, execute_bash]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第五部分：安全网：使用 Docker 沙箱进行安全代码执行&lt;/h2&gt;
&lt;p&gt;本部分解决了代码生成代理最关键的安全问题：运行不受信任的、AI 生成的代码。我们将提供一个完整、实用的指南，用于构建和集成使用 Docker 的沙箱化执行环境。&lt;/p&gt;
&lt;h3&gt;5.1 沙箱化的必要性&lt;/h3&gt;
&lt;p&gt;授予 AI 代理在主机上执行任意代码或 shell 命令的能力存在巨大的安全风险。没有适当的隔离，可能会出现许多危险情况 &lt;a href=&quot;https://anukriti-ranjan.medium.com/building-a-sandboxed-environment-for-ai-generated-code-execution-e1351301268a&quot;&gt;9&lt;/a&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;恶意代码注入：&lt;/strong&gt; 代理可能被提示生成并执行删除文件、窃取敏感数据或安装恶意软件的代码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源耗尽：&lt;/strong&gt; 生成的代码中的一个 bug 可能导致无限循环或内存泄漏，消耗所有可用的 CPU 和内存，并可能导致主机系统崩溃。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;意外的系统访问：&lt;/strong&gt; 代理可能执行读取敏感系统文件（例如，&lt;code&gt;/etc/passwd&lt;/code&gt;）、访问私有网络资源或修改系统配置的命令。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;试图通过简单的 Python &lt;code&gt;exec&lt;/code&gt; 函数包装器来减轻这些风险是不足够且危险的。唯一健壮的、行业标准的解决方案是在&lt;strong&gt;沙箱化环境&lt;/strong&gt;中执行所有不受信任的代码。容器化技术，特别是 Docker，提供了必要的进程级隔离、资源控制和依赖管理，以创建一个安全的执行环境&lt;a href=&quot;https://anukriti-ranjan.medium.com/building-a-sandboxed-environment-for-ai-generated-code-execution-e1351301268a&quot;&gt;9&lt;/a&gt;, &lt;a href=&quot;https://github.com/substratusai/sandboxai&quot;&gt;26&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;5.2 构建 Docker 执行环境&lt;/h3&gt;
&lt;p&gt;我们的沙箱将是一个从专用 &lt;code&gt;Dockerfile&lt;/code&gt; 构建的 Docker 容器。该文件定义了一个一致、可复现的代码执行环境。它将基于一个轻量级的 Python 镜像，并预装 AI 生成的代码可能需要的常用库，从而避免了危险的运行时包安装需求。&lt;/p&gt;
&lt;p&gt;以下是我们的执行环境的一个最小但功能齐全的 &lt;code&gt;Dockerfile&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 使用一个轻量级的、官方的 Python 基础镜像
FROM python:3.11-slim

# 在容器内设置一个工作目录
WORKDIR /workspace

# 安装用于数据科学和网络请求的常用 Python 库
# 这避免了代理在运行时使用 pip install 的需要
RUN pip install --no-cache-dir pandas numpy requests beautifulsoup4

# 创建一个非 root 用户以增加安全性
RUN useradd --create-home --shell /bin/bash appuser
USER appuser

# 容器将启动并等待命令
CMD [&quot;tail&quot;, &quot;-f&quot;, &quot;/dev/null&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 &lt;code&gt;Dockerfile&lt;/code&gt; 建立了一些关键的安全性和可用性特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它使用一个 &lt;code&gt;slim&lt;/code&gt; Python 镜像以减小体积。&lt;/li&gt;
&lt;li&gt;它预装了一组受信任的、常用的库。&lt;/li&gt;
&lt;li&gt;它创建了一个非 root 用户 &lt;code&gt;appuser&lt;/code&gt; 来运行代码，遵循最小权限原则。&lt;/li&gt;
&lt;li&gt;它将 &lt;code&gt;/workspace&lt;/code&gt; 设置为工作目录，我们将把用户的项目挂载到该目录中。&lt;/li&gt;
&lt;li&gt;默认的 &lt;code&gt;CMD&lt;/code&gt; 使容器无限期运行，以便我们可以在其中执行命令。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;要构建此镜像，用户将在包含 &lt;code&gt;Dockerfile&lt;/code&gt; 的目录中运行 &lt;code&gt;docker build -t mini-claude-sandbox.&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;5.3 使用 Docker Python SDK 进行程序化控制&lt;/h3&gt;
&lt;p&gt;构建了沙箱镜像后，我们现在可以实现 4.2 节中定义的 &lt;code&gt;execute_bash&lt;/code&gt; 工具。此实现将使用 &lt;code&gt;docker-py&lt;/code&gt; Python SDK 来以编程方式管理容器并在其中执行命令，捕获输出以返回给代理&lt;a href=&quot;https://docker-py.readthedocs.io/&quot;&gt;27&lt;/a&gt;, &lt;a href=&quot;https://leftasexercise.com/2018/07/09/controlling-docker-container-with-python/&quot;&gt;28&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;该工具的逻辑如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;初始化 Docker 客户端：&lt;/strong&gt; 创建一个客户端对象以与 Docker 守护进程交互。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查找或启动沙箱容器：&lt;/strong&gt; 检查名为 &lt;code&gt;mini-claude-sandbox-instance&lt;/code&gt; 的容器是否已在运行。如果没有，则从我们的 &lt;code&gt;mini-claude-sandbox&lt;/code&gt; 镜像启动一个新的。启动容器时，将用户的当前项目目录挂载到容器的 &lt;code&gt;/workspace&lt;/code&gt; 目录中至关重要。这使代理能够以受控的方式访问项目文件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行命令：&lt;/strong&gt; 使用 &lt;code&gt;exec_run&lt;/code&gt; 方法在正在运行的容器内执行所需的命令。此方法是安全的，因为命令在容器的隔离环境中运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;捕获并返回输出：&lt;/strong&gt; &lt;code&gt;exec_run&lt;/code&gt; 方法方便地返回命令的退出代码及其合并的 stdout 和 stderr 输出。该工具将解码此输出并将其作为字符串返回给代理，为重新规划步骤提供必要的反馈。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以下是 &lt;code&gt;execute_bash&lt;/code&gt; 工具的完整 Python 实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from langchain_core.tools import tool
import docker
import os

# 为容器名称定义一个常量
SANDBOX_CONTAINER_NAME = &quot;mini-claude-sandbox-instance&quot;
SANDBOX_IMAGE_NAME = &quot;mini-claude-sandbox:latest&quot;

@tool
def execute_bash(command: str) -&amp;gt; str:
    &quot;&quot;&quot;
    在一个安全的、沙箱化的 Docker 环境中执行一个 shell 命令
    并返回其 stdout 和 stderr。用户的当前工作
    目录被挂载为沙箱中的 /workspace。
    &quot;&quot;&quot;
    try:
        client = docker.from_env()

        # 检查沙箱容器是否正在运行
        try:
            container = client.containers.get(SANDBOX_CONTAINER_NAME)
            if container.status != &quot;running&quot;:
                container.start()
        except docker.errors.NotFound:
            # 未找到容器，因此创建并启动它
            print(f&quot;正在启动新的沙箱容器: {SANDBOX_CONTAINER_NAME}&quot;)
            container = client.containers.run(
                SANDBOX_IMAGE_NAME,
                name=SANDBOX_CONTAINER_NAME,
                detach=True,
                volumes={os.getcwd(): {&apos;bind&apos;: &apos;/workspace&apos;, &apos;mode&apos;: &apos;rw&apos;}},
                working_dir=&quot;/workspace&quot;,
                tty=True, # 保持容器存活
            )

        # 以非 root 用户身份在容器内执行命令
        exit_code, output = container.exec_run(
            command,
            user=&quot;appuser&quot;
        )
        
        decoded_output = output.decode(&apos;utf-8&apos;)

        if exit_code == 0:
            return f&quot;命令成功执行:\n{decoded_output}&quot;
        else:
            return f&quot;命令执行失败，退出代码 {exit_code}:\n{decoded_output}&quot;

    except Exception as e:
        return f&quot;尝试在 Docker 中执行命令时发生错误: {e}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此实现为代理提供了一种健壮且安全的方式来与 shell 环境交互，完成了我们代理核心能力的最后一部分。&lt;/p&gt;
&lt;h2&gt;第六部分：组装与操作：让“迷你版 Claude”活起来&lt;/h2&gt;
&lt;p&gt;本最终实现部分将前面各节构建的所有组件——RAG 管道、LangGraph 状态机和沙箱化工具集——集成到一个单一、连贯的应用程序中。我们将提供完整的、可运行的脚本，并通过一个实际示例来演示其操作。&lt;/p&gt;
&lt;h3&gt;6.1 完整的“迷你版 Claude”代理脚本&lt;/h3&gt;
&lt;p&gt;以下脚本将我们之前定义的所有组件整合在一起。它定义了主应用程序逻辑，包括设置 RAG 管道、编译 LangGraph 以及通过命令行使用用户输入运行代理。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# main.py - 完整的“迷你版 Claude”代理

import argparse
import asyncio
from typing import List, Tuple, Annotated
from typing_extensions import TypedDict
import operator

from langchain_core.messages import BaseMessage
from langgraph.graph import StateGraph, END, START

# --- 假设所有先前定义的组件都在单独的文件中并已导入 ---
# from rag_pipeline import setup_rag_retriever
# from agent_nodes import planner_node, executor_node, replanner_node, should_continue
# from tools import tools # 此列表包括安全的 execute_bash

# --- 1. 定义代理状态 ---
class AgentState(TypedDict):
    input: str
    plan: List[str]
    past_steps: Annotated[List[Tuple[str, str]], operator.add]
    response: str
    messages: Annotated[List[BaseMessage], operator.add]

# --- 2. 构建图 ---
def build_agent_graph():
    workflow = StateGraph(AgentState)

    # 添加节点
    workflow.add_node(&quot;planner_node&quot;, planner_node)
    workflow.add_node(&quot;executor_node&quot;, executor_node)
    workflow.add_node(&quot;replanner_node&quot;, replanner_node)

    # 定义边
    workflow.add_edge(START, &quot;planner_node&quot;)
    workflow.add_edge(&quot;planner_node&quot;, &quot;executor_node&quot;)
    workflow.add_edge(&quot;executor_node&quot;, &quot;replanner_node&quot;)
    
    # 添加用于循环或结束的条件边
    workflow.add_conditional_edges(
        &quot;replanner_node&quot;,
        should_continue,
        {
            &quot;executor_node&quot;: &quot;executor_node&quot;,
            END: END
        }
    )

    # 将图编译成可运行对象
    return workflow.compile()

# --- 3. 主应用程序逻辑 ---
async def main():
    parser = argparse.ArgumentParser(description=&quot;迷你版 Claude：一个代码原生 AI 代理&quot;)
    parser.add_argument(&quot;task&quot;, type=str, help=&quot;代理要执行的编码任务。&quot;)
    parser.add_argument(&quot;--project_path&quot;, type=str, default=&quot;.&quot;, help=&quot;项目目录的路径。&quot;)
    args = parser.parse_args()

    print(&quot;--- 初始化迷你版 Claude ---&quot;)
    
    # 在实际应用中，你将在此处设置 RAG 管道
    # 对于此示例，我们将直接传递任务
    # print(f&quot;正在索引项目于: {args.project_path}...&quot;)
    # retriever = setup_rag_retriever(args.project_path)
    
    # 构建并编译代理图
    agent_app = build_agent_graph()

    print(f&quot;--- 正在执行任务: {args.task} ---&quot;)
    
    # 运行代理
    initial_state = {&quot;input&quot;: args.task, &quot;past_steps&quot;: [], &quot;messages&quot;: []}
    
    async for event in agent_app.astream(initial_state):
        for key, value in event.items():
            print(f&quot;--- 事件: 节点 &apos;{key}&apos; ---&quot;)
            print(value)
            print(&quot;-&quot; * 30)

    final_state = list(event.values())
    print(&quot;\n--- 任务完成 ---&quot;)
    print(f&quot;最终响应: {final_state[&apos;response&apos;]}&quot;)

if __name__ == &quot;__main__&quot;:
    asyncio.run(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.2 端到端演练：一个实际的编码任务&lt;/h3&gt;
&lt;p&gt;为了看到“迷你版 Claude”的实际运行情况，让我们追踪它在一个真实的软件开发任务上的执行过程。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;任务：&lt;/strong&gt; “此项目中的 &lt;code&gt;requirements.txt&lt;/code&gt; 文件已过时。请将 &lt;code&gt;pandas&lt;/code&gt; 库更新到最新版本，然后运行测试套件以确保升级不会破坏任何东西。测试使用 &lt;code&gt;pytest&lt;/code&gt; 命令运行。”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;执行追踪：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;初始状态：&lt;/strong&gt; 用户运行 &lt;code&gt;python main.py &quot;The requirements.txt file...&quot;&lt;/code&gt;。初始状态为 &lt;code&gt;{&quot;input&quot;: &quot;The requirements.txt...&quot;, &quot;past_steps&quot;: []}&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;规划器节点输出：&lt;/strong&gt; 图从 &lt;code&gt;planner_node&lt;/code&gt; 开始。规划器 LLM 接收目标并生成一个计划。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;新状态：&lt;/strong&gt; &lt;code&gt;{&quot;plan&quot;: [&quot;1. 读取 requirements.txt 的内容。&quot;, &quot;2. 识别 pandas 的当前版本。&quot;, &quot;3. 将 pandas 的版本更新为最新版本。&quot;, &quot;4. 安装更新后的依赖项。&quot;, &quot;5. 运行测试套件以验证更改。&quot;]}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行器节点（迭代 1）：&lt;/strong&gt; 图移动到 &lt;code&gt;executor_node&lt;/code&gt;。它执行第一步，“读取 requirements.txt 的内容”，并调用 &lt;code&gt;read_file&lt;/code&gt; 工具。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;工具调用：&lt;/strong&gt; &lt;code&gt;read_file(path=&apos;requirements.txt&apos;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工具输出：&lt;/strong&gt; (文件的内容)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新状态：&lt;/strong&gt; &lt;code&gt;{&quot;past_steps&quot;: [(&quot;1. 读取...&quot;, (文件内容))], &quot;plan&quot;: [&quot;2. 识别...&quot;,... ]}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重新规划器节点（迭代 1）：&lt;/strong&gt; 重新规划器看到第一步成功，但计划尚未完成。它将现有计划传递下去。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;新状态：&lt;/strong&gt; (计划保持不变)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路由：&lt;/strong&gt; &lt;code&gt;should_continue&lt;/code&gt; 边路由回 &lt;code&gt;executor_node&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行器节点（迭代 2-5）：&lt;/strong&gt; 代理继续执行计划：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第 2 &amp;amp; 3 步：&lt;/strong&gt; 它使用 &lt;code&gt;edit_file&lt;/code&gt; 工具更新 &lt;code&gt;pandas&lt;/code&gt; 版本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 4 步：&lt;/strong&gt; 它使用 &lt;code&gt;execute_bash&lt;/code&gt; 工具，命令为 &lt;code&gt;pip install -r requirements.txt&lt;/code&gt;。该命令在安全的 Docker 沙箱内运行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第 5 步：&lt;/strong&gt; 它使用 &lt;code&gt;execute_bash&lt;/code&gt; 工具，命令为 &lt;code&gt;pytest&lt;/code&gt;。假设测试通过。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;重新规划器节点（最终）：&lt;/strong&gt; 重新规划器现在看到完整的 &lt;code&gt;past_steps&lt;/code&gt; 历史，包括成功的测试运行。它确定目标已完成。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LLM 输出：&lt;/strong&gt; 重新规划器 LLM 生成一个最终的 &lt;code&gt;Response&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;新状态：&lt;/strong&gt; &lt;code&gt;{&quot;response&quot;: &quot;我已成功将 requirements.txt 中的 pandas 库更新到最新版本，安装了新的依赖项，并确认所有测试都通过了。&quot;}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;END：&lt;/strong&gt; &lt;code&gt;should_continue&lt;/code&gt; 边看到 &lt;code&gt;response&lt;/code&gt; 字段现在已填充，并将图转换到 &lt;code&gt;END&lt;/code&gt; 状态。应用程序打印最终响应并退出。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6.3 交互的最佳实践：向 Claude Code 用户学习&lt;/h3&gt;
&lt;p&gt;代理是一个强大的协作者，但当用户采用某些交互模式时，其效能会最大化。这些最佳实践，源自高级 Claude Code 用户的经验，将代理从一个简单的工具转变为一个真正的结对程序员&lt;a href=&quot;https://www.anthropic.com/engineering/claude-code-best-practices&quot;&gt;1&lt;/a&gt;, &lt;a href=&quot;https://harper.blog/2025/05/08/basic-claude-code/&quot;&gt;29&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;拥抱测试驱动开发（TDD）&lt;/h4&gt;
&lt;p&gt;TDD 是与编码代理协同工作的一种异常强大的工作流。过程不是直接要求代理实现一个功能，而是反过来的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;编写失败的测试：&lt;/strong&gt; 首先，指示代理：“为一个新函数 &lt;code&gt;calculate_premium&lt;/code&gt; 编写一套测试，该函数接受一个用户对象并返回一个价格。测试应覆盖标准用户、管理员用户和有有效订阅的用户的案例。”&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确认失败：&lt;/strong&gt; 告诉代理运行测试并确认它们失败（因为函数尚不存在）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现以通过测试：&lt;/strong&gt; 现在，指示代理：“编写 &lt;code&gt;calculate_premium&lt;/code&gt; 函数的实现，使所有新测试都通过。不要修改测试。”&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这种工作流非常有效，因为它为代理提供了一个清晰、客观且机器可验证的“完成”定义。测试套件充当了所需功能的精确规范，减少了模糊性，并带来了更准确的结果&lt;a href=&quot;https://www.anthropic.com/engineering/claude-code-best-practices&quot;&gt;1&lt;/a&gt;, &lt;a href=&quot;https://harper.blog/2025/05/08/basic-claude-code/&quot;&gt;29&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;使用迭代优化和路线修正&lt;/h4&gt;
&lt;p&gt;应将代理视为协作者，而非无懈可击的神谕。最有效的用户会积极引导代理的过程。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;审查计划：&lt;/strong&gt; 始终检查代理生成的初始计划。如果它看起来有缺陷或效率低下，请在执行开始前立即提供反馈以进行纠正。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中断和重定向：&lt;/strong&gt; 如果在执行过程中，代理似乎走错了路，应用程序应提供一种中断它的机制（模仿 Claude Code 中的‘Escape’键功能）&lt;a href=&quot;https://www.anthropic.com/engineering/claude-code-best-practices&quot;&gt;1&lt;/a&gt;。然后用户可以提供纠正性反馈，重新规划器可以根据此人工输入生成一个新的、更好的计划。代理是一个强大的工具，但开发者必须始终是架构师。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第七部分：结论与未来展望&lt;/h2&gt;
&lt;p&gt;本文为解构像 Claude Code 这样复杂编码代理的核心原则，并重建一个简化但功能齐全的版本“迷你版 Claude”，提供了一份全面的技术蓝图。通过将其架构分解为三大关键支柱——具备代码库感知的 RAG、规划器-执行器控制循环和 Docker 化执行沙箱——我们为构建强大的开发者工具铺设了一条实用的路线图。&lt;/p&gt;
&lt;h3&gt;7.1 关键架构原则总结&lt;/h3&gt;
&lt;p&gt;构建“迷你版 Claude”的历程揭示了为软件开发创建有效 AI 代理的几个基本原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;架构简单性本身就是一种特性：&lt;/strong&gt; 在一个由 LLM 的不确定性主导的领域，复杂性是一种负担。一个简单、可观察的单进程状态机比一个由多个交互代理组成的复杂网络更健壮、更易于调试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提示工程是一项核心开发活动：&lt;/strong&gt; 对于复杂的代理，系统提示不仅仅是一条指令，而是一个详细的软件产物。它必须以与代理代码同等的严谨性进行工程设计，包含明确的算法、启发式规则和安全约束。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上下文必须具备语法感知能力：&lt;/strong&gt; 要想对代码进行推理，代理必须首先理解它。标准的 RAG 技术之所以失败，是因为它们对语法一无所知。基于 AST 的分块方法对于为 LLM 提供一个连贯、语义上有意义的代码库视图至关重要。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分层控制管理复杂性：&lt;/strong&gt; 规划器-执行器模型为处理长周期任务提供了一个健壮的框架。通过将高层战略规划与低层工具执行分离开来，它创建了一个模块化且高效的控制循环，能够自我纠正和适应。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;执行必须沙箱化：&lt;/strong&gt; 执行代码的能力伴随着安全执行的绝对责任。一个安全的、容器化的沙箱不是一个可选功能，而是任何能够编写和运行代码的代理的不可协商的要求。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7.2 高级功能路线图&lt;/h3&gt;
&lt;p&gt;我们设计的“迷你版 Claude”代理是一个强大的基础，但这仅仅是个开始。以下路线图概述了未来发展的几个关键领域，灵感来自完整 Claude Code 产品的高级功能，以进一步增强其能力和实用性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;检查点与状态持久化：&lt;/strong&gt; 对于长时间运行的任务，一个关键特性是能够保存和恢复代理的状态。LangGraph 内置了对检查点机制的支持（例如，&lt;code&gt;InMemorySaver&lt;/code&gt;，或更健壮的数据库支持的保存器）&lt;a href=&quot;https://www.langchain.com/langgraph&quot;&gt;8&lt;/a&gt;。实现这一点将允许代理被停止和重新启动，而不会丢失其在复杂计划上的进度，这是生产级代理中一个备受期待的功能&lt;a href=&quot;https://www.anthropic.com/news/claude-sonnet-4-5&quot;&gt;30&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多模态能力：&lt;/strong&gt; 现代软件开发不仅限于文本。开发者会处理 UI 模型、架构图和错误截图。代理的下一次演进将是集成一个多模态视觉模型。这将启用新的工作流，例如向代理提供一个 UI 截图并指示它“编写实现此设计的 React 组件”，这是 Claude Code 所擅长的能力&lt;a href=&quot;https://www.anthropic.com/engineering/claude-code-best-practices&quot;&gt;1&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IDE 集成：&lt;/strong&gt; 虽然一个终端原生的工具很强大，但更深度地集成到开发者的 IDE 中，例如一个 VS Code 扩展，可以解锁更无缝的用户体验&lt;a href=&quot;https://www.anthropic.com/news/claude-sonnet-4-5&quot;&gt;30&lt;/a&gt;。一个扩展可以提供代理建议更改的可视化差异对比，允许用户选择代码片段作为上下文，并直接从编辑器中触发代理操作，从而进一步减少摩擦。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高级工具与模型上下文协议（MCP）：&lt;/strong&gt; 我们定义的简单工具集可以显著扩展。构建高级工具的一个强大范式是模型上下文协议（MCP），这是一种代理与外部服务交互的标准化方式&lt;a href=&quot;https://docs.claude.com/en/docs/claude-code/overview#:~:text=Claude%20Code%20maintains%20awareness%20of%20your%20entire%20project%20structure%2C%20can%20find&quot;&gt;5&lt;/a&gt;, &lt;a href=&quot;https://medium.com/@elisowski/mcp-explained-the-new-standard-connecting-ai-to-everything-79c5a1c98288&quot;&gt;31&lt;/a&gt;。通过构建或使用 MCP 服务器，代理可以获得直接与 Figma 等服务交互以读取设计规范、与 Google Drive 交互以访问项目文档，甚至通过 Playwright MCP 服务器与正在运行的 Web 应用程序交互以对其自己的代码进行端到端测试和视觉验证的能力&lt;a href=&quot;https://www.anthropic.com/news/claude-sonnet-4-5&quot;&gt;30&lt;/a&gt;, &lt;a href=&quot;https://medium.com/@elisowski/mcp-explained-the-new-standard-connecting-ai-to-everything-79c5a1c98288&quot;&gt;31&lt;/a&gt;。另一个强大的应用是利用 Context7 MCP 服务器，它可以在代理生成代码时动态地注入最新的库文档和代码示例，从而解决 AI 依赖过时训练数据的问题&lt;a href=&quot;https://upstash.com/blog/context7-mcp&quot;&gt;32&lt;/a&gt;。此外，一个最新的突破性进展是 Chrome 开发者工具 MCP 服务器的出现，它允许代理直接在 Chrome 浏览器中调试和验证网页，诊断网络和控制台错误，并分析性能，让代理真正具备了“看见”其代码运行效果的能力&lt;a href=&quot;https://developer.chrome.com/blog/chrome-devtools-mcp?hl=zh-cn&quot;&gt;33&lt;/a&gt;。这指向了一个未来，代理不仅参与编写代码，而且参与软件开发的整个设计、实现和验证生命周期。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;https://www.anthropic.com/engineering/claude-code-best-practices&lt;/li&gt;
&lt;li&gt;https://minusx.ai/blog/decoding-claude-code/&lt;/li&gt;
&lt;li&gt;https://prismic.io/blog/claude-code&lt;/li&gt;
&lt;li&gt;https://rafaelquintanilha.com/is-claude-code-worth-the-hype-or-just-expensive-vibe-coding/&lt;/li&gt;
&lt;li&gt;https://docs.claude.com/en/docs/claude-code/overview#:~:text=Claude%20Code%20maintains%20awareness%20of,conflicts%2C%20and%20write%20release%20notes&lt;/li&gt;
&lt;li&gt;https://www.reddit.com/r/AI_Agents/comments/1jexngk/optimizing_ai_agents_with_opensouce/&lt;/li&gt;
&lt;li&gt;https://python.langchain.com/docs/tutorials/agents/&lt;/li&gt;
&lt;li&gt;https://www.datacamp.com/tutorial/langgraph-agents&lt;/li&gt;
&lt;li&gt;https://anukriti-ranjan.medium.com/building-a-sandboxed-environment-for-ai-generated-code-execution-e1351301268a&lt;/li&gt;
&lt;li&gt;https://blog.langchain.com/planning-agents/&lt;/li&gt;
&lt;li&gt;https://www.promptlayer.com/glossary/plan-and-execute-agents&lt;/li&gt;
&lt;li&gt;https://www.emergentmind.com/topics/planner-executor-architecture-4c9e0097-fe2b-4870-b41c-9519c49a07c8&lt;/li&gt;
&lt;li&gt;https://arxiv.org/html/2503.09572v2&lt;/li&gt;
&lt;li&gt;https://www.youtube.com/watch?v=uRya4zRrRx4&lt;/li&gt;
&lt;li&gt;https://langchain-ai.github.io/langgraph/tutorials/plan-and-execute/plan-and-execute/&lt;/li&gt;
&lt;li&gt;https://www.ibm.com/think/tutorials/chunking-strategies-for-rag-with-langchain-watsonx-ai&lt;/li&gt;
&lt;li&gt;https://medium.com/@jouryjc0409/ast-enables-code-rag-models-to-overcome-traditional-chunking-limitations-b0bc1e61bdab&lt;/li&gt;
&lt;li&gt;https://community.databricks.com/t5/technical-blog/the-ultimate-guide-to-chunking-strategies-for-rag-applications/ba-p/113089&lt;/li&gt;
&lt;li&gt;https://arxiv.org/html/2506.15655v1&lt;/li&gt;
&lt;li&gt;https://www.unite.ai/code-embedding-a-comprehensive-guide/&lt;/li&gt;
&lt;li&gt;https://medium.com/@shreshthg30/a-beginners-guide-to-tree-sitter-6698f2696b48&lt;/li&gt;
&lt;li&gt;https://tree-sitter.github.io/tree-sitter/using-parsers/&lt;/li&gt;
&lt;li&gt;https://github.com/yilinjz/astchunk&lt;/li&gt;
&lt;li&gt;https://modal.com/blog/6-best-code-embedding-models-compared&lt;/li&gt;
&lt;li&gt;https://www.youtube.com/watch?v=vpD9kf5Xwo0&lt;/li&gt;
&lt;li&gt;https://e2b.dev/&lt;/li&gt;
&lt;li&gt;https://docker-py.readthedocs.io/en/stable/containers.html&lt;/li&gt;
&lt;li&gt;https://leftasexercise.com/2018/07/09/controlling-docker-container-with-python/&lt;/li&gt;
&lt;li&gt;https://harper.blog/2025/05/08/basic-claude-code/&lt;/li&gt;
&lt;li&gt;https://www.anthropic.com/news/claude-sonnet-4-5&lt;/li&gt;
&lt;li&gt;https://medium.com/@elisowski/mcp-explained-the-new-standard-connecting-ai-to-everything-79c5a1c98288&lt;/li&gt;
&lt;li&gt;https://upstash.com/blog/context7-mcp&lt;/li&gt;
&lt;li&gt;https://developer.chrome.com/blog/chrome-devtools-mcp?hl=zh-cn&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>OSS 直传：如何构建一套安全高效的文件上传/访问方案</title><link>https://mc.mimeng.top/posts/full-stack/oss-api-design/</link><guid isPermaLink="true">https://mc.mimeng.top/posts/full-stack/oss-api-design/</guid><description>深度解析“客户端预签名 URL 直传”模式，从零开始构建一套完整的文件上传与管理方案，为你的应用带来极致的上传以及访问体验。</description><pubDate>Fri, 12 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;当你需要在 Web 应用中处理大文件上传时，一个经典的问题是：我们应该让文件经过应用服务器中转吗？答案通常是：不。本文将为你揭示一种更优解法——利用 &lt;strong&gt;预签名 URL&lt;/strong&gt; 实现客户端文件直传至对象存储服务（OSS），并提供一套完整的技术流程和管理接口设计。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;:::note[关于 OSS]&lt;/p&gt;
&lt;p&gt;本文中的 OSS 泛指所有对象存储服务，如阿里云 OSS、腾讯云 COS、AWS S3 等。尽管服务商不同，但核心的&lt;strong&gt;预签名 URL 直传&lt;/strong&gt;模式是通用的。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;核心概念&lt;/h2&gt;
&lt;p&gt;在我们深入技术细节之前，有几个核心概念必须先理清楚：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Key&lt;/strong&gt;：文件在 OSS 存储桶中的唯一身份标识，类似于文件在文件系统中的完整路径。为了便于管理，我们通常会采用 &lt;code&gt;年/月/日/UUID.扩展名&lt;/code&gt; 的格式。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Meta&lt;/strong&gt;：一个灵活的 JSON 对象，用于存储文件的附加信息。例如，你可以用它来记录图片的尺寸、文件来源等业务相关数据，这比在数据库中为每个字段创建列要灵活得多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预签名 URL (Pre-signed URL)&lt;/strong&gt;：一个由后端服务器通过 OSS 提供的 SDK 生成的、有时效性的 URL。前端拿到这个 URL 后，可以直接用它来执行特定的操作（如上传、下载），而不需要知道后端敏感的访问密钥。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;方案优势&lt;/h2&gt;
&lt;p&gt;为什么这种“客户端直传”模式如此受欢迎？因为它完美地解决了传统中转模式的痛点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;减轻服务器负担&lt;/strong&gt;：文件数据不经过你的应用服务器，大文件的上传带宽和处理压力完全由 OSS 承担。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高安全性和高可用性&lt;/strong&gt;：直接利用 OSS 服务商提供的成熟、稳定的基础设施，通过预签名 URL 精确控制权限和时效，确保安全。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高扩展性&lt;/strong&gt;：上传并发能力直接由 OSS 支持，具备极强的水平扩展能力。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;文件上传流程 (Create)&lt;/h2&gt;
&lt;p&gt;整个上传流程可以概括为“授权 -&amp;gt; 直传 -&amp;gt; 确认”三步走。&lt;/p&gt;
&lt;h3&gt;1. 前端：请求预签名接口&lt;/h3&gt;
&lt;p&gt;前端在用户选择文件后，不直接上传，而是先向你的后端发送一个轻量级的请求，提供文件的基本信息和元数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /api/v1/uploads/presign
Headers:
Authorization: Bearer &amp;lt;JWT&amp;gt;
Body:
{
  &quot;filename&quot;: &quot;my-photo.jpg&quot;,
  &quot;contentType&quot;: &quot;image/jpeg&quot;,
  &quot;size&quot;: 204800,
  &quot;meta&quot;: {
    &quot;source&quot;: &quot;user\_profile\_avatar&quot;
  }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 后端：生成预签名 URL 并返回&lt;/h3&gt;
&lt;p&gt;后端接收到请求后，会进行必要的验证，然后生成一个用于上传的预签名 URL。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;验证&lt;/strong&gt;：检查用户权限、文件类型、大小等是否合规。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生成 Key&lt;/strong&gt;：根据预设规则（例如 &lt;code&gt;2025/09/12/UUID.jpg&lt;/code&gt;）生成一个唯一的 Key。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调用 OSS SDK&lt;/strong&gt;：使用 Key 和操作类型（&lt;code&gt;PUT&lt;/code&gt;）生成一个有时效的预签名 URL。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;返回&lt;/strong&gt;：将生成的 &lt;code&gt;url&lt;/code&gt; 和 &lt;code&gt;key&lt;/code&gt; 返回给前端。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 200 OK
{
  &quot;url&quot;: &quot;[https://your-bucket.oss-region.aliyuncs.com/2025/09/12/...?Signature=](https://your-bucket.oss-region.aliyuncs.com/2025/09/12/...?Signature=)...&quot;,
  &quot;key&quot;: &quot;/2025/09/12/a1b2c3d4-e5f6-7890-1234-567890abcdef.jpg&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 前端：使用预签名 URL 直传&lt;/h3&gt;
&lt;p&gt;前端拿到 URL 后，直接用 &lt;code&gt;PUT&lt;/code&gt; 方法将文件二进制数据上传到 OSS。这一步&lt;strong&gt;完全绕开了你的应用服务器&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PUT https://your-bucket.oss-region.aliyuncs.com/
Headers:
  Content-Type: image/jpeg
Body:
  &amp;lt;文件二进制数据&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 前端：上传成功确认接口&lt;/h3&gt;
&lt;p&gt;文件成功上传到 OSS 后，前端需要调用后端的确认接口，通知后端将文件记录保存到数据库中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /api/v1/uploads/confirm
Headers:
  Authorization: Bearer &amp;lt;JWT&amp;gt;
Body:
{
  &quot;key&quot;: &quot;/2025/09/12/a1b2c3d4-e5f6-7890-1234-567890abcdef.jpg&quot;,
  &quot;meta&quot;: { ... }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后端接收到请求后，会通过 OSS SDK 校验文件是否存在，如果存在，则将文件信息（Key、元数据、用户ID等）存入数据库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 201 Created
{
  &quot;id&quot;: &quot;db-uuid-abcdef-123456&quot;,
  &quot;key&quot;: &quot;/2025/09/12/a1b2c3d4-e5f6-7890-1234-567890abcdef.jpg&quot;,
  &quot;accessUrl&quot;: &quot;https://cdn.yourdomain.com/2025/09/12/a1b2c3d4-e5f6-7890-1234-567890abcdef.jpg&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;文件管理接口 (CRUD)&lt;/h2&gt;
&lt;p&gt;除了上传，一个完整的文件服务还需要支持读取、更新和删除操作。&lt;/p&gt;
&lt;h3&gt;1. 读取 (Read)&lt;/h3&gt;
&lt;p&gt;要获取文件的信息或访问 URL，可以设计一个通用的读取接口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /api/v1/files/{id}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于私有文件，后端可以返回一个&lt;strong&gt;有时效性的只读预签名 URL&lt;/strong&gt;。然而，如果你的前端代码中已经写死了 OSS 的静态 URL，并且你不想或不能修改前端代码，那么后端直接返回预签名 URL 就不太实用了。这时，可以考虑以下两种方案：&lt;/p&gt;
&lt;h4&gt;方案一：后端代理 / 中转&lt;/h4&gt;
&lt;p&gt;所有文件读取请求都先发到你的后端服务器，由后端验证权限后再去 OSS 获取文件数据，最后返回给浏览器。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：实现简单，安全性高。
&lt;strong&gt;缺点&lt;/strong&gt;：所有流量都经过你的服务器，会增加服务器的带宽和处理压力，可能产生性能瓶颈。&lt;/p&gt;
&lt;h4&gt;方案二：Service Worker 无感认证&lt;/h4&gt;
&lt;p&gt;这是最优雅的解决方案，尤其适用于前端 URL 是静态的场景。&lt;strong&gt;Service Worker&lt;/strong&gt; 作为浏览器中的一个代理，能拦截所有网络请求。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工作原理&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;浏览器发起对静态 OSS URL（如 &lt;code&gt;https://oss.my-domain.com/user-photo.jpg&lt;/code&gt;）的请求。&lt;/li&gt;
&lt;li&gt;Service Worker 捕获该请求，但&lt;strong&gt;不直接放行&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;Service Worker 携带用户的 &lt;strong&gt;JWT&lt;/strong&gt; 向后端请求该文件的预签名 URL。&lt;/li&gt;
&lt;li&gt;后端验证 JWT，生成预签名 URL 并返回给 Service Worker。&lt;/li&gt;
&lt;li&gt;Service Worker 再用这个预签名 URL 去 OSS 真正获取图片数据。&lt;/li&gt;
&lt;li&gt;Service Worker 将获取到的图片数据作为原始请求的响应返回给浏览器。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;优点&lt;/strong&gt;：无需修改前端代码，用户对整个认证过程完全无感知，同时利用了 OSS 的高速 CDN。
&lt;strong&gt;缺点&lt;/strong&gt;：实现复杂度相对较高，需要管理 Service Worker 的生命周期和缓存策略。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;方案&lt;/th&gt;
&lt;th&gt;优点&lt;/th&gt;
&lt;th&gt;缺点&lt;/th&gt;
&lt;th&gt;适用场景&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;后端代理&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;实现简单；安全性高；所有逻辑在后端。&lt;/td&gt;
&lt;td&gt;增加服务器带宽和延迟；所有请求都要经过你的服务器。&lt;/td&gt;
&lt;td&gt;不在意服务器开销和轻微延迟；不具备 Service Worker 开发能力。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service Worker&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;无需修改前端代码；用户无感；利用 OSS CDN，性能好。&lt;/td&gt;
&lt;td&gt;实现相对复杂；需要管理 Service Worker 的生命周期和缓存。&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;前端 URL 静态写死&lt;/strong&gt;；追求高性能和无缝用户体验。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;2. 更新 (Update)&lt;/h3&gt;
&lt;p&gt;OSS 中的文件内容是不可变的。因此，“更新”通常意味着两种情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;更新元数据&lt;/strong&gt;：通过一个 &lt;code&gt;PATCH /api/v1/files/{id}&lt;/code&gt; 接口，只更新数据库中存储的 &lt;code&gt;meta&lt;/code&gt; 字段。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;替换文件&lt;/strong&gt;：这不应该有独立的接口。正确的做法是&lt;strong&gt;复用完整的上传流程&lt;/strong&gt;，上传一个新文件，然后你的业务逻辑（例如更新用户头像）使用新文件的 &lt;code&gt;id&lt;/code&gt; 和 &lt;code&gt;key&lt;/code&gt; 替换旧的。旧文件可以根据业务策略决定是否异步删除。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 删除 (Delete)&lt;/h3&gt;
&lt;p&gt;删除文件需要同步处理 OSS 和数据库记录。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DELETE /api/v1/files/{id}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后端通过 &lt;code&gt;id&lt;/code&gt; 找到 &lt;code&gt;key&lt;/code&gt;，先调用 OSS SDK 删除文件，然后从数据库中删除记录。为了保证数据一致性，这两个操作应该放在一个数据库事务中。&lt;/p&gt;
&lt;h2&gt;数据库设计&lt;/h2&gt;
&lt;p&gt;一个简单的 &lt;code&gt;files&lt;/code&gt; 表足以存储文件信息，我们可以在此基础上进行扩展。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TABLE files (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL,
    key VARCHAR(512) UNIQUE NOT NULL,
    original_filename VARCHAR(255),
    content_type VARCHAR(128),
    size BIGINT,
    meta JSONB,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;采用“客户端预签名 URL 直传”模式，我们构建了一套安全、高效、可扩展的文件上传与管理方案。它不仅减轻了服务器的压力，还为应用带来了流畅的用户体验。通过前后端的分工协作，我们利用了各自的优势：后端负责安全的授权，而前端则直接利用 OSS 的强大能力完成数据传输。希望这份文档能帮助你的团队高效地实现文件上传功能，为未来的业务发展打下坚实的基础。&lt;/p&gt;
</content:encoded></item><item><title>Kubernetes 实战：搭建一个三台服务器组成的 k8s 集群</title><link>https://mc.mimeng.top/posts/backend/build-a-k8s-cluster/</link><guid isPermaLink="true">https://mc.mimeng.top/posts/backend/build-a-k8s-cluster/</guid><description>从零开始构建三节点异地 Kubernetes 集群的实用教程，涵盖网络配置、安全设置与系统调优等，适合初学者和实战演练者参考。</description><pubDate>Wed, 06 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;从零开始构建三节点异地 Kubernetes 集群的实用教程，涵盖网络配置、安全设置与系统调优等，适合初学者和实战演练者参考。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;:::note[关于服务器信息]&lt;/p&gt;
&lt;p&gt;所有服务器均为 ECS 实例，分别位于境内、香港以及美国，由于服务器带宽足够，便于异地组建集群，实际生产环境建议使用同地域不同可用区服务器通过内网进行通信。&lt;br /&gt;
本文省略了所有的镜像源配置过程，若网络环境不允许，请自行配置镜像源。&lt;br /&gt;
&lt;em&gt;文中出现的 IP 并非真实 IP&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;一、服务器配置说明&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;节点&lt;/th&gt;
&lt;th&gt;IP&lt;/th&gt;
&lt;th&gt;操作系统&lt;/th&gt;
&lt;th&gt;配置&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;k8s-master&lt;/td&gt;
&lt;td&gt;1.1.1.1&lt;/td&gt;
&lt;td&gt;Ubuntu 22.04&lt;/td&gt;
&lt;td&gt;16H 16G 40+100G 硬盘&lt;/td&gt;
&lt;td&gt;主节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;k8s-worker1&lt;/td&gt;
&lt;td&gt;2.2.2.2&lt;/td&gt;
&lt;td&gt;Ubuntu 22.04&lt;/td&gt;
&lt;td&gt;16H 16G 40G 硬盘&lt;/td&gt;
&lt;td&gt;子节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;k8s-worker2&lt;/td&gt;
&lt;td&gt;3.3.3.3&lt;/td&gt;
&lt;td&gt;Ubuntu 24.04&lt;/td&gt;
&lt;td&gt;8H 8G 40G 硬盘&lt;/td&gt;
&lt;td&gt;子节点&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;:::important[有关配置操作]&lt;/p&gt;
&lt;p&gt;如果没有特殊说明，三台服务器都需要进行相同的操作，本人使用的是 Termius 的 Broadcast Input 实现三台服务器同时执行命令。&lt;br /&gt;
大部分操作均为 &lt;code&gt;sudo&lt;/code&gt; 操作，如遇到权限问题，请自行在命令前添加 &lt;code&gt;sudo&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;二、配置服务器&lt;/h2&gt;
&lt;h3&gt;1. 配置主机名&lt;/h3&gt;
&lt;p&gt;在三台服务器上分别执行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1.1.1.1
hostnamectl set-hostname k8s-master

# 2.2.2.2
hostnamectl set-hostname k8s-worker1

# 3.3.3.3
hostnamectl set-hostname k8s-worker2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 配置 hosts 解析&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;编辑 &lt;code&gt;/etc/hosts&lt;/code&gt; 文件&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;nano /etc/hosts
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;添加如下内容：&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# Kubernetes Cluster Nodes
1.1.1.1    k8s-master
2.2.2.2    k8s-worker1
3.3.3.3    k8s-worker2
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;测试连通性&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;ping k8s-master
ping k8s-worker1
ping k8s-worker2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果能 &lt;code&gt;ping&lt;/code&gt; 通，说明配置正确。&lt;/p&gt;
&lt;p&gt;:::tip[为什么这样做很重要？]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;简化配置&lt;/strong&gt;：当您使用 &lt;code&gt;kubeadm&lt;/code&gt; 命令初始化集群时，它会使用主机名来生成证书和配置。提前配置好 &lt;code&gt;/etc/hosts&lt;/code&gt; 可以确保所有组件都能够正确地解析和通信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提高稳定性&lt;/strong&gt;：即使 DNS 解析出现问题，或者跨国 DNS 延迟较高，您的集群仍然可以通过 &lt;code&gt;hosts&lt;/code&gt; 文件中的静态映射进行通信，从而减少因网络问题导致的集群不稳定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;便于管理&lt;/strong&gt;：使用主机名（如 &lt;code&gt;k8s-master&lt;/code&gt;）来管理集群比使用难以记忆的 IP 地址更加方便和直观。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;3. 配置免密登录（可选）&lt;/h3&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果后续计划进行自动化管理或维护&lt;/strong&gt;，强烈建议执行这一步。它能为你的后续工作节省大量时间和精力。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在主节点上操作&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;生成密钥对&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t ed25519 -f ~/.ssh/k8s -N &apos;&apos; -q
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;将公钥复制到其他节点&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;ssh-copy-id -i ~/.ssh/k8s.pub mengchen@k8s-worker1
ssh-copy-id -i ~/.ssh/k8s.pub mengchen@k8s-worker2
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;测试连接&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;ssh -i ~/.ssh/k8s mengchen@k8s-worker1
ssh -i ~/.ssh/k8s mengchen@k8s-worker2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果能成功连接到工作节点，说明配置成功。&lt;/p&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;如果你遇到了权限问题，可以尝试 &lt;code&gt;cat k8s.pub&lt;/code&gt; ，手动将公钥复制到工作节点服务器的 &lt;code&gt;authorized_keys&lt;/code&gt; 文件中。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;4. 设置时区（可选）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo timedatectl set-timezone Asia/Shanghai
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 配置内核转发及网桥过滤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;内核转发&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;lt;&amp;lt;EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;网桥过滤&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;#加载br_netfilter模块
modprobe br_netfilter

#查看是否加载
lsmod | grep br_netfilter
br_netfilter           32768  0
bridge                311296  1 br_netfilter

#使配置文件生效
sysctl -p /etc/sysctl.d/k8s.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;内核转发&lt;/strong&gt; 确保了 Pod 之间的&lt;strong&gt;跨节点通信&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网桥过滤&lt;/strong&gt; 确保了 &lt;strong&gt;Service 负载均衡&lt;/strong&gt; 的 &lt;code&gt;iptables&lt;/code&gt; 规则能够正常工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;6. 关闭 Swap&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 临时关闭
swapoff -a

#永远关闭swap分区
sed -i &apos;s/.*swap.*/#&amp;amp;/&apos; /etc/fstab
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[为什么 Kubernetes 需要禁用 Swap？]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;性能&lt;/strong&gt;：Swap 将内存中的数据移动到磁盘上，而磁盘的读写速度远低于内存。这会导致 Pod 的性能显著下降，尤其是在内存资源紧张时。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源管理&lt;/strong&gt;：Kubernetes 的调度器和资源管理依赖于对物理内存的准确报告。如果启用了 Swap，系统会报告一个混合了物理内存和虚拟内存的模糊值，这会影响调度器的决策，导致 Pod 被调度到不适合的节点上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;稳定性&lt;/strong&gt;：内存不足时，Kubernetes 更倾向于直接终止 Pod 以释放资源，而不是将数据交换到 Swap 空间。这样做可以保证集群的稳定性，避免因频繁的磁盘 I/O 导致整个节点响应变慢。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;7. 启用 IPVS&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;加载 IPVS 和 &lt;code&gt;br_netfilter&lt;/code&gt; 模块&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;lt;&amp;lt;EOF | sudo tee /etc/modules-load.d/ipvs.conf
ip_vs
ip_vs_rr
ip_vs_wrr
ip_vs_sh
nf_conntrack
br_netfilter
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;立即加载模块&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;modprobe --all ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh nf_conntrack br_netfilter
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;安装必要的工具&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get update &amp;amp;&amp;amp; sudo apt-get install -y ipvsadm ipset
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;验证模块是否已加载&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;lsmod | grep -e ip_vs -e nf_conntrack -e br_netfilter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只要输出中包含了 &lt;code&gt;ip_vs&lt;/code&gt;、&lt;code&gt;nf_conntrack&lt;/code&gt; 和 &lt;code&gt;br_netfilter&lt;/code&gt; 相关的行，就说明配置成功了。&lt;/p&gt;
&lt;p&gt;:::tip[为什么推荐使用 IPVS？]&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;性能更佳&lt;/strong&gt;：&lt;code&gt;ipvs&lt;/code&gt; 模式在处理大量的 Service 和 Pod 时，性能远超 &lt;code&gt;iptables&lt;/code&gt;。它的流量转发效率更高，对 CPU 资源的消耗更少。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可扩展性更好&lt;/strong&gt;：&lt;code&gt;ipvs&lt;/code&gt; 能够轻松应对 Service 和 Endpoint 数量的增长，非常适合未来的扩展。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更智能的负载均衡算法&lt;/strong&gt;：&lt;code&gt;ipvs&lt;/code&gt; 支持多种负载均衡算法，如轮询、最少连接、源地址哈希等，这让您可以更精细地控制流量分发。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;8. 修改句柄数限制&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;临时修改句柄数&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;ulimit -SHn 65535
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;永久修改句柄数&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;gt;&amp;gt; /etc/security/limits.conf &amp;lt;&amp;lt;EOF
* soft nofile 65536
* hard nofile 131072
* soft nproc 65536
* hard nproc 65536
* soft memlock unlimited
* hard memlock unlimited
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;查看修改结果&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;ulimit -a
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;9. 系统参数调优与模块加载&lt;/h3&gt;
&lt;p&gt;为了确保 Kubernetes 集群在处理高并发网络连接和大规模文件操作时能够稳定高效运行，我们需要对系统内核参数进行更精细的调优，并确保必要的内核模块已加载。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;配置内核参数&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这一步将对系统内核进行优化，以更好地适应 Kubernetes 的运行环境。执行以下命令，将配置写入到 &lt;code&gt;/etc/sysctl.d/k8s_better.conf&lt;/code&gt; 文件中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo tee /etc/sysctl.d/k8s_better.conf &amp;lt;&amp;lt; EOF
net.bridge.bridge-nf-call-iptables=1
net.bridge.bridge-nf-call-ip6tables=1
net.ipv4.ip_forward=1
vm.swappiness=0
vm.overcommit_memory=1
vm.panic_on_oom=0
fs.inotify.max_user_instances=8192
fs.inotify.max_user_watches=1048576
fs.file-max=52706963
fs.nr_open=52706963
net.ipv6.conf.all.disable_ipv6=1
net.netfilter.nf_conntrack_max=2310720
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[为什么要添加这些配置？]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;vm.swappiness=0&lt;/code&gt;&lt;/strong&gt;: 确保系统几乎不使用 &lt;strong&gt;Swap&lt;/strong&gt;，与我们之前禁用的 Swap 保持一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;vm.overcommit_memory=1&lt;/code&gt;&lt;/strong&gt;: 允许内核分配比当前可用内存更多的内存，有助于某些应用在启动时快速分配资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;net.netfilter.nf_conntrack_max&lt;/code&gt;&lt;/strong&gt;: 调高网络连接跟踪的最大数量，防止在高并发时因连接数过多导致网络问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;fs.file-max&lt;/code&gt;&lt;/strong&gt;: 调高系统级别的最大文件句柄数，避免“Too many open files”错误，特别是在大量 Pod 运行的集群中至关重要。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;加载内核模块与使配置生效&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 加载 br_netfilter 模块，确保网桥过滤功能正常
modprobe br_netfilter

# 确认 conntrack 模块是否已加载
lsmod | grep conntrack

# 如果上一步没有输出，则加载 ip_conntrack 模块
modprobe ip_conntrack

# 使新的内核参数配置立即生效
sysctl -p /etc/sysctl.d/k8s_better.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note[关于 conntrack 模块]&lt;/p&gt;
&lt;p&gt;&lt;code&gt;lsmod | grep conntrack&lt;/code&gt; 命令用于检查内核是否已加载网络连接跟踪模块。在较新的系统中，&lt;code&gt;ip_conntrack&lt;/code&gt; 已被 &lt;code&gt;nf_conntrack&lt;/code&gt; 替代，但为了兼容性，如果发现未加载，可以手动加载。在绝大多数情况下，这个模块通常是默认加载的。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;完成以上步骤后，你的服务器已经为运行 Kubernetes 做好了更充分的准备。&lt;/p&gt;
&lt;h2&gt;三、容器运行时工具安装及配置&lt;/h2&gt;
&lt;p&gt;目前，Kubernetes 社区推荐使用符合 CRI 标准的 &lt;strong&gt;Containerd&lt;/strong&gt; 作为容器运行时（而不是 Docker），它更加轻量、高效。&lt;/p&gt;
&lt;h3&gt;安装 Containerd&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get update
sudo apt-get install -y containerd
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置 Containerd&lt;/h3&gt;
&lt;p&gt;配置 Containerd，使其使用 &lt;code&gt;systemd&lt;/code&gt; 作为 cgroup 驱动。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;生成默认配置文件&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;containerd config default | sudo tee /etc/containerd/config.toml &amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;使用 sed 命令替换 cgroup 驱动为 systemd&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sudo sed -i &apos;s/SystemdCgroup = false/SystemdCgroup = true/&apos; /etc/containerd/config.toml
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;重启 Containerd 服务&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart containerd
sudo systemctl enable containerd
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;四、Kubernetes  安装&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;以下内容来自 &lt;a href=&quot;https://kubernetes.io/zh-cn/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#installing-kubeadm-kubelet-and-kubectl&quot;&gt;安装 kubeadm | Kubernetes&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;本节将安装 Kubernetes 的核心组件 &lt;code&gt;kubelet&lt;/code&gt;、&lt;code&gt;kubeadm&lt;/code&gt; 和 &lt;code&gt;kubectl&lt;/code&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;更新 &lt;code&gt;apt&lt;/code&gt; 软件包索引并安装使用 Kubernetes &lt;code&gt;apt&lt;/code&gt; 仓库所需的软件包：&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get update
# apt-transport-https may be a dummy package; if so, you can skip that package
sudo apt-get install -y apt-transport-https ca-certificates curl gpg
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;下载 Kubernetes 软件包仓库的公钥签名密钥。相同的签名密钥用于所有仓库，因此您可以忽略 URL 中的版本：&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# If the directory `/etc/apt/keyrings` does not exist, it should be created before the curl command, read the note below.
# sudo mkdir -p -m 755 /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.33/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;添加适当的 Kubernetes &lt;code&gt;apt&lt;/code&gt; 仓库:&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# This overwrites any existing configuration in /etc/apt/sources.list.d/kubernetes.list
echo &apos;deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.33/deb/ /&apos; | sudo tee /etc/apt/sources.list.d/kubernetes.list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warning&lt;/p&gt;
&lt;p&gt;请注意，此仓库仅包含 Kubernetes 1.33 的软件包；对于其他 Kubernetes 小版本，您需要更改 URL 中的 Kubernetes 小版本以匹配您想要的版本。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;更新 &lt;code&gt;apt&lt;/code&gt; 软件包索引，安装 kubelet、kubeadm 和 kubectl，并固定它们的版本：&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;在运行 kubeadm 之前启用 kubelet 服务：&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable --now kubelet
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;五、K8S 集群初始化&lt;/h2&gt;
&lt;p&gt;:::important&lt;/p&gt;
&lt;p&gt;此操作仅在主节点上进行&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;1. &lt;strong&gt;创建默认配置文件&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo kubeadm config print init-defaults &amp;gt; kubeadm-init.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;直接在 &lt;code&gt;~/&lt;/code&gt; 目录下并不是一个&lt;strong&gt;标准或推荐&lt;/strong&gt;的做法，为了方便编辑，现在只是临时生成，之后会移动到 &lt;code&gt;/etc/kubernetes/&lt;/code&gt; 目录下&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;2. &lt;strong&gt;修改配置文件&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;打开文件进行编辑：&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sudo nano kubeadm-init.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;找到并修改以下关键配置项：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;localAPIEndpoint.advertiseAddress&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;这个参数告诉其他节点如何连接到主节点的 API Server。请将它修改为您的 &lt;strong&gt;主节点（&lt;code&gt;k8s-master&lt;/code&gt;）的 IP 地址&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;localAPIEndpoint:
  advertiseAddress: 1.1.1.1 # 将此 IP 替换为您的主节点公网 IP
  bindPort: 6443
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;nodeRegistration.criSocket&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;这个参数指定了 &lt;code&gt;kubelet&lt;/code&gt; 连接容器运行时的 Socket 文件路径。由于我们使用的是 &lt;strong&gt;Containerd&lt;/strong&gt;，其默认路径就是 &lt;code&gt;/var/run/containerd/containerd.sock&lt;/code&gt;。检查确保这一行没有被修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nodeRegistration:
  criSocket: unix:///var/run/containerd/containerd.sock
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;kubernetesVersion&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;这个参数指定了要安装的 Kubernetes 版本。检查并确保它与您在第四节安装的 &lt;code&gt;kubelet&lt;/code&gt;、&lt;code&gt;kubeadm&lt;/code&gt; 和 &lt;code&gt;kubectl&lt;/code&gt; 版本一致。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubernetesVersion: 1.33.0 # 确保与安装的版本号一致
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;KubeProxyConfiguration.mode&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;这个参数用于设置 &lt;code&gt;kube-proxy&lt;/code&gt; 的工作模式。为了获得更好的性能和可扩展性，我们将其设置为 &lt;code&gt;ipvs&lt;/code&gt;。在 &lt;code&gt;kubeadm-init.yaml&lt;/code&gt; 文件的末尾追加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: ipvs
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;修改完毕后，保存文件并退出。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;将配置文件移动到 &lt;code&gt;/etc/kubernetes/&lt;/code&gt; 目录下：&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sudo mv kubeadm-init.yaml /etc/kubernetes/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. &lt;strong&gt;执行初始化命令&lt;/strong&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;cd /etc/kubernetes/
sudo kubeadm init --config=kubeadm-init.yaml --upload-certs --v=6
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;--config&lt;/code&gt; 参数指向我们刚刚配置的文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;--upload-certs&lt;/code&gt; 参数用于将集群证书上传到 etcd 中，方便后续工作节点加入时自动下载和配置。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::important&lt;/p&gt;
&lt;p&gt;如果初始化成功，终端会输出一段成功的提示信息，其中包含了后续用于将工作节点加入集群的 &lt;code&gt;kubeadm join&lt;/code&gt; 命令。&lt;/p&gt;
&lt;p&gt;一般情况下，&lt;code&gt;kubeadm&lt;/code&gt; 生成的默认 token &lt;code&gt;abcdef.0123456789abcdef&lt;/code&gt; &lt;strong&gt;需要删除掉&lt;/strong&gt;以确保安全。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo kubeadm token delete abcdef.0123456789abcdef
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来创建一个新的 token:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo kubeadm token create --print-join-command
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请务必将输出的命令&lt;strong&gt;复制并妥善保存&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;在进行这一步时我遇到了 &lt;code&gt;kube-apiserver&lt;/code&gt; 无法成功启动的问题，日志类似&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[control-plane-check] kube-apiserver is not healthy after 4m0.000278203s
kube-apiserver check failed at https://1.1.1.1:6443/livez: client rate limiter Wait returned an error: context deadline exceeded - error from a previous attempt: EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经过排查后，AI 给出了两种可能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;容器网络命名空间&lt;/strong&gt;：&lt;code&gt;etcd&lt;/code&gt; 容器在自己的网络命名空间中运行。即使主机可以访问这个公网 IP，容器内部的网络环境可能无法直接绑定到这个 IP。这在没有正确配置 &lt;code&gt;--advertise-address&lt;/code&gt; 的情况下非常常见。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;IP 地址不可用&lt;/strong&gt;：服务器可能正在使用一个私有 IP 地址作为其主要网络接口，而公网 IP 是通过 NAT（网络地址转换）映射过来的。在这种情况下，&lt;code&gt;etcd&lt;/code&gt; 容器无法直接绑定到公网 IP，因为它不存在于服务器的任何网络接口上。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;经过尝试之后，最终采用以下方案解决了这个问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;获取私有 IP&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;ip a
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;修改 &lt;code&gt;kubeadm-init.yaml&lt;/code&gt; 配置&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;localAPIEndpoint:
  advertiseAddress: 10.1.10.1 # 私有 IP
  bindPort: 6443
controlPlaneEndpoint: 1.1.1.1:6443 # 将公网 IP 作为 API Server 的外部访问地址
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;删除缓存&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sudo kubeadm reset -f
sudo rm -rf /etc/cni/net.d
sudo rm -rf /var/lib/etcd
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;重新初始化&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;sudo kubeadm init --config=kubeadm-init.yaml --upload-certs --v=6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h3&gt;4. &lt;strong&gt;配置 &lt;code&gt;kubectl&lt;/code&gt;&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;执行以下命令，以便普通用户可以使用 &lt;code&gt;kubectl&lt;/code&gt; 命令管理集群：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. &lt;strong&gt;部署网络插件&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;:::important&lt;/p&gt;
&lt;p&gt;这一步至关重要，&lt;strong&gt;在安装 Rancher 之前必须完成&lt;/strong&gt;。在主节点上部署 Calico 或其他 CNI 插件。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;安装并部署 Tigera Operator&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用 &lt;code&gt;kubectl create&lt;/code&gt; 命令部署 Tigera Operator。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.30.2/manifests/tigera-operator.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等待 &lt;code&gt;tigera-operator&lt;/code&gt; Pod 启动成功。你可以通过以下命令查看状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl get pod -n tigera-operator
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;创建 &lt;code&gt;Installation&lt;/code&gt; 资源&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Tigera Operator 部署成功后，你需要通过创建一个名为 &lt;code&gt;Installation&lt;/code&gt; 的自定义资源来告诉它如何安装 Calico。&lt;/p&gt;
&lt;p&gt;创建一个名为 &lt;code&gt;custom-resources.yaml&lt;/code&gt; 的文件，并添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# custom-resources.yaml
apiVersion: operator.tigera.io/v1
kind: Installation
metadata:
  name: default
spec:
  calicoNetwork:
    ipPools:
    - cidr: 10.244.0.0/16  # 将此 CIDR 替换为你的 Pod IP 段
      encapsulation: IPIP
      natOutgoing: Enabled
      nodeSelector: all()
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ipPools.cidr&lt;/code&gt;: 这个参数定义了 Pod 的 IP 地址段，你需要将其替换为你的实际网络配置。如果你的 &lt;code&gt;kubeadm-init.yaml&lt;/code&gt; 中配置了 &lt;code&gt;networking.podSubnet&lt;/code&gt;，请确保这里的值与之一致。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;encapsulation: IPIP&lt;/code&gt;: 这是 Calico 的默认封装模式，适用于大多数环境。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip[为什么不使用官方完整版清单？]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用官方完整版清单的优缺点：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;优点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;完整性&lt;/strong&gt;：你获得了 Calico 提供的所有可用资源的定义，这能让你对整个生态系统有更全面的了解。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;未来可扩展性&lt;/strong&gt;：如果将来你决定升级到 Tigera 的商业产品，这些资源已经存在，可以简化迁移过程。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;缺点：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;增加了复杂性&lt;/strong&gt;：部署了你可能永远用不到的资源，比如 Goldmane 和 Whisker。这会让你的集群中多出一些不必要的 Kubernetes 对象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可能引起混乱&lt;/strong&gt;：当你查看 &lt;code&gt;calico-system&lt;/code&gt; 命名空间下的资源时，会看到一些你并不了解的组件，这可能会让初学者感到困惑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志和事件噪音&lt;/strong&gt;：虽然这些组件可能不会正常工作，但它们可能会在日志或事件中产生一些错误或警告信息，增加了排查问题的难度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于三节点 Kubernetes 集群，最推荐的方案是使用我提供的&lt;strong&gt;简化版 &lt;code&gt;custom-resources.yaml&lt;/code&gt;&lt;/strong&gt;。它只部署了最核心、最必要的功能，确保你的集群网络可以正常运行，同时保持了系统的简洁和高效。&lt;/p&gt;
&lt;p&gt;如果你想尝试用官方完整版，也完全没问题。在大多数情况下，它不会对集群的稳定运行产生负面影响。不过，请记住，如果遇到与 Calico 相关的问题，排查时可以先忽略 Goldmane 和 Whisker 等非核心组件。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;部署 &lt;code&gt;Installation&lt;/code&gt; 资源&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl create -f custom-resources.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这步完成后，Calico 的部署过程已经开始。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;验证部署&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;为了观察部署进度，你可以使用 &lt;code&gt;watch&lt;/code&gt; 命令实时查看 &lt;code&gt;calico-system&lt;/code&gt; 命名空间下的 Pod 状态。当所有 Pod 都变为 &lt;strong&gt;&lt;code&gt;Running&lt;/code&gt;&lt;/strong&gt; 状态时，Calico 网络就部署成功了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 实时查看 Calico Pod 状态
watch kubectl get pods -n calico-system

# 成功后，按 Ctrl+C 退出
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，你之前处于 &lt;code&gt;NotReady&lt;/code&gt; 状态的节点也应该会变为 &lt;code&gt;Ready&lt;/code&gt;。你可以通过 &lt;code&gt;kubectl get nodes&lt;/code&gt; 命令进行验证。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;在 Kubernetes 集群中，Pod 默认是无法跨节点通信的。&lt;code&gt;kubeadm&lt;/code&gt; 在初始化集群后，只会搭建集群的控制平面，但不会自动配置 Pod 的网络。如果没有网络插件，你的集群虽然看起来已经启动，但所有 Pod（包括核心组件 &lt;code&gt;CoreDNS&lt;/code&gt;）都会处于 &lt;code&gt;Pending&lt;/code&gt; 或 &lt;code&gt;ContainerCreating&lt;/code&gt; 状态，因为它们无法互相通信。&lt;/p&gt;
&lt;p&gt;所以，部署 Calico 的目的就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;为 Pod 提供网络&lt;/strong&gt;：让集群中的 Pod 能够互相访问，无论是同一个节点还是不同节点上的 Pod。这是集群正常工作的基础。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实现网络策略&lt;/strong&gt;：Calico 提供了强大的网络策略功能，允许你定义 Pod 之间的通信规则，从而增强集群的安全性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;六、Rancher Server 安装&lt;/h2&gt;
&lt;p&gt;:::important&lt;/p&gt;
&lt;p&gt;此操作仅在主节点上进行&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;添加 Helm Chart 仓库&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们使用 Helm，一个 Kubernetes 的包管理工具，来部署 Rancher。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;helm repo add rancher-stable https://releases.rancher.com/server-charts/stable
helm repo update
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;安装 &lt;code&gt;cert-manager&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Rancher 需要 &lt;code&gt;cert-manager&lt;/code&gt; 来颁发和管理证书：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.18.2/cert-manager.yaml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等待 &lt;code&gt;cert-manager&lt;/code&gt; 的所有 Pod 启动成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl -n cert-manager get pod
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;安装 Rancher Server&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;使用 Helm 安装 Rancher，并配置一个主机名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;helm install rancher rancher-stable/rancher \
  --namespace cattle-system \
  --create-namespace \
  --set hostname=rancher.your-domain.com \
  --set ingress.tls.source=cert-manager \
  --set replicas=1
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;将 &lt;code&gt;rancher.your-domain.com&lt;/code&gt; 替换为你自己的域名。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;replicas=1&lt;/code&gt; 表示部署一个单实例 Rancher，适合测试和实验环境。如果你需要高可用，可以改为 3，但需要一个负载均衡器。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;访问 Rancher UI&lt;/strong&gt; 等待 Rancher Pod 启动成功后，你可以通过你设置的域名访问 Rancher UI。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;七、工作节点加入&lt;/h2&gt;
&lt;p&gt;:::important&lt;/p&gt;
&lt;p&gt;此操作仅在工作节点上进行&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo kubeadm join 1.1.1.1:6443 --token abcdef.0123456789abcdef \
    --discovery-token-ca-cert-hash sha256:06faf8d64c03530bf88d1a34eae877b3d446ab5e4f0e071fc96567ccf53b1e70
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成这些步骤后，你的三节点集群就会被 Rancher 成功管理。你可以在 Rancher 的 UI 中看到你的集群状态，并开始通过它来管理应用和工作负载。&lt;/p&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://kubernetes.io/zh-cn/docs/setup/production-environment/tools/kubeadm/install-kubeadm/&quot;&gt;安装 kubeadm | Kubernetes&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.tigera.io/calico/latest/getting-started/kubernetes/k8s-single-node&quot;&gt;Tutorial: Install Calico on single-host k8s cluster | Calico Documentation&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://documentation.suse.com/cloudnative/rancher-manager/latest/zh/installation-and-upgrade/installation-and-upgrade.html&quot;&gt;安装 SUSE® Rancher Prime :: Rancher product documentation&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/2301_81851270/article/details/146286268&quot;&gt;超详细kubernetes部署k8s----一台master和两台node_部署 kubernetes master-CSDN博客&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_43980547/article/details/137024280&quot;&gt;k8s入门到实战（三）—— 三台服务器从零开始搭建k8s集群-CSDN博客&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/XiaoWang0777/article/details/139806264&quot;&gt;使用Rancher快速部署K8S集群_rancher集群部署-CSDN博客&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>构建低成本的博客图片处理方案：全链路优化实践指南</title><link>https://mc.mimeng.top/posts/general/blog-image-solution/</link><guid isPermaLink="true">https://mc.mimeng.top/posts/general/blog-image-solution/</guid><description>为博客开发者提供完整的图片处理解决方案，从图片压缩、自动化上传、云端处理到前端适配的全链路实践，帮助构建高性能的个人博客系统。</description><pubDate>Tue, 05 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;这套方案是我在博客图片处理上的一次完整实践总结。无论用 Astro、Hexo、Hugo 还是其他静态站点生成器，都能参考这里的思路，优化图片性能。从压缩、上传、云端处理到前端适配，全链路自动化，省心高效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;:::note&lt;/p&gt;
&lt;p&gt;写这篇文章的一些思考可以在 &lt;a href=&quot;https://github.com/TatsukiMengChen/Blog/issues/3&quot;&gt;Github Issue - AVIF 格式引入与浏览器兼容性探讨 #3&lt;/a&gt; 查看&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;效果展示&lt;/h2&gt;
&lt;p&gt;先来看一下整体流程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    A[原始图片] --&amp;gt; B[压缩上传]
    B --&amp;gt; C[R2存储桶]
    C --&amp;gt; D[Worker 处理]
    D --&amp;gt; E[Cloudflare Images 转换]
    E --&amp;gt; F[多格式回存 R2]
    F --&amp;gt; G[博客 picture 适配]
    
    style A fill:#e1f5fe
    style G fill:#c8e6c9
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;图片质量和加载速度一直是博客体验的关键。尤其面向国内用户，既要保证清晰度，又要让页面秒开，还得控制流量和成本。经过多次探索，逐步形成了这套低成本、高兼容的图片处理方案。&lt;/p&gt;
&lt;h3&gt;核心需求&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;性能优先：图片体积尽量小，加载速度尽量快&lt;/li&gt;
&lt;li&gt;兼容性强：各种浏览器都能正常显示&lt;/li&gt;
&lt;li&gt;自动化流程：写作到发布全自动，无需手动处理&lt;/li&gt;
&lt;li&gt;成本可控：优先选用免费或低价服务&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;技术选型&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;图片上传：PicGo&lt;/li&gt;
&lt;li&gt;图片存储：Cloudflare R2&lt;/li&gt;
&lt;li&gt;图片处理：Cloudflare Images API&lt;/li&gt;
&lt;li&gt;自动化：Cloudflare Worker&lt;/li&gt;
&lt;li&gt;图片分发：主要依赖 &lt;a href=&quot;https://webp.se&quot;&gt;WebP Cloud Services&lt;/a&gt;，结合 R2 存储&lt;/li&gt;
&lt;li&gt;前端适配：多框架通用方案&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;适用场景&lt;/h3&gt;
&lt;p&gt;方案适用于 Astro、Hexo、Hugo、WordPress 及其他静态站点，核心思路通用，前端实现可灵活调整。&lt;/p&gt;
&lt;h2&gt;架构设计&lt;/h2&gt;
&lt;h3&gt;1. 图片上传层&lt;/h3&gt;
&lt;p&gt;PicGo 配合 Cloudflare R2，搭建免费图床。具体细节可参考：&lt;a href=&quot;https://sspai.com/post/90170&quot;&gt;从零开始搭建你的免费图床系统 （Cloudflare R2 + WebP Cloud + PicGo）&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;2. 云端处理层&lt;/h3&gt;
&lt;p&gt;我编写了一个 Worker 来进行图像处理。访问 Worker URL 或调用 API 时，会自动生成 AVIF、WebP、JPG 等多种格式，无需人工干预。支持健康检查和队列，保证自动化和可靠性。&lt;/p&gt;
&lt;p&gt;主要流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Worker 检查图片扩展名，决定是否处理&lt;/li&gt;
&lt;li&gt;立即返回处理状态，异步执行图片任务&lt;/li&gt;
&lt;li&gt;从 R2 拉原图，上传到 Cloudflare Images，生成多格式后再存回 R2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;相关代码和部署细节可以在 &lt;a href=&quot;https://github.com/TatsukiMengChen/r2-image-processor&quot;&gt;GitHub 仓库&lt;/a&gt; 查看，有兴趣可以参考源码和实现思路。&lt;/p&gt;
&lt;h3&gt;3. 前端适配层&lt;/h3&gt;
&lt;p&gt;不同博客框架下，图片适配方式略有不同，但核心思路一致。&lt;/p&gt;
&lt;h2&gt;核心实现代码&lt;/h2&gt;
&lt;p&gt;前端图片适配的实现，Astro 下可以通过配置 Sharp 服务自动生成多种图片格式。比如在 &lt;code&gt;astro.config.mjs&lt;/code&gt; 里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default defineConfig({
  image: {
    service: {
      entrypoint: &quot;astro/assets/services/sharp&quot;,
      config: {
        formats: [&quot;avif&quot;, &quot;webp&quot;, &quot;jpeg&quot;],
        quality: {
          avif: 80,
          webp: 80,
          jpeg: 85,
          png: 85,
        },
        progressive: true,
      },
    },
  },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果需要更智能的图片处理和降级，可以用自定义组件。比如 &lt;code&gt;ImageWrapper.astro&lt;/code&gt;，会自动遍历格式优先级，生成 &lt;code&gt;&amp;lt;source&amp;gt;&lt;/code&gt; 标签，并根据图片来源动态降级。部分核心逻辑如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
// ...参数处理与导入省略...

async function generateImageFormats(image: ImageMetadata) {
  const formats = [
    { name: &quot;avif&quot;, type: &quot;image/avif&quot; },
    { name: &quot;webp&quot;, type: &quot;image/webp&quot; },
    { name: &quot;jpeg&quot;, type: &quot;image/jpeg&quot; },
    { name: &quot;png&quot;, type: &quot;image/png&quot; },
  ];
  const availableFormats = [];
  for (const format of formats) {
    try {
      const optimizedImage = await getImage({
        src: image,
        format: format.name as any,
        width: image.width,
        height: image.height,
      });
      availableFormats.push({
        src: optimizedImage.src,
        format: format.name,
      });
    } catch (error) {
      // ...错误处理省略...
    }
  }
  // ...降级逻辑与返回省略...
}
---
// ...渲染部分省略...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整组件代码和详细逻辑可以参考：&lt;a href=&quot;https://github.com/TatsukiMengChen/Blog/blob/e0ec7488a2786dcd2053b602cdcd245be8a8717c/src/components/misc/ImageWrapper.astro&quot;&gt;ImageWrapper.astro 源码&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;对于 Hexo、Hugo 等静态博客，直接用 &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; 标签即可实现多格式适配：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;picture&amp;gt;
  &amp;lt;source srcset=&quot;https://your-r2-domain/images/photo.avif&quot; type=&quot;image/avif&quot;&amp;gt;
  &amp;lt;source srcset=&quot;https://your-r2-domain/images/photo.webp&quot; type=&quot;image/webp&quot;&amp;gt;
  &amp;lt;img src=&quot;https://your-r2-domain/images/photo.jpg&quot; alt=&quot;Photo description&quot; loading=&quot;lazy&quot;&amp;gt;
&amp;lt;/picture&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或用 JS 自动化批量处理图片标签：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function optimizeImages() {
  const images = document.querySelectorAll(&apos;img[data-optimize]&apos;);
  images.forEach(img =&amp;gt; {
    const picture = document.createElement(&apos;picture&apos;);
    picture.innerHTML = `
      &amp;lt;source srcset=&quot;${img.src.replace(/\.\w+$/, &apos;.avif&apos;)}&quot; type=&quot;image/avif&quot;&amp;gt;
      &amp;lt;source srcset=&quot;${img.src.replace(/\.\w+$/, &apos;.webp&apos;)}&quot; type=&quot;image/webp&quot;&amp;gt;
      ${img.outerHTML}
    `;
    img.parentNode.replaceChild(picture, img);
  });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip&lt;/p&gt;
&lt;p&gt;建议结合自身需求和技术栈，探索适合自己的图片优化方案。可以参考 &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; 标签的多格式适配思路，也可以尝试插件、自动化脚本或第三方服务。每个框架都有不同的扩展方式，灵活调整才能获得最佳效果。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;兼容性与探索&lt;/h2&gt;
&lt;p&gt;AVIF 格式虽然高效，但并非所有浏览器都支持。实际测试发现，部分老旧浏览器甚至新版本也有兼容性问题。采用 &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; 标签，优先提供 AVIF，其次 WebP，最后 JPEG 或 PNG，浏览器会自动选择支持的格式，兼容性和体验兼顾。&lt;/p&gt;
&lt;p&gt;也尝试过 avif.js、WebAssembly 等方案，但维护和兼容性不理想。最终还是觉得 &lt;code&gt;&amp;lt;picture&amp;gt;&lt;/code&gt; 标签最简单可靠。图片优化方面，Sharp 虽然强大，但图片放仓库会导致仓库膨胀，影响构建和拉取效率。现在远程图片都用 webp.se 或 Cloudflare R2 分发，省心高效。&lt;/p&gt;
&lt;h2&gt;性能优化策略&lt;/h2&gt;
&lt;h3&gt;渐进式加载&lt;/h3&gt;
&lt;p&gt;所有图片都用 lazy loading 和异步解码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;Image
  loading=&quot;lazy&quot;
  decoding=&quot;async&quot;
  // ... 其他属性
/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;格式优先级&lt;/h3&gt;
&lt;p&gt;现代浏览器支持度排序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;AVIF（最优）&lt;/li&gt;
&lt;li&gt;WebP&lt;/li&gt;
&lt;li&gt;JPEG/PNG（兜底）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://sspai.com/post/90170&quot;&gt;从零开始搭建你的免费图床系统 （Cloudflare R2 + WebP Cloud + PicGo）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/r2/&quot;&gt;Cloudflare R2 文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/images/&quot;&gt;Cloudflare Images 文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/workers/&quot;&gt;Cloudflare Workers 文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/en/guides/images/&quot;&gt;Astro 图片优化文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/Media/Guides/Formats/Image_types&quot;&gt;MDN 图片格式指南&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/TatsukiMengChen/r2-image-processor&quot;&gt;完整 Worker 代码&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>为你的 Fuwari 集成 Mermaid 图表支持</title><link>https://mc.mimeng.top/posts/frontend/fuwari-mermaid-integration-guide/</link><guid isPermaLink="true">https://mc.mimeng.top/posts/frontend/fuwari-mermaid-integration-guide/</guid><description>详细介绍如何在 Fuwari 中集成 Mermaid 图表支持，采用客户端渲染方式，完美适配深色模式切换。</description><pubDate>Sun, 03 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;本文记录了在 Fuwari 主题中集成 Mermaid 图表的完整实现过程。通过 MutationObserver 监听主题切换，实现图表与深色/浅色模式的完美同步。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;效果展示&lt;/h2&gt;
&lt;p&gt;首先看看最终的效果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    A[Markdown 代码块] --&amp;gt; B[Remark 插件处理]
    B --&amp;gt; C[转换为 HTML 容器]
    C --&amp;gt; D[Rehype 插件注入脚本]
    D --&amp;gt; E[客户端动态渲染]
    
    style A fill:#e1f5fe
    style E fill:#c8e6c9
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;技术方案&lt;/h2&gt;
&lt;h3&gt;整体架构&lt;/h3&gt;
&lt;p&gt;本方案采用客户端渲染，通过以下组件实现：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Remark 插件&lt;/strong&gt; - 识别 Mermaid 代码块&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rehype 插件&lt;/strong&gt; - 生成 HTML 容器和渲染脚本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;渲染脚本&lt;/strong&gt; - 动态加载 Mermaid 库并处理主题切换&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;样式文件&lt;/strong&gt; - 提供响应式布局和主题适配&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;主题切换机制&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;MutationObserver&lt;/code&gt; 监听 &lt;code&gt;document.documentElement&lt;/code&gt; 的 &lt;code&gt;class&lt;/code&gt; 变化，当检测到 &lt;code&gt;dark&lt;/code&gt; 类的添加或移除时，自动重新渲染图表以匹配新主题。&lt;/p&gt;
&lt;h2&gt;核心代码文件&lt;/h2&gt;
&lt;p&gt;所有实现代码都可以在 GitHub 仓库中找到：&lt;/p&gt;
&lt;h3&gt;插件文件&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/TatsukiMengChen/Blog/tree/feature/mermaid/src/plugins/remark-mermaid.js&quot;&gt;remark-mermaid.js&lt;/a&gt;&lt;/strong&gt; - Remark 插件，识别并转换 Mermaid 代码块&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/TatsukiMengChen/Blog/tree/feature/mermaid/src/plugins/rehype-mermaid.mjs&quot;&gt;rehype-mermaid.mjs&lt;/a&gt;&lt;/strong&gt; - Rehype 插件，生成 HTML 容器和注入渲染脚本&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/TatsukiMengChen/Blog/tree/feature/mermaid/src/plugins/mermaid-render-script.js&quot;&gt;mermaid-render-script.js&lt;/a&gt;&lt;/strong&gt; - 客户端渲染脚本，处理图表渲染和主题同步&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;配置文件&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/TatsukiMengChen/Blog/tree/feature/mermaid/astro.config.mjs&quot;&gt;astro.config.mjs&lt;/a&gt;&lt;/strong&gt; - Astro 配置，注册插件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/TatsukiMengChen/Blog/tree/feature/mermaid/src/styles/markdown-extend.styl&quot;&gt;markdown-extend.styl&lt;/a&gt;&lt;/strong&gt; - 样式文件，包含 Mermaid 相关样式&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;快速集成步骤&lt;/h2&gt;
&lt;h3&gt;1. 复制插件文件&lt;/h3&gt;
&lt;p&gt;将以下文件复制到你的项目中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/TatsukiMengChen/Blog/tree/feature/mermaid/src/plugins/remark-mermaid.js&quot;&gt;&lt;code&gt;src/plugins/remark-mermaid.js&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/TatsukiMengChen/Blog/tree/feature/mermaid/src/plugins/rehype-mermaid.mjs&quot;&gt;&lt;code&gt;src/plugins/rehype-mermaid.mjs&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/TatsukiMengChen/Blog/tree/feature/mermaid/src/plugins/mermaid-render-script.js&quot;&gt;&lt;code&gt;src/plugins/mermaid-render-script.js&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 配置 Astro&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;astro.config.mjs&lt;/code&gt; 中添加插件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { remarkMermaid } from &quot;./src/plugins/remark-mermaid.js&quot;;
import { rehypeMermaid } from &quot;./src/plugins/rehype-mermaid.mjs&quot;;

export default defineConfig({
    markdown: {
        remarkPlugins: [
            // ... 其他插件
            remarkMermaid,
        ],
        rehypePlugins: [
            // ... 其他插件  
            rehypeMermaid,
        ],
    },
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 添加样式&lt;/h3&gt;
&lt;p&gt;复制 &lt;a href=&quot;https://github.com/TatsukiMengChen/Blog/tree/feature/mermaid/src/styles/markdown-extend.styl&quot;&gt;样式文件&lt;/a&gt; 中 Mermaid 相关的样式到你的样式文件中。&lt;/p&gt;
&lt;h3&gt;4. 开始使用&lt;/h3&gt;
&lt;p&gt;在 Markdown 文件中使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```mermaid
graph TD
    A[开始] --&amp;gt; B[结束]
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;关键实现细节&lt;/h2&gt;
&lt;h3&gt;1. 单例模式防重复初始化&lt;/h3&gt;
&lt;p&gt;渲染脚本使用单例模式确保 Mermaid 只初始化一次：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (window.mermaidInitialized) {
    return;
}
window.mermaidInitialized = true;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 智能主题检测&lt;/h3&gt;
&lt;p&gt;通过 &lt;code&gt;MutationObserver&lt;/code&gt; 监听 DOM 变化，精确检测主题切换：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const observer = new MutationObserver((mutations) =&amp;gt; {
    mutations.forEach((mutation) =&amp;gt; {
        if (mutation.type === &quot;attributes&quot; &amp;amp;&amp;amp; mutation.attributeName === &quot;class&quot;) {
            const wasDark = mutation.oldValue?.includes(&quot;dark&quot;) || false;
            const isDark = mutation.target.classList.contains(&quot;dark&quot;);
            
            if (wasDark !== isDark) {
                renderMermaidDiagrams(); // 重新渲染图表
            }
        }
    });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 批量渲染优化&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;Promise.all&lt;/code&gt; 并行渲染多个图表，提升性能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const renderPromises = Array.from(mermaidElements).map(async (element) =&amp;gt; {
    // 渲染单个图表
});
await Promise.all(renderPromises);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 防并发渲染&lt;/h3&gt;
&lt;p&gt;通过标志位防止并发渲染造成的问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let isRendering = false;

function renderMermaidDiagrams() {
    if (isRendering) return;
    isRendering = true;
    // 渲染逻辑...
    isRendering = false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;图表示例&lt;/h2&gt;
&lt;h3&gt;流程图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;graph LR
    A[开始] --&amp;gt; B{条件判断}
    B --&amp;gt;|条件1| C[处理1]
    B --&amp;gt;|条件2| D[处理2]
    C --&amp;gt; E[结束]
    D --&amp;gt; E
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;序列图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant 用户
    participant 浏览器
    participant 服务器
    participant 数据库
    
    用户-&amp;gt;&amp;gt;浏览器: 输入网址
    浏览器-&amp;gt;&amp;gt;服务器: 发送HTTP请求
    服务器-&amp;gt;&amp;gt;数据库: 查询数据
    数据库--&amp;gt;&amp;gt;服务器: 返回数据
    服务器--&amp;gt;&amp;gt;浏览器: 返回HTML
    浏览器--&amp;gt;&amp;gt;用户: 显示页面
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;类图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;classDiagram
    class Animal {
        +String name
        +int age
        +makeSound()
    }
    
    class Dog {
        +String breed
        +bark()
    }
    
    class Cat {
        +String color
        +meow()
    }
    
    Animal &amp;lt;|-- Dog
    Animal &amp;lt;|-- Cat
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;甘特图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;gantt
    title 项目开发时间线
    dateFormat  YYYY-MM-DD
    section 需求分析
    需求收集           :a1, 2025-08-01, 2025-08-05
    需求整理           :a2, after a1, 3d
    section 设计阶段
    UI设计            :b1, 2025-08-08, 7d
    数据库设计         :b2, 2025-08-08, 5d
    section 开发阶段
    前端开发          :c1, 2025-08-15, 14d
    后端开发          :c2, 2025-08-15, 10d
    section 测试阶段
    功能测试          :d1, after c1, 5d
    性能测试          :d2, after d1, 3d
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;饼图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pie
    title 技术栈分布
    &quot;JavaScript&quot; : 35
    &quot;TypeScript&quot; : 25
    &quot;CSS&quot; : 20
    &quot;HTML&quot; : 20
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://mermaid.js.org/&quot;&gt;Mermaid 官方文档&lt;/a&gt; - 图表语法和配置&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.astro.build/&quot;&gt;Astro 官方文档&lt;/a&gt; - 插件开发指南&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/saicaca/fuwari&quot;&gt;Fuwari 主题&lt;/a&gt; - 主题源码和文档&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/TatsukiMengChen/Blog/tree/feature/mermaid/src/plugins&quot;&gt;完整实现代码&lt;/a&gt; - 本文所有插件代码&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>认证模块演进式设计：从 JWT 会话控制到分布式架构</title><link>https://mc.mimeng.top/posts/backend/jwt-k8s/</link><guid isPermaLink="true">https://mc.mimeng.top/posts/backend/jwt-k8s/</guid><description>旨在提供一个全面、可演进的认证鉴权系统设计方案。它从一个基于JWT和数据库的基础会话管理系统开始，逐步引入性能优化策略，最终扩展为一个适用于异地分布式 K8s 环境的高性能、高可用的统一认证平台。</description><pubDate>Tue, 29 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;JWT 会话管理基础设计&lt;/h2&gt;
&lt;h3&gt;1. 引言&lt;/h3&gt;
&lt;p&gt;本设计旨在解决基于 JWT 的无状态认证在实际应用中遇到的挑战：无法方便地踢出用户使其离线，以及难以限制用户登录的设备数量。我们将通过引入刷新令牌（Refresh Token）和会话管理机制，在保持 JWT 访问令牌轻量、可伸缩优势的同时，实现对用户会话的精细化控制。&lt;/p&gt;
&lt;p&gt;实现目标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方便地踢出特定用户或特定设备，使其当前会话立即失效。&lt;/li&gt;
&lt;li&gt;限制用户同时登录的设备数量（例如，最多3台）。&lt;/li&gt;
&lt;li&gt;实现同类型设备登录时的自动踢出：当用户在新的同类型设备（如另一台手机）上登录时，自动使最旧的同类型设备会话失效。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 核心概念&lt;/h3&gt;
&lt;h4&gt;2.1 访问令牌 (Access Token - JWT)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;性质&lt;/strong&gt;：短寿命的 JSON Web Token。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用途&lt;/strong&gt;：用于访问受保护的 API 资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：无状态，一旦签发，服务器不存储其状态。过期后自动失效。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.2 刷新令牌 (Refresh Token)&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;性质&lt;/strong&gt;：长寿命的、唯一的字符串，存储在数据库中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用途&lt;/strong&gt;：当访问令牌过期时，用于向认证服务请求新的访问令牌。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;特点&lt;/strong&gt;：有状态，与用户 ID、设备信息、过期时间等关联。服务器可以对其进行管理（撤销、查询）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.3 设备类型 (Device Type)&lt;/h4&gt;
&lt;p&gt;为了实现同类型设备踢出，我们需要在登录时识别设备类型。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;枚举值&lt;/strong&gt;：&lt;code&gt;PC&lt;/code&gt; (电脑), &lt;code&gt;MOBILE&lt;/code&gt; (手机), &lt;code&gt;TABLET&lt;/code&gt; (平板)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;获取方式&lt;/strong&gt;：通常由客户端在登录请求中提供。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2.4 会话管理&lt;/h4&gt;
&lt;p&gt;我们将通过在数据库中维护 &lt;code&gt;RefreshToken&lt;/code&gt; 记录来实现会话管理。每条 &lt;code&gt;RefreshToken&lt;/code&gt; 记录代表一个活跃的设备会话。&lt;/p&gt;
&lt;h3&gt;3. 数据库 Schema 设计 (Prisma)&lt;/h3&gt;
&lt;p&gt;在现有的 &lt;code&gt;User&lt;/code&gt; 模型基础上，我们将新增一个 &lt;code&gt;RefreshToken&lt;/code&gt; 模型来存储刷新令牌及其相关信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// prisma/schema.prisma

// 用户模型 (User Model)
model User {
  id            Int            @id @default(autoincrement())
  username      String         @unique
  email         String         @unique
  password      String
  createdAt     DateTime       @default(now())
  updatedAt     DateTime       @updatedAt
  // 关联刷新令牌
  refreshTokens RefreshToken[]
}

// 刷新令牌模型 (RefreshToken Model)
model RefreshToken {
  id         Int      @id @default(autoincrement())
  token      String   @unique // 存储唯一的刷新令牌字符串
  userId     Int      // 关联的用户ID
  user       User     @relation(fields: [userId], references: [id], onDelete: Cascade) // 当用户被删除时，其所有刷新令牌也删除
  deviceType String   // 设备类型: PC, MOBILE, TABLET
  deviceId   String   @unique // 设备的唯一标识符，例如 UUID，由客户端生成并提供
  deviceName String?  // 新增：设备的名称，方便用户识别
  expiresAt  DateTime // 刷新令牌的过期时间
  isValid    Boolean  @default(true) // 软删除标记，用于立即失效
  createdAt  DateTime @default(now())
  updatedAt  DateTime @updatedAt

  @@index([userId]) // 为 userId 字段添加索引，提高查询效率
  @@unique([userId, deviceId]) // 确保同一用户同一设备只有一个活跃令牌
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;字段解释&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;RefreshToken.isValid&lt;/code&gt;&lt;/strong&gt;: 一个布尔标志，用于“软删除”或立即使刷新令牌失效，而无需真正从数据库中删除记录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;RefreshToken.deviceId&lt;/code&gt;&lt;/strong&gt;: 客户端提供的设备唯一标识，用于区分同一用户的不同设备。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;RefreshToken.deviceName&lt;/code&gt;&lt;/strong&gt;: 用于存储用户可读的设备名称（例如“我的iPhone 15”），方便用户在会话管理界面识别。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. API 端点设计&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;POST /auth/register&lt;/code&gt;: 用户注册。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /auth/login&lt;/code&gt;: 用户登录。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /auth/refresh&lt;/code&gt;: 使用刷新令牌获取新的访问令牌。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /auth/logout&lt;/code&gt;: 注销当前设备会话。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /auth/logout-all-devices&lt;/code&gt;: 注销用户在所有设备上的会话。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /auth/active-sessions&lt;/code&gt;: 获取用户当前所有活跃的设备会话列表。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DELETE /auth/active-sessions/:id&lt;/code&gt;: 注销指定 ID 的设备会话（踢出某个设备）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 认证流程设计&lt;/h3&gt;
&lt;h4&gt;5.1 登录流程 (&lt;code&gt;/auth/login&lt;/code&gt;)&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;客户端&lt;/strong&gt;：发送 &lt;code&gt;POST /auth/login&lt;/code&gt; 请求，包含 &lt;code&gt;username&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt;, &lt;code&gt;deviceType&lt;/code&gt;, &lt;code&gt;deviceId&lt;/code&gt;, &lt;code&gt;deviceName&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务端&lt;/strong&gt;：
a. 验证用户名和密码。
b. &lt;strong&gt;检查设备限制&lt;/strong&gt;：查询数据库中该用户 &lt;code&gt;isValid = true&lt;/code&gt; 的 &lt;code&gt;RefreshToken&lt;/code&gt; 数量。
c. &lt;strong&gt;执行踢出策略&lt;/strong&gt;：如果已达到最大限制（例如3台），查找该用户最旧的同类型设备会话，将其 &lt;code&gt;isValid&lt;/code&gt; 字段设置为 &lt;code&gt;false&lt;/code&gt;。如果无同类型设备可踢，则踢出最旧的一个会话。
d. &lt;strong&gt;生成并保存令牌&lt;/strong&gt;：生成新的 &lt;code&gt;RefreshToken&lt;/code&gt; 并存入数据库，同时生成短寿命的 &lt;code&gt;AccessToken&lt;/code&gt;。
e. &lt;strong&gt;返回令牌&lt;/strong&gt;：将 &lt;code&gt;AccessToken&lt;/code&gt; 和 &lt;code&gt;RefreshToken&lt;/code&gt; 返回给客户端。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;5.2 访问令牌刷新流程 (&lt;code&gt;/auth/refresh&lt;/code&gt;)&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;客户端&lt;/strong&gt;：当 &lt;code&gt;AccessToken&lt;/code&gt; 过期时，发送 &lt;code&gt;POST /auth/refresh&lt;/code&gt; 请求，包含 &lt;code&gt;RefreshToken&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务端&lt;/strong&gt;：
a. 在数据库中查找该 &lt;code&gt;RefreshToken&lt;/code&gt;，校验其是否存在、是否有效 (&lt;code&gt;isValid = true&lt;/code&gt;)、是否过期。
b. 若验证失败，返回 401，客户端应强制用户重新登录。
c. 若验证成功，&lt;strong&gt;为了安全，生成一个新的 &lt;code&gt;RefreshToken&lt;/code&gt; 替换旧的&lt;/strong&gt;（一次性使用原则），并签发一个新的 &lt;code&gt;AccessToken&lt;/code&gt;。
d. 返回新的双令牌。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;5.3 受保护资源访问流程&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;客户端&lt;/strong&gt;：发送包含 &lt;code&gt;AccessToken&lt;/code&gt; (Bearer Token) 的请求到受保护的 API。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务端 (JWT 策略)&lt;/strong&gt;：
a. 验证 &lt;code&gt;AccessToken&lt;/code&gt; 的签名和过期时间。
b. **（关键步骤）**从 &lt;code&gt;AccessToken&lt;/code&gt; 的 payload 中提取 &lt;code&gt;userId&lt;/code&gt; 和 &lt;code&gt;deviceId&lt;/code&gt;。
c. &lt;strong&gt;查询 &lt;code&gt;RefreshToken&lt;/code&gt; 表，检查是否存在 &lt;code&gt;userId&lt;/code&gt; 和 &lt;code&gt;deviceId&lt;/code&gt; 匹配且 &lt;code&gt;isValid&lt;/code&gt; 为 &lt;code&gt;true&lt;/code&gt; 的记录。&lt;/strong&gt;
d. 如果不存在或 &lt;code&gt;isValid&lt;/code&gt; 为 &lt;code&gt;false&lt;/code&gt;，则抛出 &lt;code&gt;UnauthorizedException&lt;/code&gt;，即使 &lt;code&gt;AccessToken&lt;/code&gt; 本身未过期。
e. 验证通过，请求继续处理。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;5.4 注销流程&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注销当前设备 (&lt;code&gt;/auth/logout&lt;/code&gt;)&lt;/strong&gt;: 将数据库中当前用户和设备对应的 &lt;code&gt;RefreshToken&lt;/code&gt; 记录的 &lt;code&gt;isValid&lt;/code&gt; 字段设置为 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注销所有设备 (&lt;code&gt;/auth/logout-all-devices&lt;/code&gt;)&lt;/strong&gt;: 将该用户所有 &lt;code&gt;RefreshToken&lt;/code&gt; 记录的 &lt;code&gt;isValid&lt;/code&gt; 设为 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注销指定设备 (&lt;code&gt;DELETE /auth/active-sessions/:id&lt;/code&gt;)&lt;/strong&gt;: 将指定 ID 的 &lt;code&gt;RefreshToken&lt;/code&gt; 记录的 &lt;code&gt;isValid&lt;/code&gt; 设为 &lt;code&gt;false&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6. 客户端注意事项&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;生成 &lt;code&gt;deviceId&lt;/code&gt;&lt;/strong&gt;：客户端（前端）需在首次启动时生成一个唯一 &lt;code&gt;deviceId&lt;/code&gt; (UUID) 并持久化存储（如 &lt;code&gt;localStorage&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储 &lt;code&gt;RefreshToken&lt;/code&gt;&lt;/strong&gt;：应存储在安全的地方，例如 &lt;strong&gt;HttpOnly Cookie&lt;/strong&gt; (Web端) 或移动应用的安全存储区。绝不能存储在 &lt;code&gt;localStorage&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;AccessToken&lt;/code&gt; 管理&lt;/strong&gt;：通常存储在内存中，通过 &lt;code&gt;Authorization&lt;/code&gt; 请求头发送。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;JWT 方案性能优化策略&lt;/h2&gt;
&lt;p&gt;上述基础方案虽然功能完备，但在“受保护资源访问流程”的 5.3.c 步骤中，&lt;strong&gt;每次 API 请求都需要查询数据库&lt;/strong&gt;，这在高并发场景下会成为性能瓶颈。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;核心思想&lt;/strong&gt;：用高速缓存（如 Redis）替代慢速的关系型数据库（如 PostgreSQL）来进行高频的“有效性检查”。&lt;/p&gt;
&lt;h3&gt;1. 核心优化：引入 Redis 作为“吊销列表 (Revocation List)”&lt;/h3&gt;
&lt;p&gt;我们不再在每次请求时都去查询数据库，而是查询速度快几个数量级的 Redis。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;工作原理&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;吊销时（踢出/登出）&lt;/strong&gt;：当一个会话需要被终止时（用户登出、被踢出），我们执行两个操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;更新数据库&lt;/strong&gt;：将 PostgreSQL 中对应 &lt;code&gt;RefreshToken&lt;/code&gt; 的 &lt;code&gt;isValid&lt;/code&gt; 设为 &lt;code&gt;false&lt;/code&gt;（保持事实来源的准确性）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写入 Redis 吊销列表&lt;/strong&gt;：将被吊销的 &lt;code&gt;AccessToken&lt;/code&gt; 的一个唯一标识（使用其 &lt;code&gt;jti&lt;/code&gt; 声明）存入 Redis，并&lt;strong&gt;设置一个等于该 &lt;code&gt;AccessToken&lt;/code&gt; 剩余生命周期的 TTL&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Redis 操作示例 (假设 Access Token 还有 840 秒过期)&lt;/em&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# &apos;jti&apos; (JWT ID) 是 JWT payload 中的一个标准字段，非常适合做唯一标识
SET revoked_tokens:&amp;lt;jti_of_the_token&amp;gt; true EX 840
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样做的好处是，一旦令牌自然过期，其在 Redis 中的吊销记录也会自动清理，不会造成内存无限增长。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;验证时（访问受保护 API）&lt;/strong&gt;：&lt;code&gt;JwtStrategy&lt;/code&gt; 的验证逻辑变为：
a. 验证 JWT 的签名和基础过期时间。
b. 从 JWT 的 payload 中提取 &lt;code&gt;jti&lt;/code&gt;。
c. &lt;strong&gt;查询 Redis&lt;/strong&gt;：检查 &lt;code&gt;revoked_tokens:&amp;lt;jti&amp;gt;&lt;/code&gt; 这个键是否存在。
d. 如果键&lt;strong&gt;存在&lt;/strong&gt;，说明该令牌已被吊销，立即拒绝访问 (401 Unauthorized)。
e. 如果键&lt;strong&gt;不存在&lt;/strong&gt;，说明令牌有效，允许访问。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2. 改良后的 JWT 认证流程&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;登录 (&lt;code&gt;/auth/login&lt;/code&gt;)&lt;/strong&gt;: 流程不变，依旧操作数据库，生成含 &lt;code&gt;jti&lt;/code&gt; 的 &lt;code&gt;AccessToken&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;访问受保护API&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;客户端携带 &lt;code&gt;AccessToken&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;认证中间件验证JWT签名和&lt;code&gt;exp&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;从Payload中提取 &lt;code&gt;jti&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;查询 Redis &lt;code&gt;EXISTS revoked_tokens:&amp;lt;jti&amp;gt;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;若存在，拒绝 (401)；若不存在，放行。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;登出/踢出&lt;/strong&gt;:
&lt;ol&gt;
&lt;li&gt;服务端找到要吊销的会话。&lt;/li&gt;
&lt;li&gt;从其 &lt;code&gt;AccessToken&lt;/code&gt; 解析出 &lt;code&gt;jti&lt;/code&gt; 和剩余过期时间 &lt;code&gt;ttl&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;执行 &lt;code&gt;SET revoked_tokens:&amp;lt;jti&amp;gt; true EX &amp;lt;ttl&amp;gt;&lt;/code&gt; 写入 Redis。&lt;/li&gt;
&lt;li&gt;执行 &lt;code&gt;UPDATE &quot;RefreshToken&quot; SET &quot;isValid&quot; = false ...&lt;/code&gt; 更新数据库。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 总结对比&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;原始 JWT 方案&lt;/th&gt;
&lt;th&gt;优化后的 JWT 方案&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API 验证开销&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;高: JWT解密 + &lt;strong&gt;数据库查询&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;低: JWT解密 + &lt;strong&gt;Redis 查询&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;性能瓶颈&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;关系型数据库的 I/O&lt;/td&gt;
&lt;td&gt;Redis 的网络延迟 (通常极低)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;实现复杂度&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;中等&lt;/td&gt;
&lt;td&gt;稍高: 需管理数据库和 Redis 的同步&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;即时撤销能力&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;具备&lt;/td&gt;
&lt;td&gt;具备，且性能更高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;架构&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;应用服务器 &amp;lt;-&amp;gt; 数据库&lt;/td&gt;
&lt;td&gt;应用服务器 &amp;lt;-&amp;gt; Redis &amp;lt;-&amp;gt; 数据库&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;结论&lt;/strong&gt;：通过引入 Redis 作为“吊销列表”，我们实现了一个“两全其美”的方案：对外保留了 JWT 的优点（跨域、微服务友好），对内通过高速缓存实现了高性能、有状态的会话控制。这种 &lt;strong&gt;“JWT + Redis 吊销列表”&lt;/strong&gt; 模式是现代分布式应用中非常成熟和流行的行业实践。&lt;/p&gt;
&lt;h2&gt;异地分布式 Kubernetes 统一认证鉴权平台技术设计&lt;/h2&gt;
&lt;p&gt;当业务部署在多个地理区域的 Kubernetes 集群上时，我们需要解决跨地域延迟和全局实时会话同步的问题。本设计将前述方案扩展为一个分布式架构。&lt;/p&gt;
&lt;h3&gt;1. 设计目标&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;高性能&lt;/strong&gt;: 核心API的鉴权延迟（P99）应在10毫秒以内，不受限于跨地域网络。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高可用&lt;/strong&gt;: 无单点故障，单个地域的故障不影响其他地域。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时控制&lt;/strong&gt;: 会话吊销指令应在1秒内全局生效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;强安全性&lt;/strong&gt;: 遵循业界最佳安全实践。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2. 整体架构&lt;/h3&gt;
&lt;p&gt;本方案采用一种&lt;strong&gt;以JWT为载体、以本地缓存加速、以消息总线同步&lt;/strong&gt;的混合模式。&lt;/p&gt;
&lt;h4&gt;2.1 组件职责&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API Gateway / Ingress&lt;/strong&gt;: 流量入口。将写操作（登录/刷新/登出）路由到中央认证服务，将读操作（业务API请求）路由到各地域的业务服务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;认证服务 (Auth Service)&lt;/strong&gt;: &lt;strong&gt;中心化&lt;/strong&gt;的服务。处理用户登录、令牌刷新、会话吊销等&lt;strong&gt;写密集型&lt;/strong&gt;操作。是唯一有权限写入 PostgreSQL 和发布吊销事件的服务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务服务 (Business Service)&lt;/strong&gt;: 部署在各地域K8s集群中。包含一个认证中间件，负责所有API请求的鉴权。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL (事实来源 - Source of Truth)&lt;/strong&gt;: &lt;strong&gt;中心化&lt;/strong&gt;部署的高可用数据库集群，持久化存储用户及 &lt;code&gt;RefreshToken&lt;/code&gt; 表。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地Redis缓存 (Local Read Cache)&lt;/strong&gt;: 通过 &lt;code&gt;DaemonSet&lt;/code&gt; 部署在&lt;strong&gt;每个K8s节点&lt;/strong&gt;上。核心职责是缓存已吊销的 &lt;code&gt;jti&lt;/code&gt; 列表，为本节点的业务服务提供近乎零延迟的鉴权检查。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消息总线 (Message Bus - NATS / Redis Pub/Sub)&lt;/strong&gt;: &lt;strong&gt;全局部署&lt;/strong&gt;的高性能消息系统。核心职责是当认证服务吊销会话时，立即向全局发布一条“吊销事件”消息。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. 核心流程&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://d988089.webp.li/2025/07/29/20250729023145715.png&quot; alt=&quot;分布式鉴权流程图&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;3.1 API请求鉴权 (高性能读路径)&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;用户携带 &lt;code&gt;AccessToken&lt;/code&gt; 访问任意地域的业务服务。&lt;/li&gt;
&lt;li&gt;该服务的认证中间件开始鉴权：
a. &lt;strong&gt;JWT基础验证&lt;/strong&gt;: 验证签名和&lt;code&gt;exp&lt;/code&gt;。
b. &lt;strong&gt;本地缓存检查&lt;/strong&gt;: 从Token解析 &lt;code&gt;jti&lt;/code&gt;，查询&lt;strong&gt;节点本地的Redis&lt;/strong&gt;，检查 &lt;code&gt;revoked_jti:&amp;lt;jti&amp;gt;&lt;/code&gt; 是否存在。
c. 如果存在，立即拒绝(401)。
d. 如果不存在，鉴权通过，处理业务逻辑。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;3.2 会话吊销 (实时写路径)&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;用户（或管理员）发起登出/踢出请求，路由到&lt;strong&gt;中央认证服务&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;认证服务执行以下操作：
a. &lt;strong&gt;更新事实来源&lt;/strong&gt;: 更新 PostgreSQL 中 &lt;code&gt;RefreshToken&lt;/code&gt; 的 &lt;code&gt;isValid = false&lt;/code&gt;。
b. &lt;strong&gt;发布吊销事件&lt;/strong&gt;: 构造一条包含被吊销Token的 &lt;code&gt;jti&lt;/code&gt; 和 &lt;code&gt;exp&lt;/code&gt; 的消息。
c. 通过 &lt;strong&gt;NATS&lt;/strong&gt; 将此消息发布到全局 Topic (例如 &lt;code&gt;auth:revocation&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;全局同步&lt;/strong&gt;:
a. &lt;strong&gt;所有地域&lt;/strong&gt;的订阅者（一个轻量级服务）收到消息。
b. 订阅者解析消息，计算出 &lt;code&gt;ttl = exp - now()&lt;/code&gt;。
c. 立即将 &lt;code&gt;jti&lt;/code&gt; 写入其&lt;strong&gt;所在节点的本地Redis&lt;/strong&gt;，并设置TTL：&lt;code&gt;SET revoked_jti:&amp;lt;jti&amp;gt; 1 EX &amp;lt;ttl&amp;gt;&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;至此，该 &lt;code&gt;AccessToken&lt;/code&gt; 在全局所有节点都已即时失效。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;4. 高可用与容错设计&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;本地Redis缓存&lt;/strong&gt;: 通过 &lt;code&gt;DaemonSet&lt;/code&gt; 部署，单个Pod故障由K8s自愈。即使整个节点Redis故障，鉴权可降级为查询中央数据库（缓存回源），服务仍可用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消息总线 (NATS)&lt;/strong&gt;: 应以高可用集群模式部署。短暂中断会延迟吊销，但系统具备自愈能力（Redis的TTL会自动清理过期项）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中央数据库 (PostgreSQL)&lt;/strong&gt;: 是系统的核心依赖，必须采用主从复制、自动故障转移等高可用方案。它的故障将影响所有写操作（登录、刷新）。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Vue 3 实战 - 迷梦甄选</title><link>https://mc.mimeng.top/posts/frontend/vue3-mimeng-store/</link><guid isPermaLink="true">https://mc.mimeng.top/posts/frontend/vue3-mimeng-store/</guid><description>这是一个 Vue3 企业级实战项目笔记，涵盖了大部分实际开发中可能会使用到的库。</description><pubDate>Wed, 05 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;这是一个 Vue3 企业级实战项目笔记，涵盖了大部分实际开发中可能会使用到的库。
笔记中有自己的一些见解，与视频有部分差异。
视频教程可参考：&lt;a href=&quot;https://www.bilibili.com/video/BV1Xh411V7b5/&quot;&gt;尚硅谷Vue项目实战硅谷甄选，vue3项目+TypeScript前端项目一套通关_哔哩哔哩_bilibili&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;技术栈：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Vue 3&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;TypeScript&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Vite&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Element Plus&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Tailwind CSS&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;s&gt;SCSS&lt;/s&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Axios
规范化开发：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ESLint&lt;/li&gt;
&lt;li&gt;Prettier&lt;/li&gt;
&lt;li&gt;StyleLint&lt;/li&gt;
&lt;li&gt;Husky&lt;/li&gt;
&lt;li&gt;CommitLint&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;项目前期准备&lt;/h2&gt;
&lt;h3&gt;项目初始化&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pnpm create vite
&amp;gt; MiMengStore
&amp;gt; mi-meng-store
&amp;gt; Vue
&amp;gt; TypeScript

cd MiMengStore
pnpm install
code .
pnpm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;修改 &lt;code&gt;index.html&lt;/code&gt; 的 &lt;code&gt;title&lt;/code&gt; ，删除 &lt;code&gt;style.css&lt;/code&gt; ，清空 &lt;code&gt;App.vue&lt;/code&gt; 内的多余内容。&lt;/li&gt;
&lt;li&gt;修改 &lt;code&gt;package.json&lt;/code&gt; 中的 dev 脚本为 &lt;code&gt;vite --open&lt;/code&gt; ，自动打开浏览器。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;项目配置&lt;/h3&gt;
&lt;h4&gt;ESLint 校验代码工具&lt;/h4&gt;
&lt;h5&gt;安装&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;pnpm add eslint -D
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;npx eslint --init
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;√ How would you like to use ESLint? · problems
√ What type of modules does your project use? · esm
√ Which framework does your project use? · vue
√ Does your project use TypeScript? · typescript
√ Where does your code run? · browser
The config that you&apos;ve selected requires the following dependencies:

eslint, globals, @eslint/js, typescript-eslint, eslint-plugin-vue
√ Would you like to install them now? · No / Yes
√ Which package manager do you want to use? · pnpm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;eslint.config.js&lt;/code&gt; 中添加 &lt;code&gt;&quot;vue/multi-word-component-names&quot;: &quot;off&quot;&lt;/code&gt; 规则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import globals from &quot;globals&quot;;
import pluginJs from &quot;@eslint/js&quot;;
import tseslint from &quot;typescript-eslint&quot;;
import pluginVue from &quot;eslint-plugin-vue&quot;;
import { rules } from &quot;eslint-plugin-vue&quot;;


/** @type {import(&apos;eslint&apos;).Linter.Config[]} */
export default [
  { files: [&quot;**/*.{js,mjs,cjs,ts,vue}&quot;] },
  { languageOptions: { globals: globals.browser } },
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
  ...pluginVue.configs[&quot;flat/essential&quot;],
  {
    files: [&quot;**/*.vue&quot;], languageOptions: { parserOptions: { parser: tseslint.parser } }, rules: {
      ...rules,
      &quot;vue/multi-word-component-names&quot;: &quot;off&quot;,
    }
  }
];
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;eslintignore 忽略文件&lt;/h5&gt;
&lt;p&gt;创建 &lt;code&gt;.eslintignore&lt;/code&gt; 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dist
node_modules

.vscode
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;eslint 脚本&lt;/h5&gt;
&lt;p&gt;添加以下脚本到 &lt;code&gt;package.json&lt;/code&gt; ，&lt;code&gt;lint&lt;/code&gt; 用于校验语法， &lt;code&gt;fix&lt;/code&gt; 用于自动修补：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;scripts&quot;: {
	&quot;lint&quot;: &quot;eslint src&quot;,
    &quot;fix&quot;: &quot;eslint src --fix&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Prettier 格式化工具&lt;/h4&gt;
&lt;h5&gt;安装&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;pnpm add -D eslint-config-prettier eslint-plugin-prettier prettier
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;.prettierrc 规则文件&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;singleQuote&quot;: true,
  &quot;semi&quot;: false,
  &quot;tabWidth&quot;: 2
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;.prettierignore 忽略文件&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;**/*.svg
**/*.sh

.local

/dist/*
/node_modules/*
/public/* 
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;作为个人项目，下列 4 个没有太大的配置必要，这里不做详细说明，感兴趣可自行搜索。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;StyleLint 样式校验工具（待填）&lt;/h4&gt;
&lt;h4&gt;Husky Git 钩子（待填）&lt;/h4&gt;
&lt;h4&gt;CommitLint （待填）&lt;/h4&gt;
&lt;h4&gt;统一包管理器（待填）&lt;/h4&gt;
&lt;h3&gt;项目集成&lt;/h3&gt;
&lt;h4&gt;Element Plus&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://cn.element-plus.org/zh-CN/&quot;&gt;一个 Vue 3 UI 框架 | Element Plus&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;安装&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;pnpm install element-plus
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;按需引入&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://cn.element-plus.org/zh-CN/guide/quickstart.html#%E6%8C%89%E9%9C%80%E5%AF%BC%E5%85%A5&quot;&gt;快速开始 | Element Plus&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;pnpm add -D unplugin-vue-components unplugin-auto-import
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 &lt;code&gt;vite.config.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineConfig } from &apos;vite&apos;
import AutoImport from &apos;unplugin-auto-import/vite&apos;
import Components from &apos;unplugin-vue-components/vite&apos;
import { ElementPlusResolver } from &apos;unplugin-vue-components/resolvers&apos;

export default defineConfig({
  // ...
  plugins: [
    // ...
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
  ],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;Icon 图标&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://cn.element-plus.org/zh-CN/component/icon.html&quot;&gt;Icon 图标 | Element Plus&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;pnpm add @element-plus/icons-vue
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;按需导入以及 Iconify 集成&lt;/h6&gt;
&lt;pre&gt;&lt;code&gt;pnpm add -D unplugin-icons
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（可选）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm add -D @iconify/json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 &lt;code&gt;vite.config.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineConfig } from &apos;vite&apos;
import Icons from &apos;unplugin-icons/vite&apos;
import IconsResolver from &apos;unplugin-icons/resolver&apos;
import AutoImport from &apos;unplugin-auto-import/vite&apos;
import Components from &apos;unplugin-vue-components/vite&apos;

export default defineConfig({
  plugins: [
    AutoImport({
      resolvers: [
        // 自动导入图标组件
        IconsResolver({
          prefix: &apos;Icon&apos;,
        }),
      ],
    }),

    Components({
      resolvers: [
        // 自动注册图标组件
        IconsResolver({
          enabledCollections: [&apos;ep&apos;],
        }),
      ],
    }),
    Icons({
      autoInstall: true,
    }),
  ],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;完整配置代码示例见 &lt;a href=&quot;https://github.com/sxzz/element-plus-best-practices/blob/db2dfc983ccda5570033a0ac608a1bd9d9a7f658/vite.config.ts#L21-L58&quot;&gt;element-plus-best-practices/vite.config.ts&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5&gt;i18n 国际化&lt;/h5&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://cn.element-plus.org/zh-CN/guide/i18n.html&quot;&gt;国际化 | Element Plus&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;修改 &lt;code&gt;main.ts&lt;/code&gt; ，改为中文&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createApp } from &apos;vue&apos;
import App from &apos;./App.vue&apos;
import ElementPlus from &apos;element-plus&apos;
import zhCn from &apos;element-plus/es/locale/lang/zh-cn&apos;

const app = createApp(App)

app.use(ElementPlus, {
  locale: zhCn,
})

app.mount(&apos;#app&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;src 别名路径&lt;/h4&gt;
&lt;p&gt;修改 &lt;code&gt;vite.config.ts&lt;/code&gt; ：
`&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineConfig } from &apos;vite&apos;
export default defineConfig({
  resolve: {
    alias: {
      // 配置别名
      &apos;@&apos;: &apos;/src&apos;,
    }
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 &lt;code&gt;tsconfig.json&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;compilerOptions&quot;: {
    &quot;baseUrl&quot;: &quot;.&quot;,
    &quot;paths&quot;: {
      &quot;@/*&quot;: [&quot;src/*&quot;] 
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;环境变量&lt;/h4&gt;
&lt;p&gt;创建以下三个文件，分别为开发、生产、测试环境：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.env.development&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.env.production&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.env.test&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = &apos;development&apos;
VITE_APP_TITLE = &apos;迷梦甄选&apos;
VITE_APP_BASE_URL = &apos;/dev-api&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;NODE_ENV = &apos;production&apos;
VITE_APP_TITLE = &apos;迷梦甄选&apos;
VITE_APP_BASE_URL = &apos;/prod-api&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;NODE_ENV = &apos;test&apos;
VITE_APP_TITLE = &apos;迷梦甄选&apos;
VITE_APP_BASE_URL = &apos;/test-api&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可通过 &lt;code&gt;console.log(import.meta.env)&lt;/code&gt; 查看环境变量是否被正常加载。&lt;/p&gt;
&lt;p&gt;package.json 添加以下两条脚本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  &quot;scripts&quot;: {
    &quot;build:prod&quot;: &quot;vue-tsc -b &amp;amp;&amp;amp; vite build --mode production&quot;,
    &quot;build:test&quot;: &quot;vue-tsc -b &amp;amp;&amp;amp; vite build --mode test&quot;
  },
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;SVG 图标集成&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;pnpm add -D vite-plugin-svg-icons
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 &lt;code&gt;vite.config.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createSvgIconsPlugin } from &apos;vite-plugin-svg-icons&apos;
import path from &apos;path&apos;
export default () =&amp;gt; {
  return {
    plugins: [
      createSvgIconsPlugin({
        iconDirs: [path.resolve(process.cwd(), &apos;src/assets/icons&apos;)],
        symbolId: &apos;icon-[dir]-[name]&apos;,
      }),
    ],
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在入口文件 &lt;code&gt;main.ts&lt;/code&gt; 中引入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//@ts-expect-error: virtual module for SVG icons registration
import &apos;virtual:svg-icons-register&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;自定义组件（SvgIcon.vue）&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;svg :style=&quot;{ width: width, height: height }&quot;&amp;gt;
      &amp;lt;use :xlink:href=&quot;prefix + name&quot; :fill=&quot;color&quot;&amp;gt;&amp;lt;/use&amp;gt;
    &amp;lt;/svg&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;
defineProps({
  //xlink:href属性值的前缀
  prefix: {
    type: String,
    default: &apos;#icon-&apos;
  },
  //svg矢量图的名字
  name: String,
  //svg图标的颜色
  color: {
    type: String,
    default: &quot;&quot;
  },
  //svg宽度
  width: {
    type: String,
    default: &apos;16px&apos;
  },
  //svg高度
  height: {
    type: String,
    default: &apos;16px&apos;
  }

})
&amp;lt;/script&amp;gt;
&amp;lt;style scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;components&lt;/code&gt; 目录下创建 &lt;code&gt;index.ts&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import type { App } from &quot;vue&quot;
import SvgIcon from &quot;./SvgIcon.vue&quot;

const allGlobalComponents = { SvgIcon }

export default {
  install: (app: App) =&amp;gt; {
    for (const key in allGlobalComponents) {
      console.log(key)
      app.component(key, allGlobalComponents[key as keyof typeof allGlobalComponents])
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;main.ts&lt;/code&gt; 入口文件中注册为全局组件，避免频繁导入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import globalConponents from &apos;@/components&apos;
app.use(globalConponents)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;TailWind CSS 集成&lt;/h4&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://tailwindcss.com/docs/installation/using-vite&quot;&gt;Installing Tailwind CSS with Vite - Tailwind CSS&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;如果你更加习惯使用 Sass ，可以跳过这部分，采用后面的集成方案&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;pnpm add tailwindcss @tailwindcss/vite
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 &lt;code&gt;vite.config.ts&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineConfig } from &apos;vite&apos;
import tailwindcss from &apos;@tailwindcss/vite&apos;
export default defineConfig({
  plugins: [
    tailwindcss(),
  ],
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建 &lt;code&gt;styles&lt;/code&gt; 目录，创建 &lt;code&gt;index.css&lt;/code&gt; 文件&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;main.ts&lt;/code&gt; 中引入样式文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &apos;./styles/index.css&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;index.css&lt;/code&gt; 中 引入 &lt;code&gt;tainwindcss&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@import &quot;tailwindcss&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;s&gt;Sass 样式集成&lt;/s&gt;&lt;/h4&gt;
&lt;p&gt;创建 &lt;code&gt;styles&lt;/code&gt; 目录，新建 &lt;code&gt;index.scss&lt;/code&gt; 、 &lt;code&gt;variable.scss&lt;/code&gt; 文件&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;main.ts&lt;/code&gt; 中引入样式文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &apos;./styles/index.scss&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 &lt;code&gt;vite.config.ts&lt;/code&gt; 以支持全局变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default defineConfig((config) =&amp;gt; {
	css: {
      preprocessorOptions: {
        scss: {
          additionalData: &apos;@import &quot;/src/styles/variable.scss&quot;;&apos;,
        },
      },
    },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Normalize.css&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;pnpm add normalize.css
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;main.ts&lt;/code&gt; 引入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import &apos;normalize.css&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Mock 接口&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;pnpm add -D vite-plugin-mock mockjs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改 &lt;code&gt;vite.config.ts&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { defineConfig } from &apos;vite&apos;
import { viteMockServe } from &apos;vite-plugin-mock&apos;
export default ({ command })=&amp;gt; {
  return {
    plugins: [
      viteMockServe({
        localEnabled: command === &apos;serve&apos;,
      }),
    ],
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在根目录创建 &lt;code&gt;mock&lt;/code&gt; 文件夹，创建 &lt;code&gt;user.ts&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//用户信息数据
function createUserList() {
    return [
        {
            userId: 1,
            avatar:
                &apos;https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif&apos;,
            username: &apos;admin&apos;,
            password: &apos;111111&apos;,
            desc: &apos;平台管理员&apos;,
            roles: [&apos;平台管理员&apos;],
            buttons: [&apos;cuser.detail&apos;],
            routes: [&apos;home&apos;],
            token: &apos;Admin Token&apos;,
        },
        {
            userId: 2,
            avatar:
                &apos;https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif&apos;,
            username: &apos;system&apos;,
            password: &apos;111111&apos;,
            desc: &apos;系统管理员&apos;,
            roles: [&apos;系统管理员&apos;],
            buttons: [&apos;cuser.detail&apos;, &apos;cuser.user&apos;],
            routes: [&apos;home&apos;],
            token: &apos;System Token&apos;,
        },
    ]
}

export default [
    // 用户登录接口
    {
        url: &apos;/api/user/login&apos;,//请求地址
        method: &apos;post&apos;,//请求方式
        response: ({ body }) =&amp;gt; {
            //获取请求体携带过来的用户名与密码
            const { username, password } = body;
            //调用获取用户信息函数,用于判断是否有此用户
            const checkUser = createUserList().find(
                (item) =&amp;gt; item.username === username &amp;amp;&amp;amp; item.password === password,
            )
            //没有用户返回失败信息
            if (!checkUser) {
                return { code: 201, data: { message: &apos;账号或者密码不正确&apos; } }
            }
            //如果有返回成功信息
            const { token } = checkUser
            return { code: 200, data: { token } }
        },
    },
    // 获取用户信息
    {
        url: &apos;/api/user/info&apos;,
        method: &apos;get&apos;,
        response: (request) =&amp;gt; {
            //获取请求头携带token
            const token = request.headers.token;
            //查看用户信息是否包含有次token用户
            const checkUser = createUserList().find((item) =&amp;gt; item.token === token)
            //没有返回失败的信息
            if (!checkUser) {
                return { code: 201, data: { message: &apos;获取用户信息失败&apos; } }
            }
            //如果有返回成功信息
            return { code: 200, data: {checkUser} }
        },
    },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Axios 网络请求库&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;pnpm add axios
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试 Mock 接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import axios from &apos;axios&apos;

axios({
  method: &apos;post&apos;,
  url: &apos;/api/user/login&apos;,
  data: {
    username: &apos;admin&apos;,
    password: &apos;111111&apos;,
  },
}).then((res) =&amp;gt; {
  console.log(res.data)
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;能够看到如下输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;code&quot;: 200,
    &quot;data&quot;: {
        &quot;token&quot;: &quot;Admin Token&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;二次封装&lt;/h5&gt;
&lt;p&gt;创建 &lt;code&gt;utils/http.ts&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import axios from &apos;axios&apos;
//创建axios实例
const http = axios.create({
  baseURL: import.meta.env.VITE_APP_BASE_URL,
  timeout: 5000,
})
//请求拦截器
http.interceptors.request.use((config) =&amp;gt; {
  return config
})
//响应拦截器
http.interceptors.response.use(
  (response) =&amp;gt; {
    return response.data
  },
  (error) =&amp;gt; {
    //处理网络错误
    let msg = &apos;&apos;
    const status = error.response.status
    switch (status) {
      case 401:
        msg = &apos;token过期&apos;
        break
      case 403:
        msg = &apos;无权访问&apos;
        break
      case 404:
        msg = &apos;请求地址错误&apos;
        break
      case 500:
        msg = &apos;服务器出现问题&apos;
        break
      default:
        msg = &apos;无网络&apos;
    }
    ElMessage.error(msg)
    return Promise.reject(error)
  },
)
export default http
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你设置了 &lt;strong&gt;Element Plus&lt;/strong&gt; 按需引入，上面的代码中 &lt;code&gt;ElMessage&lt;/code&gt; 可能会报错，请不要添加 &lt;code&gt;import { ElMessage } from &quot;element-plus&quot;;&lt;/code&gt; ，这会导致样式丢失！
可在 &lt;code&gt;tsconfig.json&lt;/code&gt; 的 &lt;strong&gt;include&lt;/strong&gt; 添加 &lt;code&gt;auto-imports.d.ts&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;include&quot;: [&quot;auto-imports.d.ts&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了能成功请求，需要修改开发环境变量 &lt;code&gt;VITE_APP_BASE_URL&lt;/code&gt; 为 &lt;code&gt;/api&lt;/code&gt; 。&lt;/p&gt;
&lt;h4&gt;Pinia 状态管理&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;pnpm add pinia
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建 &lt;code&gt;store/index.ts&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createPinia } from &apos;pinia&apos;

const pinia = createPinia()

export default pinia
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;main.ts&lt;/code&gt; 中使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import pinia from &apos;./store&apos;
app.use(pinia)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;正式开发&lt;/h2&gt;
&lt;p&gt;在此之前可以先在 &lt;code&gt;index.css&lt;/code&gt; 中编写一些基础样式，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;body {
  width: 100vw;
  height: 100vh;
}

#app {
  width: 100%;
  height: 100%;
}

#app main {
  width: 100%;
  height: 100%;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;API 接口统一管理&lt;/h3&gt;
&lt;p&gt;创建 &lt;code&gt;api/user.ts&lt;/code&gt;  ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import http from &apos;@/utils/http&apos;

//项目用户相关的请求地址
enum API {
  LOGIN_URL = &apos;/user/login&apos;,
  USERINFO_URL = &apos;/user/info&apos;,
  LOGOUT_URL = &apos;/user/logout&apos;,
}

// 登录表单数据类型
export interface loginFormData {
  username: string
  password: string
}

// 登录响应数据类型
export interface loginResponseData {
  code: number
  data: {
    token: string
  }
}

export interface userInfo {
  userId: number
  avatar: string
  username: string
  desc: string
  roles: string[]
  buttons: string[]
  routes: string[]
  token: string
}

// 用户信息响应数据类型
export interface userInfoReponseData {
  code: number
  data: userInfo
}

//登录接口
export const LoginAPI = (data: loginFormData) =&amp;gt;
  http.post&amp;lt;loginFormData, loginResponseData&amp;gt;(API.LOGIN_URL, data)

//获取用户信息
export const UserInfoAPI = () =&amp;gt; http.get&amp;lt;userInfoReponseData&amp;gt;(API.USERINFO_URL)

//退出登录
export const LogoutAPI = () =&amp;gt; http.post(API.LOGOUT_URL)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;基础路由配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pnpm add vue-router
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;views&lt;/code&gt; 目录下分别创建 &lt;code&gt;404&lt;/code&gt; 、 &lt;code&gt;home&lt;/code&gt; 、 &lt;code&gt;login&lt;/code&gt; 目录，并分别创建 &lt;code&gt;index.vue&lt;/code&gt; ，编写基础内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;h1&amp;gt;Home&amp;lt;/h1&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/template&amp;gt;

&amp;lt;script setup lang=&quot;ts&quot;&amp;gt;

&amp;lt;/script&amp;gt;

&amp;lt;style lang=&quot;scss&quot; scoped&amp;gt;&amp;lt;/style&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建 &lt;code&gt;router/routes.ts&lt;/code&gt; ，暴露一个常量路由：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export const constantRoutes = [
  {
    path: &apos;/login&apos;,
    component: () =&amp;gt; import(&apos;@/views/login/index.vue&apos;),
    name: &apos;login&apos;,
  },
  {
    path: &apos;/&apos;,
    component: () =&amp;gt; import(&apos;@/views/home/index.vue&apos;),
    name: &apos;home&apos;,
  },
  {
    path: &apos;/404&apos;,
    component: () =&amp;gt; import(&apos;@/views/404/index.vue&apos;),
  },
  {
    path: &apos;/:pathMatch(.*)*&apos;,
    redirect: &apos;/404&apos;,
  },
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建 &lt;code&gt;router/index.ts&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { createRouter, createWebHashHistory } from &apos;vue-router&apos;
import { constantRoutes } from &apos;./routes&apos;

const router = createRouter({
  history: createWebHashHistory(),
  routes: constantRoutes,
  scrollBehavior: () =&amp;gt; ({ left: 0, top: 0 }),
})

export default router
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;main.ts&lt;/code&gt; 中使用路由&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import router from &apos;./router&apos;
app.use(router)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;App.vue&lt;/code&gt; 添加 &lt;code&gt;&amp;lt;router-view /&amp;gt;&lt;/code&gt; ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;template&amp;gt;
  &amp;lt;main&amp;gt;
    &amp;lt;router-view /&amp;gt;
  &amp;lt;/main&amp;gt;
&amp;lt;/template&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;登录页&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;见 &lt;code&gt;src/views/login/index.vue&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;编写基础样式后，为 Button 添加 &lt;code&gt;@click&lt;/code&gt; 事件绑定到 &lt;code&gt;login&lt;/code&gt; 函数，并且处理登录请求成功与失败情况。&lt;/p&gt;
&lt;p&gt;创建 &lt;code&gt;store/modules/user.ts&lt;/code&gt; ，实现以下功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户信息持久化存储&lt;/li&gt;
&lt;li&gt;请求登录方法&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import { LoginAPI, loginFormData } from &apos;@/api/user&apos;
import { defineStore } from &apos;pinia&apos;

export const useUserStore = defineStore(&apos;user&apos;, {
  state: () =&amp;gt; {
    return {
      token: localStorage.getItem(&apos;token&apos;) || null,
    }
  },
  getters: {},
  actions: {
    async login(loginForm: loginFormData): Promise&amp;lt;boolean&amp;gt; {
      const res = await LoginAPI(loginForm)
      if (res.code === 200) {
        this.token = res.data.token
        localStorage.setItem(&apos;token&apos;, res.data.token)
        return true
      } else {
        return false
      }
    },
  },
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写 login 函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useUserStore } from &apos;@store/modules/user&apos;
const userStore = useUserStore()
const login = async () =&amp;gt; {
  try {
    const result = await userStore.login(loginForm);
    if (result) {
      $router.replace(&apos;/&apos;);
    }
  } catch (error) {
    console.log(error);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;后续内容流程类似，不做详细记录，具体参见视频教程。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item></channel></rss>