Skip to content

学习前置准备

在学习本课程之前,请先将 nest-commander 仓库克隆到本地 external 目录:

# 使用 depth=1 减少克隆时间
git clone --depth=1 https://github.com/jmcdo29/nest-commander.git external/nest-commander

当前文档信息:

  • 研究分支 (Branch): master
  • Git SHA1: b0493c16e52636fc05edf6dcb7d42c573bbbd0a3
  • 外部仓库位置: external/nest-commander/

Nest Commander 学习资料

1. 项目概述

Nest Commander 是一个为 NestJS 框架设计的 CLI (命令行界面) 构建工具。它允许开发者使用与 NestJS 相同的装饰器模式和依赖注入机制来构建命令行应用程序。该项目基于流行的 Commander.js 包构建。

  • GitHub 仓库: https://github.com/jmcdo29/nest-commander
  • 当前版本: 3.20.1
  • 许可证: MIT
  • 作者: Jay McDoniel

2. 项目架构

2.1 Monorepo 结构

该项目使用 Nx 作为 Monorepo 管理工具,包含以下主要包:

包名 版本 描述
nest-commander 3.20.1 核心包,提供 CLI 构建能力
nest-commander-testing 3.5.1 测试工具包
nest-commander-schematics 3.2.0 Angular Schematics 代码生成工具

2.2 目录结构

nest-commander/
├── apps/
│   └── docs/              # Astro 文档站点
├── packages/
│   ├── nest-commander/    # 核心包
│   ├── nest-commander-testing/  # 测试工具
│   └── nest-commander-schematics/ # 代码生成
├── integration/           # 集成测试 (21 个测试用例)
├── package.json           # 根 package.json
└── nx.json               # Nx 配置

2.3 核心依赖

主要依赖:

  • commander: 11.1.0 - CLI 参数解析
  • @golevelup/nestjs-discovery: 5.0.0 - NestJS 服务发现
  • inquirer: 8.2.7 - 交互式命令行提示
  • cosmiconfig: 8.3.6 - 配置文件加载
  • @fig/complete-commander: 3.0.0 - 命令补全

对等依赖 (Peer Dependencies):

  • @nestjs/common: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0
  • @nestjs/core: ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0

3. 核心功能模块

3.1 命令定义 (@Command() 装饰器)

使用装饰器定义 CLI 命令:

@Command({
  name: 'greet',
  arguments: '<name>',
  description: 'Greet someone',
})
export class GreetCommand implements CommandRunner {
  async run([name]: string[], options: Record<string, any>): Promise<void> {
    console.log(`Hello, ${name}!`);
  }
}

3.2 选项定义 (@Option() 装饰器)

@Option({
  flags: '-l, --love',
  description: 'Express your love',
  defaultValue: false,
})
parseLove(val: string): boolean {
  return JSON.parse(val);
}

3.3 命令工厂 (CommandFactory)

import { CommandFactory } from 'nest-commander';

async function bootstrap() {
  await CommandFactory.run(RootModule, ['greet', 'bye']);
}

4. 集成测试覆盖

项目包含 21 个集成测试用例,覆盖以下功能场景:

测试目录 功能描述
basic 基础命令功能
multiple 多命令支持
sub-commands 子命令
default-sub-commands 默认子命令
root-command 根命令
this-command 命令嵌套
this-handler 自定义处理器
dot-command 点命令
help-tests 帮助信息
version-option 版本选项
option-choices 选项可选值
output-config 输出配置
pizza Inquirer 交互式提问
with-questions 交互式问题
plugins 插件系统
register-provider 自定义 Provider
request-provider-override Provider 覆盖

5. 学习计划

5.1 入门阶段 (预计 2-3 小时)

  1. 环境准备

  2. 安装 Node.js (推荐 LTS 版本)

  3. 克隆项目仓库
  4. 安装 pnpm 依赖: pnpm install

  5. 基础概念学习

  6. 阅读官方文档: https://nest-commander.jaymcdoniel.dev

  7. 理解 @Command() 装饰器
  8. 理解 @Option() 装饰器
  9. 了解 CommandRunner 接口

  10. Hello World 示例

  11. 创建第一个命令

  12. 使用 CommandFactory 启动

5.2 进阶阶段 (预计 3-4 小时)

  1. 依赖注入

  2. 在命令中使用 NestJS 依赖注入

  3. 自定义 Provider 注册

  4. 子命令与嵌套

#### 5.1 子命令基础用法

使用 @SubCommand() 装饰器定义子命令,通过父命令的 subCommands 数组注册:

import { Command, CommandRunner } from 'nest-commander';
import { SubCommand } from 'nest-commander';

// 定义子命令
@SubCommand({ name: 'mid-1' })
export class Mid1Command extends CommandRunner {
  async run() {
    console.log('mid-1 command executed');
  }
}

// 父命令注册子命令
@Command({
  name: 'top',
  subCommands: [Mid1Command],
})
export class TopCommand extends CommandRunner {
  async run() {
    console.log('top command');
  }
}

#### 5.2 子命令嵌套

子命令可以拥有自己的子命令,形成多层嵌套结构:

// 底层子命令
@SubCommand({ name: 'bottom' })
export class BottomCommand extends CommandRunner {
  async run() {
    console.log('bottom command');
  }
}

// 中层子命令(拥有自己的子命令)
@SubCommand({ name: 'mid-1', subCommands: [BottomCommand] })
export class Mid1Command extends CommandRunner {
  async run() {
    console.log('mid-1 command');
  }
}

// 顶层命令
@Command({
  name: 'top',
  subCommands: [Mid1Command],
})
export class TopCommand extends CommandRunner {
  async run() {
    console.log('top command');
  }
}

运行效果:

  • top → 执行 TopCommand
  • top mid-1 → 执行 Mid1Command
  • top mid-1 bottom → 执行 BottomCommand

#### 5.3 默认子命令

使用 options: { isDefault: true } 设置默认子命令,当不指定子命令时自动执行:

@SubCommand({
  name: 'mid-1',
  subCommands: [BottomCommand],
  options: { isDefault: true },  // 设置为默认子命令
})
export class Mid1Command extends CommandRunner {
  async run() {
    console.log('default sub command');
  }
}

运行 top 时会自动执行 mid-1 子命令。

#### 5.4 子命令别名

使用 aliases 为子命令设置简短别名:

@SubCommand({ name: 'mid-2', aliases: ['m'] })
export class Mid2Command extends CommandRunner {
  async run() {
    console.log('mid-2 command');
  }
}

现在可以使用 top mid-2 或 top m 两种方式调用。

#### 5.5 完整示例:多层嵌套命令

结合以上特性,创建一个完整的多层嵌套命令:

// bottom.command.ts
@SubCommand({ name: 'bottom' })
export class BottomCommand extends CommandRunner {
  constructor(private readonly log: LogService) { super(); }
  async run() { this.log.log('bottom command executed'); }
}

// mid-1.command.ts(带默认子命令)
@SubCommand({
  name: 'mid-1',
  subCommands: [BottomCommand],
  options: { isDefault: true },
})
export class Mid1Command extends CommandRunner {
  constructor(private readonly log: LogService) { super(); }
  async run() { this.log.log('mid-1 command executed'); }
}

// mid-2.command.ts(带别名)
@SubCommand({ name: 'mid-2', aliases: ['m'] })
export class Mid2Command extends CommandRunner {
  constructor(private readonly log: LogService) { super(); }
  async run() { this.log.log('mid-2 command executed'); }
}

// top.command.ts
@Command({
  name: 'top',
  subCommands: [Mid1Command, Mid2Command],
})
export class TopCommand extends CommandRunner {
  constructor(private readonly log: LogService) { super(); }
  async run(inputs: string[]) { this.log.log('top command'); }
}

命令调用对照表:

命令 执行结果
top Mid1Command(默认)→ BottomCommand(默认)
top mid-1 Mid1Command
top mid-1 bottom BottomCommand
top mid-2 Mid2Command
top m Mid2Command(别名)
  1. 选项处理

  2. 必选选项 vs 可选选项

  3. 选项类型转换 (boolean, number, array)

5.3 高级阶段 (预计 4-5 小时)

  1. 交互式命令行

#### 7.1 基础用法 - InquirerService 注入

通过注入 InquirerService 实现交互式命令行:

import { Command, CommandRunner, InquirerService } from 'nest-commander';

@Command({ name: 'hello' })
export class HelloCommand extends CommandRunner {
  constructor(private readonly inquirer: InquirerService) {
    super();
  }

  async run(_inputs: string[], options?: HelloOptions): Promise<void> {
    // 询问问题集 'hello',传入已解析的选项
    options = await this.inquirer.ask('hello', options);
    console.log(`Hello ${options.name}`);
  }
}

#### 7.2 问题定义 - @QuestionSet 和 @Question 装饰器

使用装饰器定义问题集:

import { Question, QuestionSet } from 'nest-commander';

@QuestionSet({ name: 'hello' })
export class WhoQuestion {
  @Question({
    message: 'What is your name?',
    name: 'name',
  })
  parseName(val: string) {
    return val;
  }
}

常用问题类型:

类型 描述 适用场景
input 文本输入 姓名、地址等
confirm 是/否确认 布尔值选项
list 列表选择 单选
rawlist 数字索引列表 单选(带编号)
expand 展开列表 单选(带快捷键)
checkbox 复选框 多选
editor 编辑器 长文本输入

#### 7.3 问题验证 - @ValidateFor 装饰器

使用 @ValidateFor 添加字段验证:

@ValidateFor({ name: 'phone' })
validatePhone(value: string) {
  const pass = value.match(/^\d{10}$/);
  if (pass) {
    return true;
  }
  return 'Please enter a valid 10-digit phone number';
}

#### 7.4 条件问题 - @WhenFor 装饰器

根据前一个答案决定是否显示某个问题:

@WhenFor({ name: 'prize' })
whenPrize(answers: { comments: string }): boolean {
  // 只有当 comments 不是默认值时才显示 prize 问题
  return answers.comments !== 'Nope, all good!';
}

#### 7.5 完整示例:Pizza 订单

综合运用所有特性:

// pizza.interface.ts
export interface PizzaOptions {
  toppings?: string;
  toBeDelivered?: boolean;
  phone?: string;
  size?: string;
  quantity?: number;
  beverage?: string;
  comments?: string;
  prize?: string;
}

// pizza.question.ts
import { Question, QuestionSet, ValidateFor, WhenFor } from 'nest-commander';

@QuestionSet({ name: 'pizza' })
export class PizzaQuestion {
  // 展开列表选择
  @Question({
    type: 'expand',
    name: 'toppings',
    message: 'What about the toppings?',
    choices: [
      { key: 'p', name: 'Pepperoni and cheese', value: 'PepperoniCheese' },
      { key: 'a', name: 'All dressed', value: 'alldressed' },
      { key: 'w', name: 'Hawaiian', value: 'hawaiian' },
    ],
  })
  parseToppings(val: string) { return val; }

  // 确认问题
  @Question({
    type: 'confirm',
    name: 'toBeDelivered',
    message: 'Is this for delivery?',
    default: false,
  })
  parseToBeConfirmed(val: boolean) { return val; }

  // 带验证的输入
  @Question({
    type: 'input',
    name: 'phone',
    message: "What's your phone number?",
  })
  parsePhone(val: string) { return val; }

  @ValidateFor({ name: 'phone' })
  validatePhone(value: string) {
    const pass = value.match(/^\d{10}$/);
    return pass ? true : 'Please enter a valid phone number';
  }

  // 列表选择
  @Question({
    type: 'list',
    name: 'size',
    message: 'What size do you need?',
    choices: ['Large', 'Medium', 'Small'],
  })
  parseSize(val: string) { return val.toLowerCase(); }

  // 条件问题
  @WhenFor({ name: 'prize' })
  whenPrize(answers: { comments: string }): boolean {
    return answers.comments !== 'Nope, all good!';
  }
}

// pizza.command.ts
import { Command, CommandRunner, InquirerService } from 'nest-commander';
import { PizzaOptions } from './pizza.interface';

@Command({ name: 'pizza', options: { isDefault: true } })
export class PizzaCommand extends CommandRunner {
  constructor(private readonly inquirerService: InquirerService) {
    super();
  }

  async run(_inputs: string[], options?: PizzaOptions): Promise<void> {
    options = await this.inquirerService.ask('pizza', options);
    console.log(options);  // 输出完整订单
  }
}

执行效果:

$ my-cli pizza
? What about the toppings? (p) Pepperoni and cheese, (a) All dressed, (w) Hawaiian
? Is this for delivery? No
? What's your phone number? 1234567890
? What size do you need? Large
? How many do you need? 2
? You also get a free 2L beverage Pepsi
? Any comments on your purchase experience? Great service!
? For leaving a comment, you get a freebie cake
{ toppings: 'PepperoniCheese', toBeDelivered: false, phone: '1234567890', ... }

#### 7.6 在模块中注册问题

确保在 NestJS 模块中注册问题和命令:

import { Module } from '@nestjs/common';
import { PizzaCommand } from './pizza.command';
import { PizzaQuestion } from './pizza.question';

@Module({
  providers: [PizzaCommand, PizzaQuestion],
})
export class PizzaModule {}
  1. 配置管理

#### 8.1 cosmiconfig 集成

Nest Commander 内置支持 cosmiconfig,用于自动搜索和加载配置文件。CommandFactory.run() 会自动调用 cosmiconfig 搜索配置文件。

支持的配置文件格式:

文件名 格式
.myclirc JavaScript
.myclirc.json JSON
.myclirc.yaml YAML
.myclirc.yml YAML
myclirc.json JSON
myclirc.yaml YAML
myclirc.yml YAML
package.json 包含 mycli 字段

(假设 CLI 名称为 mycli)

#### 8.2 配置文件位置搜索顺序

cosmiconfig 从项目根目录开始搜索:

  1. 当前目录
  2. 逐级向上查找直到根目录

#### 8.3 插件配置

在配置文件中使用 plugins 字段加载自定义插件:

// .myclirc.json
{
  "plugins": ["./plugins/my-plugin"]
}
// my-plugin.js - 导出默认 NestJS 模块
import { Module } from '@nestjs/common';

@Module({
  providers: [MyCustomProvider],
})
export class MyPluginModule {}

工作原理:

  1. CommandFactory.run() 调用 cosmiconfig 搜索配置文件
  2. 读取配置中的 plugins 数组
  3. 使用 import() 动态加载插件模块
  4. 将插件模块添加到 NestJS 应用上下文中

#### 8.4 环境变量处理

在 @Option 装饰器中使用 env 属性绑定环境变量:

import { Command, CommandRunner, Option } from 'nest-commander';

@Command({ name: 'config' })
export class ConfigCommand extends CommandRunner {
  async run() {
    console.log('Config loaded from environment');
  }

  @Option({
    flags: '-u, --username <username>',
    description: 'Username',
    env: 'MYCLI_USERNAME',  // 绑定环境变量
  })
  parseUsername(val: string): string {
    return val;
  }

  @Option({
    flags: '-t, --token <token>',
    description: 'API Token',
    env: 'MYCLI_TOKEN',  // 绑定环境变量
  })
  parseToken(val: string): string {
    return val;
  }
}

优先级:命令行参数 > 环境变量 > 默认值

#### 8.5 完整配置示例

假设 CLI 名称为 myapp:

// .myapprc.json
{
  "plugins": [
    "./plugins/auth-plugin",
    "./plugins/logger-plugin"
  ],
  "verbose": true,
  "logLevel": "debug"
}
// bootstrap.ts
import { CommandFactory } from 'nest-commander';
import { AppModule } from './app.module';

async function bootstrap() {
  await CommandFactory.run(AppModule);
}

bootstrap();

执行时,Nest Commander 会自动:

  1. 搜索 .myapprc.json 配置文件
  2. 加载 auth-plugin 和 logger-plugin 模块
  3. 将它们注册到 NestJS 应用中

#### 8.6 自定义配置加载位置

如果需要自定义配置加载行为,可以扩展 CommandFactory:

import { CommandFactory } from 'nest-commander';

class CustomCommandFactory extends CommandFactory {
  // 自定义逻辑
}

但通常不需要这样做,默认的 cosmiconfig 集成已足够满足大多数场景。

  1. 测试

  2. 使用 nest-commander-testing 进行单元测试

  3. 集成测试编写

5.4 生产实践 (预计 2-3 小时)

  1. 插件系统

    • 理解 Commander 插件机制
    • 自定义插件开发
  2. Schematics

    • 使用 nest-commander-schematics 生成代码
    • 自定义 schematics 开发
  3. 最佳实践

    • 错误处理
    • 日志记录
    • 构建与发布

6. 关键文件索引

文件路径 描述
packages/nest-commander/src/command.decorators.ts @Command() 和 @Option() 装饰器实现
packages/nest-commander/src/command.factory.ts CommandFactory 核心类
packages/nest-commander/src/command-runner.service.ts 命令执行服务
packages/nest-commander/src/inquirer.service.ts Inquirer 集成服务
packages/nest-commander-testing/src/command-test.factory.ts 测试工具工厂
integration/basic/src/basic.command.ts 基础命令示例

7. 相关资源

  • 官方文档: https://nest-commander.jaymcdoniel.dev
  • GitHub Issues: https://github.com/jmcdo29/nest-commander/issues
  • Commander.js 文档: https://github.com/tj/commander.js
  • NestJS 文档: https://docs.nestjs.com
Backlinks (1)
flowchart LR
  n0["AI"]
  n1["Database"]
  n2["Dev Tools"]
  n3["Emoji"]
  n4["Frontend"]
  n5["Game Dev"]
  n6["Collection"]
  n7["Languages"]
  n8["Maps"]
  n9["Media"]
  n10["Monitor"]
  n11["Study Materials"]
  n12["Plans"]
  n13["Storage"]
  n14["Film & TV"]
  n15["Bitcask 存储模型(日志结构哈希表)"]
  n16["对象存储基本用法(Bucket / Object / 常用操作)"]
  n17["挂载 Bucket 为本地文件系统(FUSE Mount)"]
  n18["对象存储签名 URL(Signed URL)原理与实战"]
  n19["对象存储供应商对比(S3 / R2 / OSS / Supabase / MinIO)"]
  n20["Prototypes"]
  n21["Research"]
  n22["Better Auth 源码阅读指南"]
  n23["DuckDB 环境与基本使用"]
  n24["DuckDB 实战研究"]
  n25["DuckDB 模拟数据"]
  n26["PostgreSQL 数据用 DuckDB 加速查询"]
  n27["HK 角色阅读"]
  n28["HK 台词阅读"]
  n29["Hollow Knight 英语主题"]
  n30["HK 物品阅读"]
  n31["HK 地点阅读"]
  n32["英语学习 Dashboard"]
  n33["English Scraps Archive"]
  n34["English Scraps 使用指南"]
  n35["Jellyfin 源码阅读指南"]
  n36["Lux 资料整理"]
  n37["Nest Commander 学习资料"]
  n38["NestJS 源码阅读指南"]
  n39["Protomaps 自建底图研究"]
  n40["自制 PMTiles 地图(最简单例子)"]
  n41["MapLibre 集成 Protomaps"]
  n42["PMTiles 格式与工具链"]
  n43["上海地区底图项目"]
  n44["Redash 源码阅读指南"]
  n45["shadcn/ui 进阶使用"]
  n46["shadcn/ui 组件添加与基本使用"]
  n47["shadcn/ui 实用研究(从环境到基本使用)"]
  n48["shadcn/ui 环境与初始化"]
  n49["TRIP 项目核心原理与代码阅读指南"]
  n1 --> n24
  n1 --> n26
  n4 --> n47
  n6 --> n0
  n6 --> n1
  n6 --> n2
  n6 --> n3
  n6 --> n4
  n6 --> n5
  n6 --> n7
  n6 --> n8
  n6 --> n9
  n6 --> n10
  n6 --> n11
  n6 --> n12
  n6 --> n13
  n6 --> n21
  n9 --> n14
  n16 --> n18
  n16 --> n19
  n16 --> n20
  n17 --> n16
  n17 --> n18
  n17 --> n19
  n18 --> n20
  n19 --> n16
  n19 --> n18
  n19 --> n20
  n20 --> n15
  n20 --> n16
  n20 --> n17
  n20 --> n18
  n20 --> n19
  n20 --> n39
  n21 --> n22
  n21 --> n24
  n21 --> n32
  n21 --> n35
  n21 --> n36
  n21 --> n37
  n21 --> n38
  n21 --> n39
  n21 --> n44
  n21 --> n47
  n21 --> n49
  n23 --> n25
  n24 --> n1
  n24 --> n21
  n24 --> n23
  n24 --> n25
  n24 --> n26
  n25 --> n23
  n25 --> n26
  n26 --> n24
  n26 --> n25
  n29 --> n27
  n29 --> n28
  n29 --> n30
  n29 --> n31
  n29 --> n34
  n32 --> n29
  n32 --> n33
  n32 --> n34
  n34 --> n32
  n34 --> n33
  n39 --> n8
  n39 --> n40
  n39 --> n41
  n39 --> n42
  n39 --> n43
  n40 --> n42
  n41 --> n20
  n41 --> n43
  n42 --> n40
  n42 --> n43
  n43 --> n41
  n43 --> n42
  n45 --> n46
  n46 --> n45
  n46 --> n48
  n47 --> n4
  n47 --> n21
  n47 --> n45
  n47 --> n46
  n47 --> n48
  n48 --> n46
  click n0 "../../../collection/ai/" "AI"
  click n1 "../../../collection/database/" "Database"
  click n2 "../../../collection/dev-tools/" "Dev Tools"
  click n3 "../../../collection/emoji/" "Emoji"
  click n4 "../../../collection/frontend/" "Frontend"
  click n5 "../../../collection/game-dev/" "Game Dev"
  click n6 "../../../collection/" "Collection"
  click n7 "../../../collection/languages/" "Languages"
  click n8 "../../../collection/maps/" "Maps"
  click n9 "../../../collection/media/" "Media"
  click n10 "../../../collection/monitor/" "Monitor"
  click n11 "../../../collection/reading/" "Study Materials"
  click n12 "../../../collection/scraps/plans/" "Plans"
  click n13 "../../../collection/storage/" "Storage"
  click n14 "../../../film-tv/" "Film & TV"
  click n15 "../../../knowledge/database/kv/bitcask/" "Bitcask 存储模型(日志结构哈希表)"
  click n16 "../../../knowledge/infrastructure/cloud/object-storage/basic-usage/" "对象存储基本用法(Bucket / Object / 常用操作)"
  click n17 "../../../knowledge/infrastructure/cloud/object-storage/mount-bucket/" "挂载 Bucket 为本地文件系统(FUSE Mount)"
  click n18 "../../../knowledge/infrastructure/cloud/object-storage/signed-url/" "对象存储签名 URL(Signed URL)原理与实战"
  click n19 "../../../knowledge/infrastructure/cloud/object-storage/vendors-comparison/" "对象存储供应商对比(S3 / R2 / OSS / Supabase / MinIO)"
  click n20 "../../../prototypes/" "Prototypes"
  click n21 "../../" "Research"
  click n22 "../better-auth/" "Better Auth 源码阅读指南"
  click n23 "../duckdb/basic-usage/" "DuckDB 环境与基本使用"
  click n24 "../duckdb/" "DuckDB 实战研究"
  click n25 "../duckdb/mock-data/" "DuckDB 模拟数据"
  click n26 "../duckdb/postgresql-acceleration/" "PostgreSQL 数据用 DuckDB 加速查询"
  click n27 "../english/hollow-knight/characters/" "HK 角色阅读"
  click n28 "../english/hollow-knight/dialogues/" "HK 台词阅读"
  click n29 "../english/hollow-knight/" "Hollow Knight 英语主题"
  click n30 "../english/hollow-knight/items/" "HK 物品阅读"
  click n31 "../english/hollow-knight/locations/" "HK 地点阅读"
  click n32 "../english/" "英语学习 Dashboard"
  click n33 "../english/scraps/archive/" "English Scraps Archive"
  click n34 "../english/scraps/" "English Scraps 使用指南"
  click n35 "../jellyfin/" "Jellyfin 源码阅读指南"
  click n36 "../lux/" "Lux 资料整理"
  click n37 "./" "Nest Commander 学习资料"
  click n38 "../nestjs/" "NestJS 源码阅读指南"
  click n39 "../protomaps/" "Protomaps 自建底图研究"
  click n40 "../protomaps/make-own-map/" "自制 PMTiles 地图(最简单例子)"
  click n41 "../protomaps/maplibre/" "MapLibre 集成 Protomaps"
  click n42 "../protomaps/pmtiles/" "PMTiles 格式与工具链"
  click n43 "../protomaps/shanghai-map/" "上海地区底图项目"
  click n44 "../redash/" "Redash 源码阅读指南"
  click n45 "../shadcn-ui/advanced/" "shadcn/ui 进阶使用"
  click n46 "../shadcn-ui/components/" "shadcn/ui 组件添加与基本使用"
  click n47 "../shadcn-ui/" "shadcn/ui 实用研究(从环境到基本使用)"
  click n48 "../shadcn-ui/setup/" "shadcn/ui 环境与初始化"
  click n49 "../trip/" "TRIP 项目核心原理与代码阅读指南"