Skip to main content

xyz_rust/
dispatch.rs

1// 根派发器:读取进程参数,自行判断运行模式,按派发产出的退出码退出。
2// 整个程序可以是一条 define 链。
3//
4// main(以及 main_config)派发进程级默认注册表并内部调用
5// std::process::exit,因此 main 里写的清理代码不会在它们之后执行。需要
6// 清理、自定义退出码或嵌入派发器时,用显式注册表的 run/run_config 版本,
7// 它们只返回退出码。
8//
9// 模式探测:
10//	<app> [命令] ...          -> CLI 前端(子命令、flag、位置参数、-h / -v)
11//	<app> mcp stdio|http      -> MCP 前端(官方 SDK;--versions 钉定协议版本)
12//	<app> serve [--addr ...]  -> HTTP 前端(REST + /openapi.json + /mcp)
13//	<app>(无参数)| help     -> 总览
14//
15// 模式关键词默认为 serve/http/mcp/help,皆可在 Modes 配置里重命名;顶层
16// 段撞词只是遮蔽让位(spec §13.1),不再是保留名。
17
18use std::io::Write as _;
19
20use crate::config::Config;
21use crate::ctx::Ctx;
22use crate::errors;
23use crate::registry::Registry;
24
25/// 前端编译标记(总览标注用;对齐 Go 的 cliFrontend/httpFrontend 常量)。
26pub const fn cli_frontend_compiled() -> bool {
27    cfg!(feature = "cli")
28}
29
30pub const fn http_frontend_compiled() -> bool {
31    cfg!(feature = "http")
32}
33
34pub const fn mcp_frontend_compiled() -> bool {
35    cfg!(feature = "mcp")
36}
37
38/// main 注册传入的全部已构建命令定义(来自 define)、按进程参数派发
39/// 默认注册表并按退出码退出。零个参数表示「命令已注册,只派发」。
40/// 需要自取退出码(嵌入、测试、清理)或用显式注册表时,用 run/run_config。
41pub fn main(cmds: &[&dyn crate::builder::Definable]) -> ! {
42    if !cmds.is_empty() {
43        for cmd in cmds {
44            if let Err(e) = cmd.register(Registry::default()) {
45                eprintln!("{e}");
46                std::process::exit(2);
47            }
48        }
49    }
50    let args: Vec<String> = std::env::args().skip(1).collect();
51    std::process::exit(run(Registry::default(), args));
52}
53
54/// 带自定义配置的 main(如重命名模式词)。
55pub fn main_config(cfg: Config) -> ! {
56    let args: Vec<String> = std::env::args().skip(1).collect();
57    std::process::exit(run_config(Registry::default(), args, cfg));
58}
59
60/// main 的显式参数与默认配置形态:返回退出码而不退出进程。
61pub fn run(reg: &Registry, args: Vec<String>) -> i32 {
62    run_config(reg, args, Config::default())
63}
64
65/// 带自定义配置的 run(重命名模式词、通道能力)。
66pub fn run_config(reg: &Registry, args: Vec<String>, cfg: Config) -> i32 {
67    run_internal(reg, args, cfg, false).0
68}
69
70/// 可组合派发:与 run_config 同管线,但当参数进入 CLI 模式且首段不是任何
71/// 已注册命令段/别名(也不是 flag)时,不打印任何东西、返回 (0, false),
72/// 由宿主路由其余参数(false 即「未命中」)。其余路径行为与 run_config
73/// 完全一致。
74pub fn try_run(reg: &Registry, args: Vec<String>) -> (i32, bool) {
75    try_run_config(reg, args, Config::default())
76}
77
78/// 带自定义配置的 try_run。
79pub fn try_run_config(reg: &Registry, args: Vec<String>, cfg: Config) -> (i32, bool) {
80    run_internal(reg, args, cfg, true)
81}
82
83/// CLI 首段是否命中树(已知命令段、别名或 default 子命令)。
84fn cli_known_top(reg: &Registry, first: &str) -> bool {
85    if first.starts_with('-') {
86        return true; // flag/帮助等交给 CLI 自身路径
87    }
88    for e in reg.all() {
89        if e.cli.skip {
90            continue;
91        }
92        if e.name.split('.').next() == Some(first) {
93            return true;
94        }
95        if e.cli.aliases.iter().any(|a| a == first) {
96            return true;
97        }
98        if e.cli.default {
99            return true; // 默认子命令吞掉未知名
100        }
101    }
102    false
103}
104
105fn run_internal(reg: &Registry, args: Vec<String>, cfg: Config, composable: bool) -> (i32, bool) {
106    let words = match resolve_modes(&cfg) {
107        Ok(m) => m,
108        Err(e) => {
109            eprintln!("{e}");
110            return (2, true);
111        }
112    };
113    // 没有任何已注册命令:什么都不做,静默退出 0。
114    if reg.names().is_empty() {
115        return (0, true);
116    }
117    // 壳能力:-v/--version 由根派发器管,任何能力组合下都可用
118    // ("--" 之后的 token 一律是位置参数,不再识别 -v)。
119    for a in &args {
120        if a == "--" {
121            break;
122        }
123        if a == "-v" || a == "--version" {
124            let bin = crate::cli::app::bin_name();
125            // 稳健写入:下游早关管道不 panic(对齐 Go 的 SIGPIPE 静默)。
126            let _ = writeln!(
127                std::io::stdout(),
128                "{bin} version {}",
129                crate::version::version()
130            );
131            return (0, true);
132        }
133    }
134    // 内置参数 --xyz.*:剥离开分发给各前端(帮助/版本不受影响)。
135    let mut cfg = cfg;
136    let args = match crate::builtins::strip_xyz_flags(args, &mut cfg) {
137        Ok(a) => a,
138        Err(e) => {
139            eprintln!("xyz: {e}");
140            return (2, true);
141        }
142    };
143    if cfg.log_level != crate::logx::Level::Unset {
144        crate::logx::set_level(cfg.log_level);
145    }
146    // 界面语言:--xyz.lang(已写回 cfg)> Config.lang > 环境检测 > 英文。
147    let resolved_lang = if !cfg.lang.is_empty() {
148        match crate::lang::XyzLang::parse(&cfg.lang) {
149            Some(l) => l,
150            None => {
151                eprintln!("xyz: invalid --xyz.lang {:?} (want en|zh-CN)", cfg.lang);
152                return (2, true);
153            }
154        }
155    } else {
156        crate::lang::XyzLang::detect()
157    };
158    crate::lang::set(
159        resolved_lang,
160        cfg.translations.get(resolved_lang.as_str()).cloned(),
161    );
162    // 遮蔽让位(spec §13.1):顶层段等于模式词的用户命令让裸词改为路由到
163    // 用户命令;内建模式经 xyz.<词> 仍可达。
164    let shadowed = shadowed_modes(reg, &words);
165    // 总览触发(§13.2 第 4 步):空参数 / 根 -h/--help。裸 help 词交给
166    // 模式匹配(被遮蔽时归用户命令)。
167    if args.is_empty() || args[0] == "--help" || args[0] == "-h" {
168        let mut stdout = std::io::stdout();
169        let _ = crate::overview::print_overview(
170            &mut stdout,
171            reg,
172            &words,
173            &shadowed,
174            cfg.capabilities,
175            &cfg.help_before,
176            &cfg.help_after,
177        );
178        return (0, true);
179    }
180    // 模式派发:xyz.<词> 恒命中;裸词仅未被遮蔽时命中(§13.2 第 5 步)。
181    let kind = match_mode(&args[0], &words, &shadowed);
182    if kind != ModeKind::None {
183        let rest = &args[1..];
184        if kind == ModeKind::Help {
185            return (run_help(reg, rest, &words, &shadowed, cfg), true);
186        }
187        // serve/http/mcp 的 -h/--help:打印模式帮助而非起服务。
188        if has_help_flag(rest) {
189            return (print_mode_help(kind, &words), true);
190        }
191        // 优雅关停:信号取消的 ctx 贯穿 CLI/HTTP/MCP,长任务可在退出前排空。
192        let ctx = Ctx::new();
193        spawn_signal_watcher(ctx.clone());
194        crate::logx::debugf(format_args!(
195            "dispatch: mode word='{}' addr={} tokens={} timeout={:?} cors={}",
196            args[0],
197            cfg.addr,
198            cfg.bearer_tokens.len(),
199            cfg.timeout,
200            cfg.cors_origins.len()
201        ));
202        match kind {
203            ModeKind::Serve => {
204                if cfg.capabilities.no_http {
205                    crate::logx::warnf(format_args!(
206                        "{}",
207                        crate::lang::tf("warn.mode_disabled", &[&words.serve, "HTTP"])
208                    ));
209                    return (1, true);
210                }
211                // serve:REST + /openapi.json + 挂 /mcp(is_mcp 依 no_mcp 裁剪)。
212                let mount = !cfg.capabilities.no_mcp;
213                return (run_serve(&ctx, reg, rest, cfg, mount), true);
214            }
215            ModeKind::Http => {
216                if cfg.capabilities.no_http {
217                    crate::logx::warnf(format_args!(
218                        "{}",
219                        crate::lang::tf("warn.mode_disabled", &[&words.http, "HTTP"])
220                    ));
221                    return (1, true);
222                }
223                // http:仅 REST + /openapi.json,不挂 /mcp(spec §13.1)。
224                return (run_serve(&ctx, reg, rest, cfg, false), true);
225            }
226            _ => {}
227        }
228        let mcp_word = words.mcp.clone();
229        if cfg.capabilities.no_mcp {
230            crate::logx::warnf(format_args!(
231                "{}",
232                crate::lang::tf("warn.mode_disabled", &[&mcp_word, "MCP"])
233            ));
234            return (1, true);
235        }
236        return (run_mcp(&ctx, reg, rest, cfg), true);
237    }
238    if cfg.capabilities.no_cli {
239        crate::logx::warnf(format_args!(
240            "{}",
241            crate::lang::tf("warn.no_cli", &[&words.mcp, &words.serve])
242        ));
243        return (1, true);
244    }
245    if composable && !args.is_empty() && !cli_known_top(reg, &args[0]) {
246        // 宿主兜底:静默交还,不做任何输出。
247        return (0, false);
248    }
249    let ctx = Ctx::new();
250    spawn_signal_watcher(ctx.clone());
251    (run_cli(&ctx, reg, &args, &cfg), true)
252}
253
254/// 解析后的四个模式词(spec §13.1)。
255pub struct Words {
256    pub(crate) serve: String,
257    pub(crate) http: String,
258    pub(crate) mcp: String,
259    pub(crate) help: String,
260}
261
262impl Words {
263    // (pub(crate):overview 读取词面)
264    fn all(&self) -> [&str; 4] {
265        [
266            self.serve.as_str(),
267            self.http.as_str(),
268            self.mcp.as_str(),
269            self.help.as_str(),
270        ]
271    }
272}
273
274/// 命中的内建模式(spec §13.2 第 5 步)。
275#[derive(PartialEq, Eq, Clone, Copy)]
276pub(crate) enum ModeKind {
277    Serve,
278    Http,
279    Mcp,
280    Help,
281    None,
282}
283
284/// resolveModes 默认并校验模式词:必须是无前导横线的普通词且两两不同
285/// (四词:serve/http/mcp/help,spec §13.1)。
286fn resolve_modes(cfg: &Config) -> errors::Result<Words> {
287    let pick = |v: &str, dflt: &str| {
288        if v.is_empty() {
289            dflt.to_string()
290        } else {
291            v.to_string()
292        }
293    };
294    let w = Words {
295        serve: pick(&cfg.modes.serve, "serve"),
296        http: pick(&cfg.modes.http, "http"),
297        mcp: pick(&cfg.modes.mcp, "mcp"),
298        help: pick(&cfg.modes.help, "help"),
299    };
300    for word in w.all() {
301        if word.starts_with('-') || word.chars().any(|c| c == ' ' || c == '\t') {
302            return Err(errors::Error::new(
303                errors::Kind::Internal,
304                format!("xyz: invalid mode word {word:?} (no leading dash, no whitespace)"),
305            ));
306        }
307    }
308    let words = w.all();
309    for i in 0..words.len() {
310        for j in (i + 1)..words.len() {
311            if words[i] == words[j] {
312                return Err(errors::Error::new(
313                    errors::Kind::Internal,
314                    format!(
315                        "xyz: mode words must be pairwise distinct (serve={:?} http={:?} mcp={:?} help={:?})",
316                        w.serve, w.http, w.mcp, w.help
317                    ),
318                ));
319            }
320        }
321    }
322    Ok(w)
323}
324
325/// 计算哪些模式词被用户命令的顶层段遮蔽(spec §13.1):遮蔽时裸词让位给
326/// 用户命令,内建模式仅经 `xyz.<词>` 可达;CLI-Skip 的命令不参与遮蔽。
327fn shadowed_modes(reg: &Registry, w: &Words) -> std::collections::BTreeSet<String> {
328    let mut tops = std::collections::BTreeSet::new();
329    for name in reg.names() {
330        if let Some(e) = reg.get(&name)
331            && e.cli.skip
332        {
333            continue;
334        }
335        if let Some(top) = name.split('.').next() {
336            tops.insert(top.to_string());
337        }
338    }
339    w.all()
340        .iter()
341        .filter(|word| tops.contains(**word))
342        .map(|word| word.to_string())
343        .collect()
344}
345
346/// 首 token → 内建模式:`xyz.<词>` 恒命中;裸词仅在未被遮蔽时命中。
347fn match_mode(token: &str, w: &Words, shadowed: &std::collections::BTreeSet<String>) -> ModeKind {
348    if let Some(rest) = token.strip_prefix("xyz.") {
349        return match rest {
350            r if r == w.serve => ModeKind::Serve,
351            r if r == w.http => ModeKind::Http,
352            r if r == w.mcp => ModeKind::Mcp,
353            r if r == w.help => ModeKind::Help,
354            _ => ModeKind::None,
355        };
356    }
357    if shadowed.contains(token) {
358        return ModeKind::None;
359    }
360    match token {
361        t if t == w.serve => ModeKind::Serve,
362        t if t == w.http => ModeKind::Http,
363        t if t == w.mcp => ModeKind::Mcp,
364        t if t == w.help => ModeKind::Help,
365        _ => ModeKind::None,
366    }
367}
368
369/// args 中("--" 之前)是否出现 -h/--help。
370fn has_help_flag(args: &[String]) -> bool {
371    for a in args {
372        if a == "--" {
373            break;
374        }
375        if a == "-h" || a == "--help" {
376            return true;
377        }
378    }
379    false
380}
381
382/// `help` 模式(spec §10.4/§13.2):裸 help → 总览;help <模式> → 模式帮助;
383/// help <命令路径> → 该命令详细帮助(点分或空格分隔皆可,等同 `<path> -h`)。
384fn run_help(
385    reg: &Registry,
386    rest: &[String],
387    w: &Words,
388    shadowed: &std::collections::BTreeSet<String>,
389    cfg: Config,
390) -> i32 {
391    if rest.is_empty() {
392        let mut stdout = std::io::stdout();
393        let _ = crate::overview::print_overview(
394            &mut stdout,
395            reg,
396            w,
397            shadowed,
398            cfg.capabilities,
399            &cfg.help_before,
400            &cfg.help_after,
401        );
402        return 0;
403    }
404    let kind = match_mode(&rest[0], w, shadowed);
405    match kind {
406        ModeKind::Help | ModeKind::None if kind == ModeKind::Help => {
407            let mut stdout = std::io::stdout();
408            let _ = crate::overview::print_overview(
409                &mut stdout,
410                reg,
411                w,
412                shadowed,
413                cfg.capabilities,
414                &cfg.help_before,
415                &cfg.help_after,
416            );
417            0
418        }
419        ModeKind::Serve | ModeKind::Http | ModeKind::Mcp => print_mode_help(kind, w),
420        _ => {
421            // 命令路径:token 内的点再拆(help user.add == help user add)。
422            let mut path: Vec<String> = Vec::new();
423            for r in rest {
424                path.extend(r.split('.').map(|seg| seg.to_string()));
425            }
426            if cfg.capabilities.no_cli {
427                crate::logx::warnf(format_args!(
428                    "{}",
429                    crate::lang::tf("warn.no_cli", &[&w.mcp, &w.serve])
430                ));
431                return 1;
432            }
433            path.push("-h".to_string());
434            run_cli(&Ctx::new(), reg, &path, &cfg)
435        }
436    }
437}
438
439/// 某个模式的帮助文本(spec §10.4/§13.2;纯函数便于测试)。
440pub(crate) fn mode_help_text(kind: ModeKind, w: &Words) -> String {
441    let key = match kind {
442        ModeKind::Serve => "mode_help.serve",
443        ModeKind::Http => "mode_help.http",
444        ModeKind::Mcp => "mode_help.mcp",
445        _ => "mode_help.help",
446    };
447    let word = match kind {
448        ModeKind::Serve => &w.serve,
449        ModeKind::Http => &w.http,
450        ModeKind::Mcp => &w.mcp,
451        _ => &w.help,
452    };
453    crate::lang::tf(key, &[word])
454}
455
456/// 打印某个模式的帮助(spec §10.4/§13.2),不起服务。
457fn print_mode_help(kind: ModeKind, w: &Words) -> i32 {
458    // 稳健写入:早关管道静默(同 -v)。
459    let _ = writeln!(std::io::stdout(), "{}", mode_help_text(kind, w));
460    0
461}
462
463/// 信号接线:可用 tokio 时走 tokio::signal;纯 CLI 构建走 ctrlc。
464fn spawn_signal_watcher(ctx: Ctx) {
465    // 第三态(http-stack 与 cli 都裁掉):函数体为空,引用参数抑警告。
466    #[cfg(not(any(feature = "http-stack", feature = "cli")))]
467    let _ = &ctx;
468
469    #[cfg(feature = "http-stack")]
470    {
471        std::thread::spawn(move || {
472            let rt = match tokio::runtime::Builder::new_current_thread()
473                .enable_all()
474                .build()
475            {
476                Ok(rt) => rt,
477                Err(_) => return,
478            };
479            rt.block_on(async move {
480                let _ = tokio::signal::ctrl_c().await;
481                ctx.cancel();
482            });
483        });
484    }
485    #[cfg(all(not(feature = "http-stack"), feature = "cli"))]
486    {
487        let _ = ctrlc::set_handler(move || {
488            ctx.cancel();
489        });
490    }
491}
492
493// ---- 通道运行时路径(feature 裁剪;stub 与 Go 的 build-tag 口袋对齐)----
494
495#[cfg(feature = "cli")]
496fn run_cli(ctx: &Ctx, reg: &Registry, args: &[String], cfg: &Config) -> i32 {
497    // --xyz.format 的默认格式经 Options 注入 CLI 前端(spec §10.7)。
498    crate::cli::run_context(
499        ctx,
500        reg,
501        args,
502        crate::cli::Options {
503            format: Some(cfg.format.clone()),
504            format_from_flag: cfg.format_from_flag,
505            format_interactive: Some(cfg.format_interactive.clone()),
506            format_piped: Some(cfg.format_piped.clone()),
507            ..Default::default()
508        },
509    )
510}
511
512#[cfg(not(feature = "cli"))]
513fn run_cli(_ctx: &Ctx, _reg: &Registry, _args: &[String], _cfg: &Config) -> i32 {
514    eprintln!("xyz: {}", crate::lang::tf("stub.not_compiled", &["CLI"]));
515    1
516}
517
518#[cfg(feature = "http")]
519fn run_serve(ctx: &Ctx, reg: &Registry, args: &[String], cfg: Config, mount_mcp: bool) -> i32 {
520    crate::httpapi::serve(ctx, reg, args, cfg, mount_mcp)
521}
522
523#[cfg(not(feature = "http"))]
524fn run_serve(_ctx: &Ctx, _reg: &Registry, _args: &[String], _cfg: Config, _mount_mcp: bool) -> i32 {
525    eprintln!("xyz: {}", crate::lang::tf("stub.not_compiled", &["HTTP"]));
526    1
527}
528
529#[cfg(feature = "mcp")]
530fn run_mcp(ctx: &Ctx, reg: &Registry, args: &[String], cfg: Config) -> i32 {
531    crate::mcp::run_with_config(ctx, reg, args, cfg)
532}
533
534#[cfg(not(feature = "mcp"))]
535fn run_mcp(_ctx: &Ctx, _reg: &Registry, _args: &[String], _cfg: Config) -> i32 {
536    eprintln!("xyz: {}", crate::lang::tf("stub.not_compiled", &["MCP"]));
537    1
538}