aboutsummaryrefslogtreecommitdiff
path: root/docs/_zh_CN/pages
diff options
context:
space:
mode:
Diffstat (limited to 'docs/_zh_CN/pages')
-rw-r--r--docs/_zh_CN/pages/10-help.md69
-rw-r--r--docs/_zh_CN/pages/11-resource-system.md89
-rw-r--r--docs/_zh_CN/pages/12-exit-code.md70
-rw-r--r--docs/_zh_CN/pages/13-hook.md107
-rw-r--r--docs/_zh_CN/pages/14-testing.md129
-rw-r--r--docs/_zh_CN/pages/2-define-a-dispatcher.md103
-rw-r--r--docs/_zh_CN/pages/3-define-a-chain.md131
-rw-r--r--docs/_zh_CN/pages/4-render-result.md142
-rw-r--r--docs/_zh_CN/pages/5-multiple-commands.md109
-rw-r--r--docs/_zh_CN/pages/6-argument-parse-picker.md393
-rw-r--r--docs/_zh_CN/pages/7-argument-parse-clap.md87
-rw-r--r--docs/_zh_CN/pages/8-setup-and-resources.md89
-rw-r--r--docs/_zh_CN/pages/9-error-handling.md120
-rw-r--r--docs/_zh_CN/pages/advanced/.name1
-rw-r--r--docs/_zh_CN/pages/advanced/1-completion.md83
-rw-r--r--docs/_zh_CN/pages/advanced/2-structural-renderer.md120
-rw-r--r--docs/_zh_CN/pages/concepts/.name1
-rw-r--r--docs/_zh_CN/pages/concepts/1-the-pipeline.md129
-rw-r--r--docs/_zh_CN/pages/concepts/2-resource.md60
-rw-r--r--docs/_zh_CN/pages/concepts/3-any-output.md73
-rw-r--r--docs/_zh_CN/pages/concepts/4-program-collect.md52
21 files changed, 2157 insertions, 0 deletions
diff --git a/docs/_zh_CN/pages/10-help.md b/docs/_zh_CN/pages/10-help.md
new file mode 100644
index 0000000..7b56557
--- /dev/null
+++ b/docs/_zh_CN/pages/10-help.md
@@ -0,0 +1,69 @@
+<h1 align="center">帮助信息</h1>
+<p align="center">
+ 为命令添加 --help 支持
+</p>
+
+没有帮助信息的 CLI 不是好 CLI。
+
+Mingling 里用 `#[help]` 宏给命令添加帮助文本。
+
+## 最简单的帮助
+
+直接给 Entry 写一个帮助函数:
+
+```rust
+@@@use mingling::macros::help;
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+#[help]
+fn help_greet(_entry: EntryGreet) {
+ r_println!("Usage: greet [name]");
+ r_println!("Say hello to someone.");
+}
+```
+
+> [!NOTE]
+> 帮助函数里也用 `r_println!`,因为 `#[help]` 走的也是渲染流程 —— 它是被 `--help` 参数提前触发的短路渲染,不是独立于管线之外的逻辑。
+
+## 全局帮助
+
+你也可以为 `ErrorDispatcherNotFound` 写帮助,作为"根帮助":
+
+```rust
+@@@use mingling::macros::help;
+// 用户直接输入 --help 时触发
+#[help]
+fn help_root(entry: ErrorDispatcherNotFound) {
+ r_println!("Usage: my-cli <command>");
+ r_println!("Commands:");
+ r_println!(" greet Say hello");
+}
+```
+
+> [!TIP]
+> `ErrorDispatcherNotFound` 是 `gen_program!()` 自动生成的类型,代表"没有匹配到任何命令"的情况。为它写 `#[help]` 就是给程序的根命令加帮助。
+
+## 需要 Setup 配合
+
+要让 `--help` 正常工作,需要在 `main` 里加上 `BasicProgramSetup`:
+
+```rust
+@@@use mingling::macros::help;
+@@@use mingling::setup::BasicProgramSetup;
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(BasicProgramSetup);
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+```
+
+`BasicProgramSetup` 内置了 `HelpFlagSetup`,它的作用仅仅是把 `program.user_context.help` 设为 `true`。
+
+真正把请求路由到 `#[help]` 函数的是 `gen_program!()` 生成的代码 —— 它在调度时检查这个标记,如果为 `true` 就走帮助渲染路径,不经过 Chain。
+
+不加 `BasicProgramSetup` 的话,`--help` 只是一个普通参数,会被当成 Entry 的输入传给 Chain。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/11-resource-system.md b/docs/_zh_CN/pages/11-resource-system.md
new file mode 100644
index 0000000..5c3cf90
--- /dev/null
+++ b/docs/_zh_CN/pages/11-resource-system.md
@@ -0,0 +1,89 @@
+<h1 align="center">使用资源系统</h1>
+<p align="center">
+ 手把手带你使用资源
+</p>
+
+资源是 Mingling 中管理全局状态的机制。任何实现了 `Default + Clone` 的类型都可以成为资源。
+
+## 定义一个资源
+
+```rust
+// 只要实现 Default + Clone,就可以作为资源使用
+#[derive(Default, Clone)]
+struct ResCurrentDir(String);
+
+// 注册到 Program
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_resource(ResCurrentDir(".".into()));
+ program.exec_and_exit();
+}
+```
+
+因为 `ResCurrentDir` 同时实现了 `Default` 和 `Clone`,框架会自动为它实现 `ResourceMarker` trait,无需手动 impl。
+
+## 注入并使用
+
+在 Chain 或 Renderer 中,只需在参数列表里声明你要的资源:
+
+```rust
+@@@#[derive(Default, Clone)]
+@@@struct ResCurrentDir(String);
+@@@dispatcher!("pwd", CMDPrintWorkingDir => EntryPrintWorkingDir);
+@@@pack!(ResultPath = String);
+// 通过 &T 注入只读资源
+#[chain]
+fn handle_pwd(_args: EntryPrintWorkingDir, cwd: &ResCurrentDir) -> Next {
+ ResultPath::new(cwd.0.clone()).to_render()
+}
+
+#[renderer]
+fn render_path(result: ResultPath) {
+ r_println!("{}", *result);
+}
+```
+
+## 修改资源
+
+用 `&mut T` 注入可修改资源:
+
+```rust
+@@@#[derive(Default, Clone)]
+@@@struct ResVisitCount(u32);
+@@@dispatcher!("visit", CMDVisit => EntryVisit);
+@@@pack!(ResultDone = ());
+#[chain]
+fn handle_visit(_args: EntryVisit, counter: &mut ResVisitCount) -> Next {
+ counter.0 += 1;
+ ResultDone::default()
+}
+
+#[renderer]
+fn render_done(_done: ResultDone, counter: &ResVisitCount) {
+ r_println!("visit count is : {}", counter.0);
+}
+```
+
+## 多个资源同用
+
+Chain 可以同时注入任意多个资源,框架按类型自动匹配:
+
+```rust
+@@@#[derive(Default, Clone)] struct ResConfig(String);
+@@@#[derive(Default, Clone)] struct ResCounter(u32);
+@@@dispatcher!("test", CMDTest => EntryTest);
+@@@pack!(ResultDone = ());
+// 同时注入只读 + 可修改
+#[chain]
+fn handle_test(_args: EntryTest, config: &ResConfig, counter: &mut ResCounter) -> Next {
+ println!("config: {}", config.0);
+ counter.0 += 1;
+ ResultDone::default().to_render()
+}
+```
+
+如果要深入了解 `ResourceMarker`、`LazyRes` 惰性加载等进阶内容,可以查看 [核心概念:资源系统](pages/concepts/2-resource)。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/12-exit-code.md b/docs/_zh_CN/pages/12-exit-code.md
new file mode 100644
index 0000000..7c55b60
--- /dev/null
+++ b/docs/_zh_CN/pages/12-exit-code.md
@@ -0,0 +1,70 @@
+<h1 align="center">退出码控制</h1>
+<p align="center">
+ 如何使用资源系统管理程序退出码
+</p>
+
+程序退出时给 shell 一个正确的退出码是 CLI 的基本素养
+
+。Mingling 提供了开箱即用的 `ExitCodeSetup`,配合 `ResExitCode` 资源,让退出码控制变得极其简单。
+
+## 启用 ExitCodeSetup
+
+```rust
+@@@use mingling::prelude::*;
+@@@use mingling::setup::ExitCodeSetup;
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(ExitCodeSetup::default());
+@@@ program.exec_and_exit();
+}
+```
+
+`ExitCodeSetup` 做了两件事:
+
+1. 注册 `ResExitCode` 资源(默认值为 `0`)
+2. 注册一个 `finish` hook,在程序退出前读取 `ResExitCode` 的值作为最终退出码
+
+## 修改退出码
+
+在 Chain 或 Renderer 中通过 `ResExitCode` 注入来修改退出码:
+
+```rust
+@@@use mingling::res::ResExitCode;
+@@@use mingling::setup::ExitCodeSetup;
+@@@pack!(EntryCheck = Vec<String>);
+#[chain]
+fn handle_check(_args: EntryCheck, ec: &mut ResExitCode) {
+ // 检查失败的时候修改退出码资源
+ ec.exit_code = 1;
+}
+```
+
+> [!TIP]
+> `ResExitCode` 就是一个 `struct ResExitCode { pub exit_code: i32 }`。`&mut ResExitCode` 注入后直接改字段即可。
+
+## `Program` 的三种执行方式
+
+`Program` 提供了三种执行方式(不包括 `repl` 特性下的 `exec_repl`):
+
+| 方式 | 行为 |
+| ------------------------------- | -------------------------------------------------------------------------- |
+| `program.exec_and_exit()` | 执行并直接以退出码终止进程 |
+| `program.exec()` | 执行后返回 `i32` 退出码,由调用方决定怎么处理 |
+| `program.exec_without_render()` | 返回 `Result<RenderResult, ProgramExecuteError>`,可读取内部的 `exit_code` |
+
+```rust
+@@@use mingling::setup::ExitCodeSetup;
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(ExitCodeSetup::default());
+
+ // 获取退出码自行处理
+ let exit_code = program.exec();
+ std::process::exit(exit_code);
+}
+@@@gen_program!();
+```
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/13-hook.md b/docs/_zh_CN/pages/13-hook.md
new file mode 100644
index 0000000..543ee55
--- /dev/null
+++ b/docs/_zh_CN/pages/13-hook.md
@@ -0,0 +1,107 @@
+<h1 align="center">钩子系统</h1>
+<p align="center">
+ 如何使用 ProgramHook 向程序内部插入行为
+</p>
+
+Hook 让你在管线的各个生命周期节点插入自定义逻辑 —— 在 dispatch 之前、chain 之后、render 前后、程序退出时 ……
+
+你可以把横切关注点(日志、鉴权、指标收集)写在 hook 里,而不是散落在各处的业务代码中。
+
+## 基本用法
+
+`ProgramHook` 采用 builder 模式构造:
+
+```rust
+@@@use mingling::hook::ProgramHook;
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_hook(
+ ProgramHook::empty()
+ .on_pre_chain(|info| {
+ println!("before chain: {}", info.input);
+ })
+ .on_post_render(|info| {
+ println!("after render: {}", info.result);
+ }),
+ );
+ program.exec_and_exit();
+}
+```
+
+> [!TIP]
+> `ProgramHook::empty()` 创建一个空 hook,然后链式调用 `.on_*()` 方法注册你关心的生命周期节点。没有注册的节点不会执行。
+
+## 生命周期节点
+
+Hook 覆盖了管线的完整生命周期:
+
+| 阶段 | Hook | 触发时机 |
+| ------------ | ------------------ | ------------- |
+| **Dispatch** | `on_begin` | 执行开始 |
+| | `on_pre_dispatch` | Dispatch 之前 |
+| | `on_post_dispatch` | Dispatch 之后 |
+| **Chain** | `on_pre_chain` | Chain 执行前 |
+| | `on_post_chain` | Chain 执行后 |
+| **Render** | `on_pre_render` | Render 执行前 |
+| | `on_post_render` | Render 执行后 |
+| **Finish** | `on_finish` | 程序退出前 |
+
+每个 hook 回调接收对应的 `Hook*Info` 结构体,里面包含当前上下文的信息(输入类型、参数、渲染结果等)。
+
+## 实际例子:记录操作日志
+
+```rust
+@@@use mingling::prelude::*;
+@@@use mingling::hook::ProgramHook;
+@@@
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@
+@@@#[chain] fn handle_greet(args: EntryGreet) -> Next {
+@@@ ResultName::new(args.inner.first().cloned().unwrap_or_default()).to_render()
+@@@}
+@@@#[renderer] fn render_name(r: ResultName) { r_println!("Hello, {}!", *r); }
+fn main() {
+ let mut program = ThisProgram::new();
+
+ // 记录每次 chain 执行前后的信息
+ program.with_hook(
+ ProgramHook::empty()
+ .on_pre_chain(|info| {
+ eprintln!("[hook] executing chain for: {}", info.input);
+ })
+ .on_post_chain(|info| {
+ eprintln!("[hook] chain output: {}", info.output.member_id);
+ }),
+ );
+
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+```
+
+运行效果:
+
+```text
+[hook] executing chain for: EntryGreet
+[hook] chain output: ResultName
+Hello, World!
+```
+
+## 通过 Hook 控制行为
+
+Hook 不仅用来 "看",还可以用 `ProgramControlUnit` 改变程序行为:
+
+| 变体 | 效果 |
+| -------------------------- | ----------------------------------------- |
+| `Continue` | 什么都不做,继续执行 |
+| `OverrideExitCode(i32)` | 覆盖退出码 |
+| `RouteToChain(AnyOutput)` | 将当前数据替换为新的,重新进入 Chain 循环 |
+| `RouteToRender(AnyOutput)` | 跳过后续 Chain,直接渲染 |
+
+> [!NOTE]
+> Hook 可以注册多个,按注册顺序执行。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/14-testing.md b/docs/_zh_CN/pages/14-testing.md
new file mode 100644
index 0000000..af9d077
--- /dev/null
+++ b/docs/_zh_CN/pages/14-testing.md
@@ -0,0 +1,129 @@
+<h1 align="center">测试你的程序</h1>
+<p align="center">
+ 为 Chain 和 Renderer 编写单元测试
+</p>
+
+管线模型附带的一个好处就是 **可测试性**。
+
+Chain 只是一个接收输入、返回输出的函数,Renderer 也只是接收输入、写入内容的函数 —— 没有全局状态的黑魔法,测试起来很直接。
+
+## 测试 Renderer
+
+Renderer 是最容易测试的——调用函数,断言返回结果:
+
+```rust
+@@@pack!(ResultName = String);
+// 返回 String 而不是 ()
+#[renderer]
+fn render_name(name: ResultName) -> String {
+ r_println!("Hello, {}!", *name);
+}
+
+#[test]
+fn test_render_name() {
+ let result = render_name(ResultName::new("Alice".to_string()));
+ assert_eq!(result, "Hello, Alice!\n");
+}
+```
+
+注意到 Renderer 的返回值改成了 `-> String`——`#[renderer]` 会把 `RenderResult` 自动转换成你指定的返回类型(默认是 `()`)。返回 `String` 后你就可以直接断言输出内容了。
+
+## 测试 Chain
+
+测试 Chain 稍微复杂一点,因为它的返回值是 `Next`(实际是 `impl Into<ChainProcess<ThisProgram>>`)。需要用框架提供的断言宏:
+
+```rust
+@@@use mingling::{assert_member_id, assert_render_result, unpack_chain_process};
+@@@dispatcher!("hello", CMDHello => EntryHello);
+@@@pack!(ResultName = String);
+@@@pack!(ErrorNoName = ());
+@@@#[chain]
+@@@fn handle_hello(args: EntryHello) -> Next {
+@@@ let name = args.inner.first().cloned().unwrap_or_default();
+@@@ if name.is_empty() {
+@@@ ErrorNoName::default().to_render()
+@@@ } else {
+@@@ ResultName::new(name).to_render()
+@@@ }
+@@@}
+#[test]
+fn test_handle_hello_with_name() {
+ let chain_process = handle_hello(EntryGreet::new(vec!["Alice".to_string()])).into();
+ // 断言这是一个渲染结果(不是继续 chain)
+ assert_render_result!(chain_process);
+ // 断言 member_id 是 ResultName
+ assert_member_id!(chain_process, ResultName);
+ // 解包出内部值
+ let result_name = unpack_chain_process!(chain_process, ResultName);
+ assert_eq!(result_name.inner, "Alice");
+}
+```
+
+三个测试宏的作用:
+
+| 宏 | 功能 |
+| ----------------------- | --------------------------------------------- |
+| `assert_render_result!` | 断言 Chain 返回的是渲染路径(而非继续 chain) |
+| `assert_member_id!` | 断言返回值的成员 ID 是某个类型 |
+| `unpack_chain_process!` | 从 ChainProcess 中解包出原始类型 |
+
+## 用 entry! 宏构造数据
+
+如果启用了 `extra_macros`,可以用 `entry!` 快速构造 Entry:
+
+```rust
+// Features: ["extra_macros"]
+
+@@@use mingling::{assert_member_id, unpack_chain_process};
+@@@use mingling::macros::entry;
+@@@dispatcher!("hello", CMDHello => EntryHello);
+@@@pack!(ResultName = String);
+@@@#[chain]
+@@@fn handle_hello(args: EntryHello) -> Next {
+@@@ let name = args.inner.first().cloned().unwrap_or_default();
+@@@ ResultName::new(name).to_render()
+@@@}
+#[test]
+fn test_with_entry_macro() {
+ // entry! 从字符串字面量构造 Entry
+ let entry = entry!("--name", "Alice");
+ let chain_process = handle_hello(entry).into();
+ let result_name = unpack_chain_process!(chain_process, ResultName);
+ assert_eq!(result_name.inner, "Alice");
+}
+```
+
+## 测试资源注入
+
+如果 Chain 使用了资源,测试时需要提供资源实例:
+
+```rust
+@@@use mingling::{assert_render_result, unpack_chain_process};
+@@@#[derive(Default, Clone)]
+@@@struct ResPrefix(String);
+@@@dispatcher!("hello", CMDHello => EntryHello);
+@@@pack!(ResultGreeting = String);
+@@@
+#[chain]
+fn handle_hello(args: EntryHello, prefix: &ResPrefix) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_default();
+ ResultGreeting::new(format!("{}, {}", prefix.0, name)).to_render()
+}
+
+#[test]
+fn test_handle_with_resource() {
+ // 资源需要在测试中手动传入
+ let result = handle_hello(
+ EntryHello::new(vec!["World".to_string()]),
+ &ResPrefix("Hello".to_string()),
+ );
+ let greeting = unpack_chain_process!(result, ResultGreeting, ThisProgram);
+ assert_eq!(greeting.inner, "Hello, World");
+}
+```
+
+管线模型让测试变得简单:每个 Chain 和 Renderer 都是相对独立的函数,构造输入、断言输出即可。
+
+<p align="center" style="font-size: 0.85em; color: clear;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/2-define-a-dispatcher.md b/docs/_zh_CN/pages/2-define-a-dispatcher.md
new file mode 100644
index 0000000..0afd911
--- /dev/null
+++ b/docs/_zh_CN/pages/2-define-a-dispatcher.md
@@ -0,0 +1,103 @@
+<h1 align="center">声明一个分发器</h1>
+<p align="center">
+ 使用 dispatcher! 宏声明命令,并注册
+</p>
+
+Mingling 的管线从 Dispatcher 开始。
+
+它的工作很简单:**匹配用户输入的命令,把参数包装成一个 Entry 类型**。
+
+## `dispatcher!` 宏
+
+`dispatcher!` 宏会同时生成两个类型:
+
+| 生成物 | 用途 |
+| ----------- | ----------------------------------------------- |
+| `CMDType` | 分发器本身,需要注册到 Program |
+| `EntryType` | 入口类型,包裹 `Vec<String>`,作为 Chain 的输入 |
+
+写法是固定的三个部分:
+
+```rust
+dispatcher!("命令路径", 分发器类型 => 入口类型);
+```
+
+看一个具体的例子:
+
+```rust
+dispatcher!("greet", CMDGreet => EntryGreet);
+```
+
+> [!NOTE]
+> 命令名(`"greet"`)会自动转换为 kebab-case。即使你写 `"GreetUser"`,匹配时也会变成 `greet-user`。
+
+## 注册到 Program
+
+有了分发器之后,需要告诉 Program 它的存在:
+
+```rust
+@@@ dispatcher!("greet", CMDGreet => EntryGreet);
+@@@ fn main() {
+@@@ let mut program = ThisProgram::new();
+// 注册分发器
+program.with_dispatcher(CMDGreet);
+@@@ }
+@@@ gen_program!();
+```
+
+> [!TIP]
+> 如果命令多了,可以用 `with_dispatchers` 一次注册多个:`program.with_dispatchers((CMDGreet, CMDAdd, CMDRemoteRm))`。
+
+## 多级命令
+
+如果你的程序有层级结构——比如 `remote add`、`remote rm`——只需要在命令名里加点号分隔:
+
+```rust
+dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd);
+dispatcher!("remote.rm", CMDRemoteRm => EntryRemoteRm);
+```
+
+用户在终端输入 `remote add` 时,Mingling 会依次匹配 `remote` 和 `add` 两个层级。
+
+## 入口类型 `EntryGreet`
+
+你可能会好奇 `EntryGreet` 里面到底有什么。它本质上就是一个包装了 `Vec<String>` 的结构体:
+
+```rust
+// 示意,dispatcher! 宏实际生成的代码
+pub struct EntryGreet {
+ pub inner: Vec<String>,
+}
+```
+
+用户在命令行输入 `greet Alice Bob`,`EntryGreet.inner` 就是 `vec!["Alice", "Bob"]`。
+
+> [!IMPORTANT]
+> Entry 的 `inner` 只包含 **匹配后剩余的参数**。
+>
+> 以 `remote add origin` 为例,`remote` 和 `add` 用于匹配命令路径,只有 `origin` 会进入 `EntryRemoteAdd.inner`。
+
+## 进阶:隐式声明
+
+以上是标准写法。如果你启用了 `extra_macros` 特性,还可以更简洁:
+
+```rust
+// Features: ["extra_macros"]
+// 省略 CMDType 和 EntryType,名字自动推导
+ dispatcher!("greet");
+// dispatcher!("greet", CMDGreet => EntryGreet);
+```
+
+这种写法会自动生成 `CMDGreet` 和 `EntryGreet`,效果跟显式声明完全一样。
+
+不过在教程阶段,我们继续用显式写法——更清晰,也不依赖额外特性。
+
+详见[特性列表](pages/other/features)。
+
+## 下一步
+
+接下来我们写一个 Chain 来接收 Entry,处理真正的业务逻辑。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/3-define-a-chain.md b/docs/_zh_CN/pages/3-define-a-chain.md
new file mode 100644
index 0000000..7ac5c60
--- /dev/null
+++ b/docs/_zh_CN/pages/3-define-a-chain.md
@@ -0,0 +1,131 @@
+<h1 align="center">声明一个链</h1>
+<p align="center">
+ 使用 chain 宏声明链,并承接 Entry 输入
+</p>
+
+上一节我们声明了 `dispatcher!("greet", CMDGreet => EntryGreet)`
+
+现在用户输入 `greet` 时会被匹配并包装成 `EntryGreet`。
+
+但拿到 Entry 之后呢?
+
+我们需要一个 Chain 来处理它。
+
+## `#[chain]` 宏
+
+`#[chain]` 用来标记一个处理函数,格式非常直接:
+
+```rust
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+pack!(ResultName = String);
+
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ // args 就是用户输入经过匹配后剩下的参数
+ let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string());
+ // 把结果包装成 Next,告诉调度器下一步去哪
+ ResultName::new(name)
+}
+```
+
+注意到了吗?
+
+Chain 函数签名里写着它需要什么——`args: EntryGreet`
+
+然后用 `ResultName::new(name)` 返回一个新类型。
+
+这个返回的 `Next` 会展开成 `impl Into<ChainProcess<ThisProgram>>`。
+
+> [!TIP]
+> 想知道 `Into<ChainProcess<G>>` 是怎么工作的?
+>
+> 可以去 [任意输出机制](pages/concepts/3-any-output) 章节了解 `ChainProcess`。
+
+## `pack!` 宏
+
+你大概猜到了,`pack!(ResultName = String)` 定义了一个管线中传递的类型:
+
+```rust
+// pack!(ResultName = String) 大概生成了这样的代码
+
+#[derive(Groupped)]
+pub struct ResultName {
+ pub inner: String,
+}
+```
+
+你可以把它理解为一个 打了标签的 `String`。
+
+调度器通过这个标签来精确路由,确保数据不会混淆 —— 比如发给 `RenderGreet` 的数据不会被误传给 `RenderError`。
+
+> [!NOTE]
+> 与简单的类型别名 (`type`) 不同,`pack!` 会生成一个全新的类型,拥有独立的 `TypeId`。
+
+命名上推荐这样的习惯:
+
+| 角色 | 命名模式 | 示例 |
+| -------- | ---------------- | -------------------- |
+| 入口 | `Entry` + 命令名 | `EntryGreet` |
+| 中间状态 | `State` + 描述 | `StateParsedArgs` |
+| 最终结果 | `Result` + 描述 | `ResultGreetSomeone` |
+| 错误 | `Error` + 描述 | `ErrorUserNotFound` |
+
+详见 [命名规范](pages/other/naming_rule),不过现在你只需要记住:**用 `pack!` 给你的数据取一个有意义的名字**。
+
+## 从 Entry 中提取参数
+
+`EntryGreet` 的 `inner` 是一个 `Vec<String>`,你可以在 Chain 里自由地处理它:
+
+```rust
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ // 取第一个参数,没有就用默认值
+ let name = args
+ .inner
+ .first()
+ .cloned()
+ .unwrap_or_else(|| "World".to_string());
+
+ ResultName::new(name)
+}
+```
+
+如果你启用了 `parser` 特性,还可以用 `Picker` 做更灵活的参数提取,不过那是后话了。
+
+## 组合起来
+
+现在把 Dispatcher 和 Chain 连在一起:
+
+```rust
+// 1. 声明命令
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+// 2. 声明管线中的数据类型
+pack!(ResultName = String);
+
+// 3. 处理逻辑
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let name = args.inner
+ .first()
+ .cloned()
+ .unwrap_or_else(|| "World".to_string());
+ ResultName::new(name)
+}
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+
+gen_program!();
+```
+
+不过这段代码还没写完 —— 我们只有 Dispatcher 和 Chain,还差最后一步:**把结果渲染出来**。这就是下一篇要讲的 Renderer。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/4-render-result.md b/docs/_zh_CN/pages/4-render-result.md
new file mode 100644
index 0000000..9fac8a4
--- /dev/null
+++ b/docs/_zh_CN/pages/4-render-result.md
@@ -0,0 +1,142 @@
+<h1 align="center">将结果渲染</h1>
+<p align="center">
+ 使用 renderer 宏声明渲染器,将结果输出
+</p>
+
+现在,我们创建了 Dispatcher 和 Chain,也通过 `pack!` 产出了一个 Result 类型。最后一步:**把结果展示给用户**。
+
+## `#[renderer]` 宏
+
+跟 `#[chain]` 类似,`#[renderer]` 用于标记一个输出函数:
+
+```rust
+@@@pack!(ResultName = String);
+#[renderer]
+fn render_name(name: ResultName) {
+ r_println!("Hello, {}!", *name);
+}
+```
+
+Renderer 接收 Chain 产出的结果,然后用 `r_println!` 输出。这个 `r_println!` 跟我们平常用的 `println!` 有什么区别?
+
+## `r_println!` 和 `r_print!` 宏
+
+`r_println!` 和 `r_print!` 是 Mingling 提供的打印宏,它们把内容写入 `RenderResult` 而不是直接输出到终端。这样做的好处是:
+
+1. **RenderResult 持有退出码**——你可以设置程序以特定退出码结束
+2. **方便测试**——可以捕获渲染结果做断言
+3. **便于后处理**——你可以将结果捕获,统一进行文本后处理
+
+> [!TIP]
+> 如果只是简单打印,你可以先把它理解为 `println!` 的平替。用 `r_println!` 替换 `println!` 不会错。
+
+## 完整的可运行程序
+
+把三篇教程的内容合在一起,你的第一个 Mingling 程序就完整了:
+
+```rust
+// 1. 用 Dispatcher 声明命令
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+// 2. 用 pack! 声明结果数据
+pack!(ResultName = String);
+
+// 3. 用 Chain 处理逻辑
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let name = args.inner
+ .first()
+ .cloned()
+ .unwrap_or_else(|| "World".to_string());
+ ResultName::new(name)
+}
+
+// 4. 用 Renderer 输出结果
+#[renderer]
+fn render_name(name: ResultName) {
+ r_println!("Hello, {}!", *name);
+}
+
+// 5. 在 main 函数内装配程序并运行
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+
+// 6. 使用 gen_program! 生成完整程序
+gen_program!();
+```
+
+## 跑起来试试
+
+```bash
+~# cargo run -- greet Alice
+```
+
+```text
+Hello, Alice!
+```
+
+试试不给参数:
+
+```bash
+~# cargo run -- greet
+```
+
+```text
+Hello, World!
+```
+
+试试不存在的命令:
+
+```bash
+cargo run -- great
+```
+
+```text
+# 什么也没输出!
+```
+
+## 补上 Fallback
+
+`gen_program!()` 自动生成了一个 `ErrorDispatcherNotFound` 类型,包裹 `Vec<String>`——它存的是用户输入的那些没匹配到的命令。你只需要给它写一个 Renderer:
+
+```rust
+#[renderer]
+fn render_dispatcher_not_found(err: ErrorDispatcherNotFound) {
+ if err.inner.is_empty() {
+ r_println!("Unknown command");
+ } else {
+ r_println!("Command not found: \"{}\"", err.inner.join(" "));
+ }
+}
+```
+
+加上之后,再试试不存在的命令:
+
+```bash
+cargo run -- great
+```
+
+```text
+Command not found: "great"
+```
+
+## 恭喜
+
+你完成了第一个完整的 Mingling 程序!来回顾一下学到的东西:
+
+| 概念 | 对应宏/函数 | 一句话 |
+| -------- | ---------------- | -------------------------- |
+| 声明命令 | `dispatcher!` | 告诉程序用户能输入什么 |
+| 处理逻辑 | `#[chain]` | 收到参数后做什么 |
+| 输出结果 | `#[renderer]` | 怎么把结果告诉用户 |
+| 类型包装 | `pack!` | 给你的数据取个有意义的名字 |
+| 程序入口 | `gen_program!()` | 自动生成管线的接线图 |
+
+在真实项目中你还会用到资源注入、hook、补全、REPL 等高级功能,不过核心骨架永远不变:**Dispatcher → Chain → Renderer**。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/5-multiple-commands.md b/docs/_zh_CN/pages/5-multiple-commands.md
new file mode 100644
index 0000000..4a9c72d
--- /dev/null
+++ b/docs/_zh_CN/pages/5-multiple-commands.md
@@ -0,0 +1,109 @@
+<h1 align="center">多命令程序</h1>
+<p align="center">
+ 在一个程序里添加多个命令
+</p>
+
+真实世界的 CLI 很少只有一个命令。这篇我们来扩展之前的 greet 程序,加上第二个命令,看看多命令的程序长什么样。
+
+## 添加第二个命令
+
+继续在同一个项目里操作:
+
+```rust
+// 声明两个命令
+dispatcher!("greet", CMDGreet => EntryGreet);
+dispatcher!("add", CMDAdd => EntryAdd);
+
+pack!(ResultGreeting = String);
+pack!(ResultSum = i32);
+
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_else(|| "World".to_string());
+ ResultGreeting::new(name)
+}
+
+#[chain]
+fn handle_add(args: EntryAdd) -> Next {
+ let sum: i32 = args.inner.iter().filter_map(|s| s.parse::<i32>().ok()).sum();
+ ResultSum::new(sum)
+}
+
+#[renderer]
+fn render_greet(result: ResultGreeting) {
+ r_println!("Hello, {}!", *result);
+}
+
+#[renderer]
+fn render_sum(result: ResultSum) {
+ r_println!("Sum: {}", *result);
+}
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatchers((CMDGreet, CMDAdd));
+ program.exec_and_exit();
+}
+
+gen_program!();
+```
+
+两个命令共享同一个管线模型,但各走各的:
+
+```text
+> my-cli greet Alice
+Hello, Alice!
+> my-cli add 1 2 3
+Sum: 6
+```
+
+## 注册多个分发器
+
+注意到 `with_dispatchers` 了吗?当你需要注册多个分发器时,一次传一个元组就行:
+
+```rust
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@dispatcher!("add", CMDAdd => EntryAdd);
+@@@pack!(ResultGreeting = String);
+@@@pack!(ResultSum = i32);
+@@@#[chain] fn handle_greet(_args: EntryGreet) -> Next { ResultGreeting::new("ok".into()) }
+@@@#[renderer] fn render_greet(_greeting: ResultGreeting) { r_println!("hi"); }
+@@@#[chain] fn handle_add(_args: EntryAdd) -> Next { ResultSum::new(0) }
+@@@#[renderer] fn render_sum(_sum: ResultSum) { r_println!("sum"); }
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatchers((CMDGreet, CMDAdd));
+ program.exec_and_exit();
+}
+```
+
+等价于一个个注册,效果一样。
+
+> [!TIP]
+> 元组最多支持 7 个分发器。超过 7 个时链式调用 `with_dispatcher` 就行。
+
+## 子命令
+
+多层级的命令也是同理——每个点号分隔的层级都只是名字的一部分:
+
+```rust
+dispatcher!("remote.add", CMDRemoteAdd => EntryRemoteAdd);
+dispatcher!("remote.rm", CMDRemoteRm => EntryRemoteRm);
+```
+
+每个子命令的 Entry、Chain、Renderer 完全独立,互不干扰。
+
+## 数据类型的独立性
+
+注意我们用了两个不同的 `pack!`:
+
+- `pack!(ResultGreeting = String)`
+- `pack!(ResultSum = i32)`
+
+它们都是独立的类型,`gen_program!()` 会给它们分配不同的枚举变体。
+
+调度器永远不会把 `ResultGreeting` 的数据送到 `render_sum` 去 —— **类型安全从命名那一刻就保证了**。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/6-argument-parse-picker.md b/docs/_zh_CN/pages/6-argument-parse-picker.md
new file mode 100644
index 0000000..9c7e028
--- /dev/null
+++ b/docs/_zh_CN/pages/6-argument-parse-picker.md
@@ -0,0 +1,393 @@
+<h1 align="center">使用 Picker 完成参数解析</h1>
+<p align="center">
+ 用 Picker 完成基本的参数解析
+</p>
+
+前面教程中我们都是手动从 `EntryGreet.inner`(`Vec<String>`)中提取参数。
+
+```rust
+@@@ fn main() {
+@@@ let args : Vec<String> = vec![];
+let name = args.first().cloned().unwrap_or_else(|| "World".to_string());
+@@@ }
+```
+
+但是,对于参数较多的场景,这个方案就不够用了:Mingling 提供了 `Picker` —— 通过链式调用来提取和转换参数。
+
+要启用 `Picker`,你需要修改 `Cargo.toml`
+
+```toml
+# Cargo.toml
+[dependencies.mingling]
+features = ["parser"]
+```
+
+好了,让我们看看 `Picker` 的写法:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+
+#[chain]
+fn handle_greet_entry(prev: EntryGreet) -> Next {
+ let name = prev.pick_or((), "World").unpack();
+ ResultName::new(name)
+}
+```
+
+`AsPicker` 为所有可以转换为 `Vec<String>` 的类型实现了 `pick`、`pick_or`、`pick_or_route` 函数:它们可以语义化地从字符串列表中 **拾取 (Pick)** 参数,并转换为结构化数据。
+
+对于上述示例中的代码:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@#[chain]
+@@@fn handle_greet_entry(prev: EntryGreet) -> Next {
+let name = prev.pick_or((), "World").unpack();
+@@@ResultName::new(name)
+@@@}
+```
+
+它的语义为:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@#[chain]
+@@@fn handle_greet_entry(prev: EntryGreet) {
+@@@let name: String =
+ prev.pick_or((), "World").unpack();
+// ~~~~ ~~~~~~~ ~~ ~~~~~~~ ~~~~~~~~
+// | | | | |_ 解包为 String
+// | | | |__________ 默认值为 "World"
+// | | |______________ 取出第一个位置参数(不指定标志)
+// | |______________________ 拾取或使用默认
+// |___________________________ 从前一个输入中
+@@@}
+```
+
+## 解析标志参数
+
+若你的程序需要解析标志参数(例如 `greet --name Alice`),可以使用如下方式
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+
+#[chain]
+fn handle_greet_entry(prev: EntryGreet) -> Next {
+ let name = prev.pick_or(["--name", "-n"], "World").unpack();
+ ResultName::new(name)
+}
+```
+
+同理,它的语义为:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@#[chain]
+@@@fn handle_greet_entry(prev: EntryGreet) {
+@@@let name: String =
+ prev.pick_or(["--name", "-n"], "World").unpack();
+// ~~~~ ~~~~~~~ ~~~~~~~~~~~~~~~~ ~~~~~~~ ~~~~~~~~
+// | | | | |_ 解包为 String
+// | | | |__________ 默认值为 "World"
+// | | |____________________________ 取出 "--name" 或 "-n" 后面的参数
+// | |____________________________________ 拾取或使用默认
+// |_________________________________________ 从前一个输入中
+@@@}
+```
+
+## 关于 `.unpack()`
+
+你可能注意到了,`Picker` 在命令解析的最后,会执行一个 `.unpack()` 函数,它的作用是将前面解析出来的结果,转换为结构化信息。
+
+对于只拾取了一次的数据来说,`.unpack()` 会返回单个数据,而对于多次拾取,`Picker` 则会返回元组:
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("test", CMDTest => EntryTest);
+@@@pack!(ResultInfo = (String, u8, u32));
+
+#[chain]
+fn handle_test_entry(prev: EntryTest) -> Next {
+ let (name, age, id) = prev
+ .pick::<String>(["--name", "-n"])
+ .pick::<u8>(["--age", "-a"])
+ .pick::<u32>(["--id", "-I"])
+ .unpack();
+
+ ResultInfo::new((name, age, id))
+}
+```
+
+> [!IMPORTANT]
+> `Picker` 对解析顺序极其敏感,特别是位置参数:因为它是顺序解析的。若你需要解析位置参数,请确保解析前已拾取并消费所有 **标志参数**。
+
+## 使用 `pick_or_route` 处理边界情况
+
+就像那句老话:"永远不要相信你的用户"。为了应对必要参数缺失、输入类型不匹配等错误情况,`pick_or_route` 能将执行链路由到专门的错误处理类型上。
+
+先来看一个简单示例
+
+```rust
+// Features: ["parser", "extra_macros"]
+@@@use mingling::macros::route;
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@pack!(ErrorNoName = ());
+
+#[chain]
+fn handle_greet_entry(prev: EntryGreet) -> Next {
+ let pick_result = prev
+ .pick_or_route(["--name", "-n"], ErrorNoName::default())
+ .unpack();
+
+ // 使用 route! 宏展开 pick_result
+ let name = route!(pick_result);
+ ResultName::new(name).into()
+}
+
+#[renderer]
+fn render_no_name(_prev: ErrorNoName) {
+ r_println!("Error: No name provided.");
+}
+
+#[renderer]
+fn render_name(prev: ResultName) {
+ r_println!("Hello, {}!", *prev);
+}
+```
+
+若使用 `pick_or_route`,写法会变得相对复杂:因为 `.unpack()` 不再直接返回参数,而是 `Result<Value, Route>`。
+
+不过 **Mingling** 的 `extra_macros` 特性提供了简化展开的宏 `route!`,它不复杂,只是省略了一部分样板代码:
+
+```rust
+// Features: ["parser", "extra_macros"]
+@@@ pack!(ErrorFail = ());
+@@@ use mingling::macros::route;
+@@@ fn func() -> mingling::ChainProcess<ThisProgram> {
+@@@ let args: Vec<String> = vec![];
+@@@ let pick_result = args.pick_or_route::<String, _>((), ErrorFail::new(())).unpack();
+let name = route!(pick_result);
+@@@ mingling::macros::empty_result!()
+@@@ }
+```
+
+它展开为:
+
+```rust
+// Features: ["parser", "extra_macros"]
+@@@ pack!(ErrorFail = ());
+@@@ fn func() -> mingling::ChainProcess<ThisProgram> {
+@@@ let args: Vec<String> = vec![];
+@@@ let pick_result = args.pick_or_route::<String, _>((), ErrorFail::new(())).unpack();
+let name = match pick_result {
+ Ok(r) => r,
+ Err(e) => return e.to_chain(),
+};
+@@@ mingling::macros::empty_result!()
+@@@ }
+```
+
+## 提取值的后处理
+
+在您使用 `pick` 提取了用户输入后,可以使用 `after` 立刻处理该参数
+
+```rust
+// Features: ["parser"]
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+
+#[chain]
+fn handle_greet_entry(prev: EntryGreet) -> Next {
+ let name = prev
+ .pick_or(["--name", "-n"], "World")
+ // 在提取出 --name 后,立刻格式化
+ .after(|name: String| {
+ name.replace(['-', '_', '.'], " ")
+ .to_lowercase()
+ .trim()
+ .to_string()
+ })
+ .unpack();
+
+ ResultName::new(name)
+}
+```
+
+同样,你可以使用 `after_or_route` 来处理输入参数的格式错误
+
+```rust
+// Features: ["parser", "extra_macros"]
+@@@use mingling::macros::route;
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultName = String);
+@@@pack!(ErrorNameTooLong = usize);
+
+#[chain]
+fn handle_greet_entry(prev: EntryGreet) -> Next {
+ let pick_result = prev
+ .pick_or(["--name", "-n"], "World")
+ .after_or_route(|name: &String| {
+ if name.len() < 32 {
+ Ok(name.clone())
+ } else {
+ Err(ErrorNameTooLong::new(name.len()))
+ }
+ })
+ .unpack();
+ let name = route!(pick_result);
+
+ ResultName::new(name).into()
+}
+
+#[renderer]
+fn render_name_too_long(prev: ErrorNameTooLong) {
+ let len = *prev;
+ r_println!("Error: name too long (length: {} > 32)", len);
+}
+
+#[renderer]
+fn render_name(prev: ResultName) {
+ r_println!("Hello, {}!", *prev);
+}
+```
+
+## 布尔值解析
+
+`Picker` 当然也可以解析布尔类型,但是布尔类型分为显式和隐式模式:
+
+| 模式 | 格式 |
+| ---- | ----------------------------------- |
+| 隐式 | `--confirmed` |
+| 显式 | `--confirm true` 或 `--confirm yes` |
+
+- 使用 `.pick::<bool>(flag)` 时,采用隐式解析:只要标志存在即为 `true`
+- 使用 `.pick::<Yes>(flag)` 或 `.pick::<True>(flag)` 时,采用显式解析
+
+一般来说使用隐式解析即可,但在处理重要的确认行为时,显式逻辑更符合语义。
+
+```rust
+// Features: ["parser"]
+@@@use mingling::parser::Yes;
+@@@dispatcher!("test", CMDTest => EntryTest);
+@@@pack!(ResultDone = ());
+
+#[chain]
+fn handle_entry(prev: EntryTest) -> Next {
+@@@ let prev1 = prev.clone();
+ let _confirmed: bool = prev.pick::<Yes>(()).unpack().is_yes();
+@@@ let prev = prev1;
+ let _confirm: bool = prev.pick::<bool>(["--confirm", "-C"]).unpack();
+ ResultDone::default().to_render()
+}
+```
+
+## 特殊用法:`usize` 解析
+
+**Mingling** 为 `usize` 提供了一个特殊的用法:解析类似 `25G`、`32mib` 等字样
+
+```rust
+// Features: ["parser"]
+
+#[test]
+fn parse_size() {
+ let vec = vec!["--size".to_string(), "25mib".to_string()];
+ let size: usize = vec.pick(["--size", "-S"]).unpack();
+ assert_eq!(size, 25 * 1024 * 1024);
+}
+```
+
+## 自定义可解析类型
+
+你可以使用 `Pickable` trait 使你的类型支持被 `Picker` 解析,这也是 `Picker` 拓展性的来源
+
+```rust
+// Features: ["parser"]
+@@@use mingling::parser::{Pickable, Argument};
+@@@use mingling::Flag;
+#[derive(Default)]
+pub struct Address {
+ ip: String,
+ port: u16,
+}
+
+impl Pickable for Address {
+ type Output = Self;
+ fn pick(args: &mut Argument, flag: Flag) -> Option<Self::Output> {
+ let raw = args.pick_argument(flag)?;
+ let parts: Vec<&str> = raw.split(':').collect();
+ let ip = parts.first()?.to_string();
+ let port: u16 = parts.get(1)?.parse().ok()?;
+ Some(Address { ip, port })
+ }
+}
+@@@dispatcher!("connect", CMDConnect => EntryConnect);
+@@@pack!(ResultConnected = Address);
+
+#[chain]
+fn handle_connect_entry(prev: EntryConnect) -> Next {
+ let address: Address = prev.pick("--addr").unpack();
+ ResultConnected::new(address)
+}
+
+#[renderer]
+fn render_connected(prev: ResultConnected) {
+ let addr = prev.inner;
+ r_println!("Connected: IP: {} PORT: {}", addr.ip, addr.port);
+}
+```
+
+执行效果如下:
+
+```text
+~# my-cli connect --addr 127.0.0.1:8080
+Connected: IP: 127.0.0.1 PORT: 8080
+```
+
+## 自动为枚举实现 Pickable
+
+要为枚举类型实现 `Pickable`,只需该枚举实现了 `EnumTag`,然后为其实现 `PickableEnum` 即可
+
+```rust
+// Features: ["parser"]
+@@@use mingling::parser::PickableEnum;
+@@@use mingling::EnumTag;
+#[derive(Debug, Default, EnumTag)]
+pub enum Fruits {
+ #[default]
+ Apple,
+ Banana,
+ Orange,
+}
+
+impl PickableEnum for Fruits {}
+@@@dispatcher!("eat", CMDEat => EntryEat);
+@@@pack!(ResultFruit = Fruits);
+
+#[chain]
+fn handle_eat_entry(prev: EntryEat) -> Next {
+ let fruit: Fruits = prev.pick("--fruit").unpack();
+ ResultFruit::new(fruit)
+}
+
+#[renderer]
+fn render_fruit(prev: ResultFruit) {
+ r_println!("Picked fruit: {:?}", *prev);
+}
+```
+
+以上便是 `Picker` 的所有用法。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/7-argument-parse-clap.md b/docs/_zh_CN/pages/7-argument-parse-clap.md
new file mode 100644
index 0000000..e15bebb
--- /dev/null
+++ b/docs/_zh_CN/pages/7-argument-parse-clap.md
@@ -0,0 +1,87 @@
+<h1 align="center">使用 Clap 完成参数解析</h1>
+<p align="center">
+ 用 clap 做更复杂的参数解析
+</p>
+
+Picker 适合轻量级的参数提取,但当参数数量多、有复杂的校验规则、或者需要自动生成 `--help` 时,可以接入 [clap](https://crates.io/crates/clap)。
+
+## 开启 clap 特性
+
+```toml
+[dependencies.mingling]
+features = ["clap"]
+
+[dependencies.clap]
+version = "4"
+features = ["derive", "color"]
+```
+
+## dispatcher_clap
+
+`#[dispatcher_clap]` 加在 `clap::Parser` 结构体上,自动生成 Dispatcher:
+
+```rust
+// Features: ["clap"]
+// Dependencies:
+// clap = "4"
+@@@ use mingling::macros::dispatcher_clap;
+#[derive(Default, clap::Parser, Groupped)]
+#[dispatcher_clap("greet", CMDGreet, help = true, error = ErrorGreetParsed)]
+pub struct EntryGreet {
+ #[clap(default_value = "World")]
+ name: String,
+ #[arg(short, long, default_value_t = 1)]
+ repeat: i32,
+}
+
+#[renderer]
+fn render_greet(greet: EntryGreet) {
+ let count = greet.repeat.max(0) as usize;
+ r_print!("Hello, ");
+ for _ in 0..count {
+ r_print!("{} ", greet.name);
+ }
+ r_println!("!");
+}
+
+#[renderer]
+fn render_greet_parse_failed(err: ErrorGreetParsed) {
+ r_println!("{}", *err);
+}
+```
+
+## 与 BasicProgramSetup 配合
+
+如果需要 `--help` 支持,在 main 中注册 `BasicProgramSetup` 并设置 clap 帮助的输出模式:
+
+```rust
+// Features: ["clap"]
+// Dependencies:
+// clap = "4"
+@@@use mingling::setup::BasicProgramSetup;
+@@@use mingling::macros::dispatcher_clap;
+@@@#[derive(Default, clap::Parser, Groupped)]
+@@@#[dispatcher_clap("greet", CMDGreet)]
+@@@pub struct EntryGreet {
+@@@ name: String,
+@@@}
+@@@#[renderer]
+@@@fn render_greet(greet: EntryGreet) {
+@@@ r_println!("Hello, {}!", greet.name);
+@@@}
+@@@
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(BasicProgramSetup);
+ program.stdout_setting.clap_help_print_behaviour =
+ mingling::ClapHelpPrintBehaviour::WriteToRenderResult;
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+```
+
+详见 [example-clap-binding](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-clap-binding)。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/8-setup-and-resources.md b/docs/_zh_CN/pages/8-setup-and-resources.md
new file mode 100644
index 0000000..d38e69d
--- /dev/null
+++ b/docs/_zh_CN/pages/8-setup-and-resources.md
@@ -0,0 +1,89 @@
+<h1 align="center">程序装配</h1>
+<p align="center">
+ 用 Setup 初始化你的程序
+</p>
+
+当程序启动时需要做一些初始化工作——比如解析全局参数、注册资源——你可以用 `#[program_setup]` 来组织这些逻辑。
+
+## 用 Setup 做初始化
+
+```rust
+// Features: ["extra_macros"]
+@@@use mingling::macros::program_setup;
+@@@use mingling::Program;
+#[program_setup]
+fn my_setup(program: &mut Program<ThisProgram>) {
+ // 从参数中提取全局标志
+ program.global_flag(["-v", "--verbose"], |program| {
+ program.stdout_setting.verbose = true;
+ });
+}
+@@@
+@@@fn main() {
+@@@ let mut program = ThisProgram::new();
+@@@ program.with_setup(MySetup);
+@@@ program.exec_and_exit();
+@@@}
+@@@gen_program!();
+```
+
+`#[program_setup]` 标记的函数接收 `&mut Program<ThisProgram>`,你可以在里面做任何初始化操作。
+
+在 `main` 里通过 `program.with_setup(...)` 注册即可使用。
+
+> [!NOTE]
+> `#[program_setup]` 需要 `extra_macros` 特性。没有此特性时,可以手动实现 `ProgramSetup` trait。
+
+## 提取全局参数
+
+Setup 里最常用的操作就是提取全局参数。Mingling 提供了几个辅助方法:
+
+```rust
+// Features: ["extra_macros"]
+@@@use mingling::macros::program_setup;
+@@@use mingling::Program;
+#[program_setup]
+fn my_setup(program: &mut Program<ThisProgram>) {
+ // 布尔标志
+ program.global_flag(["-v", "--verbose"], |program| {
+ program.stdout_setting.verbose = true;
+ });
+
+ // 带值的参数
+ program.global_argument("--name", |_program, value| {
+ // value 就是 "Alice"
+ let _ = value;
+ });
+}
+```
+
+> [!TIP]
+> `global_flag` 和 `global_argument` 会自动从 `program.args` 中移除已匹配的参数,这些参数不会进入管线。
+
+## 内置 Setup
+
+Mingling 提供了一些开箱即用的 Setup,覆盖了 CLI 程序最常见的需求:
+
+| Setup | 功能 |
+| --------------------------- | --------------------------------------------------------------------- |
+| `BasicProgramSetup` | 解析 `--help`/`-h`、`--quiet`/`-q`、`--confirm`/`-C` |
+| `DirectoryEnvironmentSetup` | 注册目录资源:当前目录、可执行目录、Home 目录、临时目录 |
+| `ExitCodeSetup` | 通过 `ResExitCode` 控制程序退出码 |
+| `StructuralRendererSetup` | 启用 `--json`、`--yaml` 等结构化输出(需 `structural_renderer` 特性) |
+
+用法就是在 `main` 里加一行:
+
+```rust
+@@@use mingling::setup::BasicProgramSetup;
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_setup(BasicProgramSetup);
+ program.exec_and_exit();
+}
+```
+
+`BasicProgramSetup` 帮你处理了绝大多数 CLI 程序都需要的通用参数,省去自己手动解析的麻烦。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/9-error-handling.md b/docs/_zh_CN/pages/9-error-handling.md
new file mode 100644
index 0000000..a4d5b0a
--- /dev/null
+++ b/docs/_zh_CN/pages/9-error-handling.md
@@ -0,0 +1,120 @@
+<h1 align="center">错误处理</h1>
+<p align="center">
+ 将错误优雅地展示给用户
+</p>
+
+管线里不只有成功路径。当输入有误、资源找不到、操作失败时,你需要一个地方来处理这些"意外",而不是让程序 panic。
+
+## 两个路径:成功 vs 错误
+
+回顾管线模型,Chain 的返回值是 `Next`,它有两个去向:
+
+| 路由 | 含义 |
+| -------------- | ---------------------------- |
+| `.to_render()` | 出结果了,交给 Renderer 展示 |
+| `.to_chain()` | 还没处理完,交给下一个 Chain |
+
+错误类型的值也可以走任意一条路——你可以选择直接渲染错误信息,也可以交给下一个 Chain 尝试恢复。
+
+## 用独立类型区分错误
+
+```rust
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+pack!(ResultGreeting = String);
+pack!(ErrorNameEmpty = String);
+
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_default();
+
+ if name.is_empty() {
+ ErrorNameEmpty::new("name is required".to_string()).to_render()
+ } else {
+ ResultGreeting::new(name).to_render()
+ }
+}
+```
+
+然后各自写 Renderer:
+
+```rust
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+@@@pack!(ResultGreeting = String);
+@@@pack!(ErrorNameEmpty = String);
+@@@#[chain] fn handle_greet(args: EntryGreet) -> Next { ResultGreeting::new(args.inner.first().cloned().unwrap_or_default()).to_render() }
+
+#[renderer]
+fn render_greeting(result: ResultGreeting) {
+ r_println!("Hello, {}!", *result);
+}
+
+#[renderer]
+fn render_error_name_empty(err: ErrorNameEmpty) {
+ r_println!("Error: {}", *err);
+}
+```
+
+两个 Renderer 各司其职,用户看到什么取决于 Chain 返回了什么。
+
+## 完整的例子
+
+```rust
+dispatcher!("greet", CMDGreet => EntryGreet);
+
+pack!(ResultGreeting = String);
+pack!(ErrorNameEmpty = String);
+
+#[chain]
+fn handle_greet(args: EntryGreet) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_default();
+ if name.is_empty() {
+ ErrorNameEmpty::new("name is required".to_string()).to_render()
+ } else {
+ ResultGreeting::new(name).to_render()
+ }
+}
+
+#[renderer]
+fn render_greeting(result: ResultGreeting) {
+ r_println!("Hello, {}!", *result);
+}
+
+#[renderer]
+fn render_error_name_empty(err: ErrorNameEmpty) {
+ r_println!("Error: {}", *err);
+}
+
+fn main() {
+ let mut program = ThisProgram::new();
+ program.with_dispatcher(CMDGreet);
+ program.exec_and_exit();
+}
+
+gen_program!();
+```
+
+运行效果:
+
+```text
+~# my-cli greet Alice
+Hello, Alice!
+
+~# my-cli greet
+Error: name is required
+```
+
+## 关于 `pack_err!`
+
+如果你启用了 `extra_macros`,还可以用 `pack_err!` 快速声明带有自动 `name` 字段的错误类型:
+
+```rust
+// Features: ["extra_macros"]
+pack_err!(ErrorNotFound);
+// 生成: struct ErrorNotFound { pub name: String }
+```
+
+详见 [特性列表](pages/other/features)。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/advanced/.name b/docs/_zh_CN/pages/advanced/.name
new file mode 100644
index 0000000..eb92a2d
--- /dev/null
+++ b/docs/_zh_CN/pages/advanced/.name
@@ -0,0 +1 @@
+进阶
diff --git a/docs/_zh_CN/pages/advanced/1-completion.md b/docs/_zh_CN/pages/advanced/1-completion.md
new file mode 100644
index 0000000..3941404
--- /dev/null
+++ b/docs/_zh_CN/pages/advanced/1-completion.md
@@ -0,0 +1,83 @@
+<h1 align="center">补全</h1>
+<p align="center">
+ 使用 comp 特性实现完全动态的补全系统
+</p>
+
+Mingling 的补全是**完全动态**的——没有静态的补全文件,而是在运行时根据用户当前输入实时计算补全建议。
+
+## 开启 comp
+
+```toml
+# Cargo.toml
+[dependencies.mingling]
+features = ["comp"]
+
+[build-dependencies.mingling]
+features = [
+ "comp",
+ # 启用 `builds` 特性以提供构建期支持
+ "builds"
+]
+```
+
+## 工作原理
+
+当用户按下 `TAB` 时,补全脚本会调用程序的隐藏子命令 `__comp`,它会根据输入的 `ShellContext` 动态地查询最合适的建议。
+
+这个隐藏子命令由 `gen_program!()` 在启用 `comp` 特性时自动生成,对应的分发器是 `CMDCompletion`,你需要使用 `with_dispatcher` 添加到程序中。
+
+补全流程:
+
+1. 二次匹配用户当前输入的 `Dispatcher`
+2. 调用对应的 `#[completion]` 函数
+3. 函数返回 `Suggest`(文件补全或建议列表)
+4. 通知 Shell 将建议呈现出来
+
+## 定义补全
+
+用 `#[completion(EntryType)]` 为 Entry 定义补全逻辑:
+
+```rust
+// Features: ["comp"]
+@@@use mingling::prelude::*;
+@@@use mingling::{ShellContext, Suggest, SuggestItem};
+@@@use std::collections::BTreeSet;
+@@@dispatcher!("greet", CMDGreet => EntryGreet);
+
+#[completion(EntryGreet)]
+fn complete_greet(ctx: &ShellContext) -> Suggest {
+ if ctx.previous_word == "greet" {
+ let mut items = BTreeSet::new();
+ items.insert(SuggestItem::new_with_desc("Alice".into(), "Likes to receive messages".into()));
+ items.insert(SuggestItem::new("World".into()));
+ Suggest::Suggest(items)
+ } else {
+ Suggest::FileCompletion
+ }
+}
+```
+
+`suggest!` 宏是更简洁的写法,效果相同:
+
+```rust
+// Features: ["comp"]
+@@@use mingling::macros::suggest;
+@@@fn example() {
+suggest! {
+ "Alice": "Likes to receive messages",
+ "World"
+};
+@@@}
+```
+
+`ShellContext` 包含用户当前的输入状态(`previous_word`、`current_word`、`all_words` 等),`Suggest` 有两种变体:`Suggest::Suggest(list)` 返回建议列表,`Suggest::FileCompletion` 交给 shell 自己做文件补全。
+
+## 生成补全脚本
+
+在 `build.rs` 中调用 `build_comp_scripts` 生成补全脚本(需要 `builds` + `comp` 特性)。
+
+详见 [example-completion](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-completion)。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/advanced/2-structural-renderer.md b/docs/_zh_CN/pages/advanced/2-structural-renderer.md
new file mode 100644
index 0000000..14164da
--- /dev/null
+++ b/docs/_zh_CN/pages/advanced/2-structural-renderer.md
@@ -0,0 +1,120 @@
+<h1 align="center">结构化渲染</h1>
+<p align="center">
+ 使用 structural_renderer 特性将结果渲染为序列化文本
+</p>
+
+启用 `structural_renderer` 后,你的程序可以通过 `--json`、`--yaml` 等参数将输出切换为结构化格式,方便与其他工具集成。
+
+## 开启特性
+
+```toml
+[dependencies.mingling]
+features = ["structural_renderer"]
+```
+
+`structural_renderer` 会自动启用 `json_serde_fmt`。
+
+如果需要更多格式,可以启用 `structural_renderer_full`(包含 JSON、YAML、TOML、RON)。
+
+> [!NOTE]
+> 若需要定制输出类型,可以查看 [特性](./pages/other/features)
+
+## 基本用法
+
+启用 `StructuralRendererSetup` 后,用 `pack_structural!` 替代 `pack!` 来声明支持结构化输出的类型:
+
+```rust
+// Features: ["structural_renderer"]
+// Dependencies:
+// serde = "1"
+@@@use mingling::setup::StructuralRendererSetup;
+@@@dispatcher!("render", CMDRender => EntryRender);
+
+// pack_structural! 等价于 pack! + StructuralData
+pack_structural!(ResultInfo = (String, i32));
+
+#[chain]
+fn handle_render(args: EntryRender) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_default();
+ let age = args.inner.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
+ ResultInfo::new((name, age))
+}
+
+#[renderer]
+fn render_info(r: ResultInfo) {
+ r_println!("{:?}", *r);
+}
+```
+
+运行效果:
+
+```text
+~# my-cli render Bob 22
+("Bob", 22)
+
+~# my-cli render Bob 22 --json
+{"inner":["Bob",22]}
+```
+
+用户传入 `--json` 时,框架自动将渲染结果序列化为 JSON,无需修改业务代码。
+
+## 自定义输出结构
+
+`pack_structural!` 的默认输出包含 `inner` 字段。要完全控制输出结构,可以用 `#[derive(StructuralData, Serialize, Groupped)]` 手动定义类型:
+
+```rust
+// Features: ["structural_renderer"]
+// Dependencies:
+// serde = "1"
+@@@use mingling::prelude::*;
+@@@use mingling::setup::StructuralRendererSetup;
+@@@use mingling::StructuralData;
+@@@use serde::Serialize;
+@@@dispatcher!("render", CMDRender => EntryRender);
+
+#[derive(Serialize, StructuralData, Groupped)]
+struct Info {
+ name: String,
+ age: i32,
+}
+
+#[chain]
+fn handle_render(args: EntryRender) -> Next {
+ let name = args.inner.first().cloned().unwrap_or_default();
+ let age = args.inner.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
+ Info { name, age }.to_render()
+}
+
+#[renderer]
+fn render_info(info: Info) {
+ r_println!("{} is {} years old", info.name, info.age);
+}
+@@@
+@@@fn main() {
+@@@ let mut program = ThisProgram::new();
+@@@ program.with_setup(StructuralRendererSetup);
+@@@ program.with_dispatcher(CMDRender);
+@@@ program.exec();
+@@@}
+@@@gen_program!();
+```
+
+这时 `--json` 输出:
+
+```json
+{ "name": "Bob", "age": 22 }
+```
+
+## 注意事项
+
+- 支持格式:JSON、YAML、TOML、RON(取决于启用的特性)
+- `StructuralRendererSetup` 注册 `--json`、`--yaml`、`--toml`、`--ron` 等全局参数
+
+> [!NOTE]
+> 每个类型仍需一个**空的 Renderer**,否则该类型 **不被视为可渲染**
+
+详见 [example-structural-renderer](https://mingling-rs.github.io/mingling/docs/example-viewer.html?name=example-structural-renderer)。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/concepts/.name b/docs/_zh_CN/pages/concepts/.name
new file mode 100644
index 0000000..dfd4543
--- /dev/null
+++ b/docs/_zh_CN/pages/concepts/.name
@@ -0,0 +1 @@
+核心概念
diff --git a/docs/_zh_CN/pages/concepts/1-the-pipeline.md b/docs/_zh_CN/pages/concepts/1-the-pipeline.md
new file mode 100644
index 0000000..b0bb19d
--- /dev/null
+++ b/docs/_zh_CN/pages/concepts/1-the-pipeline.md
@@ -0,0 +1,129 @@
+<h1 align="center">基础管线</h1>
+<p align="center">
+ Mingling 的核心执行流程
+</p>
+
+Mingling 把命令的处理拆成三个独立的阶段:Dispatcher → Chain → Renderer。这篇讲实际的执行逻辑——从用户输入到最终输出,每一步发生了什么。
+
+## 完整流程
+
+```mermaid
+graph TD
+ A["program.exec_and_exit()"] --> B["Hook: pre_dispatch"]
+ B --> C["Dispatch<br/>匹配命令 → Entry"]
+ C --> D["Hook: post_dispatch"]
+ D --> E{"user_context.help?"}
+ E -->|"true"| F["render_help<br/>直接跳帮助渲染"]
+ E -->|"false"| G{"has_chain?"}
+ G -->|"有"| H["Hook: pre_chain"]
+ H --> I["do_chain<br/>执行业务逻辑"]
+ I --> J{"ChainProcess?"}
+ J -->|"Ok(any, Renderer)"| K["Hook: pre_render →<br/>render → post_render"]
+ J -->|"Ok(any, Chain)"| G
+ J -->|"Err"| L["finish"]
+ G -->|"无"| M{"has_renderer?"}
+ M -->|"有"| K
+ M -->|"无"| N["build_renderer_not_found"]
+ N --> G
+ K --> O["Hook: finish → 返回 RenderResult"]
+ L --> O
+ F --> O
+```
+
+## 阶段详解
+
+### 1. 分发
+
+`exec_with_args` 首先调用 `dispatch_args_dynamic` 或 `dispatch_args_trie`(取决于是否启用 `dispatch_tree` 特性),将用户输入的参数与注册的 Dispatcher 匹配。
+
+匹配规则是按空格分割的**前缀匹配**——优先匹配最长的那个。例如注册了 `remote.add` 和 `remote`,输入 `remote add origin` 会匹配 `remote.add`。
+
+```mermaid
+graph LR
+ Input["用户输入"] --> M{匹配 Dispatcher}
+ M -->|"匹配到"| E["调用 dispatcher.begin(args)<br/>返回包装好的 Entry"]
+ M -->|"未匹配"| NF["build_dispatcher_not_found<br/>生成 ErrorDispatcherNotFound"]
+```
+
+匹配成功后调用 `dispatcher.begin(args)`,返回 `ChainProcess::Ok((AnyOutput, _))`,即包装好用户输入参数的 Entry 类型。
+
+如果没有匹配到任何 Dispatcher,则生成 `ErrorDispatcherNotFound`(包裹完整的输入参数),后续可以被 Renderer 处理显示 "Command not found"。
+
+### 2. Help 短路
+
+在进入主循环前,检查 `program.user_context.help`。如果为 `true`(由 `BasicProgramSetup` 中的 `HelpFlagSetup` 在解析到 `--help` 时设置),直接调用 `render_help` 跳过整个管线。
+
+### 3. Chain 主循环
+
+这是核心调度逻辑。每次循环检查当前的 `AnyOutput`:
+
+1. **有 Chain 能处理** → 执行 `C::do_chain(current)`
+ - 返回 `(AnyOutput, Renderer)` → 退出循环,进入渲染
+ - 返回 `(AnyOutput, Chain)` → 继续循环,把结果交给下一个 Chain
+ - 返回 `Err` → 终止程序
+
+2. **没有 Chain,但有 Renderer** → 直接渲染
+
+3. **两者都没有** → 生成 `renderer_not_found`,再循环一次(因为刚生成的这个类型可能有 Renderer)
+
+```mermaid
+graph TD
+ Start["当前 AnyOutput"] --> C{"has_chain?"}
+ C -->|"是"| Chain["do_chain"]
+ Chain -->|"返回 (any, Chain)"| C
+ Chain -->|"返回 (any, Renderer)"| Render["render"]
+ Chain -->|"Err"| Exit["退出"]
+ C -->|"否"| R{"has_renderer?"}
+ R -->|"是"| Render
+ R -->|"否"| N["build_renderer_not_found<br/>再试一次"]
+ N --> C
+```
+
+### 4. Render
+
+渲染阶段调用 `C::render(any, &mut render_result)`,根据 `member_id` 找到对应的 `#[renderer]` 函数,将结果写入 `RenderResult`。如果启用了 `structural_renderer`,还会根据 `program.structural_renderer_name` 将结果序列化为 JSON/YAML 等格式。
+
+### 5. 退出
+
+设置 `exit_code`,触发 `finish` hook,返回 `RenderResult`。
+
+> [!TIP]
+> 这套运行时调度代码由 `gen_program!()` 生成的枚举和 `ProgramCollect` 实现来驱动。
+>
+> 编译期只生成了类型到 Chain / Renderer / Help / Completion 的映射关系,实际的匹配和路由都是在运行时完成的。
+
+## 这和直接函数调用的区别
+
+这套管线可以避免你写出如下的代码:
+
+```rust
+@@@ struct Config;
+@@@ impl Config { fn read() -> Self { Config } }
+@@@ fn main() {
+@@@ let json = true;
+// 读取配置
+let mut config = Config::read();
+
+// 执行操作
+let Ok(result) = operation(&mut config) else {
+ panic!("错误处理");
+};
+
+// 渲染结果
+if json {
+ print_json();
+} else {
+ println!("成功!");
+}
+@@@ }
+@@@ fn operation(config: &mut Config) -> Result<(),()> { Ok(()) }
+@@@ fn print_json() {}
+```
+
+Mingling 的管线把 **命令匹配**、**业务逻辑**、**输出展示** 拆成三个独立的位置,每个位置只负责一件事。
+
+更重要的是,管线经过 hook 和 `AnyOutput` 机制,允许横切关注点(日志、鉴权、退出码)无侵入地插入,不会污染你的业务代码。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/concepts/2-resource.md b/docs/_zh_CN/pages/concepts/2-resource.md
new file mode 100644
index 0000000..1052254
--- /dev/null
+++ b/docs/_zh_CN/pages/concepts/2-resource.md
@@ -0,0 +1,60 @@
+<h1 align="center">资源系统</h1>
+<p align="center">
+ Mingling 如何管理全局状态
+</p>
+
+命令行程序经常需要共享一些全局的东西——配置文件、数据库连接、计数器、当前工作目录。
+
+在普通 Rust 里你可能会用 `OnceCell` 或 `lazy_static`,在 Mingling 里有一套统一的机制:**资源系统**。
+
+## 什么是资源?
+
+资源就是在多个 Chain 和 Renderer 之间共享的数据。
+
+你只需要定义一个类型、注册到 Program,然后在函数签名里声明你需要它——剩下的注入和生命周期管理都由框架完成。
+
+## 核心机制:ResourceMarker
+
+任何同时实现了 `Default + Clone` 的类型都可以自动成为资源。框架会为它实现 `ResourceMarker` trait,使其具备:
+
+- **`res_clone()`** —— 当多个 Chain 同时访问时,框架可以通过 clone 来避免锁竞争
+- **`res_default()`** —— 资源未注册时提供兜底值
+
+如果你需要更精细的生命周期控制,可以使用 `LazyRes<T>`。它允许资源在第一次被访问时才初始化,并且可以在析构时执行回调(比如退出前保存状态到磁盘)。
+
+## 为什么不用全局变量?
+
+传统做法的静态变量是隐式依赖 —— 你看函数签名根本不知道它用了什么全局状态。而 Mingling 的资源注入让 **依赖显式化**:
+
+- 函数需要什么资源,参数列表就写什么
+- `&T` 表示只读访问,`&mut T` 表示可修改
+- 调用者一眼就能看出这个函数的副作用
+
+例如:
+
+```rust
+@@@ use mingling::res::ResExitCode;
+@@@ pack!(ErrorFileNotFound = ());
+#[chain]
+fn handle_error_file_not_found(
+ error: ErrorFileNotFound,
+ ec: &mut ResExitCode // 通过签名可以看出副作用!
+) {
+ ec.exit_code = 2; // 这里修改了退出码
+}
+```
+
+## 资源与 Setup 的关系
+
+资源通常通过两个途径注册到 Program:
+
+1. **直接注册** —— 在 `main` 中调用 `program.with_resource(...)`
+2. **通过 Setup** —— 使用 `DirectoryEnvironmentSetup` 等内置 Setup 批量注册(如 `ResCurrentDir`、`ResHomeDir`)
+
+Setup 是比资源更高层的抽象,一个 Setup 可以注册多个资源并做其他初始化工作。
+
+详见教程中的 [程序装配](./pages/8-setup-and-resources) 一章。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/concepts/3-any-output.md b/docs/_zh_CN/pages/concepts/3-any-output.md
new file mode 100644
index 0000000..9b820da
--- /dev/null
+++ b/docs/_zh_CN/pages/concepts/3-any-output.md
@@ -0,0 +1,73 @@
+<h1 align="center">任意输出机制</h1>
+<p align="center">
+ 关于 AnyOutput 和 ChainProcess 的运作模式
+</p>
+
+Dispatcher → Chain → Renderer 三阶段之间传递的数据是什么?
+
+Chain 的输出可能是一个成功结果、一个错误、或者还需要继续交给下一个 Chain——这些类型各不相同,管线如何在编译期不知道具体类型的情况下,把它们送到正确的地方?
+
+## AnyOutput:类型擦除 + 组标签
+
+Mingling 的解法是把**所有类型擦除到同一个包装里**,然后用一个**枚举标签**来区分它们:
+
+```
+AnyOutput<G>
+├── inner: Box<dyn Any + Send> ← 真正的数据,类型已被擦除
+├── type_id: TypeId ← 运行时类型 ID,用于安全 downcast
+└── member_id: G ← 枚举标签,标记"这是谁"
+```
+
+这里的 `G` 就是 `gen_program!()` 生成的程序枚举(也就是你熟知的 `ThisProgram`)。
+
+每个被 `pack!` 或 `#[derive(Groupped)]` 标记的类型都被分配到这个枚举的一个变体。
+
+## ChainProcess:数据 + 路由
+
+在 `AnyOutput` 的基础上,`ChainProcess<G>` 加了一个**路由信息**:
+
+```
+ChainProcess<G>
+├── Ok(AnyOutput<G>, NextProcess) ← 携带数据,告诉调度器下一步去哪
+│ ├── NextProcess::Chain ← "还没完,继续交给下一个 Chain"
+│ └── NextProcess::Renderer ← "出结果了,展示给用户"
+└── Err(ChainProcessError) ← "出错了,终止程序"
+```
+
+这就是为什么 Chain 函数返回的不是裸数据,而是 `ChainProcess`——它把 **"下一步去哪"** 和 **"数据"** 打包在一起。
+
+调度器根据 `NextProcess` 决定是继续循环还是退出渲染。
+
+## Groupped:谁是谁
+
+调度器如何知道 `AnyOutput` 里装的是 `ResultName` 还是 `ErrorUserBlocked`?答案是 `Groupped` trait:
+
+```
+trait Groupped<G> {
+ fn member_id() -> G;
+}
+```
+
+当你用 `pack!(ResultName = String)` 时,宏自动为 `ResultName` 实现 `Groupped`,`member_id()` 返回枚举中对应的变体。调度器一看 `member_id`,就去找对应的 Chain 或 Renderer。
+
+`to_chain()` 和 `to_render()` 本质上是 `AnyOutput` 的快捷方法,分别构造 `ChainProcess::Ok(any, Chain)` 和 `ChainProcess::Ok(any, Renderer)`。
+
+## 调度的执行
+
+在运行时,主循环的工作就是:
+
+1. 看当前 `AnyOutput` 的 `member_id`
+2. 查这个变体有没有对应的 Chain → 有就执行,拿到新的 `AnyOutput` 和 `NextProcess`
+3. 如果 `NextProcess` 是 `Chain` → 回到第 1 步
+4. 如果 `NextProcess` 是 `Renderer` → 退出循环,渲染
+
+这套机制保证了**类型安全**:`gen_program!()` 生成的调度代码在做 `restore`(从 `Box<dyn Any>` 还原为具体类型)时,一定是在匹配的 `member_id` 分支内做的,不可能把 `ResultName` 的数据当作 `ErrorUserBlocked` 来解包。
+
+> [!TIP]
+> 日常开发中你不需要手动操作 `AnyOutput` 或 `ChainProcess`。
+>
+> `pack!`、`#[chain]`、`#[renderer]` 这些宏帮你处理了所有的包装和解包。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>
diff --git a/docs/_zh_CN/pages/concepts/4-program-collect.md b/docs/_zh_CN/pages/concepts/4-program-collect.md
new file mode 100644
index 0000000..f5e6b8f
--- /dev/null
+++ b/docs/_zh_CN/pages/concepts/4-program-collect.md
@@ -0,0 +1,52 @@
+<h1 align="center">关于 ProgramCollect</h1>
+<p align="center">
+ 了解 gen_program!() 是如何生成程序的
+</p>
+
+每个 Mingling 程序最后都有一行 `gen_program!()`。它在背后做了三件事,把整个程序的骨架搭建出来。
+
+## gen_program!() 的三件事
+
+### 1. 生成枚举
+
+扫描当前模块中所有 `pack!`、`#[chain]`、`#[renderer]` 等宏标记的类型,为每个类型生成一个枚举变体。
+
+这个枚举就是 `AnyOutput<G>` 中 `G` 的类型 —— 调度器靠枚举变体来区分管线中传递的不同数据。
+
+### 2. 生成 ProgramCollect 实现
+
+`ProgramCollect` 是一个 trait,定义了 **"每个枚举变体对应什么类型、由谁处理"** 的映射关系:
+
+- **`do_chain`** —— 根据 `member_id` 调用对应的 `#[chain]` 函数,返回新的 `AnyOutput` 和 `NextProcess`
+- **`render`** —— 根据 `member_id` 调用对应的 `#[renderer]` 函数,写入 `RenderResult`
+- **`render_help`** —— 根据 `member_id` 调用对应的 `#[help]` 函数
+- **`has_chain` / `has_renderer`** —— 判断某个变体有没有对应的处理函数
+- **`build_dispatcher_not_found` / `build_renderer_not_found` / `build_empty_result`** —— 三个内置降级类型,处理边界情况
+
+这套映射在运行时通过枚举匹配来完成——编译期只生成了枚举和匹配分支,实际的函数调用发生在运行时。
+
+### 3. 生成 ThisProgram
+
+生成 `ThisProgram` 类型别名,指向 `Program<生成的枚举>`。这就是为什么在 `main` 中可以直接写 `ThisProgram::new()`——它就是你整个程序的完整类型。
+
+---
+
+## 关于 `pathf` 和 `dispatch_tree` 下的差异
+
+以上是默认行为,但在启用特定 feature 时会有变化:
+
+### 1. `dispatch_tree` 特性
+
+Dispatcher 的匹配不再使用 `Vec<Box<dyn Dispatcher>>` 做线性匹配,而是在编译期将子命令结构构建为前缀树(Trie)。
+
+匹配复杂度从 `O(n)` 降到 `O(k)` —— `k` 是输入长度,与命令数量无关。
+
+### 2. `pathf` 特性(Module Pathfinder)
+
+默认情况下所有宏标记的类型必须在同一模块才能被 `gen_program!()` 收集到
+
+启用 `pathf` 后,编译期自动扫描所有子模块,找到所有用宏标记的类型并生成完整的模块路径引用 —— 类型定义在深层子模块也无需手动 `use`。
+
+<p align="center" style="font-size: 0.85em; color: gray;">
+ Written by @Weicao-CatilGrass
+</p>