diff --git a/Eino/quick_start/chapter_05_middleware.md b/Eino/quick_start/chapter_05_middleware.md
index f4ae03d..0236b1f 100644
--- a/Eino/quick_start/chapter_05_middleware.md
+++ b/Eino/quick_start/chapter_05_middleware.md
@@ -125,6 +125,59 @@ flowchart LR
>
> 将 `safeToolMiddleware`(错误捕获)放在最内层(数组末尾),确保其他 Middleware 抛出的中断错误能正确向外传播,不被吞掉。
+### 深入:为什么安全中间件要放在最内层?
+
+很多开发者会问:**为什么不在最外层放一个全局的 `RecoverMiddleware` 来兜底?** 要理解这一点,需要区分三种不同的错误处理方式:
+
+| 方式 | 捕获目标 | 处理策略 | 放置位置 |
+|------|---------|---------|---------|
+| **安全转换** (`safeToolMiddleware`) | 业务错误(文件不存在、参数错误) | 转为字符串喂给模型,让 Agent 自愈 | **最内层**(工具调用前) |
+| **中断传播** | `InterruptRerunError` 等控制信号 | 不做任何转换,原样向上抛出 | 贯穿所有层 |
+| **系统兜底** (`defer recover()`) | `panic`(nil pointer、数组越界) | 记录日志,保护进程不崩溃 | **入口层**(如 `main()` / `http.Server`) |
+
+```go
+// ❶ 最内层:业务错误转换 —— 模型可以继续执行
+func (m *safeToolMiddleware) WrapInvokableToolCall(...) (...) {
+ return func(ctx context.Context, args string, opts ...tool.Option) (string, error) {
+ result, err := endpoint(ctx, args, opts...)
+ if err != nil {
+ if _, ok := compose.IsInterruptRerunError(err); ok {
+ return "", err // ⚠️ 中断错误必须穿透所有中间件
+ }
+ return fmt.Sprintf("[tool error] %v", err), nil // ✅ 业务错误转字符串
+ }
+ return result, nil
+ }
+}
+
+// ❷ 入口处:panic 兜底 —— 保护进程
+func main() {
+ defer func() {
+ if r := recover(); r != nil {
+ log.Printf("recovered from panic: %v", r)
+ }
+ }()
+ // ... 启动 Agent 服务
+}
+```
+
+> [!question] 进阶思考
+>
+> 如果把 `safeToolMiddleware` 移到最外层(数组首位),会发生什么?
+>
+>
+> 🔍 点击查看推导
+>
+> 考虑这个场景:用户在 Agent 多轮对话中点击了"停止"按钮,底层产生了一个 `InterruptRerunError`。
+>
+> 1. Tool 执行器检测到中断信号,抛出 `InterruptRerunError`
+> 2. 如果 `safeToolMiddleware` 在外层——此时请求还在外层尚未进入内层,**中断信号在内层往外冒泡时会首先经过内层中间件**
+> 3. 因为中断信号是在最内层的 Tool 处产生的,无论 `safeToolMiddleware` 在哪一层,只要它检查了 `IsInterruptRerunError` 就不会吞掉它
+> 4. 但真正的问题是:如果内层的其他逻辑(非中断)也出错,外层中间件还没来得及处理就被内层的错误"跳过"了
+>
+> 更准确地说,顺序的关键在于:**中间件是按装饰器模式嵌套的,外层包裹内层,内层最先执行也最先返回**。内层先做错误分类,外层再做全局处理,这样既保证了中断信号的畅通,又保证了业务错误的收敛。
+>
+
### ModelRetryConfig:内置重试配置
`ModelRetryConfig` 提供了 ChatModel 级别的自动重试能力:
diff --git a/hhs/GORM/02-模型定义.md b/hhs/GORM/02-模型定义.md
index 3b13615..4842298 100644
--- a/hhs/GORM/02-模型定义.md
+++ b/hhs/GORM/02-模型定义.md
@@ -174,6 +174,9 @@ db.Create(&order)
> [!tip] 为什么选 UUID 做主键?
> 自增 ID 存在 URL 泄露、ID 猜测等安全风险。UUID v4 完全随机,不会暴露数据量级。代价是索引碎片化更严重——InnoDB 的聚簇索引以主键排序,随机插入会导致页分裂,吞吐量下降约 30%。如果性能敏感可考虑 [ULID](https://github.com/ulid/spec)(有序且随机),或雪花算法生成的 `int64`。
+> [!abstract] 子文档
+> - [[02-模型定义/UUID主键策略]] — UUID 版本选型(v4/v7)、BeforeCreate 自动生成、存储优化(char(36) vs binary(16))、InnoDB 页分裂原理、ULID / Snowflake 替代方案、决策流程
+
### 复合主键
```go
diff --git a/hhs/GORM/02-模型定义/UUID主键策略.md b/hhs/GORM/02-模型定义/UUID主键策略.md
new file mode 100644
index 0000000..f0c6c9e
--- /dev/null
+++ b/hhs/GORM/02-模型定义/UUID主键策略.md
@@ -0,0 +1,320 @@
+---
+tags: [GORM, Go, UUID, 主键, 分布式, 性能优化, InnoDB]
+create time: 2026-05-05 00:00
+---
+
+# UUID 主键策略
+
+## 概述
+
+自增 ID 简单高效,但在分布式系统和对外 API 中暴露出明显短板——ID 可被猜测、数据规模暴露、多节点冲突难解。UUID 提供了去中心化的主键生成方案,代价则是对 InnoDB 写入性能的冲击。本文从选型到落地,完整讲解在 GORM 中使用 UUID 的方方面面。
+
+## 自增 ID 的隐性代价
+
+```go
+// 这是最直观的写法——但你很快会发现它的问题
+type User struct {
+ ID uint `gorm:"primaryKey;autoIncrement"`
+ Name string
+}
+
+// RESTful API 中,用户的个人资料页 URL 可能是这样:
+// GET /users/1 → 某个用户
+// GET /users/2 → 另一个用户
+// GET /users/100 → 哦,你们只有 100 个用户?
+```
+
+> [!warning] 自增 ID 的三大风险
+> 1. **信息泄露**:通过 ID 可推断用户总量、增长速度、业务规模。
+> 2. **枚举攻击**:遍历 `/users/1`、`/users/2`、`/users/3`……轻松抓取全量数据。
+> 3. **分布式冲突**:多个数据库节点各自自增,合并时必然产生重复 ID。
+
+> [!question] 思考
+> 如果只是在 API 层面对外暴露一个哈希过的「展示 ID」、内部仍用自增主键,能否解决上述问题?
+> **部分可以**——它能隐藏数据规模,但增加了额外的映射开销(哈希冲突处理、额外索引等)。如果业务本身就是分布式的,自增 ID 的主键冲突问题依然无解。因此 UUID 在分布式场景下是更根本的解法。
+
+## UUID 版本选型
+
+UUID 不是一个单一的算法,它有多个版本,选错版本等于白用。
+
+| 版本 | 生成依据 | 有序性 | 适用场景 |
+|------|----------|--------|----------|
+| v1 | 时间戳 + MAC 地址 | 有序 | 需全局唯一 + 可排序,但有 MAC 隐私风险 |
+| v4 | 随机数 | 完全无序 | 最通用,隐私性好 |
+| v7 | 毫秒时间戳 + 随机数 | 时间有序 | **推荐**:兼顾唯一性与 InnoDB 写入性能 |
+
+```go
+import "github.com/google/uuid"
+
+// UUID v4 — 最常用但写入性能差
+id := uuid.New() // 例如: 550e8400-e29b-41d4-a716-446655440000
+
+// UUID v7 — Go 1.24+ 才原生支持,现阶段用 google/uuid 的 Must 方法
+id, _ := uuid.NewV7() // 例如: 018f3a80-8c5d-7a1b-b000-6b3a1c234567
+// ^^^^^^^^ 时间戳部分使写入更有序
+```
+
+> [!tip] 优先使用 UUID v7
+> UUID v7 的前 48 位是毫秒级时间戳(Unix Epoch),使得生成的 ID 天然按时间递增。在 InnoDB 聚簇索引下,这能大幅减少页分裂——后文会详细说明原因。
+
+## GORM 中的三种 UUID 用法
+
+### 方式一:手动赋值(最直接)
+
+```go
+import "github.com/google/uuid"
+
+type Order struct {
+ ID uuid.UUID `gorm:"type:char(36);primaryKey"`
+ Code string `gorm:"uniqueIndex;not null"`
+}
+
+// 每次 Create 前手动生成
+order := Order{
+ ID: uuid.New(),
+ Code: "ORD-20240001",
+}
+db.Create(&order)
+```
+
+> [!warning] 手动赋值的问题
+> 如果你忘记在某个 `Create` 调用前生成 UUID,GORM 不会报错——它会尝试插入零值 `00000000-0000-0000-0000-000000000000`,导致重复主键错误。下面的方式二可以根治此问题。
+
+### 方式二:BeforeCreate 钩子(推荐)
+
+```go
+type Order struct {
+ ID uuid.UUID `gorm:"type:char(36);primaryKey"`
+ Code string `gorm:"uniqueIndex;size:32;not null"`
+ CreatedAt time.Time
+}
+
+// BeforeCreate:GORM 在每次 INSERT 前自动调用,ID 零值时自动生成
+func (o *Order) BeforeCreate(tx *gorm.DB) error {
+ if o.ID == uuid.Nil { // uuid.Nil 是零值判断的标准方式
+ o.ID = uuid.New()
+ }
+ return nil
+}
+
+// 调用方无需关心 ID 生成
+db.Create(&Order{Code: "ORD-20240002"}) // ID 自动填充
+```
+
+> [!tip] 为什么还要检查 `uuid.Nil`?
+> 如果调用方已经显式设置了 ID(例如从消息队列消费带有 ID 的数据),`BeforeCreate` 不应覆盖它。这个检查确保了「只在未设置时自动生成」的语义。
+
+### 方式三:自定义类型封装(适合团队规范)
+
+```go
+type PK uuid.UUID // 自定义主键类型
+
+func (pk *PK) BeforeCreate(tx *gorm.DB) error {
+ if *pk == PK(uuid.Nil) {
+ *pk = PK(uuid.New())
+ }
+ return nil
+}
+
+// Value:写入数据库时的转换
+func (pk PK) Value() (driver.Value, error) {
+ return uuid.UUID(pk).String(), nil
+}
+
+// Scan:从数据库读取时的转换
+func (pk *PK) Scan(value interface{}) error {
+ id, err := uuid.Parse(string(value.([]byte)))
+ if err != nil {
+ return err
+ }
+ *pk = PK(id)
+ return nil
+}
+
+// 使用示例
+type Product struct {
+ ID PK `gorm:"type:char(36);primaryKey"`
+ Name string `gorm:"size:128;not null"`
+}
+```
+
+> [!note] 封装自定义类型的收益
+> - 所有使用 `PK` 的模型自动获得 UUID 生成能力,零样板代码。
+> - 团队统一主键策略——Code Review 时一看 `PK` 就明白这是 UUID 主键模型。
+> - 将来如果想从 v4 切换到 v7,只需改 `BeforeCreate` 一处。
+
+## 存储优化:char(36) vs binary(16)
+
+UUID 的标准字符串形式是 `550e8400-e29b-41d4-a716-446655440000`,占 36 字节。但实际的 UUID 数据只需 16 字节。
+
+```go
+// 方案 A:存为字符串(可读性好)
+type OrderA struct {
+ ID uuid.UUID `gorm:"type:char(36);primaryKey"`
+ // 每条记录主键占用 36 字节 + 索引开销
+}
+
+// 方案 B:存为 binary(16)(空间效率高,推荐)
+type OrderB struct {
+ ID uuid.UUID `gorm:"type:binary(16);primaryKey"`
+ // 每条记录主键仅占 16 字节
+}
+```
+
+| 方案 | 主键大小 | 1000 万行主键索引体积 | 可读性 |
+|------|----------|----------------------|--------|
+| `char(36)` | 36 字节 | ~360 MB(不含辅助索引) | 直接 SELECT 可读 |
+| `binary(16)` | 16 字节 | ~160 MB | 需 `HEX()` 或 `UUID_TO_BIN()` 转换 |
+
+> [!tip] MySQL 特殊技巧
+> MySQL 提供了 `UUID_TO_BIN()` 和 `BIN_TO_UUID()` 函数,可在存储时自动做二进制转换。如果使用原生 SQL 或迁移脚本建表,可以配合触发器自动处理。但在 GORM 中,用 `type:binary(16)` + `Value/Scan` 接口手动控制更为灵活。
+
+> [!question] 思考
+> 如果你采用 `binary(16)` 存储,在数据库管理工具(如 DataGrip、DBeaver)中查看数据时主键列显示为乱码——你会如何处理?
+> **两种思路**:① 查询时用 `SELECT BIN_TO_UUID(id), ... FROM orders`;② 保留 `char(36)`,毕竟现代 SSD 下 20 字节的差异对大多数业务影响微乎其微。**对于内部系统或数据量不大(<1000 万)的场景,char(36) 的可读性优势远大于空间节省的收益。**
+
+## 性能深度解析:UUID 为什么"慢"
+
+### InnoDB 的聚簇索引机制
+
+```mermaid
+graph LR
+ A["INSERT 自增 ID
id=1001"] --> B["写入最后一页"]
+ C["INSERT UUID v4
随机值"] --> D["定位目标页
(可能已刷出)"] --> E["页已满 →
页分裂"] --> F["分裂后的两页
各写一半"]
+```
+
+InnoDB 使用聚簇索引——表数据按主键物理排序存储在 B+ 树中:
+
+- **自增 ID**:每次插入都在 B+ 树的「最右侧」追加新页,触发的页分裂极少。
+- **UUID v4**:随机值意味着新行可能插入到 B+ 树的**任意位置**。如果目标页已满,InnoDB 必须执行页分裂——将一页拆成两页,重新平衡树结构。这导致:
+ - 磁盘随机 I/O 增加
+ - 页利用率下降(分裂后的页各约 50% 填充率)
+ - 缓冲池命中率降低
+
+> [!note] 性能下降的量级
+> 业界测试表明,UUID v4 的写入吞吐量比自增 ID 低约 **25%-40%**,页分裂导致的数据碎片化还会拖慢范围查询。但请注意——这是在**持续高并发写入**场景下的差异。如果你的写入 QPS 在几百级别,日常几乎感受不到。
+
+### UUID v7:兼顾唯一性与有序性
+
+UUID v7 的前 48 位是毫秒级 Unix 时间戳,使得不同毫秒生成的 ID 天然递增:
+
+```
+UUID v4: e2d3f1a4-... (随机) → 随机插入,页分裂严重
+UUID v7: 018f3a80-... (时间) → 大致有序,页分裂大幅减少
+ ^^^^^^^^
+ 毫秒时间戳,同一毫秒内顺序排列
+```
+
+> [!warning] 同一毫秒内的并发写入
+> UUID v7 在同一毫秒内的多个 ID,随机部分决定了它们的相对顺序。因此同一毫秒内的批量写入仍可能触发少量页分裂——但这远比 v4 的全随机要温和。
+
+## 替代方案对比
+
+| 方案 | 长度 | 有序 | 全局唯一 | 可读性 | 性能(vs 自增) |
+|------|------|------|----------|--------|-----------------|
+| 自增 ID (`int64`) | 8 字节 | 有序 | ❌ 单点 | 高 | 基准 |
+| UUID v4 | 16 字节 | 无序 | ✅ | 中(字符串) | -30% |
+| UUID v7 | 16 字节 | 大致有序 | ✅ | 中(字符串) | -10% |
+| ULID | 16 字节 / 26 字符 | 有序 | ✅ | 高(Crockford Base32) | -10% |
+| Snowflake (`int64`) | 8 字节 | 有序 | ✅ | 低(需反解) | -5% |
+
+### ULID 示例
+
+[ULID](https://github.com/ulid/spec) 的结构与 UUID v7 类似(时间戳 + 随机数),但使用 Crockford Base32 编码,生成的字符串不含混淆字符(如 `I`、`L`、`O`),对用户更友好:
+
+```go
+import "github.com/oklog/ulid/v2"
+
+type Article struct {
+ ID string `gorm:"type:char(26);primaryKey"` // ULID 固定 26 字符
+ // ...
+}
+
+func (a *Article) BeforeCreate(tx *gorm.DB) error {
+ if a.ID == "" {
+ a.ID = ulid.Make().String() // 例如: 01HX3V8M2P4K5Q6R7S8T9V0W1X
+ }
+ return nil
+}
+```
+
+### Snowflake(雪花算法)
+
+适合对 ID 长度敏感、且已有分布式 ID 生成基础设施的场景:
+
+```go
+import "github.com/bwmarrin/snowflake"
+
+var snowflakeNode *snowflake.Node
+
+func init() {
+ snowflakeNode, _ = snowflake.NewNode(1) // 节点编号 1
+}
+
+// 生成 int64 ID:ID 包含时间戳 + 节点 ID + 序列号
+id := snowflakeNode.Generate().Int64() // 例如: 1541828496137482240
+```
+
+> [!tip] Snowflake 的取舍
+> Snowflake 小巧(8 字节),性能优异,但需要部署节点编号服务(或依赖 Redis/etcd 协调 WorkerID)。而 UUID v7 是纯本地生成,零依赖。**团队规模小(<5 个节点)优先用 UUID v7,大规模微服务架构再考虑 Snowflake。**
+
+## UUID 作为外键
+
+当主键使用 UUID 时,关联表的外键也必须使用相同类型:
+
+```go
+type Order struct {
+ ID uuid.UUID `gorm:"type:char(36);primaryKey"`
+ OrderCode string `gorm:"uniqueIndex;size:32;not null"`
+}
+
+type OrderItem struct {
+ ID uuid.UUID `gorm:"type:char(36);primaryKey"`
+ OrderID uuid.UUID `gorm:"type:char(36);not null;index"` // 外键列
+ ProductID uuid.UUID `gorm:"type:char(36);not null"`
+ Quantity int `gorm:"not null"`
+ Order Order `gorm:"foreignKey:OrderID"` // GORM 关联定义
+}
+
+func (oi *OrderItem) BeforeCreate(tx *gorm.DB) error {
+ if oi.ID == uuid.Nil {
+ oi.ID = uuid.New()
+ }
+ return nil
+}
+
+// 预加载关联查询
+var items []OrderItem
+db.Preload("Order").Where("order_id = ?", orderID).Find(&items)
+```
+
+> [!warning] 外键 UUID 的索引考量
+> 外键列通常需要创建索引(如上例中的 `index` tag)。如果 OrderItem 数据量很大(千万级),`order_id` 的 UUID 索引同样会面临 InnoDB 页分裂问题。此时可以考虑在该索引上使用 `binary(16)` 存储来减少索引体积。
+
+## 决策流程
+
+```mermaid
+graph TD
+ A["选择主键策略"] --> B{"是否需要
分布式部署?"}
+ B -->|"否,单数据库"| C{"API 是否
对外暴露 ID?"}
+ C -->|"否,纯内部"| D["自增 ID (uint)
简单高效"]
+ C -->|"是"| E["UUID v7
binary(16)存储"]
+ B -->|"是"| F{"写 QPS 是否
超过 5000?"}
+ F -->|"否"| E
+ F -->|"是"| G{"团队是否有
分布式 ID 基础设施?"}
+ G -->|"有"| H["Snowflake (int64)
8字节,高吞吐"]
+ G -->|"无"| I["UUID v7
配合 binary(16)
可接受性能折损"]
+ style D fill:#10B981,color:#fff
+ style E fill:#3B82F6,color:#fff
+ style H fill:#F59E0B,color:#fff
+ style I fill:#EF4444,color:#fff
+```
+
+> [!summary] 一句话建议
+> **2026 年了,新项目默认用 UUID v7 + `binary(16)` 存储。** 除非你在做极致的写入性能优化(如日志采集、时序数据),否则它带来的分布式友好性和隐私保护远超那 10% 的性能差异。
+
+## 关联笔记
+
+- [[02-模型定义]] — 父文档,包含主键策略概述与其他字段定义
+- [[03-CRUD 操作]] — UUID 模型的增删改查操作
+- [[12-自定义字段类型]] — Value/Scan 接口的更多用法
diff --git a/hzh/AI/怎么创建一个自己的 SKILL.md b/hzh/AI/怎么创建一个自己的 SKILL.md
index 4c189be..d2c6d34 100644
--- a/hzh/AI/怎么创建一个自己的 SKILL.md
+++ b/hzh/AI/怎么创建一个自己的 SKILL.md
@@ -110,13 +110,13 @@ trigger: "/loop"
常见的触发方式:
-| 方式 | 示例 | 适用场景 |
-|------|------|---------|
-| **关键词匹配** | `trigger: "send email"` | 功能明确的任务 |
-| **文件路径匹配** | `trigger: "\.secrets\.""` | 特定文件或目录变更时 |
-| **命令前缀** | `trigger: "/deploy"` | Slash Command 触发 |
-| **正则表达式** | `trigger: "imports .anthropic."` | 复杂模式匹配 |
-| **手动 @ 引用** | `@my-skill` | 用户主动调用 |
+| 方式 | 示例 | 适用场景 |
+| ----------- | -------------------------------- | ---------------- |
+| **关键词匹配** | `trigger: "send email"` | 功能明确的任务 |
+| **文件路径匹配** | `trigger: "\.secrets\.""` | 特定文件或目录变更时 |
+| **命令前缀** | `trigger: "/deploy"` | Slash Command 触发 |
+| **正则表达式** | `trigger: "imports .anthropic."` | 复杂模式匹配 |
+| **手动 @ 引用** | `@my-skill` | 用户主动调用 |
> [!TIP] ⚡ 最佳实践
> Trigger 要尽可能精确。过于宽泛会导致错误的 Skill 被激活,干扰正常对话。遵循 **"最小充分原则"**:用最少的匹配条件覆盖目标场景。