命令行宿主
本页讲命令行宿主怎样把命令行参数翻译成内核调用:内置命令、操作调用的载荷与上下文选项、输出与退出码、自定义命令,以及经 stdio 提供 MCP,适用于运维工具、批处理脚本和要给 MCP 客户端当子进程的应用。完整 API 见 Hosting.CommandLine 包。
宿主做什么
Hosting.CommandLine 是 Ring 4 的宿主,跑在 .NET 通用宿主(Host.CreateApplicationBuilder)里,一次进程执行一条命令。同一份操作契约在命令行上是一条命令:invoke greetings.create --name Ada 或简写 greetings create --name Ada,调用与 HTTP 一样经过完整的内核管道,Authenticate 阶段解释凭据,Authorize 阶段按契约判定。stdout 只承载命令输出,日志与失败信息只写 stderr,脚本可以直接把 stdout 交给 jq 一类的工具。
启用
- 用控制台项目,引用
Microsoft.Extensions.Hosting与XiHan.Framework.Hosting.CommandLine;要暴露操作时再引用Kernel.Generators(以分析器方式)生成操作登记代码。 AddXiHan(...)装配特性,builder.Services.AddXiHanCommandLine()登记内置命令。- 以
RunXiHanCommandLineAsync(args)运行,返回值就是进程退出码。
samples/SimpleCli 的 Program.cs:
using Microsoft.Extensions.Hosting;
using SimpleCli;
using XiHan.Framework.Hosting.CommandLine;
using XiHan.Framework.Kernel.Hosting;
using XiHan.Framework.Localization.Abstractions;
using XiHan.Framework.Localization.Abstractions.Messages;
using XiHan.Framework.Profiles.Minimal;
using XiHan.Framework.Protocols.Mcp;
var builder = Host.CreateApplicationBuilder(args);
// 同一份操作契约既是命令(greetings create --name Ada),也是 MCP tool(mcp 命令的 stdio 传输)
builder.AddXiHan(xihan => xihan
.UseMinimalProfile()
.UseFeature<McpProtocolFeature>()
.UseFeature<GreetingsFeature>());
builder.Services.AddMessageCatalog(MessageCatalog.FromEmbeddedResources("samples", typeof(CliMessages)));
builder.Services.AddXiHanCommandLine();
return await builder.Build().RunXiHanCommandLineAsync(args);GreetingsFeature 登记了一个操作 greetings.create,它的 [Operation] 写了 Expose = Expose.CommandLine | Expose.Mcp。HostApplicationBuilder 上的 AddXiHan 与 ASP.NET Core 宿主一样,先装载 XIHAN_CONFIG_PATH 目录里的 JSON 文件(可重载,紧接 appsettings.{环境}.json 插入),见宿主与 Web 应用。AddXiHanCommandLine() 清掉通用宿主的默认日志提供器,只留写 stderr 的控制台日志。RunXiHanCommandLineAsync 先启动宿主再执行命令,命令结束后按顺序停止宿主;Ctrl+C 取消当前命令(退出码 8),宿主仍正常停止。
内置命令
| 命令 | 输出 |
|---|---|
help | 用法与命令列表 |
graph | 能力图谱(App.DescribeGraph()) |
pipeline | 管道阶段与中间件(App.DescribePipeline()) |
operations | 操作登记表,每行 ID<TAB>稳定性<TAB>暴露的协议<TAB>permission=…、policy=… 或 - |
invoke <操作 ID> | 调用操作,见下一节 |
mcp | Protocols.Mcp 注册:MCP 的 stdio 传输,接受与 invoke 相同的上下文选项 |
没有命令时(包括只写了 --help)把用法写到 stderr 并返回 64;写了命令再带 --help 时把用法写到 stdout 并返回 0。
dotnet run --project framework/samples/SimpleCli -- operationsgreetings.create Experimental Mcp, CommandLine -operations 列出登记表里的全部操作,包括没有暴露给命令行的,用来核对各协议上的暴露情况。
调用操作
invoke <操作 ID> 只接受 Expose 含 CommandLine、稳定性不是 Stability.Internal 的操作,其他操作得到 HOSTING_NOTFOUND_001、退出码 3。命令名不是已登记的命令、且与下一个参数能拼成合法操作 ID 时,<resource> <verb> 等价于 invoke <resource>.<verb>。
载荷是一个 JSON 对象,按下面的优先顺序取第一个:
| 来源 | 写法 | 说明 |
|---|---|---|
--json | --json '{"name":"Ada","times":2}' | 必须是 JSON 对象 |
--data | --data request.json 或 --data @request.json | 文件内容必须是 JSON 对象 |
| 字段选项 | --name Ada --times 2 | 其余非保留选项拼成对象:值按操作的请求 Schema 写成对应类型,重复的选项成数组,没有字段时为 {} |
保留选项不进载荷:json、data、authorization、tenant、correlation、locale、timeout、help。
字段选项的值按操作的请求 JSON Schema 转换,所以 --times 2 进载荷是 "times":2:
| Schema 声明 | 写成 |
|---|---|
string,或没有约束类型 | 字符串,--name 42 仍是 "name":"42" |
boolean | true 或 false,不跟值的 --flag 是 true |
integer、number | JSON 数字 |
array | 数组,元素按 items 转换,单个值也是一元数组 |
对象字段不从文本解析,要传嵌套对象就用 --json 或 --data,这两种载荷都原样进入、不做转换。转换不了的值(如 --times abc)不是用法错误:它留成字符串进管道,由 Validate 阶段报 KERNEL_VALIDATION_003,退出码 2。
操作没有 Schema(手写 AddOperation 时没传 OperationSchema)时值都写字符串。这时请求类型要从字符串读数字,序列化上下文需要 NumberHandling = JsonNumberHandling.AllowReadingFromString。
参数的解析规则:第一个不是选项的记号是命令名,其余是位置参数;--name value 与 --name=value 等价;后面不跟值的 --flag 记为 "true";-- 之后全是位置参数。标志后面要接位置参数时写 --flag=true,或把标志放到最后。
dotnet run --project framework/samples/SimpleCli -- greetings create --name Ada --times 2{"message":"Hello, Ada! Hello, Ada!","traceId":"4bf92f3577b34da6a3ce929d0e0e4736"}调用上下文
| 选项 | 调用上下文 | 说明 |
|---|---|---|
--tenant <id> | Baggage xihan.tenant | |
--correlation <id> | Baggage xihan.correlation | |
--locale <区域> | Baggage xihan.locale | 规范化后写入,失败文案据此翻译;格式不合法时忽略 |
--timeout <秒> | 截止时间 | 正数秒,可带小数,按宿主时钟换算成绝对时间;不是正数时为用法错误 |
--authorization '<方案> <值>' | 凭据 | 没有选项时取环境变量 XIHAN_AUTHORIZATION;原样放入,由 Authenticate 阶段解释;格式不是「方案 空格 值」时为用法错误 |
每次调用生成新的 trace-id,调用来源记协议 cli 与操作 ID,没有远端地址。凭据放进环境变量可以不出现在进程列表与 shell 历史里:
export XIHAN_AUTHORIZATION="ApiKey dev-alice-key"
app orders create --name widget --amount 3 --tenant acme --locale zh-CN --timeout 30这里的 app 指应用发布出的可执行文件。
输出与退出码
成功时把响应 JSON 写一行到 stdout,退出码 0。失败时把一行 JSON 写到 stderr:{"code":"<错误码>","detail":"<文案>","traceId":"<trace-id>"},detail 按 --locale 或默认区域翻译;还没有建出调用上下文的失败(用法错误、操作未暴露)没有 traceId。
dotnet run --project framework/samples/SimpleCli -- invoke greetings.create --json '{"name":"Ada","times":99}'; echo $?{"code":"SAMPLES_VALIDATION_001","detail":"The number of greetings must be between 1 and 10, but 99 was given.","traceId":"0af7651916cd43dd8448eb211c80319c"}
2退出码按错误码的 KIND 段决定(CommandLineExitCodes.For):
| KIND / 情形 | 退出码 | 常量 |
|---|---|---|
| 成功 | 0 | Success |
VALIDATION | 2 | Validation |
NOTFOUND | 3 | NotFound |
DENIED | 4 | Denied |
CONFLICT | 5 | Conflict |
UNAVAILABLE | 6 | Unavailable |
TIMEOUT | 7 | Timeout |
CANCELLED、Ctrl+C | 8 | Cancelled |
其他(含 UNEXPECTED) | 1 | Unexpected |
| 用法错误 | 64 | Usage |
用法错误包括:没有命令、未知命令(HOSTING_NOTFOUND_002)、invoke 缺操作 ID、--json 不是 JSON 对象、--data 文件不存在、--timeout 或 --authorization 格式不对(都是 HOSTING_VALIDATION_001)。认证失败与缺少权限都是 DENIED,退出码 4,不区分 HTTP 上的 401 与 403。
自定义命令
实现 ICommand(命令名、说明的消息键、执行),用 AddXiHanCommand<TCommand>() 登记;help 按名字排序列出全部命令,说明用 DescriptionKey 按当前区域取文案。
using XiHan.Framework.Hosting.CommandLine;
using XiHan.Framework.Hosting.CommandLine.Commands;
using XiHan.Framework.Primitives;
public sealed class VersionCommand : ICommand
{
public string Name => "version";
public string DescriptionKey => ToolMessages.VersionDescription;
public async ValueTask<int> ExecuteAsync(CommandContext context)
{
// 命令输出只写 context.Output(stdout),诊断信息写 context.Error(stderr)
await context.Output.WriteLineAsync(FrameworkInfo.Version);
return CommandLineExitCodes.Success;
}
}
public static class ToolMessages
{
// 文案放在应用消息目录的 Localization/<区域>.json 里
public const string VersionDescription = "tool.version.description";
}builder.Services.AddXiHanCommand<VersionCommand>();CommandContext 提供解析结果(CommandLine、Arguments、Option(name)、HasOption(name))、作用域服务(Services)、输出流与取消令牌。命令要调用操作时,用 CommandLineInvocation.Create(context, operationId) 按同一套选项建调用上下文(一条命令里发起多次独立调用时用 Create(context, services, operationId),每次在自己的服务作用域里建,mcp 命令就是这样为每个 tools/call 建上下文),失败用 CommandLineFailure.WriteAsync 写 stderr 并得到退出码,与 invoke 一致。命令名重复时 CommandLineRunner 构造失败(ArgumentException)。
MCP stdio
装配了 McpProtocolFeature 时,mcp 命令以 stdio 提供 MCP 服务端:stdin 与 stdout 每行一条 JSON-RPC 消息,日志只写 stderr,stdin 关闭即结束会话。stdin 按 UTF-8 解码,与控制台代码页无关;答复只含 ASCII(非 ASCII 字符按 JSON 转义)。MCP 客户端以子进程方式启动应用:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | dotnet run --project framework/samples/SimpleCli -- mcp答复里的 tool greetings_create 带消息目录里的本地化描述。mcp 命令接受与 invoke 相同的上下文选项,作用于整个会话里的每个 tools/call:凭据 --authorization '<scheme> <value>' 或环境变量 XIHAN_AUTHORIZATION(选项优先,MCP 规范要求 stdio 服务端从运行环境取凭据),--tenant、--correlation、--locale(tools/list 的标题与描述、失败文案按它翻译),--timeout <秒> 是每个 tools/call 的时限。每个 tools/call 经完整管道,凭据在这次调用的 Authenticate 阶段认证;stdio 没有外层认证,凭据无效时需要认证的 tool 得到 isError(SECURITY_DENIED_003),会话继续。选项不合法(凭据不是 <scheme> <value>、时限不是正数)时不进入会话,stderr 写一行用法错误,退出码 64。
XIHAN_AUTHORIZATION="ApiKey dev-alice-key" dotnet run --project framework/samples/SimpleCli -- mcp --tenant acme --locale zh-CNtool 的规则见 gRPC、MCP 与 CloudEvents。
相关
- Hosting.CommandLine 包、Protocols.Mcp 包
- 操作契约:
Expose.CommandLine - 宿主与 Web 应用:同一份契约的 HTTP 宿主
- 结果与错误:错误码与 KIND
- 规范:
spec/protocol/commandline.md《命令行载体》、spec/conventions/operation-ids.md《操作 ID》、RFC-0011《操作契约与 XGI》
